Захоплення результату пошуку. -print0 у масив bash


76

Використання find . -print0здається єдиним безпечним способом отримання списку файлів у bash через можливість імен файлів, що містять пробіли, нові рядки, лапки тощо.

Однак мені важко зробити насправді результати пошуку find корисними в bash або за допомогою інших утиліт командного рядка. Єдиний спосіб, яким мені вдалося скористатися результатом, - це переведення його в perl і зміна IFS-файлу perl на null:

find . -print0 | perl -e '$/="\0"; @files=<>; print $#files;'

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

find . | wc -l

Оскільки більшість програм командного рядка не підтримують введення з розділеним нулем введенням, я вважаю, що найкращим було б захопити вихідні дані find . -print0в масиві bash, як це було зроблено у фрагменті perl вище, а потім продовжити завдання, як би там не було бути.

Як я можу це зробити?

Це не працює:

find . -print0 | ( IFS=$'\0' ; array=( $( cat ) ) ; echo ${#array[@]} )

Набагато більш загальне запитання може бути: як я можу робити корисні речі зі списками файлів у bash?


Що ви маєте на увазі, роблячи корисні речі?
Balázs Pozsár

4
О, ти знаєш, звичайні речі масивів корисні для: з’ясування їх розміру; перегляд їх змісту; їх друк назад; сортуючи їх. Такі речі. У Unix існує безліч службових програм для виконання цих дій з даними: wc, bash's for-loops, tac і sort відповідно; але всі вони здаються марними при роботі зі списками, в яких можуть бути пробіли або нові рядки. Тобто імена файлів. Здавання даних навколо з нульовими значеннями роздільників вхідних полів, здається, є рішенням, але дуже мало комунальних служб можуть впоратися з цим.
Ідріс

1
Ось есе про те, як правильно обробляти імена файлів у оболонці, з багатьма специфіками: http://www.dwheeler.com/essays/filenames-in-shell.html
Девід А. Вілер

Відповіді:


103

Безсоромно викрадене з Greg's BashFAQ :

unset a i
while IFS= read -r -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done < <(find /tmp -type f -print0)

Зверніть увагу, що використана тут конструкція перенаправлення cmd1 < <(cmd2)схожа на, але не зовсім така ж, як і на звичайніший конвеєр ( cmd2 | cmd1) - якщо команди є вбудованими в оболонку (наприклад while), версія конвеєра виконує їх у підшелутках та будь-які змінні, які вони встановлюють (наприклад, масив a) втрачаються при виході з них. cmd1 < <(cmd2)виконує лише cmd2 у підоболонці, тому масив живе після своєї побудови. Попередження: ця форма перенаправлення доступна лише в bash, навіть не в bash в режимі ш-емуляції; ви повинні розпочати свій сценарій з #!/bin/bash.

Крім того, оскільки на кроці обробки файлів (у цьому випадку просто a[i++]="$file", але ви можете зробити щось захоплююче безпосередньо в циклі) вхід перенаправлено, він не може використовувати жодних команд, які можуть читатися з stdin. Щоб уникнути цього обмеження, я, як правило, використовую:

unset a i
while IFS= read -r -u3 -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done 3< <(find /tmp -type f -print0)

... який передає список файлів через блок 3, а не через stdin.


Аааа, майже там ... це найкраща відповідь ще. Однак я щойно спробував його в каталозі, що містить файл із новим рядком в його назві, і після перевірки цього елемента за допомогою echo $ {a [1]} новий рядок, здається, став пробілом (0x20). Будь-яка ідея, чому це відбувається?
Ідріс

Яку версію bash ви використовуєте? У мене були проблеми зі старішими версіями (на жаль, я точно не пам’ятаю, які) не маючи справу з новими рядками та видаляє ( \177) у рядках. IIRC, навіть x = "$ y" не завжди буде працювати правильно з цими символами. Я щойно тестував з bash 2.05b.0 і 3.2.17 (найстаріший і найновіший, який мені зручний); обидва обробляли нові рядки належним чином, але v2.05b.0 з’їв символ видалення.
Гордон Девіссон

Я спробував це на 3.2.17 на osx, 3.2.39 на linux і 3.2.48 на netBSD; всі перетворюють новий рядок у космос.
Ідріс

12
-d ''еквівалентно -d $'\0'.
l0b0

15
Найпростіший спосіб додати елемент до кінця масиву:arr+=("$file")
dogbane

7

Можливо, ви шукаєте ксарг:

find . -print0 | xargs -r0 do_something_useful

Параметр -L 1 може бути корисним і для вас, що робить xargs exec do_something_useful лише з 1 аргументом файлу.


3
Це не зовсім те, що я шукав, тому що немає можливості робити подібні до масиву речі зі списком, наприклад, сортування: ви повинні використовувати кожен елемент як і коли він з’являється з команди find. Якщо ви могли б детальніше розказати цей приклад, коли частина "do_something_useful" є операцією push-масиву bash, то це може бути те, що я шукаю.
Ідріс

6

Починаючи з Bash 4.4, вбудований модуль mapfileмає -dперемикач (щоб вказати роздільник, подібний до -dперемикача readоператора), і роздільник може бути нульовим байтом. Отже, приємна відповідь на запитання в заголовку

Захоплення вихідних даних find . -print0у масив bash

це:

mapfile -d '' ary < <(find . -print0)

5

Основна проблема полягає в тому, що роздільник NUL (\ 0) тут марний, оскільки неможливо призначити IFS значення NUL. Отже, як хороші програмісти, ми дбаємо про те, щоб вхідні дані для нашої програми були те, з чим вона здатна впоратися.

Спочатку ми створюємо невелику програму, яка робить для нас цю частину:

#!/bin/bash
printf "%s" "$@" | base64

... і назвіть це base64str (не забувайте chmod + x)

По-друге, тепер ми можемо використовувати простий і зрозумілий for-loop:

for i in `find -type f -exec base64str '{}' \;`
do 
  file="`echo -n "$i" | base64 -d`"
  # do something with file
done

Отже, фокус полягає в тому, що рядок base64 не має знаку, що створює проблеми для bash - звичайно, xxd або щось подібне також може виконати цю роботу.


1
Потрібно переконатися, що частина файлової системи, яку обробляє find, не змінюється з моменту виклику find до завершення роботи сценарію. Якщо це не так, виходить умова перегони, яку можна використати для виклику команд на неправильні файли. Наприклад, каталог, який потрібно видалити (скажімо / tmp / сміття), може бути замінений символічним посиланням на / home користувачем без права доступу. Якщо команда find виконувалась як root, і вона знаходила -type d -exec rm -rf '{}' \;, це видаляло б домашні папки всіх користувачів.
Демі

2
read -r -d ''прочитає все до наступного NUL в "$REPLY". Не потрібно дбати IFS.
Чарльз Даффі,

4

Ще один спосіб підрахунку файлів:

find /DIR -type f -print0 | tr -dc '\0' | wc -c 

2

Ви можете сміливо робити підрахунок за допомогою цього:

find . -exec echo ';' | wc -l

(Він друкує новий рядок для кожного знайденого файлу / каталогу, а потім підраховує роздруковані нові рядки ...)


Набагато швидше використовувати -printfопцію замість -execкожного файлу:find . -printf "\n" | wc -l
Олівер I,

1

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

i=0;
for f in *; do
  array[$i]="$f"
  ((i++))
done

Потім ви можете, наприклад, перерахувати файли по одному (у цьому випадку в зворотному порядку):

for ((i = $i - 1; i >= 0; i--)); do
  ls -al "${array[$i]}"
done

Ця сторінка дає хороший приклад, і більше см в главі 26 в Розширеному керівництві Bash-Scripting .


Це (та інші подібні приклади нижче) - це майже те, що я хочу, але з великою проблемою: воно працює лише для глобусів поточного каталогу. Я хотів би мати можливість маніпулювати абсолютно довільними списками файлів; результат "find", наприклад, який перелічує каталоги рекурсивно, або будь-який інший список. Що, якби мій список був: (/tmp/foo.jpg | /home/alice/bar.jpg | / home / bob / my holiday / baz.jpg | /tmp/new\nline/grault.jpg) або будь-який інший абсолютно довільний список файлів (звичайно, потенційно з пробілами та новими рядками в них)?
Ідріс

1

Уникайте ксарг, якщо можете:

man ruby | less -p 777 
IFS=$'\777' 
#array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' \; 2>/dev/null) ) 
array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' + 2>/dev/null) ) 
echo ${#array[@]} 
printf "%s\n" "${array[@]}" | nl 
echo "${array[0]}" 
IFS=$' \t\n' 

Чому ви встановлюєте IFS \777?
sschober

1

Я новачок, але я вважаю, що це відповідь; сподіваюся, це комусь допоможе:

STYLE="$HOME/.fluxbox/styles/"

declare -a array1

LISTING=`find $HOME/.fluxbox/styles/ -print0 -maxdepth 1 -type f`


echo $LISTING
array1=( `echo $LISTING`)
TAR_SOURCE=`echo ${array1[@]}`

#tar czvf ~/FluxieStyles.tgz $TAR_SOURCE

0

Це схоже на версію Stephan202, але файли (і каталоги) розміщуються в масиві відразу. forЦикл тут просто «робити корисні речі»:

files=(*)                        # put files in current directory into an array
i=0
for file in "${files[@]}"
do
    echo "File ${i}: ${file}"    # do something useful 
    let i++
done

Щоб отримати підрахунок:

echo ${#files[@]}

0

Старе запитання, але ніхто не пропонував цього простого методу, тож я подумав. Звичайно, якщо у ваших іменах файлів є ETX, це не вирішить вашу проблему, але я підозрюю, що воно служить для будь-якого реального сценарію. Спроба використовувати null, здається, порушує стандартні правила обробки IFS. Приправляйте на свій смак за допомогою варіантів пошуку та обробки помилок.

savedFS="$IFS"
IFS=$'\x3'
filenames=(`find wherever -printf %p$'\x3'`)
IFS="$savedFS"

1
Що означає ETX ? Можливо, ім'я файлу EXT ension або, можливо, Кінець тексту ...
oHo

0

Відповідь Гордона Девіссона чудово підходить для Баша. Однак корисний ярлик існує для користувачів zsh:

Спочатку помістіть рядок у змінну:

A="$(find /tmp -type f -print0)"

Далі розділіть цю змінну та збережіть її в масиві:

B=( ${(s/^@/)A} )

Є хитрість: ^@це символ NUL. Для цього потрібно набрати Ctrl + V, а потім Ctrl + @.

Ви можете перевірити, що кожен запис $ B містить правильне значення:

for i in "$B[@]"; echo \"$i\"

Уважливі читачі можуть помітити, що findв більшості випадків використання **синтаксису можна уникнути заклику до команди . Наприклад:

B=( /tmp/** )

-1

Bash ніколи не вмів обробляти імена файлів (або будь-який інший текст), оскільки він використовує пробіли як роздільник списку.

Я б рекомендував замість цього використовувати python з бібліотекою sh .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.