Wie kann ich Leerzeichen in einer Bash-Loop-Liste umgehen?

121

Ich habe ein Bash-Shell-Skript, das alle untergeordneten Verzeichnisse (aber keine Dateien) eines bestimmten Verzeichnisses durchläuft. Das Problem ist, dass einige der Verzeichnisnamen Leerzeichen enthalten.

Hier ist der Inhalt meines Testverzeichnisses:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Und der Code, der die Verzeichnisse durchläuft:

for f in `find test/* -type d`; do
  echo $f
done

Hier ist die Ausgabe:

Test / Baltimore
Test / Kirsche
Hügel
Test / Edison 
Test / Neu
York
Stadt
Test / Philadelphia

Cherry Hill und New York City werden als 2 oder 3 separate Einträge behandelt.

Ich habe versucht, die Dateinamen wie folgt zu zitieren:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

aber ohne Erfolg.

Es muss einen einfachen Weg geben, dies zu tun.


Die Antworten unten sind großartig. Um dies jedoch komplizierter zu machen, möchte ich nicht immer die in meinem Testverzeichnis aufgeführten Verzeichnisse verwenden. Manchmal möchte ich stattdessen die Verzeichnisnamen als Befehlszeilenparameter übergeben.

Ich nahm Charles 'Vorschlag an, das IFS einzurichten, und fand Folgendes:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

und dies funktioniert einwandfrei, es sei denn, die Befehlszeilenargumente enthalten Leerzeichen (auch wenn diese Argumente in Anführungszeichen stehen). Wenn Sie das Skript beispielsweise folgendermaßen aufrufen, test.sh "Cherry Hill" "New York City"wird die folgende Ausgabe erzeugt:

Kirsche
Hügel
Neu
York
Stadt
MCS
quelle
re: edit list="$@"verwirft die Liste des ursprünglichen Werts vollständig und reduziert sie zu einer Zeichenfolge. Bitte befolgen Sie die Praktiken in meiner Antwort genau wie angegeben - eine solche Aufgabe wird nirgendwo darin empfohlen; Wenn Sie eine Liste von Befehlszeilenargumenten an ein Programm übergeben möchten, sollten Sie diese in einem Array sammeln und dieses Array direkt erweitern.
Charles Duffy

Antworten:

105

Mach es zuerst nicht so. Der beste Ansatz ist die find -execrichtige Verwendung :

# this is safe
find test -type d -exec echo '{}' +

Der andere sichere Ansatz ist die Verwendung einer NUL-terminierten Liste. Dies setzt jedoch voraus, dass Sie Unterstützung finden -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Sie können ein Array auch aus find füllen und dieses Array später übergeben:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Wenn Ihr Fund dies nicht unterstützt -print0, ist Ihr Ergebnis unsicher. Das Folgende verhält sich nicht wie gewünscht, wenn Dateien vorhanden sind, deren Namen Zeilenumbrüche enthalten (was ja legal ist):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Wenn einer der oben genannten IFSPunkte nicht verwendet wird, besteht ein dritter Ansatz (der sowohl hinsichtlich der Zeit als auch der Speichernutzung weniger effizient ist, da er die gesamte Ausgabe des Unterprozesses liest, bevor eine Wortaufteilung durchgeführt wird ) darin, eine Variable zu verwenden , die dies nicht tut Enthält kein Leerzeichen. Ausschalten Globbing ( set -f) Strings zu verhindern , enthaltend glob Zeichen wie [], *oder ?aus erweitert:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Schließlich sollten Sie für den Fall der Befehlszeilenparameter Arrays verwenden, wenn Ihre Shell diese unterstützt (dh ksh, bash oder zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

wird die Trennung aufrechterhalten. Beachten Sie, dass das Zitieren (und die Verwendung von $@anstatt $*) wichtig ist. Arrays können auch auf andere Weise ausgefüllt werden, z. B. durch Glob-Ausdrücke:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
quelle
1
Ich wusste nichts über das '+' Aroma für -exec. süß
Johannes Schaub - litb
1
das sieht so aus, als könnte es auch, wie xargs, die Argumente nur am Ende des gegebenen Befehls setzen: / das nervt mich manchmal
Johannes Schaub - litb
Ich denke, -exec [Name] {} + ist eine GNU- und 4.4-BSD-Erweiterung. (Zumindest erscheint es nicht in Solaris 8, und ich glaube auch nicht, dass es in AIX 4.3 war.) Ich denke, der Rest von uns könnte mit Rohrleitungen zu
Xargs
2
Ich habe die Syntax $ '\ n' noch nie gesehen. Wie funktioniert das? (Ich hätte gedacht, dass entweder IFS = '\ n' oder IFS = "\ n" funktionieren würde, aber auch nicht.)
MCS
1
@ Crosstalk Es ist definitiv in Solaris 10, ich habe es gerade benutzt.
Nick
26
find . -type d | while read file; do echo $file; done

Funktioniert jedoch nicht, wenn der Dateiname Zeilenumbrüche enthält. Das Obige ist die einzige Lösung, die ich kenne, wenn Sie den Verzeichnisnamen tatsächlich in einer Variablen haben möchten. Wenn Sie nur einen Befehl ausführen möchten, verwenden Sie xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
quelle
Keine Notwendigkeit für xargs, siehe find -exec ... {} +
Charles Duffy
4
@Charles: Für eine große Anzahl von Dateien ist xargs viel effizienter: Es wird nur ein Prozess erzeugt. Die Option -exec gibt für jede Datei einen neuen Prozess aus, der um eine Größenordnung langsamer sein kann.
Adam Rosenfield
1
Ich mag Xargs mehr. Diese beiden scheinen im Wesentlichen dasselbe zu tun, während xargs mehr Optionen hat, wie parallel laufen
Johannes Schaub - litb
2
Adam, nein, dass '+' man so viele Dateinamen wie möglich aggregiert und dann ausführt. aber es wird nicht so nette Funktionen haben wie parallel laufen :)
Johannes Schaub - litb
2
Beachten Sie, dass Sie die Dateinamen zitieren müssen, wenn Sie etwas mit ihnen tun möchten. ZB:find . -type d | while read file; do ls "$file"; done
David Moles
23

Hier ist eine einfache Lösung, die Tabulatoren und / oder Leerzeichen im Dateinamen behandelt. Wenn Sie mit anderen seltsamen Zeichen im Dateinamen wie Zeilenumbrüchen umgehen müssen, wählen Sie eine andere Antwort.

Das Testverzeichnis

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Der Code, um in die Verzeichnisse zu gelangen

find test -type d | while read f ; do
  echo "$f"
done

Der Dateiname muss in Anführungszeichen ( "$f") stehen, wenn er als Argument verwendet wird. Ohne Anführungszeichen fungieren die Leerzeichen als Argumenttrennzeichen, und dem aufgerufenen Befehl werden mehrere Argumente zugewiesen.

Und die Ausgabe:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard
quelle
Vielen Dank, dies funktionierte für den Alias, den ich erstellt habe, um aufzulisten, wie viel Speicherplatz jedes Verzeichnis im aktuellen Ordner verwendet. Es wurde an einigen Verzeichnissen mit Leerzeichen in der vorherigen Inkarnation erstickt. Dies funktioniert in zsh, aber einige der anderen Antworten nicht:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid
2
Wenn Sie zsh verwenden, können Sie dies auch tun:alias duc='du -sh *(/)'
cbliard
@cbliard Das ist immer noch fehlerhaft. Versuchen Sie, es mit einem Dateinamen auszuführen, der beispielsweise eine Tabulatorsequenz oder mehrere Leerzeichen enthält. Sie werden feststellen, dass eines dieser Elemente in ein einzelnes Leerzeichen geändert wird, da Sie in Ihrem Echo nicht zitieren. Und dann gibt es den Fall von Dateinamen, die Zeilenumbrüche enthalten ...
Charles Duffy
@ CharlesDuffy Ich habe es mit Tabulatorsequenzen und mehreren Leerzeichen versucht. Es funktioniert mit Anführungszeichen. Ich habe es auch mit Zeilenumbrüchen versucht und es funktioniert überhaupt nicht. Ich habe die Antwort entsprechend aktualisiert. Vielen Dank für den Hinweis.
Cbliard
1
@cbliard Richtig - das Hinzufügen von Anführungszeichen zu Ihrem Echo-Befehl war das, worauf ich hinaus wollte. Bei Zeilenumbrüchen können Sie diese Funktion mithilfe von find -print0und ausführen IFS='' read -r -d '' f.
Charles Duffy
7

Dies ist unter Standard-Unix äußerst schwierig, und die meisten Lösungen weisen keine Zeilenumbrüche oder andere Zeichen auf. Wenn Sie jedoch das GNU-Toolset verwenden, können Sie die findOption ausnutzen -print0und xargsmit der entsprechenden Option -0(Minus-Null) verwenden. Es gibt zwei Zeichen, die in einem einfachen Dateinamen nicht vorkommen können. das sind Schrägstrich und NUL '\ 0'. Offensichtlich erscheint ein Schrägstrich in Pfadnamen, daher ist die GNU-Lösung, ein NUL '\ 0' zum Markieren des Namensendes zu verwenden, genial und narrensicher.

Jonathan Leffler
quelle
4

Warum nicht einfach setzen

IFS='\n'

vor dem for-Befehl? Dadurch wird das Feldtrennzeichen von <Leerzeichen> <Tabulator> <Nachricht> in nur <Zeitlinie> geändert

oshunluvr
quelle
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
quelle
1
-d $'\0'ist genau das Gleiche wie -d ''- da bash NUL-terminierte Zeichenfolgen verwendet, ist das erste Zeichen einer leeren Zeichenfolge eine NUL, und aus dem gleichen Grund können NULs in C-Zeichenfolgen überhaupt nicht dargestellt werden.
Charles Duffy
4

ich benutze

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Wäre das nicht genug?
Idee von http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

Murpel
quelle
guter Tipp: die für die Optionen auf eine Befehlszeilen osascript (OS X Apple) sehr hilfreich ist, wo Räume ein Argument in mehrere Parameter aufgeteilt , wo nur eine bestimmt ist
tim
Nein, das reicht nicht. Es ist ineffizient (aufgrund der unnötigen Verwendung von $(echo ...)), behandelt Dateinamen mit Glob-Ausdrücken nicht korrekt, behandelt Dateinamen, die $'\b'Zeichen oder $ '\ n' enthalten, nicht korrekt und konvertiert darüber hinaus mehrere Durchläufe von Leerzeichen in einzelne Leerzeichen auf dem Ausgabeseite wegen falscher Anführungszeichen.
Charles Duffy
4

Speichern Sie Listen nicht als Zeichenfolgen. Speichern Sie sie als Arrays, um all diese Trennzeichenverwirrung zu vermeiden. Hier ist ein Beispielskript, das entweder in allen Unterverzeichnissen des Tests oder in der Liste in der Befehlszeile ausgeführt wird:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Probieren wir dies nun in einem Testverzeichnis mit ein oder zwei eingeworfenen Kurven aus:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
Gordon Davisson
quelle
1
Im Rückblick auf diese - es tatsächlich war eine Lösung mit POSIX sh: Sie könnten die Wiederverwendung "$@"Array, mit ihm anhängt set -- "$@" "$f".
Charles Duffy
4

Sie können IFS (internes Feldtrennzeichen) zeitlich verwenden, indem Sie:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS

erstaunlich da
quelle
Bitte geben Sie eine Erklärung.
Steve K
Wenn IFS angegeben hat, was das Trennzeichen ist, wird der Dateiname mit Leerzeichen nicht abgeschnitten.
Erstaunlich,
$ IFS = $ OLD_IFS am Ende sollte sein: IFS = $ OLD_IFS
Michel Donais
3

ps Wenn es nur um Platz in der Eingabe geht, dann haben einige doppelte Anführungszeichen für mich reibungslos funktioniert ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
Hardbutnot
quelle
2

Um das zu ergänzen, was Jonathan gesagt hat: Verwenden Sie die -print0Option für findin Verbindung mit xargswie folgt:

find test/* -type d -print0 | xargs -0 command

Dadurch wird der Befehl commandmit den richtigen Argumenten ausgeführt. Verzeichnisse mit Leerzeichen werden korrekt in Anführungszeichen gesetzt (dh sie werden als ein Argument übergeben).

Adam Rosenfield
quelle
1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Der obige Code konvertiert .mov-Dateien in .avi. Die .mov-Dateien befinden sich in verschiedenen Ordnern und die Ordnernamen haben auch Leerzeichen . Mein obiges Skript konvertiert die .mov-Dateien in die .avi-Datei im selben Ordner. Ich weiß nicht, ob es euch Völkern hilft.

Fall:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Prost!

Sony George
quelle
echo "$name" | ...funktioniert nicht, wenn dies der Fall nameist -n, und wie es sich mit Namen mit Backslash-Escape-Sequenzen verhält, hängt von Ihrer Implementierung ab. POSIX macht das Verhalten echoin diesem Fall explizit undefiniert (während XSI-erweitertes POSIX die Erweiterung von Backslash-Escape-Sequenzen standardmäßig definiert und GNU-Systeme - einschließlich Bash - ohne POSIXLY_CORRECT=1den POSIX-Standard durch Implementierung zu brechen -e(während die Spezifikation echo -edas Drucken -eauf der Ausgabe erfordert ). printf '%s\n' "$name" | ...ist sicherer.
Charles Duffy
1

Musste mich auch mit Leerzeichen in Pfadnamen befassen. Was ich schließlich tat, war eine Rekursion und for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Gilles 'SO - hör auf böse zu sein'
quelle
1
Verwenden Sie das functionSchlüsselwort nicht - es macht Ihren Code mit POSIX sh inkompatibel, hat aber keinen anderen nützlichen Zweck. Sie können einfach eine Funktion mit definieren recursedir() {, die beiden Parens hinzufügen und das Funktionsschlüsselwort entfernen. Dies ist mit allen POSIX-kompatiblen Shells kompatibel.
Charles Duffy
1

Konvertieren Sie die Dateiliste in ein Bash-Array. Dies verwendet den Ansatz von Matt McClure zum Zurückgeben eines Arrays von einer Bash-Funktion: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Das Ergebnis ist ein Weg um eine mehrzeilige Eingabe in ein Bash-Array umzuwandeln.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Dieser Ansatz scheint auch dann zu funktionieren, wenn fehlerhafte Zeichen vorhanden sind, und ist eine allgemeine Methode, um Eingaben in ein Bash-Array zu konvertieren. Der Nachteil ist, dass Sie bei langen Eingaben die Grenzwerte für die Befehlszeilengröße von Bash überschreiten oder viel Speicher belegen können.

Ansätze, bei denen die Schleife, die schließlich an der Liste arbeitet, auch die Liste einleitet, haben den Nachteil, dass das Lesen von stdin nicht einfach ist (z. B. das Bitten des Benutzers um Eingabe), und die Schleife ist ein neuer Prozess, sodass Sie sich möglicherweise fragen, warum Variablen Sie, die innerhalb der Schleife festgelegt wurden, sind nach Beendigung der Schleife nicht mehr verfügbar.

Ich mag es auch nicht, IFS einzustellen, es kann anderen Code durcheinander bringen.

Steve Zobell
quelle
Wenn Sie IFS='' readin derselben Zeile verwenden, ist die IFS-Einstellung nur für den Lesebefehl vorhanden und entgeht ihr nicht. Es gibt keinen Grund, IFS nicht auf diese Weise einzustellen.
Charles Duffy
1

Nun, ich sehe zu viele komplizierte Antworten. Ich möchte die Ausgabe des Dienstprogramms find nicht übergeben oder eine Schleife schreiben, da find hierfür die Option "exec" hat.

Mein Problem war, dass ich alle Dateien mit der Erweiterung dbf in den aktuellen Ordner verschieben wollte und einige davon Leerzeichen enthielten.

Ich habe es so angepackt:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Sieht für mich sehr einfach aus

Tebe
quelle
0

Ich habe gerade herausgefunden, dass es einige Ähnlichkeiten zwischen meiner und Ihrer Frage gibt . Offenbar, wenn Sie Argumente an Befehle übergeben möchten

test.sh "Cherry Hill" "New York City"

um sie in der richtigen Reihenfolge auszudrucken

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

Beachten Sie, dass das $ @ von doppelten Anführungszeichen umgeben ist, einige Anmerkungen hier

Jeffrey04
quelle
0

Ich brauchte das gleiche Konzept, um mehrere Verzeichnisse oder Dateien aus einem bestimmten Ordner nacheinander zu komprimieren. Ich habe es mit awk gelöst, die Liste von ls zu analysieren und das Problem der Leerstelle im Namen zu vermeiden.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

Was denken Sie?

Hìr0
quelle
Ich denke, das wird nicht richtig funktionieren, wenn die Dateinamen Zeilenumbrüche enthalten. Vielleicht solltest du es versuchen.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
quelle
-3

Für mich funktioniert das und es ist ziemlich "sauber":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
quelle
4
Das ist aber schlimmer. Die doppelten Anführungszeichen um den Fund bewirken, dass alle Pfadnamen als einzelne Zeichenfolge verkettet werden. Ändern Sie das Echo in ein ls , um das Problem zu sehen.
NVRAM
-4

Hatte gerade ein einfaches Variantenproblem ... Konvertiere Dateien von typisiertem .flv in .mp3 (gähnen).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

Suchen Sie rekursiv alle Macintosh-Benutzer-Flash-Dateien und wandeln Sie sie in Audio um (Kopieren, kein Transcode) .

mark washeim
quelle
2
Das readNachher inist ein weiteres Wort in der Liste, über die Sie iterieren. Was Sie gepostet haben, ist eine leicht kaputte Version dessen, was der Fragesteller hatte, was nicht funktioniert. Sie haben vielleicht vorgehabt, etwas anderes zu posten, aber es wird wahrscheinlich trotzdem von anderen Antworten hier abgedeckt.
Gilles 'SO - hör auf böse zu sein'