在shell中搜索路径的最佳方法

时间:2013-03-11 16:15:46

标签: parsing shell

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

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

但如果我还有/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 faq entry。 (Proviso:我没有使用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仍然在引号内),但它也不起作用因为某些原因,{{ 1}}似乎被忽略了。或者......它退出某些东西(带有while循环的子shell,可能会终止管道),但脚本每次都会以exit 0的值退出。

1中逐步浏览目录的最佳方法是什么,而不存在特殊字符会混淆事物的风险?

或者,我是否重新发明轮子?有没有办法在现有的shell工具中内置这个?

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

感谢。

3 个答案:

答案 0 :(得分: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中的任何目录实际上包含冒号,则上述操作将失败。

答案 1 :(得分:1)

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

但是,我仍然不认为你因为错误的名字破坏了你的循环而存在风险,因为你将变量包装在引号中。在最坏的情况下,我怀疑你可能会错过奇怪的有效可执行文件,但我看不出脚本会如何产生错误。 (我没有看到 脚本如何错过有效的可执行文件,而我还没有测试过 - 我只是说我乍看之下没有看到问题。)

至于你的第二个问题,关于循环,我认为你已经击中了头部。当您运行像this | that | while condition; do things; done这样的管道时,while循环在管道末端的自己的shell中运行。退出该shell可能会终止管道的操作,但这只会将您带回到父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的旧值,但您似乎是在独立脚本中执行操作,因此当脚本中的“new”值被抛出退出。

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

答案 2 :(得分:1)

另一种解决PATH解析需求的方法是在带有剥离环境的新shell中运行内置type命令(即,根本没有要查找的函数或别名; cf. 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的退出代码也会设置为1(参见which awk sedxyz ls; echo $?) 。要模仿which命令的这种行为,有必要打开/关闭两个变量(下面的cntnomatches)。

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