如何检查bash脚本中是否存在程序?

How to check if a program exists from a Bash script?

我将如何验证程序是否存在,以返回错误并退出或继续脚本的方式验证?

这看起来应该很容易,但一直困扰着我。


回答

POSIX兼容:

1
command -v <the_command>

对于bash特定环境:

1
2
hash <the_command> # For regular commands. Or...
type <the_command> # To check built-ins and keywords

解释

避开which。它不仅是一个你启动的外部过程,只做很少的事情(也就是说,像hashtypecommand这样的内置设备要便宜得多),你还可以依靠内置设备来实际做你想做的事情,而外部命令的效果在不同的系统之间很容易发生变化。

为什么要关心?

  • 许多操作系统都有一个甚至不设置退出状态的which,这意味着if which foo甚至不会在那里工作,并且会始终报告foo存在,即使它不存在(请注意,有些posix shell似乎也会为hash做这件事)。
  • 许多操作系统使which做定制和邪恶的事情,例如更改输出,甚至钩住包管理器。

所以,不要使用which。相反,使用以下方法之一:

1
2
3
$ command -v foo >/dev/null 2>&1 || { echo >&2"I require foo but it's not installed.  Aborting."; exit 1; }
$ type foo >/dev/null 2>&1 || { echo >&2"I require foo but it's not installed.  Aborting."; exit 1; }
$ hash foo 2>/dev/null || { echo >&2"I require foo but it's not installed. &nbsp;Aborting."; exit 1; }

(小旁注:有些人会认为2>&-2>/dev/null相同,但较短——这是不真实的。2>&-关闭fd 2,fd 2在程序试图写入stderr时会导致错误,这与成功写入并丢弃输出(和危险!)截然不同。

如果您的hash bang是/bin/sh,那么您应该关心posix所说的内容。typehash的退出代码没有被posix很好地定义,并且当命令不存在时(还没有在type中看到),hash被视为成功退出。command的退出状态由posix很好地定义,因此使用起来可能最安全。

如果脚本使用bash,那么posix规则就不再重要了,typehash都变得非常安全。type现在有了一个-P来搜索PATHhash的副作用是对命令的位置进行散列(以便下次使用时更快地查找),这通常是一件好事,因为您可能会为了实际使用而检查它的存在。

作为一个简单的例子,这里有一个运行gdate的函数,如果它存在,则运行date

1
2
3
4
5
6
7
gnudate() {
    if hash gdate 2>/dev/null; then
        gdate"$@"
    else
        date"$@"
    fi
}


以下是检查$PATH中是否存在命令并可执行的可移植方法:

1
[ -x"$(command -v foo)" ]

例子:

1
2
3
4
if ! [ -x"$(command -v git)" ]; then
  echo 'Error: git is not installed.' >&2
  exit 1
fi

需要执行检查,因为如果在$PATH中找不到具有该名称的可执行文件,bash将返回一个非可执行文件。

另外,请注意,如果在早期的$PATH中存在与可执行文件同名的非可执行文件,dash将返回前者,即使后者将被执行。这是一个bug,违反了POSIX标准。[错误报告][标准]

此外,如果要查找的命令已定义为别名,则此操作将失败。


我同意lhunath不鼓励使用which,他的解决方案对bash用户完全有效。但是,为了便于携带,应使用command -v

1
$ command -v foo >/dev/null 2>&1 || { echo"I require foo but it's not installed.  Aborting.">&2; exit 1; }

命令command符合POSIX,其规范见:http://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html

注:type符合POSIX,但type -P不符合。


我在.bashrc中定义了一个函数,它使这变得更容易。

1
2
3
command_exists () {
    type"$1" &> /dev/null ;
}

下面是一个如何使用它的示例(来自我的.bash_profile)。

1
2
3
if command_exists mvim ; then
    export VISUAL="mvim --nofork"
fi


这取决于您是否想知道它是否存在于$PATH变量的某个目录中,或者您是否知道它的绝对位置。如果您想知道它是否在$PATH变量中,请使用

1
2
3
4
5
if which programname >/dev/null; then
    echo exists
else
    echo does not exist
fi

否则使用

1
2
3
4
5
if [ -x /path/to/programname ]; then
    echo exists
else
    echo does not exist
fi

在第一个例子中,重定向到/dev/null/会抑制which程序的输出。


在@lhunath's和@gregv's答案的基础上,下面是那些希望轻松将支票放入if声明中的人的代码:

1
2
3
4
exists()
{
  command -v"$1">/dev/null 2>&1
}

以下是如何使用它:

1
2
3
4
5
if exists bash; then
  echo 'Bash exists!'
else
  echo 'Your system does not have Bash'
fi


尝试使用:

1
test -x filename

1
[ -x filename ]

从条件表达式下的bash手册页:

1
2
 -x file
          True if file exists and is executable.


如@lhunath所建议的,在bash脚本中使用hash

1
2
3
4
hash foo &> /dev/null
if [ $? -eq 1 ]; then
    echo >&2"foo not found."
fi

这个脚本运行hash,然后检查最新命令的退出代码(存储在$?中的值)是否等于1。如果hash找不到foo,则退出代码为1。如果存在foo,则退出代码为0

&> /dev/null重定向hash的标准错误和标准输出,使其不出现在屏幕上,echo >&2将消息写入标准错误。


我从来没有得到过以上的解决方案来处理我能接触到的盒子。首先,安装了类型(执行其他操作)。因此需要内置指令。这个命令对我有效:

1
if [ `builtin type -p vim` ]; then echo"TRUE"; else echo"FALSE"; fi


如果您检查程序是否存在,您可能稍后再运行它。为什么不先试着运行它呢?

1
2
3
4
5
if foo --version >/dev/null 2>&1; then
    echo Found
else
    echo Not found
fi

这是一个更值得信赖的检查程序是否运行,而不仅仅是查看路径目录和文件权限。

另外,您还可以从程序中获得一些有用的结果,比如它的版本。

当然,缺点是有些程序启动起来可能很重,有些程序没有--version选项可以立即(成功)退出。


检查多个依赖项并向最终用户通知状态

1
2
3
4
5
6
7
8
for cmd in latex pandoc; do
  printf '%-10s'"$cmd"
  if hash"$cmd" 2>/dev/null; then
    echo OK
  else
    echo missing
  fi
done

样品输出:

1
2
latex     OK
pandoc    missing

10调整到最大命令长度。不是自动的,因为我看不到非详细的POSIX方式:如何在bash中对齐空间分隔表的列?


如果可以,为什么不使用bash内置的呢?

1
which programname

1
type -P programname


以东十一〔22〕与西施、巴施、大施、亚施同工。

type -p foo:它似乎与zsh、bash和ash(busybox)一起工作,但不是dash(它将-p解释为一个论点)。

command -v foo:与zsh、bash、dash一起工作,但不与ash(busybox)一起工作(-ash: command: not found)。

另请注意,builtin不适用于ashdash


对于那些感兴趣的人,如果您希望检测已安装的库,上述方法都不起作用。我想您要么需要对路径进行物理检查(可能检查头文件等),要么像这样(如果您使用的是基于Debian的发行版):

1
2
3
4
5
dpkg --status libdb-dev | grep -q not-installed

if [ $? -eq 0 ]; then
    apt-get install libdb-dev
fi

从上面可以看到,查询中的"0"答案意味着没有安装包。这是"grep"的函数-"0"表示找到匹配项,"1"表示找不到匹配项。


我想说,没有便携式和100%可靠的方式,因为悬挂的aliases。例如:

1
2
3
4
alias john='ls --color'
alias paul='george -F'
alias george='ls -h'
alias ringo=/

当然只有最后一个是有问题的(对林戈没有冒犯!)但从command -v的观点来看,它们都是有效的aliases。

为了拒绝像ringo这样的悬空问题,我们必须解析shell内置的alias命令的输出,并将其递归到其中(command -v并不优于alias)。这里没有可移植的解决方案,甚至一个bash特定的解决方案也相当繁琐。

请注意,这样的解决方案将无条件拒绝alias ls='ls -F'

1
test() { command -v $1 | grep -qv alias }


which命令可能有用。人哪

如果找到可执行文件,则返回0;如果找不到或不可执行,则返回1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NAME

       which - locate a command

SYNOPSIS

       which [-a] filename ...

DESCRIPTION

       which returns the pathnames of the files which would be executed in the
       current environment, had its arguments been  given  as  commands  in  a
       strictly  POSIX-conformant  shell.   It does this by searching the PATH
       for executable files matching the names of the arguments.

OPTIONS

       -a     print all matching pathnames of each argument

EXIT STATUS

       0      if all specified commands are found and executable

       1      if one or more specified commands is  nonexistent  or  not  exe-
          cutable

       2      if an invalid option is specified

很好的一点是,它可以找出运行环境中的可执行文件是否可用,从而节省了一些问题…

-亚当


如果没有可用的外部type命令(这里是理所当然的),我们可以使用符合posix的env -i sh -c 'type cmd 1>/dev/null 2>&1'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# portable version of Bash's type -P cmd (without output on stdout)
typep() {
   command -p env -i PATH="$PATH" sh -c '
      export LC_ALL=C LANG=C
      cmd="$1"
      cmd="`type"$cmd" 2>/dev/null || { echo"error: command $cmd not found; exiting ..." 1>&2; exit 1; }`"
      [ $? != 0 ] && exit 1
      case"$cmd" in
        *\ /*) exit 0;;
            *) printf"%s
""error: $cmd" 1>&2; exit 1;;
      esac
   '
_"$1" || exit 1
}

# get your standard $PATH value
#PATH="$(command -p getconf PATH)"
typep ls
typep builtin
typep ls-temp

至少在Mac OS X 10.6.8上,使用bash 4.2.24(2)command -v ls与移动的/bin/ls-temp不匹配。


如果您想检查程序是否存在并且实际上是一个程序,而不是一个bash内置命令,那么commandtypehash不适合测试,因为它们都返回0退出状态作为内置命令。

例如,有一个时间程序,它提供了比时间内置命令更多的功能。为了检查程序是否存在,我建议使用which,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
# first check if the time program exists
timeProg=`which time`
if ["$timeProg" ="" ]
then
  echo"The time program does not exist on this system."
  exit 1
fi

# invoke the time program
$timeProg --quiet -o result.txt -f"%S %U + p" du -sk ~
echo"Total CPU time: `dc -f result.txt` seconds"
rm result.txt

这里有很多选择,但我很惊讶没有一个快速的单字,这是我在脚本开始时使用的:
[["$(command -v mvn)" ]] || { echo"mvn is not installed" 1>&2 ; exit 1; }
[["$(command -v java)" ]] || { echo"java is not installed" 1>&2 ; exit 1; }

这是基于这里选择的答案和另一个来源(和我玩一点)。

希望这对其他人有用。


我第二次使用"命令-v"。像这样:

1
2
3
4
5
md=$(command -v mkdirhier) ; alias md=${md:=mkdir}  # bash

emacs="$(command -v emacs) -nw" || emacs=nano
alias e=$emacs
[[ -z $(command -v jed) ]] && alias jed=$emacs

为了模仿bash的type -P cmd,我们可以使用符合posix的env -i type cmd 1>/dev/null 2>&1

1
2
3
4
5
6
7
8
9
10
11
12
13
man env
#"The option '-i' causes env to completely ignore the environment it inherits."
# In other words, there are no aliases or functions to be looked up by the type command.

ls() { echo 'Hello, world!'; }

ls
type ls
env -i type ls

cmd=ls
cmd=lsx
env -i type $cmd 1>/dev/null 2>&1 || { echo"$cmd not found"; exit 1; }


如果你们不能让上面/下面的东西工作,把头发从背后拔出来,试着用bash -c运行相同的命令。看看这个梦魇,这就是运行$(sub命令)时实际发生的情况:

第一。它可以给你完全不同的输出。

1
2
3
4
$ command -v ls
alias ls='ls --color=auto'
$ bash -c"command -v ls"
/bin/ls

第二。它根本不能给你任何输出。

1
2
3
4
5
$ command -v nvm
nvm
$ bash -c"command -v nvm"
$ bash -c"nvm --help"
bash: nvm: command not found


hash变量有一个陷阱:在命令行上,您可以键入

1
one_folder/process

执行进程。为此,一个文件夹的父文件夹必须位于$path中。但当您尝试散列此命令时,它将始终成功:

1
hash one_folder/process; echo $? # will always output '0'


它会根据位置判断程序是否存在

1
2
3
if [ -x /usr/bin/yum ]; then
    echo This is Centos
fi


我的Debian服务器设置。当多个包包含相同的名称时,我遇到了一个问题。例如Apache2。所以这就是我的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function _apt_install() {
    apt-get install -y $1 > /dev/null
}

function _apt_install_norecommends() {
    apt-get install -y --no-install-recommends $1 > /dev/null
}
function _apt_available() {
    if [ `apt-cache search $1 | grep -o"$1" | uniq | wc -l` ="1" ]; then
        echo"Package is available : $1"
        PACKAGE_INSTALL="1"
    else
        echo"Package $1 is NOT available for install"
        echo "We can not continue without this package..."
        echo "Exitting now.."
        exit 0
    fi
}
function _package_install {
    _apt_available $1
    if ["${PACKAGE_INSTALL}" ="1" ]; then
        if ["$(dpkg-query -l $1 | tail -n1 | cut -c1-2)" ="ii" ]; then
             echo "package is already_installed: $1"
        else
            echo "installing package : $1, please wait.."
            _apt_install $1
            sleep 0.5
        fi
    fi
}

function _package_install_no_recommends {
    _apt_available $1
    if ["${PACKAGE_INSTALL}" ="1" ]; then
        if ["$(dpkg-query -l $1 | tail -n1 | cut -c1-2)" ="ii" ]; then
             echo "package is already_installed: $1"
        else
            echo "installing package : $1, please wait.."
            _apt_install_norecommends $1
            sleep 0.5
        fi
    fi
}

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# Commands found in the hash table are checked for existence before being
# executed and non-existence forces a normal PATH search.
shopt -s checkhash

function exists() {
 local mycomm=$1; shift || return 1

 hash $mycomm 2>/dev/null || \
 printf"\xe2\x9c\x98 [ABRT]: $mycomm: command does not exist
"
; return 1;
}
readonly -f exists

exists notacmd
exists bash
hash
bash -c 'printf"Fin.
"'

结果

1
2
3
4
? [ABRT]: notacmd: command does not exist
hits    command
   0    /usr/bin/bash
Fin.

我使用这个是因为它非常简单:

1
if [ `LANG=C type example 2>/dev/null|wc -l` = 1 ];then echo exists;else echo"not exists";fi

1
2
3
4
if [ `LANG=C type example 2>/dev/null|wc -l` = 1 ];then
echo exists
else echo"not exists"
fi

它使用shell builtin和程序echo status到stdout,而不使用任何内容到stderr。另一方面,如果找不到命令,它只将status返回到stderr。


如果为设置了posix_内置选项以进行测试,那么command-v可以正常工作,但如果没有,则可能失败。(它已经为我工作了多年,但最近遇到了一个不起作用的地方)。

我发现以下内容更具防故障性:

1
test -x $(which <command>)

因为它测试三件事:路径、存在和执行权限。


1
2
3
4
5
6
checkexists() {
    while [ -n"$1" ]; do
        [ -n"$(which"$1")" ] || echo"$1": command not found
        shift
    done
}


1
2
3
4
5
6
7
8
9
10
11
GIT=/usr/bin/git                     # STORE THE RELATIVE PATH
# GIT=$(which git)                   # USE THIS COMMAND TO SEARCH FOR THE RELATIVE PATH

if [[ ! -e $GIT ]]; then             # CHECK IF THE FILE EXISTS
    echo"PROGRAM DOES NOT EXIST."
    exit 1                           # EXIT THE PROGRAM IF IT DOES NOT
fi

# DO SOMETHING ...

exit 0                               # EXIT THE PROGRAM IF IT DOES


我必须检查是否安装了git作为部署CI服务器的一部分。我最后的bash脚本如下(ubuntu服务器):

1
2
3
if ! builtin type -p git &>/dev/null; then
  sudo apt-get -y install git-core
fi

希望这能帮助别人!


我找不到一个有效的解决方案,但在编辑了一点之后,我想出了这个办法。这对我很有用:

1
2
3
4
5
dpkg --get-selections | grep -q linux-headers-$(uname -r)

if [ $? -eq 1 ]; then
        apt-get install linux-headers-$(uname -r)
fi


我试着用--version--help调用程序,检查命令是否成功或失败。

如果找不到程序,将退出与set -e脚本一起使用,并将收到一条有意义的错误消息:

1
2
3
#!/bin/bash
set -e
git --version >> /dev/null

@lhunath的回答和解释真是太棒了。救了我的一天我把它延长了一点。无法控制自己分享它-希望它可能对某人有用。如果有人需要检查(一组)多个程序,这里是快速片段。

What it is doing? (1) Read array of programs. (2) Show message for
failed program. (3) Prompt user to continue (forcing loop) y/n options
for the validation of the rest of the programs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash

proginstalldir=/full/dir/path/of/installation
progsbindir=$proginstalldir/bin
echo -e"
My install directory - $proginstalldir"

echo -e"My binaries directory - $progsbindir"

VerifyInstall () {
clear
myprogs=( program1 program2 program3 program4 program5 programn );
echo -e"
Validation of my programs started...."

for ((i=0; i<${#myprogs[@]}; i++)) ; do
command -v $progsbindir/${myprogs[i]} >/dev/null && echo -e"Validating....\t${myprogs[i]}\tSUCCESSFUL"  || { echo -e"Validating.... \t${myprogs[i]}\tFAILED">&2;
while true; do
printf"%s: " "ERROR.... Validation FAILED for ${myprogs[i]} !!!! Continue?"; read yn;
case $yn in [Yy] )  echo -e"Please wait..." ; break;;
[Nn]) echo -e"

#################################
##   Validation Failed .. !!   ##
#################################

"
; exit 1; break;;
*) echo -e"
Please answer y or n then press Enter
"
; esac; done; >&2; }; done
sleep 2
}

VerifyInstall