Erstellen eines BASH-Skripts zum Behandeln von Dateinamen mit Leerzeichen (oder einer Problemumgehung)


12

Während ich BASH seit mehreren Jahren benutze, sind meine Erfahrungen mit BASH-Skripten relativ begrenzt.

Mein Code ist wie folgt. Es sollte die gesamte Verzeichnisstruktur aus dem aktuellen Verzeichnis heraus abrufen und in dieses replizieren $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

Das Problem ist, hier ist ein Beispiel meiner Dateistruktur:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Beachten Sie die Leerzeichen: -S Und fornimmt Parameter wortweise an, sodass die Ausgabe meines Skripts ungefähr so ​​aussieht:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Aber ich brauche es, um ganze Dateinamen (eine Zeile nach der anderen) aus der Ausgabe von zu holen find. Ich habe auch versucht, findjeden Dateinamen in doppelte Anführungszeichen zu setzen. Aber das hilft nicht.

for DIR in `find . -type d -printf "\"%P\"\040"`

Und mit dieser geänderten Zeile ausgeben:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Jetzt brauche ich eine Möglichkeit, die ich auf diese Weise durchlaufen kann, da ich auch einen komplizierteren Befehl ausführen möchte, der gstreamerfür jede Datei eine ähnliche Struktur aufweist. Wie soll ich das machen?

Bearbeiten: Ich benötige eine Codestruktur, mit der ich für jedes Verzeichnis / jede Datei / Schleife mehrere Codezeilen ausführen kann. Entschuldigung, wenn ich unklar war.

Lösung: Ich habe anfangs versucht:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Dies hat zum größten Teil gut funktioniert. Später stellte ich jedoch fest, dass, da die Pipe dazu führt, dass die while-Schleife in einer Subshell ausgeführt wird, alle in der Schleife gesetzten Variablen später nicht mehr verfügbar waren, was die Implementierung eines Fehlerzählers ziemlich schwierig machte. Meine endgültige Lösung (aus dieser Antwort auf SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Dies erlaubte mir später, Variablen innerhalb der Schleife bedingt zu inkrementieren, die später im Skript verfügbar bleiben würden.


Warum_wollen_Sie_immer_einen_Speicherplatz_in_einem_Dateinamen_notwendig machen?
Kevin Panko

Stimmt, nicht meine Präferenz. Um Leerzeichen zu entfernen, müssen Sie jedoch zuerst Dateien mit Leerzeichen behandeln;)
Samuel Jaeschke

1
Eigentlich sollten Dateinamen Leerzeichen enthalten. Ich würde alles andere als /und nicht druckbare Zeichen erlauben . Aber alles ist erlaubt, außer /und \0du musst es ihnen erlauben.
Kevin Panko

Antworten:


11

Sie müssen das findin eine whileSchleife leiten.

find ... | while read -r dir
do
    something with "$dir"
done

Sie müssen -printfin diesem Fall auch nicht verwenden.

Sie können diesen Beweis gegen Dateien mit Zeilenumbrüchen im Namen erbringen, indem Sie ein Nullbyte-Trennzeichen verwenden (dies ist das einzige Zeichen, das in einem * nix-Dateipfad nicht vorkommen kann):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Sie werden auch feststellen, dass die Verwendung $()von Backticks anstelle von Backticks vielseitiger und einfacher ist. Sie können viel einfacher verschachtelt werden und Zitate können viel einfacher erstellt werden. Dieses erfundene Beispiel wird diese Punkte veranschaulichen:

echo "$(echo "$(echo "hello")")"

Versuchen Sie das mit Backticks.


2
Außerdem ist "$dir"die Verwendung vorzuziehen "${dir}"- es ist einfach, den Unterschied zwischen $ {dir} name und $ {dirname} zu erkennen, aber $ dirname kann so oder so interpretiert werden.
James Polley

Wichtig hierbei ist, dass readeine ganze Zeile eingelesen wird ${dir}, sodass das IFS keine Rolle spielt.
James Polley

1
. Vielen Dank für die $ zu finden , /“Typo Die Streben sind nicht erforderlich , wenn nichts nach dem Variablennamen gibt.
Pausiert bis auf weiteres.

4
Damit werden Pfadnamen mit Leerzeichen (U + 0020) behandelt, Pfadnamen mit Zeilenumbrüchen (U + 000A) werden jedoch weiterhin nicht ordnungsgemäß behandelt. Ich bevorzuge, find … -print0 | xargs -0 …weil das verwendete Trennzeichen genau dem einzigen Zeichen entspricht, das in POSIX-Pfadnamen nicht zulässig ist: NUL (U + 0000).
Chris Johnsen

2
Perfekt! Genau das, wonach ich gesucht habe. Mir war noch nie in den Sinn gekommen, dass Sie vielleicht an etwas herankommen könnten while. @ Chris Johnsen: Stimmt, aber selbst Musik-Ripping-Programme neigen nicht dazu, Zeilenvorschübe in ihre Dateinamen einzufügen. Und wenn ja, möchte ich wissen (dh, etwas läuft schief) und sie sofort loswerden ...
Samuel Jaeschke

8

In dieser Antwort, die ich vor ein paar Tagen geschrieben habe, finden Sie ein Beispiel für ein Skript, das Dateinamen mit Leerzeichen behandelt.

Es gibt jedoch eine etwas gewundenere (aber präzisere) Möglichkeit, das zu erreichen, was Sie versuchen:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0teilt find mit, die Argumente durch eine Null zu trennen; das -0 bis xargs weist es an, Argumente zu erwarten, die durch Nullen getrennt sind. Dies bedeutet, dass Leerzeichen problemlos verarbeitet werden können.

-I {}Weist xargs an, die Zeichenfolge {}durch den Dateinamen zu ersetzen . Dies impliziert auch, dass nur ein Dateiname pro Befehlszeile verwendet werden sollte (xargs stopft normalerweise so viele Dateien, wie in die Zeile passen).

Der Rest sollte offensichtlich sein.


Dennis Williamsons Vorschlag ist jedoch (abgesehen von den Tippfehlern) viel lesbarer und daher in fast jeder Hinsicht vorzuziehen.
James Polley

Funktioniert für mkdir, aber es tut mir leid, ich hätte klarer sein sollen - ich möchte eine Reihe von Befehlen für jede Datei ausführen. Sie sehen, für meine ähnliche Routine möchte ich später einen Ausgabedateinamen auf der Grundlage des Eingabedateinamens generieren (der das Entfernen der Erweiterung .ogg und das Hinzufügen von .mp3 umfasst) und dann diese mehreren Variablen in meiner Pipline verwenden, wenn ich gst-launch aufrufe.
Samuel Jaeschke

5

Das Problem, auf das Sie stoßen, ist, dass die for-Anweisung als separate Argumente auf die Suche reagiert. Der Leerzeichenbegrenzer. Sie müssen die IFS-Variable von bash verwenden, um keine Aufteilung nach Speicherplatz vorzunehmen.

Hier ist ein Link , der erklärt, wie das geht.

Die interne IFS-Variable

Eine Möglichkeit, dieses Problem zu umgehen, besteht darin, die interne IFS-Variable (Internal Field Separator) von Bash so zu ändern, dass Felder durch ein anderes als das standardmäßige Leerzeichen (Leerzeichen, Tabulator, Zeilenvorschub), in diesem Fall ein Komma, geteilt werden.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Stellen Sie Ihren Fund so ein, dass Ihr Feldbegrenzer nach dem% P ausgegeben wird, und stellen Sie Ihr IFS entsprechend ein. Ich habe Semikolon gewählt, da es sehr unwahrscheinlich ist, dass es in Ihren Dateinamen gefunden wird.

Die andere Alternative ist, mkdir direkt aus dem Find aufzurufen, indem -execSie die for-Schleife überspringen. Dies ist der Fall, wenn Sie kein zusätzliches Parsing durchführen müssen.


Was ist, wenn der Dateiname das IFS enthält? Dann müssen Sie sich für einen anderen entscheiden. Aber was ist, wenn ...
Bis auf weiteres angehalten.

3
Sie können /auf POSIX- und :DOS-Dateisystemen auswählen . Es gibt unzulässige Zeichen für verschiedene Dateisysteme, die Sie für das IFS auswählen können. Alles, was komplizierter ist, und Sie sind besser dran, Perl zu verwenden.
Darren Hall

2
Das Problem bei der Verwendung von / ist, dass es sich um den Verzeichnisbegrenzer handelt und findDateinamen mit Pfaden einschließlich Schrägstrichen zurückgibt. Ändern Sie das Semikolon in Ihrem Skript in einen Schrägstrich und das Echo gibt das Verzeichnis und den Dateinamen in separaten Zeilen aus.
Bis auf weiteres angehalten.

Das sieht auch ganz nützlich aus. Ich bin mit der Pipe auf whileOption gegangen , aber das sieht auch ganz brauchbar aus. Ja, in meiner ähnlichen Struktur musste ich später weiter analysieren. (Der Eingabedateiname wäre .ogg, der wie filesrcin der gst-Pipeline übergeben wird, aber eine entsprechende Endung in .mp3 basierend auf dem Ausgabeverzeichnis würde generiert und auch an die Pipeline übergeben werden filesink, und dies muss natürlich erfolgen für jede Datei, zusammen mit einigen echoan den Benutzer.)
Samuel Jaeschke

4

Wenn der Rumpf Ihrer Schleife mehr als ein einzelner Befehl ist, können Sie mit xargs ein Shell-Skript ausführen :

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Stellen Sie sicher, dass Sie den abschließenden Gedankenstrich (oder ein anderes 'Wort') einfügen, wenn die Shell der Sorte Bourne / POSIX angehört (es wird verwendet, um $ 0 im Shell-Skript festzulegen). Vorsicht ist auch beim Zitieren geboten, da das Shell-Skript nicht direkt an der Eingabeaufforderung, sondern in einer Zeichenfolge in Anführungszeichen geschrieben wird.


Ein weiteres interessantes Konzept. Danke - ich bin sicher, ich werde später eine Verwendung dafür finden :)
Samuel Jaeschke

1

in deiner aktualisierten frage hast du

mkdir -p \"${OUTPATH}${DIR}\"

das sollte sein

mkdir -p "${OUTPATH}${DIR}"

Vielen Dank. Fest. Es wurde auch zu FILENAME anstelle von DIR gelesen - Kopieren-Einfügen: P
Samuel Jaeschke

1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'

0

oder um das Ganze viel unkomplizierter zu machen:

% rsync -av --include='*/' --exclude='*' SRC DST

Dadurch wird die Verzeichnisstruktur von SRC in die Sommerzeit repliziert.


Nein, ich benötige eine solche iterative Struktur, mit der ich für jede Datei mehrere Codezeilen ausführen kann. "Jetzt brauche ich eine Möglichkeit, die ich auf diese Weise durchlaufen kann, da ich auch einen komplizierteren Befehl ausführen möchte, bei dem gstreamer für jede Datei in einer ähnlichen Struktur verwendet wird." Entschuldigung, wenn ich unklar war.
Samuel Jaeschke

Der Befehl, den ich gegeben habe, löst das Problem, nach dem Sie gefragt haben. Es spielt keine Rolle, ob dies nur ein Teil einer größeren "Pipeline" auf Ihrer Seite ist. für jemanden, der das in der frage beschriebene problem hat, funktioniert der rsync-ansatz. Also, keine Notwendigkeit, sich für mögliche Unklarheiten zu entschuldigen :)
Akira

Ja. Nein, ich meine, ich würde später eine ähnliche while... do... doneStruktur verwenden, um eine ähnliche Verarbeitung von find durchzuführen, bei der mehrere Codezeilen für jede Datei ausgeführt werden müssten (Modifizieren von Zeichenfolge, Echo, GST-Start usw.). ) und rsyncwürde dies nicht erreichen. Aus diesem Grund habe ich angegeben, dass ich in der Lage sein muss, einen komplizierteren Satz von Befehlen in einer ähnlichen Struktur auszuführen. Mein Skript verwendet diese Schleifenstruktur zweimal. Für die Frage habe ich also die mit weniger Rohheit in der Mitte gepostet.
Samuel Jaeschke

0

Wenn Sie GNU Parallel http: // www.gnu.org/software/parallel/ installiert haben, können Sie dies tun:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Sehen Sie sich das Einführungsvideo für GNU Parallel an, um mehr zu erfahren: http://www.youtube.com/watch?v=OpaiGYxkSuQ

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.