Führen Sie für jede Datei einen Befehl mit einem Flag-Argument aus, das vom Dateinamen abhängt

7

Angenommen, ich habe einen Ordner mit Dateien mit Namen wie

file1.txt
file2.txt
file2.txt

usw. Ich möchte für jeden einen Befehl ausführen, wie folgt:

mycommand file1.txt -o file1-processed.txt
mycommand file2.txt -o file2-processed.txt
mycommand file3.txt -o file3-processed.txt

usw.

Es gibt mehrere ähnliche Fragen auf dieser Site - der Unterschied besteht darin, dass ich den -processedTest vor der Erweiterung in die Mitte des Dateinamens einfügen möchte .

Es scheint, findsollte das Werkzeug für den Job sein. Ohne die -oFlagge könnte ich das tun

find *.txt -exec mycommand "{}" ";"

Die {}Syntax gibt jedoch den gesamten Dateinamen an, z file1.txt. B. usw., sodass ich das " -processed" nicht zwischen dem Dateinamen und seiner Erweiterung einfügen kann. Ein ähnliches Problem besteht bei der Verwendung einer einfachen Bash- forSchleife.

Gibt es eine einfache Möglichkeit, diese Aufgabe mit findoder auf andere Weise zu erfüllen ?

Nathaniel
quelle
1
Zu Ihrer Information, find *.txtfinden Sie alle Dateien und Verzeichnisse im aktuellen Verzeichnis, deren Namen mit enden .txt, und dann alles unter diesen Verzeichnissen (falls vorhanden).
G-Man sagt "Reinstate Monica"

Antworten:

25

Wenn sich alle zu verarbeitenden Dateien im selben Ordner befinden, müssen Sie sie nicht verwenden findund können mit nativem Shell-Globbing auskommen.

for foo in *.txt ; do
  mycommand "${foo}" -o "${foo%.txt}-processed.txt"
done

Das Shell-Idiom ${foo%bar}entfernt die kleinste Suffix-Zeichenfolge, die dem Muster entspricht, baraus dem Wert von foo, in diesem Fall der .txtErweiterung, damit wir sie durch das gewünschte Suffix ersetzen können.

user1404316
quelle
Um sicherzugehen, dass dies mycommandnur für reguläre Dateien aufgerufen wird , könnte man einfügen [ -f "$foo" ] && oder test -f "$foo" && davor mycommand ....
Kusalananda
6

Wenn Sie ein Skript schreiben, das auf verschiedenen Systemen ausgeführt werden soll, und die Portabilität ein Problem darstellt , ist nichts besser alsfor Schleifen und die Antwort ${var%sfx}von user1404316 .

Wenn Sie jedoch nach einer bequemen Möglichkeit suchen, ähnliche Dinge auf Ihrem eigenen System zu tun, empfehle ich von Herzen GNU Parallel , das sich hauptsächlich xargsauf Steroide bezieht. Hier ist eine Lösung für Ihre spezielle Aufgabe:

parallel mycommand {} -o {.}-processed.txt ::: *.txt

Es wird eine Liste von Zeichenfolgen nach :::(Shell würde *.txtzu einer Liste von übereinstimmenden Dateinamen erweitert) und mycommand {} -o {.}-processed.txtfür jede Zeichenfolge ausgeführt, wobei {}durch Eingabezeichenfolge (genau wie xargs) und {.}durch Dateinamen ohne Erweiterung ersetzt wird.

Sie können die Liste der Eingabezeichenfolgen über stdin( xargs -way) eingeben, um sie mit findoder zu koppeln locate, aber ich benötige selten mehr als zshdie erweiterten Globs.

Schauen Sie sich das GNU Parallel-Tutorial an, um eine Vorstellung davon zu bekommen, was es kann. Ich benutze es die ganze Zeit für Batch-Konvertierungen, das Extrahieren von Archiven in Sudirectories usw.

Klas Š.
quelle
4

Dadurch wird die Antwort von user1404316 an folgende Bedingungen angepasstfind :

find . -type f -name '*.txt' \
   -exec sh -c 'for file do mycommand "$file" -o "${file%.txt}-processed.txt"; done' sh {} +

(Sie können das alles in eine Zeile eingeben; lassen Sie das einfach weg \. Ich habe es zur besseren Lesbarkeit in zwei Zeilen unterteilt.)


Eine andere Möglichkeit, es für die Lesbarkeit zu formatieren, macht das eingebettete Shell-Skript ein bisschen klarer:

find . -type f -name '*.txt' -exec sh -c '
  for file
  do
    mycommand "$file" -o "${file%.txt}-processed.txt";
  done
' sh {} +

Grundsätzlich wird ein unbenanntes Shell-Skript erstellt:

for file
do
    mycommand "$file" -o "${file%.txt}-processed.txt"
done

(das ist die Zeichenfolge zwischen den einfachen Anführungszeichen, '…'abgewickelt) und übergibt sie als Befehl ( sh -c) mit den Namen aller Ihrer .txtDateien als Parameter an die Shell . (Sie müssen im Allgemeinen nicht zitieren {}, und Sie brauchen keine geschweiften Klammern "$file".)

G-Man sagt "Reinstate Monica"
quelle
Sehr schöne Lösung; Genau das wollte ich schreiben, wenn Sie es noch nicht getan hatten. :) Ich habe eine Bearbeitung hinzugefügt; Sehen Sie, ob es Ihnen gefällt - aber ich habe den umgebenden Text im Lichte der Bearbeitung nicht aufgeräumt, sodass Sie vielleicht einen Blick darauf werfen möchten.
Wildcard
@Wildcard: Ihre Bearbeitung erscheint mir etwas ausführlich, aber ich nehme an, dass sie für andere nützlich sein könnte. Ich sehe im umgebenden Text nichts, was wirklich geändert werden muss.
G-Man sagt "Reinstate Monica"
Das ist dann gut. Wenn ich wirklich alles daran setzen würde, es zu bearbeiten, würde ich nur die von mir gepostete Version mit Zeilenumbruch belassen, da das unbenannte Shell-Skript (innerhalb des '...') dadurch leicht zu sehen ist, ohne dass es separat erneut präsentiert werden muss. Aber ich bin ein bisschen ein Perfektionist in solchen Dingen. :)
Wildcard
4

Mit zsh:

for f (*.txt) mycommand -o $f:r-processed.txt -- $f
  • Vermeiden Sie die Verwendung von Optionen nach Nicht-Optionsargumenten. Dies wird von wenigen Befehlen unterstützt, und für die meisten, die dies tun (wie diejenigen, die die GNU getopt-API verwenden), nicht, wenn sie $POSIXLY_CORRECTsich in der Umgebung befinden. Das bedeutet auch, dass Sie Probleme mit Dateinamen, die mit -(wie --hier) beginnen, nicht umgehen können, außer mit ./-file-.txt.
  • for var (values) cmdist eine praktische Kurzform der for-Schleife, die an perldie Syntax erinnert.
  • zshist eine dieser seltenen Shells, bei denen $fkeine Anführungszeichen erforderlich sind (vorausgesetzt, sie befindet sich nicht in einem Modus, in dem andere Shells emuliert werden).
  • $file:rwird auf den Stammnamen der Datei erweitert, dh die Erweiterung wird wie in entfernt csh. Es ist mehr oder weniger gleichbedeutend mit der Korn - Shell ist ${f%.*}mit dem zusätzlichen Vorteil , dass es keine Kreuz /Grenzen (mit f=./foo, $f:rist ./fooanstelle des leeren String), obwohl es hier nicht anwendbar.
  • das schließt versteckte Dateien (wie .foo.txt) aus. Wenn Sie möchten, dass sie eingeschlossen werden, können Sie das Dglob-Qualifikationsmerkmal ( *.txt(D)) hinzufügen , aber Sie möchten möglicherweise eine Datei ausschließen, die .txtalleine aufgerufen wird, da dann die Ausgabedatei -processed.txtmit ideal wäre (und auch nicht ausgeblendet wäre) ?*.txt(D).
  • Wenn sich *.txtim aktuellen Verzeichnis keine Datei befindet, schlägt dies mit einem Fehler fehl (und wird zum Glück mycommandim Gegensatz zu anderen Shells überhaupt nicht ausgeführt). Wenn Sie möchten, dass die Schleife stattdessen nichts tut, können Sie das Nglob-Qualifikationsmerkmal ( *.txt(N)) hinzufügen .
  • Beachten Sie, dass dies alle *.txtDateien unabhängig von ihrem Typ umfasst (regulär, Symlink, Verzeichnis, FIFO, Socket, Gerät ...). Wenn Sie nur reguläre Dateien berücksichtigen möchten , können Sie das .Glob-Qualifikationsmerkmal hinzufügen (oder -.auch Symlinks zu regulären Dateien hinzufügen, dh zu den Dateien, für die [ -f "$f" ]true zurückgegeben wird). Also, mit all den oben genannten angewendet:

    for f (?*.txt(ND.)) mycommand -o $f:r-processed.txt -- $f
  • *.txtUm auch in Unterverzeichnissen nach Dateien zu suchen , ändern Sie diese in **/*.txt(mit dem DFlag wird sie auch in versteckten Verzeichnissen angezeigt).

Stéphane Chazelas
quelle
3

Eine Kombination aus find, sedund xargskönnte funktionieren, wenn Sie die Rekursion und komplexe String - Ersetzung benötigen:

find . -iname '*.txt' -printf "%p\0-o\0%p\0" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand

Beispiel:

$ find
.
./bar baz.txt
./foo.txt
$ find . -iname '*.txt' -printf "%p\0-o\0%p\0" | sed -z '3~3s/\.txt$/-processed&/' | xargs -0 -n 3 echo mycommand
mycommand ./bar baz.txt -o ./bar baz-processed.txt
mycommand ./foo.txt -o ./foo-processed.txt

Der -printf "%p\0-o\0%p\0"druckt den Dateipfad zweimal, wobei -odazwischen das ASCII-NUL-Zeichen verwendet wird, und der sedBefehl fügt in jeder dritten Zeile ein -processedvor ein nachfolgendes Zeichen ein .txt. Anschließend wird xargsder Befehl mit drei Argumenten gleichzeitig ausgeführt, nämlich dem Dateinamen -ound dem bearbeiteten Dateinamen.

muru
quelle
0

Man könnte immer den basenameBefehl verwenden, der Teil der GNU-Coreutils ist. Aus dem Verzeichnis mit den Dateien:

for filename in ./file*.txt
do
    mycommand "$filename" -o ./"$(basename -s '.txt' "$filename" )"-processed.txt
done
Sergiy Kolodyazhnyy
quelle