Wie kann ich die Ergebnisse des Befehls "find" als Array in Bash speichern?

92

Ich versuche, das Ergebnis findals Arrays zu speichern . Hier ist mein Code:

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

Ich erhalte 2 TXT-Dateien im aktuellen Verzeichnis. Also erwarte ich '2' als Ergebnis von ${len}. Es wird jedoch 1 gedruckt. Der Grund dafür ist, dass alle Ergebnisse findals ein Element betrachtet werden. Wie kann ich das beheben?

PS
Ich habe auf StackOverFlow mehrere Lösungen für ein ähnliches Problem gefunden. Sie sind jedoch etwas anders, so dass ich mich in meinem Fall nicht bewerben kann. Ich muss die Ergebnisse in einer Variablen vor der Schleife speichern. Danke noch einmal.

Juneyoung Oh
quelle

Antworten:

133

Update 2020 für Linux-Benutzer:

Wenn Sie eine aktuelle Version von bash (4.4-alpha oder besser) haben, wie Sie es wahrscheinlich unter Linux tun, sollten Sie die Antwort von Benjamin W. verwenden .

Wenn Sie unter Mac OS arbeiten, das - wie ich zuletzt überprüft habe - immer noch Bash 3.2 verwendet oder anderweitig ein älteres Bash verwendet, fahren Sie mit dem nächsten Abschnitt fort.

Antwort für Bash 4.3 oder früher

Hier ist eine Lösung, um die Ausgabe von findin ein bashArray zu bekommen:

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

Dies ist schwierig, da Dateinamen im Allgemeinen Leerzeichen, neue Zeilen und andere skriptfeindliche Zeichen enthalten können. Die einzige Möglichkeit, finddie Dateinamen zu verwenden und sicher voneinander zu trennen, besteht darin, -print0die mit einem Nullzeichen getrennten Dateinamen zu drucken. Dies würde nicht viel von einem Nachteil sein , wenn bash readarray/ mapfileFunktionen null getrennte Zeichenketten unterstützt , aber sie tun es nicht. Bash's readtut es und das führt uns zu der Schleife oben.

[Diese Antwort wurde ursprünglich im Jahr 2014 geschrieben. Wenn Sie eine aktuelle Version von Bash haben, lesen Sie bitte das Update unten.]

Wie es funktioniert

  1. Die erste Zeile erstellt ein leeres Array: array=()

  2. Jedes Mal, wenn die readAnweisung ausgeführt wird, wird ein durch Null getrennter Dateiname aus der Standardeingabe gelesen. Die -rOption weist readan, Backslash-Zeichen in Ruhe zu lassen. Das -d $'\0'teilt mit, readdass die Eingabe durch Null getrennt wird. Da wir den Namen auf weglassen, readsetzt die Shell die Eingabe in den Standardnamen : REPLY.

  3. Die array+=("$REPLY")Anweisung hängt den neuen Dateinamen an das Array an array.

  4. Die letzte Zeile kombiniert Umleitung und Befehlssubstitution, um die Ausgabe findan die Standardeingabe der whileSchleife bereitzustellen .

Warum Prozesssubstitution verwenden?

Wenn wir keine Prozessersetzung verwenden würden, könnte die Schleife wie folgt geschrieben werden:

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

Oben wird die Ausgabe von findin einer temporären Datei gespeichert und diese Datei wird als Standardeingabe für die while-Schleife verwendet. Die Idee der Prozessersetzung besteht darin, solche temporären Dateien unnötig zu machen. Anstatt dass die whileSchleife ihren Standard erhält tmpfile, können wir sie auch ihren Standard erhalten lassen <(find . -name ${input} -print0).

Prozesssubstitution ist weithin nützlich. An vielen Stellen, an denen ein Befehl aus einer Datei lesen möchte , können Sie <(...)anstelle eines Dateinamens die Prozessersetzung angeben . Es gibt eine analoge Form, >(...)die anstelle eines Dateinamens verwendet werden kann, in den der Befehl in die Datei schreiben möchte .

Wie Arrays ist die Prozessersetzung ein Merkmal von Bash und anderen fortgeschrittenen Shells. Es ist nicht Teil des POSIX-Standards.

Alternative: Lastpipe

Falls gewünscht, lastpipekann anstelle der Prozesssubstitution verwendet werden (Hutspitze: Caesar ):

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 lastpipeweist bash an, den letzten Befehl in der Pipeline in der aktuellen Shell auszuführen (nicht den Hintergrund). Auf diese Weise arraybleibt das nach Abschluss der Pipeline bestehen. Da lastpipedie Jobsteuerung nur wirksam wird, werden sie ausgeführt set +m. (In einem Skript ist die Jobsteuerung im Gegensatz zur Befehlszeile standardmäßig deaktiviert.)

Zusätzliche Bemerkungen

Der folgende Befehl erstellt eine Shell-Variable, kein Shell-Array:

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

Wenn Sie ein Array erstellen möchten, müssen Sie Parens um die Ausgabe von find setzen. Naiv könnte man also:

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

Das Problem ist, dass die Shell eine Wortaufteilung für die Ergebnisse von durchführt, findsodass nicht garantiert wird, dass die Elemente des Arrays Ihren Wünschen entsprechen.

Update 2019

Ab Version 4.4-alpha unterstützt bash jetzt eine -dOption, sodass die obige Schleife nicht mehr erforderlich ist. Stattdessen kann man verwenden:

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

Weitere Informationen hierzu finden Sie (und upvote) Benjamin W. Antwort .

John1024
quelle
1
@JuneyoungOh Ich bin froh, dass es geholfen hat. Ich habe einen Abschnitt zur Prozessersetzung hinzugefügt.
John1024
3
@ Rockallite Das ist eine gute Beobachtung, aber unvollständig. Es stimmt zwar, dass wir nicht in mehrere Wörter aufteilen, aber wir müssen dennoch IFS=vermeiden, Leerzeichen von den Anfängen oder Enden der Eingabezeilen zu entfernen. Sie können dies einfach testen, indem Sie die Ausgabe von read var <<<' abc '; echo ">$var<"mit der Ausgabe von vergleichen IFS= read var <<<' abc '; echo ">$var<". Im ersteren Fall werden die Leerzeichen davor und danach abcentfernt. In letzterem sind sie nicht. Dateinamen, die mit Leerzeichen beginnen oder enden, sind möglicherweise ungewöhnlich, aber wenn sie vorhanden sind, möchten wir, dass sie korrekt verarbeitet werden.
John1024
1
Hallo, nachdem ich Ihren Code ausgeführt habe, erhalte ich einen Nachrichtensyntaxfehler in der Nähe eines unerwarteten Tokens. <' << (Finden Sie aaa / -not -newermt "$ last_build_timestamp_v" -Typ f -print0) '
Przemysław Sienkiewicz
1
Ein Hinweis: der einfachere ''kann verwendet werden anstelle von $'\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 Ich gehe davon aus, dass du schreiben wolltest BLAH=$(find . -name '*.php'). Wie in der Antwort erläutert, funktioniert dieser Ansatz in begrenzten Fällen, funktioniert jedoch im Allgemeinen nicht mit allen Dateinamen und erzeugt nicht wie erwartet ein Array .
John1024
35

Bash 4.4 hat eine -dOption für readarray/ eingeführt mapfile, mit der dies nun gelöst werden kann

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

für eine Methode, die mit beliebigen Dateinamen wie Leerzeichen, Zeilenumbrüchen und Globbing-Zeichen arbeitet. Dies erfordert, dass Ihre findUnterstützung -print0, wie zum Beispiel GNU find.

Aus dem Handbuch (ohne andere Optionen):

mapfile [-d delim] [array]

-d
Das erste Zeichen von delimwird verwendet, um jede Eingabezeile und nicht die neue Zeile zu beenden. Wenn delimdie leere Zeichenfolge ist, mapfilewird eine Zeile beendet, wenn ein NUL-Zeichen gelesen wird.

Und readarrayist nur ein Synonym für mapfile.

Benjamin W.
quelle
18

Wenn Sie bash4 oder höher verwenden, können Sie Ihre Verwendung von finddurch ersetzen

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

Das **durch aktivierte Muster globstarstimmt mit 0 oder mehr Verzeichnissen überein, sodass das Muster mit einer beliebigen Tiefe im aktuellen Verzeichnis übereinstimmen kann. Ohne die nullglobOption wird das Muster (nach der Parametererweiterung) wörtlich behandelt, sodass Sie ohne Übereinstimmungen ein Array mit einer einzelnen Zeichenfolge anstelle eines leeren Arrays hätten.

Fügen Sie die dotglobOption auch in die erste Zeile ein, wenn Sie versteckte Verzeichnisse (wie .ssh) durchsuchen und auch versteckte Dateien (wie .bashrc) abgleichen möchten .

chepner
quelle
4
Vielleicht nullglobauch ...
Kojiro
1
Ja, das vergesse ich immer.
Chepner
5
Beachten Sie, dass dies nicht die versteckten Dateien und Verzeichnisse einschließt, es dotglobsei denn, dies ist festgelegt (dies kann erwünscht sein oder nicht, aber es ist auch erwähnenswert).
gniourf_gniourf
10

Sie können so etwas versuchen

array=(`find . -type f | sort -r | head -2`)
und um die Array-Werte zu drucken, können Sie so etwas wie Echo ausprobieren "${array[*]}"

Ahmed Al-Haffar
quelle
7
Bricht ab, wenn Dateinamen mit Leerzeichen oder Glob-Zeichen vorhanden sind.
gniourf_gniourf
0

Das Folgende scheint sowohl für Bash als auch für Z Shell unter macOS zu funktionieren.

#! /bin/sh

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

printf "%s\n" "${paths[@]}"
sunknudsen
quelle
-1

$(<any_shell_cmd>)Hilft in bash, einen Befehl auszuführen und die Ausgabe zu erfassen. Wenn Sie dies als Trennzeichen an IFSwith übergeben \n, können Sie dies in ein Array konvertieren.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
Rashok
quelle
3
Dadurch wird nur die erste Datei der Ergebnisse von findin das Array aufgenommen.
Benjamin W.
-2

Das könnte man so machen:

#!/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
quelle
Vielen Dank. viel. Aber wie @anishsane zeigte, sollten leere Stellen im Dateinamen in meinem Programm berücksichtigt werden. Trotzdem danke!
Juneyoung Oh
-3

Für mich hat das bei Cygwin gut funktioniert:

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

Dies funktioniert mit Leerzeichen, jedoch nicht mit doppelten Anführungszeichen (") in den Verzeichnisnamen (die in einer Windows-Umgebung sowieso nicht zulässig sind).

Beachten Sie den Platz in der Option -printf.

R Risack
quelle
3
Defekt und gefährlich : Behandelt keine Anführungszeichen und unterliegt einer willkürlichen Code-Injektion. VERWENDE NICHT.
gniourf_gniourf
2
Es sieht so aus, als hätte jemand diesen Beitrag zum Löschen markiert. "Es ist falsch" ist kein Grund zum Löschen auf SO. Der Benutzer hat versucht zu antworten, es handelt sich um ein Thema und erfüllt die Kriterien für Antworten. Die Schaltfläche zum Herunterstimmen wird verwendet, um die Nützlichkeit und Richtigkeit zu messen, nicht die Schaltfläche zum Löschen.
Frambot
3
Wie gniourf hervorhob, ist dies nicht für Umgebungen gedacht, in denen andere die Optionen auf Ihrem System eingeben, z. B. Webseiten. Aber nicht jeder programmiert für diese Umgebung. Ich habe es zum Umbenennen von Dateien in Verzeichnissen verwendet.
R Risack