Shell:选择可用的程序

Shell:选择可用的程序

$PATH在 bash/ksh/zsh 中,是否有一个好的习惯用法将变量设置为可以在其中找到(或可以由 shell 调用)的替代程序列表中的第一个?

例如,如果我有一个脚本需要工作真实路径(1)安装在realpath或下grealpath,我可以走很长的路:

 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

我能想到的构建表达式的大多数方法||&&需要嵌套的子shell(没什么大不了的,只是烦人并且对经常调用的函数的性能不利)和/或复杂的重定向或丑陋的grep来处理各种输出。 (请注意,输出如果安装的“程序”是函数或别名,则type或不能直接使用;which因此,如果我只是想假设如果我可以调用的东西realpath可以完成这项工作,我必须打开返回值而不是输出或执行 grep。)

上面的内容很好,只是真的很啰嗦,如果你有多个这样的程序选择,那就太痛苦了。还有吗优雅的我可以选择列表中第一个可用程序并将其分配给变量吗?

答案1

最直接、最容易理解的方法就是一个for循环,其中包含一个ifs ,break当它找到匹配项时,它就会脱离循环。


如果你不想这样做,在 Bash 中,type接受多个参数搜索:

type grealpath realpath ...

匹配条目的输出行的第一个字始终是1命令/函数/别名的名称。您可以捕获数组中的标准输出并对其进行索引,以避免进一步的子进程:

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

从某种角度来看,这很优雅,但当你偶然遇到它时,它显然更难理解。


数组初始值设定项中的分词依赖于取决于的价值IFS。如果IFS已更改,或者您的任何命令包含空格,则此操作将不起作用。


1从实验来看,情况似乎如此最多区域设置,即使这不是自然词序,并且后面总是有一个 U+0020 ASCII 空格,但没有进一步指定输出格式。在某些本地化版本中,这不起作用;您需要考虑这是否会给您带来问题。您可以使用LC_ALL=C type ...(或多或少)保证合适的输出格式。


在zsh中,如果您只关心可执行文件,而不关心函数或别名,则可以使用$commands:

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

如果非空,则扩展${param:-...}给出值,否则;如果失败则出错。这仍然相当难看。param...${param:?...}...param

如果您不关心命令选择之间的顺序,则可以使用更简单的版本(i) 下标标志:

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

要在其中包含函数和别名,您可以使用$functionsand $aliases,但它会变得重复。

答案2

您可以将其重构为:

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
}

但我认为你的版本很好并且可读。我不会担心它的长度。

请注意,变量赋值的左侧不能有美元符号,并且type除了 PATH 中的可执行文件之外,还会查找函数和别名。

答案3

type你对and的输出是正确的which:所以你不应该使用它们。你应该使用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

这只会这样做command -v $PATH_component/$cmd,因此它不会列出别名或内置函数或任何其他内容 - 它将递归地搜索其$PATH环境变量的每个组件以查找其每个参数,并将找到的任何内容打印到其标准输出。

如果您在&& break后面添加,它将在第一次成功找到以其参数之一命名的 'd 可执行文件时command -v ...中止搜索。$PATH$PATH

这是迈克尔·霍默的想法——而且确实是最好的想法。

while它的工作原理是在循环中嵌套循环for。循环for的每次迭代forwhile循环都会迭代 中的每个组件,测试它可以切片的$PATH最短的:冒号分隔字符串,并为下一次迭代保存最长的字符串。只有当循环尝试执行 字符串并且失败时,它才会完成并开始下一次迭代。${2%%:*}command -v $slice/$cmd${2#*:}$PATHwhile"${2:-NUL}"for

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

显然你确实想要aliases什么东西。嗯,这是可行的:

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

这比我记忆中的要难,但是一旦发现其中一个参数可执行,就会退出。它把它的类型 - 之一别名,函数::内置, 或者EXE文件$type,然后命令进入$cmd。别名将定义写入$cmd- 其中zsh, bash, 和yash看起来像......

alias x='something or other'

……里面ksh93只是……

something or other

……其中dash有……

x='something or other'

...但是,再次,所有人都得到了变量$type分配的。

如果你传递一个参数一个 shell 别名,并且以某种方式被定义,即使它的名称包含=,那么,好吧,这不会找到它。如果您需要该功能,则需要 grep 的输出alias而不是测试其返回。

如果找到可执行文件,command -V则在函数返回之前将输出写入标准错误。如果未找到可执行文件,则返回 false。

相关内容