Konvertieren von "for file in" in "find", damit mein Skript rekursiv angewendet werden kann


7

Ich habe die Idee, ein Bash-Skript auszuführen, um einige Bedingungen zu überprüfen und ffmpegalle Videos in meinem Verzeichnis von einem beliebigen Format in ein anderes zu konvertieren, .mkvund es funktioniert hervorragend!

Die Sache ist, ich wusste nicht, dass eine for file inSchleife nicht rekursiv funktioniert ( /programming/4638874/how-to-loop-through-a-directory-recursively )

Aber ich verstehe "Piping" kaum und freue mich darauf, ein Beispiel zu sehen und einige Unsicherheiten zu beseitigen.

Ich denke an dieses Szenario, von dem ich denke, dass es mir sehr helfen würde, es zu verstehen.

Angenommen, ich habe dieses Bash-Skript-Snippet:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

Suchen Sie für das aktuelle Verzeichnis nach einem beliebigen Verzeichnis und *.mkv *avi *mp4 *flv *ogg *movdeklarieren Sie die Ausgabe als Erweiterung. .mkvAnschließend löschen Sie die Originaldatei. Die Ausgabe sollte dann in demselben Ordner gespeichert werden, in dem sich das Originalvideo befindet.

  1. Wie kann ich dies konvertieren, um es rekursiv auszuführen? Wenn ich benutze find, wo soll die Variable deklariert werden $file? Und wo solltest du deklarieren $target? Sind alle findnur wirklich Einzeiler? Ich muss die Datei wirklich an eine Variable übergeben $file, da ich die Bedingungsprüfung noch ausführen muss.

  2. Und unter der Annahme, dass (1) erfolgreich ist, wie kann sichergestellt werden, dass die Anforderung "Dann sollte die Ausgabe in demselben Ordner gespeichert werden, in dem sich das Originalvideo befindet" erfüllt ist?


1
Einige Shells verfügen über einen **Globing-Operator, mit dem rekursiv nach Dateien gesucht werden kann. Es ist nicht portabel, spielt aber gut mit der For-Loop-Syntax.
Hugomg

1
Am Ende habe ich die Reihe der Kommentare, die ich geschrieben habe, in eine eigene Antwort verwandelt und Ihre kleinen Fragen wie das Deklarieren von Shell-Vars beantwortet.
Peter Cordes

Antworten:


5

Sie haben diesen Code:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

welches im aktuellen Verzeichnis läuft. Um daraus einen rekursiven Prozess zu machen, haben Sie mehrere Möglichkeiten. Am einfachsten (IMO) ist es, findwie von Ihnen vorgeschlagen zu verwenden. Die Syntax für findist sehr "un-UNIX-ähnlich", aber das Prinzip hier ist, dass jedes Argument mit UND- oder ODER-Bedingungen angewendet werden kann. Hier werden wir sagen " Wenn dieser Dateiname mit ODER dieser Dateiname übereinstimmt, dann drucke ihn aus ". Die Dateinamen Muster zitiert werden , so dass der Schal nicht halten , sie bekommen kann (denken Sie daran , dass die Schale für den Ausbau aller nicht notierten Muster verantwortlich ist, also , wenn Sie ein nicht notierte Muster hatten *.mp4und man mußte janeeyre.mp4in Ihrem aktuellen Verzeichnis, würde die Schale ersetzen *.mp4mit das Spiel, und findwürde -name janeeyre.mp4statt Ihrer gewünschten sehen -name *.mp4, es wird schlimmer, wenn*.mp4stimmt mit mehreren Namen überein ...). Den Klammern wird auch ein Präfix vorangestellt \, damit die Shell nicht versucht, sie als Unterschalenmarkierungen zu verwenden (wir könnten stattdessen die Klammern zitieren, falls dies bevorzugt wird :) '('.

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print

Die Ausgabe davon muss in die Eingabe einer whileSchleife eingespeist werden, die jede Datei der Reihe nach verarbeitet:

while IFS= read file    ## IFS= prevents "read" stripping whitespace
do
    target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

Jetzt müssen nur noch die beiden Teile mit einer Pipe verbunden werden, |sodass der Ausgang von findzum Eingang der whileSchleife wird.

Während Sie diesen Code zu test würde ich empfehlen , Sie beide Präfix ffmpegund rmmit echoso können Sie sehen , was würde ausgeführt werden - und mit welchen Pfaden.

Hier ist das Endergebnis, einschließlich der echoAussagen, die ich zum Testen empfehle:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print |
    while IFS= read file    ## IFS= prevents "read" stripping whitespace
        do
            target="${file%.*}.mkv"
            echo ffmpeg -i "$file" "$target" && echo rm -rf "$file"
        done

1
Die Shell erweitert zuerst die Inhalte, was unerwartete Folgen haben kann. Versuchen Sie es echo *in einem leeren Verzeichnis. Versuchen Sie es dann in einem nicht leeren Verzeichnis. Ersteres druckt *und letzteres ersetzt den Stern durch eine Liste von Dateien. Das gleiche passiert mit find.
Sobrique

1
@ TheWolf xargskann ein Minenfeld sein. Sei sehr vorsichtig damit.
Roaima

2
@ TheWolf: Wenn xargsund findohne -0Option, haben Sie immer noch Probleme. Lassen Sie finddie ganze Arbeit mit -execist bessere Lösung.
Cuonglm

1
@ CharlesDuffy, du kannst es gerne verbessern. Ich wollte etwas, das so einfach ist, dass es von einem Anfänger verstanden werden kann. Wenn wir anfangen, -print0die anderen professionellen Ausstattungen zu verwenden, erhalten Sie zwar solideren Code, aber mehr Komplexität, die erklärt werden muss.
Roaima

1
Es ist besser, von Anfang an sichere Methoden zum Erstellen von Skripten zu erlernen, als Methoden zu lernen, die bei ungeraden Dateinamen brechen. (Bis zu einem gewissen Punkt jedenfalls. IFS = read ... ist ziemlich gut und wird von mywiki.wooledge.org/BashFAQ/001 vorgeschlagen. )
Peter Cordes

6

Mit POSIX finden Sie:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \
          -name '*ogg' -o -name '*mov' \) -exec sh -c '
  for file do
    target="${file%.*}.mkv"
    echo ffmpeg -i "$file" "$target"
  done' sh {} +

Ersetzen Sie echodurch einen beliebigen Befehl, den Sie verwenden möchten.

Wenn Sie GNU find oder BSD find haben, können Sie verwenden -regex:

find . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'

Entschuldigung, aber ich kann nicht verfolgen, wie das Hinzufügen find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \ -name '*ogg' -o -name '*mov' \) -exec sh -c 'am oberen Rand des Snippets nach unten geleitet wird file. und was bedeutet sh {} +? Vielen Dank!
Arvil

Nicht tragbar, aber kürzerfind . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'
Costas

1
@ TheWolf: Ich lade Sie ein, unix.stackexchange.com/q/93324/38906
cuonglm

2

Beispiel-Snippet ohne Piping (vorausgesetzt, Sie geben den Pfad als Argument an):

#!/bin/bash

backup_dir=/backup/

OIFS="$IFS"
IFS=$'\n'

files="$(find "$1" -type f -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv')"

for f in $files; do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir" 
done

IFS="$OIFS"

Die Schale liest die IFSVariable, die (festgelegt ist space, tab, newline) standardmäßig. Dann wird jedes Zeichen in der Ausgabe von betrachtet find. Wenn es also spacedenkt, dass es das Ende des Dateinamens ist (eine Datei mit Leerzeichen, zum Beispiel "Sin City.avi", wird als zwei Dateien "Sin" und "City.avi" behandelt). Mit IFS = $ '\ n' sagen wir also, dass die Eingabe aufgeteilt werden soll newlines. Und schließlich stellen wir alt (Standard) wieder her, IFSdas in $OIFSVariablen gespeichert ist.
Oder wie in Kommentaren vorgeschlagen, könnte ein besserer Ansatz sein:

#!/bin/bash

backup_dir=/backup/

find "$1" -type f \( -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv' \) -print0 | while IFS= read -r -d '' f
do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir"
done

Das scheint praktisch zu sein, aber eigentlich habe ich die Deklaration von vars OIFSund IFSam Anfang und am Ende des Skripts nicht verstanden. und wenn ich richtig verstehe, wird der Pfad bei var deklariert $1? und das backup_dirist nur ein Backup zum Debuggen, oder?
Arvil

1
IFS ist eine Variable, die definiert, wie Ihr Feldtrennwert aussehen soll. Das Standard-IFS ist normalerweise "Whitespace". Leerzeichen, Tabulatoren, Zeilenumbrüche usw. werden daher als "Feldtrennzeichen" erkannt. Da einige Filme und Songs möglicherweise mit Leerzeichen versehen sind, werden in diesem Skript Leerzeichen und Tabellen ignoriert und Zeilenumbrüche als Trennzeichen verwendet. Auf diese Weise wird "Terminator 2.mp4" als 2 Filme erkannt, "Terminator" und "2.mp4" als 1 Film "Terminator 2.mp4". Da IFS als OIFS gespeichert wurde, wird dies am Ende des Skripts als Standard festgelegt.
Tim Kennedy

Eine andere Möglichkeit, dies zu tun, besteht darin, Ihren Code in eine Shell-Funktion einzufügen local IFS=, die dies tut . Daher ist IFS nur für den Kontext Ihrer Funktion leer und muss nicht gespeichert / wiederhergestellt werden.
Peter Cordes

1

Willkommen bei Unix :)

Um einige Ihrer Nebenfragen zu beantworten, die in den Antworten auf die Hauptfrage nicht behandelt wurden:

Shell-Skripte haben sicherlich einige Ecken und Kanten, da bei Dateinamen mit Leerzeichen viele Dinge kaputt gehen. Und fast alles bricht bei Dateinamen mit Zeilenumbrüchen ab (zum Glück macht niemand diese absichtlich). Dateinamen mit Glob-Zeichen wie [, ]und *sind manchmal auch ein Problem. Manchmal lohnt es sich einfach nicht, schwer lesbaren Shell-Code zu schreiben, der den Standards von Wooledges BashGuide entspricht , für den eigenen Gebrauch oder für einen einmaligen Vorgang , bei dem Sie wissen, dass Ihre Dateinamen nicht seltsam sind.

Wo soll die Variable deklariert werden?

Shell-Variablen müssen nicht deklariert werden. In Bash können Sie die shopt -o nounsetReferenz- und UnSET-Variable zu einem Fehler machen, aber das ist nicht ganz dasselbe wie nicht deklariert. Das Deaktivieren einer Variablen kann hilfreich sein. In einer Shell-Funktion empfiehlt es sich, alle Ihre Provisorien mit zu deklarieren local foo bar baz;, damit Sie die Shell-Umgebung nicht mit Variablen verunreinigen oder, schlimmer noch, auf die gleichnamige Variable des Aufrufers treten.

Ich verstehe "Rohrleitungen" kaum.

Bei der Arbeit mit der Shell werden viele Daten übergeben, indem die Daten auf stdout gedruckt werden. Pipes senden diese Daten an ein anderes Programm, das sie auf stdin liest (und normalerweise etwas auf stdout druckt). Sie können die Ausgabe in Shell-Variablen mithilfe der Befehlssubstitution erfassen $(). zB for i in $( locate foo | grep bar );do echo "$i"; done. (Dies wird bei Dateinamen mit Leerzeichen unterbrochen, wie z. B. viel Shell-Code, wenn Sie nicht vorsichtig sind. Verwenden readSie diese Option, wenn Sie zuverlässige Skripts schreiben möchten.) locateDruckt, grepliest und druckt, und die Shell liest die Ausgabe von grep. (Die Shell erhält die Ausgabe von, grepindem sie grep mit ihrer Ausgabe startet, die mit der Eingabeseite einer von der Shell erstellten Pipe verbunden ist. Die Shell liest die Ausgabeseite der Pipe.)

Eine Pipe ist nur eine Möglichkeit für Programme, so zu arbeiten, als würden sie in eine Datei schreiben, aber tatsächlich schreiben sie in einen kleinen Puffer. Bei einem Prozess, der aus einer Pipe liest, wird der read(2)Systemaufruf zurückgegeben, wenn Daten verfügbar sind. Dies geschieht nur, wenn etwas an das andere Ende der Pipe geschrieben wird.

Die Schale ist |, $()und einige andere Syntaxelemente sind , wie Sie die Schale sagen , wie die Sanitär - Verbindungsprogramme miteinander zu gründen, und an der Schale.

Es ist leicht, schlechte Redewendungen für die Shell-Programmierung zu lernen, da viele der offensichtlichen Dinge und alten Methoden versteckte Fallstricke aufweisen, die bei ungeraden Dateinamen auftreten. Siehe zum Beispiel http://mywiki.wooledge.org/BashFAQ/001 .

Es ist besser, von Anfang an sichere Methoden zum Erstellen von Skripten zu erlernen, als Methoden zu lernen, die bei ungeraden Dateinamen brechen, solange sie nicht zu klobig zum Tippen sind. :) :)

Viele GNU-Utils haben die Option -0, um ASCII NUL (das 0-Byte kann in Dateinamen oder Text nicht vorhanden sein) als Datensatztrennzeichen zu verwenden. Auf diese Weise können Sie Daten zwischen findund sortbeispielsweise weiterleiten, ohne dass eine "Zeile" der Suchausgabe in mehrere Zeilen der Sortiereingabe umgewandelt werden kann. Dies ist nicht besonders nützlich, wenn Sie die Daten in eine Shell-Variable übertragen möchten, da bash keine Möglichkeit hat, \0begrenzte Zeilen zu lesen . (Ich denke nicht, dass dies ein gültiger Wert für IFS ist.)

Auf jeden Fall ist es der Grund, zu vermeiden, dass die Shell Daten als Code behandelt, alles, was Sie können, immer in doppelten Anführungszeichen zu setzen, es sei denn, Sie möchten wirklich eine Wortteilung. Wenn Sie jemals möchten, dass Ihr Gehirn beim Betrachten von komplexem Shell-Code verletzt wird, schauen Sie sich einfach den Bash-Completion-Code an. (Es behandelt die programmierbare Vervollständigung, die clevere Dinge wie das Vervollständigen ls --colo => --coloroder nur das Vervollständigen von * .zip-Dateien zum Entpacken erledigt.) set -xUnd drücken Sie die Tabulatortaste: P. (Setzen Sie + x, um die Ausführungsverfolgung zu deaktivieren.)

re: your for loop: Mit *.mkveinem Ihrer Muster haben Sie source = dest für diese Eingabedateien. ffmpegfordert Sie auf, die Ausgabedatei für jede Datei zu überschreiben.

Müssen Sie das Audio auch wirklich transkodieren? -c:a copykönnte eine gute Idee sein. Die Videobitrate ist normalerweise eine größere Sache. Und Sie möchten möglicherweise -preset slow(oder slowersogar veryslow) verwenden, um mehr Qualität pro Bitrate zu erzielen, und zwar auf Kosten einer höheren CPU-Auslastung. Es gibt auch -crf 20(Standard 23). https://trac.ffmpeg.org/wiki/Encode/H.264 . Sie haben das hoffentlich schon gewusst und es -c:v libx264weggelassen, weil es für das Bash-Scripting nicht relevant war, aber nur für den Fall ...: P ist die Standardeinstellung bei der Ausgabe auf mkv, also ist das gut.

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.