为什么不使用“哪个”?那用什么?

Sté*_*las 422 shell history which portability

当寻找的路径,可执行文件或检查,如果你在Unix shell中输入命令的名称会发生什么,有不同的公用事业过多(whichtypecommandwhencewherewhereiswhatishash,等)。

我们经常听说which应该避免。为什么?我们应该用什么来代替?

Sté*_*las 463

以下是您从未想过不想知道的所有内容:

概括

在类似 Bourne 的 shell 脚本中获取可执行文件的路径名(有一些注意事项;见下文):

ls=$(command -v ls)
Run Code Online (Sandbox Code Playgroud)

要找出给定的命令是否存在:

if command -v given-command > /dev/null 2>&1; then
  echo given-command is available
else
  echo given-command is not available
fi
Run Code Online (Sandbox Code Playgroud)

在类似 Bourne 的交互式 shell 的提示下:

type ls
Run Code Online (Sandbox Code Playgroud)

which命令是 C-Shell 的一个破碎的遗产,最好单独留在类似 Bourne 的 shell 中。

用例

作为脚本的一部分或在 shell 提示符下以交互方式查找该信息是有区别的。

在 shell 提示符下,典型的用例是:这个命令的行为很奇怪,我使用的是正确的吗?我打字时到底发生了什么mycmd?我可以进一步看看它是什么吗?

在这种情况下,您想知道在调用命令而不实际调用命令时 shell 做了什么。

在 shell 脚本中,它往往是完全不同的。在 shell 脚本中,如果您只想运行命令,那么您没有理由想知道命令在哪里或命令是什么。通常,您想知道的是可执行文件的路径,因此您可以从中获取更多信息(例如与该文件相关的另一个文件的路径,或者从该路径处的可执行文件的内容中读取信息)。

交互,你可能想知道所有my-cmd系统上可用的命令,在脚本中,很少如此。

大多数可用工具(通常是这种情况)都设计为可交互使用。

历史

先说一点历史。

直到 70 年代后期的早期 Unix shell 都没有函数或别名。只有传统的在$PATH. csh1978年左右推出的别名(虽然csh最初发布2BSD,在1979年5月),并且还的加工.cshrc,为用户定制的外壳(每壳,如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通过你可能已经在提示符下或例如后面定义,而不是那些source荷兰国际集团的另一个csh文件,(虽然这不会是一个好主意),PATH可能会在被重新定义~/.cshrc

运行which从一个Bourne shell命令将在您的定义仍然查找别名~/.cshrc,但如果你没有一个,因为你不使用csh,这将仍然可能得到你正确的答案。

直到 1984 年在 SVR2 中使用type内置命令才将类似的功能添加到 Bourne shell 。它是内置的(与外部脚本相反)这一事实意味着它可以为您提供正确的信息(在某种程度上),因为它可以访问 shell 的内部结构。

初始type命令遇到与which脚本类似的问题,因为如果找不到命令,它不会返回失败退出状态。此外,对于可执行文件,相反which,它输出类似ls is /bin/ls,而不是仅仅/bin/ls这使得它不太容易使用的脚本。

Unix 版本 8(未正式发布)的 Bourne shell 将其type内置程序重命名为whatis并扩展为还报告有关参数和打印函数定义的信息。它还type解决了在找不到名称时不返回失败的问题。

rc,Plan9(曾经是 Unix 的继任者)(及其衍生物,如akangaes)的外壳whatis也有。

Korn shell(POSIXsh定义所基于的一个子集)开发于 80 年代中期,但在 1988 年之前并未广泛使用csh,在 Bourne shell 之上添加了许多功能(行编辑器、别名...)。它添加了自己的whence内置(除了type),它采用了多个选项(-v提供type类似 - 的详细输出,并-p仅查找可执行文件(而不是别名/函数...))。

与 AT&T 和 Berkeley 之间版权问题的混乱相吻合,80 年代末 90 年代初出现了一些免费的软件外壳实现。所有的 Almquist shell(ash用于替代 BSD 中的 Bourne shell),kshpdksh)的公共领域实现,bash(由 FSF 赞助),zsh在 1989 年和 1991 年之间问世。

Ash,虽然打算替代 Bourne shell,但type直到很久以后(在 NetBSD 1.3 和 FreeBSD 2.3 中)才具有内置,尽管它有hash -v. OSF/1/bin/sh有一个type内置函数,它在 OSF/1 v3.x 之前总是返回 0。bash没有添加 awhence但添加了一个-p选项来type打印路径(type -p就像whence -p)并-a报告所有匹配的命令。tcshwhich内置并增加了一个where表现得象命令bashtype -azsh都有。

fish外壳(2005)具有type作为一个功能来实现命令。

which从NetBSD的取出CSH脚本同时(因为它是在tcsh中和其他炮弹没有多大用处的内置),并加入到功能whereis(如时调用whichwhereis行为就像which但它仅查找可执行程序$PATH)。在 OpenBSD 和 FreeBSD 中,which也改为用 C 编写的,$PATH只在其中查找命令。

实现

which在各种具有不同语法和行为的 Unice 上有许多命令的实现。

在Linux(旁边的内置者tcshzsh),我们发现几种实现。例如,在最近的 Debian 系统上,它是一个简单的 POSIX shell 脚本,它在$PATH.

busybox也有which命令。

有一种GNU which可能是最奢侈的。它试图将whichcsh 脚本所做的扩展到其他 shell:您可以告诉它您的别名和函数是什么,以便它可以给您一个更好的答案(我相信一些 Linux 发行版为此设置了一些全局别名bash) .

zsh有几个运算符可以扩展到可执行文件的路径:= 文件名扩展运算符和:c历史扩展修饰符(此处应用于参数扩展):

$ print -r -- =ls
/bin/ls
$ cmd=ls; print -r -- $cmd:c
/bin/ls
Run Code Online (Sandbox Code Playgroud)

zsh,在zsh/parameters模块中也将命令哈希表作为commands关联数组:

$ print -r -- $commands[ls]
/bin/ls
Run Code Online (Sandbox Code Playgroud)

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并且command -v在所有类似 Bourne 的 shell 中无处不在(不过,正如@jarno 所指出的,请注意bash不在 POSIX 模式下或下面的 Almquist shell 的一些后代在评论中的警告/错误)。tcsh是您想要使用的唯一外壳which(因为那里没有type并且which是内置的)。

在比其他的外壳tcshzshwhich可能会告诉你给定的可执行文件的路径,只要有任何我们没有别名或功能的相同名称~/.cshrc~/.bashrc或任何shell启动文件,你不定义$PATH你的~/.cshrc。如果您为它定义了别名或函数,它可能会也可能不会告诉您有关它的信息,或者告诉您错误的信息。

如果您想通过给定名称了解所有命令,则没有什么可移植的。你会使用wheretcsh还是zshtype -abash或者zshwhence -a在ksh93的和其他炮弹,你可以用type结合which -a这可能工作。

建议

获取可执行文件的路径名

现在,要在脚本中获取可执行文件的路径名,有一些注意事项:

ls=$(command -v ls)
Run Code Online (Sandbox Code Playgroud)

将是做到这一点的标准方法。

不过有几个问题:

  • 不执行它就不可能知道可执行文件的路径。所有type, which, command -v... 都使用启发式方法来找出路径。它们遍历$PATH组件并找到您对其具有执行权限的第一个非目录文件。但是,根据 shell,在执行命令时,它们中的许多(Bourne、AT&T ksh、zsh、ash...)只会按照 的顺序执行它们,$PATH直到execve系统调用不返回错误. 例如,如果$PATHcontains/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. 对于诸如ash, pdkshor 之类的外壳,如果包含空字符串并且当前目录中有一个可执行文件zsh,它也可能返回。在某些情况下,您可能需要考虑到这一点。例如,请记住,内置函数列表随 shell 实现而变化(例如,有时内置于 busybox ),例如可以从环境中获取函数。foo$PATHfoomountshbash
  • 如果$PATH包含相对路径组件(通常.或空字符串,它们都引用当前目录但可以是任何内容),取决于外壳,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
Run Code Online (Sandbox Code Playgroud)

人们可能想要使用的地方 which

(t)csh

csh和 中tcsh,您没有太多选择。在 中tcsh,这很好,就像which内置的一样。在 中csh,这将是系统which命令,在某些情况下可能无法执行您想要的操作。

仅在某些 shell 中查找命令

使用可能有意义的一种情况which是,如果您想知道命令的路径,而忽略bashcsh(不是tcshdash、 或Bourneshell 脚本中的潜在 shell 内置函数或函数,即没有的 shell whence -p(如kshzsh) , command -ev(like yash), whatis -p( rc, akanga) 或在可用且不是脚本的系统上的内置which(like tcshor zsh)。whichcsh

如果满足这些条件,则:

echo=$(which echo)
Run Code Online (Sandbox Code Playgroud)

会给你的第一个路径echo$PATH(除了在角落的情况下),无论是否echo也恰好是一个shell内建/别名/功能与否。

在其他 shell 中,您更喜欢:

  • zsh中echo==echoecho=$commands[echo]echo=${${:-echo}:c}
  • kshzshecho=$(whence -p echo)
  • 亚什echo=$(command -ev echo)
  • rc , akanga :(echo=`whatis -p echo`注意带空格的路径)
  • set echo (type -fp echo)

请注意,如果您只想运行echo命令,则不必获取其路径,只需执行以下操作:

env echo this is not echoed by the builtin echo
Run Code Online (Sandbox Code Playgroud)

例如,使用tcsh, 以防止使用内置函数which

set Echo = "`env which echo`"
Run Code Online (Sandbox Code Playgroud)

当您确实需要外部命令时

您可能想要使用的另一种情况which是您确实需要外部命令。POSIX 要求所有 shell 内置命令(如command)也可用作外部命令,但不幸的是,command在许多系统上并非如此。例如,很少command在基于 Linux 的操作系统上找到命令,而大多数操作系统都有which命令(尽管不同的命令具有不同的选项和行为)。

您可能需要外部命令的情况将是您在不调用 POSIX shell 的情况下执行命令的任何地方。

C 或各种语言的system("some command line"), popen()... 函数确实会调用 shell 来解析该命令行,因此system("command -v my-cmd")请使用它们。一个例外是perl如果没有看到任何 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
Run Code Online (Sandbox Code Playgroud)

添加的那:;上面的力量perl来调用一个shell那里。通过使用which,您将不必使用该技巧。

  • @Joe,`which` 是许多商业 Unices 上的 `csh` 脚本。原因是历史性的,这就是我给出历史的原因,所以人们了解它的来源,人们为什么习惯使用它以及为什么实际上你没有理由使用它。是的,有些人使用 (t)csh。不是每个人都使用 Linux (27认同)
  • 阅读这篇文章后,我找到了很多答案的背景,但不是答案本身。这篇文章的哪里实际上说明了为什么*不*使用“which”,而不是您可能尝试使用“which”来做的事情、“which”的历史、“which”的实现、其他命令做相关任务,或实际使用“which”的原因?为什么其他命令*更好*?它们与“哪个”有什么不同?他们如何避免它的陷阱?这个答案实际上在替代方案的问题上花费的词多于`which`的问题。 (15认同)
  • `command` 由 POSIX 描述。 (3认同)
  • @StéphaneChazelas如果我通过`touch /usr/bin/mytestfile`创建新文件,然后运行`command -v mytestfile`,它将给出路径(而`which mytestfile`没有)。 (3认同)
  • @jarno,哦,是的,你说得对。如果找不到可执行文件,`bash` 将在不可执行文件上解决,所以它“OK”(尽管在实践中人们宁愿 `command -v`/`type` 返回错误),因为那是命令它会在你运行 `mytestfile` 时尝试执行,但是 `dash` 的行为有问题,就好像在可执行文件之前有一个不可执行的 `cmd`,`command -v` 在执行时返回不可执行的`cmd` 将执行可执行文件(错误的也会被散列)。FreeBSD `sh`(也基于 `ash`)有同样的错误。zsh、yash、ksh、mksh、bash 作为 sh 都可以。 (2认同)

Sté*_*las 62

可能不想使用的原因which已经解释过,但这里有几个which实际失败的系统的示例。

在类似 Bourne 的 shell 上,我们将 的输出whichtype(type作为一个 shell 内置函数,它意味着是基本事实,因为它是 shell 告诉我们它将如何调用命令) 的输出。

许多情况都是极端情况,但请记住,which/type经常在极端情况下使用(以找到意外行为的答案,例如:为什么该命令的行为如此,我要调用哪个?)。

大多数系统,大多数 Bourne-like shell:函数

最明显的例子是函数:

$ type ls
ls is a function
ls ()
{
[ -t 1 ] && set -- -F "$@";
command ls "$@"
}
$ which ls
/bin/ls
Run Code Online (Sandbox Code Playgroud)

原因是which只报告可执行文件,有时报告别名(虽然并不总是你的shell 的),而不是函数。

$@关于如何使用它来报告函数的 GNU 手册页有一个损坏的(因为他们忘记引用)示例,但就像别名一样,因为它没有实现 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;
}
Run Code Online (Sandbox Code Playgroud)

大多数系统,大多数类似 Bourne 的 shell:内置函数

另一个明显的情况是内置函数或关键字,因为which作为外部命令无法知道您的 shell 具有哪些内置函数(有些 shell 像zshbash或者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
Run Code Online (Sandbox Code Playgroud)

(这并不适用于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
Run Code Online (Sandbox Code Playgroud)

这是因为在大多数商业 Unices 上which(就像在 3BSD 上的原始实现中一样)是一个csh读取~/.cshrc. 它将报告的别名是在那里定义的别名,无论您当前定义的别名如何,也无论您实际使用的外壳如何。

在 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
Run Code Online (Sandbox Code Playgroud)

(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
Run Code Online (Sandbox Code Playgroud)

或者:

$ 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
Run Code Online (Sandbox Code Playgroud)

(当然,作为一个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='
Run Code Online (Sandbox Code Playgroud)

在该系统上,有一个在系统范围内定义的别名,用于包装 GNUwhich命令。

虚假输出是因为which读取bash's的输出alias但不知道如何正确解析它并使用启发式(每行一个别名,在 a |, ;, &...之后查找第一个找到的命令)

CentOS 上最糟糕的事情是它zsh有一个完美的which内置命令,但 CentOS 设法通过用一个非工作别名 GNU 替换它来破坏它which

Debian 7.0,ksh93:

(虽然适用于大多数具有许多外壳的系统)

$ unset PATH
$ which which
/usr/local/bin/which
$ type which
which is a tracked alias for /bin/which
Run Code Online (Sandbox Code Playgroud)

在 Debian 上,/bin/which是一个/bin/sh脚本。在我的情况下,shdash但是当它是bash.

取消设置PATH不是禁用PATH查找,而是意味着使用系统的默认 PATH不幸的是在 Debian 上,没有人同意 ( dashand bashhave /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, zshhas /bin:/usr/bin:/usr/ucb:/usr/local/bin, ksh93has /bin:/usr/bin, mkshhas /usr/bin:/bin( $(getconf PATH)), execvp()(就像 in env) has :/bin:/usr/bin(是的,首先在当前目录中查找! ))。

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

GNUwhich并没有更好的报告:

which: no which in ((null))
Run Code Online (Sandbox Code Playgroud)

(有趣的是/usr/local/bin/which,我的系统上确实有一个akanga脚本,它实际上是一个附带的脚本akangarc默认PATH为的shell 衍生物/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
Run Code Online (Sandbox Code Playgroud)

同样在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)
Run Code Online (Sandbox Code Playgroud)

现在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
Run Code Online (Sandbox Code Playgroud)

现在,有一些贝壳:

$ foo
bash: ./b/foo: /: bad interpreter: Permission denied
Run Code Online (Sandbox Code Playgroud)

和其他人:

$ foo
a/foo
Run Code Online (Sandbox Code Playgroud)

which不能也type不能提前知道b/foo不能执行。某些外壳程序(如bashkshyash)在调用foo时确实会尝试运行b/foo并报告错误,而其他外壳程序(如zshashcshBournetcsh)将a/fooexecve()系统调用 on失败时运行b/foo


Chr*_*own 25

一件事(从我的快速浏览中)似乎 Stephane 没有提到的一件事是which不知道您的 shell 的路径哈希表。这会导致它可能返回不代表实际运行内容的结果,从而使其在调试中无效。


ary*_*din 6

which经常使用这样的方式快速进入可执行文件:vim $(which my_executable). 最近我发现这个帖子,很惊讶——我一直都做错了?但后来我检查了所有答案,并没有找到任何which完全放弃的理由。此外,没有一个答案为我提供了以下内容的直接替代vim $(which my_executable):(输出保持原样)

$ type -a pacman
pacman is aliased to `pacman'
pacman is /usr/bin/pacman
$ command -v pacman
alias pacman='pacman'
$ which pacman
/usr/bin/pacman
$ bash --version
GNU bash, version 5.1.8(1)-release (x86_64-pc-linux-gnu)
Run Code Online (Sandbox Code Playgroud)

which 命令是 C-Shell 的一个被破坏的遗产,最好单独保留在类似 Bourne 的 shell 中。

我认为你应该使用任何工具,只要它能满足你的所有需求。我用来which在 中查找可执行路径$PATH,当然它不是一个合适的工具来调查命令背后的实际内容,然后typecommand会更适合。