1

我有一个名为“onewhich”的小脚本。它的目的是表现得像which,除了它只会第一次出现指定为选项的任何可执行文件,按照它们在路径中出现的顺序找到。

例如,如果我的路径是/opt/bin:/usr/bin:/bin,并且我同时拥有/opt/bin/runme/usr/bin/runme,那么命令onewhich runme将返回/opt/bin/runme

但是,如果我也有一个/usr/bin/doit,那么该命令onewhich doit runme将返回/usr/bin/doit

这个想法是遍历路径,检查指定的每个可执行文件,如果存在,则显示并退出。

这是到目前为止的脚本。

#!/bin/sh

for what in "$@"; do
  for loc in `echo "${PATH}" | awk -vRS=: 1`; do
    if [ -f "${loc}/${what}" ]; then
      echo "${loc}/${what}"
      exit 0
    fi
  done
done

exit 1

问题是,我想更好地处理带有特殊字符的 PATH 目录。StackOverflow 上的每一个 shell 问题都在谈论使用 awk 和 sed 等工具解析路径有多糟糕。甚至还有一个关于它的bash 常见问题解答条目。(附带条件:我没有为此使用 bash,但该建议仍然有效。)

所以我尝试重写脚本以分隔管道中的路径,就像这样“

#!/bin/sh

for what in "$@"; do
  echo "${PATH}" | awk -vRS=: 1 | while read loc ; do
    if [ -f "${loc}/${what}" ]; then
      echo "${loc}/${what}"
      exit 0
    fi
  done
done

exit 1

我不确定这是否给了我任何真正的优势(因为$loc仍在引号内),但它也不起作用,因为由于某种原因,exit 0似乎被忽略了。或者......它退出了某些东西(可能是带有终止管道的while循环的子shell),但脚本以1每次的值退出。

有什么更好的方法来逐步浏览目录${PATH}而不会有特殊字符混淆事物的风险?

或者,我是在重新发明轮子吗?是否有一种方法可以内置到现有的 shell 工具中?

这需要在 Linux 和 FreeBSD 中运行,这就是为什么我用 Bourne 而不是 bash 编写它的原因。

谢谢。

4

3 回答 3

1

另一种完全解决解析需求的方法PATH是在具有剥离环境的新 shell 中运行内置type命令(即根本没有要查找的函数或别名;参见env -i sh -c 'type cmd 2>/dev/null)。

# using `cmd` instead of $(cmd) for portability
onewhich() {
  ec=0  # exit code
  for cmd in "$@"; do
    command -p env -i PATH="$PATH" sh -c '
      export LC_ALL=C LANG=C
      cmd="$1"
      path="`type "$cmd" 2>/dev/null`"
      if [ X"$path" = "X" ]; then
        printf "%s\n" "error: command \"${cmd}\" not found in PATH" 1>&2
        exit 1
      else
        case "$path" in
          *\ /*)
            path="/${path#*/}"
            printf "%s\n" "$path";;
          *)
            printf "%s\n" "error: no disk file: $path" 1>&2
            exit 1;;
        esac
        exit 0
      fi
    ' _ "$cmd"
    [ $? != 0 ] && ec=1
  done
  [ $ec != 0 ] && return 1
}

onewhich awk ls sed
onewhich builtin 
onewhich if

which如果将两个命令指定为参数,则成功时会返回两个完整的命令路径,因此exit 0在上面的第一个onewhich脚本中会过早地中止程序。此外,如果将两个命令指定为 的参数,则即使只有一个命令查找失败(参见) ,也会which将退出代码which设置为。为了模仿命令的这种行为,有必要打开/关闭两个变量(及以下)。1which awk sedxyz ls; echo $?whichcntnomatches

onewhich() (
   IFS=":"
   nomatches=0
   for cmd in "$@"; do
      cnt=0
      for loc in $PATH  ; do
         if [ $cnt = 0 ] && [ -x "$loc"/"$cmd" ]; then
            echo "$loc"/"$cmd"
            cnt=1
         fi
      done
      [ $cnt = 0 ] && nomatches=1
   done
   [ $nomatches = 1 ] && exit 1 || exit 0  # exit 1: at least one cmd was not in PATH
)


onewhich awk ls sed
onewhich awk lsxyz sed
onewhich builtin 
onewhich if
于 2014-11-24T13:56:38.473 回答
1

在我看来,您的第一种方法似乎应该有效。实际上,如果它真的是$PATH您要搜索的内容,那么您不太可能在目录中嵌入空格和换行符。如果你这样做了,那么可能是重构的时候了。

但是,我认为您不会面临坏名可能破坏您的循环的风险,因为您将变量包装在引号中。在最坏的情况下,我怀疑您可能会错过奇怪的有效可执行文件,但我看不出脚本会如何产生错误。(我看不出脚本会如何遗漏有效的可执行文件,而且我还没有测试过——我只是说乍一看我没有发现问题。)

至于你的第二个问题,关于循环,我认为你已经一针见血了。当您运行类似的管道this | that | while condition; do things; done时,while 循环在管道末端的自己的外壳中运行。退出该 shell 可能会终止管道的操作,但这只会让您回到父 shell,它有自己的执行线程,以exit 1.

至于更好的方法,我会考虑which.

#!/bin/sh

for what in "$@"; do
  which "$what"
done | head -1

如果你真的想要退出值:

#!/bin/sh

for what in "$@"; do
  which "$what" && exit 0
done

exit 1

第二个甚至可能更少的资源,因为它不必打开文件句柄和管道通过head.

您还可以使用IFS. 例如,如果你想用另一种方式包装你的循环,你可以这样做:

#!/bin/sh

IFS=":"

for loc in $PATH; do
  for what in "$@"; do
    if [ -x "$loc"/"$what" ]; then
      echo "$loc"/"$what"
      exit 0
    fi
  done
done

exit 1

请注意,在正常情况下,您可能希望保存 的旧值$IFS,但您似乎是在独立脚本中执行操作,因此脚本退出时会丢弃“新”值。

以上所有代码均未经测试。YMMV。

于 2013-03-11T19:36:34.553 回答
1

这并不能直接回答您的问题,但确实消除了解析的需要PATH

onewhich () {
    for what in "$@"; do
        which "$what" 2>/dev/null && break
    done
}

这只是调用which输入列表中的每个命令,直到找到匹配项。


要解析PATH,您可以简单地设置 `IFS=':'。

if [ "${IFS:-x}" = "${IFS-x}" ]; then
    # Only preserve the value of IFS if it is currently set
    OLDIFS=$IFS
fi
IFS=":"
for f in $PATH; do  # Do not quote $PATH, to allow word splitting
    echo $f
done
if [ "${OLDIFS:-x}" = "${OLDIFS-x}" ]; then
    IFS=$OLDIFS
fi

PATH如果其中的任何目录实际上包含冒号,则上述操作将失败。

于 2013-03-11T17:18:14.020 回答