So trennen Sie die Befehlsausgabe in einzelne Zeilen

10
list=`ls -a R*`
echo $list

In einem Shell-Skript listet dieser Echo-Befehl alle Dateien aus dem aktuellen Verzeichnis auf, beginnend mit R, jedoch in einer Zeile. Wie kann ich jeden Artikel in einer Zeile drucken?

Ich brauche einen allgemeinen Befehl für alle Szenarien geschieht mit ls, du, find -type -detc.

Anony
quelle
6
Allgemeiner Hinweis: Tun Sie dies nicht mit ls, da es bei seltsamen Dateinamen kaputt geht . Siehe hier .
Terdon

Antworten:

12

Wenn die Ausgabe des Befehls mehrere Zeilen enthält, zitieren Sie Ihre Variablen, um diese Zeilenumbrüche beim Echo beizubehalten:

echo "$list"

Andernfalls wird die Shell den Inhalt der Variablen in Leerzeichen (Leerzeichen, Zeilenumbrüche, Tabulatoren) erweitern und aufteilen, und diese gehen verloren.

muru
quelle
15

Anstatt die lsAusgabe schlecht in eine Variable einzufügen und sie dann zu bearbeiten echo, wodurch alle Farben entfernt werden, verwenden Sie

ls -a1

Von man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Ich rate dir nicht, irgendetwas mit der Ausgabe von zu tun ls, außer sie anzuzeigen :)

Verwenden Sie zum Beispiel Shell-Globs und eine forSchleife, um etwas mit Dateien zu tun ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* Dies wird das aktuelle Verzeichnis nicht enthalten .oder seine Eltern , ..obwohl

Zanna
quelle
1
Ich würde empfehlen, Folgendes zu ersetzen: echo "$i"mit printf "%s\n" "$i" (dass man nicht erstickt, wenn ein Dateiname mit einem "-" beginnt). Eigentlich sollte die meiste Zeit echo somethingdurch ersetzt werden printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Sie fangen alle Rhier an, aber im Allgemeinen ja. Doch selbst bei Skripten verursacht der Versuch, immer die führenden -Pfade zu berücksichtigen, Probleme: Die Leute gehen davon aus --, dass nur einige Befehle das Ende der Optionen bedeuten . Ich habe gesehen, find . [tests] -exec [cmd] -- {} \;wo [cmd]nicht unterstützt --. Jeder Weg dorthin beginnt .sowieso mit! Aber das ist kein Argument gegen das Ersetzen echodurch printf '%s\n'. Hier kann man die gesamte Schleife durch ersetzen printf '%s\n' R/*. Bash ist printfeingebaut, daher gibt es praktisch keine Begrenzung für die Anzahl / Länge der Argumente.
Eliah Kagan
11

Wenn Sie es in Anführungszeichen setzen, wie von @muru vorgeschlagen, wird dies tatsächlich das tun, wonach Sie gefragt haben. Vielleicht möchten Sie auch ein Array dafür verwenden. Zum Beispiel:

IFS=$'\n' dirs=( $(find . -type d) )

Die IFS=$'\n'Anweisung bash, die Ausgabe nur auf Zeilenumbrüche aufzuteilen, um jedes Element des Arrays abzurufen. Ohne es wird es auf Leerzeichen aufgeteilt, also a file name with spaces.txtwären 5 separate Elemente anstelle von einem. Dieser Ansatz wird jedoch unterbrochen, wenn Ihre Datei- / Verzeichnisnamen newlines ( \n) enthalten können . Jede Zeile der Befehlsausgabe wird als Element des Arrays gespeichert.

Beachten Sie, dass ich auch die alten Stil geändert `command`zu $(command)denen ist die bevorzugte Syntax.

Sie haben jetzt ein Array mit dem Namen $dirsjedes Bof, dessen Elemente eine Zeile der Ausgabe des vorherigen Befehls sind. Zum Beispiel:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Aufgrund einiger Bash-Seltsamkeiten (siehe hier ) müssen Sie danach IFSauf den ursprünglichen Wert zurücksetzen . Also entweder speichern und neu signieren:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Oder machen Sie es manuell:

IFS=" "$'\t\n '

Alternativ schließen Sie einfach das aktuelle Terminal. Bei Ihrem neuen wird das ursprüngliche IFS erneut eingestellt.

Terdon
quelle
Ich hätte vielleicht so etwas vorgeschlagen, aber der Teil über duOP sieht mehr danach aus, das Ausgabeformat beizubehalten.
Muru
Wie kann ich das zum Laufen bringen, wenn die Verzeichnisnamen Leerzeichen enthalten?
Nachtisch
@ Dessert whoops! Es sollte jetzt mit Leerzeichen (aber nicht mit Zeilenumbrüchen) funktionieren. Vielen Dank für den Hinweis.
Terdon
1
Beachten Sie, dass Sie IFS nach dieser Array-Zuweisung zurücksetzen oder deaktivieren sollten. unix.stackexchange.com/q/264635/70524
muru
@muru oh wow, danke, ich hatte diese Verrücktheit vergessen.
Terdon
1

Wenn Sie die Ausgabe speichern müssen und ein Array möchten, mapfileist dies einfach.

Überlegen Sie zunächst, ob Sie die Ausgabe Ihres Befehls überhaupt speichern müssen . Wenn Sie dies nicht tun, führen Sie einfach den Befehl aus.

Wenn Sie die Ausgabe eines Befehls als Zeilenarray lesen möchten, können Sie das Globbing deaktivieren, das Aufteilen IFSin Zeilen festlegen , die Befehlssubstitution in der ( )Syntax zur Arrayerstellung verwenden und anschließend zurücksetzen IFS. Das ist komplexer als es scheint. Terdons Antwort deckt einen Teil dieses Ansatzes ab. Ich schlage jedoch vor, dass Sie stattdessen den inmapfile die Bash-Shell integrierten Befehl zum Lesen von Text als Zeilenarray verwenden. Hier ist ein einfacher Fall, in dem Sie aus einer Datei lesen:

mapfile < filename

Dies liest Zeilen in ein Array namens MAPFILE. Ohne die Umleitung der Eingabe würden Sie anstelle der von benannten Datei von der Standardeingabe der Shell (normalerweise Ihrem Terminal) lesen . Übergeben Sie den Namen, um in ein anderes als das Standardarray einzulesen . Dies liest beispielsweise in das Array ein :< filenamefilenameMAPFILElines

mapfile lines < filename

Ein weiteres Standardverhalten, das Sie möglicherweise ändern möchten, besteht darin, dass die Zeilenumbrüche an den Zeilenenden beibehalten werden. Sie werden als letztes Zeichen in jedem Array-Element angezeigt (es sei denn, die Eingabe endete ohne ein Zeilenumbruchzeichen. In diesem Fall hat das letzte Element kein Zeichen). Um diese Zeilenumbrüche chomp , so dass sie in dem Array nicht angezeigt werden , übergeben Sie die -tOption mapfile. Dies liest beispielsweise in das Array recordsund schreibt keine nachgestellten Zeilenumbrüche in seine Array-Elemente:

mapfile -t records < filename

Sie können auch verwenden, -tohne einen Array-Namen zu übergeben. Das heißt, es funktioniert auch mit einem impliziten Array-Namen von MAPFILE.

Die integrierte mapfileShell unterstützt andere Optionen und kann alternativ als aufgerufen werden readarray. Führen Sie help mapfile(und help readarray) für Details aus.

Sie möchten jedoch nicht aus einer Datei lesen, sondern aus der Ausgabe eines Befehls. Verwenden Sie dazu die Prozessersetzung . Dieser Befehl liest Zeilen aus dem Befehl some-commandmit arguments...seinen Befehlszeilenargumenten und platziert sie im mapfileStandardarray MAPFILE, wobei die abschließenden Zeilenumbruchzeichen entfernt werden:

mapfile -t < <(some-command arguments...)

Die Prozessersetzung wird durch einen tatsächlichen Dateinamen ersetzt, aus dem die Ausgabe von running gelesen werden kann. Die Datei ist eher eine Named Pipe als eine reguläre Datei. Unter Ubuntu wird sie wie folgt benannt (manchmal mit einer anderen Nummer als ), aber Sie müssen sich nicht um die Details kümmern, da sich die Shell darum kümmert alles hinter den Kulissen.<(some-command arguments...)some-command arguments.../dev/fd/6363

Sie könnten denken, Sie könnten auf die Prozessersetzung verzichten, indem Sie stattdessen verwenden, aber das funktioniert nicht, da Bash , wenn Sie eine Pipeline mit mehreren durch getrennten Befehlen haben , alle Befehle in Subshells ausführt . Somit werden beide und in ihren eigenen Umgebungen ausgeführt , die von der Umgebung der Shell, in der Sie die Pipeline ausführen, initialisiert, aber von dieser getrennt sind. In der Subshell, in der ausgeführt wird, wird das Array zwar gefüllt , aber dieses Array wird verworfen, wenn der Befehl endet. wird niemals für den Anrufer erstellt oder geändert.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

So sieht das Beispiel in Terdons Antwort vollständig aus, wenn Sie Folgendes verwenden mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Das ist es. Sie müssen nicht überprüfenIFS , ob und mit welchem ​​Wert festgelegt wurde, ob es nicht festgelegt wurde, auf eine neue Zeile setzen und später zurücksetzen oder wieder deaktivieren. Sie müssen das Globbing nicht deaktivieren (z. B. mit set -f) - was wirklich erforderlich ist, wenn Sie diese Methode ernsthaft verwenden möchten , da Dateinamen möglicherweise enthalten *, ?und - und [anschließend erneut aktivieren ( set +f).

Sie können diese bestimmte Schleife auch vollständig durch einen einzelnen printfBefehl ersetzen - obwohl dies eigentlich kein Vorteil ist mapfile, da Sie dies unabhängig davon tun können, ob Sie mapfiledas Array oder eine andere Methode zum Auffüllen des Arrays verwenden. Hier ist eine kürzere Version:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

Es ist wichtig zu beachten, dass, wie Terdon erwähnt , das zeilenweise Arbeiten nicht immer angemessen ist und mit Dateinamen, die Zeilenumbrüche enthalten, nicht ordnungsgemäß funktioniert. Ich empfehle, Dateien nicht so zu benennen, aber es kann passieren, auch aus Versehen.

Es gibt keine einheitliche Lösung.

Sie haben nach "einem generischen Befehl für alle Szenarien" und dem Ansatz gefragt, mapfileetwas näher an dieses Ziel heranzugehen, aber ich fordere Sie auf, Ihre Anforderungen zu überdenken.

Die oben gezeigte Aufgabe wird besser mit nur einem einzigen findBefehl erreicht:

find . -type d -printf 'DIR: %p\n'

Sie können auch einen externen Befehl verwenden , der am Anfang jeder Zeile sedhinzugefügt DIR: werden soll. Dies ist wohl etwas hässlich, und im Gegensatz zu diesem Befehl find werden zusätzliche "Präfixe" in Dateinamen hinzugefügt, die Zeilenumbrüche enthalten. Es funktioniert jedoch unabhängig von der Eingabe, sodass es Ihren Anforderungen entspricht, und es ist immer noch vorzuziehen, die Ausgabe in a einzulesen Variable oder Array :

find . -type d | sed 's/^/DIR: /'

Wenn Sie für jedes gefundene Verzeichnis eine Aktion auflisten und ausführen müssen, z. B. das Ausführen some-commandund Übergeben des Verzeichnispfads als Argument, findkönnen Sie dies auch tun:

find . -type d -print -exec some-command {} \;

Kehren wir als weiteres Beispiel zur allgemeinen Aufgabe zurück, jeder Zeile ein Präfix hinzuzufügen. Angenommen, ich möchte die Ausgabe von sehen, help mapfileaber die Zeilen nummerieren. Ich würde mapfiledafür weder eine andere Methode verwenden, die es in eine Shell-Variable oder ein Shell-Array einliest. Angenommen, help mapfile | cat -nes wird nicht die gewünschte Formatierung angegeben, kann ich Folgendes verwenden awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Das Einlesen der gesamten Ausgabe eines Befehls in eine einzelne Variable oder ein einzelnes Array ist manchmal nützlich und angemessen, hat jedoch große Nachteile. Sie müssen sich nicht nur mit der zusätzlichen Komplexität der Verwendung Ihrer Shell auseinandersetzen, wenn ein vorhandener Befehl oder eine Kombination vorhandener Befehle möglicherweise bereits das tut, was Sie benötigen, oder besser, sondern die gesamte Ausgabe des Befehls muss im Speicher gespeichert werden. Manchmal wissen Sie vielleicht, dass dies kein Problem ist, aber manchmal verarbeiten Sie eine große Datei.

Eine Alternative, die häufig versucht wird - und manchmal richtig gemacht wird -, besteht darin, die Eingabe Zeile für Zeile read -rin einer Schleife zu lesen . Wenn Sie während der Bearbeitung späterer Zeilen keine vorherigen Zeilen speichern müssen und lange Eingaben verwenden müssen, ist dies möglicherweise besser als mapfile. Aber es sollte auch in Fällen vermieden werden, in denen Sie es einfach an einen Befehl weiterleiten können, der die Arbeit erledigen kann, was in den meisten Fällen der Fall ist .

Eliah Kagan
quelle