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:
- Bash supports changing directories without entering the
cd
command - Bash supports simple file and directory path autocorrection
**
recursive globbing is supported- Command
history
can be shared between concurrent Bash instances - Bash does support tab-completion cycling
- Command and context-aware completion is supported through the Bash Completion package
- Interactive completion is supported by way of a 3rd party package
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:
-
The
<TAB>
and<Shift-Tab>
keys will now cycle completions choices -
Completions will commence immediately after pressing the first
<TAB>
-
Partially typing a command and pressing
<Up>
will engagehistory
substring matching, not simply just start of line matching -
The
<Alt-e>
edit-and-execute binding is a reasonable Vim-mode substitute if Vim or Neovim is the configured$EDITOR
; noting that Readline only supports a very basic Vi-mode if configured
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:
- Each running shell will have its own history, and when a shell ends the
running history of the current shell will entirely replace the contents of
~/.bash_history
file - Hence, history from the next exiting shell will overwrite the history of the previously exited shell
- History will not be shared between concurrent shell sessions
- Duplicates will exist in the current session’s history
- Only the last 500 commands are preserved
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:
Control-r
- fuzzy reverse history searchControl-t
- append fuzzy selections to the current shell commandAlt-c
- change to the fuzzy found directory
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:
-
bat, an enhanced
cat
clone -
delta, a syntax-highlighting pager for
diff
andgit
-
dust, an intuitive version of
du
-
exa, a modern replacement for
ls
-
fd, a simple, fast and user-friendly alternative to
find
-
hyperfine, a modern benchmarking alternative to
time
-
ripgrep, a recursive pattern search alternative to
grep
-
sd, an intuitive
sed
alternative -
zoxide, a smarter
cd
command, inspired by z and autojump, that remembers visited directories for instant navigation
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.