Как использовать символ NUL в качестве разделителя в командах замены и удаления sed?

Вопрос или проблема

Вот что я пробовал, намереваясь заменить /path/to/a на /path/to/b, используя NUL в качестве разделителя:

$ cat pathsList| sed -r -e 's\0/path/to/a\0/path/to/b\0g'
sed: -e expression #1, char 27: number option to `s' command may not be zero

Почему я хотел использовать NUL: NUL и / — единственные символы, которые запрещены в ext4fs, а / уже используется как разделитель путей. Также я хочу избежать экранирования и деэкранирования данных только ради использования sed.

Если NUL невозможно использовать в качестве разделителя (скажем так), я согласен на любое решение, лучшее чем экранирование и деэкранирование моих данных.

$ sed --version
sed (GNU sed) 4.4

К сожалению, похоже, что невозможно использовать NUL в качестве разделителя для команды s/// в sed.

Если вы хотите создать строку с символом NUL, вы можете использовать форму $'...', которая распознается bash и другими оболочками, поэтому вы могли бы подумать, что это сработает:

sed -r -e $'s\0o\0x\0g'

Но способ передачи аргументов в Linux (и вообще Unix) делает невозможным передачу строк с встроенными NUL. Поскольку вы получаете только количество аргументов (argc) и массив строк (argv), NUL-терминированные строки (C-строки) — единственный возможный способ принять аргументы. Другими словами, все, что sed (или любая программа) увидит, если передан $'s\0o\0x\0g', это просто "s" (и NUL, который они должны рассматривать как конец строки.)

Я подумал, что, возможно, передача этого как внешнего файла в sed может сработать, так как в этом случае sed может знать, что NUL встроены и потенциально отследить полную строку по ее длине. Поэтому я попробовал это:

$ cat -v script.sed 
s^@o^@x^@g

^@ — это байты NUL. Я вставил их в vim, используя Ctrlv000 (три нуля), что является комбинацией клавиш vim для ввода символа по его ASCII значению.

Но это, похоже, тоже не работает:

$ echo "/path/to/a/folder" | sed -r -f script.sed 
sed: file script.sed line 1: delimiter character is not a single-byte character

Любопытно, что это отличается от случая, когда в файле скрипта только одиночный s, в этом случае sed жалуется на unterminated 's' command… Похоже, он следит за строкой по ее длине, но все еще не хочет использовать NUL в качестве символа-разделителя.

Просмотр исходного кода sed показал, что неясно, было ли это намерением или ошибкой. В функции is_mb_char(), которая пытается определить, является ли байт частью многобайтового символа, обработка NUL выглядит так:

case 0: /* Special case of mbrtowc(3): the NUL character */
  /* TODO: test this */
  return 1;

В данном случае, return 1 означает “да, это многобайтовый символ”, что не совсем так.

Комментарий несколькими строками выше гласит:

/*
 * Return zero in all other cases:
 *   CH is a valid single-byte character (e.g. 0x01-0x7F in UTF-8 locales);
 *   CH is an invalid byte in a multibyte sequence for the currentl locale,
 *   CH is the NUL byte.
 */

Так что, возможно, планировалось return 0?

Коммит, который ввел этот код, не имеет к контекста здесь…

Страница man для mbrtowc(3) упоминает L'\0', который я предполагаю, что это какой-то многобайтовый NUL, поэтому, возможно, именно поэтому они решили обработать это таким образом?

Я надеюсь, что эта информация все же полезна!

Хотя NUL нельзя найти в имени файла (по той же причине, почему его нельзя найти в аргументе команды), . (очень распространен), ^, *, [, $, \ могут встречаться и также должны быть экранированы, поскольку они являются операторами регулярных выражений, понимаемых sed командой s.

Вы можете всегда выполнить это экранирование автоматически.

Обратите внимание, что кроме NUL, символ новой строки и все многобайтовые символы также нельзя использовать в GNU sed. Другие реализации могут иметь другие ограничения. POSIX также запрещает обратную косую черту (хотя это работает для GNU sed), поэтому я рекомендую придерживаться графических символов, кроме обратной косой черты, из портативного набора символов.

GNU sed поддерживает опцию -z начиная с 2012 года.

Пример:

$ printf 'foo\0bar\0' | sed -z 's/$/!/' | tr '\0' '\n'
foo!
bar!

Но в большинстве случаев лучше использовать Perl.

$ printf '%s\n' path1 path2 | perl -pe 'BEGIN {($a, $b) = (shift, shift)} s($a){$b}g' 'path' $'some/fance/new/name\t'
some/fance/new/name     1
some/fance/new/name     2

Если вы хотите заменить одиночные символы (байты) на одиночные символы (байты), используйте tr:

$ echo "/path/to/a/folder" | tr ao xy
/pxth/ty/x/fylder

Для произвольных строк вы можете использовать Perl:

$ echo "/path/to/a/folder" | patt=o repl=xx perl -pe 's/$ENV{patt}/$ENV{repl}/g'
/path/txx/a/fxxlder

(Я передал patt и repl через окружение, так как perl -p предполагает использование аргументов командной строки как имена файлов для обработки.)

Здесь, конечно, patt принимается как регулярное выражение, со всеми вытекающими последствиями:

$ echo "/path/to/a/folder" | patt="a." repl=x perl -pe 's/$ENV{patt}/$ENV{repl}/g'
/pxh/to/xfolder

Поэтому вам нужно либо экранировать точки (\.) и другие специальные символы, либо использовать \Q$ENV{patt}:

$ echo "/path/to/a/folder.txt" | patt=. repl=, perl -pe 's/\Q$ENV{patt}/$ENV{repl}/g'
/path/to/a/folder,txt

В обоих случаях (аргументы командной строки и переменные окружения), интерфейс между ОС и утилитой передает строки как строки, заканчивающиеся на NUL, как это используется стандартной библиотекой C. Этот интерфейс делает невозможным внедрение буквальных байтов NUL в аргументы, и sed -e 's\a\x\g' заставляет sed использовать буквальную обратную косую в качестве разделителя для команды s.

Ответ от @cerving’ близок, но нет необходимости использовать tr.

cat pathsList| sed -z 's/\n/\x0/g'

-z для использования \x0 как разделителя. Это фактически превращает ваш файл в длинную строку (если pathsList уже не содержит \x0). Таким образом, ваш файл не должен быть слишком большим, чтобы поместиться в доступную память.

[...] | sed -e 's/ /😀/' | tr --squeeze-repeats '😀' '\0' | [...]

Это плохой хак, особенно потому, что tr видимо, некорректно обрабатывает unicode (он выводит четыре символа замены на одно эмодзи, поэтому используется параметр --squeeze-repeats). Но это может быть достаточно для некоторых случаев.
(Как часто в ваших именах файлов в системе встречаются эмодзи?)

echo 'Welcome to Unicode World' | sed -e 's/ /😀/g' | tr --squeeze-repeats '😀' '\0' | tr '\0' '\n'
Welcome
to
Unicode
World

Вы можете попробовать, работает ли это:

$ echo "/path/to/a/folder" | sed -r -e 's/\0o/\0x/g'

.

Ответ или решение

Использование NUL-символа (\0) в качестве разделителя в командах замены и удаления в sed привлекает внимание ввиду своей потенциальной полезности, ведь NUL является одним из немногих символов, не допускаемых в именах файлов в файловых системах ext4fs. Однако, как многие уже обнаружили, при попытке использовать NUL в таком качестве возникают существенные технические препятствия.

Теория

Начнем с разбора ключевых аспектов работы sed и ограничений, связанных с использованием символа NUL. Sed, будучи утилитой текстовой обработки, основанной на регулярных выражениях, позволяет гибко манипулировать текстовыми потоками, подстроками и строками. Однако один из недостатков использования sed заключается в невозможности управления строками, содержащими NUL-символы, поскольку строки в C – языке программирования, на котором основан sed, определяются как последовательность символов, заканчивающаяся NUL-символом. Это ограничение означает, что попытка передачи строки с встраиванием NUL-символов приведет к неожиданным результатам, поскольку этот символ будет воспринят как конец строки.

Пример

Предположим, что вы пытаетесь заменить путь /path/to/a на /path/to/b в файле, используя NUL-символ в качестве разделителя из-за его редкости и отсутствия в именах файлов. Ваша команда может выглядеть следующим образом:

$ cat pathsList | sed -r -e 's\0/path/to/a\0/path/to/b\0g'

Однако, как вы уже заметили, это приводит к ошибке:

sed: -e expression #1, char 27: number option to `s' command may not be zero

Эта ошибка возникает из-за невозможности sed корректно обработать строки, содержащие NUL в качестве символа-разделителя.

Применение и обходные пути

Так как NUL-символ не может быть использован в качестве разделителя, нам придется переключиться на другие стратегии.

  1. Использование уникальных заменителей: Один из наиболее простых обходных путей – это выбор другого уникального символа, который можно использовать в качестве разделителя и который не присутствует в ваших входных данных. Обычно это может быть символ, такой как # или @, которые затем будут заменены обратно на нужный символ.

    $ cat pathsList | sed -r 's#/path/to/a#/path/to/b#g'
  2. Применение GNU sed с опцией -z: В GNU sed можно использовать -z, чтобы читать данные по NUL-символам:

    $ printf '%s\0' $(cat pathsList) | sed -z 's|/path/to/a|/path/to/b|g'

    Это позволяет обойти ограничение на использование NUL в качестве разделителя, интерпретируя входные данные как одну длинную строку.

  3. Переход на Perl или другие языки обработки текста: Perl предлагает более гибкий подход к обработке строк с NUL-символами. Использование Perl позволяет обрабатывать сложные логики замены без необходимости беспокоиться о внутренних ограничениях, связанных с разделителями.

    $ perl -pe 'BEGIN{($a, $b)=(shift, shift)} s/\Q$a\E/$b/g' -- '/path/to/a' '/path/to/b' pathsList
  4. Использование tr для простых замен: Если ваша задача заключается в замене отдельных символов на другие, попутно можно использовать команду tr:

    $ echo "/path/to/a/folder" | tr 'a' 'b'

Вывод

Таким образом, хотя sed не может непосредственно использовать NUL-символ в качестве разделителя из-за своих внутренних ограничений, существует несколько способов обойти данную проблему, будь то путем выбора другого символа в качестве разделителя, перехода на более мощные инструменты, такие как Perl, или применения различных подходов к пред- и пост-обработке данных. Применение этих методов позволит вам эффективно и безопасно обрабатывать текстовые данные даже в сложных случаях.

Оцените материал
Добавить комментарий

Капча загружается...