DEBUG trap and PROMPT_COMMAND in Bash

Update 03/08/2016: A patch by Dan Stromberg adds a PS0 variable to Bash that greatly simplifies what’s described in this article. This patch will likely be merged into Bash 4.4. Please refer to his post for details.

The DEBUG trap

The DEBUG trap is an extremely handy feature of Bash. The idea is pretty straightforward: if you run

trap "echo Hello" DEBUG

then Bash will run echo Hello before it executes each subsequent command. For example:

~/Scratch $ ls
Hello
file1 file2
~/Scratch $ echo Bye
Hello
Bye

A caveat, however, is that the DEBUG trap is triggered once per simple command; if you have command lists or control structures, the trap will be triggered multiple times. For example, using the setup above:

~/Scratch $ echo 1 && echo 2; echo 3
Hello
1
Hello
2
Hello
3
~/Scratch $ if [ -e /etc/passwd ]; then echo "/etc/passwd exists"; fi
Hello
Hello
/etc/passwd exists

What if we only want to run a command once per composite command, like the preexec hook in zsh?

Enter PROMPT_COMMAND.

PROMPT_COMMAND

The idea behind PROMPT_COMMAND is also very simple: if you run

PROMPT_COMMAND="echo Bye"

then Bash will execute echo Bye before it prints each subsequent prompt (i.e., after it has finished executing the previous command line). For example, using the setup above:

~/Scratch $ echo 1; echo 2
Hello
1
Hello
2
Hello
Bye

Note that the DEBUG trap is triggered again for PROMPT_COMMAND, in addition to the user-supplied commands.

Combining the DEBUG trap and PROMPT_COMMAND

By combining the DEBUG trap and PROMPT_COMMAND, we can now hack Bash to run some code right before and right after executing a full command. For example, try adding this to your ~/.bashrc:

# This will run before any command is executed.
function PreCommand() {
  if [ -z "$AT_PROMPT" ]; then
    return
  fi
  unset AT_PROMPT

  # Do stuff.
  echo "Running PreCommand"
}
trap "PreCommand" DEBUG

# This will run after the execution of the previous full command line.  We don't
# want it PostCommand to execute when first starting a bash session (i.e., at
# the first prompt).
FIRST_PROMPT=1
function PostCommand() {
  AT_PROMPT=1

  if [ -n "$FIRST_PROMPT" ]; then
    unset FIRST_PROMPT
    return
  fi

  # Do stuff.
  echo "Running PostCommand"
}
PROMPT_COMMAND="PostCommand"

The result:

~/Scratch $ echo 1; echo 2 && echo 3
Running PreCommand
1
2
3
Running PostCommand

This gives rise to some neat applications, such as a command timer script I wrote that prints out the execution time of each command:

Please feel free to check it out on GitHub :)

Happy Bash hacking!

How To Set Default Fonts and Font Aliases on Linux

Font Woes

It’s fairly straightforward to set the default font used in native apps on a modern Linux desktop, or the default fonts used to render web pages in your browser of choice.

But if you’re reading this, you probably know that that’s far from the end of the story. You might have noticed that Firefox and Chrome rudely ignore your font settings for many websites. This is because many (if not most) popular sites, including Google, Yahoo, Facebook or GitHub, specify preferred fonts for text:

Site Font Specification

Google

arial, sans-serif

Yahoo

Helvetica Neue, Helvetica, Arial

Facebook

lucida grande, tahoma, verdana, arial, sans-serif

GitHub

Helvetica, arial, freesans, clean, sans-serif

(Retrieved on 02/21/2014)

You might immediately notice that the most commonly used fonts on these sites, Arial and Helvetica, are fonts that come bundled with Microsoft Windows, and are most likely not installed on your Linux system. In this case, what font is actually used is anyone’s guess. If they are installed (e.g., via a package like ttf-ms-fonts or directly copied from a Windows machine), well, you still probably want to display your favorite font instead :)

So, let’s find out what your default fonts and aliases are with fc-match:

for family in serif sans-serif monospace Arial Helvetica Verdana "Times New Roman" "Courier New"; do
  echo -n "$family: "
  fc-match "$family"
done

This is what I get on my machine by default:

serif: DejaVuSerif.ttf: "DejaVu Serif" "Book"
sans-serif: DejaVuSans.ttf: "DejaVu Sans" "Book"
monospace: DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"
Arial: DejaVuSans.ttf: "DejaVu Sans" "Book"
Helvetica: n019003l.pfb: "Nimbus Sans L" "Regular"
Verdana: DejaVuSans.ttf: "DejaVu Sans" "Book"
Times New Roman: DejaVuSerif.ttf: "DejaVu Serif" "Book"
Courier New: DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

Font Configuration Files

So, assuming you’ve installed your fonts of choice (via a package, copying to /usr/share/fonts or ~/.fonts - please verify with the fc-list command), how do you set them as default in all apps and web sites?

Well, there are two places where fonts are configured: system-wide configuration resides in /etc/fonts/, and per-user configs are stored in ~/.config/fontconfig/fonts.conf (note that this used to be ~/.fonts.conf before fontconfig 2.10.1). For simplicity’s sake, we’ll do it in ~/.config/fontconfig/fonts.conf.

Let’s open up ~/.config/fontconfig/fonts.conf, or create it if it doesn’t already exist. Put the following skeleton structure in there:

<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
</fontconfig>

We will put all of our custom configuration between <fontconfig> and </fontconfig>.

Setting Default Fonts

First, let’s set the default serif, sans serif, and monospace fonts. I’ll use the beautiful Chrome OS fonts as an example (ttf-chromeos-fonts if you’re running Arch Linux). Insert the following between <fontconfig> and </fontconfig>:

  <!-- Set preferred serif, sans serif, and monospace fonts. -->
  <alias>
    <family>serif</family>
    <prefer><family>Tinos</family></prefer>
  </alias>
  <alias>
    <family>sans-serif</family>
    <prefer><family>Arimo</family></prefer>
  </alias>
  <alias>
    <family>sans</family>
    <prefer><family>Arimo</family></prefer>
  </alias>
  <alias>
    <family>monospace</family>
    <prefer><family>Cousine</family></prefer>
  </alias>

Aliasing Microsoft Fonts

Now, we will create aliases for commonly used fonts like Arial and Helvetica, so that our favorite fonts will always be used instead of these fonts, e.g. when requested by a web site.

Insert the following between <fontconfig> and </fontconfig>, after the previous snippet:

  <!-- Aliases for commonly used MS fonts. -->
  <match>
    <test name="family"><string>Arial</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Helvetica</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Verdana</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Tahoma</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <!-- Insert joke here -->
    <test name="family"><string>Comic Sans MS</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Times New Roman</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Tinos</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Times</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Tinos</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Courier New</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Cousine</string>
    </edit>
  </match>

Note that the Microsoft fonts are aliased directly to the our preferred substitute fonts. Aliasing to generic families (serif, sans-serif etc.) may or may not work depending on your configuration in /etc/fonts (they didn’t work for me), so it’s safer this way.

This list is of course by no means definitive; add/remove aliases as you like.

The Result

You’ll need to log out and back in for all applications to update. You should see the difference immediately:

Google search results, before (Arial):

/assets/files/font_config_before.png

Google search results, after (Arimo):

/assets/files/font_config_after.png

You can verify that the aliases have been set up correctly with fc-match:

for family in serif sans-serif monospace Arial Helvetica Verdana "Times New Roman" "Courier New"; do
  echo -n "$family: "
  fc-match "$family"
done

which should now give you something like:

serif: Tinos-Regular.ttf: "Tinos" "Regular"
sans-serif: Arimo-Regular.ttf: "Arimo" "Regular"
monospace: Cousine-Regular.ttf: "Cousine" "Regular"
Arial: Arimo-Regular.ttf: "Arimo" "Regular"
Helvetica: Arimo-Regular.ttf: "Arimo" "Regular"
Verdana: Arimo-Regular.ttf: "Arimo" "Regular"
Times New Roman: Tinos-Regular.ttf: "Tinos" "Regular"
Courier New: Cousine-Regular.ttf: "Cousine" "Regular"

Other Notes

Some existing examples you may find online show the following syntax:

<!-- Deprecated syntax -->
<match target="pattern" name="family">
  <test name="family" qual="any"><string>Arial</string></test>
  ...
</match>

This will produce an error message like

Fontconfig error: "/home/username/.config/fontconfig/fonts.conf", line 38: invalid attribute 'name'

The fix is to change <match target="pattern" name="family"> to just <match>, as shown above.

Shortening paths in the Bash prompt with PROMPT_DIRTRIM

Bash 4 introduced a new environment variable called PROMPT_DIRTRIM that allows the shortening of paths displayed in its prompts (via w in PS1. I added

PROMPT_DIRTRIM=3

to my ~/.bashrc and whereas Bash used to display this prompt:

[[email protected] [email protected]ramework]$

It now displays

[[email protected] .../generic/system/framework]$

which is a huge improvement especially when working with deep directory structures.