Erhalte Dateinamen ohne Erweiterung in Bash

1928
Hashim

Ich habe die folgende forSchleife, um sortalle Textdateien innerhalb eines Ordners einzeln zu erstellen (dh für jeden eine sortierte Ausgabedatei zu erzeugen).

for file in *.txt;  do printf 'Processing %s\n' "$file" LC_ALL=C sort -u "$file" > "./$_sorted"  done 

Dies ist nahezu perfekt, außer dass derzeit Dateien im folgenden Format ausgegeben werden:

originalfile.txt_sorted 

... während ich möchte, dass es Dateien im folgenden Format ausgibt:

originalfile_sorted.txt 

Dies liegt daran, dass die $Variable den Dateinamen einschließlich der Erweiterung enthält. Ich verwende Cygwin auf Windows. Ich bin nicht sicher, wie sich das in einer echten Linux-Umgebung verhalten würde, aber unter Windows wird diese Datei durch die Verschiebung der Erweiterung für Windows Explorer nicht zugänglich gemacht.

Wie kann ich den Dateinamen von der Erweiterung trennen, sodass ich das _sortedSuffix zwischen den beiden hinzufügen kann, sodass ich die ursprüngliche und sortierte Version der Dateien leicht unterscheiden kann, während die Dateierweiterungen von Windows erhalten bleiben?

Ich habe mir, was könnte sein, mögliche Lösungen, aber ich diese scheinen mehr ausgestattet mit komplizierteren Problemen zu tun hat . Noch wichtiger ist, dass sie mit meinem derzeitigen bashWissen über meinen Kopf gehen, und ich hoffe, dass es eine einfachere Lösung gibt, die für meine bescheidene forSchleife gilt, oder dass jemand erklären kann, wie diese Lösungen auf meine Situation angewendet werden.

6

1 Antwort auf die Frage

19
Kamil Maciorowski

Diese Lösungen, auf die Sie verweisen, sind wirklich gut. Bei einigen Antworten kann es an Erklärungen mangeln, also lass uns das klären, vielleicht noch etwas hinzufügen.

Diese Zeile von dir

for file in *.txt 

zeigt an, dass die Erweiterung im Voraus bekannt ist (Hinweis: In POSIX-kompatiblen Umgebungen wird die Groß- und Kleinschreibung beachtet, es *.txtstimmt nicht überein FOO.TXT). In diesem Fall

basename -s .txt "$file" 

sollte den Namen ohne die Erweiterung zurückgeben ( basenameentfernt auch den Verzeichnispfad: /directory/path/filenamefilename; in Ihrem Fall spielt es keine Rolle, da er $filekeinen solchen Pfad enthält). Um das Werkzeug in Ihrem Code zu verwenden, müssen Sie die Befehlsersetzung, die in der Regel wie folgt aussieht: $(some_command). Die Befehlsersetzung übernimmt die Ausgabe von some_command, behandelt sie als Zeichenfolge und platziert sie, wo sie $(…)ist. Ihre besondere Weiterleitung wird sein

… > "./$(basename -s .txt "$file")_sorted.txt" # ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this 

Verschachtelte Anführungszeichen sind hier in Ordnung, da Bash klug genug ist, um zu wissen, dass die Anführungszeichen in einem $(…)Paar gepaart sind.

Dies kann verbessert werden. Hinweis basenameist eine separate ausführbare Datei, keine eingebaute Shell (in Bash-Ausführung type basename, Vergleich mit type cd). Das Anlegen zusätzlicher Prozesse ist kostspielig und erfordert Ressourcen und Zeit. Das Ablegen in einer Schleife funktioniert normalerweise schlecht. Verwenden Sie daher das, was Ihnen die Shell bietet, um zusätzliche Prozesse zu vermeiden. In diesem Fall lautet die Lösung:

… > "./$_sorted.txt" 

Die Syntax wird unten für einen allgemeineren Fall erläutert.


Falls Sie die Erweiterung nicht kennen:

… > "./$_sorted.$" 

Die Syntax erklärt:

  • $- $file, aber der kürzeste passende String *.wird von vorne entfernt;
  • $- $file, aber der längste passende String *.wird von vorne entfernt; Verwenden Sie es, um nur eine Erweiterung zu erhalten;
  • $- $file, aber der kürzeste passende String .*wird am Ende entfernt; Verwenden Sie es, um alles außer Erweiterung zu erhalten;
  • $- $file, aber die längste Zeichenfolge .*wird vom Ende entfernt;

Der Musterabgleich ist glob-artig und nicht regulär. Das bedeutet, *ein Platzhalter für null oder mehr Zeichen, ?ein Platzhalter für genau ein Zeichen ( ?in Ihrem Fall ist dies jedoch nicht erforderlich ). Wenn Sie den gleichen Pattern-Matching-Mechanismus aufrufen ls *.txtoder for file in *.txt;verwenden. Ein Muster ohne Platzhalter ist zulässig. Wir haben bereits verwendet, $wo .txtdas Muster ist.

Beispiel:

$ file=name.name2.name3.ext $ echo "$" name2.name3.ext $ echo "$" ext $ echo "$" name.name2.name3 $ echo "$" name 

Aber Vorsicht:

$ file=extensionless $ echo "$" extensionless $ echo "$" extensionless $ echo "$" extensionless $ echo "$" extensionless 

Aus diesem Grunde ist die folgende contraption könnte nützlich sein (aber es ist nicht, Erklärung unten):

$} 

Es identifiziert alles außer extension ( $) und entfernt es dann aus dem gesamten String. Die Ergebnisse sind wie folgt:

$ file=name.name2.name3.ext $ echo "$}" .ext $ file=extensionless $ echo "$}"  $ # empty output above 

Beachten Sie, dass .diesmal enthalten ist. Sie erhalten möglicherweise unerwartete Ergebnisse, wenn Sie ein $fileLiteral *oder enthalten ?. Aber Windows (wo Erweiterungen eine Rolle spielen) lässt diese Zeichen in Dateinamen sowieso nicht zu, so dass es Sie vielleicht nicht interessiert. Jedoch […]oder {…}, falls vorhanden, kann ihr eigenes Muster Anpassungsschema auslösen und die Lösung brechen!

Ihre "verbesserte" Weiterleitung wäre:

… > "./$_sorted$}" 

Es sollte Dateinamen mit oder ohne Erweiterung unterstützen, allerdings nicht mit eckigen oder geschweiften Klammern. Schade eigentlich. Um dies zu beheben, müssen Sie die innere Variable doppelt zitieren.

Wirklich verbesserte Weiterleitung:

… > "./$_sorted$"}" 

Doppelte Anführungszeichen $wirken nicht als Muster! Bash ist intelligent genug, um innere und äußere Anführungszeichen voneinander zu unterscheiden, da die inneren in die äußere ${…}Syntax eingebettet sind . Ich denke, das ist der richtige Weg .

Eine weitere (unvollkommene) Lösung, analysieren wir sie aus pädagogischen Gründen:

$ 

Es ersetzt das erste .mit _sorted.. Es wird gut funktionieren, wenn Sie höchstens einen Punkt haben $file. Es gibt eine ähnliche Syntax $, die alle Punkte ersetzt. Soweit ich weiß, gibt es keine Variante, die nur den letzten Punkt ersetzt.

Immer noch die erste Lösung für Dateien, die .robust aussehen. Die Lösung für erweiterungs $fileist trivial: $_sorted. Jetzt brauchen wir nur noch eine Möglichkeit, die beiden Fälle voneinander zu unterscheiden. Hier ist es:

[[ "$file" == *?.* ]] 

Exit-Status 0 (wahr) wird nur dann zurückgegeben, wenn der Inhalt der $fileVariablen dem Muster auf der rechten Seite entspricht. Das Muster sagt "es gibt nach mindestens einem Zeichen einen Punkt" oder "Es gibt einen Punkt, der nicht am Anfang steht". Es geht darum, versteckte Linux-Dateien (zB .bashrc) als erweiterungslos zu behandeln, es sei denn, es gibt irgendwo einen anderen Punkt.

Beachten Sie, dass wir [[hier nicht brauchen [. Ersteres ist leistungsfähiger, aber leider nicht portabel ; Letzteres ist tragbar, aber für uns zu begrenzt.

Die Logik geht jetzt so:

[[ "$file" == *?.* ]] && file1="./$_sorted.$" || file1="$_sorted" 

Danach, $file1enthält den gewünschten Namen, so sollten Sie Ihre Umleitung sein

… > "./$file1" 

Und das gesamte Code-Snippet ( *.txtersetzt durch, *um anzuzeigen, dass wir mit einer Erweiterung oder keiner Erweiterung arbeiten):

for file in *;  do printf 'Processing %s\n' "$file" [[ "$file" == *?.* ]] && file1="./$_sorted.$" || file1="$_sorted" LC_ALL=C sort -u "$file" > "./$file1"  done 

Dies würde auch versuchen, Verzeichnisse (falls vorhanden) zu verarbeiten. Sie wissen bereits, was Sie tun müssen, um das Problem zu beheben.

Nochmals eine brillante Antwort, danke. Ich bin definitiv weit davon entfernt, alles zu verstehen, aber jetzt lasse ich das zur Seite und lese einfach mehr über die Befehlsersetzung, wenn ich Zeit habe. Eine Frage, die ich habe: Sie haben erwähnt, dass "...>" ./$ _sorted.txt "` "vermeidet zusätzliche Prozesse" - liegt daran, dass wir basename in der Variablen "$ file" außerhalb von verwenden die `for` Schleife hier:` basename -s .txt "$ file" `... oder habe ich das falsch verstanden? Hashim vor 5 Jahren 0
@Hashim `…>" ./$ _sorted.txt "" ist die einzige Änderung, die Sie an Ihrem Skript vornehmen müssen (Ellipse "...") zeigt nur alles an, was Sie vor ">" haben, es ist * nicht * ein tatsächliches Zeichen, das Sie in Ihr Skript einfügen sollten; ersetzen Sie `>` und den Rest der Zeile durch `> ./$ _sorted.txt" `). Es vermeidet zusätzliche Prozesse, da wir jetzt `basename` * * überhaupt nicht verwenden. Die gesamte Magie wird von der Shell selbst dank der $ -Syntax ausgeführt. Randbemerkung: Alleine `basename -s .txt" $ file "` druckt etwas; Wenn Sie denken, dass die Variable verändert wird, liegen Sie falsch. Kamil Maciorowski vor 5 Jahren 0
Ah, also wird die Befehlssubstitution anstelle von 'basename' anstatt von daneben verwendet. Aha. Danke nochmal für deine Hilfe. Hashim vor 5 Jahren 0
@Hashim Nicht ganz. Dieses Fragment `>" ./$(basename -s .txt "$ file") _ sortiert.txt "" verwendet die Befehlsersetzung, der Befehl lautet "basename…". Sie verwenden entweder diese oder `" ./$ _sorted.txt "`, die keine Befehlsersetzung verwendet. Es ist also (Befehlssubstitution + `basename`) * xor * nur eine Erweiterung der Erweiterungsvariablen $ ` ohne Befehlsersetzung. Kamil Maciorowski vor 5 Jahren 1
@Hashim Oder vielleicht habe ich dein "anstelle von" basename "nicht verstanden. Kamil Maciorowski vor 5 Jahren 0
Ah ich sehe. Sieht aus, als müsste ich in diesem Fall auch nach einer variablen Erweiterung suchen, haha. In jedem Fall habe ich die Befehlssubstitutions- / basename-Methode für meine for-Schleife angewendet, und ich habe auch bemerkt, dass es eine leichte Masche in der Funktionsweise gibt ... Hashim vor 5 Jahren 0
Wenn der ursprüngliche Dateiname der Datei (einschließlich der Erweiterung) eckige Klammern enthält, in denen sogar ein einziges (normales) Zeichen enthalten ist, wie "[i]", wird der Dateiname der Ausgabe in "originalfile_sortedoriginalfile.txt" - In Mit anderen Worten fügt er den ursprünglichen Dateinamen erneut an den neuen Dateinamen an *, wenn dies nicht der Fall ist. Die einzige Ursache hierfür sind eckige Klammern mit mindestens 1 Zeichen. Klammern, geschweifte Klammern und einfache oder leere eckige Klammern verursachen dieses Problem nicht. Hashim vor 5 Jahren 0
Lassen Sie uns [diese Diskussion im Chat fortsetzen] (https://chat.stackexchange.com/rooms/83190/discussion-between-kamil-maciorowski-and-hashim). Kamil Maciorowski vor 5 Jahren 0
Vielen Dank für die zusätzlichen Anstrengungen, nachdem ich Sie auf das Problem der eckigen Klammern aufmerksam gemacht habe. Zur Klarstellung: `file1 =" ./$ _ Sortiert. $ "` `Unterstützt nur Erweiterungen mit einem Punkt, daher sollte die endgültige Lösung nicht für Dateien verwendet werden die mehr als eine Periode in sich haben? Ich habe keine Notwendigkeit dazu, ich möchte nur sicherstellen, dass dies klar ist, da es in der aktuellen Antwort nicht so scheint, und ich möchte sicherstellen, dass ich diese Schleife in Zukunft nicht missbrauchen kann. Hashim vor 5 Jahren 0
@Hashim Mehr als ein Punkt ist damit kein Problem. Null Punkte sind. Kamil Maciorowski vor 5 Jahren 0
Ah, ich verstehe jetzt, es gibt so viele Code-Schnipsel in der Antwort, dass es an diesem Punkt verwirrend ist. Dies ist der letzte Code, den ich noch übrig habe: https://pastebin.com/6XvWdcKB. Es funktioniert perfekt für meine aktuellen Daten, aber wenn Sie nichts dagegen haben, wäre ich Ihnen dankbar, wenn Sie darüber nachsehen, was ich vermisse. Vor allem dachte ich, es brauche möglicherweise eine if -Anweisung, um alles zu enthalten und zu brechen, falls [-f "$ file"] `fehlschlägt. Hashim vor 5 Jahren 0
Ich habe meine Antwort verbessert und eine relativ einfache, aber robuste Lösung hinzugefügt. Finden Sie heraus, wo sich der fette Text befindet (oder verwenden Sie [Revisionsverlauf] (https://superuser.com/posts/1358024/revisions)). Kamil Maciorowski vor 5 Jahren 0