Führen Sie in $ PATH einen Befehl aus, der einem Platzhalter entspricht

7

Ich möchte einen Befehl in der aktuellen finden und führen $PATHfür diese Platzhalter libreoffice?.?(zB. libreoffice4.0, libreoffice4.3Usw.)

BEARBEITEN: Wenn mehrere Übereinstimmungen gefunden werden, können Sie eine zufällig auswählen.

Ich bevorzuge eine POSIX-kompatible Lösung.

eadmaster
quelle

Antworten:

3

Stellen Sie ein IFS, :um den Wert von PATHon Doppelpunkten zu teilen . Wenn Sie finddie -quitAktion und die -maxdepthprimäre haben (z. B. FreeBSD, OSX, GNU), wissen Sie, dass der Befehl vorhanden ist und Sie sich nicht für den Rückkehrcode des Befehls interessieren, können Sie diesen Einzeiler verwenden:

pattern='libreoffice?.?'
IFS=:; find $PATH -maxdepth 1 -type f -name "$pattern" -exec {} \; -quit; unset IFS

Dies bietet keine einfache Möglichkeit, um zu melden, ob der Befehl gefunden wurde. Um robuster zu sein, deaktivieren Sie außerdem das Globbing, falls der Wert von PATHPlatzhalter enthält. Es ist auch möglich, eine leere Komponente PATHzu haben, um das aktuelle Verzeichnis zu bezeichnen (aber mein Rat ist, .stattdessen zu verwenden ). Der folgende Code kümmert sich um all diese Komplikationen.

pattern='libreoffice?.?'
case $PATH in
  :*) directories=.$PATH;;
  *::*) directories=${PATH%%::*}:.:${PATH#*::};;
  *:) directories=$PATH.;;
  *) directories=$PATH;;
esac
set -f; IFS=:
cmd=
for d in $directories; do
  set +f
  for x in "$d"/$pattern; do
    if [ -x "$x" ] && ! [ -d "$x" ]; then
      cmd=$x
      break
    fi
  done
  if [ -n "$cmd" ]; then break; fi
done
set +f; unset IFS
if [ -z "$cmd" ]; then
  echo 1>&2 "$pattern: not found in PATH"
  exit 127
else
  exec "$cmd"
fi

Wenn Sie zufällig zsh verwenden (im Gegensatz zu normalem sh, bash, ksh,…), ist es viel einfacher, eine robuste Lösung zu erstellen.

pattern='libreoffice?.?'
matches=($^path/$~pattern(N.*[1]))
if ((!#matches)); then
  $matches[1]
else
  echo 1>&2 "$pattern: not found in PATH"
  exit 127
fi
Gilles 'SO - hör auf böse zu sein'
quelle
Genau genommen kann ::es mehrmals vorkommen $PATH. Möglicherweise möchten Sie den Fall berücksichtigen, in dem $PATHfestgelegt, aber auch leer (durch ersetzen .).
Stéphane Chazelas
@ StéphaneChazelas - nach unserer vorherigen Konversation wird ::davon ausgegangen, dass .for ::sowieso nur Legacy-konform ist - und neuere Anwendungen sollten eine solche Annahme treffen.
Mikeserv
Ich bevorzuge die Oneliner-Lösung, aber mit Posix-kompatiblen Schaltern:find $PATH -type f -perm /u=x,g=x,o=x -prune -name "$@" | head -n1
eadmaster
@eadmaster Sie rekursieren in Unterverzeichnisse in Verzeichnisse in $PATH(und ja, das passiert).
Gilles 'SO - hör auf böse zu sein'
2
@eadmaster Mach das "$1", da es hier nur ein Muster geben kann.
Gilles 'SO - hör auf böse zu sein'
2

Mit zsh:

$commands[(i)libreoffice?.?]

In zsh, $commandsist eine spezielle assoziative Array , dessen Schlüssel Befehlsnamen und den Wert ihres Pfades.

iOben befindet sich ein Array-Index-Flag , das angibt zsh, dass das Muster mit den Array-Schlüsseln abgeglichen und der erste übereinstimmende Schlüssel zurückgegeben werden soll.

Die Elemente des assoziativen Arrays befinden sich jedoch nicht in einer bestimmten Reihenfolge, sodass der erste übereinstimmende Schlüssel nicht unbedingt der erste ist, der in auftritt $PATH. Wenn Sie die libreofficemit der größten Versionsnummer möchten , können Sie Folgendes tun:

${${(nO)${commands[(I)libreoffice?.?]}}[1]}

Das Itiefgestellte Flag wird auf alle übereinstimmenden Schlüssel erweitert. Wir verwenden die Parametererweiterungsflagsn (numerische Sortierung) und O(umgekehrte Reihenfolge) , um diese Liste von der größten zur kleinsten Versionsnummer zu sortieren und dann die erste auszuwählen.[1]

Siehe auch:

whence -m 'libreoffice?.?'

um die Pfade der entsprechenden Befehle zu finden.

Stéphane Chazelas
quelle
1
@mikeserv, es meldet den Schlüssel, daher ruft der Befehlsname, die normale $ PATH-Suche, den ersten in $ PATH für die größte Version von libreoffice auf (und $commands[cmd]ist der erste cmdin $PATH, es ist nur so, dass, wenn Sie /bin/libreoffice4.2und haben /usr/bin/libreoffice4.1, die erste Lösung sein kann sehr gut zurückkehren, libreoffice4.1auch wenn /binvorher /usr/binin $PATH). In zshist das $pathArray an den $PATHSkalar gebunden (ein Merkmal von csh entlehnt)
Stéphane Chazelas
Aber ich dachte, $pathkann geändert werden, ohne zu ändern $PATH? Jedenfalls weiß ich sehr wenig darüber - nur, dass es sich um getrennte Dinge handelte. Nein - getestet - das Ändern hat sich $pathauch geändert $PATH.
Mikeserv
@mikerserv, nein, $pathund $PATHsind gebunden. Jede Änderung des einen spiegelt sich im anderen wider. Siehe die Beschreibung von typeset -T.
Stéphane Chazelas
Interessant ... Die ersten beiden Argumente sind der Name eines Skalars und eines Array-Parameters (in dieser Reihenfolge), die in der Art von $PATHund miteinander verknüpft werden $path. Das optionale dritte Argument ist ein einstelliges Trennzeichen, mit dem die Elemente des Arrays zum Skalar zusammengefügt werden. Wenn nicht vorhanden, wird ein Doppelpunkt verwendet. zshSicher macht es ein paar nette Sachen, es ist nur so, dass es so viel davon macht, dass ich mich oft verliere. Deshalb habe ich gelernt, für die komplizierten Dinge ksh93viel besser zu mögen - es ist einfacher, wie ich denke. Aber das ist nur Präferenz.
Mikeserv
1

Hier ist eine einfachere Alternative:

$(compgen -c libreoffice)

Es wird davon ausgegangen, dass Bash und nur eine libreoffice*installiert ist.

Es emuliert, was die Vervollständigung der Bash-Registerkarte tun würde, wenn Sie tippen libreofficeTab.

Wenn Sie absichtlich versucht haben, libreofficeohne Versionsnummer auszuschließen , und die Existenz mehrerer Versionen behandeln möchten, versuchen Sie Folgendes:

run_libreoffice() {
    compgen -c libreoffice |
        while read -r exe; do
            case "$exe" in libreoffice?.?)
                "$exe" "$@"
                return
                ;;
            esac
        done
}
run_libreoffice "$@"

Die caseAnweisung stimmt nur überein libreoffice?.?, und wir durchlaufen die Ergebnisse und führen nur die erste aus.

Mikel
quelle
1
Es wird mehr als angenommen bash- es wird ein vollständiges Ergebnis pro Zeile und keine $IFSoder globale Zeichen in den Ergebnissen angenommen.
Mikeserv
Das ist eine interessante Option, aber sie ist auf Platzhalter beschränkt, die mit einer festen Zeichenfolge beginnen ...
eadmaster
Die zweite Form ist nicht auf feste Zeichenfolgen beschränkt, aber es ist nicht ideal , den Teilstring anzugeben, um compgendas Muster zu caseerstellen.
Mikel
0

Sie können den Befehl "find" wie folgt verwenden

find ./ -name "libreoffice?.?"
Schlafstörungen
quelle
1
funktioniert nicht, beachte, dass ich das System meinte $PATH, nicht den$PWD
eadmaster
1
find $ (echo $ PATH | sed 's /: / / g') -name "libreoffice?.?"
Schlaf
ok, es funktioniert! :)
eadmaster
Um in allen $ PATH-Verzeichnissen zu finden, sollten wir in allen suchen. Dies bedeutet ungefähr so: 'find dir1 dir2 dir3 -name "libreoffice?.?"'. Daher ersetzen wir das Semikolon für Leerzeichen durch den Befehl $ (echo $ PATH | sed 's /: / / g')
dgsleeps
0

POSIXly, wenn Sie nach einem $PATHausführbaren Befehl suchen , sollten Sie Folgendes verwenden command:

  • command -v
    • Schreiben Sie eine Zeichenfolge in die Standardausgabe, die den Pfadnamen oder Befehl angibt, der von der Shell in der aktuellen Shell-Ausführungsumgebung verwendet wird (siehe Shell-Ausführungsumgebung , um Befehlsname aufzurufen , aber Befehlsname nicht aufzurufen .
    • ... Andernfalls wird keine Ausgabe geschrieben und der Exit-Status muss widerspiegeln, dass der Name nicht gefunden wurde.

Wenn eithout einen Schalter genannt, command wird aufrufen command_name , aber Sie müssen noch eine voll aufgelösten Pfad eines vollständig lösen Shell - Glob .

Um dies zu tun, sollten Sie den Glob gegen jedes durch :Doppelpunkte getrennte Element $PATHin der angegebenen Reihenfolge prüfen :

  • PATH
    • Diese Variable muss die Folge von Pfadpräfixen darstellen, die bestimmte Funktionen und Dienstprogramme bei der Suche nach einer ausführbaren Datei anwenden, die nur unter einem Dateinamen bekannt ist . Die Präfixe sind durch einen Doppelpunkt zu trennen :. Wenn auf diesen Dateinamen ein Präfix ungleich Null angewendet wird, wird zwischen dem Präfix und dem Dateinamen ein Schrägstrich eingefügt. Ein Präfix mit der Länge Null ist eine Legacy-Funktion , die das aktuelle Arbeitsverzeichnis angibt. Es wird als zwei benachbarte Doppelpunkte angezeigt ::, als anfänglicher Doppelpunkt vor dem Rest der Liste oder als nachfolgender Doppelpunkt nach dem Rest der Liste. Eine streng konforme Anwendung muss einen tatsächlichen Pfadnamen (z. B. .) verwenden , um das aktuelle Arbeitsverzeichnis in darzustellenPATH. Die Liste wird von Anfang bis Ende durchsucht, wobei der Dateiname auf jedes Präfix angewendet wird, bis eine ausführbare Datei mit dem angegebenen Namen und den entsprechenden Ausführungsberechtigungen gefunden wird. Wenn der gesuchte Pfadname einen Schrägstrich enthält, wird die Suche durch die Pfadpräfixe nicht durchgeführt. Wenn der Pfadname mit einem Schrägstrich beginnt, wird der angegebene Pfad aufgelöst (siehe Pfadnamenauflösung ) . Wenn PATHnicht gesetzt oder auf null gesetzt ist, ist die Pfadsuche implementierungsdefiniert .

Sie können aufzuschlüsseln $PATHin ein Feld pro $PATHOn - Komponente $IFS, aber Sie können dies nur tun , sicher , wenn Sie auch set -filename Expansion als deaktiviert , wenn Sie das tun, falls eine Komponente in $PATHeinem enthält [?*Zeichen, weil Dateinamen Generation (sprich: Globbing) tritt nach Feld -Splitting in der Erweiterungsreihenfolge der Shell.

Auf diese Weise besteht die einzige wirklich robuste Möglichkeit, dies POSIXly zu tun, darin, zuerst die Feldliste zu erweitern, $IFSwährend die Dateinamengenerierung deaktiviert ist, und das Array-Ergebnis irgendwie zu speichern, dann die Dateinamengenerierung erneut zu aktivieren und die Feldaufteilung zu deaktivieren ( so dass Sie vielleicht speichern und Ihre glob Muster erweitern unquoted in einer variablen w / out jede Gefahr davon durch folgende Faktoren beeinträchtigt werden $IFSzuerst) , und arbeiten auf den Ergebnissen jeder der Reihe nach .

Zum Glück ist das nicht so schwierig:

g='libreoffice?.?'
(   IFS=:; set-f             #split on :; disable filename gen
    set +f -- $PATH          #store split array, enable filename gen after
    IFS=                     #disable field splitting
    for p do for x in ${p:+"$p/"$g}
          do command -v "$x"
    done;done
)                            #done in subshell to avoid affecting current shell

Die letzten Teile dort werden nicht kommentiert, da sie hier besser erklärt werden.

  • for p do- Die erste forSchleife weist iterativ $pden Wert jedes Positionsparameters im Array der Shell zu"$@" , setder der Feldaufteilungserweiterung von $PATHgeteilt durch :Doppelpunkte entspricht.

  • for x in ${p:+"$p/"$g}- Die innere forSchleife setzt iterativ $xauf jede (falls vorhanden) der Erweiterungen von ${p:+"$p/"$g}- das heißt, sie iteriert überhaupt über nichts, es sei denn, sie $pist sowohl gesetzt als auch nicht null. Dies ist wichtig, weil ::oder ein Leading in :Nullfelder aufgeteilt würde und auf diese Weise die beiden Schleifen genau der $PATHSpezifikation (wie oben angegeben) entsprechen, indem Felder mit der Länge Null nicht anerkannt werden.

  • Da die Art $PATHund Weise geteilt wird, während die Dateinamengenerierung deaktiviert ist und "$p/"$gaufgelöst wird, während sie aktiviert ist, aber die Feldaufteilung deaktiviert wurde, kann keines der Elemente in $PATHetwas anderes als ihre Felder als geteilt erweitert werden : (unabhängig von den anderen Zeichen, die sie enthalten kann enthalten) und es kann auch kein Wert für die "$p/"$gErweiterung auf etwas anderes als sich selbst oder einen beliebigen Dateinamen verwendet werden (unabhängig davon, ob Zeichen in $poder andere als Mustervergleichszeichen $genthalten sind) .

  • command- und commanddruckt zuletzt nur die Werte aus, für $xdie ausführbare Befehle standardisiert sind.

Hier ist das Ganze in einer Shell-Funktion verpackt, mit der zusätzlichen Möglichkeit, mehrere $gArgumente zu verarbeiten:

glob_path()( 
    for g do IFS=:; set -f
          set +f -- ${PATH:-.}; IFS=
          for p do for x in ${p:+"$p/"$g}
                do command -v "$x"
    done; done; done
)

Damit erhalten Sie eine Liste der ausführbaren Dateien, die mit jedem Argument übereinstimmen, das Sie übergeben. Es wird eine pro Zeile gedruckt, die zuerst nach Argumentreihenfolge, dann nach $PATHPriorität und zuletzt nach Sortierreihenfolge Ihres Gebietsschemas sortiert ist. Es sollte nicht bei Zeilenumbrüchen oder Sonderzeichen in den Pfadnamen oder den Glob-Mustern fehlschlagen, die Sie ihm geben. Es kann geändert werden (siehe Bearbeitungsverlauf hier) , um das Shell- "$@"Array mit einer ausführbaren Datei pro Positionsparameter genau zu füllen.

Sie können es auch ändern, um das erste erfolgreiche Glob-Match auszuführen und die Suche nach diesem bestimmten Glob-Argument zu beenden und mit dem nächsten fortzufahren, wenn der Befehl erfolgreich beendet wird, wie ...

glob_path()( 
    for g do IFS=:; set -f
          set +f -- ${PATH:-.}; IFS=
          for p do for x in ${p+:"$p/"$g}
                do command -v "$x" &&
                   command "$x"    &&
                   break 2
    done; done; done
)

Dies ist ein tragbares POSIX-Mittel, um Ihre Glob-Übereinstimmung zu finden, aber einige Shells benötigen nicht einmal die zweite for g...Schleife. In bashfunktioniert dies beispielsweise auch wie ...

glob_path()( 
    for g do IFS=:; set -f
          set +f -- ${PATH:-.}; IFS=
          for p do command -v ${p:+"$p/"$g}
    done; done
)

... die weiterhin alle ausführbaren Glob-Übereinstimmungen für alle Argumente und für jedes Element $PATHauflistet, unabhängig davon, ob eines dieser Elemente oder Glob-Muster Zeilenumbrüche oder Sonderzeichen usw. enthält.

Sie können es verwenden wie:

glob_path '?sh' '??sh'       #I'd rather not install libreoffice

... was für mich druckt ...

/usr/bin/ksh
/usr/bin/rsh
/usr/bin/ssh
/usr/bin/zsh
/usr/local/bin/dash
/usr/local/bin/yash
/usr/bin/bash
/usr/bin/bssh
/usr/bin/chsh
/usr/bin/dash
/usr/bin/mksh
/usr/bin/posh
/usr/bin/slsh
/usr/bin/yash

Wie bereits erläutert, glob_pathinterpretiert die Funktion die $PATHUmgebungsvariable so, wie es in der Spezifikation angegeben ist, dass eine streng konforme Anwendung durch Ignorieren führender, nachfolgender oder aufeinanderfolgender :Vorkommen im $PATHWert von 'sollte, interpretiert jedoch ein leeres oder nicht gesetztes $PATHMittel als Bedeutung ..

Wenn Sie diese Legacy-Funktion jedoch wollten, könnte die Funktion geschrieben werden:

glob_path()(
    for g do IFS=:; set -f
          set +f -- ${0+$PATH:}; IFS=
          for p do for x in "${p:-.}/"$g
                do command -v "$x"
    done; done; done
)

... die alle Vorkommen von führenden, nachfolgenden oder zwei aufeinanderfolgenden :Doppelpunkten als Mittelwert interpretieren würden . (und so .oft suchen, wie eine $PATHKomponente mit der Länge Null im $PATHWert von erscheint) , aber das ist nicht streng konform.

mikeserv
quelle
Ich bevorzuge die Antwort von dgsleeps, weil sie einfacher aussieht ...
eadmaster
Schlägt fehl, wenn $PATHleere Komponenten enthalten sind. Beachten Sie auch, dass IFS Folgendes berücksichtigt: als Trennzeichen, nicht als Trennzeichen, da Sie $ PATH verarbeiten müssen (Werte von $PATHlike /bin:/usr/bin:sind nicht ungewöhnlich)
Stéphane Chazelas
@ StéphaneChazelas - Guter Punkt - ich hatte das in der älteren. Was bedeutet das auch ... ? Wollen Sie damit sagen, dass eine $PATHKomponente enthalten sein kann :? Wie auch immer; Ich habe das ::Ding repariert.
Mikesserv
IFS=:; $PATHteilt sich /bin:/usr/bin:in (/bin /usr/bin)in POSIX-Shells auf, nicht (/bin /usr/bin '')(Ausnahme zshin Sh-Emulation, Yash, PDKSH und älteren Versionen von MKSH)
Stéphane Chazelas
Ein leerer $ PATH bedeutet Suche im aktuellen Verzeichnis, /bin:bedeutet Suche in / bin und im aktuellen Verzeichnis. Für einen nicht gesetzten $ PATH, der von der Shell abhängt, überprüfen Sie meine Antwort auf das Warum nicht welche Frage verwenden . Eine korrektere Methode finden Sie unter @Gilles '.
Stéphane Chazelas