如何检查已安装的软件包并在未安装时进行安装?

297

我正在使用Ubuntu系统,并且目前正在进行以下操作:

if ! which command > /dev/null; then
   echo -e "Command not found! Install? (y/n) \c"
   read
   if "$REPLY" = "y"; then
      sudo apt-get install command
   fi
fi

这是大多数人会做的吗?还是有更优雅的解决方案?


9
命令名称并不总是反映它们所属的软件包名称。你的大目标是什么?为什么不尝试安装它,最坏的情况下它也不会出现问题,因为它已经安装了。 - viam0Zah
13
幸运的是,apt-get install 是幂等的,因此只需运行它而不必担心它是否已安装,这样是安全的。 - David Baucum
3
相关的,你应该使用 command -v <command> 而不是 which <command>。另请参见如何在Bash脚本中检查程序是否存在 - jww
7
不完全正确;在已安装的软件包上运行 install 命令将会升级它,如果它是过时的。通常为了安全问题保持最新状态是可取的,但如果想要稳定性则可能会令人惊讶。 - Izkata
另外值得注意的是:在现有软件包上运行安装所需的时间比检查是否已安装要长得多,例如,检查已安装软件包列表中的条目。这需要更长的时间,不是一个实际可行的解决方案。 - Peter Kionga-Kamau
1
@DavidBaucum:手动apt install已经安装的软件包也会将其标记为手动安装,如果该软件包以前被设置为自动安装,则会产生非常不希望的副作用。 - MestreLion
30个回答

367

要检查是否安装了packagename,请输入:

dpkg -s <packagename>

你也可以使用dpkg-query来实现你的目的,它有更整洁的输出,并且也支持通配符。

dpkg-query -l <packagename>

要查找拥有command命令的软件包,请尝试:

dpkg -S `which <command>`

更多细节请参见文章在Linux中查找软件包是否已安装dpkg速查表


53
如果您个人希望非程序化实现此目标,可以直接使用这些信息。但是,在编写脚本时,不能仅仅依赖返回代码或仅仅依赖输出/缺少输出。您需要扫描这些命令的输出,这会限制它们在此问题中的有用性。 - UpAndAdam
6
有趣的是,最近我发现 dpkg-query 在缺失软件包时曾返回 1,但现在(Ubuntu 12.04)返回 0,导致我的 Jenkins 构建节点安装脚本出现了各种问题!dpkg -s 命令在软件包已安装时返回 0,在未安装时返回 1。 - Therealstubot
26
楼上的用户要求了解“if”的用法,我也在寻找“if”的用法。 - Tomáš Zato
2
@Therealstubot:我也在使用Ubuntu 12.04,dpkg -s 在缺少软件包时返回1,在有软件包时返回0,这是应该的。在早期(或最近)版本中有什么不同吗? - MestreLion
8
注意:dpkg -s 命令会在一个软件包被安装并移除后返回零,此时会显示 Status: deinstall ok config-files 或类似的结果,因此这是“正常”的情况,对我来说,这不是一个安全的测试方式。在这种情况下,dpkg-query -l 命令似乎也无法返回有用的结果。 - keen
显示剩余4条评论

121

更明确地说,以下是一个Bash脚本的示例,用于检查软件包是否存在,并在必要时安装它。当然,您也可以在发现软件包不存在时执行其他操作,比如简单地退出并返回错误代码。

REQUIRED_PKG="some-package"
PKG_OK=$(dpkg-query -W --showformat='${Status}\n' $REQUIRED_PKG|grep "install ok installed")
echo Checking for $REQUIRED_PKG: $PKG_OK
if [ "" = "$PKG_OK" ]; then
  echo "No $REQUIRED_PKG. Setting up $REQUIRED_PKG."
  sudo apt-get --yes install $REQUIRED_PKG
fi

如果脚本在GUI中运行(例如,它是一个Nautilus脚本),您可能希望将'sudo'调用替换为'gksudo'调用。


8
“--force-yes”的做法似乎不太好。从man页面中可以看到:“这是一个危险的选项,如果apt-get正在执行可能会造成损害的操作,它将继续进行而不提示。除非在非常特殊的情况下,否则不应该使用。使用--force-yes可能会潜在地破坏您的系统!”在脚本中使用它甚至更糟。 - reducing activity
${Status} 是什么意思? - ArjunSahlot
1
这并没有考虑到软件包可能根本不存在的情况。 - Peter Kionga-Kamau
@Urhixidur 一个完整的算法应该考虑到不太可能发生的情况。一个好的答案不应该对目的做出假设。依赖于apt-get的错误信息的问题在于我们仍然没有一种机制告诉调用脚本包不存在,只有一个带有stdout错误的停止。此外,这种方法比检查包是否存在要慢得多(需要读取包列表、构建依赖树、读取状态信息才能确定未找到包),因此性能可能成为一个问题(特别是对于涉及许多潜在未知包的自动化)。 - Peter Kionga-Kamau
显示剩余2条评论

106

这个一行代码会返回1(已安装)或0(未安装)关于“nano”软件包的状态...

$(dpkg-query -W -f='${Status}' nano 2>/dev/null | grep -c "ok installed")

即使该软件包不存在或不可用,下面的示例将安装“nano”软件包(如果尚未安装)...

if [ $(dpkg-query -W -f='${Status}' nano 2>/dev/null | grep -c "ok installed") -eq 0 ];
then
  apt-get install nano;
fi

4
我的翻译版本是:对此进行我的变化:dpkg-query -W -f='${Status}' MYPACKAGE | grep -q -P '^install ok installed$'; echo $? - ThorSummoner
@ThorSummoner:能否解释一下你的更好的原因? - knocte
1
@knocte,我不确定是否有关于客观上更好的争论。虽然我相信逐字帖子的一行代码将执行结果输出,但我不想在答案中留下悬而未决的情况。我展示的这个一行代码的例子是获取(打印)退出代码。 - ThorSummoner
1
@ThorSummoner,像这样简单的正则表达式不需要使用 grep -P - tripleee
14
简化版:如果 dpkg-query -W -f='${Status}' nano 的输出中不包含 "ok installed",那么执行 apt install nano。不需要使用 grep -c,只需使用 grep 的退出状态。 - Stephen Ostermiller
显示剩余3条评论

36

dpkg-query --showformat='${db:Status-Status}'

这将产生一个小的输出字符串,不太可能改变,并且很容易进行确定性比较而不需要使用 grep

pkg=hello
status="$(dpkg-query -W --showformat='${db:Status-Status}' "$pkg" 2>&1)"
if [ ! $? = 0 ] || [ ! "$status" = installed ]; then
  sudo apt install $pkg
fi

需要进行$? = 0检查,因为如果您以前从未安装过软件包,并且在删除某些软件包(如hello)后,dpkg-query将以状态1退出并输出到stderr:

dpkg-query: no packages found matching hello

不要输出not-installed。当错误消息出现时,2>&1也会捕获它,并防止其进入终端。

对于多个软件包:

pkgs='hello certbot'
install=false
for pkg in $pkgs; do
  status="$(dpkg-query -W --showformat='${db:Status-Status}' "$pkg" 2>&1)"
  if [ ! $? = 0 ] || [ ! "$status" = installed ]; then
    install=true
    break
  fi
done
if "$install"; then
  sudo apt install $pkgs
fi

可能的状态在man dpkg-query中有记录,包括:
   n = Not-installed
   c = Config-files
   H = Half-installed
   U = Unpacked
   F = Half-configured
   W = Triggers-awaiting
   t = Triggers-pending
   i = Installed

单个字母版本可通过db:Status-Abbrev获得,但它们会与操作和错误状态一起出现,因此您会得到3个字符,需要将其剪切。

因此,我认为可以信赖未大写的状态(Config-files vs config-files)不会改变。

dpkg -s退出状态

不幸的是,这并不是大多数用户想要的:

pkgs='qemu-user pandoc'
if ! dpkg -s $pkgs >/dev/null 2>&1; then
  sudo apt-get install $pkgs
fi

对于某些软件包,例如certbot,执行以下操作:

sudo apt install certbot
sudo apt remove certbot

certbot 以“config-files”状态离开,这意味着配置文件仍留在计算机中。在该状态下,dpkg -s 仍然返回 0,因为软件包元数据仍被保存,以便更好地处理这些配置文件。

要使 dpkg -s 实际返回所需的值 1,则需要使用 --purge

sudo apt remove --purge certbot

这实际上将其移动到not-installed/dpkg-query: no packages found matching

请注意,只有某些软件包会留下配置文件。像hello这样的简单软件包直接从installed转移到not-installed,无需使用--purge

在Ubuntu 20.10上测试通过。

Python apt 软件包

Ubuntu 18.04中有一个预安装的Python 3软件包,名为apt,它提供了Python apt接口!

可以查看一个检查软件包是否安装并在未安装时安装它的脚本:How to install a package using the python-apt API

以下是参考副本:

#!/usr/bin/env python
# aptinstall.py

import apt
import sys

pkg_name = "libjs-yui-doc"

cache = apt.cache.Cache()
cache.update()
cache.open()

pkg = cache[pkg_name]
if pkg.is_installed:
    print "{pkg_name} already installed".format(pkg_name=pkg_name)
else:
    pkg.mark_install()

    try:
        cache.commit()
    except Exception, arg:
        print >> sys.stderr, "Sorry, package installation failed [{err}]".format(err=str(arg))

检查可执行文件是否在PATH

参见:如何从Bash脚本中检查程序是否存在?

另请参阅


3
Ciro,你不能依赖于“dpkg -s”的退出代码。例如,如果您“apt安装”了一个软件包,然后将其“apt删除”,并尝试“dpkg -s packagename”,那么您将注意到状态:deinstall和退出代码为零(就像已安装一样)。你必须解析“dpkg -s”的输出。 - Dmitry Shevkoplyas
1
@DmitryShevkoplyas 感谢您的报告。我无法在Ubuntu 19.10上重现此问题,使用以下命令:sudo apt install hello; dpkg -s hello; echo $?; sudo apt remove hello; dpkg -s hello; echo $?。您能否提供更多细节? - Ciro Santilli OurBigBook.com
2
确实,对于您的测试用例"hello"包,"dpkg -s"正确显示该包未安装,并给出了预期的退出代码"1"。但是尝试使用"certbot"包进行相同的安装/删除检查,然后您将在"apt remove certbot"之后看到"Status: deinstall ok config-files"作为"dpkg -s"输出,而退出代码错误地显示为"0"。我的错误假设是它对于任何其他软件包都是精确的情况,但似乎并非所有软件包都是如此,这甚至更糟糕和不可预测。必须解析"dpkg -s"(版权所有)Yoda :) - Dmitry Shevkoplyas
2
大多数答案都没有提到dpkg remove package-name不会导致dpkg -s package-name返回0的信息,这几乎让我发疯了。实际上,它的状态是已卸载而不仅仅是未安装!哎呀,Debian 真是...特别。 - TNT

12
Ubuntu加入了“个人软件包档案”(PPA),而PPA软件包具有不同的结果。
  1. A native Debian repository package is not installed:

    ~$ dpkg-query -l apache-perl
    ~$ echo $?
    1
    
  2. A PPA package registered on the host and installed:

    ~$ dpkg-query -l libreoffice
    ~$ echo $?
    0
    
  3. A PPA package registered on the host, but not installed:

    ~$ dpkg-query -l domy-ce
    ~$ echo $?
    0
    ~$ sudo apt-get remove domy-ce
    [sudo] password for user:
    Reading package lists... Done
    Building dependency tree
    Reading state information... Done
    Package domy-ce is not installed, so not removed
    0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
    

同时发布在:测试是否已安装APT软件包


4
如果您安装或卸载一个软件包,接下来使用 dpkg-query package ; echo $? 命令,即使该软件包未被安装,返回值也将为0。 - Pol Hallen

8

UpAndAdam写道:

但是在脚本中,您不能仅仅依赖于返回代码。

根据我的经验,您可以依赖于dpkg的退出代码。

dpkg -s的返回代码为0表示软件包已安装,为1表示未安装,因此我发现最简单的解决方案是:

dpkg -s <pkg-name> 2>/dev/null >/dev/null || sudo apt-get -y install <pkg-name>

对我来说它工作得很好...



13
在运行apt-get remove <package>之后,即使软件包已被“卸载”,运行dpkg -s <package>仍然会返回0。 - ThorSummoner
@ThorSummoner:结论是什么? - Peter Mortensen
非常简短明了,解释得很好。感谢rocka84。@ThorSummoner,你能澄清一下你的观点吗? - Kasir Barati
@KasirBarati,未安装和已卸载软件包的返回代码不一致-在这两种情况下,软件都丢失了,但在一半的情况下,返回代码指示软件丢失。我想是这样吧?我已经有一段时间没有看过这个了。 - ThorSummoner

7

我选择了基于Nultyi的答案的一个方案:

MISSING=$(dpkg --get-selections $PACKAGES 2>&1 | grep -v 'install$' | awk '{ print $6 }')
# Optional check here to skip bothering with apt-get if $MISSING is empty
sudo apt-get install $MISSING

基本上,dpkg --get-selections 的错误信息比大多数其他信息更容易解析,因为它不包括像“deinstall”这样的状态。此外,它还可以同时检查多个软件包,而仅使用错误代码无法实现。

说明/示例:

$ dpkg --get-selections  python3-venv python3-dev screen build-essential jq
dpkg: no packages found matching python3-venv
dpkg: no packages found matching python3-dev
screen                                          install
build-essential                                 install
dpkg: no packages found matching jq

因此,grep从列表中删除已安装的软件包,而awk从错误消息中提取软件包名称,导致MISSING = 'python3-venv python3-dev jq',可以轻松地插入到安装命令中。

我不是盲目地发出apt-get install $PACKAGES,因为如评论中所述,这可能会意外升级您没有计划的软件包。对于预期稳定的自动化过程来说,这不是一个好主意。


我喜欢这个解决方案。简洁明了且可以同时测试多个软件包。而且,您可以将可选检查设置得像[[ ! -z $MISSING ]] && sudo apt-get install $MISSING这么简单。 - Shenk
请注意,这将会忽略已安装并已删除的软件包,因为输出以“deinstall”结尾。建议更改grep参数为 grep -v ' install$' (注意前导空格)。 - RobM

6
这样就可以了。 apt-get --no-upgrade install 是幂等的。
sudo apt-get install --no-upgrade command

10
有时候,当一个包已经安装了,执行apt-get install命令虽然本身是幂等的,但是这样做可能是不合适的。在我的场景中,我正在使用Ansible的原始模块在远程系统上安装一个包,如果我不加判断地运行apt-get install,它每次都会报告系统已修改。通过添加条件语句可以解决这个问题。 - JBentley
3
@JBentley说得很对。作为依赖安装的软件包将被标记为手动安装,如果您使用apt-get安装它,则在删除其依赖项时,不会被移除。请注意,此翻译仅供参考,不得用于正式场合。 - David Baucum
我认为如果你想要幂等行为,应该使用apt-get install --no-upgrade package-name而不是普通的install,因为如果有可用的升级,install会升级包。 - Mikko Rantalainen
不错的点子 @MikkoRantalainen - David Baucum

6

看起来这个相当有效。

$ sudo dpkg-query -l | grep <some_package_name> | wc -l
  • 如果未安装,它会返回0,如果已安装,则返回> 0的某个数字。

10
grep | wc -l 是一个反模式。如果要检查某个东西是否存在,只需使用 grep -q 即可。如果要计算出现次数(这在大多数情况下并不常用),请使用 grep -c - tripleee
@tripleee 那么,dpkg -s zip | grep -c "Package: zip"?(以zip作为示例包) - David Tabernero M.
@Davdriver 这并不完全是上面所做的,但是没错。在脚本中,您可能希望使用 grep -q 'Package: zip' 返回一个退出代码,该代码指示是否找到结果而无需打印任何内容。 - tripleee
这似乎对于未安装的软件包也能正常工作。 - mehmet

5

Chris回答的启发:

#! /bin/bash

installed() {
    return $(dpkg-query -W -f '${Status}\n' "${1}" 2>&1|awk '/ok installed/{print 0;exit}{print 1}')
}

pkgs=(libgl1-mesa-dev xorg-dev vulkan-tools libvulkan-dev vulkan-validationlayers-dev spirv-tools)
missing_pkgs=""

for pkg in ${pkgs[@]}; do
    if ! $(installed $pkg) ; then
        missing_pkgs+=" $pkg"
    fi
done

if [ ! -z "$missing_pkgs" ]; then
    cmd="sudo apt install -y $missing_pkgs"
    echo $cmd
fi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接