How to Use the apt Command to List Installed Packages on Ubuntu

LinuxForDevices featured banner: Count Installed Packages Correctly

Listing installed packages on Ubuntu is one command: apt list –installed. The extra line it prints is not a package, and the quiet flag removes it from the count.

I ran every command on this page on Ubuntu 24.04 LTS with apt 2.8.3 and dpkg 1.22.6, and the terminal output below is copied from those runs. Ubuntu 26.04 LTS ships apt 3.2.0, whose manual documents the same list subcommand and its –installed, –upgradeable, and –all-versions options.

What one line of apt list –installed tells you

A single line from this command packs five fields that you can read one at a time.

apt list --installed output on Ubuntu showing the Listing line and package rows with installed and automatic markers
The start of apt list –installed output. The Listing line is progress, not a package.
apt list --installed | head -n 6
Listing...
acl/noble-updates,now 2.3.2-1build1.1 arm64 [installed]
adduser/noble,now 3.137ubuntu1 all [installed,automatic]
adwaita-icon-theme/noble,now 46.0-1 all [installed,automatic]
alsa-topology-conf/noble,now 1.2.5.1-2 all [installed,automatic]
alsa-ucm-conf/noble-updates,now 1.2.10-1ubuntu5.14 all [installed,automatic]

Take the second line apart and the format stops looking arbitrary.

Part of the lineValueWhat it tells you
Package nameaclThe name you pass to apt and dpkg
Archivenoble-updatesWhich configured repository the installed version came from
Selection statenowThe version currently present on the system
Version2.3.2-1build1.1The exact build installed
Architecturearm64The build target, or all for architecture independent packages
Marker[installed]How the package got there

The marker is the field that changes meaning most often, because four different values appear in a single run.

MarkerMeaning
[installed]You asked for this package by name
[installed,automatic]It arrived as a dependency of something else
[installed,local]It was installed from a file rather than from a configured repository
[installed,upgradable to: 2.39-0ubuntu8.9]A newer version is waiting in the repositories

A line marked local did not come from a repository, so APT cannot upgrade it, and the DEB file install walkthrough covers how those packages get onto the system in the first place.

List every installed package on Ubuntu

The command that lists installed packages on Ubuntu is apt list with the –installed option.

apt list --installed

That output covers everything in the dpkg database, including the libraries you never asked for. The distinction between what you chose and what was pulled in matters later, when you decide what is safe to remove.

CommandWhat it prints
apt list –installedOne line per installed package, including automatic dependencies
apt listEvery package in your configured repositories, installed or not
apt list –upgradeableOnly the installed packages that have a newer version available

The upgradeable view is the one to check before you plan maintenance, because it shows the current version and the version waiting for you on the same line.

apt list --upgradeable | head -n 4
Listing...
base-files/noble-updates 13ubuntu10.5 arm64 [upgradable from: 13ubuntu10.4]
libc-bin/noble-security 2.39-0ubuntu8.9 arm64 [upgradable from: 2.39-0ubuntu8.8]
libc-dev-bin/noble-security 2.39-0ubuntu8.9 arm64 [upgradable from: 2.39-0ubuntu8.8]

Count the installed packages without the off-by-one

Piping the listing into wc is the obvious way to count packages, and the obvious way to get a number that is one too high.

Two apt list counts in a terminal showing 1300 from the plain command and 1299 from the quiet command
The plain command counts the Listing line as a package. Adding -qq removes it.
apt list --installed | wc -l
apt -qq list --installed | wc -l
1300
1299

APT writes the Listing line into the same stream as the packages, so redirecting standard error does not remove it and only the quiet flag does. The wc command guide covers the other counters if you need characters or words instead of lines.

Two independent commands agree on the corrected total, which is how you know 1299 is the count of records and 1300 is the count of lines.

dpkg-query -l | grep -c '^ii'
apt-mark showmanual | wc -l
apt-mark showauto | wc -l
1299
134
1165

The two apt-mark totals add up to the same number, which is a useful check in its own right. Packages installed by hand plus packages installed as dependencies should always equal the installed total, and a mismatch means something was removed without cleaning up.

CommandWhat it countsResult
apt list –installed | wc -lEvery output line, including the progress line1300
apt -qq list –installed | wc -lInstalled packages only1299
dpkg-query -l | grep -c ‘^ii’Records dpkg marks as installed1299
apt-mark showmanual | wc -lPackages you installed by name134
apt-mark showauto | wc -lPackages installed as dependencies1165

Check one specific package or a package family

When you already know the name, pass it to the command and skip the filtering layer entirely.

apt list --installed bash returning a single package line on Ubuntu
A named argument returns one line when the package is installed.
apt list --installed bash
Listing...
bash/noble,now 5.2.21-2ubuntu4 arm64 [installed]

The argument accepts globs as well, which is how you check a family of related packages in one call. Quote the glob so that your shell passes it through instead of expanding it against the current directory.

apt -qq list --installed "libc*" | head -n 4
libc-bin/noble-updates,now 2.39-0ubuntu8.8 arm64 [installed,upgradable to: 2.39-0ubuntu8.9]
libc-dev-bin/noble-updates,now 2.39-0ubuntu8.8 arm64 [installed,upgradable to: 2.39-0ubuntu8.9]
libc-devtools/noble-updates,now 2.39-0ubuntu8.8 arm64 [installed,upgradable to: 2.39-0ubuntu8.9]
libc6-dev/noble-updates,now 2.39-0ubuntu8.8 arm64 [installed,upgradable to: 2.39-0ubuntu8.9]

Filtering with grep works too, and it behaves differently from a named argument in a way that catches people out. A substring match returns every package whose name contains the text, in alphabetical order rather than by relevance.

apt list --installed 2>/dev/null | grep bash
bash-completion/noble,now 1:2.11-8 all [installed,automatic]
bash/noble,now 5.2.21-2ubuntu4 arm64 [installed]

Anchoring the match on the name field and the slash that follows it removes the near misses, which is the same technique the grep command guide uses for exact matches in other files.

apt list --installed 2>/dev/null | grep "^bash/"
apt list --installed 2>/dev/null | grep -c "^libc"
bash/noble,now 5.2.21-2ubuntu4 arm64 [installed]
42

One boundary in this section decides whether a script works. A name that does not exist produces no error and no package line, and the command still reports success, so you cannot branch on its exit status.

apt list --installed nosuchpkg-xyz
echo $?
Listing...
0

dpkg-query exits with a status you can test instead, which makes it the right tool for the question of whether a package exists. The apt and dpkg comparison explains why the two tools read the same data yet behave this differently.

dpkg-query -W -f='${binary:Package}\t${Version}\t${db:Status-Abbrev}\n' bash
dpkg-query -W -f='${binary:Package}\n' nosuchpkg-xyz
echo "exit=$?"
bash	5.2.21-2ubuntu4	ii
dpkg-query: no packages found matching nosuchpkg-xyz
exit=1

The status abbreviation ii in that output is short enough to read at a glance once you know the two letters. The first letter is the requested state and the second is the current state, so ii means you asked for the package and it is installed.

Save the installed package list to a file

A file is what you compare when you move to a new machine or rebuild one, and the quiet flag matters more here than anywhere else because the progress line would be written into the file as a package name.

apt -qq list --installed | cut -d/ -f1 | sort -u > installed-packages.txt
wc -l installed-packages.txt
1299 installed-packages.txt

The cut command keeps the text before the slash, which is the package name on its own, and sort removes any duplicate that a repository change could introduce.

dpkg-query is the cleaner source when you want versions in the file, because it prints exactly what its format string asks for and nothing else. The status code with the trailing space is dropped here on purpose, so the output is a list of names and versions only.

dpkg-query -W -f='${binary:Package} ${Version}\n' > installed-with-versions.txt
head -n 3 installed-with-versions.txt
acl 2.3.2-1build1.1
adduser 3.137ubuntu1
adwaita-icon-theme 46.0-1

That second file holds one more line than the first, and the next section of this page explains where the extra record comes from.

See which packages were installed recently

Neither command tells you when a package arrived, because the listing carries no dates. dpkg records every transaction in a log, and one grep pulls the installations out of it.

grep " install " /var/log/dpkg.log | tail -n 4
2026-09-11 04:10:19 install figlet:arm64 <none> 2.2.5-3
2026-09-11 04:10:22 install myapp-okdep:arm64 <none> 1.0.0
2026-09-11 04:10:26 install figlet:arm64 <none> 2.2.5-3
2026-09-11 04:10:37 install myapp:arm64 <none> 1.0.0

Each line is fixed in the same order, and reading the fields left to right answers the whole question without any further work.

  1. The date and time place the install in sequence with everything else on the machine.
  2. The action word tells you whether this was an install, an upgrade, or a removal.
  3. The package name matches the entry you are asking about.
  4. The last two fields are the previous version and the new one, and the placeholder before the first install shows what was there before.

The apt history log answers a slightly different question, because it records the command line you ran rather than the unpack operations that followed it. That is the file to read when you want to know what you asked for instead of what the package manager did with it.

grep "Commandline" /var/log/apt/history.log | tail -n 3
Commandline: apt install ./myapp_1.0.0_arm64.deb -y
Commandline: apt remove -y myapp
Commandline: apt remove -y myapp

Both files are readable without sudo at their default permissions. Rotated copies sit beside them with a .1 or .2.gz suffix, so a search across a longer period means reading those as well.

Why dpkg counts one more package than apt

The dpkg database keeps a record of a package after the package itself is gone, and that leftover is the difference between the two totals you saw earlier.

dpkg-query printing a package count that is one higher than the apt count
dpkg-query reads the status database and includes records APT leaves out.
dpkg-query -W -f='${Package}\n' | wc -l
1300

Filtering out the records in the fully installed state leaves the package responsible, and there is exactly one on this machine.

dpkg -l showing a package with the rc status code, meaning removed with configuration files left behind
The rc status code marks a package that was removed while its configuration files stayed on disk.
dpkg-query -W -f='${binary:Package} ${db:Status-Abbrev}\n' | grep -v ' ii '
dpkg -l legcord
legcord rc
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version      Architecture Description
+++-==============-============-============-=================================
rc  legcord        1.3.0        arm64

dpkg prints its own legend above the rows, and the two letters map onto it directly. The r means the removal was requested and the c means configuration files are still on disk, which is a state APT does not show you in its installed list.

Status codeRequested stateCurrent stateWhere you see it
iiinstallinstalledIn apt list –installed and dpkg-query -l
rcremoveconfig files remainOnly in dpkg-query -W and dpkg -l

Snap adds a third count on top of those two, because snap packages are stored and tracked somewhere else entirely. Asking apt about them returns nothing, and the snap package introduction covers the separate command they need.

snap list | tail -n +2 | wc -l
17

Cleaning up a leftover record means removing the package with purge rather than remove, and the APT removal walkthrough shows both forms side by side. The unused package cleanup guide covers the related job of finding dependencies nothing needs any more.

Boundaries and failures worth knowing

Two commands in the same family fail in different ways, and both failures are quick to recognise once you have seen them.

apt-get list
E: Invalid operation list

apt-get is the older front end and it has no list verb, so the command fails before it reads anything. The apt and apt-get comparison covers which options survive the move between them.

The second boundary is the warning APT prints every time you use this command. It is telling you that its command line output is not a stable interface, and the manual page repeats the point by recommending apt-get and apt-cache inside scripts.

SituationWhat happensWhat to use instead
You need to parse the result in a scriptAPT warns that its output can change between versionsdpkg-query -W with an explicit format string
The package does not existapt list exits 0 with no package linedpkg-query, which exits 1
You want every version, not just the installed oneapt list –installed hides the alternativesapt list –all-versions, or the -a short form
A package has no repository behind itThe line is marked local and APT cannot upgrade itReinstall from an updated file, or move it to a repository

The all-versions option is worth one demonstration, because it makes the difference between what is installed and what is available visible on a single line of output.

apt list --all-versions sudo
Listing...
sudo/noble-updates,noble-security,now 1.9.15p5-3ubuntu5.24.04.2 arm64 [installed,automatic]
sudo/noble 1.9.15p5-3ubuntu5 arm64

Only the first line carries the now marker and the installed tag, so that is the version running on the machine. The second line is the version in the base repository, which is older and would only be used if you pinned it deliberately.

One failure outside this command shows up when you run several package operations at once. A second APT process reports that it cannot acquire the dpkg frontend lock, and the dpkg frontend lock fix explains how to find the process holding it.

Frequently asked questions

What is the command to list installed packages on Ubuntu?

apt list –installed prints one line for every installed package. Add -qq to remove the progress line when the output feeds a count, a file, or a script.

Why does apt list –installed | wc -l return one more line than the package count?

APT writes a line that reads Listing before the package rows, and it goes to standard output rather than standard error. wc counts that line as a package, so the quiet flag is what removes it.

How do I check whether a single package is installed?

Pass the package name to apt list –installed and read the marker at the end of the line. Use dpkg-query when a script needs to branch on the result, because apt list reports success even when the package does not exist.

Why does dpkg report more packages than apt?

The dpkg database keeps a record for a package whose files were removed while its configuration stayed on disk. Those rows carry the rc status code, they appear in dpkg-query -W and dpkg -l, and APT leaves them out of its installed list.

Start with apt list –installed to see what is on the machine, switch to the quiet form whenever the answer has to be a number, and move to dpkg-query the moment the answer has to go into a script.