为什么不用“哪个”呢?那该用什么呢?

为什么不用“哪个”呢?那该用什么呢?

当查找可执行文件的路径或检查在 Unix shell 中输入命令名称会发生​​什么时,有大量不同的实用程序(whichtypecommandwhencewherewhereiswhatishash等)。

我们经常听说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. csh1978 年左右引入了别名(尽管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),一个whichcsh脚本添加的目的是为了csh帮助用户识别可执行文件,它与which当今许多商业 Unices(如 Solaris、HP/UX、AIX 或 Tru64)上可以找到的脚本几乎没有什么不同。

该脚本读取用户的~/.cshrc(就像所有csh脚本一样,除非使用 调用),并在别名列表和(基于 维护的数组)csh -f中查找提供的命令名称。$pathcsh$PATH

给你:which首先是当时最流行的 shell(csh直到 90 年代中期仍然流行),这也是它被记录在书籍中并且仍然被广泛使用的主要原因。

请注意,即使对于csh用户来说,whichcsh 脚本也不一定会为您提供正确的信息。它获取 中定义的别名~/.cshrc,而不是您稍后在提示符下定义的别名,或者例如通过sourceing 另一个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 的曾经的继承者)的外壳(及其衍生产品,如akangaeswhatis也有。

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 年间问世。kshpdkshbashzsh

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类似于bashs 的命令type -azsh都有。

shell fish(2005) 有一个type作为函数实现的命令。

同时,csh脚本which已从 NetBSD 中删除(因为它内置于 tcsh 中,在其他 shell 中没有太多用处),并且添加了功能whereis(当作为 调用时whichwhereis其行为类似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 数据库,即手册页概要)。

whereis3BSD也被同时添加,就好像which它是在 中编写的一样C,而不是csh用于同时查找可执行文件、手册页和源代码,但不基于当前环境。再说一遍,这满足了不同的需求。

现在,在标准方面,POSIX 指定了command -v-V命令(在 POSIX.2008 之前它一直是可选的)。 UNIX 指定type命令(无选项)。这就是全部(wherewhichwhence没有在任何标准中指定)。

直到某些版本,typecommand -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 (因为那里没有并且是内置的)。bashtcshwhichtypewhich

tcsh在和之外的 shell 中zshwhich只要我们的任何 或任何 shell 启动文件中没有同名的别名或函数,~/.cshrc并且~/.bashrc您没有$PATH~/.cshrc.如果您为其定义了别名或函数,它可能会也可能不会告诉您,或者告诉您错误的事情。

如果您想了解给定名称的所有命令,则没有什么可移植的。您可以在 ksh93 和其他 shell 中使用whereintcshzshtype -ainbashzsh,您可以将其结合使用。whence -atypewhich -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$PATHfoomountshbash
  • 如果$PATH包含相对路径组件(通常.或空字符串,它们都引用当前目录,但可以是任何内容),根据 shell,command -v cmd可能不会输出绝对路径。所以你运行时获得的路径在你到达其他地方command -v后将不再有效。cd
  • 轶事:使用 ksh93 shell,如果/opt/ast/bin(尽管我相信确切的路径在不同的系统上可能有所不同)在您的 中$PATH,ksh93 将提供一些额外的内置函数(chmodcmpcat...),但即使该路径不存在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 内置函数或函数bashcshtcsh没有dashBournewhence -pkshzsh的shell 、command -ev(如yash)、whatis -prc、 )或系统上可用且不是脚本的akanga内置函数which(如tcsh或) 。zshwhichcsh

如果满足这些条件,则:

echo=$(which echo)

echo将为您提供第一个in的路径$PATH(除了极端情况),无论是否echo也恰好是 shell 内置/别名/函数。

在其他 shell 中,您更喜欢:

  • 桀骜echo==echoecho=$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很少找到命令,而大多数操作系统都有命令(尽管不同的操作系统具有不同的选项和行为)。commandwhich

您可能需要外部命令的情况是在不调用 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与 的输出进行比较typetype作为 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,bashksh可以动态加载内置命令):

$ 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的输出,但不知道如何正确解析它并使用启发式(每行一个别名,在, , ... 之后查找第一个找到的命令)bashalias|;&

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脚本。就我而言,shdash但当它是时是一样的bash

取消设置PATH并不是禁用PATH查找,而是意味着使用系统的默认路径不幸的是,在 Debian 上,没有人同意(dash并且bashhas /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binzshhas /bin:/usr/bin:/usr/ucb:/usr/local/binksh93has /bin:/usr/binmkshhas /usr/bin:/bin( $(getconf PATH)) 、execvp()(就像env) has :/bin:/usr/bin(是的,首先查看当前目录!))。

这就是为什么which上面会出错,因为它使用的是与不同的dash默认值PATHksh93

GNU 的情况并没有更好,which它报告:

which: no which in ((null))

/usr/local/bin/which(有趣的是,我的系统上确实有一个脚本,它实际上是akanga附带的脚本akanga(一个rcshell 衍生物,默认值为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

既不能whichtype不能提前知道b/foo不能执行。某些 shell(如bashkshyash)在调用时foo确实会尝试运行b/foo并报告错误,而其他 shell(如zshashcshBourne、 )将在 上的系统调用失败时tcsh运行。a/fooexecve()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,那么几乎总是没有意义。完全去掉whichand $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 所需的命令列表包括commandtype但不包括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就可以使程序的路径无效。

相关内容