Shell: sélection d'un programme disponible

Dans bash / ksh / zsh, existe-t-il un bon idiome pour mettre une variable à la première d'une list de programmes alternatifs que l'on trouve dans $PATH (ou autrement appelable par le shell)?

Par exemple, si j'ai un script qui doit fonctionner si realpath (1) est installé sous realpath ou grealpath , je pourrais faire le long path:

  if type "grealpath" > /dev/null; then realpath_exec=grealpath elif type "realpath" > /dev/null; then realpath_exec=realpath else echo "$0: No realpath found in PATH" >&2 exit 1 fi 

La plupart des façons dont je peux penser à build une expression avec || et && ont besoin de sous-shell nesteds (pas une grosse affaire, juste ennuyeux et mauvais pour les performances dans une fonction fréquemment appelée) et / ou des redirections compliquées ou des grep moche pour gérer les différentes sorties. (Notez que la sortie de type ou which n'est pas directement utilisable si le "programme" installé est une fonction ou un alias, donc si je veux juste supposer que si quelque chose à ma disposition appelé realpath peut faire le travail, renvoyer la valeur plutôt que la sortie ou faire un grep.)

Ce qui précède est très bien, c'est juste vraiment verbeux et si vous avez plusieurs choix de programmes comme celui-ci, c'est douloureusement. Existe-t-il une façon plus élégante de choisir le premier programme disponible d'une list et de l'affecter à une variable?

La façon la plus simple et compréhensible serait juste une boucle for avec un seul de vos if intérieur, qui se break hors de la boucle quand il trouve une correspondance.


Si vous ne voulez pas faire cela, dans Bash, type accepte plusieurs arguments à searchr:

 type grealpath realpath ... 

et le premier mot de la ligne de sortie pour une input correspondante est toujours 1 le nom de la command / fonction / alias. Vous pouvez capturer la sortie standard dans un tableau et l'indexer pour éviter un autre sous-process:

 words=($(type realpath grealpath foo make 2>/dev/null)) realpath_exec=${words[0]} if ! [ "$realpath_exec" ] ... 

D'un certain sharepoint vue, c'est élégant, mais il est décidément plus difficile à comprendre quand on a l'occasion de traverser.


La division de mots dans les initialiseurs de tableau dépend de la valeur de l' IFS . Si IFS a été modifié ou si une de vos commands contient un espace, cela ne fonctionnera pas.


1 Expérimentalement, cela semble être le cas dans la plupart des locales, même si ce n'est pas l'ordre des mots naturels, et il semble toujours y avoir un espace ASCII U + 0020, mais le format de sortie n'est pas spécifié plus loin. Dans certaines localizations, cela ne fonctionnera pas; vous devez considérer si cela va être un problème pour vous. Vous pouvez utiliser LC_ALL=C type ... pour (plus ou less) garantir un format de sortie approprié.


Dans zsh , si vous vous souciez uniquement des exécutables, et non des fonctions ou des alias, vous pouvez utiliser les $commands :

 realpath_exec=${commands[grealpath]:-${commands[realpath]:-${commands[another]:?No compatible command found}}} 

L'extension ${param:-...} donne la valeur de param si elle n'est pas nulle, et ... sinon; ${param:?...} erreurs avec ... si le param échoue. Cela rest assez laid.

Si vous ne vous souciez pas de la command entre la sélection des commands, vous pouvez utiliser une version plus simple avec l' indicateur (i) indice :

 realpath_exec=${commands[(i)realpath|grealpath|another]} 

Pour inclure des fonctions et des alias dans l'une de ces deux options, vous pouvez utiliser $functions et $aliases , mais il sera répétitif.

Vous pourriez le refactoriser, par exemple, dans:

 isExecutable(){ type "$1" >/dev/null 2>&1 && printf "%s\n" "$1"; } realpath_exe=`isExecutable grealpath || isExecutable realpath` [ -n "$realpath_exe" ] || { echo "$0: No realpath found in PATH" >&2 exit 1 } 

mais je pense que vous êtes la version est bien et lisible. Je ne m'inquiéterais pas de sa longueur.

S'il vous plaît noter que vous ne pouvez pas avoir des signes dollar sur le côté gauche des affectations de variables et aussi ce type search également des fonctions et des alias en plus des files exécutables dans PATH.

Vous avez raison sur la sortie de type et which : vous ne devriez donc pas les utiliser. Vous devriez utiliser la command .

 pathx() for cmd do set "" "${PATH:?is NULL!}:" while "${2:+set}" -- "${2%%:*}" "${2#*:}" 2>&3 do command -v -- "${1:-.}/$cmd" done; done 3>/dev/null 

Cela ne fait que command -v $PATH_component/$cmd et donc il ne listra jamais les alias, les builtins ou tout autre – il searchra récursivement chaque composant de sa variable d'environnement $PATH pour chacun de ses arguments et imprimera à son stdout tout ce qu'il trouve.

Si vous ajoutez une command -v ... && break after command -v ... elle annulera sa search $PATH la première fois qu'elle localisera un exécutable $PATH 'd nommé pour l'un de ses arguments.

C'était l'idée de Michael Homer – et c'est vraiment la meilleure.

Il fonctionne en imbriquant la boucle while dans la boucle for . for chaque itération de la boucle for , la boucle while parcourt tous les composants de $PATH , en testant la string la plus courte : séparée par deux-points, elle peut couper ${2%%:*} avec la command -v $slice/$cmd et sauvegarder string la plus longue pour la prochaine itération ${2#*:} . Ce n'est que lorsque $PATH a été testé complètement que la boucle while essaie d'exécuter une string "${2:-NUL}" et échoue qu'elle se termine et que la prochaine for itération commence.

 cp /bin/cat /tmp (PATH=$PATH:/tmp pathx cat dd dummy doesntexist read echo) 

 /usr/bin/cat /tmp/cat /usr/bin/dd /usr/bin/echo 

Apparemment, vous voulez des aliases et des trucs. Eh bien, c'est faisable:

 shellx() for cmd do "set" -- "$cmd";"unset" cmd for type in alias exe do case $type in (a*) "alias" "${1%%*=*}" ;; (e*) PATH= "command" -v -- "$1" && type=function::builtin || "command" -v -- "$1" ;; esac >&2&& "command" -V -- "$1" >&3 && cmd=$("command" -v -- "$1") && return done;done 3>&2 2>/dev/null 

C'était plus difficile que je ne m'en souviens, mais cela s'arrête dès qu'un de ses arguments est jugé exécutable. Il met son type – un alias , function :: builtin , ou exe dans $type , et la command va dans $cmd . Les alias obtiennent la définition écrite dans $cmd – qui dans zsh , bash et yash ressemble à …

 alias x='something or other' 

… en ksh93 c'est juste …

 something or other 

… et au dash c'est …

 x='something or other' 

… mais, encore une fois, tous reçoivent la variable $type assignée.

Si vous lui transmettez un argument qui est un alias de shell et qui a été défini d'une manière ou d'une autre même si son nom contient = , alors, cela ne le finda pas. Si vous avez besoin de cette fonctionnalité, vous devrez grep la sortie de l' alias plutôt que de tester son return.

Si un exécutable est trouvé, la sortie command -V la command -V est écrite en erreur standard juste avant le return de la fonction. Il renvoie false si aucun exécutable n'est trouvé.