Правильное экранирование вывода из конвейера в xargs

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

Пример:

% touch -- safe-name -name-with-dash-prefix "name with space" \
    'name-with-double-quote"' "name-with-single-quote'" \
    'name-with-backslash\'

xargs не может обрабатывать двойные кавычки:

% ls | xargs ls -l 
xargs: несоответствующая двойная кавычка; по умолчанию кавычки являются специальными для xargs, если вы не используете опцию -0
ls: недействительная опция -- 'e'
Попробуйте 'ls --help' для получения дополнительной информации.

Если мы используем опцию -0, она вызывает проблемы с именем, которое имеет дефис в префиксе:

% ls -- * | xargs -0 -- ls -l --
ls: недействительная опция -- 'e'
Попробуйте 'ls --help' для получения дополнительной информации.

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

Спецификация POSIX дает вам пример:

ls | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' | xargs -E '' printf '<%s>\n'

(с именами файлов в виде произвольных последовательностей байтов (кроме / и NULL) и sed/xargs ожидающими текст, вам также нужно будет установить локаль в C (где все не-NUL байты будут действительными символами), чтобы сделать это надежным (за исключением реализации xargs, которые имеют очень низкий предел на максимальную длину аргумента))

Опция -E '' необходима для некоторых реализаций xargs, которые без нее бы поняли аргумент _ как указание на конец ввода (где echo a _ b | xargs выводит только a, к примеру).

С GNU xargs вы можете использовать:

ls | xargs -rd '\n' printf '<%s>\n'

(также добавляя -r (также расширение GNU), чтобы команда не выполнялась, если ввод пустой).

GNU xargs также имеет -0, который был скопирован несколькими другими реализациями, поэтому:

ls | tr '\n' '\0' | xargs -0 printf '<%s>\n'

является немного более портативным.

Все это предполагает, что имена файлов не содержат символов новой строки. Если могут быть имена файлов с символами новой строки, вывод ls просто нельзя обрабатывать после. Если вы получите:

a
b

Это может быть как оба файла a и b, так и один файл с именем a<newline>b, нет способа это определить.

GNU ls имеет --quoting-style=shell-always, который делает его вывод недвусмысленным и потенциально обрабатываемым после, но кавычки не совместимы с кавычками, ожидаемыми xargs. xargs распознает формы кавычек "...", \x и '...'. Но как "...", так и '...' являются сильными кавычками и не могут содержать символы новой строки (только \ может экранировать символы новой строки для xargs), так что это не совместимо с кавычками sh, где только '...' являются сильными кавычками (и могут содержать символы новой строки), но \<newline> является продолжением строки (удаляется) вместо экранированной новой строки.

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

eval "files=($(ls --quoting-style=shell-always))"
[ "${#files[@]}" -eq 0 ] || printf '%s\0' "${files[@]}" |
  xargs -0 printf '<%s>\n'

Или вы можете заставить оболочку получить список файлов и передать его с разделением NUL в xargs. Например:

  • с zsh:

    print -rNC1 -- *(N) | xargs -r0 printf '<%s>\n'
    
  • с ksh93:

    (set -- ~(N)*; (($# == 0)) || printf '%s\0' "$@") |
      xargs -r0 printf '<%s>\n'
    
  • с fish:

    begin set -l files *; string join0 -- $files; end |
      xargs -r0 printf '<%s>\n'
    
  • с bash:

    (
      shopt -s nullglob
      set -- *
      (($# == 0)) || printf '%s\0' "$@"
    ) | xargs -r0 printf '<%s>\n'
    

Редактирование 2023. Начиная с версии 9.0 GNU coreutils (сентябрь 2021), GNU ls теперь имеет опцию --zero, которую можно использовать вместе с xargs -r0:

ls --zero | xargs -r0 printf '<%s>\n'

Для того чтобы xargs понимал опцию ввода, разделенной нулем -0, отправляющая сторона также должна применить нулевой разделитель к данным, которые они отправляют.

Иначе между ними не будет синхронизации.

Одной из опций является команда GNU find, которая может устанавливать такие разделители:

find . -maxdepth 1 ! -name . -print0 | xargs -0 ls -ld

Как вы сказали, xargs не любит несоответствующие двойные кавычки, если вы не используете -0, но -0 имеет смысл только если вы передаете ей данные, завершающиеся нулем. Поэтому это не срабатывает:

$ echo * | xargs
xargs: несоответствующая двойная кавычка; по умолчанию кавычки являются специальными для xargs, если вы не используете опцию -0
name-with-backslash -name-with-dash-prefix

Но это работает:

$ printf '%s\0' -- * | xargs -0
-- name-with-backslash\ -name-with-dash-prefix name-with-double-quote" name-with-single-quote' name with space safe-name

В любом случае, ваш основной подход не является наилучшим способом сделать это. Вместо того чтобы возиться с xargs и ls и прочим, просто используйте оболочечные шаблоны:

$ for f in *; do ls -l -- "$f"; done
-rw-r--r-- 1 terdon terdon 4142 Aug 11 16:03 a
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name-with-backslash\'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 -name-with-dash-prefix
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name-with-double-quote"'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 "name-with-single-quote'"
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 'name with space'
-rw-r--r-- 1 terdon terdon 0 Aug 11 15:34 safe-name

Это крайне глупо пытаться разобрать вывод команды ls, которая не предназначена для разбора, чтобы передать команде, которая не предназначена для работы с несколькими символами (например: новые строки и {}), когда оболочка делает это сама:

set -- *; for f; do echo "<$f>"; done

set    -- *
for    f
do     ls "$f"
done

Или, в одной строке команды:

$ set -- *; for f; do echo "<$f>"; done
<name-with-backslash\>
<-name-with-dash-prefix>
<name-with-double-quote">
<name-with-single-quote'>
<name with space>
<safe-name>
<with_a
newline>

Обратите внимание, что вывод обрабатывает (и имеет пример в качестве последнего имени файла) новые строки вполне нормально.

Или, если количество файлов делает оболочку медленной, используйте find:

$ find ./ -type f -exec echo '<{}>' \;
<./safe-name>
<./with_a
newline>
<./name-with-double-quote">
<./-name-with-dash-prefix>
<./name with space>
<./name-with-single-quote'>
<./name-with-backslash\>

Просто имейте в виду, что find обрабатывает все скрытые файлы и все подкаталоги по-другому, чем оболочка.

Решение с xargs -d '\n'

Если ваша команда не поддерживает разделение нулем, и если входные данные также не содержат буквальных новых строк, мы также можем решить проблему, передав -d '\n' в xargs.

Например, эта попытка объединить каждую пару строк завершается ошибкой:

printf "1'\n2\n3\n4\n" | xargs -n2 echo

но нам удается обойти это с помощью:

printf "1'\n2\n3\n4\n" | xargs -d '\n' -n2 echo

что дает желаемый вывод:

1' 2
3 4

man xargs говорит:

--delimiter=delim, -d delim

Элементы ввода завершаются указанным символом. Указанный разделитель может быть одиночным символом, символом экранирования в стиле C, таким как \n, или восьмеричным или шестнадцатеричным экранирующим кодом. Восьмеричные и шестнадцатеричные экранирующие коды понимаются так же, как и для команды printf. Много байтные символы не поддерживаются. При обработке ввода кавычки и обратная косая черта не являются специальными; каждый символ во вводе принимается буквально. Опция -d отключает любую строку конца файла, которая обрабатывается как любой другой аргумент. Вы можете использовать эту опцию, когда ввод состоит просто из строк, разделенных новой строкой, хотя почти всегда лучше разработать вашу программу с использованием --null, если это возможно.

Связано:

Почему происходит эта ошибка

По умолчанию printf позволяет входным данным содержать аргументы со специальными символами, такими как пробелы, если они окружены сбалансированными кавычками, например:

printf "'1 2' 3 4" | xargs -n1 echo

выдает:

1 2
3
4

поэтому мы понимаем, что вызовы, где:

echo 1 2
echo 3
echo 4

Тестировалось на xargs 4.9.0, Ubuntu 24.04.

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

Эффективное экранирование вывода в xargs: Полное руководство

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

Проблема с экранированием в xargs

При использовании xargs для обработки вывода из других команд, пользователи часто сталкиваются с проблемами экранирования. Например, если команды, такие как ls, выводят имена файлов, содержащие пробелы или кавычки, xargs может не корректно интерпретировать эти имена файлов. Рассмотрим следующие примеры:

$ ls | xargs ls -l
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option

Этот пример демонстрирует, что xargs не может справиться с неподходящими кавычками, что приводит к ошибкам.

Решение с использованием -0

Для безопасной передачи данных в xargs, рекомендуется использовать опцию -0. Она ожидает, что данные будут разделены нулевыми символами (\0). Однако, для этого необходимо передавать данные в таком формате из исходной команды. Например, можно использовать find, который поддерживает вывод с нулевыми разделителями:

find . -maxdepth 1 -name '*' -print0 | xargs -0 ls -l

Этот подход позволяет избежать проблем с кавычками и пробелами в именах файлов.

Альтернативные решения

Если ваше окружение не поддерживает нулевые разделители, можно использовать механизмы оболочки для решения подобных проблем. Например, в bash можно воспользоваться следующей конструкцией:

for f in *; do
    ls -l -- "$f"
done

Этот метод избегает использования xargs, что может быть более надежным при работе с именами файлов, содержащими специальные символы.

Совместимость с другими оболочками

Каждая оболочка предлагает свои методы работы с именами файлов и экранированием. Например, в zsh и ksh93 можно использовать аналогичные подходы:

zsh

print -rNC1 -- *(N) | xargs -r0 printf '<%s>\n'

ksh93

(set -- ~(N)*; (($# == 0)) || printf '%s\0' "$@") | xargs -r0 printf '<%s>\n'

Каждый из этих примеров демонстрирует, как можно безопасно обрабатывать имена файлов.

Передача через printf и xargs

Также можно воспользоваться printf для формирования строки с нулевыми разделителями, что будет полезно при использовании xargs. Пример:

printf '%s\0' -- * | xargs -0 printf '<%s>\n'

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

Заключение

Для успешного использования xargs с потенциально проблемными именами файлов, рекомендуется заранее обрабатывать вывод, используя нулевые разделители или механизмы оболочки для корректного экранирования. Запомните, что команда ls не предназначена для парсинга, и лучше использовать альтернативы, такие как find для безопасного и надежного взаимодействия с файловой системой.

Применяя описанные методы, вы значительно повысьте надежность своих сценариев и избежите распространенных ошибок, связанных с экранированием символов в командной строке.

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

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