Використання згенерованого списку імен як списку аргументів - з пробілами


16

Я намагаюся викликати сценарій зі списком назви файлів, зібраним користувачем find. Нічого особливого, просто щось таке:

$ myscript `find . -name something.txt`

Проблема полягає в тому, що деякі імена шляхів містять пробіли, тому вони розбиваються на два недійсні імена при розширенні аргументів. Зазвичай я б оточував імена лапками, але сюди вони вставлені розширенням зворотного цитата. Я намагався відфільтрувати вихід findі оточувати кожне ім'я файлів цитатами, але до того моменту, коли баш бачить їх, уже пізно знімати їх, і вони трактуються як частина імені файлу:

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

Так, це правила, як обробляється командний рядок, але як я його обходжу?

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

Редагувати: Мене збентежило те, що xargs він збирає всі аргументи в єдиний список аргументів, якщо не сказано інше або якщо системні обмеження можуть бути перевищені. Дякую всім за те, що вони мене встановили прямо! Інші, майте це на увазі, читаючи прийняту відповідь, оскільки це не вказано дуже прямо.

Я прийняв відповідь, але моє питання залишається: Чи не існує способу захисту пробілів у $(...)розширенні backtick (або )? (Зауважте, що прийняте рішення - невідповідна відповідь).


Я думаю, вам знадобиться змінити те, що оболонка використовує як роздільники імен файлів (наприклад, граючи зі значенням IFS, один із можливих способів - IFS="новий рядок "). Але чи потрібно виконувати сценарій над усіма іменами файлів? Якщо ні, подумайте про те, щоб знайти себе для виконання сценарію для кожного файлу.
njsg

Зміна IFS - чудова ідея, не думав про це! Не практично для використання командного рядка, але все ж. :-) І так, мета - передати всі аргументи на одне і те ж виклик мого сценарію.
alexis

Відповіді:


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

Або коли я використовую другу форму так:

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

Деталі

знайти + xargs

Зазначені вище методи, хоч і виглядають по-різному, по суті однакові. Перший - це отримання виводу з пошуку, розділення його за допомогою NULLs ( \0) через -print0перемикач для пошуку. Розроблено xargs -0спеціально для прийому даних, розбитих за допомогою NULL. Це нестандартний синтаксис був введений GNU findі , xargsале також знаходиться в даний час в деяких інших , як і більшості останнього BSDs. -rОпція потрібно , щоб уникнути не викликаючи myscriptякщо findвбачає з GNU , findале не з BSDs.

ПРИМІТКА. Весь цей підхід залежить від того, що ви ніколи не проходите дуже довгий рядок. Якщо це так, то другий виклик ./myscriptбуде розпочато з рештою наступних результатів з пошуку.

знайти з +

Це стандартний спосіб (хоча він був доданий порівняно недавно (2005 р.) До впровадження GNU find). Можливість робити те, що ми робимо xargs, буквально вбудована find. Таким чином, findви знайдете список файлів, а потім перейдете до цього списку стільки ж аргументів, скільки може вміститися в команді, зазначеній після -exec(зауважте, що в цьому випадку {}може бути останньою лише заздалегідь +), виконуючи команди кілька разів, якщо потрібно.

Чому немає цитування?

У першому прикладі ми використовуємо ярлик, повністю уникаючи проблем із цитуванням, використовуючи NULLs для розділення аргументів. Коли 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

1
Дякую, але мені потрібно передати всі аргументи на одне й те саме виклик мого сценарію. Це в описі проблеми, але я думаю, я не дав зрозуміти, що це не випадково.
alexis

@alexis - прочитайте відповіді ще раз, вони передають усі аргументи на один виклик вашого сценарію.
slm

Я буду проклятий! Я не знав про +аргумент до find(і ви також використовуєте +в прозі, тому я пропустив ваше пояснення вперше). Але до речі, я неправильно зрозумів, що xargsробить за замовчуванням !!! За три десятиліття використання Unix я ніколи до цього не використовував, але думав, що знаю свою панель інструментів ...
alexis

@alexis - я подумав, що ти пропустив те, що ми говорили. Так xargs- чорт команди. Ви повинні прочитати його та findчоловічі сторінки багато разів, щоб дізнатися, що вони можуть зробити. Травень комутаторів є протилежними один одному, так що додає плутанини.
slm

@alexis - також ще одна річ, яку потрібно додати до вікна інструментів, не використовуйте зворотні котирування / backticks для запуску вкладених команд, $(..)замість цього використовуйте зараз. Він автоматично обробляє введення котирувань і т.д.
slm

3
find . -name something.txt -exec myscript {} +

У вищесказаному findзнаходить усі відповідні імена файлів та надає їх як аргументи myscript. Це працює з іменами файлів незалежно від пробілів або будь-яких інших непарних символів.

Якщо всі назви файлів вміщуються в одному рядку, то сценарій виконується один раз. Якщо список занадто довгий, щоб оболонка обробляла, тоді пошук буде запускати таємницю кілька разів, якщо потрібно.

БІЛЬШЕ: Скільки файлів вміщується в командному рядку? man findговорить, що findбудує його командні рядки "майже так само, як xargs будує свої". І man xargsщо обмеження залежать від системи і їх можна визначити, запустивши xargs --show-limits. ( getconf ARG_MAXтакож є можливість). В Linux обмеження зазвичай (але не завжди) становить близько 2 мільйонів символів на командному рядку.


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

(Розмір стека 1GiB, чверть якого можна використовувати для списку 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 у сценарії від stdin, можливо на основі аргументу командного рядка -o -. Я б робив щось на кшталт:

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

і відповідно реалізувати аргументи варіанту myscript.


Так, ОС накладає обмеження в кількості / розмірі аргументів, які можна передавати. У сучасних системах Linux це (гігантський) ( linux.die.net/man/2/execve ) (1/4 розміру стека, аргументи 0x7FFFFFFF). AFAIK баш сам по собі не встановлює жодних обмежень. Мої списки набагато менші, і моя проблема була викликана нерозумінням або неправильним запам'ятовуванням того, як це xargsпрацює. Ваше рішення справді є найбільш надійним, але в цьому випадку це надмірно.
alexis

0

Чи не існує способу захисту пробілів у розширенні backtick (або $ (...))?

Ні, немає. Чому так?

Баш не може знати, що слід захищати, а що не слід.

У файлі / трубі unix немає масивів. Це просто потік байтів. Команда всередині ``або $()виводить потік, який 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)

баш

Якщо ви не хочете залучати зовнішню команду, цикл під час читання, що подає масив, як показано на /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 не оптимізований для обробки масиву.


man xargs :

-n max-args

Використовуйте не більше аргументів max-args у командному рядку. Менше, ніж аргументи max-args будуть використані, якщо розмір (див. Опцію -s) буде перевищено, якщо не вказано опцію -x, у цьому випадку xargs вийде.

-s max-chars

Використовуйте не більше max-chars символів у командному рядку, включаючи команду та початкові аргументи та завершальні нулі на кінцях рядків аргументів. Найбільше дозволене значення залежить від системи і обчислюється як обмеження довжини аргументу для exec, за винятком розміру вашого середовища, менше 2048 байтів прогону. Якщо це значення перевищує 128KiB, то за замовчуванням використовується 128Kib; в іншому випадку значення за замовчуванням є максимальним. 1KiB - 1024 байти.

Вийдіть, якщо розмір (див. Опцію -s) перевищено.


Дякую за всі проблеми, але ваша основна передумова ігнорує той факт, що bash зазвичай використовує складну систему обробки цитат. Але не в розширенні зворотного котирування. Порівняйте наступні (помилки , які обидва дають, але показати різницю): ls "what is this"проти ls `echo '"what is this"'` . Хтось знехтував реалізувати обробку цитат у результаті зворотних цитат.
alexis

Я радий, що зворотні котирування не обробляють цитати. Те, що вони навіть роблять розбиття слів, спричинило досить заплутані погляди, подряпини в голові та недоліки безпеки у сучасній історії обчислень.
клак

Питання "Чи не існує способу захисту пробілів у $(...)розширенні backtick (або )?", Тому здається доречним ігнорувати обробку, що не робиться в цій ситуації.
клак

Формат масиву елементів, що закінчуються нулем, є найпростішим і, таким чином, найбезпечнішим способом вираження масиву. Це просто ганьба, що bashне підтримує це споконвічно, як очевидно zsh.
clacke

Насправді, якраз на цьому тижні я використовував printf "%s\0"та xargs -0прокладав ситуацію котирування, коли проміжний інструмент передавав би параметри через рядок, розібраний оболонкою. Цитування завжди повертається, щоб вкусити вас.
клак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.