Skip to main content

Extending Bash Commandline with Bash Scripts

Bash logo

This blog post will show a very elegant way on how to easily extend the Bash command line with Bash scripts, or basically any command. The pivotal feature for all of this is the innocent looking function shell-expand-line accessible from GNU Readline used by Bash.

As we will see, this allows us to elegantly extend the Bash command line with our own hot keys. The screencast below shows a demo to archive a document just downloaded through Firefox tagged with a current date stamp. While entering the mv command, two hot keys are used:

  • Alt-l: Paste the least recently used file in GNOME

  • Alt-d: Insert a date-stamp at cursor position

Bash session

shell-expand-line

The function shell-expand-line is bound to M-C-e by default and will replace the preceding shell construct with its evaluation. The description of key strokes is similar to what GNU Emacs uses and thus M-C-e refers to "Meta Control e". Usually the Alt key represents Meta but if an Alt key is not available, prefixing C-e (Control e) with Esc will also do.

So for example pressing M-C-e when the cursor is positioned at the end of "$HOME" replaces the variable reference with its contents. Although this does not look very useful at first, we will revisit this in a short while for another nice interactive usage.

But first let’s see what else we can do with M-C-e. Maybe it can be used on arithmetic expansions? Testing it on an example like "$(( 40 + 2))" indeed replaces the construct immediately by "42" in the command line. Nice. So maybe we can even use it on command substitution? Yet another quick test on "$(date)" shows that we can execute arbitrary commands this way and interpolate their output right into the active command line.

inputrc

But even though this is nice, it still looks like it is of no real value to us. The missing piece of the puzzle is Readline’s Init File which allows us to bind this functionality to a key combination. Including these two lines in your ~/.inputrc will insert a date stamp at cursor position on pressing M-d:

# Insert datestamp prefix
"\ed": "$(date +'%Y-%m-%d')-\e\C-e"

Note that we have to use \e to specify Meta in the init file. The expected \M- syntax found in the documentation simply does not work for me.

With this knowledge, it is easy to improve the usefulness of my gnome-recent script introduced in a previous post. Binding it to M-l is even simpler:

# Insert recent file used in GNOME
"\el": "$(gnome-recent)\e\C-e"

More readline fun

Knowing how to combine command line editing functions into shortcuts, let’s revisit the expansion of environment variables. One task that crops up from time to time is to edit environment variables, especially PATH. Some shells have command to deal with this (e.g. vared in zsh), but the following binding for C-x P (Ctrl-C followed by the single character P) works nicely in bash:

# edit the path
"\C-xP": "PATH=${PATH}\e\C-e\C-a\ef\C-f"

Let’s take this in small steps to understand it. First the literal text PATH=${PATH} is inserted into (the hopefully empty) command line. M-C-e will then substitute the contents of PATH for ${PATH}. C-a jumps back to the beginning of the line and M-f moves the cursor forward one word, leaving it just before the = sign. Moving the cursor right once more with C-f positions it so we are at the start of the contents to edit it. Pressing Enter after editing will commit the new contents.

In this vein, we can actually come up with a more generic version of a binding to edit the contents of an environment variable:

# Edit variable on current line.
"\C-xv": "\C-a\C-k$\C-y\e\C-e\C-a\C-y="

Calling C-x v after entering the name of an environment variable will eventually setup a similar editing environment. First C-a moves to the beginning of the line and C-k kills the whole line and puts it into the kill-ring. A $ is inserted and C-y inserts (yanks) the contents of the kill-ring (i.e. the variable name). The well known M-C-e then replaces it with its content. Again C-a jumps to the beginning of the line and another C-y inserts the variable name again followed by a = character. Nice, isn’t it?

Your creativity is the limit!

Below you can find the whole contents of my .inputrc and this should remind you that not only bash uses readline and thus this file will be used by other interactive programs also. So it makes sense to restrict all of those key combinations with the $if Bash conditional:

set bell-style visible
set colored-completion-prefix on
set colored-stats on

set input-meta on
set output-meta on
set convert-meta off

# Macros that are convenient for shell interaction
$if Bash
# edit the path
"\C-xP": "PATH=${PATH}\e\C-e\C-a\ef\C-f"
# prepare to type a quoted word --
# insert open and close double quotes
# and move to just after the open quote
"\C-x\"": "\"\"\C-b"
# insert a backslash (testing backslash escapes
# in sequences and macros)
"\C-x\\": "\\"
# Quote the current or previous word
"\C-xq": "\eb\"\ef\""
# Add a binding to refresh the line, which is unbound
"\C-xr": redraw-current-line
# Edit variable on current line.
"\C-xv": "\C-a\C-k$\C-y\e\C-e\C-a\C-y="
# Insert datestamp prefix
"\ed": "$(date +'%Y-%m-%d')-\e\C-e"
# Insert recent file used in GNOME
"\el": "$(gnome-recent)\e\C-e"
# Located at the end of a filename, remove all path components
"\C-xb": "\e\C-b$(basename \e\C-f)\e\C-e"
# Located at the end of a filename, remove basename leaving path
"\C-xp": "\e\C-b$(dirname \e\C-f)\e\C-e"
$endif

Comments

Comments powered by Disqus