Listing what is installed

What is on the machine, and when it arrived

Updated 2026-08-26

Several commands answer "what is installed", and they disagree, because they are answering slightly different questions. One of them counts packages that were removed months ago. One of them is a table that truncates itself to your terminal width. Which one you want depends on whether a person or a script is going to read the answer.

The reliable listing

dpkg-query -W with an explicit format is the one to build anything on. You name the fields, so nothing about the output depends on terminal width, locale or dpkg's mood about columns:

dpkg-query -W -f='${Package}\n' | head -8
adduser
apt
apt-utils
base-files
base-passwd
bash
bash-completion
bsdextrautils

Add fields as needed. ${Version} is the usual second one:

dpkg-query -W -f='${Package} ${Version}\n' | head -5
adduser 3.152
apt 3.0.3
apt-utils 3.0.3
base-files 13.8+deb13u6
base-passwd 3.6.7

Use ${Package} rather than ${binary:Package}. The second appends an architecture qualifier to any package that could be installed for more than one architecture at a time, so roughly half your list comes back as libc6:amd64 while the rest stays bare. That is correct behaviour but it's hardly ever what you want in a script.

dpkg-query lists more than is installed

The listing above includes bash-completion, which is not installed on this machine. dpkg -W reports every package dpkg has a record of, and a package that was removed without being purged keeps its record along with its configuration files. Filter on the status field to count only what is really there:

echo "known:     $(dpkg-query -W -f='x\n' | wc -l)"
echo "installed: $(dpkg-query -W -f='${db:Status-Abbrev}\n' | grep -c '^ii')"
known:     191
installed: 190

The gap is packages in the rc state, and on a machine a few years old it is usually larger than one:

dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' | grep -v '^ii'
rc  bash-completion

remove vs purge vs autoremove covers how packages end up there and how to finish the job.

The two commands people try first

dpkg -l and apt list --installed both work and neither is meant for a pipe.

dpkg -l                 # a table, truncated to your terminal width
apt list --installed    # one line per package, plus a warning when piped

dpkg -l cuts the description column to fit the terminal, so the same command gives different output in a narrow window, and its columns are not a stable interface between dpkg versions. apt list says as much itself: if you pipe it anywhere, it will print a warning that its command-line interface is not stable. Both are fine to read in a terminal, but shouldn't be parsed in a script.

Narrowed to a package or a pattern, apt list shows one thing dpkg's database does not hold at all. The bracket at the end of each line says whether the package was asked for by name or arrived as a dependency:

apt list --installed 'cowsay*' 'perl-modules*' 2>/dev/null | sort
Listing...
cowsay-off/stable,now 3.03+dfsg2-8 all [installed]
cowsay/stable,now 3.03+dfsg2-8 all [installed]
perl-modules-5.40/stable,now 5.40.1-6 all [installed,automatic]

automatic is apt's own bookkeeping, kept in /var/lib/apt/extended_states and not in dpkg's database. dpkg-query has no field that will report it.

What you asked for, and what came along

The more useful question is usually not "what is installed" but "what did I install", and apt records the difference as one bit per package. apt-mark showmanual lists the packages someone asked for by name; showauto lists the ones that arrived as dependencies.

echo "manual:    $(apt-mark showmanual | wc -l)"
echo "automatic: $(apt-mark showauto | wc -l)"
manual:    34
automatic: 156

That first list is the interesting one. It is short enough to read, it is what actually distinguishes this machine from a fresh install, and it is what you would want if you were rebuilding the machine:

apt-mark showmanual | head -6
apt-utils
bzip2
ca-certificates
coreutils
cowsay
cowsay-off

It is also what autoremove consults, so a package appearing here that you never asked for is worth investigating rather than ignoring.

What is taking up room

${Installed-Size} is in kilobytes and comes from the package's own record, so this needs no disk access at all:

dpkg-query -W -f='${Installed-Size}\t${Package}\n' cowsay perl-modules-5.40
92	cowsay
19988	perl-modules-5.40

Sort the whole list to find the packages worth deleting:

dpkg-query -W -f='${Installed-Size}\t${Package}\n' | sort -rn | head -20

Expect the top of that list to be compiled libraries, and expect it to differ between machines of different architectures even where the package set is identical, because the same source builds to different sizes.

This is the archive's estimate of unpacked size rather than a measurement, so it will not match du on the files. It is the right tool for "what is big", and the wrong one for "where has my disk gone".

When it arrived

dpkg writes a line to /var/log/dpkg.log for every state change it makes, timestamped:

grep ' cowsay-off:' /var/log/dpkg.log | head -3
2026-08-26 07:54:44 install cowsay-off:all <none> 3.03+dfsg2-8
2026-08-26 07:54:44 status half-installed cowsay-off:all 3.03+dfsg2-8
2026-08-26 07:54:44 status unpacked cowsay-off:all 3.03+dfsg2-8

The install line is the one that answers the question; the status lines are dpkg narrating its own progress through unpack and configure.

There is a second answer that survives log rotation. dpkg writes a file list for every installed package under /var/lib/dpkg/info/, and never touches it again, so its modification time is when the package was configured:

ls -l --time-style=long-iso /var/lib/dpkg/info/cowsay-off.list
-rw-r--r-- 1 root root 370 2026-08-26 07:54 /var/lib/dpkg/info/cowsay-off.list

Sorting that directory by time (ls -t /var/lib/dpkg/info/*.list) gives you the machine's install history in order.

Which command did it

/var/log/apt/history.log records apt's side: what was typed, who typed it, and when.

grep -E '^(Start-Date|Commandline)' /var/log/apt/history.log | tail -2
Start-Date: 2026-08-26  07:54:48
Commandline: apt-get remove -y bash-completion

Each stanza in that file also carries Install:, Remove: or Upgrade: lines naming every package the command touched, with its version.

Debian rotates that file, so anything older than a few weeks is in history.log.1.gz and its numbered siblings. zgrep reads them without unpacking anything:

zgrep '^Commandline:' /var/log/apt/history.log.*.gz
/var/log/apt/history.log.1.gz:Commandline: apt install nginx
/var/log/apt/history.log.1.gz:Commandline: apt upgrade
/var/log/apt/history.log.2.gz:Commandline: apt install cowsay
/var/log/apt/history.log.2.gz:Commandline: apt purge telnet

That is the fastest way to answer "when did this machine get nginx, and who decided that". Note that it only covers apt: a package installed by hand with dpkg -i never touches this file, though it still appears in /var/log/dpkg.log.

Copying the list to another machine

The advice you will find most often is dpkg --get-selections > packages.txt on one machine and dpkg --set-selections < packages.txt followed by apt-get dselect-upgrade on the other. It works, and it copies all 190 packages including every dependency, which means the new machine gets a frozen dependency set rather than one apt is free to resolve.

apt-mark showmanual is usually the better source:

apt-mark showmanual > wanted.txt              # on the old machine
xargs -a wanted.txt sudo apt install          # on the new one

Thirty-odd names instead of two hundred, and apt works out the dependencies for the release you are installing onto rather than the one you left behind. See xargs for what that second line is doing.

Go deeper

  • dpkg for the query commands in full, including -L and -S for files.
  • apt-cache for the same questions asked of the archive rather than of the machine.
  • APT essentials for how packages arrive in the first place.