I update Ubuntu with a very simple script I call apt-update that looks like this:

$ cat ./apt-update 
sudo apt-get update; sudo apt-get dist-upgrade; sudo apt-get autoremove

Nothing too crazy there. It updates the apt-get cache, performs the upgrade, and then removes all the residual junk that’s laying around. Well, almost all. If you do this enough, eventually you’ll see the following (assuming you’ve got the default motd Ubuntu script running and you’re logging in from a terminal):

=> /boot is using 86.3% of 227MB

This is because that script I mentioned doesn’t consider old kernel images to be junk. However, unless you’ve got an abnormal /boot partition, it doesn’t take too many old images to fill it up.

A quick Google search found Ubuntu Cleanup: How to Remove All Unused Linux Kernel Headers, Images and Modules. The solution on the page had exactly what I’m looking for, however, I couldn’t take it at face value. While the article offers an adequate solution, it doesn’t offer much explanation. The remainder of this article explains the details for this one-liner noted in the article above:

$ dpkg -l 'linux-*' | sed '/^ii/!d;/'"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"'/d;s/^[^ ]* [^ ]* \([^ ]*\).*/\1/;/[0-9]/!d' | xargs sudo apt-get -y purge

Important Note: Only run this if you’ve rebooted after installing a new kernel.

Ick. Let’s dig into what’s going on here. The pipe characters are chaining a bunch of commands together. Each command’s output becomes the input for the next. Given that, let’s walk through what’s going on in 3 steps.

Step 1 - List Linux Kernels

What does dpkg -l 'linux-*' do?

This one is fairly simple. Just pull up the man page for dpkg:

dpkg - package manager for Debian
...
dpkg-query actions
              -l, --list package-name-pattern...
                  List packages matching given pattern.

So, it should just list out the packages matching the pattern provided. Let’s try:

$ dpkg -l 'linux-*'
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
+++-==============-============-============-=================================
un  linux-doc-2.6.                     (no description available)
un  linux-doc-3.0.                     (no description available)
un  linux-doc-3.2.                     (no description available)
un  linux-doc-3.5.                     (no description available)
ii  linux-firmware 1.95         all          Firmware for Linux kernel drivers
un  linux-image                        (no description available)
un  linux-image-2.                     (no description available)
ii  linux-image-2. 2.6.38-11.50 amd64        Linux kernel image for version 2.
ii  linux-image-2. 2.6.38-8.42  amd64        Linux kernel image for version 2.
un  linux-image-3.                     (no description available)
rc  linux-image-3. 3.0.0-12.20  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.0.0-13.22  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.0.0-14.23  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.0.0-15.26  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.0.0-16.29  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.0.0-17.30  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-23.36  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-24.39  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-25.40  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-26.41  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-27.43  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-29.46  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-30.48  amd64        Linux kernel image for version 3.
rc  linux-image-3. 3.2.0-31.50  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.2.0-32.51  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-17.28  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-18.29  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-19.30  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-21.32  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-22.34  amd64        Linux kernel image for version 3.
ii  linux-image-3. 3.5.0-23.35  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-17.28  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-18.29  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-19.30  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-21.32  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-22.34  amd64        Linux kernel image for version 3.
ii  linux-image-ex 3.5.0-23.35  amd64        Linux kernel image for version 3.
ii  linux-image-ge 3.5.0.23.29  amd64        Generic Linux kernel image
ii  linux-image-se 3.5.0.23.29  amd64        Transitional package.
un  linux-initramf                     (no description available)
un  linux-kernel-h                     (no description available)
un  linux-kernel-l                     (no description available)
ii  linux-libc-dev 3.5.0-23.35  amd64        Linux Kernel Headers for developm
un  linux-restrict                     (no description available)
ii  linux-sound-ba 1.0.25+dfsg- all          base package for ALSA and OSS sou
un  linux-source-2                     (no description available)
un  linux-source-3                     (no description available)
un  linux-source-3                     (no description available)
un  linux-source-3                     (no description available)
un  linux-tools                        (no description available)

You can look at the man page for dpkg-query for more detail, but the items with an i for the 2nd character are currently installed. We’ll essentially want to dig through these installed old kernels and purge the ones we don’t want.

Step 2 - Show Unused Linux Kernels:

What does sed '/^ii/!d;/'"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"'/d;s/^[^ ]* [^ ]* \([^ ]*\).*/\1/;/[0-9]/!d' do?

This one is a little more tricky. We need to dissect it from the inside out. In the middle of that mess, you’ll see:

"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"

That’s essentially a command inside a command (kind of like the pipe concept, but injected in middle instead of the start of the input stream). We can drop the encapsulating "$( )" to see what this does:

$ uname -r
3.5.0-23-generic

… and then …

$ uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/"
3.5.0-23

As we see, this is using the uname command with the -r option to print the kernel release that’s active on our system. That next piece calls a powerful program called sed. It uses a regular expression to trim down the output of uname. If read in English, it says, “for the input given, try to first find as many characters as you can until you get to the last hyphen and remember them; if after the hyphen you find only non-numeric characters then replace the matched string with what was remembered, otherwise do nothing.” The regular expression is much more eloquent. :)

To better understand the remaining logic, we can reduce that original monster to the following by manually inserting "3.5.0-23" where that uname-sed combo was before:

sed '/^ii/!d;/'"3.5.0-23"'/d;s/^[^ ]* [^ ]* \([^ ]*\).*/\1/;/[0-9]/!d'

This isn’t as bad as it first appears. It’s simply a chain of matches with /d or /!d, which mean delete the match or delete everything but the match respectively. Plus there’s one substitution. So …

/^ii/!d 

… means, delete all lines except those that start with ii

/'"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"'/d

… means, delete all lines with the current kernel version in them (remember, this is equivalent to /'"3.5.0-23"'/d)

s/^[^ ]* [^ ]* \([^ ]*\).*/\1/

Consume all non-space characters, if any, followed by a space. Repeat. Remember the next set of non-space characters. Consume everything that follows. Replace everything with the remembered match.

/[0-9]/!d

… means, delete all lines except those with numbers.

My final output is the following:

linux-image-2.6.38-11-server
linux-image-2.6.38-8-server
linux-image-3.0.0-17-server
linux-image-3.2.0-32-generic
linux-image-3.5.0-17-generic
linux-image-3.5.0-18-generic
linux-image-3.5.0-19-generic
linux-image-3.5.0-21-generic
linux-image-3.5.0-22-generic
linux-image-extra-3.5.0-17-generic
linux-image-extra-3.5.0-18-generic
linux-image-extra-3.5.0-19-generic
linux-image-extra-3.5.0-21-generic
linux-image-extra-3.5.0-22-generic

Step 3 - Remove the Unused Kernels

What does xargs sudo apt-get -y purge do?

From the xargs man page, “xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo) one or more times with any initial-arguments followed by items read from standard input. Blank lines on the standard input are ignored.” In other words, xargs is going to let us issue the same command to each of those items listed in the last section.

The rest is fairly self explanatory. For every package listed, we’re going to ask apt-get to purge it, which the man page states means, “packages are removed and purged (any configuration files are deleted too).” The -y option means apt-get won’t ask for a confirmation. If you’re uneasy about the process, you might remove this for a dry-run, but given the cyclic nature of how we’re calling the command, it will abort (which is fine for a dry run, but you’ll have to add -y for it to work).

For completeness, sudo runs the purge as the super user.

All Together

Let’s try without -y first:

$ dpkg -l 'linux-*' | sed '/^ii/!d;/'"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"'/d;s/^[^ ]* [^ ]* \([^ ]*\).*/\1/;/[0-9]/!d' | xargs sudo apt-get purge
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages will be REMOVED:
  linux-image-2.6.38-11-server* linux-image-2.6.38-8-server* linux-image-3.0.0-17-server* linux-image-3.2.0-32-generic* linux-image-3.5.0-17-generic*
  linux-image-3.5.0-18-generic* linux-image-3.5.0-19-generic* linux-image-3.5.0-21-generic* linux-image-3.5.0-22-generic* linux-image-extra-3.5.0-17-generic*
  linux-image-extra-3.5.0-18-generic* linux-image-extra-3.5.0-19-generic* linux-image-extra-3.5.0-21-generic* linux-image-extra-3.5.0-22-generic*
0 upgraded, 0 newly installed, 14 to remove and 0 not upgraded.
After this operation, 1,359 MB disk space will be freed.
Do you want to continue [Y/n]? Abort.

Note the abort. This occurs automatically. Everything looks good here. Our current kernel isn’t in the list. Let’s do it! This time a lot more happens. I’ve inserted [SIMILAR OUTPUT DELETED] where the output is iterating over versions:

$ dpkg -l 'linux-*' | sed '/^ii/!d;/'"$(uname -r | sed "s/\(.*\)-\([^0-9]\+\)/\1/")"'/d;s/^[^ ]* [^ ]* \([^ ]*\).*/\1/;/[0-9]/!d' | xargs sudo apt-get -y purge
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages will be REMOVED:
  linux-image-2.6.38-11-server* linux-image-2.6.38-8-server* linux-image-3.0.0-17-server* linux-image-3.2.0-32-generic* linux-image-3.5.0-17-generic*
  linux-image-3.5.0-18-generic* linux-image-3.5.0-19-generic* linux-image-3.5.0-21-generic* linux-image-3.5.0-22-generic* linux-image-extra-3.5.0-17-generic*
  linux-image-extra-3.5.0-18-generic* linux-image-extra-3.5.0-19-generic* linux-image-extra-3.5.0-21-generic* linux-image-extra-3.5.0-22-generic*
0 upgraded, 0 newly installed, 14 to remove and 0 not upgraded.
After this operation, 1,359 MB disk space will be freed.
(Reading database ... 196310 files and directories currently installed.)
Removing linux-image-2.6.38-11-server ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 2.6.38-11-server /boot/vmlinuz-2.6.38-11-server
update-initramfs: Deleting /boot/initrd.img-2.6.38-11-server
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 2.6.38-11-server /boot/vmlinuz-2.6.38-11-server
Generating grub.cfg ...
Found linux image: /boot/vmlinuz-3.5.0-23-generic
Found initrd image: /boot/initrd.img-3.5.0-23-generic
Found linux image: /boot/vmlinuz-3.5.0-22-generic
Found initrd image: /boot/initrd.img-3.5.0-22-generic
Found linux image: /boot/vmlinuz-3.5.0-21-generic
Found initrd image: /boot/initrd.img-3.5.0-21-generic
Found linux image: /boot/vmlinuz-3.5.0-19-generic
Found initrd image: /boot/initrd.img-3.5.0-19-generic
Found linux image: /boot/vmlinuz-3.5.0-18-generic
Found initrd image: /boot/initrd.img-3.5.0-18-generic
Found linux image: /boot/vmlinuz-3.5.0-17-generic
Found initrd image: /boot/initrd.img-3.5.0-17-generic
Found linux image: /boot/vmlinuz-3.2.0-32-generic
Found initrd image: /boot/initrd.img-3.2.0-32-generic
Found memtest86+ image: /memtest86+.bin
done
Purging configuration files for linux-image-2.6.38-11-server ...
Examining /etc/kernel/postrm.d .
run-parts: executing /etc/kernel/postrm.d/initramfs-tools 2.6.38-11-server /boot/vmlinuz-2.6.38-11-server
run-parts: executing /etc/kernel/postrm.d/zz-update-grub 2.6.38-11-server /boot/vmlinuz-2.6.38-11-server

[SIMILAR OUTPUT DELETED]

Note how the apt-get process even runs grub for you so your reboot menu is ready to go. Very nice!

Now let’s check our /boot partition, which was using 86% of 227 MB of storage space.

$ du -h /boot --max-depth=0
35M /boot

Ah, only 35 M in use, i.e., 15%. Plenty of room for new kernel releases!