Wie kann ich das erste Match aus der Wildcard-Erweiterung erhalten?


37

Shells wie Bash und Zsh erweitern Platzhalter in Argumente, so viele Argumente wie dem Muster entsprechen:

$ echo *.txt
1.txt 2.txt 3.txt

Aber was ist, wenn ich nur die erste Übereinstimmung zurückgeben möchte, nicht alle Übereinstimmungen?

$ echo *.txt
1.txt

Ich habe nichts gegen Shell-spezifische Lösungen, aber ich möchte eine Lösung, die mit Leerzeichen in Dateinamen arbeitet.


ls * .txt | Kopf -1?
Archemar

1
@Archemar: Funktioniert nicht mit Zeilenumbrüchen in Dateinamen.
Flimm

Antworten:


24

Eine robuste Methode in bash besteht darin, in ein Array zu expandieren und nur das erste Element auszugeben:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Sie können sogar nur echo $fileseinen fehlenden Index als [0] behandeln.)

Dies behandelt sicher Leerzeichen, Tabulatoren, Zeilenumbrüche und andere Metazeichen, wenn die Dateinamen erweitert werden. Beachten Sie, dass die Gebietsschemaeinstellungen das ändern können, was "zuerst" ist.

Sie können dies auch interaktiv mit einer Bash- Vervollständigungsfunktion tun :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Dadurch wird die _echoFunktion an die Vervollständigung von Argumenten an den echoBefehl gebunden (wobei die normale Vervollständigung außer Kraft gesetzt wird). Ein zusätzliches "*" wird im obigen Code angehängt. Sie können einfach auf einen Teil des Dateinamens tippen, und hoffentlich wird das Richtige passieren.

Der Code ist leicht verworren, anstatt set oder assume nullglob( shopt -s nullglob) zu verwenden. Wir überprüfen compgen -G, ob der Globus auf einige Übereinstimmungen erweitert werden kann, erweitern ihn dann sicher zu einem Array und setzen schließlich COMPREPLY, damit das Zitieren robust ist.

Sie können dies teilweise mit bash tun (programmgesteuert ein Glob erweitern) compgen -G, aber es ist nicht robust, da es ohne Anführungszeichen für stdout ausgibt.

Wie üblich ist die Vervollständigung ziemlich kompliziert, was die Vervollständigung anderer Dinge, einschließlich Umgebungsvariablen, bricht ( Einzelheiten zum Emulieren des Standardverhaltens finden Sie in der _bash_def_completion()Funktion hier ).

Sie können auch nur compgenaußerhalb einer Vervollständigungsfunktion Folgendes verwenden:

files=( $(compgen -W "$pattern") )

Ein Punkt, den Sie beachten sollten, ist, dass "~" kein Glob ist, sondern von bash in einer separaten Expansionsstufe behandelt wird, ebenso wie $ variables und andere Expansionen. compgen -GFührt nur das Verschieben von Dateinamen durch, compgen -Wgibt Ihnen jedoch die gesamte Standarderweiterung von bash wieder, obwohl möglicherweise zu viele Erweiterungen (einschließlich ``und $()) vorhanden sind. Im Gegensatz zu -Gder -W ist sicher zitiert (ich die Unterschiede nicht erklären kann). Da der Zweck von darin -Wbesteht, dass es Token erweitert, bedeutet dies, dass es "a" zu "a" erweitert, auch wenn keine solche Datei vorhanden ist, so dass es möglicherweise nicht ideal ist.

Dies ist einfacher zu verstehen, kann jedoch unerwünschte Nebenwirkungen haben:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Dann:

touch $'curious \n filename'

echo curious*tab

Beachten Sie die Verwendung von printf %q, um die Werte sicher anzugeben.

Eine letzte Option ist die Verwendung einer durch 0 getrennten Ausgabe mit GNU-Dienstprogrammen (siehe bash FAQ ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Diese Option gibt Ihnen ein wenig mehr Kontrolle über die Sortierreihenfolge (die Reihenfolge beim Erweitern eines Glob LC_COLLATEhängt von Ihrem Gebietsschema ab / und kann die Groß- / Kleinschreibung ändern), ist aber ansonsten ein ziemlich großer Hammer für ein so kleines Problem ;-)


20

Verwenden Sie in zsh das [1] Glob-Qualifikationsmerkmal . Beachten Sie, dass dieser Sonderfall zwar höchstens eine Übereinstimmung zurückgibt, aber dennoch eine Liste ist und Globs nicht in Kontexten erweitert werden, die ein einzelnes Wort erwarten, z. B. Zuweisungen (außer Array-Zuweisungen).

echo *.txt([1])

In ksh oder bash können Sie die gesamte Liste der Übereinstimmungen in ein Array einfügen und das erste Element verwenden.

tmp=(*.txt)
echo "${tmp[0]}"

In jeder Shell können Sie die Positionsparameter festlegen und den ersten verwenden.

set -- *.txt
echo "$1"

Dadurch werden die Positionsparameter überlastet. Wenn Sie das nicht wollen, können Sie eine Unterschale verwenden.

echo "$(set -- *.txt; echo "$1")"

Sie können auch eine Funktion verwenden, die über einen eigenen Satz von Positionsparametern verfügt.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"

1
Und um die ersten $ n $ -Matches zu erhalten, können Sie*.txt([1,n])
Emre

6

Versuchen:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Hinweis: Die Dateinamenerweiterung wird nach der Sortierfolge sortiert, die für das aktuelle Gebietsschema gilt.



1

Ich bin gerade über diese alte Frage gestolpert, als ich mich das gleiche gefragt habe. Ich endete damit:

echo $(ls *.txt | head -n1)

Sie können natürlich headmit tailund -n1mit jeder anderen Nummer ersetzen .


Das oben Genannte funktioniert nicht, wenn Sie mit Dateien arbeiten, deren Name Zeilenumbrüche enthält. Zum Arbeiten mit Zeilenumbrüchen können Sie Folgendes verwenden:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Funktioniert nicht bei BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Funktioniert mit jedem Sonderzeichen)

3
Nein, das schlägt fehl, wenn der Dateiname Zeilenumbrüche enthält.
Isaac

7
In was für einer verrückten Welt leben wir, in der Dateinamen Zeilenumbrüche enthalten?
Billynoah

-1

Ein Anwendungsfall, auf den ich häufig stoße, ist das Definieren des oberen / unteren Verzeichnisses nach der Glob-Erweiterung (z. B. ein Verzeichnis mit versionierten SDKs oder Build-Tools). In dieser Situation möchte ich diesen Verzeichnisnamen normalerweise in einer Variablen speichern, um ihn an einigen Stellen in einem Shell-Skript zu verwenden.

Dieser Befehl erledigt das normalerweise für mich:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Haftungsausschluss: Die Glob-Erweiterung sortiert Ihre Ordner nicht nach Semver. du wurdest gewarnt. Dies ist großartig, wenn Sie Dockerfilenur eine Version eines Verzeichnisses haben, diese Verzeichnisversion jedoch von Bild zu Bild variieren kann 🤷


Willkommen bei U & L! Dies behandelt die meisten Verzeichnisnamen, jedoch nicht Verzeichnisnamen mit einer neuen Zeile. Versuchen Sie, ein Verzeichnis wie dieses zu erstellen, mkdir "$(echo one; echo two)"und sehen Sie, was ich meine.
Flimm

Was ist der Vorteil gegenüber den anderen Alternativen, insbesondere der verwendeten Version tail?
RalfFriedl

Standard dirnameakzeptiert nur einen einzelnen Pfadnamen. Sie können sich also nicht darauf verlassen, dass dieser für mehrere Pfadnamen verwendet wird, es sei denn, Sie wissen, dass Ihre spezielle Implementierung dies unterstützt.
Kusalananda

@ Flimm Guter Punkt; Ich denke, die meisten Entwickler hätten größere Probleme, wenn ihre Ordnerstruktur Zeilenumbrüche enthält ... Ich musste mich nie damit befassen und erwarte nicht, dass ich halbwegs vernünftige Container und Software verwende
Andrew Odri

@RalfFriedl Gute Frage; Dies filtert im Wesentlichen alles heraus, was kein gültiges Verzeichnis ist (und nicht auflistet / durchquert. und ..)
Andrew Odri
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.