Использование сгенерированного списка имен файлов в виде списка аргументов – с пробелами

Я пытаюсь вызвать скрипт со списком имен файлов, собранных find . Ничего особенного, просто что-то вроде этого:

 $ myscript `find . -name something.txt` 

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

 $ myscript `find . -name something.txt | sed 's/.*/"&"/'` No such file or directory: '"./somedir/something.txt"' 

Да, это правила для обработки командной строки, но как мне обойти ее?

Это неловко, но я не могу придумать правильный подход. Я наконец понял, как это сделать с xargs -0 -n 10000 … но это такой уродливый взлом, который я все еще хочу спросить: как я могу привести результаты расширения backquote или добиться того же эффекта по-другому?

Редактирование: меня смутило то обстоятельство, что xargs действительно собирает все аргументы в один список аргументов, если не сказано иначе, или системные ограничения могут быть превышены. Спасибо всем за то, что вы меня прямо! Другие, помните об этом, когда вы читаете принятый ответ, потому что это не указано очень прямо.

Я принял ответ, но мой вопрос остается: нет ли способа защитить места в backtick (или $(...) ) расширении? (Обратите внимание, что принятое решение является ответом non-bash).

Вы можете сделать следующее, используя некоторые реализации find и xargs подобные этому.

 $ find . -type f -print0 | xargs -r0 ./myscript 

или, как правило, просто find :

 $ find . -type f -exec ./myscript {} + 

пример

Скажем, у меня есть следующий образец каталога.

 $ tree . |-- dir1 | `-- a\ file1.txt |-- dir2 | `-- a\ file2.txt |-- dir3 | `-- a\ file3.txt `-- myscript 3 directories, 4 files 

Теперь предположим, что у меня есть это для ./myscript .

 #!/bin/bash for i in "$@"; do echo "file: $i" done 

Теперь, когда я запускаю следующую команду.

 $ find . -type f -print0 | xargs -r0 ./myscript file: ./dir2/a file2.txt file: ./dir3/a file3.txt file: ./dir1/a file1.txt file: ./myscript 

Или когда я использую вторую форму следующим образом:

 $ find . -type f -exec ./myscript {} + file: ./dir2/a file2.txt file: ./dir3/a file3.txt file: ./dir1/a file1.txt file: ./myscript 

Детали

найти + xargs

Вышеуказанные 2 метода, хотя и выглядят по-разному, по сути, одинаковы. Первый принимает результат поиска, разбивая его с помощью NULL ( \0 ) с помощью переключателя -print0 для поиска. xargs -0 специально разработан для ввода данных, разделенных с помощью NULL. Этот нестандартный синтаксис был введен с помощью GNU find и xargs но xargs он также встречается в нескольких других, таких как последние BSD. Параметр -r требуется, чтобы избежать вызова myscript если find находит ничего с GNU find но не с BSD.

ПРИМЕЧАНИЕ. Весь этот подход зависит от того, что вы никогда не пропустите строку, которая чрезвычайно длинная. Если это так, то второй вызов ./myscript начнется с остальной части последующих результатов поиска.

найти с +

Это стандартный способ (хотя он был добавлен сравнительно недавно (2005) к реализации GNU find ). Способность делать то, что мы делаем с xargs , буквально встроена в find . Таким образом, find найдет список файлов, а затем передаст этот список как столько аргументов, сколько подходит для команды, указанной после -exec (обратите внимание, что {} может быть только последним как раз перед + в этом случае), несколько раз выполняя команды, если необходимо.

Почему нет цитат?

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

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

Максимальный размер командной строки?

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

 $ xargs --show-limits Your environment variables take up 4791 bytes POSIX upper limit on argument length (this system): 2090313 POSIX smallest allowable upper limit on argument length (all systems): 4096 Maximum length of command we could actually use: 2085522 Size of command buffer we are actually using: 131072 
 find . -name something.txt -exec myscript {} + 

В приведенном выше myscript find находит все совпадающие имена файлов и предоставляет их в качестве аргументов для myscript . Это работает с именами файлов независимо от пробелов или любых других нечетных символов.

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

БОЛЬШЕ: Сколько файлов подходит для командной строки? man find говорит, что find строит его командные строки «почти так же, как xargs строит свое». И, man xargs что ограничения зависят от системы и что вы можете определить их, запустив xargs --show-limits . ( getconf ARG_MAX также является возможностью). В Linux лимит обычно (но не всегда) составляет около 2 миллионов символов в командной строке.

Несколько дополнений к тонкому ответу @ slm.

Ограничение размера аргументов на execve(2) вызове execve(2) (на самом деле, это кумулятивный размер аргумента и строк и указателей окружения). Если myscript написан на языке, который может интерпретировать ваша оболочка, то, возможно, вам не нужно его выполнять , вы можете заставить вашу оболочку просто интерпретировать ее, не выполняя другого интерпретатора.

Если вы запустите скрипт как:

 (. myscript xy) 

Это как:

 myscript xy 

За исключением того, что он интерпретируется дочерним элементом текущей оболочки, вместо того, чтобы выполнять ее (что в конечном итоге включает выполнение sh (или что бы то ни было, строка she-bang указывает, если таковая имеется) с еще большим количеством аргументов).

Теперь, очевидно, вы не можете использовать find -exec {} + с помощью . команда, as . будучи встроенной командой оболочки, она должна выполняться оболочкой, а не путем find .

С zsh это легко:

 IFS=$'\0' (. myscript $(find ... -print0)) 

Или:

 (. myscript ${(ps:\0:)"$(find ... -print0)"} 

Хотя с zsh , вам не нужно будет find в первую очередь, так как большинство его функций встроены в zsh globbing.

Однако переменные bash не могут содержать символы NUL, поэтому вам нужно найти другой способ. Одним из способов может быть:

 files=() while IFS= read -rd '' -u3 file; do files+=("$file") done 3< <(find ... -print0) (. myscript "${files[@]}") 

Вы также можете использовать рекурсивное globstar в стиле zsh с параметром globstar в bash 4.0 и более поздних версиях:

 shopt -s globstar failglob dotglob (. myscript ./**/something.txt) 

Обратите внимание, что ** следует за символическими ссылками в каталоги, пока не будет исправлено в bash 4.3. Также обратите внимание, что bash не реализует квалификаторы zsh globbing, поэтому вы не получите все возможности find там.

Другой альтернативой было бы использование GNU ls :

 eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)" (. myscript "${files[@]}") 

Вышеупомянутые методы также могут быть использованы, если вы хотите, чтобы myscript выполнялся только один раз (если слишком большой список аргументов). В последних версиях Linux вы можете повысить и даже снять это ограничение в списке аргументов с помощью:

 ulimit -s 1048576 

(Размер стека 1GiB, четверть из которых может использоваться для списка arg + env).

 ulimit -s unlimited 

(безлимитный)

В большинстве систем существует ограничение на длину командной строки, переданной в любую программу, с использованием xargs или -exec command {} + . От man find :

 -exec command {} + This variant of the -exec action runs the specified command on the selected files, but the command line is built by appending each selected file name at the end; the total number of invoca‐ tions of the command will be much less than the number of matched files. The command line is built in much the same way that xargs builds its command lines. Only one instance of `{}' is allowed within the command. The command is executed in the starting directory. 

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

 $ find . -name something.txt -print0 | myscript -0 -o - 

и реализовать аргументы опции для myscript соответственно.

Разве нет способа защитить пространства в backtick (или $ (…)) расширении?

Нет, нет. Почему это?

У Баша нет возможности узнать, что нужно защищать, а что нет.

В файле unix unix нет массивов. Это просто поток байтов. Команда внутри `` или $() выводит поток, который bash проглатывает и обрабатывает как одну строку. В этом случае у вас есть только два варианта: поместите его в кавычки, чтобы сохранить его как одну строку или поставить наготу, так что bash разбивает его в соответствии с настроенным поведением.

Итак, что вам нужно сделать, если вы хотите, чтобы массив определял байтовый формат, который имеет массив, и это то, что инструменты, такие как xargs и find do: Если вы запускаете их с аргументом -0 , они работают в соответствии с форматом двоичного массива который завершает элементы с нулевым байтом, добавляя семантику к второму непрозрачному байтовому потоку.

К сожалению, bash не может быть настроен на разделение строк на нулевой байт. Спасибо https://unix.stackexchange.com/a/110108/17980 за показ нам, что zsh может.

xargs

Вы хотите, чтобы ваша команда запускалась один раз, и вы сказали, что xargs -0 -n 10000 решает вашу проблему. Это не так, это гарантирует, что если у вас более 10000 параметров, ваша команда будет работать более одного раза.

Если вы хотите, чтобы он строго выполнялся один раз или не выполнялся, вы должны -x аргумент -x аргумент -n больше аргумента -s (на самом деле: достаточно большой, чтобы целая куча аргументов нулевой длины плюс имя команда не соответствует размеру -s ). ( man xargs , см. отрывок далеко внизу)

Система, в которой я сейчас работаю, имеет стек, ограниченный примерно 8 М, поэтому вот мой предел:

 $ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true xargs: argument list too long $ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true (no output) 

удар

Если вы не хотите привлекать внешнюю команду, цикл while-read, подающий массив, как показано на https://unix.stackexchange.com/a/110108/17980 , является единственным способом для bash разделить вещи на нулевой байт.

Идея источника сценария ( . ... "$@" ) чтобы избежать ограничения размера стека, классная (я пробовал, он работает!), Но, вероятно, не важно для нормальных ситуаций.

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

Итак, самый простой «родной» способ для повседневных бытовых нужд:

 files=() while IFS= read -rd '' file; do files+=("$file") done <(find ... -print0) myscriptornonscript "${files[@]}" 

Если вы хотите, чтобы дерево процессов было чистым и красивым, этот метод позволяет вам выполнить exec mynonscript "${files[@]}" , который удаляет процесс bash из памяти, заменяя его вызванной командой. xargs всегда будет оставаться в памяти во время xargs вызываемой команды, даже если команда будет запускаться только один раз.


То, что говорит против метода родного bash, таково:

 $ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; } real 0m2.014s user 0m2.008s sys 0m0.172s $ time { args=() while IFS= read -rd '' arg; do args+=( "$arg" ) done < <(printf '%s\0' -- $(echo {1..1302581})) /bin/true "${args[@]}" } bash: /bin/true: Argument list too long real 107m51.876s user 107m38.532s sys 0m7.940s 

bash не оптимизирован для обработки массивов.


человек xargs :

-n max-args

Используйте максимум аргументов max-args для командной строки. Если параметр размера (см. Параметр -s) превышен, будет меньше аргументов max-args, если не указана опция -x, и в этом случае xargs выйдут.

-s max-chars

Используйте максимум символов max-chars в командной строке, включая команду и начальные-аргументы, и завершающие нули в концах строк аргумента. Наибольшее допустимое значение зависит от системы и рассчитывается как предел длины аргумента для exec, меньше размер вашей среды, менее 2048 байт запаса. Если это значение больше 128 Кбайт, в качестве значения по умолчанию используется 128Kib; в противном случае значением по умолчанию является максимальное значение. 1KiB – 1024 байта.

-Икс

Выход, если размер (см. Параметр -s) превышен.