Вопрос или проблема
Вот что я пробовал, намереваясь заменить /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-символ не может быть использован в качестве разделителя, нам придется переключиться на другие стратегии.
-
Использование уникальных заменителей: Один из наиболее простых обходных путей – это выбор другого уникального символа, который можно использовать в качестве разделителя и который не присутствует в ваших входных данных. Обычно это может быть символ, такой как
#
или@
, которые затем будут заменены обратно на нужный символ.$ cat pathsList | sed -r 's#/path/to/a#/path/to/b#g'
-
Применение GNU sed с опцией
-z
: В GNU sed можно использовать-z
, чтобы читать данные по NUL-символам:$ printf '%s\0' $(cat pathsList) | sed -z 's|/path/to/a|/path/to/b|g'
Это позволяет обойти ограничение на использование NUL в качестве разделителя, интерпретируя входные данные как одну длинную строку.
-
Переход на Perl или другие языки обработки текста: Perl предлагает более гибкий подход к обработке строк с NUL-символами. Использование Perl позволяет обрабатывать сложные логики замены без необходимости беспокоиться о внутренних ограничениях, связанных с разделителями.
$ perl -pe 'BEGIN{($a, $b)=(shift, shift)} s/\Q$a\E/$b/g' -- '/path/to/a' '/path/to/b' pathsList
-
Использование
tr
для простых замен: Если ваша задача заключается в замене отдельных символов на другие, попутно можно использовать командуtr
:$ echo "/path/to/a/folder" | tr 'a' 'b'
Вывод
Таким образом, хотя sed
не может непосредственно использовать NUL-символ в качестве разделителя из-за своих внутренних ограничений, существует несколько способов обойти данную проблему, будь то путем выбора другого символа в качестве разделителя, перехода на более мощные инструменты, такие как Perl, или применения различных подходов к пред- и пост-обработке данных. Применение этих методов позволит вам эффективно и безопасно обрабатывать текстовые данные даже в сложных случаях.