Jak mogę zapisać wyniki polecenia „znajdź” jako tablicę w Bash

92

Próbuję zapisać wynik z findjako tablice. Oto mój kod:

#!/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

Otrzymuję 2 pliki .txt w bieżącym katalogu. Spodziewam się więc „2” w wyniku ${len}. Jednak wypisuje 1. Powodem jest to, że przyjmuje wszystkie wyniki findjako jeden element. Jak mogę to naprawić?

PS
Znalazłem kilka rozwiązań na StackOverFlow dotyczących podobnego problemu. Jednak są one trochę inne, więc nie mogę złożyć wniosku w moim przypadku. Muszę zapisać wyniki w zmiennej przed pętlą. Dzięki jeszcze raz.

Juneyoung Oh
źródło

Odpowiedzi:

133

Aktualizacja 2020 dla użytkowników Linuksa:

Jeśli masz wersję up-to-date bash (4,4-alfa lub lepsza), jak zapewne zrobić, jeśli jesteś na Linuksie, to powinieneś być w użyciu odpowiedź Benjamin W. .

Jeśli korzystasz z Mac OS, który - ostatnio, jak sprawdziłem - nadal używa basha 3.2 lub w inny sposób używasz starszego basha, przejdź do następnej sekcji.

Odpowiedź dla basha 4.3 lub wcześniejszego

Oto jedno rozwiązanie dla uzyskania wyjścia finddo bashtablicy:

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

Jest to trudne, ponieważ ogólnie nazwy plików mogą zawierać spacje, nowe wiersze i inne znaki wrogie dla skryptów. Jedynym sposobem bezpiecznego używania findi oddzielania nazw plików od siebie jest użycie polecenia, -print0które wypisuje nazwy plików oddzielone znakiem null. Nie stanowiłoby to dużej niedogodności, gdyby funkcje readarray/ basha mapfileobsługiwały ciągi znaków oddzielone znakiem null, ale tak nie jest. Bash readrobi i to prowadzi nas do powyższej pętli.

[Ta odpowiedź została pierwotnie napisana w 2014 r. Jeśli masz najnowszą wersję basha, zapoznaj się z aktualizacją poniżej.]

Jak to działa

  1. Pierwsza linia tworzy pustą tablicę: array=()

  2. Za każdym razem, gdy readwykonywana jest instrukcja, ze standardowego wejścia odczytywana jest nazwa pliku oddzielona znakiem null. -rOpcja nakazuje read, aby zostawić znaki BACKSLASH. -d $'\0'Mówi read, że wkład będzie zerowa rozdzielono. Ponieważ pomijamy nazwę na read, powłoka daje wejście do domyślnej nazwy: REPLY.

  3. array+=("$REPLY")Oświadczenie dołącza nową nazwę pliku do tablicy array.

  4. Ostatnia linia łączy przekierowanie i podstawianie poleceń, aby zapewnić wyjście findna standardowe wejście whilepętli.

Dlaczego warto korzystać z zastępowania procesów?

Gdybyśmy nie używali podstawiania procesów, pętla mogłaby zostać zapisana jako:

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

W powyższym przykładzie dane wyjściowe programu findsą przechowywane w pliku tymczasowym i ten plik jest używany jako standardowe wejście do pętli while. Ideą podstawiania procesów jest uczynienie takich plików tymczasowych niepotrzebnymi. Więc zamiast whilepobierać stdin z pętli tmpfile, możemy sprawić, by pobierała stdin z <(find . -name ${input} -print0).

Zastępowanie procesów jest bardzo przydatne. W wielu miejscach, w których polecenie chce czytać z pliku <(...), zamiast nazwy pliku można określić podstawianie procesu . Istnieje analogiczna forma, >(...)której można użyć zamiast nazwy pliku, w którym polecenie chce zapisać do pliku.

Podobnie jak w przypadku tablic, podstawianie procesów jest funkcją powłoki bash i innych zaawansowanych powłok. Nie jest częścią standardu POSIX.

Alternatywa: lastpipe

W razie potrzeby lastpipemożna użyć zamiast zastępowania procesu (końcówka kapelusza: Cezar ):

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 lastpipemówi bashowi, aby uruchomił ostatnie polecenie w potoku w bieżącej powłoce (nie w tle). W ten sposób arraypozostaje po zakończeniu rurociągu. Ponieważ lastpipedziała tylko wtedy, gdy kontrola zadań jest wyłączona, uruchamiamy set +m. (W skrypcie, w przeciwieństwie do wiersza poleceń, kontrola zadań jest domyślnie wyłączona).

Dodatkowe uwagi

Następujące polecenie tworzy zmienną powłoki, a nie tablicę powłoki:

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

Jeśli chciałbyś stworzyć tablicę, musiałbyś umieścić parens wokół wyniku find. Można więc naiwnie:

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

Problem polega na tym, że powłoka dokonuje podziału na słowa na wynikach programu, findtak że elementy tablicy nie są gwarantowane, że będą zgodne z oczekiwaniami.

Zaktualizuj 2019

Począwszy od wersji 4.4-alpha, bash obsługuje teraz -dopcję, dzięki czemu powyższa pętla nie jest już potrzebna. Zamiast tego można użyć:

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

Aby uzyskać więcej informacji na ten temat można znaleźć (i upvote) odpowiedź Benjamin W. .

John1024
źródło
1
@JuneyoungOh Cieszę się, że pomogło. Dodałem sekcję o zastępowaniu procesów.
John1024,
3
@Rockallite To dobra obserwacja, ale niekompletna. Chociaż prawdą jest, że nie dzielimy się na wiele słów, nadal musimy IFS=unikać usuwania białych znaków na początku lub na końcu linii wejściowych. Możesz to łatwo przetestować, porównując dane wyjściowe programu read var <<<' abc '; echo ">$var<"z danymi wyjściowymi programu IFS= read var <<<' abc '; echo ">$var<". W pierwszym przypadku spacje przed i po abcsą usuwane. W tym drugim przypadku tak nie jest. Nazwy plików, które zaczynają się lub kończą białymi znakami, mogą być nietypowe, ale jeśli istnieją, chcemy, aby były poprawnie przetwarzane.
John1024,
1
Cześć, po wykonaniu kodu otrzymuję błąd składni wiadomości w pobliżu nieoczekiwanego tokena, <' wykonano <<(find aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz
1
Uwaga: prostszego ''można użyć zamiast $'\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 Zakładam, że zamierzałeś pisać BLAH=$(find . -name '*.php'). Jak omówiono w odpowiedzi, to podejście będzie działać w ograniczonych przypadkach, ale ogólnie nie będzie działać ze wszystkimi nazwami plików i nie tworzy, zgodnie z oczekiwaniami OP, tablicy .
John1024,
35

Bash 4.4 wprowadził -dopcję readarray/ mapfile, więc można to teraz rozwiązać za pomocą

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

dla metody, która działa z dowolnymi nazwami plików, w tym spacjami, znakami nowej linii i znakami globowania. Wymaga to twojego findwsparcia -print0, jak na przykład GNU find.

Z instrukcji (pomijając inne opcje):

mapfile [-d delim] [array]

-d
Pierwszy znak delimjest używany do zakończenia każdego wiersza wejściowego, a nie do nowej linii. Jeśli delimjest pustym łańcuchem, mapfilezakończy wiersz, gdy odczyta znak NUL.

I readarrayto tylko synonim mapfile.

Benjamin W.
źródło
18

Jeśli używasz wersji bash4 lub nowszej, możesz zastąpić korzystanie findz

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

**Wzór włączona globstarmeczów 0 lub więcej katalogów, dzięki czemu wzór pasuje do dowolnej głębokości w bieżącym katalogu. Bez tej nullglobopcji wzorzec (po rozwinięciu parametrów) jest traktowany dosłownie, więc bez dopasowań powstałaby tablica z pojedynczym ciągiem zamiast pustej tablicy.

Dodaj tę dotglobopcję również do pierwszego wiersza, jeśli chcesz przechodzić przez ukryte katalogi (takie jak .ssh) i dopasowywać również ukryte pliki (takie jak .bashrc).

Chepner
źródło
4
Może nullglobteż…
kojiro
1
Tak, zawsze o tym zapominam.
chepner
5
Zauważ, że nie obejmie to ukrytych plików i katalogów, chyba że dotglobjest ustawione (może to być pożądane lub nie, ale warto o tym wspomnieć).
gniourf_gniourf
10

możesz spróbować czegoś takiego

array=(`find . -type f | sort -r | head -2`)
, a żeby wydrukować wartości tablic, możesz spróbować czegoś takiego jak echo "${array[*]}"

Ahmed Al-Haffar
źródło
7
Przerywa, jeśli istnieją nazwy plików ze spacjami lub znakami globu.
gniourf_gniourf,
1

Wydaje się, że poniższe działa zarówno dla Bash, jak i Z Shell w systemie MacOS.

#! /bin/sh

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

printf "%s\n" "${paths[@]}"
sunknudsen
źródło
-1

W bashu $(<any_shell_cmd>)pomaga uruchomić polecenie i przechwycić dane wyjściowe. Przekazanie tego do za IFSpomocą \njako separatora pomaga przekonwertować to na tablicę.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
rashok
źródło
3
Spowoduje to pobranie tylko pierwszego pliku wyników finddo tablicy.
Benjamin W.
-2

Możesz zrobić tak:

#!/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
user1357768
źródło
Dzięki. dużo. Ale jak wskazał @anishsane, w moim programie powinny być uwzględniane puste spacje w nazwie pliku. W każdym razie dzięki!
Juneyoung Oh
-3

Dla mnie to działało dobrze na cygwin:

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

Działa to ze spacjami, ale nie z podwójnymi cudzysłowami (") w nazwach katalogów (które i tak są niedozwolone w środowisku Windows).

Uważaj na spację w opcji -printf.

R Risack
źródło
3
Zepsuty i niebezpieczny : nie obsługuje cudzysłowów i podlega wstrzyknięciu dowolnego kodu. NIE UŻYWAJ.
gniourf_gniourf,
2
Wygląda na to, że ktoś oznaczył ten post do usunięcia. „To źle” nie jest powodem do usunięcia w SO. Użytkownik podjął próbę odpowiedzi, jest na temat i spełnia kryteria odpowiedzi. Do pomiaru przydatności i poprawności służy przycisk „przeciw”, a nie przycisk usuwania.
Frambot
3
Jak zauważył gniourf, to nie jest dla środowisk, w których inni wprowadzają opcje w twoim systemie, np. Strony internetowe. Ale nie wszyscy programują dla tego środowiska. Użyłem go do zmiany nazw plików w katalogach.
R Risack