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 | 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 line | Value | What it tells you |
|---|---|---|
| Package name | acl | The name you pass to apt and dpkg |
| Archive | noble-updates | Which configured repository the installed version came from |
| Selection state | now | The version currently present on the system |
| Version | 2.3.2-1build1.1 | The exact build installed |
| Architecture | arm64 | The 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.
| Marker | Meaning |
|---|---|
| [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.
| Command | What it prints |
|---|---|
| apt list –installed | One line per installed package, including automatic dependencies |
| apt list | Every package in your configured repositories, installed or not |
| apt list –upgradeable | Only 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.

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.
| Command | What it counts | Result |
|---|---|---|
| apt list –installed | wc -l | Every output line, including the progress line | 1300 |
| apt -qq list –installed | wc -l | Installed packages only | 1299 |
| dpkg-query -l | grep -c ‘^ii’ | Records dpkg marks as installed | 1299 |
| apt-mark showmanual | wc -l | Packages you installed by name | 134 |
| apt-mark showauto | wc -l | Packages installed as dependencies | 1165 |
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
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.
- The date and time place the install in sequence with everything else on the machine.
- The action word tells you whether this was an install, an upgrade, or a removal.
- The package name matches the entry you are asking about.
- 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 -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-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 code | Requested state | Current state | Where you see it |
|---|---|---|---|
| ii | install | installed | In apt list –installed and dpkg-query -l |
| rc | remove | config files remain | Only 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.
| Situation | What happens | What to use instead |
|---|---|---|
| You need to parse the result in a script | APT warns that its output can change between versions | dpkg-query -W with an explicit format string |
| The package does not exist | apt list exits 0 with no package line | dpkg-query, which exits 1 |
| You want every version, not just the installed one | apt list –installed hides the alternatives | apt list –all-versions, or the -a short form |
| A package has no repository behind it | The line is marked local and APT cannot upgrade it | Reinstall 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.
