在 Windows 上编写批处理或者在 Linux 上编写 shell 脚本时,经常会有需要判断脚本中依赖的某个命令是否存在的需求,根据所依赖的命令是否存在,我们可能会需要执行不同的代码路径,或者报错退出。

Windows 上,我们可以使用 where 命令来进行判断,在 Linux 上,我们则有 which, type, hash, command 等命令可以使用。

Windows cmd.exe

在 Windows 的命令提示符 cmd.exe 下,我们可以使用 where 命令来检测当前目录或 %PATH% 下,有没有我们所需要的命令。如:

where notepad
where slmgr.vbs

如果找到匹配的话,%ERRORLEVEL% 就会被设为 0,否则 %ERRORLEVEL% 则会被设为 1

where 命令后所跟的名字可以不包含后缀,也可以包含后缀(如 .exe, .bat, .cmd 等),如果不包含后缀,where 命令会自动补全 %PATHEXT% 里列出的后缀并进行查找。

需要注意的是,如果没有指明后缀,如 where test,且在当前目录或 %PATH% 下存在同名无后缀的文件,也是会被 where 所匹配到的,然而由于缺少后缀,该文件却是不可执行的。也就是说,存在 where 命令正常执行,返回匹配结果,却不存在相应的命令的情况。

另外,where 命令在查找匹配时,会按照以下规则进行:

  1. 如果在多个文件夹下存在匹配,则按照先当前文件夹,再依照 %PATH% 中指定的顺序进行排序;
  2. 如果同一文件夹下有多个匹配,则会按文件名进行排序。

如在 C:\Users\zzzbuzz 目录下执行 where abc

C:\Users\zzzbuzz\abc
C:\Users\zzzbuzz\abc.bat
C:\Users\zzzbuzz\abc.cmd
C:\Users\zzzbuzz\abc.exe
C:\Windows\abc
C:\Windows\abc.bat
C:\Windows\abc.cmd
C:\Windows\abc.exe

而在执行一个可执行文件时,则是按照另外的规则:

  1. 如果在多个文件夹下存在匹配,则按照先当前文件夹,再依照 %PATH% 中指定的顺序进行选择;
  2. 如果同一文件夹下有多个匹配,则有以下规则:
    1. 若输入的命令包含后缀,如 foo.bar,则无论该后缀是否存在于 %PATHEXT% 中,会首先尝试 foo.bar,只有当该文件不存在时,才会继续依照 %PATHEXT% 中的顺序进行查找;
    2. 若输入的命令不包含后缀,如 foo,则会直接依照 %PATHEXT% 中列出的优先级进行选择,而不论 foo 是否存在,即没有后缀的文件则永远不可能被执行。

%PATHEXT% 的值为 .COM;.EXE;.BAT;.CMD,则在输入 abc 时,上例中的 C:\Users\zzzbuzz\abc.exe 会被执行。

由于使用 where 命令查找可执行文件与实际执行可执行文件时的优先级规则不同,因此其实我们并不能保证 where 命令成功执行时一定有相应的命令存在,也不能保证 where 命令返回的结果中排在前面的命令会被优先执行。这一点在考虑边界情况时需要注意。

Linux

在 Linux 下,我们有 which, type, hash, command 等命令可以用于检测命令可用性。虽然初看之下选择很多,但是考虑到跨平台兼容性,并非这其中的所有命令都是合适的选择。

which

which 是一个可以返回可执行命令完整路径的命令,在各发行版中一般由 which 包提供(在 Debian 系中由 debianutils 包提供)。当相应命令存在时,会打印其在系统上的位置;而不存在时,根据系统不同,可能不会有任何输出,也可能会有相应的提示信息(如在 Cygwin 下,会有类似如下输出 which: no cmd in (/usr/local/bin:/usr/bin:/bin))。

命令用法如下:

$ which bash
/usr/bin/bash
$ echo $?
0
$ which nonexistent
$ echo $?
1

脚本写法如下:

CMD=cmd
if which "$CMD" >/dev/null 2>&1; then
	echo 命令 $CMD 存在
else
	echo 命令 $CMD 不存在
fi

不过在脚本中使用 which 来检测命令并非一个好的选择,因为

  • which 是一个外部命令,在不同系统中的行为并非总是一致的,甚至在一些系统中,无论是否有匹配的命令,其返回值永远为 0;
  • 有些系统会对 which 命令挂钩,在调用 which 命令时,可能还会有些额外的代码被执行。

type

type 是 shell 内置的命令,可以用于显示命令的类型,当然同样也可以用于检测命令是否存在。存在时会在 STDOUT 输出相关信息,并返回 0;否则会在 STDERR 中打印相关错误信息,并返回大于 0 的值。

由于 type 命令是用于显示命令类型的,自然也会匹配到 shell 中定义的 alias, function, builtin 等非外部命令的名字,因此我们可以附上 -P 选项来仅匹配外部命令。不过需要注意的是 -P 选项并没有定义在 POSIX 标准中,使用时需要考虑到潜在的兼容性问题。

命令用法如下:

$ type bash
bash is /usr/bin/bash
$ echo $?
0
$ type nonexistent
nonexistent: not found
$ echo $?
1

脚本写法如下:

CMD=cmd
if type "$CMD" >/dev/null 2>&1; then
	echo 命令 $CMD 存在
else
	echo 命令 $CMD 不存在
fi

hash

hash 是 shell 内置的可以用于缓存命令在磁盘上位置的命令,成功执行时没有任何输出,返回 0;而执行失败时,则会在 STDERR 上打印相应的提示信息,并返回一个大于 0 的值。

命令用法如下:

$ hash bash
$ echo $?
0
$ hash nonexistent
sh: 1: hash: nonexistent: not found
$ echo $?
1

脚本写法如下:

CMD=cmd
if hash "$CMD" 2>/dev/null; then
	echo 命令 $CMD 存在
else
	echo 命令 $CMD 不存在
fi

使用 hash 命令来检测命令可用性有一额外的副作用就是会将被检测的命令缓存在 shell 的 hash 表中以减少后续定位该命令所需的时间。因此,如果被检测的命令随后就需要被执行的话,这一副作用就正好是我们所需要的了;反之,则最好不要使用 hash 以避免引入该副作用。

command

command 同样也是 shell 内置的命令,可以用于执行命令,或者使用 -v 选项以显示关于命令的有关信息。

该命令的 STDERR 用于输出可能的诊断信息;指定 -v 选项时,命令的 STDOUT 会被用于显示命令的路径。

当能够找到相关匹配的命令时,command 返回 0,否则会返回大于一个 0 的值。

命令用法如下:

$ command -v bash
/usr/bin/bash
$ echo $?
0
$ command -v nonexistent
$ echo $?
1

脚本写法如下:

CMD=cmd
if command -v "$CMD" >/dev/null 2>&1; then
	echo 命令 $CMD 存在
else
	echo 命令 $CMD 不存在
fi

command 的行为被 POSIX 标准良好地定义了,在各系统中有着一致的行为,是编写脚本时作为检测命令可用性的一个良好选择。

参考