Erfassen der Ausgabe von find. -print0 in ein Bash-Array


76

Die Verwendung find . -print0scheint der einzig sichere Weg zu sein, um eine Liste der Dateien in Bash zu erhalten, da Dateinamen Leerzeichen, Zeilenumbrüche, Anführungszeichen usw. enthalten können.

Es fällt mir jedoch schwer, die Ausgabe von find innerhalb von Bash oder mit anderen Befehlszeilenprogrammen nützlich zu machen. Die einzige Möglichkeit, die Ausgabe zu nutzen, besteht darin, sie an Perl weiterzuleiten und das IFS von Perl in Null zu ändern:

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

In diesem Beispiel wird die Anzahl der gefundenen Dateien gedruckt, um die Gefahr zu vermeiden, dass Zeilenumbrüche in Dateinamen die Anzahl beschädigen, wie dies bei folgenden Fällen der Fall wäre:

find . | wc -l

Da die meisten Befehlszeilenprogramme keine durch Nullen getrennten Eingaben unterstützen, ist es meines Erachtens am besten, die Ausgabe find . -print0in einem Bash-Array zu erfassen , wie ich es im obigen Perl-Snippet getan habe, und dann mit der Aufgabe fortzufahren, wie auch immer Sein.

Wie kann ich das machen?

Das funktioniert nicht:

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

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


Was meinst du mit nützlichen Dingen?
Balázs Pozsár

4
Oh, wissen Sie, die üblichen Dinge, für die Arrays nützlich sind: Finden ihrer Größe; Iterieren über ihren Inhalt; sie rückwärts ausdrucken; sie sortieren. Derartiges. Unter Unix gibt es eine Vielzahl von Dienstprogrammen, mit denen Sie diese Dinge mit Daten ausführen können: wc, bashs for-Schleifen, tac und sort; Aber all dies scheint nutzlos zu sein, wenn es um Listen geht, die Leerzeichen oder Zeilenumbrüche enthalten können. Dh Dateinamen. Das Weiterleiten von Daten mit Eingabefeldern mit Nullwerten scheint die Lösung zu sein, aber nur sehr wenige Dienstprogramme können damit umgehen.
Idris

1
Hier ist ein Aufsatz über den richtigen Umgang mit Dateinamen in der Shell mit vielen
David A. Wheeler

Antworten:


103

Schamlos aus Gregs BashFAQ gestohlen :

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)

Beachten Sie, dass das hier verwendete Umleitungskonstrukt ( cmd1 < <(cmd2)) dem der üblichen Pipeline ( cmd2 | cmd1) ähnelt, jedoch nicht ganz dem entspricht. Wenn es sich bei den Befehlen um Shell-Builds handelt (z. B. while), werden sie von der Pipeline-Version in Subshells und allen von ihnen festgelegten Variablen ausgeführt (zB das Array a) gehen beim Beenden verloren. cmd1 < <(cmd2)Läuft nur cmd2 in einer Subshell, sodass das Array über seine Konstruktion hinaus lebt. Warnung: Diese Form der Umleitung ist nur in Bash verfügbar, nicht einmal in Bash im Sh-Emulationsmodus. Sie müssen Ihr Skript mit beginnen #!/bin/bash.

Da die a[i++]="$file"Eingabe des Dateiverarbeitungsschritts (in diesem Fall nur , aber Sie möchten möglicherweise etwas ausgefalleneres direkt in der Schleife ausführen) umgeleitet wird, kann er keine Befehle verwenden, die möglicherweise von stdin lesen. Um diese Einschränkung zu vermeiden, verwende ich normalerweise:

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 Einheit 3 ​​und nicht über stdin übergibt.


Ahhh fast da ... das ist die bisher beste Antwort. Ich habe es jedoch gerade in einem Verzeichnis versucht, das eine Datei mit einem Zeilenumbruch enthält. Bei der Überprüfung dieses Elements mit echo $ {a [1]} scheint der Zeilenumbruch zu einem Leerzeichen (0x20) geworden zu sein. Irgendeine Idee, warum das passiert?
Idris

Welche Version von Bash führen Sie aus? Ich hatte Probleme mit älteren Versionen (leider weiß ich nicht genau, welche), die sich nicht mit Zeilenumbrüchen und Löschungen ( \177) in Zeichenfolgen befassten . 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 (das älteste und neueste, das ich zur Hand habe); Beide behandelten Zeilenumbrüche ordnungsgemäß, aber v2.05b.0 aß das Löschzeichen.
Gordon Davisson

Ich habe es unter 3.2.17 unter OSX, 3.2.39 unter Linux und 3.2.48 unter NetBSD versucht. Alle verwandeln Newline in Space.
Idris

12
-d ''ist äquivalent zu -d $'\0'.
10b0

15
Eine einfachere Möglichkeit, ein Element am Ende eines Arrays hinzuzufügen, ist:arr+=("$file")
dogbane

7

Vielleicht suchen Sie nach xargs:

find . -print0 | xargs -r0 do_something_useful

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


2
Dies ist nicht ganz das, wonach ich gesucht habe, da es keine Möglichkeit gibt, Array-ähnliche Dinge mit der Liste zu tun, z. B. Sortieren: Sie müssen jedes Element so verwenden, wie es außerhalb des Befehls find angezeigt wird. Wenn Sie dieses Beispiel näher erläutern könnten, wobei der Teil "do_something_useful" eine Bash-Array-Push-Operation ist, dann könnte dies das sein, wonach ich suche.
Idris

5

Das Hauptproblem ist, dass das Trennzeichen NUL (\ 0) hier unbrauchbar ist, da es nicht möglich ist, IFS einen NUL-Wert zuzuweisen. Als gute Programmierer achten wir also darauf, dass die Eingabe für unser Programm etwas ist, das es verarbeiten kann.

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

#!/bin/bash
printf "%s" "$@" | base64

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

Zweitens können wir jetzt eine einfache und unkomplizierte for-Schleife verwenden:

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

Der Trick ist also, dass ein base64-String kein Vorzeichen hat, was Probleme für bash verursacht - natürlich kann auch ein xxd oder ähnliches die Arbeit erledigen.


1
Es muss sichergestellt werden, dass sich der Teil des Dateisystems, den find verarbeitet, vom Aufruf von find bis zum Abschluss des Skripts nicht ändert. Ist dies nicht der Fall, ergibt sich eine Racebedingung, die ausgenutzt werden kann, um Befehle für die falschen Dateien aufzurufen. Zum Beispiel könnte ein zu löschendes Verzeichnis (z. B. / tmp / junk) durch einen Symlink zu / home eines nicht konkurrenzfähigen Benutzers ersetzt werden. Wenn der Befehl find als root ausgeführt wurde und find -type d -exec rm -rf '{}' \; war, wurden die Home-Ordner aller Benutzer gelöscht.
Demi

2
read -r -d ''werde alles bis zum nächsten NUL in lesen "$REPLY". Es besteht kein Grund zur Sorge IFS.
Charles Duffy

5

Seit Bash 4.4 hat das eingebaute mapfileden -dSchalter (um ein Trennzeichen anzugeben, ähnlich dem -dSchalter der readAnweisung), und das Trennzeichen kann das Nullbyte sein. Daher eine schöne Antwort auf die Frage im Titel

Erfassen der Ausgabe von find . -print0in einem Bash-Array

ist:

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

4

Noch eine andere Art, Dateien zu zählen:

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

2

Damit können Sie sicher zählen:

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

(Es druckt eine neue Zeile für jede gefundene Datei / jedes gefundene Verzeichnis und zählt dann die ausgedruckten Zeilenumbrüche ...)


Es ist viel schneller, die -printfOption anstelle -execfür jede Datei zu verwenden:find . -printf "\n" | wc -l
Oliver I

1

Ich denke, es gibt elegantere Lösungen, aber ich werde diese einwerfen. Dies funktioniert auch für Dateinamen mit Leerzeichen und / oder Zeilenumbrüchen:

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

Sie können dann zB die Dateien einzeln auflisten (in diesem Fall in umgekehrter Reihenfolge):

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

Diese Seite enthält ein schönes Beispiel. Weitere Informationen finden Sie in Kapitel 26 im Advanced Bash-Scripting Guide .


Dies (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 beliebige Dateilisten zu manipulieren. die Ausgabe von "find" zum Beispiel, die Verzeichnisse rekursiv auflistet, oder jede andere Liste. Was wäre, wenn meine Liste wäre: (/tmp/foo.jpg | /home/alice/bar.jpg | / home / bob / mein Urlaub / baz.jpg | /tmp/new\nline/grault.jpg) oder eine andere völlig willkürliche Liste von Dateien (natürlich möglicherweise mit Leerzeichen und Zeilenumbrüchen)?
Idris

1

Vermeiden Sie Xargs, wenn Sie können:

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' 

Warum setzen Sie IFS auf \777?
7.

1

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

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

0

Dies ähnelt der Version von Stephan202, aber die Dateien (und Verzeichnisse) werden auf einmal in einem Array abgelegt. Die forSchleife hier ist 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

Um eine Zählung zu erhalten:

echo ${#files[@]}

0

Alte Frage, aber niemand schlug diese einfache Methode vor, also dachte ich, ich würde es tun. Zugegeben, wenn Ihre Dateinamen eine ETX haben, löst dies Ihr Problem nicht, aber ich vermute, dass es für jedes reale Szenario geeignet ist. Der Versuch, null zu verwenden, scheint gegen die Standardregeln für die IFS-Behandlung zu verstoßen. Würzen Sie nach Ihrem Geschmack mit Suchoptionen und Fehlerbehandlung.

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

1
Was bedeutet ETX ? Vielleicht Dateiname EXT ension oder vielleicht Ende des Textes ...
oHo

0

Gordon Davissons Antwort ist großartig für Bash. Für zsh-Benutzer gibt es jedoch eine nützliche Verknüpfung:

Platzieren Sie zuerst Ihren String in einer Variablen:

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

Teilen Sie diese Variable als Nächstes auf und speichern Sie sie in einem Array:

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

Es gibt einen Trick: ^@ist der NUL-Charakter. Dazu müssen Sie Strg + V gefolgt von Strg + @ eingeben.

Sie können überprüfen, ob jeder Eintrag von $ B den richtigen Wert enthält:

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

Sorgfältige Leser können feststellen, dass der Aufruf eines findBefehls in den meisten Fällen mithilfe der **Syntax vermieden werden kann . Zum Beispiel:

B=( /tmp/** )

-1

Bash war noch nie gut im Umgang mit Dateinamen (oder wirklich jedem Text), da Leerzeichen als Listenbegrenzer verwendet werden.

Ich würde empfehlen, stattdessen Python mit der sh- Bibliothek zu verwenden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.