Durchlaufen Sie eine Liste von Dateien mit Leerzeichen

201

Ich möchte eine Liste von Dateien durchlaufen. Diese Liste ist das Ergebnis eines findBefehls, daher habe ich Folgendes gefunden:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Es ist in Ordnung, außer wenn eine Datei Leerzeichen in ihrem Namen hat:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

Was kann ich tun, um die Aufteilung auf Leerzeichen zu vermeiden?

gregseth
quelle
Dies ist im Grunde ein spezifischer Unterfall von Wann werden Anführungszeichen um eine Shell-Variable
Tripleee

Antworten:

253

Sie können die wortbasierte Iteration durch eine zeilenbasierte ersetzen:

find . -iname "foo*" | while read f
do
    # ... loop body
done
Martin Clayton
quelle
31
Das ist extrem sauber. Und ich fühle mich besser, als IFS in Verbindung mit einer for-Schleife zu ändern
Derrick
15
Dadurch wird ein einzelner Dateipfad aufgeteilt, der ein \ n enthält. OK, die sollten nicht da sein, aber sie können erstellt werden:touch "$(printf "foo\nbar")"
Ollie Saunders
4
Verwenden Sie IFS= while read -r fstattdessen, um eine Interpretation der Eingabe (Backslashes, führende und nachfolgende Leerzeichen) zu verhindern.
mklement0
2
Diese Antwort zeigt eine sicherere Kombination aus findund einer while-Schleife.
Moi
5
Scheint, als würde man auf das Offensichtliche hinweisen, aber in fast allen einfachen Fällen -execwird es sauberer sein als eine explizite Schleife : find . -iname "foo*" -exec echo "File found: {}" \;. Außerdem können Sie in vielen Fällen den letzten \;durch ersetzen, +um viele Dateien in einem Befehl abzulegen.
naught101
152

Es gibt verschiedene praktikable Möglichkeiten, um dies zu erreichen.

Wenn Sie sich eng an Ihre Originalversion halten möchten, können Sie dies folgendermaßen tun:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Dies schlägt immer noch fehl, wenn Dateinamen wörtliche Zeilenumbrüche enthalten, Leerzeichen diese jedoch nicht beschädigen.

Es ist jedoch nicht erforderlich, mit IFS zu spielen. Hier ist meine bevorzugte Methode, um dies zu tun:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Wenn Sie die < <(command)Syntax nicht kennen, sollten Sie sich über die Prozessersetzung informieren . Dies hat den Vorteil, for file in $(find ...)dass Dateien mit Leerzeichen, Zeilenumbrüchen und anderen Zeichen korrekt behandelt werden. Dies funktioniert, weil findwith -print0ein null(aka \0) als Abschlusszeichen für jeden Dateinamen verwendet und im Gegensatz zu newline null kein zulässiges Zeichen in einem Dateinamen ist.

Der Vorteil gegenüber der nahezu gleichwertigen Version

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

Ist das eine Variablenzuweisung im Körper der while-Schleife erhalten bleibt. Das heißt, wenn Sie whilewie oben whilepfeifen, befindet sich der Körper des in einer Unterschale, die möglicherweise nicht das ist, was Sie wollen.

Der Vorteil der Prozessersetzungsversion gegenüber der Version find ... -print0 | xargs -0ist minimal: Die xargsVersion ist in Ordnung, wenn Sie lediglich eine Zeile drucken oder einen einzelnen Vorgang für die Datei ausführen müssen. Wenn Sie jedoch mehrere Schritte ausführen müssen, ist die Schleifenversion einfacher.

BEARBEITEN : Hier ist ein schönes Testskript, damit Sie sich ein Bild über den Unterschied zwischen verschiedenen Versuchen machen können, dieses Problem zu lösen

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"
sorpigal
quelle
1
Akzeptierte Ihre Antwort: die vollständigste und interessanteste - ich wusste nichts über $IFSund die < <(cmd)Syntax. Trotzdem bleibt mir eines unklar, warum das $in $'\0'? Vielen Dank.
Gregseth
2
+1, aber Sie sollten ... while IFS= read... hinzufügen , um Dateien zu verarbeiten, die mit Leerzeichen beginnen oder enden.
Gordon Davisson
1
Die Prozesssubstitutionslösung weist eine Einschränkung auf. Wenn Sie eine Eingabeaufforderung in der Schleife haben (oder auf andere Weise aus STDIN lesen), wird die Eingabe mit dem Material gefüllt, das Sie in die Schleife einspeisen. (Vielleicht sollte dies zur Antwort hinzugefügt werden?)
andsens
2
@uvsmtid: Diese Frage wurde markiert, bashdamit ich mich mit Bash-spezifischen Funktionen sicher fühlte. Die Prozessersetzung ist nicht auf andere Shells übertragbar (sh selbst wird wahrscheinlich nie ein so bedeutendes Update erhalten).
Sorpigal
2
Durch die Kombination IFS=$'\n'mit wird fordie zeileninterne Wortteilung verhindert, die resultierenden Zeilen werden jedoch weiterhin einem Globbing unterzogen, sodass dieser Ansatz nicht vollständig robust ist (es sei denn, Sie deaktivieren auch zuerst das Globbing). Während es read -d $'\0'funktioniert, ist es insofern leicht irreführend, als es darauf hindeutet, dass Sie $'\0'NULs erstellen können - Sie können nicht: a \0in einer ANSI-Zeichenfolge mit C-Anführungszeichen beendet die Zeichenfolge effektiv , sodass dies praktisch -d $'\0'dasselbe ist wie -d ''.
mklement0
29

Es gibt auch eine sehr einfache Lösung: Verlassen Sie sich auf Bash Globbing

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Beachten Sie, dass ich nicht sicher bin, ob dieses Verhalten das Standardeinstellung ist, aber ich sehe keine spezielle Einstellung in meinem Shopt, also würde ich sagen, dass es "sicher" sein sollte (getestet auf OSX und Ubuntu).

Marchelbling
quelle
13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
Karoly Horvath
quelle
6
Als Randnotiz funktioniert dies nur, wenn Sie einen Befehl ausführen möchten. Eine eingebaute Shell funktioniert auf diese Weise nicht.
Alex
11
find . -name "fo*" -print0 | xargs -0 ls -l

Siehe man xargs.

Torp
quelle
6

Da Sie mit keiner anderen Art der Filterung arbeiten find, können Sie ab bash4.0 Folgendes verwenden :

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

Das entspricht **/null oder mehr Verzeichnissen, sodass das vollständige Muster foo*im aktuellen Verzeichnis oder in einem beliebigen Unterverzeichnis übereinstimmt .

chepner
quelle
3

Ich mag Loops und Array-Iterationen sehr, daher denke ich, dass ich diese Antwort dem Mix hinzufügen werde ...

Ich mochte auch Marchelblings dummes Dateibeispiel. :) :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

Im Testverzeichnis:

readarray -t arr <<< "`ls -A1`"

Dadurch wird jede Dateilistenzeile in ein Bash-Array arreingefügt, dessen Name mit einer nachgestellten neuen Zeile entfernt wird.

Nehmen wir an, wir möchten diesen Dateien bessere Namen geben ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! arr [@]} wird auf 0 1 2 erweitert, sodass "$ {arr [$ i]}" das i- te Element des Arrays ist. Die Anführungszeichen um die Variablen sind wichtig, um die Leerzeichen zu erhalten.

Das Ergebnis sind drei umbenannte Dateien:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3
terafl0ps
quelle
2

findhat ein -execArgument, das die Suchergebnisse durchläuft und einen beliebigen Befehl ausführt. Beispielsweise:

find . -iname "foo*" -exec echo "File found: {}" \;

Hier werden {}die gefundenen Dateien dargestellt. Wenn Sie sie einschließen, ""kann der resultierende Shell-Befehl Leerzeichen im Dateinamen verarbeiten.

In vielen Fällen können Sie den letzten \;(der einen neuen Befehl startet) durch einen ersetzen \+, der mehrere Dateien in einen Befehl einfügt (jedoch nicht unbedingt alle gleichzeitig, siehe man findfür weitere Details).

naught101
quelle
0

In einigen Fällen, wenn Sie nur eine Liste von Dateien kopieren oder verschieben müssen, können Sie diese Liste auch an awk weiterleiten.
Wichtig ist das \"" "\"Feld $0(kurz Ihre Dateien, eine Zeilenliste = eine Datei).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'
Steve
quelle
0

Ok - mein erster Beitrag zu Stack Overflow!

Obwohl meine Probleme damit immer darin bestanden haben, dass die von mir vorgestellte Lösung nicht in beiden Fällen funktioniert, bin ich mir sicher, dass sie in beiden Fällen funktionieren wird. Das Problem liegt in der Interpretation der "ls" -Renditen durch die Shell. Wir können "ls" aus dem Problem entfernen, indem wir einfach die Shell-Erweiterung des *Platzhalters verwenden. Dies führt jedoch zu einem "no match" -Fehler, wenn sich keine Dateien im aktuellen (oder angegebenen Ordner) befinden. Um dies zu umgehen, erweitern wir einfach das Erweiterung um Punktdateien also: * .*- Dies führt immer zu Ergebnissen seit den Dateien. und .. wird immer anwesend sein. Also können wir in csh dieses Konstrukt verwenden ...

foreach file (* .*)
   echo $file
end

Wenn Sie die Standard-Punktdateien herausfiltern möchten, ist das einfach genug ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

Der Code im ersten Beitrag zu diesem Thread würde folgendermaßen geschrieben:

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Hoffe das hilft!

Andy Foster
quelle
0

Eine andere Lösung für den Job ...

Ziel war:

  • Dateinamen in Verzeichnissen rekursiv auswählen / filtern
  • Behandle jeden Namen (egal welchen Platz im Pfad ...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}

Vince B.
quelle
Vielen Dank für die konstruktive Bemerkung, aber: 1- dies ist ein tatsächliches Problem, 2- Shell könnte sich in der Zeit entwickelt haben ... wie jeder, den ich annehme; 3- Keine Antwort oben konnte eine DIREKTE Auflösung des pb erfüllen, ohne das Problem zu ändern oder zu zerstreuen :-)
Vince B