Как я могу сохранить результаты команды «найти» в виде массива в Bash

92

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

Июнь молодой О
источник

Ответы:

133

Обновление 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который печатает имена файлов, разделенные нулевым символом. Это не было бы большим неудобством, если бы функции readarray/ mapfileфункции bash поддерживали строки, разделенные нулем, но это не так. Bash 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получать стандартный ввод цикла tmpfile, мы можем заставить его получать свой стандартный ввод из<(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. в .

Джон1024
источник
1
@JuneyoungOh Рад, что это помогло. Добавил раздел подстановки процессов.
John1024
3
@Rockallite Это хорошее наблюдение, но неполное. Хотя это правда, что мы не разбиваем на несколько слов, нам все же нужно IFS=избегать удаления пробелов в начале или в конце строк ввода. Вы можете легко проверить это, сравнив вывод read var <<<' abc '; echo ">$var<"с выводом IFS= read var <<<' abc '; echo ">$var<". В первом случае abcубираются пробелы до и после . В последнем - нет. Имена файлов, которые начинаются или заканчиваются пробелом, могут быть необычными, но если они существуют, мы хотим, чтобы они обрабатывались правильно.
John1024
1
Привет, после того, как я выполню ваш код, я получаю синтаксическую ошибку сообщения возле неожиданного токена <' done <<(find 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'). Как обсуждалось в ответе, этот подход будет работать в ограниченных случаях, но в целом он не будет работать со всеми именами файлов и не создает, как ожидалось OP, массив .
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[@]}"
Sunknudsen
источник
Это работает с файлами, содержащими пробелы и другие специальные символы, не работает с (по общему признанию редким) случаем, когда файлы имеют разрыв строки в своем имени. Вы можете создать его для теста сprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Стефаном Гуришоном
-1

В bash $(<any_shell_cmd>)помогает запустить команду и записать вывод. Передача этого параметра с IFSпомощью \nas delimiter помогает преобразовать это в массив.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
рашок
источник
4
Это приведет к получению только первого файла результатов findв массив.
Benjamin W.
-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
пользователь1357768
источник
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.

R Risack
источник
3
Сломанный и опасный : не обрабатывает кавычки и может быть введен произвольным кодом. НЕ ИСПОЛЬЗУЙТЕ.
gniourf_gniourf
2
Похоже, кто-то отметил это сообщение для удаления. «Это неправильно» не является причиной удаления на SO. Пользователь попытался ответить, он по теме и соответствует критериям для ответов. Кнопка отрицательного голоса используется для измерения полезности и правильности, а не кнопка удаления.
Frambot
3
Как заметил Гниурф, это не для сред, в которых другие вводят параметры в вашу систему, например веб-страниц. Но не все программируют для этой среды. Я использовал его для переименования файлов в каталогах.
R Risack