我有一个名为“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编写它。
感谢。
答案 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
命令的这种行为,有必要打开/关闭两个变量(下面的cnt
和nomatches
)。
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