June 2, 2023

Maximize Productivity Of The Bash Shell

This is a 2023 followup to my 2018 Bash Shell Tweaks & Tips article.

Since that last article there has been a renaissance in shell-agnostic command line tooling, often implemented in high-performance Rust, that has elevated the interactive shell experience. Hence, now is the right time to revisit Bash with such tooling in mind.

In this article we will: enable a host of useful shell options, script automatic pushd along with matching directory stack navigation bindings, integrate an interactive completion interface and we will document some modern command line tools, among other tips and suggestions.

Note, my bashrc and inputrc files incorporate all the ideas documented in this article.

Dispelling Bash Misconceptions

Before proceeding, it is worth correcting some Bash misconceptions that still intermittently persist:

Keep reading for details about all the above.

Installation And Setup For macOS

Since OS Catalina (2019), Zsh is now the default shell for macOS. Apple still ships with Bash, but only version 3.2 which dates back to 2006; such a legacy version of Bash should be avoided for interactive use these days.

Updating to a modern version of Bash is easily accomplished with the Homebrew package manager:

brew install bash
echo /opt/homebrew/bin/bash | sudo tee -a /etc/shells
chsh -s /opt/homebrew/bin/bash

Note, if using an older Intel-based Mac, please replace /opt/homebrew with /usr/local.

Please also install the Bash Completion package as follows:

brew install bash-completion@2

And lastly add the following to your ~/.bash_profile:

[[ -r "$HOMEBREW_PREFIX/etc/profile.d/bash_completion.sh" ]] && . "$HOMEBREW_PREFIX/etc/profile.d/bash_completion.sh"

Readline Configuration

The GNU Readline library is used by Bash, and certain other utilities, for line-editing and history interaction.

Many beneficial Readline features are disabled by default; thankfully, it is easy to enable these features for the betterment of the interactive experience.

The Readline library is configured through the ~/.inputrc file. I use and recommend these settings (with comments provided detailing each setting):

# TAB cycles forward and Shift-TAB cycles backward through completion choices.
TAB: menu-complete
"\e[Z": menu-complete-backward

# Substring history search using UP and DOWN arrow keys.
"\e[A": history-substring-search-backward
"\e[B": history-substring-search-forward

# Launch $EDITOR on the current command and execute when finished editing.
"\ee": edit-and-execute-command

# Enable completion coloring.
set colored-completion-prefix on
set colored-stats on

# Ignore case when completing.
set completion-ignore-case on

# Treat hypen and underscores as equivalent.
set completion-map-case on

# The number of completions to display without prompt; when exceeded a
# prompt-to-display will appear.
set completion-query-items 200

# Automatically add slash to the end of symlinked directories when completing.
set mark-symlinked-directories on

# Don't automatically match files beginning with dot.
set match-hidden-files off

# Display the common prefix choices on the first completion then cycle the
# available choices on the next completion.
set menu-complete-display-prefix on

# Turn off the completions pager.
set page-completions off

# Immediately display completion matches.
set show-all-if-ambiguous on

# Smartly complete items when the cursor is not at the end of the line.
set skip-completed-text on

Highlighting a few capabilities enabled above:

Shell Options

Somewhat similar to the previous Readline section, Bash provides a number of useful shell options that are also disabled by default.

Shell options should be set in ~/.bashrc. I use and recommend these options (with comments detailing each option):

#  - autocd - change directory without entering the 'cd' command
#  - cdspell - automatically fix directory typos when changing directory
#  - direxpand - automatically expand directory globs when completing
#  - dirspell - automatically fix directory typos when completing
#  - globstar - ** recursive glob
#  - histappend - append to history, don't overwrite
#  - histverify - expand, but don't automatically execute, history expansions
#  - nocaseglob - case-insensitive globbing
#  - no_empty_cmd_completion - do not TAB expand empty lines
shopt -s autocd cdspell direxpand dirspell globstar histappend histverify \
    nocaseglob no_empty_cmd_completion

I find it somewhat baffling that these useful options are disabled by default.

History

In its out of the box configuration, Bash history is quite primitive:

Fortunately, it is easy to greatly improve Bash history.

Firstly, in the previous Shell Options section, we enabled the important shopt -s histappend option which appends the current shell history to the history file rather than overwriting it.

And to compliment that shell option change, these are the history controls I recommend in ~/.bashrc:

HISTCONTROL=ignoreboth:erasedups # Ignore and erase duplicates
HISTIGNORE=?:??                  # Ignore one and two letter commands
HISTFILESIZE=99999               # Max size of history file
HISTSIZE=99999                   # Amount of history to preserve

Lastly, to share history between concurrent Bash sessions:

PROMPT_COMMAND="history -a; history -n"

This will immediately append the current shell history to the history file and load history from other active sessions in the current shell history each time the prompt is updated.

Prompt

Speaking of prompts, an informative, colorful and configurable prompt is easily attained nowadays. The cross-shell Starship prompt has become a fan-favourite due to its compatibility and customization simplicity. For many, Starship is all the prompt they will ever need.

Install as per Starship instructions, then add the following to ~/.bashrc:

eval "$(starship init bash)"

And configure according to the documentation on the Starship website.

Be aware, Starship has now taken control of the PROMPT_COMMAND and the history sharing noted in the previous section will no longer apply. I am led to believe the following ~/.bashrc configuration will restore history sharing:

function history_sharing() {
    history -a && history -n
}
starship_precmd_user_func="history_sharing"

With all that said, I still use my own bash-seafly-prompt package instead, which predates Starship, and which also has superior Git status performance when combined with either the git-status-fly or gitstatus utilities.

Other popular prompt choices include:

Fuzzy Finding

These days, it is hard to imagine using an interactive shell without fuzzy finding integration. fzf is the most popular such fuzzy finding tool.

I wrote a Fuzzy Finding in Bash with fzf article a few years ago. That article is still relevant and applicable these days; if time, permits I do recommend reading it.

Install as per fzf instructions, then add the following key-binding configuration to ~/.bashrc:

. /LOCATION/OF/FZF/INSTALLATION/shell/key-bindings.bash

That will add the following three key-bindings:

An assortment of custom search scripts are documented in the previously mentioned Fuzzy Finding in Bash with fzf article, they are well worth inspecting. These include: editing a fuzzy found file, killing a fuzzy found process, Git staging fuzzy found files and fuzzy Git log browsing to name a few.

The forgit project, which also combines fzf with interactive Git, may be of interest.

Interactive Completion Powered By fzf

Unlike Zsh and Fish, Bash does not provide a native interactive completion mechanism; that is, completion that can be interactively navigated with arrow keys to quickly select the desired completion.

As noted above in the Readline section, Bash does support completion cycling, but when there are many completions there is no ability to interactively navigate the displayed completions.

The excellent fzf-tab-completion package however does provide interactive completions powered by the aforementioned fzf utility.

Install by cloning the repository:

git clone --depth 1 https://github.com/lincheney/fzf-tab-completion ~/.fzf-tab-completion

Then add the following to ~/.bashrc:

source $HOME/.fzf-tab-completion/bash/fzf-bash-completion.sh
bind -x '"\C-f": fzf_bash_completion'

This binds <Control-f> (f for fuzzy) to fzf-tab-completion whilst keeping <TAB> bound to native Bash completion. I find native Bash <TAB> completion preferable for most simple completions, whilst reserving fzf-tab-completion for the few occasions where there are many completion matches. Note, a <TAB> initiated completion can be converted to fzf-tab-completion just by pressing <Control-f>.

Lastly, <Control-f> is just my preferred binding, another binding, such as <TAB> itself, can be defined instead.

Modern Shell Tools

As mentioned in the introduction, there has been a resurgence in command line tool development in recent years. Some of these new tools are direct replacements for long-established core utilities.

A few that are noteworthy:

Whilst not a new tool, I would also like to shout out (again), the brilliant qmv utility which makes bulk renames a breeze by way of your current $EDITOR. I discuss it in greater detail here.

Similarly, for those interested, I detail the ripgrep and fd utilities here.

Miscellaneous Helpers

Lastly, I will end with a couple of simple Bash helpers, inspired by other shells, that have enhanced my shell usage.

Automatic pushd And Associated Navigation Bindings

The Fish shell automatically provides directory history when changing directories along with companion prevd and nextd commands, which themselves are bound to <Alt-Left> and <Alt-Right>, for directory history navigation.

That functionality can be mimicked in Bash as follows:

cd() {
    local target="$@"
    if [[ $# -eq 0 ]]; then
        # Handle 'cd' without arguments; change to the $HOME directory.
        target="$HOME"
    elif [[ $1 == "--" ]]; then
        # Handle 'autocd' shopt, that is just a directory name entered without
        # a preceding 'cd' command. In that case the first argument will be '--'
        # with the target directory defined by the remaining arguments.
        shift
        target="$@"
    fi

    # Note, if the target directory is the same as the current directory do
    # nothing since we don't want to populate the directory stack with
    # consecutive repeat entries.
    if [[ "$target" != "$PWD" ]]; then
        builtin pushd "$target" 1>/dev/null
    fi
}

# Alt-Left: rotate back through the directory stack.
bind -x '"\C-x\C-p": "pushd +1 &>/dev/null"'
bind '"\e[1;3D":"\C-x\C-p\n"'
# Alt-Right rotate forward through the directory stack.
bind -x '"\C-x\C-n": "pushd -0 &>/dev/null"'
bind '"\e[1;3C":"\C-x\C-n\n"'

All cd executions will be recorded to the directory stack and <Alt-Left> and <Alt-Right> will now act like a browser’s back and forward buttons, but this time for stacked directories.

Web Searching

The Oh My Zsh framework provides a handy web-search plugin.

We can mimic that as follows:

if [[ $(uname) == Linux ]]; then
    alias open='xdg-open 2>/dev/null'
fi

web() {
    GOLD=$(tput setaf 222)
    GREEN=$(tput setaf 79)
    NC=$(tput sgr0)

    read -ep "$(echo -e "${GOLD}Search ${GREEN}${NC}")" search_term
    if [[ -n "$search_term" ]]; then
        open "https://duckduckgo.com/?q=${search_term}" &>/dev/null
    fi
}

This will execute a DuckDuckGo search using the default browser when web is executed. I like DuckDuckGo because it provides bang shortcuts. For example, the following will do a GitHub search, via the !gh bang, for Neovim:

Search➜ !gh Neovim

Copy Directory

Also inspired by Oh My Zsh, a clone of the copydir plugin which simply copies the current working directory to the system clipboard:

alias cwd='copy_working_directory'

copy_working_directory() {
    if [[ $(uname) == Linux ]]; then
        echo -n ${PWD/#$HOME/\~} | tr -d "\r\n" | xclip -selection clipboard -i
    elif [[ $(uname) == Darwin ]]; then
        echo -n ${PWD/#$HOME/\~} | tr -d "\r\n" | pbcopy
    fi
    # Also copy current directory to a tmux paste buffer if tmux is active.
    if [[ -n $TMUX ]]; then
        echo -n ${PWD/#$HOME/\~} | tr -d "\r\n" | tmux load-buffer -
    fi
}

Conclusion

Hopefully this assortment of settings, utilities and helpers provides inspiration to improve your interactive Bash usage.