2009-07-12 9 views
70

find . -print0 Verwendung scheint der einzig sichere Weg zu erhalten, eine Liste von Dateien in bash zu sein aufgrund der Möglichkeit von Dateinamen mit Leerzeichen, Zeilenumbrüchen, Anführungszeichen usw.Erfassung der Ausgabe von find. -print0 in eine bash Array

Aber ich habe Eine harte Zeit, die tatsächlich die Ausgabe von find innerhalb von bash oder mit anderen Befehlszeilenprogrammen nützlich macht. Die einzige Art, wie ich es geschafft haben, die Verwendung der Ausgabe zu machen, ist durch kochend es Perl und das Ändern von Perl IFS auf null:

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

Dieses Beispiel gibt die Anzahl der Dateien gefunden, die Vermeidung der Gefahr von Zeilenumbrüchen in Dateinamen korrumpieren der Graf, wie es auftreten würde mit:

find . | wc -l 

da die meisten Kommandozeilenprogramme, nicht null-separierte Eingang stützen ich das beste, was herauszufinden, die Ausgabe von find . -print0 in einem bash-Array zu erfassen wäre, wie ich es getan habe im Perl-Snippet oben, und dann mit der Aufgabe fortfahren, was auch immer es sein mag.

Wie kann ich das tun?

funktioniert das nicht:

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

Eine viel allgemeinere Frage sein könnte: Wie kann ich mit Listen von Dateien in bash nützliche Dinge tun?

+0

Was meinen Sie durch nützliche Dinge zu tun? –

+4

Oh, weißt du, die üblichen Dinge Arrays sind nützlich für: ihre Größe herauszufinden; Iterieren über ihren Inhalt; sie rückwärts ausdrucken; sortiert sie. Derartiges. Es gibt eine Fülle von Dienstprogrammen in Unix, um diese Dinge mit Daten zu tun: wc, bash's for-Schleifen, tac und sort; Aber all das scheint nutzlos, wenn es um Listen geht, die Leerzeichen oder Zeilenumbrüche enthalten können. I.e. Dateinamen. Piping Daten mit null Wert Eingabefeld-Trennzeichen scheint die Lösung zu sein, aber sehr wenige Dienstprogramme können damit umgehen. – Idris

+1

Hier ist ein Essay über die korrekte Handhabung von Dateinamen in der Shell, mit vielen Besonderheiten: [http://www.dwheeler.com/essays/filenames-in-shell.html](http://www.dwheeler.com/ essays/filenames-in-shell.html) –

Antwort

95

Schamlos gestohlen 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) 

beachte, dass das Umleitungs-Konstrukt hier verwendet (cmd1 < <(cmd2)) ist ähnlich, aber nicht ganz dasselbe wie die üblichere Pipeline (cmd2 | cmd1) - wenn die Befehle Schale builtins sind (z.B. while), die Pipeline-Version führt sie in Subshells aus, und alle Variablen, die sie setzen (z. B. das Array a), gehen beim Beenden verloren. cmd1 < <(cmd2) führt nur cmd2 in einer Subshell aus, sodass das Array über seine Konstruktion hinaus lebt. Achtung: Diese Art der Umleitung ist nur in bash verfügbar, nicht einmal im sh-Emulationsmodus. Sie müssen Ihr Skript mit #!/bin/bash starten.

Auch weil der Dateiverarbeitungsschritt (in diesem Fall nur a[i++]="$file", aber Sie möchten etwas Züchter direkt in der Schleife zu tun) seine Eingabe umgeleitet hat, kann es keine Befehle verwenden, die von stdin lesen könnten. Um diese Einschränkung zu vermeiden, neige ich dazu verwenden:

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) 

..., die die Dateiliste über die Einheit passiert 3, statt stdin.

+0

Ahhh fast da ... das ist die beste Antwort noch. Allerdings habe ich es gerade in einem Verzeichnis versucht, das eine Datei mit einem Newline in seinem Namen enthält, und bei der Überprüfung dieses Elements mit echo $ {a [1]} scheint der Zeilenumbruch zu einem Leerzeichen (0x20) geworden zu sein. Eine Idee, warum das passiert? – Idris

+0

Welche Version von Bash spielst du? Ich hatte Probleme mit älteren Versionen (leider weiß ich nicht genau, welche) nicht mit Zeilenumbrüchen und löscht ('\ 177') in Strings. IIRC, sogar x = "$ y" würde mit diesen Zeichen nicht immer richtig funktionieren. Ich habe gerade mit bash 2.05b.0 und 3.2.17 getestet (die älteste und neueste habe ich praktisch); Beide haben Zeilenumbrüche korrekt behandelt, aber v2.05b.0 hat das Löschzeichen aufgefressen. –

+0

Ich habe es am 3.2.17 auf osx, 3.2.39 auf Linux und 3.2.48 auf netBSD versucht; alle verwandeln Newline in Raum. – Idris

1

Ich denke, elegantere Lösungen existiert, aber ich werde diese in werfen Dies wird auch mit Leerzeichen für Dateinamen arbeiten und/oder Zeilenumbrüche.

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

Sie können dann z Liste der Dateien eines von einem (in diesem Fall in umgekehrter Reihenfolge):

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

This page ein schönes Beispiel gibt, und für mehr sehen Chapter 26 im Advanced Bash-Scripting Guide.

+0

Dieses (und andere ähnliche Beispiele unten) ist fast das, wonach ich suche - aber mit einem großen Problem: Es funktioniert nur für Globs des aktuellen Verzeichnisses. Ich möchte in der Lage sein, völlig willkürliche Listen von Dateien zu manipulieren; zum Beispiel die Ausgabe von "find", die Verzeichnisse rekursiv auflistet, oder irgendeine andere Liste. Was wäre, wenn meine Liste wäre: (/tmp/foo.jpg | /home/alice/bar.jpg |/home/bob/meine Ferien/baz.jpg | /tmp/new\nline/grault.jpg), oder jede andere völlig willkürliche Liste von Dateien (natürlich mit Leerzeichen und Zeilenumbrüchen in ihnen)? – Idris

7

suchen Sie vielleicht für xargs:

find . -print0 | xargs -r0 do_something_useful 

Die Option -L 1 auch für Sie nützlich sein könnten, die xargs exec do_something_useful mit nur 1 Dateiargument macht.

+2

Das ist nicht ganz das, was ich suchte, weil es keine Möglichkeit gibt, Array-ähnliche Dinge mit der Liste zu tun, wie das Sortieren: Sie müssen jedes Element verwenden, wenn es aus dem find-Befehl herauskommt. Wenn Sie dieses Beispiel näher erläutern könnten, mit dem Teil "do_something_useful", der eine Bash-Array-Push-Operation ist, dann könnte es das sein, wonach ich suche. – Idris

+0

Dies ist, was ich am meisten benutze, wenn das, wonach ich suche, einfach genug ist;) –

1

Sie können die Zählung mit diesem sicher tun:

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

(Es druckt eine neue Zeile für jede Datei/Verzeichnis gefunden, und dann die Zeilenumbrüche ausgedruckt zählen ...)

0

Dies ist ähnlich zur Version von Stephan202, aber die Dateien (und Verzeichnisse) werden auf einmal in ein Array gelegt.Die for Schleife ist hier nur um "nützliche Dinge zu tun":

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 

eine Zählung zu erhalten:

echo ${#files[@]} 
3

noch eine weitere Möglichkeit des Zählens Dateien:

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

xargs Vermeiden Sie, wenn Sie kann:

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' 
+0

Warum setzt man IFS auf '\ 777'? – sschober

1

Ich bin neu, aber ich glaube, dass dies eine Antwort ist; hoffe, es hilft jemand:

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 
5

Das Hauptproblem ist, dass das Trennzeichen NUL (\ 0) hier nutzlos ist, weil es nicht möglich ist, IFS einen NUL-Wert zuweisen. Als gute Programmierer achten wir also darauf, dass der Input für unser Programm etwas ist, mit dem es umgehen kann.

Zuerst erstellen wir ein kleines Programm, das diesen Teil für uns tut:

#!/bin/bash 
printf "%s" "[email protected]" | base64 

... und es nennen base64str (nicht vergessen chmod + x)

Zweite wir jetzt nutzen können eine einfache und unkomplizierte for-Schleife:

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

Also der Trick ist, dass ein base64-String kein Zeichen hat, die Probleme für bash verursacht - natürlich eine xxd oder etwas ähnliches kann die Arbeit erledigen auch.

+1

Es muss sichergestellt werden, dass sich der Teil des Dateisystems, der die Verarbeitung durchführt, nicht ändert, wenn die Suche aufgerufen wird, bis das Skript abgeschlossen ist. Wenn dies nicht der Fall ist, wird eine Racebedingung ausgelöst, die ausgenutzt werden kann, um Befehle für die falschen Dateien aufzurufen. Zum Beispiel könnte ein zu löschendes Verzeichnis (zB/tmp/junk) durch einen Symlink nach/home durch einen unprivilegierten Benutzer ersetzt werden. Wenn der Befehl find als root ausgeführt wird und der Befehl find -type d-exec rm -rf '{}' \; lautet, werden alle Benutzerordner der Benutzer gelöscht. – Demi

+1

'read -r -d ''' liest alles bis zum nächsten NUL in '" $ REPLY "'. Es gibt keine Notwendigkeit, sich um "IFS" zu kümmern. –

+1

Siehe auch Eintrag # 1 in http://mywiki.wooledge.org/BashPitfalls –

-1

Bash war noch nie gut in der Handhabung von Dateinamen (oder wirklich jedem Text), weil es Leerzeichen als Listenbegrenzer verwendet.

Ich würde stattdessen Python mit der Bibliothek sh empfehlen.

+3

Sie sind einfach völlig falsch mit Bash. –

+1

Und doch ist er nicht. –

0

Alte Frage, aber niemand schlug diese einfache Methode vor, also dachte ich, ich würde. Zugegeben, wenn Ihre Dateinamen ein ETX haben, löst das Ihr Problem nicht, aber ich vermute, dass es für jedes reale Szenario dient.Der Versuch, NULL zu verwenden, scheint mit den IFS-Standardregeln zu kollidieren. Mit Suchoptionen und Fehlerbehandlung nach deinem Geschmack abstimmen.

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

Was bedeutet ** ETX **? Vielleicht Dateiname ** EXT ** ension oder vielleicht [** Ende des Textes **] (http://www.abcurriations.com/ETX) ... – olibre

0

Gordon Davissons Antwort ist großartig für bash. Doch eine nützliche Abkürzung für zsh Benutzer vorhanden sind:

Zuerst legen Sie String in einer Variablen:

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

Als nächstes geteilt diese Variable und speichern sie in einem Array:

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

Es gibt ein Trick: ^@ ist das NUL-Zeichen. Um dies zu tun, müssen Sie Strg + V gefolgt von Strg + @ eingeben.

Sie können jeden Eintrag von $ B überprüfen enthält richtigen Wert:

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

Sorgfältige Leser diesen Anruf bemerken kann find Befehl in den meisten Fällen vermieden werden kann ** Syntax. Zum Beispiel:

B=(/tmp/**) 
0

Seit Bash 4.4, hat die eingebauten mapfile die -d Schalter (ein Trennzeichen, ähnlich den -d Schalter der read Anweisung angeben), und der Begrenzer kann der Null-Byte sein. Daher ist eine nette Antwort auf die Frage im Titel

Capturing Ausgabe von find . -print0 in einen Bash-Array

ist:

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