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

16

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

$ myscript `find . -name something.txt`

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

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

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

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

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

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

Alexis
источник
Я полагаю, вам нужно изменить то, что оболочка использует в качестве разделителей имен файлов (например, играя со значением IFS, один из возможных способов - это IFS="перевод строки "). Но нужно ли выполнять скрипт над всеми именами файлов? Если нет, рассмотрите возможность использования find для выполнения скрипта для каждого файла.
njsg
Смена IFS - отличная идея, не подумал об этом! Непрактично для использования командной строки, но все же. :-) И да, цель состоит в том, чтобы передать все аргументы в один и тот же вызов моего сценария.
Алексис

Ответы:

12

Вы могли бы сделать следующее, используя некоторые реализации 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

Или когда я использую 2-ую форму примерно так:

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

Детали

найти + xargs

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

ПРИМЕЧАНИЕ. Весь этот подход основан на том факте, что вы никогда не пропустите чрезвычайно длинную строку. Если это так, то 2-й вызов ./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
SLM
источник
1
Спасибо, но мне нужно передать все аргументы в тот же вызов моего сценария. Это в описании проблемы, но, думаю, я не дал понять, что это не случайно.
Алексис
@alexis - прочитайте ответы еще раз, они передают все аргументы в один вызов вашего скрипта.
SLM
Будь я проклят! Я не знал об +аргументе find(и вы +тоже используете прозу, поэтому я пропустил ваше объяснение в первый раз). Но, что более важно, я бы неправильно понял, что xargsделает по умолчанию !!! За три десятилетия использования Unix у меня никогда не было его применения до сих пор, но я думал, что знаю свой инструментарий ...
alexis
@alexis - я подумал, что ты пропустил то, что мы говорили. Да xargs, это дьявол команды. Вы должны прочитать его и findсправочные страницы много раз, чтобы понять, что они могут сделать. Май выключателей противоречит друг другу, что добавляет путаницы.
SLM
@alexis - еще одна вещь, которую нужно добавить в панель инструментов, не используйте обратные кавычки / обратные кавычки для запуска вложенных команд, используйте $(..)вместо этого сейчас. Он автоматически обрабатывает вложение кавычек и т. Д. Бэктикс не рекомендуется.
SLM
3
find . -name something.txt -exec myscript {} +

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

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

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

John1024
источник
2

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

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

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

(. myscript x y)

Это как:

myscript x y

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

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

С zshэтим легко:

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

Или:

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

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

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

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

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

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

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

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

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

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

ulimit -s 1048576

(Размер стека 1 ГБ, четверть которого можно использовать для списка arg + env).

ulimit -s unlimited

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

Стефан Шазелас
источник
1

В большинстве систем существует ограничение на длину командной строки, передаваемой любой программе с использованием 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 разделенные имена файлов в сценарии из стандартного ввода, возможно, на основе аргумента командной строки -o -. Я хотел бы сделать что-то вроде:

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

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

Timo
источник
Да, ОС накладывает ограничение на количество / размер аргументов, которые могут быть переданы. В современных системах Linux это (гигантский) ( linux.die.net/man/2/execve ) (1/4 размера стека, аргументы 0x7FFFFFFF). AFAIK bash сам по себе не накладывает никаких ограничений. Мои списки намного меньше, и моя проблема была вызвана неправильным пониманием или неправильным запоминанием того, как xargsработает. Ваше решение действительно самое надежное, но в этом случае оно излишне.
Алексис
0

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

Нет, нет Это почему?

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

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

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

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

xargs

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

Если вы хотите, чтобы он строго выполнялся один раз или не выполнялся, вы должны предоставить -xаргумент и -nаргумент, больший, чем -sаргумент (действительно: достаточно большой, чтобы целый набор аргументов нулевой длины плюс имя команды не помещались в -sразмер). ( человек 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, питающий массив, как показано в /unix//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всегда будет оставаться в памяти во время выполнения вызываемой команды, даже если команда будет выполняться только один раз.


Что говорит против родного метода 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 символов в командной строке, включая команду и начальные аргументы и завершающие нули на концах строк аргументов. Максимально допустимое значение зависит от системы и рассчитывается как ограничение длины аргумента для exec, за вычетом размера вашей среды, меньше 2048 байт запаса. Если это значение больше 128 КБ, в качестве значения по умолчанию используется 128 КБ; в противном случае значение по умолчанию является максимальным. 1 КБ составляет 1024 байта.

-Икс

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

clacke
источник
Спасибо за все проблемы, но ваша основная посылка игнорирует тот факт, что bash обычно использует сложную систему обработки цитат. Но не в расширении кавычки. Сравните следующее (которые оба дают ошибки, но показывают разницу): ls "what is this"против ls `echo '"what is this"'` . Кто-то забыл внедрить обработку цитат для результата обратных цитат.
Алексис
Я рад, что обратные кавычки не выполняют обработку кавычек. Тот факт, что они даже делят слова, вызвал достаточно запутанную внешность, царапины на голове и недостатки безопасности в современной компьютерной истории.
глухой
Вопрос в том, «есть ли какой-нибудь способ защитить пробелы в $(...)расширении backtick (или )?», Поэтому представляется целесообразным игнорировать обработку, которая не выполняется в этой ситуации.
глухой
Формат массива элементов с нулевым символом в конце является самым простым и, следовательно, самым безопасным способом выражения массива. Это просто позор, bashкоторый не поддерживает его изначально, как, очевидно, zshделает.
глухой
Фактически, на этой неделе я использовал printf "%s\0"и xargs -0для обхода ситуации цитирования, когда промежуточный инструмент передавал параметры через строку, анализируемую оболочкой. Цитирование всегда возвращается, чтобы укусить вас.
Clacke