Чи існує спосіб запобігти sed інтерпретувати рядок заміни? [зачинено]


16

Якщо ви хочете замінити ключове слово рядком за допомогою sed, sed намагається інтерпретувати ваш рядок заміни. Якщо у рядку заміни є символи, які sed вважає спеціальними, наприклад символом '/', він не вийде, якщо, звичайно, ви не мали на увазі, що у вашій рядку заміни є символи, які підказують sed, як діяти.

Наприклад:

VAR="hi/"

sed "s/KEYWORD/$VAR/g" somefile

Чи є спосіб сказати sed не намагатися інтерпретувати рядок заміни для спеціальних символів? Все, що я хочу, - це можливість замінити ключове слово у файлі вмістом змінної, незалежно від того, який це вміст.


Якщо ви хочете вставити спеціальні символи, sedщоб вони не були особливими, просто ухиліться від них. VAR='hi\/'не дає такої проблеми.
Wildcard

6
Чому всі потоки? Мені здається цілком розумним питанням
roaima

sed(1)просто інтерпретує те, що отримує. У вашому випадку це відбувається через інтерполяцію оболонки. Я вважаю, що ти не можеш робити так, як хочеш, але перевіриш посібник. Я знаю, що в Perl (який робить прохідну sedзаміну, з набагато багатшими регулярними виразами) ви можете вказати, що рядок слід сприймати буквально, знову ж таки, перевірте інструкцію.
фонбранд

Відповіді:


5

Ви можете використовувати Perl замість sed з -p(припустимо, цикл над входом) та -e(надати програму в командному рядку). За допомогою Perl ви можете отримати доступ до змінних оточення, не інтерполюючи їх у оболонку. Зауважте, що змінну потрібно експортувати :

export VAR='hi/'
perl -p -e 's/KEYWORD/$ENV{VAR}/g' somefile

Якщо ви не хочете експортувати змінну скрізь, просто надайте її лише для цього процесу:

PATTERN="$VAR" perl -p -e 's/KEYWORD/$ENV{PATTERN}/g' somefile

Зауважте, що синтаксис регулярного виразу Perl за замовчуванням дещо відрізняється від sed.


Це здавалося дуже перспективним, але під час його тестування я отримую помилку "Аргумент надто довгий", оскільки мій рядок заміни занадто довгий, що має сенс - використовуючи цей метод, ми використовуємо весь рядок заміни як частина аргументів, які ми наводимо perl, тому існує обмеження на тривалість.
Тал

1
Ні, це буде йти в PATTERN змінній середовищі , а не в аргументах. У будь-якому випадку ця помилка була б E2BIG, яку ви однаково отримаєте, якби використали sed.
Антті Хаапала

4

Є тільки 4 спеціальних символів у змінному частини: \, &, переклад рядка і роздільник ( посилання )

$ VAR='abc/def&ghi\foo
next line'

$ repl=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$VAR")

$ echo "$repl"
abc\/def\&ghi\\foo\
next line

$ echo ZYX | sed "s/Y/$repl/g"
Zabc/def&ghi\foo
next lineX

Ця проблема має таку ж проблему, що і рішення Antti. Якщо рядок заміни минув певну довжину, ви отримаєте помилку "Аргумент задовгий". Крім того, що робити, якщо рядок заміни містить символи "[','] ',' * ','. 'Та інші подібні символи? Чи справді sed не інтерпретував би це?
Тал

Заміна сторона s///є НЕ регулярним виразом, це дійсно просто рядок (для зворотних косих рис пагонів і , крім &). Якщо рядок заміни настільки довгий, однолінійна оболонка оболонки - не ваше рішення.
glenn jackman

Дуже корисний список, якщо, наприклад, вашим рядком заміни є закодований текст base64 (наприклад, заміна заповнювача на ключ SHA256). Тоді хвилюватися просто роздільником.
Хіт

2

Найпростішим рішенням, яке б все-таки правильно обробляло переважну більшість значень змінних, було б використання символу, що не друкується, як роздільник sedкоманди команди-замінника.

У viви можете уникнути будь-якого символу управління, набравши Ctrl-V (частіше записуються в вигляді ^V). Отже, якщо ви використовуєте деякий керуючий символ (я часто використовую ^Aяк роздільник у цих випадках), ваша sedкоманда порушиться лише в тому випадку, якщо цей недрукований символ присутній у змінній, в яку ви потрапляєте.

Отже, ви введете "s^V^AKEYWORD^V^A$VAR^V^Ag"та як би ви viвиглядали:

sed "s^AKEYWORD^A$VAR^Ag" somefile

Це буде працювати до тих пір, $VARпоки не буде містити ^Aнедрукувальний символ - що надзвичайно малоймовірно.


Звичайно, якщо ви передаєте користувачеві вклад у значення $VAR, тоді всі ставки відключені, і вам краще ретельно опрацювати свої дані, а не покладатися на контрольні символи, які важко набрати середньому користувачеві.


Хоча насправді варто більше остерігатися, ніж розділовий рядок. Наприклад, &якщо він присутній у рядку заміни, означає "весь текст, який був зіставлений". Наприклад, s/stu../my&/замінили б "речі" на "mystuff", "stung" на "mystung" і т. Д. Тож якщо у вас може бути якийсь символ змінної, яку ви впускаєте як рядок заміни, але ви хочете використовувати буквальний значення лише змінної, тоді вам доведеться виконати деякі дезінфекції даних, перш ніж ви зможете використовувати змінну в якості рядка заміни sed. (Хоча санітарна обробка даних також може бути виконана sed.)


Ось такий мій погляд - заміна рядка іншою рядком - це дуже проста операція. Чи справді це має бути настільки складно, як з'ясувати, які символи sed не сподобаються, і використовувати sed для санітарії власного вкладу? Це звучить безглуздо і без зайвих сумнівів. Я не професійний програміст, але я впевнений, що можу кодувати невелику функцію, яка замінює ключове слово рядком майже будь-якою мовою, на яку я коли-небудь стикався, включаючи bash - я просто сподівався на простий Linux рішення з використанням існуючих інструментів - я не можу повірити, що там немає жодного.
Тал

1
@Tal, якщо ваш рядок заміни становить "100 сторінок сторінок", як ви згадуєте в іншому коментарі ... навряд чи можна назвати це "простим" випадком використання. Відповідь тут - Perl, до речі, я просто не навчився Perl. Складність тут випливає з того, що ви хочете дозволити будь-який довільний введення як рядок заміни в регулярному виразі .
Wildcard

Існує чимало інших рішень, які ви могли використовувати, багато з них дуже прості. Наприклад, якщо ваша рядок заміни на насправді лінія на основі і не повинні бути вставлені в середині рядка, використовуйте sed«и iкомандної nsert. Але sedце не гарний інструмент для обробки величезної кількості тексту складними способами. Я опублікую ще одну відповідь, яка показує, як це зробити awk.
Wildcard

1

Ви можете використовувати a ,або a |замість цього, це візьме це як сепаратор, а технічно ви можете використовувати що завгодно

зі сторінки man

\cregexpc
           Match lines matching the regular expression regexp.  The  c  may
      be any character.

Як ви бачите, ви повинні почати з \ перед роздільником на початку, тоді ви можете використовувати його як роздільник.

з документації http://www.gnu.org/software/sed/manual/sed.html#The-_0022s_0022-Command :

The / characters may be uniformly replaced by any other single character 
within any given s command.

The / character (or whatever other character is used in its stead) can appear in 
the regexp or replacement only if it is preceded by a \ character.

Приклад:

sed -e 'somevar|s|foo|bar|'
echo "Hello all" | sed "s_all_user_"
echo "Hello all" | sed "s,all,user,"

echo "Hello/ World" | sed "s,Hello/,Neo,"


Ви говорите про те, щоб дозволити використання одного, конкретного символу в рядку заміни - у цьому випадку "/". Я говорю про те, щоб запобігти спробі взагалі інтерпретувати рядок заміни. Незалежно від того, який символ ви використовуєте ("/", ",", "|" тощо), ви завжди ризикуєте, що цей символ з'явиться в рядку заміни. Крім того, початковий персонаж - не єдиний особливий персонаж, який турбує sed, чи не так?
Тал

@Tal ні, він може взяти що-небудь замість цього, /і він буде ігнорувати /щасливо, як я щойно зазначив .. насправді, ви навіть можете шукати його і замінювати його в рядку >>> я відредагував прикладом >>> ці речі не такі безпечні, і ви завжди знайдете розумнішого чувака
user3566929

@Та чому ви хочете не допустити його тлумачення? я маю на увазі, що це використання sedв першу чергу, який ваш проект?
користувач3566929

Все, що мені потрібно - це замінити ключове слово рядком. sed, здається, є найбільш поширеним способом, на сьогоднішній день, зробити це в Linux. Рядок може бути довжиною 100 сторінок. Я не хочу намагатися оздоровити рядок, щоб sed не вигадував під час її читання - я хочу, щоб він міг обробляти будь-які символи в рядку, і під «ручкою» я маю на увазі не намагатися знайти магічну значення всередині.
Тал

1
@Tal, bashце НЕ для роботи зі рядками. Взагалі, взагалі. Він призначений для обробки файлів та координації команд . У деяких випадках є зручний функціонал для струн, але насправді обмежений і зовсім не дуже швидкий, якщо це головне, що ви робите. Див. "Чому використання циклу оболонки для обробки тексту вважається поганою практикою?" Деякі інструменти, які призначені для обробки тексту, в порядку від найпростіших до самих потужних: sed, awkі Perl.
Wildcard

1

Якщо на основі рядка є лише один рядок, який потрібно замінити, я рекомендую попередньо попереджати сам файл із лінією заміни, використовуючи printf, зберігаючи цей перший рядок у sedпросторі утримування і закидаючи його в міру необхідності. Таким чином вам зовсім не доведеться турбуватися про особливі символи. (Єдине припущення тут полягає в тому, що $VARмістить один рядок тексту без будь-яких нових рядків. Це те, про що ви вже говорили в коментарях.) Окрім нових рядків, VAR може містити все, що завгодно, і це може працювати незалежно.

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/KEYWORD/g'

printf '%s\n'буде надрукувати вміст у $VARпрямому рядку, незалежно від його вмісту, а потім новий рядок. ( echoробитиме інші речі в деяких випадках, наприклад, якщо вміст $VARпочинається з дефісу - він буде інтерпретуватися як прапор опції, який передається echo.)

Дужки використовуються для додавання виводу printfдо вмісту somefile, який він передає sed. Тут важливим є пробіл, що відокремлює фігурні дужки, як і крапка з комою перед закриттям фігурної дужки.

1{h;d;};як sedкоманда буде зберігати перший рядок тексту в sed«S трюму , потім dдаліть лінію (а не друк).

/KEYWORD/застосовує наступні дії до всіх рядків, які містять KEYWORD. Дія get, яка отримує вміст простору утримування і скидає його замість простору шаблону - іншими словами, весь поточний рядок. (Це не для заміни лише частини рядка.) Простір утримування не спорожняється, до речі, просто скопійовано у простір шаблону, замінивши все, що там є.

Якщо ви хочете закріпити ваш регулярний вираз, щоб він не збігався з рядком, який містить лише KEYWORD, а лише рядок, у якому на лінії немає нічого іншого, крім KEYWORD, додайте початок якірного рядка ( ^) і кінець прив’язки рядка $до) ваш регекс:

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/^KEYWORD$/g'

Здається чудово, якщо VAR - одна лінія. Я фактично згадував у коментарях, що VAR "може бути довжиною 100 сторінок", а не один рядок. Вибачте за непорозуміння.
Тал

0

Ви можете змінити відхилення від косої риски прямої косої риски в рядку заміни, використовуючи розширення параметра заміни шаблону Bash. Це трохи безладно, тому що передні косої частини також потрібно уникати для Баша.

$ var='a/b/c';var="${var//\//\\/}";echo 'this is a test' | sed "s/i/$var/g"

вихід

tha/b/cs a/b/cs a test

Ви можете розмістити розширення параметра безпосередньо у вашій команді sed:

$ var='a/b/c';echo 'this is a test' | sed "s/i/${var//\//\\/}/g"

але я думаю, що перша форма трохи читабельніша. І звичайно, якщо ви збираєтеся повторно використовувати ту саму схему заміни у кількох командах sed, є сенс просто перетворити один раз.

Іншим варіантом буде використання сценарію, написаного в awk, perl або Python, або програмою C, щоб зробити заміни замість sed.


Ось простий приклад в Python, який працює, якщо ключове слово, яке слід замінити, є повним рядком у вхідному файлі (не рахуючи нового рядка). Як бачите, це по суті той же алгоритм, що і ваш приклад Bash, але він читає вхідний файл ефективніше.

import sys

#Get the keyword and replacement texts from the command line
keyword, replacement = sys.argv[1:]
for line in sys.stdin:
    #Strip any trailing whitespace
    line = line.rstrip()
    if line == keyword:
        line = replacement
    print(line)

Це просто ще один спосіб очищення вхідних даних, а не великий у цьому, оскільки він обробляє лише один конкретний символ ('/'). Як зауважив Уайлдкард, слід остерігатися більше, ніж просто рядок, що розділяє.
Тал

Справедливий дзвінок. Наприклад, якщо текст заміни містить будь-які послідовності, що вийшли з косої риски, вони будуть інтерпретовані, що може бути небажаним. Одним із способів цього було б перетворення проблемних ознак (або всієї справи) в \xпослідовності втечі. Або використовувати програму, яка може обробляти довільне введення, як я вже згадував у своєму останньому абзаці.
PM 2Ring

@Tal: Я додам простий приклад Python до своєї відповіді.
PM 2Ring

Сценарій python працює чудово і, здається, виконує саме те, що робить моя функція, лише набагато ефективніше. На жаль, якщо основний скрипт є bash (як у моєму випадку), для цього потрібно використовувати вторинний зовнішній скрипт python.
Тал

-1

Ось так я пішов:

#Replaces a keyword with a long string
#
#This is normally done with sed, but sed
#tries to interpret the string you are
#replacing the keyword with too hard
#
#stdin - contents to look through
#Arg 1 - keyword to replace
#Arg 2 - what to replace keyword with
replace() {
        KEYWORD="$1"
        REPLACEMENT_STRING="$2"

        while IFS= read -r LINE
        do
                if [[ "$LINE" == "$KEYWORD" ]]
                then
                        printf "%s\n" "$REPLACEMENT_STRING"
                else
                        printf "%s\n" "$LINE"
                fi
        done < /dev/stdin
}

це чудово працює в моєму випадку, оскільки моє ключове слово знаходиться в рядку сама по собі. Якби ключове слово було в рядку з іншим текстом, це не працює.

Я все ще хотів би знати, чи існує простий спосіб зробити це, що не передбачає кодування мого власного рішення.


1
Якщо ви дійсно переживаєте за особливі символи та надійність, вам взагалі не слід користуватися echo. Використовуйте printfзамість цього. І обробка тексту в циклі оболонки - погана ідея.
Wildcard

1
Було б корисно, якби ви згадали у питанні, що ключове слово завжди буде повним рядком. FWIW, bash's readдосить повільний. Він призначений для обробки інтерактивного введення користувача, а не для обробки текстових файлів. Це повільно, оскільки він читає stdin char за допомогою char, роблячи системний виклик для кожного char.
PM 2Ring,

@PM 2Ring У моєму запитанні не було зазначено, що ключове слово є власним рядком, тому що я не хочу відповіді, яка працює лише в такій обмеженій кількості випадків - я хотів щось, що може легко працювати незалежно від того, де ключове слово був. Я також ніколи не казав, що мій код ефективний - якби він був, я б не шукав альтернативи ...
Тал

@Wildcard Якщо я чогось не пропускаю, printf абсолютно інтерпретує спеціальні символи, і це набагато більше, ніж "ехо" за замовчуванням. printf "hi\n"зробить printf друком нового рядка, друкуючи echo "hi\n"його як є.
Тал

@Tal, "f" у printfпозначає "формат" - перший аргумент до printf- це специфікатор формату . Якщо специфікатор %s\n, що означає «рядок з подальшим переведенням рядка», нічого в наступному аргументи не буде інтерпретуватися або переведені printf на всі . (Звичайно, оболонка все ще може її інтерпретувати; найкраще вставити все це в одиничні лапки, якщо це буквальна рядок, або подвійні лапки, якщо ви хочете змінне розширення.) Додаткову інформацію див. У моїй відповідіprintf .
Wildcard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.