当查找可执行文件的路径或检查在 Unix shell 中输入命令名称会发生什么时,有大量不同的实用程序(which
、type
、command
、whence
、where
、whereis
、whatis
、hash
等)。
我们经常听说which
应该避免这种情况。为什么?我们应该用什么来代替?
答案1
以下是您从未想过您不想知道的所有内容:
概括
要在类似 Bourne 的 shell 脚本中获取可执行文件的路径名(有一些注意事项;请参见下文):
ls=$(command -v ls)
要查明给定命令是否存在:
if command -v given-command > /dev/null; then
echo given-command is available
else
echo given-command is not available
fi
在类似 Bourne 的交互式 shell 的提示下:
type ls
该which
命令是来自 C-Shell 的破坏性遗产,最好单独保留在类似 Bourne 的 shell 中。
用例
作为脚本的一部分查找该信息或在 shell 提示符下以交互方式查找该信息是有区别的。
在 shell 提示符下,典型的用例是:这个命令的行为很奇怪,我使用的命令正确吗?我打字时到底发生了什么mycmd
?我可以进一步看看它是什么吗?
在这种情况下,您想知道调用该命令时 shell 执行的操作而不是实际调用该命令。
在 shell 脚本中,它往往有很大不同。在 shell 脚本中,如果您只想运行命令,那么您就没有理由想知道命令在哪里或是什么。一般来说,您想知道的是可执行文件的路径,因此您可以从中获取更多信息(例如相对于该文件的另一个文件的路径,或者从该路径处的可执行文件的内容中读取信息)。
通过互动方式,您可能想了解全部my-cmd
系统上可用的命令(在脚本中)很少如此。
大多数可用工具(通常是这种情况)都被设计为交互式使用。
历史
首先介绍一下历史。
直到 70 年代末,早期的 Unix shell 都没有函数或别名。仅在$PATH
. csh
1978 年左右引入了别名(尽管csh
是第一个释放在2BSD
,1979 年 5 月),并且还.cshrc
为用户定制 shell 进行了处理(每个 shell,如csh
,.cshrc
即使在不像脚本那样交互式时也会读取)。
虽然 Bourne shell 于 1979 年早些时候在 Unix V7 中首次发布,但功能支持是在很晚之后才添加的(1984 年在 SVR2 中),而且无论如何,它从未有过一些rc
文件(用于.profile
配置您的环境,而不是 shell本身)。
csh
它比 Bourne shell 更受欢迎(尽管它的语法比 Bourne shell 差得多),因为它添加了许多更方便、更好的交互式使用功能。
在3BSD
(1980),一个which
csh脚本添加的目的是为了csh
帮助用户识别可执行文件,它与which
当今许多商业 Unices(如 Solaris、HP/UX、AIX 或 Tru64)上可以找到的脚本几乎没有什么不同。
该脚本读取用户的~/.cshrc
(就像所有csh
脚本一样,除非使用 调用),并在别名列表和(基于 维护的数组)csh -f
中查找提供的命令名称。$path
csh
$PATH
给你:which
首先是当时最流行的 shell(csh
直到 90 年代中期仍然流行),这也是它被记录在书籍中并且仍然被广泛使用的主要原因。
请注意,即使对于csh
用户来说,which
csh 脚本也不一定会为您提供正确的信息。它获取 中定义的别名~/.cshrc
,而不是您稍后在提示符下定义的别名,或者例如通过source
ing 另一个csh
文件定义的别名,并且(尽管这不是一个好主意),PATH
可能会在 中重新定义~/.cshrc
。
which
从 Bourne shell运行该命令仍然会查找您的 中定义的别名~/.cshrc
,但如果您因为不使用而没有别名csh
,那么仍然可能会得到正确的答案。
直到 1984 年,类似的功能才通过内置命令在 SVR2 中添加到 Bourne shell type
。它是内置的(而不是外部脚本)这一事实意味着它能为您提供正确的信息(在某种程度上),因为它可以访问 shell 的内部。
初始type
命令遇到了与脚本类似的问题,which
因为如果未找到该命令,它不会返回失败退出状态。此外,对于可执行文件,与 相反which
,它输出类似ls is /bin/ls
而不是仅仅的内容/bin/ls
,这使得在脚本中使用起来不太容易。
Unix 版本 8(未公开发布)的 Bourne shell 的type
内置函数被重命名为whatis
并扩展为还报告参数和打印函数定义。它还修复了type
找不到名称时不返回失败的问题。
rc
、Plan9(Unix 的曾经的继承者)的外壳(及其衍生产品,如akanga
和es
)whatis
也有。
Korn shell(POSIXsh
定义所基于的子集)于 80 年代中期开发,但在 1988 年之前并未广泛使用,它csh
在 Bourne shell 之上添加了许多功能(行编辑器、别名......)。它添加了自己的whence
内置函数(除了type
),它采用了几个选项(-v
提供type
类似的详细输出,并-p
仅查找可执行文件(而不是别名/函数...))。
与 AT&T 和伯克利之间的版权问题风波不谋而合,一些自由软件shell 实现出现于 80 年代末 90 年代初。所有 Almquist shell( ,将取代 BSD 中的 Bourne shell), ()ash
的公共域实现(由 FSF 赞助),均在 1989 年至 1991 年间问世。ksh
pdksh
bash
zsh
Ash 虽然旨在替代 Bourne shell,但type
直到很久以后(在 NetBSD 1.3 和 FreeBSD 2.3 中)才拥有内置程序,尽管它有hash -v
. OSF/1/bin/sh
有一个type
内置函数,在 OSF/1 v3.x 之前始终返回 0。bash
没有添加,whence
但添加了一个-p
选项来type
打印路径(type -p
就像whence -p
)并-a
报告全部匹配的命令。tcsh
内置which
并添加了一个where
类似于bash
s 的命令type -a
。zsh
都有。
shell fish
(2005) 有一个type
作为函数实现的命令。
同时,csh脚本which
已从 NetBSD 中删除(因为它内置于 tcsh 中,在其他 shell 中没有太多用处),并且添加了功能whereis
(当作为 调用时which
,whereis
其行为类似which
,只是它只在 中查找可执行文件$PATH
)。在 OpenBSD 和 FreeBSD 中,which
也更改为用 C 编写的,仅在 中查找命令$PATH
。
实施
在各种 Unice 上,命令有数十种which
不同的语法和行为实现。
tcsh
在 Linux 上(除了和中的内置实现zsh
),我们找到了几种实现。例如,在最近的 Debian 系统上,它是一个简单的 POSIX shell 脚本,用于在$PATH
.
busybox
还有一个which
命令。
有一种GNU
which
可能是最奢侈的一种。它尝试将 csh 脚本的功能扩展which
到其他 shell:您可以告诉它您的别名和函数是什么,以便它可以为您提供更好的答案(我相信一些 Linux 发行版为此设置了一些全局别名bash
) 。
zsh
有几个运营商扩展到可执行文件的路径:=
文件名扩展运算符和:c
历史扩展修饰符(此处应用于参数扩展):
$ print -r -- =ls
/bin/ls
$ cmd=ls; print -r -- $cmd:c
/bin/ls
zsh
,在zsh/parameters
模块中也将命令哈希表作为commands
关联数组:
$ print -r -- $commands[ls]
/bin/ls
该whatis
实用程序(除了 Unix V8 Bourne shell 或 Plan 9 rc
/中的实用程序es
)并不真正相关,因为它仅用于文档(greps Whatis 数据库,即手册页概要)。
whereis
3BSD
也被同时添加,就好像which
它是在 中编写的一样C
,而不是csh
用于同时查找可执行文件、手册页和源代码,但不基于当前环境。再说一遍,这满足了不同的需求。
现在,在标准方面,POSIX 指定了command -v
和-V
命令(在 POSIX.2008 之前它一直是可选的)。 UNIX 指定type
命令(无选项)。这就是全部(where
、which
、whence
没有在任何标准中指定)。
直到某些版本,type
和command -v
在 Linux Standard Base 规范中都是可选的,这解释了为什么例如某些旧版本posh
(尽管基于pdksh
其两者)都没有。command -v
还被添加到一些 Bourne shell 实现中(例如在 Solaris 上)。
今日状态
现在的状态是,type
并且在所有类似 Bourne 的 shell 中都很普遍(不过,正如 @jarno 所指出的,请注意下面注释中的“when not in POSIX mode”或 Almquist shell 的一些后代中的command -v
警告/错误)。是您想要使用的唯一 shell (因为那里没有并且是内置的)。bash
tcsh
which
type
which
tcsh
在和之外的 shell 中zsh
,which
只要我们的任何 或任何 shell 启动文件中没有同名的别名或函数,~/.cshrc
并且~/.bashrc
您没有$PATH
在~/.cshrc
.如果您为其定义了别名或函数,它可能会也可能不会告诉您,或者告诉您错误的事情。
如果您想了解给定名称的所有命令,则没有什么可移植的。您可以在 ksh93 和其他 shell 中使用where
intcsh
或zsh
、type -a
inbash
或zsh
,您可以将其结合使用。whence -a
type
which -a
建议
获取可执行文件的路径名
现在,要获取脚本中可执行文件的路径名,有一些注意事项:
ls=$(command -v ls)
将是标准的方法。
但存在一些问题:
- 如果不执行可执行文件,就不可能知道它的路径。所有的
type
,which
,command -v
... 都使用启发式方法来找出路径。它们循环遍历$PATH
组件并找到您具有执行权限的第一个非目录文件。但是,根据 shell 的不同,在执行命令时,许多命令(Bourne、AT&T ksh、zsh、ash...)只会按顺序执行它们,$PATH
直到execve
系统调用不返回错误为止。例如,如果$PATH
包含/foo:/bar
并且您想要执行ls
,它们将首先尝试执行,/foo/ls
否则如果失败/bar/ls
。现在执行/foo/ls
可能会失败,因为您没有执行权限,而且还有许多其他原因,例如它不是有效的可执行文件。command -v ls
会报告/foo/ls
您是否具有 的执行权限,但如果不是有效的可执行文件,/foo/ls
则运行ls
实际上可能会运行。/bar/ls
/foo/ls
- 如果
foo
是内置函数或函数或别名,则command -v foo
返回foo
。对于某些 shell,如ash
,pdksh
或,如果包含空字符串并且当前目录中有可执行文件zsh
,它也可能返回。在某些情况下,您可能需要考虑到这一点。请记住,内置函数列表随 shell 实现的不同而变化(例如,有时是 busybox 的内置函数),例如可以从环境中获取函数。foo
$PATH
foo
mount
sh
bash
- 如果
$PATH
包含相对路径组件(通常.
或空字符串,它们都引用当前目录,但可以是任何内容),根据 shell,command -v cmd
可能不会输出绝对路径。所以你运行时获得的路径在你到达其他地方command -v
后将不再有效。cd
- 轶事:使用 ksh93 shell,如果
/opt/ast/bin
(尽管我相信确切的路径在不同的系统上可能有所不同)在您的 中$PATH
,ksh93 将提供一些额外的内置函数(chmod
、cmp
、cat
...),但即使该路径不存在command -v chmod
也会返回/opt/ast/bin/chmod
不存在。
判断命令是否存在
要查明给定命令是否标准存在,您可以执行以下操作:
if command -v given-command > /dev/null 2>&1; then
echo given-command is available
else
echo given-command is not available
fi
人们可能想在哪里使用which
(t)csh
在csh
和中tcsh
,您没有太多选择。在 中tcsh
,这很好,因为which
是内置的。在 中csh
,这将是系统which
命令,在某些情况下它可能不会执行您想要的操作。
仅在某些 shell 中查找命令
使用它可能有意义的情况which
是,如果您想知道命令的路径,忽略 、(不是 )、 或 shell 脚本中潜在的 shell 内置函数或函数bash
,csh
即tcsh
没有dash
(Bourne
如whence -p
或ksh
)zsh
的shell 、command -ev
(如yash
)、whatis -p
(rc
、 )或系统上可用且不是脚本的akanga
内置函数which
(如tcsh
或) 。zsh
which
csh
如果满足这些条件,则:
echo=$(which echo)
echo
将为您提供第一个in的路径$PATH
(除了极端情况),无论是否echo
也恰好是 shell 内置/别名/函数。
在其他 shell 中,您更喜欢:
- 桀骜:
echo==echo
或echo=$commands[echo]
或echo=${${:-echo}:c}
- 什,桀骜:
echo=$(whence -p echo)
- 亚什:
echo=$(command -ev echo)
- RC,阿坎加:(
echo=`whatis -p echo`
注意带空格的路径) - 鱼:
set echo (type -fp echo)
请注意,如果您想做的只是跑步该echo
命令,你不必获取它的路径,你可以这样做:
env echo this is not echoed by the builtin echo
例如,使用, 来防止使用tcsh
内置函数:which
set Echo = "`env which echo`"
当您确实需要外部命令时
您可能想要使用的另一种情况which
是当您实际上需要一个外部命令。 POSIX 要求所有 shell 内置命令(如)也可用作外部命令,但不幸的是,在许多系统上command
并非如此。例如,在基于 Linux 的操作系统上command
很少找到命令,而大多数操作系统都有命令(尽管不同的操作系统具有不同的选项和行为)。command
which
您可能需要外部命令的情况是在不调用 POSIX shell 的情况下执行命令的任何地方。
system("some command line")
C 或各种语言的, ...函数popen()
确实会调用 shell 来解析该命令行,因此system("command -v my-cmd")
请在其中工作。一个例外是,perl
如果 shell 没有看到任何 shell 特殊字符(空格除外),则会优化 shell。这也适用于它的反引号运算符:
$ perl -le 'print system "command -v emacs"'
-1
$ perl -le 'print system ":;command -v emacs"'
/usr/bin/emacs
0
$ perl -e 'print `command -v emacs`'
$ perl -e 'print `:;command -v emacs`'
/usr/bin/emacs
添加:;
上面的内容会强制perl
调用 shell。通过使用which
,您就不必使用该技巧。
答案2
人们可能不想使用的原因which
已经被解释过,但这里有一些实际失败的系统的一些示例which
。
在类似 Bourne 的 shell 上,我们将 的输出which
与 的输出进行比较type
(type
作为 shell 内置命令,它应该是基本事实,因为它是 shell 告诉我们它将如何调用命令)。
很多情况都是角落情况下,但请记住which
/type
经常用于极端情况(以找到意外行为的答案,例如:到底为什么这个命令会有这样的行为,我在调用哪个命令?)。
大多数系统,大多数类似 Bourne 的 shell:函数
最明显的情况是函数:
$ type ls
ls is a function
ls ()
{
[ -t 1 ] && set -- -F "$@";
command ls "$@"
}
$ which ls
/bin/ls
原因是which
仅报告有关可执行文件的信息,有时还报告有关别名的信息(尽管并不总是你的shell),而不是函数。
GNU which 手册页有一个损坏的(因为他们忘记引用$@
)示例,说明如何使用它来报告函数,但就像别名一样,因为它没有实现 shell 语法解析器,所以很容易被愚弄:
$ which() { (alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@";}
$ f() { echo $'\n}\ng ()\n{ echo bar;\n}\n' >> ~/foo; }
$ type f
f is a function
f ()
{
echo '
}
g ()
{ echo bar;
}
' >> ~/foo
}
$ type g
bash: type: g: not found
$ which f
f ()
{
echo '
}
$ which g
g ()
{ echo bar;
}
大多数系统,大多数类似 Bourne 的 shell:内置程序
另一个明显的情况是内置命令或关键字,因为which
作为外部命令无法知道您的 shell 有哪些内置命令(有些 shell 像zsh
,bash
或ksh
可以动态加载内置命令):
$ type echo . time
echo is a shell builtin
. is a shell builtin
time is a shell keyword
$ which echo . time
/bin/echo
which: no . in (/bin:/usr/bin)
/usr/bin/time
(这不适用于内置的zsh
地方)which
Solaris 10、AIX 7.1、HP/UX 11i、Tru64 5.1 等:
$ csh
% which ls
ls: aliased to ls -F
% unalias ls
% which ls
ls: aliased to ls -F
% ksh
$ which ls
ls: aliased to ls -F
$ type ls
ls is a tracked alias for /usr/bin/ls
这是因为在大多数商业 Unices 上which
(如 3BSD 上的原始实现)是一个csh
读取~/.cshrc
.它将报告的别名是在那里定义的别名,无论您当前定义的别名是什么,也无论您实际使用的 shell 是什么。
在 HP/UX 或 Tru64 中:
% echo 'setenv PATH /bin:/usr/bin' >> ~/.cshrc
% setenv PATH ~/bin:/bin:/usr/bin
% ln -s /bin/ls ~/bin/
% which ls
/bin/ls
(Solaris 和 AIX 版本已通过在$path
读取之前保存~/.cshrc
并在查找命令之前恢复它来解决该问题)
$ type 'a b'
a b is /home/stephane/bin/a b
$ which 'a b'
no a in /usr/sbin /usr/bin
no b in /usr/sbin /usr/bin
或者:
$ d="$HOME/my bin"
$ mkdir "$d"; PATH=$PATH:$d
$ ln -s /bin/ls "$d/myls"
$ type myls
myls is /home/stephane/my bin/myls
$ which myls
no myls in /usr/sbin /usr/bin /home/stephane/my bin
(当然,作为一个csh
脚本,你不能指望它能够处理包含空格的参数......)
CentOS 6.4,bash
$ type which
which is aliased to `alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
$ alias foo=': "|test|"'
$ which foo
alias foo=': "|test|"'
/usr/bin/test
$ alias $'foo=\nalias bar='
$ unalias bar
-bash: unalias: bar: not found
$ which bar
alias bar='
在该系统上,系统范围内定义了一个别名来包装 GNUwhich
命令。
虚假输出是因为读取了swhich
的输出,但不知道如何正确解析它并使用启发式(每行一个别名,在, , ... 之后查找第一个找到的命令)bash
alias
|
;
&
CentOS 上最糟糕的事情是它zsh
有一个完美的which
内置命令,但 CentOS 设法通过用 GNU 的非工作别名替换它来破坏它which
。
Debian 7.0、ksh93:
(尽管适用于大多数具有多个 shell 的系统)
$ unset PATH
$ which which
/usr/local/bin/which
$ type which
which is a tracked alias for /bin/which
在 Debian 上,/bin/which
是一个/bin/sh
脚本。就我而言,sh
是dash
但当它是时是一样的bash
。
取消设置PATH
并不是禁用PATH
查找,而是意味着使用系统的默认路径不幸的是,在 Debian 上,没有人同意(dash
并且bash
has /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
、zsh
has /bin:/usr/bin:/usr/ucb:/usr/local/bin
、ksh93
has /bin:/usr/bin
、mksh
has /usr/bin:/bin
( $(getconf PATH)
) 、execvp()
(就像env
) has :/bin:/usr/bin
(是的,首先查看当前目录!))。
这就是为什么which
上面会出错,因为它使用的是与不同的dash
默认值PATH
ksh93
GNU 的情况并没有更好,which
它报告:
which: no which in ((null))
/usr/local/bin/which
(有趣的是,我的系统上确实有一个脚本,它实际上是akanga
附带的脚本akanga
(一个rc
shell 衍生物,默认值为PATH
)/usr/ucb:/usr/bin:/bin:.
)
bash,任何系统:
唯一的那个克里斯在他的回答中提到:
$ PATH=$HOME/bin:/bin
$ ls /dev/null
/dev/null
$ cp /bin/ls bin
$ type ls
ls is hashed (/bin/ls)
$ command -v ls
/bin/ls
$ which ls
/home/chazelas/bin/ls
同样在hash
手动调用后:
$ type -a which
which is /usr/local/bin/which
which is /usr/bin/which
which is /bin/which
$ hash -p /bin/which which
$ which which
/usr/local/bin/which
$ type which
which is hashed (/bin/which)
which
现在是有时会失败的情况type
:
$ mkdir a b
$ echo '#!/bin/echo' > a/foo
$ echo '#!/' > b/foo
$ chmod +x a/foo b/foo
$ PATH=b:a:$PATH
$ which foo
b/foo
$ type foo
foo is b/foo
现在,使用一些 shell:
$ foo
bash: ./b/foo: /: bad interpreter: Permission denied
和其他人:
$ foo
a/foo
既不能which
也type
不能提前知道b/foo
不能执行。某些 shell(如bash
、ksh
或yash
)在调用时foo
确实会尝试运行b/foo
并报告错误,而其他 shell(如zsh
、ash
、csh
、Bourne
、 )将在 上的系统调用失败时tcsh
运行。a/foo
execve()
b/foo
答案3
(从我的快速浏览来看)Stephane 似乎没有提到的一件事是它which
不知道你的 shell 的路径哈希表。这样做的结果是,它可能返回一个不能代表实际运行结果的结果,这使得它在调试中无效。
答案4
(由于这个问题被标记为“可移植性”,因此排除了对该问题的各种解释,包括“交互式使用”和“我只关心我正在使用的系统”。所以这个答案只考虑为什么我不应该在 shell 脚本中执行此操作?)
Stéphane 对这些选项进行了全面分析,并给出了每个选项好坏的原因。没有任何一个原因which
总是错误的答案。相反,还有许多较小的影响因素。虽然您可能可以容忍其中的一个或几个,但将它们放在一起可能会改变您的想法。
尚未提及的不使用的一个原因which
是它引出了一个问题:
which
为什么你首先想要输出?
我的意思是,有时目标不应该是找到 的直接替代品which
,而是以一种一开始就不需要它的方式重构代码。
一个常见的模式是:
CMD=$( which cmd )
# some time later...
$CMD --some --args
撇开几乎总是不被引用的事实$CMD
不谈,我认为整个模式都被打破了。
CMD=/my/path/to/cmd
当目的是为了避免依赖于时,硬编码是一回事$PATH
。
但如果你无论如何都要搜索$PATH
,那么几乎总是没有意义。完全去掉which
and $CMD
,然后写:
cmd --some --args
如果您认为需要该路径是因为您想将其嵌入到函数或别名中,那么就可以command
使用该路径:
function cmd {
command cmd --extra-arg "$@"
}
当然也有例外,您需要基于不同的PATH
.在这种情况下,请考虑使用:
function cmd {
PATH=$OTHERPATH command cmd "$@"
}
总的来说,重点是避免对“如何获取命令的路径”有一个心理捷径,而是根据具体情况进行考虑:我真的需要这条路吗?为什么?
在某些情况下which
可能是可以接受的,但至少在某些时候会有更好的解决方案。如果您正在为其他人编写教程,请更加努力地考虑其他选择。
即使您认为这CMD=$( which cmd ) ; ... $CMD args
是有道理的,也有其他原因需要避免which
这不是通用的
这POSIX 所需的命令列表包括command
和type
但不包括which
。
其输出格式未指定
which
只是有义务表明指定命令的路径,而不是逐字提供且不带注释。虽然大多数版本。
which foo
输出如下内容是完全合法的:
foo might be /opt/foobar/bin/foo (unverified)
;或者foo is in /bin
;或者~foo/bin/foo
(其中~foo
表示 foo 的主目录);或者/proc/1234/root/bin/foo
(进程 1234 已终止)
答案可能会过时
(这基本上排除了所有形式的CMD=$( something ) ; ... $CMD ...
并且不特定于which
。)
由于各种原因,给定命令名称的可执行文件的位置可能会发生变化。
有时您想要旧位置,有时则不需要。
有时命令的路径是相对的(从而.
不是开始/
)。这种情况很少见,因为它通常被认为是一种安全风险,但在特殊情况下可能仍然适用,在这种情况下,简单地执行cd
就可以使程序的路径无效。