it-swarm.com.de

Durchlaufen Sie eine Liste von Dateien mit Leerzeichen

Ich möchte eine Liste von Dateien durchlaufen. Diese Liste ist das Ergebnis eines find-Befehls, also kam ich mit:

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 im 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 Trennung von Leerzeichen zu vermeiden?

177
gregseth

Sie können die Word-basierte Iteration durch eine zeilenbasierte Iteration ersetzen:

find . -iname "foo*" | while read f
do
    # ... loop body
done
220
martin clayton

Es gibt mehrere praktikable Wege, um dies zu erreichen. 

Wenn Sie sich an Ihre ursprüngliche Version halten wollten, 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, die jedoch durch Leerzeichen nicht beschädigt werden.

Es ist jedoch nicht notwendig, sich mit IFS zu beschäftigen. Hier ist mein bevorzugter Weg, 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 über Prozessersetzung lesen. Der Vorteil gegenüber for file in $(find ...) ist, dass Dateien mit Leerzeichen, Zeilenumbrüchen und anderen Zeichen korrekt behandelt werden. Dies funktioniert, weil find mit -print0 eine null (aka \0) als Abschlusszeichen für jeden Dateinamen verwendet, und im Gegensatz zu newline ist null kein gültiges Zeichen in einem Dateinamen.

Der Vorteil gegenüber der fast gleichwertigen Version

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

Ist das die Zuweisung einer Variablen im Rumpf der while-Schleife? Das heißt, wenn Sie wie oben zu while pipeieren, befindet sich der Körper der while in einer Subshell, die möglicherweise nicht Ihren Wünschen entspricht.

Der Vorteil der Prozessersetzungsversion gegenüber find ... -print0 | xargs -0 ist minimal: Die xargs-Version 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.

EDIT: Hier ist ein Test-Skript von Nice, mit dem Sie den Unterschied zwischen den verschiedenen Lösungsversuchen erkennen können

#!/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"
146
Sorpigal

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 die Standardeinstellung ist, aber ich sehe keine speziellen Einstellungen in meinem Shopt. Ich würde also sagen, dass es "sicher" sein sollte (getestet auf osx und ubuntu).

27
marchelbling
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
12
Karoly Horvath
find . -name "fo*" -print0 | xargs -0 ls -l

Siehe man xargs.

11
Torp

Da Sie mit find keine andere Art von Filterung durchführen, können Sie ab bash 4.0 Folgendes verwenden:

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

**/ stimmt mit keinem oder mehreren Verzeichnissen überein, sodass das vollständige Muster mit foo* im aktuellen Verzeichnis oder in allen Unterverzeichnissen übereinstimmt.

6
chepner

Ich mag Schleifen und Array-Iteration wirklich sehr gerne, also werde ich diese Antwort dem Mix hinzufügen ...

Das dumme Beispiel von marchelbling hat mir auch gefallen. :)

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

Im Testverzeichnis:

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

Dadurch wird jede Dateiliste in ein Bash-Array mit dem Namen arr eingefügt, wobei alle nachfolgenden Zeilen entfernt werden.

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 [@]} erweitert sich auf 0 1 2, "$ {arr [$ i]}" ist das i th - Element des Arrays. 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
2
terafl0ps

find hat ein -exec-Argument, das die Suchergebnisse durchläuft und einen beliebigen Befehl ausführt. Zum Beispiel:

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

Hier steht {} für die gefundenen Dateien, und durch das Einschließen in "" kann der resultierende Shell-Befehl mit Leerzeichen im Dateinamen umgehen. 

In vielen Fällen können Sie den letzten \; (der einen neuen Befehl startet) durch einen \+ ersetzen. Dadurch werden mehrere Dateien in den einen Befehl eingefügt (jedoch nicht unbedingt alle auf einmal, siehe man find für weitere Details).

0
naught101

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

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

Ok - mein erster Beitrag zum Stack Overflow!

Obwohl meine Probleme damit immer in csh nicht bash waren, wird die Lösung, die ich vorstelle, sicherlich in beidem funktionieren. Das Problem ist mit der Shell-Interpretation der "ls" -Antworten. Wir können "ls" aus dem Problem entfernen, indem Sie einfach die Shell-Erweiterung des *-Platzhalters verwenden. Wenn jedoch keine Dateien im aktuellen Ordner (oder im angegebenen Ordner) vorhanden sind, wird der Fehler "Keine Übereinstimmung" angezeigt die Erweiterung um dot-files aufzunehmen, also: * .* - das ergibt immer Ergebnisse seit den Dateien. und .. wird immer anwesend sein. Also in csh können wir 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 werden:

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

Hoffe das hilft!

0
Andy Foster

Eine weitere Lösung, die ihren Job macht ...

Ziel war:

  • dateinamen in Verzeichnissen rekursiv auswählen/filtern
  • behandle jeden Namen (egal welches Feld 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}


0
Vince B