Як я можу зберегти результати команди “find” як масив у Bash


93

Я намагаюся зберегти результат findяк масиви. Ось мій код:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

Я отримую 2 файли .txt у поточному каталозі. Тож я очікую "2" як результат ${len}. Однак друкується 1. Причина полягає в тому, що він приймає всі результати findяк один елемент. Як я можу це виправити?

PS
Я знайшов кілька рішень на StackOverFlow щодо подібної проблеми. Однак вони трохи відрізняються, тому я не можу подати заяву у своєму випадку. Мені потрібно зберігати результати у змінній перед циклом. Знову дякую.

Відповіді:


134

Оновлення 2020 для користувачів Linux:

Якщо у вас є версія уточненого Баша (4,4-альфа або краще), як ви , ймовірно , робити , якщо ви на Linux, то ви повинні використовувати відповідь Benjamin W. в .

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

Відповідь для bash 4.3 або раніше

Ось одне рішення для отримання вихідного сигналу findв bashмасиві:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

Це складно, оскільки загалом імена файлів можуть мати пробіли, нові рядки та інші ворожі символи. Єдиний спосіб використовувати findта безпечно відокремити імена файлів - це використання, -print0яке друкує імена файлів, розділені нульовим символом. Це не було б великою незручністю, якби функції bash readarray/ mapfilefunctions підтримували рядки, розділені нулем, але вони цього не роблять. Це readробить Баш, і це веде нас до цикла вище.

[Ця відповідь була спочатку написана в 2014 році. Якщо у вас остання версія bash, перегляньте оновлення нижче.]

Як це працює

  1. Перший рядок створює порожній масив: array=()

  2. Кожного разу, коли readвиконується оператор, із стандартного вводу зчитується ім’я файлу, розділене нулем. -rОпція вказує , readщоб залишити символи зворотної косої межі в поодинці. -d $'\0'Каже про readте , що вхід буде нульовим розділені. Так як ми опускаємо ім'я до read, оболонка поміщає вхід в ім'я за замовчуванням: REPLY.

  3. Оператор array+=("$REPLY")додає нове ім'я файлу до масиву array.

  4. Останній рядок поєднує в собі переспрямування та заміну команди, щоб забезпечити вихід findна стандартний вхід whileциклу.

Навіщо використовувати заміну процесу?

Якби ми не використовували заміну процесу, цикл можна було б записати так:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

У наведеному вище вихідні дані findзберігаються у тимчасовому файлі, і цей файл використовується як стандартний вхід у цикл while. Ідея заміщення процесу полягає в тому, щоб зробити такі тимчасові файли непотрібними. Отже, замість того, щоб whileцикл отримував свій stdin tmpfile, ми можемо отримати з нього свій stdin <(find . -name ${input} -print0).

Заміна процесу є широко корисною. У багатьох місцях, де команда хоче читати з файлу, ви можете вказати заміну процесу <(...)замість імені файлу. Існує аналогічна форма >(...), яка може бути використана замість імені файлу , де команда хоче записати в файл.

Як і масиви, заміна процесів є особливістю bash та інших вдосконалених оболонок. Він не є частиною стандарту POSIX.

Альтернатива: lastpipe

За бажанням lastpipeможе використовуватися замість заміни процесу (підказка капелюха: Цезар ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipeговорить bash виконати останню команду в конвеєрі в поточній оболонці (а не у фоновому режимі). Таким чином, arrayзалишки існують після завершення трубопроводу. Оскільки lastpipeнабирає чинності лише якщо контроль роботи вимкнено, ми запускаємо set +m. (У сценарії, на відміну від командного рядка, керування завданнями за замовчуванням вимкнено.)

Додаткові нотатки

Наступна команда створює змінну оболонки, а не масив оболонки:

array=`find . -name "${input}"`

Якщо ви хочете створити масив, вам потрібно буде розмістити парени навколо результату пошуку. Отже, наївно, можна було б:

array=(`find . -name "${input}"`)  # don't do this

Проблема полягає в тому, що оболонка виконує розбиття слів на результати, findтак що елементи масиву не гарантовано будуть такими, як ви хочете.

Оновлення 2019

Починаючи з версії 4.4-alpha, bash тепер підтримує -dопцію, так що вищезазначений цикл більше не потрібен. Натомість можна використовувати:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

Для отримання додаткової інформації про це, дивіться (і upvote) відповідь Benjamin W. в .


1
@JuneyoungOh Радий, що це допомогло. Я додав розділ заміщення процесу.
John1024,

3
@Rockallite Це хороше спостереження, але неповне. Хоча це правда, що ми не ділимося на кілька слів, нам все одно потрібно IFS=уникати видалення пробілів із початку чи кінця рядків введення. Ви можете це легко перевірити, порівнявши вихідні дані read var <<<' abc '; echo ">$var<"з вихідними результатами IFS= read var <<<' abc '; echo ">$var<". У першому випадку пробіли до і після abcвидаляються. В останньому вони не є. Імена файлів, які починаються або закінчуються пробілами, можуть бути незвичними, але, якщо вони існують, ми хочемо, щоб їх правильно обробляли.
John1024

1
Привіт, після виконання вашого коду я отримую синтаксичну помилку повідомлення біля несподіваного токена <' <<(знайти aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Пшемислав Сенкевич

1
Примітка: простіший ''можна використовувати замість $'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman

1
@theeagle Я припускаю, що ви мали намір писати BLAH=$(find . -name '*.php'). Як обговорювалось у відповіді, такий підхід буде працювати в обмежених випадках, але він не працюватиме в цілому з усіма іменами файлів і не буде створювати масив , як очікується в ОП .
John1024,

35

Bash 4.4 представив -dопцію readarray/ mapfile, тому це тепер можна вирішити за допомогою

readarray -d '' array < <(find . -name "$input" -print0)

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

З посібника (без інших варіантів):

mapfile [-d delim] [array]

-d
Перший символ delimвикористовується для закінчення кожного рядка введення, а не нового рядка. Якщо delimце порожній рядок, mapfileзакінчується рядок, коли він читає символ NUL.

І readarrayце просто синонім mapfile.


18

Якщо ви використовуєте bash4 або більш пізню версію, ви можете замінити використання findз

shopt -s globstar nullglob
array=( **/*"$input"* )

**Шаблон включається globstarсірників 0 або більше каталогів, дозволяючи шаблон відповідно до довільної глибиною в поточному каталозі. Без цієї nullglobопції шаблон (після розширення параметрів) трактується буквально, тому без збігів у вас буде масив з одним рядком, а не з порожнім масивом.

Також додайте dotglobопцію до першого рядка, якщо ви хочете обходити приховані каталоги (начебто .ssh) та збігатись із прихованими файлами (як .bashrc).


4
Можливо, nullglobтеж ...
Коджіро

1
Так, я завжди про це забуваю.
Чепнер

5
Зверніть увагу, що це не буде включати приховані файли та каталоги, якщо dotglobце не встановлено (це може бути чи не потрібно, але про це також варто згадати).
gniourf_gniourf

10

ви можете спробувати щось на зразок

array=(`find . -type f | sort -r | head -2`)
, і для того, щоб надрукувати значення масиву, ви можете спробувати щось на зразок echo "${array[*]}"


8
Розриви, якщо є імена файлів із пробілами або символами глобуса.
gniourf_gniourf

1

Наведене нижче працює як для Bash, так і для Z Shell на macOS.

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"

Це працює з файлами, що мають пробіли та іншими спеціальними символами, не вдається у випадку (правда, рідкісного) випадку файлів, що мають розрив рядків у своєму назві. Ви можете створити його для тесту зprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Стефаном Гурішоном

-1

У bash $(<any_shell_cmd>)допомагає запустити команду та захопити результати. Передача цього символу IFSз \nяк роздільник допомагає перетворити це на масив.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")

4
Це отримає лише перший файл результатів findв масив.
Бенджамін В.

-2

Ви можете зробити так:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done

1
Дякую. багато. Але, як зазначив @anishsane, в моїй програмі слід враховувати порожні пробіли в назві файлу. У будь-якому разі Дякую!
Juneyoung Oh

-3

Для мене це добре працювало на cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

Це працює з пробілами, але не з подвійними лапками (") в іменах каталогів (які в будь-якому випадку не дозволені в середовищі Windows).

Остерігайтеся місця в опції -printf.


3
Непрацюючий та небезпечний : не обробляє котирування та підлягає довільному введенню коду. НЕ ВИКОРИСТОВУВАТИ.
gniourf_gniourf

2
Схоже, хтось позначив цю публікацію для видалення. "Це неправильно" не є причиною для видалення на SO. Користувач зробив спробу відповісти, це тема і відповідає критеріям відповідей. Кнопка downvote використовується для вимірювання корисності та правильності, а не кнопка видалення.
Фрамбот

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