4.2. External Commands in the Prompt

You can use the output of regular Linux commands directly in the prompt as well. Obviously you don't want to insert a lot of material or it will create a large prompt. You also want to use a fast command, because it's going to be executed every time your prompt appears on the screen, and delays in the appearance of your prompt while you're working can be very annoying.

[21:58:33][giles@nikola:~]$ PS1="[\$(date +%H%M)][\u@\h:\w]\$ "
[2159][giles@nikola:~]$ ls
bin   mail

It's important to notice the backslash before the dollar sign of the command substitution. Without it, the external command is executed exactly once: when the PS1 string is read into the environment. For this prompt, that would mean that it would display the same time no matter how long the prompt was used. The backslash protects the contents of $() from immediate shell interpretation, so date is called every time a prompt is generated.

In version 3+ of Bash, the prompt supports using any date format you want without calling date. If you have a recent version of Bash, you can use the following instead:

[14:04:42][giles@glo:~]$ PS1="[\D{%H%M}][\u@\h:\w]\$ "
[1404][giles@glo:~]$ ls
bin   mail

The format for parameters for \D{} is the same as for date, so check man date.

Linux comes with a lot of small utility programs like date, grep, or wc that allow you to manipulate data. If you find yourself trying to create complex combinations of these programs within a prompt, it may be easier to make an alias, function, or shell script of your own, and call it from the prompt. Escape sequences are often required in bash shell scripts to ensure that shell variables are expanded at the correct time (as seen above with the date command): this is raised to another level within the prompt PS1 line, and avoiding it by creating functions is a good idea.

An example of a small shell script used within a prompt is given below:

#     lsbytesum - sum the number of bytes in a directory listing
#     and output the size in B, KiB, MiB, or GiB as appropriate.
#     There are a number of compromises here:
#     - "ls -l" outputs a file count as the first line, but it doesn't have
#       a fifth field, so it can be ignored ...
#     - "ls -l" doesn't list hidden files: that could be done with "ls -lA"

# How many decimal places? (zero or positive integer)
let scale=2

# Use awk to sum the fifth field of the list of files (only):
TotalBytes="$(ls -l | awk '/^-/ {total+=$5} ; END {printf ("%.0f", total) }')"

# if TotalBytes is empty, set it to zero:
if [ "${TotalBytes}x" = "x" ]
  let TotalBytes=0

# Count digits, then print out B, KiB, MiB, GiB, or TiB:
case $(echo -n $TotalBytes | wc -c) in
  1 | 2 | 3) echo -n "${TotalBytes}B" ;;
  4 | 5 | 6) echo "$(echo -e "scale=${scale} \n${TotalBytes}/1024 " | bc)KiB" ;;
  7 | 8 | 9) echo "$(echo -e "scale=${scale} \n${TotalBytes}/1048576 " | bc)MiB" ;;
  10 | 11 | 12) echo "$(echo -e "scale=${scale} \n${TotalBytes}/1073741824 " | bc)GiB" ;;
  *) echo "$(echo -e "scale=${scale} \n${TotalBytes}/1099511627776 " | bc)TiB" ;;

I used to keep this as a function, it now lives as a shell script in my ~/bin/ directory, which is on my path.

There's actually a small logical problem in this script, can you spot it? There's a disjoint between the number of digits and the size in KiB or MiB or TiB as we divide by 1024 (rather than 1000 as for the digits) each time. This leads to a bigger discrepancy every time you hit a 1024 byte boundary. In a practical sense this isn't a major problem and "case" offers such a simple solution that I haven't worked very hard on a solution.

Used in a prompt:

[1635][giles@glo:~]$ PS1="[\D{%H%M}][\u@\h:\w (\$(lsbytesum))]\$ "
[1636][giles@glo:~ (140.97MiB)]$ cd /bin/
[1636][giles@glo:/bin (3.37MiB)]$ cd /dev/
[1637][giles@glo:/dev (0B)]$