используйте sed для замены текста только в кавычках - PullRequest
9 голосов
/ 25 ноября 2011

У меня есть этот тестовый файл.

[root@localhost ~]# cat f.txt 
"a aa"  MM  "bbb  b"
MM    MM
MM"b b "
[root@localhost ~]#

Я хочу заменить все пробелы в кавычках, обратите внимание, только в кавычках.Все символы из кавычек не должны быть затронуты.То есть я хочу что-то похожее на:

"a_aa"  MM  "bbb__b"
MM    MM
MM"b_b_"

Может ли это быть реализовано с использованием sed ?

Спасибо,

Ответы [ 4 ]

8 голосов
/ 25 ноября 2011

Это совершенно нетривиальный вопрос.

Это работает, заменяя первый пробел внутри кавычек подчеркиванием:

$ sed 's/\("[^ "]*\) \([^"]*"\)/\1_\2/g' f.txt
"a_aa"  MM  "bbb_ b"
MM    MM
MM"b_b "
$

В этом примере, где внутри кавычек не более двух пробелов, заманчиво просто повторить команду, но это дает неверный результат:

$ sed -e 's/\("[^ "]*\) \([^"]*"\)/\1_\2/g' \
>     -e 's/\("[^ "]*\) \([^"]*"\)/\1_\2/g' f.txt
"a_aa"_ MM  "bbb_ b"
MM    MM
MM"b_b_"
$

Если ваша версия sed поддерживает «расширенные регулярные выражения», то это работает для примеров данных:

$ sed -E \
>    -e 's/^(([^"]*("[^ "]*")?)*)("[^ "]*) ([^"]*")/\1\4_\5/' \
>    -e 's/^(([^"]*("[^ "]*")?)*)("[^ "]*) ([^"]*")/\1\4_\5/' \
>    -e 's/^(([^"]*("[^ "]*")?)*)("[^ "]*) ([^"]*")/\1\4_\5/' \
>    f.txt
"a_aa"  MM  "bbb__b"
MM    MM
MM"b_b_"
$

Вы должны повторить это ужасное регулярное выражение для каждого пробела в двойных кавычках - следовательно, три раза для первой строки данных.

Регулярное выражение можно объяснить так:

  • Начиная с начала строки,
  • Ищите последовательности «ноль или более не кавычек, за которыми может следовать кавычка, без пробелов или кавычек и кавычки», вся сборка повторяется ноль или более раз,
  • За ним следует кавычка, ноль или более не кавычек, пробелы, пробел и ноль или более не кавычек, а также кавычка.
  • Замените соответствующий материал на ведущую часть, материал в начале текущего цитируемого фрагмента, подчеркивание и завершающий материал текущего цитируемого фрагмента.

Из-за начального якоря это нужно повторять один раз для пробела ... но sed имеет циклическую конструкцию, поэтому мы можем сделать это с:

$ sed -E -e ':redo
>            s/^(([^"]*("[^ "]*")?)*)("[^ "]*) ([^"]*")/\1\4_\5/
>            t redo' f.txt
"a_aa"  MM  "bbb__b"
MM    MM
MM"b_b_"
$

:redo определяет метку; команда s/// такая же, как и раньше; команда t redo переходит на метку, если с момента последнего чтения строки или перехода к метке была произведена какая-либо подстановка.


Учитывая обсуждение в комментариях, стоит упомянуть пару моментов:

  1. Опция -E применяется к sed в MacOS X (проверено 10.7.2). Соответствующая опция для версии GNU sed - -r (или --regex-extended). Параметр -E соответствует grep -E (который также использует расширенные регулярные выражения). «Классические системы Unix» не поддерживают ERE с sed (Solaris 10, AIX 6, HP-UX 11).

  2. Вы можете заменить ?, который я использовал (это единственный символ, который вынуждает использовать ERE вместо BRE), на *, а затем иметь дело с круглыми скобками (которые требуют обратной косой черты перед из них в BRE, чтобы сделать их в скобках), оставив сценарий:

    sed -e ':redo
            s/^\(\([^"]*\("[^ "]*"\)*\)*\)\("[^ "]*\) \([^"]*"\)/\1\4_\5/g
            t redo' f.txt
    

    Это приводит к тому же выводу на том же входе - я попробовал несколько более сложные шаблоны на входе:

    "a aa"  MM  "bbb  b"
    MM    MM
    MM"b b "
    "c c""d d""e  e" X " f "" g "
     "C C" "D D" "E  E" x " F " " G "
    

    Это дает вывод:

    "a_aa"  MM  "bbb__b"
    MM    MM
    MM"b_b_"
    "c_c""d_d""e__e" X "_f_""_g_"
     "C_C" "D_D" "E__E" x "_F_" "_G_"
    
  3. Даже с нотацией BRE sed поддерживал нотацию \{0,1\} для указания 0 или 1 вхождений предыдущего термина RE, поэтому версию ? можно преобразовать в BRE с помощью:

    sed -e ':redo
            s/^\(\([^"]*\("[^ "]*"\)\{0,1\}\)*\)\("[^ "]*\) \([^"]*"\)/\1\4_\5/g
            t redo' f.txt
    

    Это дает тот же результат, что и другие альтернативы.

0 голосов
/ 25 ноября 2011

Это может сработать для вас:

 sed 's/^/\n/;:a;s/\(\n[^"]*"[^ "]*\) \([^"]*"\)\n*/\1_\2\n/;ta;s/\n//;ta;s/\n//' file

Объяснение:

Добавьте \n к началу строки, это будет использовано для подстановки вдоль подстановок.Замените один на _ в пределах " и, пока он там, поместите \n, готовый к следующему раунду замен.Заменив все , удалите \n и повторите.Когда все замены произошли, удалите разделитель \n.

или этот:

sed -r ':a;s/"/\n/;s/"/\n/;:b;s/(\n[^\n ]*) ([^\n]*\n)/\1_\2/g;tb;s/\n/%%%/g;ta;s/%%%/"/g' file

Объяснение:

Замените первый набор "" на \n s.Замените первый пробел между символами новой строки на _, повторите.Замените \n на уникальный разделитель (%%%), повторите с начала.Уберите в конце, заменив все %%% на ".

Третий способ:

sed 's/"[^"]*"/\n&\n/g;$!s/$/@@@/' file |
sed '/"/y/ /_/;1{h;d};H;${x;s/\n//g;s/@@@/\n/g;p};d'

Объяснение:

Окружите все выражения в кавычках ("...") с символами новой строки (\n).Вставьте разделитель конца строки @@@ во все строки, кроме последней.Результат передается второй команде sed.Переведите все в _ для строк с " в них.Храните каждую строку в трюме (HS).В конце файла переключитесь на HS и удалите все \n и замените разделители конца строки на \n

, наконец:

sed 's/\("[^"]*"\)/$(tr '"' ' '_'"'<<<'"'"'\1'"'"')/g;s/^/echo /' file | sh

или GNUsed:

sed 's/\("[^"]*"\)/$(tr '"' ' '_'"'<<<'"'"'\1'"'"')/g;s/^/echo /e' file

осталось для читателя для работы.

0 голосов
/ 25 ноября 2011

Это было бы действительно легко, если бы цитируемый текст был в отдельных строках. Таким образом, один из подходов состоит в том, чтобы разделить текст, чтобы у вас было это, выполнить простое преобразование, а затем перестроить строки.

Разделить текст легко, но нам нужно различать строки, которые были

  1. уже присутствует в файле
  2. добавлено нами

Для этого мы можем завершить каждую строку символом, указывающим, к какому классу она принадлежит. Я просто буду использовать 1 и 2, соответствующие непосредственно выше. В седе имеем:

sed -e 's/$/1/' -e 's/"[^"]*"/2\n&2\n/g'

Это производит:

2
"a aa"2
  MM  2
"bbb  b"2
1
MM    MM1
MM2
"b b "2
1

Это легко трансформировать, просто используйте

sed -e '/".*"/ s/ /_/g' 

1020 * дает *

2
"a_aa"2
  MM  2
"bbb__b"2
1
MM    MM1
MM2
"b_b_"2
1

Наконец, нам нужно собрать его обратно. Это на самом деле довольно ужасно в Sed, но выполнимо, используя пространство удержания:

sed -e '/1$/ {s/1$//;H;s/.*//;x;s/\n//g}' -e '/2$/ {s/2$//;H;d}'

(Это было бы намного понятнее, например, в awk.)

Соедините эти три шага вместе, и все готово.

0 голосов
/ 25 ноября 2011

Необычный ответ в XSLT 2.0:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="2.0">
    <xsl:output method="text"></xsl:output>
    <xsl:template name="init">
        <xsl:for-each select="tokenize(unparsed-text('f.txt'),'&#10;')">
            <xsl:for-each select="tokenize(.,'&quot;')">
                <xsl:value-of select="if (position() mod 2 = 0) 
                  then concat('&quot;',translate(.,' ','_'),'&quot;') else ."></xsl:value-of>
            </xsl:for-each>
            <xsl:text>&#10;</xsl:text>
        </xsl:for-each>
    </xsl:template>    
</xsl:stylesheet>

Чтобы проверить, просто получите saxon.jar в sourceforge и используйте следующую командную строку:

java -jar saxon9.jar -it:init regexp.xsl

Файл xsltвключите ссылку на файл f.txt, текстовый файл должен находиться в том же каталоге, что и файл xslt.Это можно легко изменить, указав параметр в таблице стилей.

Работает за один проход.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...