Make the terminal great again

🗓️ • ⏳ 12 min

The terminal is a very powerful tool, but it seems to scare developers into using less productive GUIs.
This guide provides tips, tricks, and tools to enhance your terminal experience, making it more efficient and enjoyable.

Choosing a shell

The shell is what interprets the command you write in the terminal.

This is not to be confused with the terminal emulator itself.
When you change the background of your terminal, you are configuring the emulator; when you set up an alias or run commands you are interacting with the shell.

There are a lot of shells out there, but the most frequently used ones are bash and zsh.
While bash is the most ubiquitous, zsh offers some worthwhile advantages that make it IMO the go-to option:

OMZSH sometimes gets mixed up with zsh itself.
OMZSH is a big framework built on top of the z shell, with tons of functionality built in.

While using it might make sense as a first step, I would suggest you move past it as soon as you feel comfortable doing so.
Realistically, there are two features you care about here: plugin management and fancy prompts.

As a plugin manager, OMZSH is incredibly overkill. Consider that zsh plugins can be handled simply by cloning and updating them.
There is really not much management needed.
Alternatives like zap are much, much faster and simpler.

As a prompt, it’s clunky as all hell and doesn’t really help with customization all that much.
Dedicated solutions like starship are much faster and more customizable.

Just in general, it pollutes your zsh config with a bunch of settings you might not really need (or want), as well as a huge amount of aliases you wouldn’t even notice are there, but might alter how commands behave.

So yea, I’d say use it to help you get started but leave it behind as soon as you get comfortable.
More often than not, less is more.

Plugins

There are (at least) three zsh plugins you should use to greatly improve your experience with the shell.

Use zsh-syntax-highlighting to add pretty colors to your commands as you type.
This will help you catch typos and mistakes faster.

zsh-autosuggestions suggests past commands based on what you type, as you type.

You can use fzf-tab to fuzzy find through zsh’s built-in tab completion.

Tips

People seem to think that using the terminal requires you to manually type out long commands or have a near-infinite amount of aliases.

Here are some other things you can do to reduce redundant typing.

Clipboard

Copy/pasting to/from the terminal seems to cause more issues than one might expect.
Most modern emulators support Ctrl+Shift+C for copying and Ctrl+Shift+V for pasting, but remember that you can always just pipe the results of a command to your clipboard:

sh
some --cool command | xclip -sel clipboard # or wl-copy, pbcopy, ...

Or pipe the content of your clipboard into a command:

sh
xclip -sel clipboard -o | some --cool command # or wl-copy, pbcopy, ...

History expansions

If you use the command line long enough, you will eventually get a Permission denied message after forgetting to use sudo to run a command.
You don’t need to re-write or copy the whole thing: sudo !! will re-run the last command with sudo at the start of it.

Here are a bunch of other useful expansions:

sh
!! # expands to the previous command
!$ # expands to the last argument of the previous command
!^ # expands to the first argument of the previous command
!* # expands to all arguments of the previous command
!echo # expands to the most recent `echo` command
!?echo? # expands to the most command containing the string `echo`

If these look like random symbols, understanding some basic regex syntax might help.

Simple but useful

Moving around the file system with the command line can be a bit of a pain.
Always remember that running cd with no arguments sends you to your home directory, while cd - sends you back (and forth) to the previously visited directory.
So you can swap between two distant directories easily.

Similarly, you might find yourself running the same set of commands in the same order multiple times, especially while troubleshooting issues.
This hints to a shell script being a better approach, but if that seems like overkill, you can use ; and && to automate this a bit:

sh
echo one && echo two # runs the second command only if the first one succeeds
echo one; echo two # runs the second command even if the first one fails

You can combine as many commands as you want, and go for a coffee while they run unattended.

Tools

As much as those tips help, some things are better handled by some clever command line tools.

z.lua

z.lua “learns” which directories you move to the most, and suggest them by frecency.

In practice, this means that after a while of cding to directories like these:

sh
cd ~/Documents/repos/work/legacy
cd ~/Pictures/trips/colombia

You’ll be able to run z leg or z col to go to those directories no matter where you are. It really does feel like it’s reading your mind.

Using zsh, you can install it directly as a plugin by adding something like this to your config:

sh
plug "skywind3000/z.lua" # adapt syntax to match plugin manager

fzf

fzf is a command line fuzzy finder.
What does it fuzzy find? Anything.

Running fzf in your home directory will list every single file in it, typing something will filter the results and pressing enter prints out the selected entry.

This might not seem like much at first, but consider that anything can be piped into fzf, and its output can also be piped into any other command.
We’ll see this in action with some useful aliases. For now, adding these env vars to your shell config will make it behave more intuitively:

sh
export FZF_DEFAULT_COMMAND='rg --files -g "!.git" --hidden' # show hidden files but still ignore .git/ dir
# export FZF_DEFAULT_COMMAND='find . -type f -not -path "./.git/*"' # if not using ripgrep
export FZF_DEFAULT_OPTS='--height 70% --layout=reverse --border' # "better" (?) layout

bat and eza

bat is a prettier cat with git integration, while eza is a prettier ls with icons.
Plain and simple.

bat

File management

mmv allows you to do bulk rename of files:

mmv

More broadly, consider installing a terminal file manager like yazi, ranger, or even good old vifm, especially if you often have to fiddle around with the file system.

Utilities

As far as system monitoring goes, you’d be hard-pressed to find a better solution than btop.

btop

If your struggle to handle git though the command line, lazygit might help.

lazygit

Same goes for docker and lazydocker.

lazydocker

Aliases

Having gone through those tools, you can see how I cope with my crippling allergy to unnecessary typing:

sh
alias cat="bat"
alias cb="cd .."
alias cc="z"
alias cl="clear"
alias cpd="cp -ir"
alias fm="yazi"
alias l="eza --group --all --icons --long"
alias mkdir="mkdir -p"
alias rmd="rm -rf"
alias sctl="sudo systemctl"
alias tre="eza --all --icons --group-directories-first --tree --git-ignore"
# maybe add `--level=2` to reduce output

Which especially applies to git:

sh
alias ga="git add -A"
alias gamen="git commit --amend"
alias gc="git commit"
alias gcempty="git commit --allow-empty --allow-empty-message"
alias gcom="git add -A && git commit"
alias gfs="git fetch && git status"
alias glg='git log --graph --abbrev-commit --decorate --format=tformat:"%C(yellow)%h%C(reset)%C(reset)%C(auto)%d%C(reset) %s %C(white)%C(bold green)(%ar)%C(reset) %C(dim blue)<%an>%C(reset)" -15'
alias gmkb="git checkout -b"
alias gmv="git checkout"
alias gp="git pull"
alias gpush='git push'
alias gpushf='git push --force'
alias grmb="git branch -D"
alias gs="git status"

Aliases work as long as they are self-contained, but you can’t be specific with the arguments.
For more complex use cases, we can use functions instead, which for this particular use case will act just like aliases.

Fancy funcs

How often do you create a directory only to then have to cd into it?

sh
mkd() { mkdir -p "$1" && cd "$1" }

Or cd into a directory and instantly run ls?

sh
c() { cd "$1" && ls }

Adding this to your zsh config will allow you to run these functions as if they were built-in commands.

You can fuzzy find files in the current directory and open them with your favorite text editor in one go:

sh
vo() { file="$(fzf)" && nvim "$file" }

Or fuzzy find your zsh history to look for that command you barely remember:

sh
hist() {
eval "$(fc -l -1 0 | awk '{$1=""; print substr($0,2)}' | awk '!seen[$0]++' | fzf)"
}

How about installing packages? Package managers are awesome, but you need to know the exact name of what you’re after.
We can use fzf to make our lives much easier:

sh
# Arch
install() {
package=$(paru -Slq | fzf --preview 'paru -Si {1}') && paru -S --skipreview "$package"
}
# Debian
install() {
package=$(apt-cache pkgnames | fzf --preview 'apt-cache show {1}') && sudo apt install -y "$package"
}

This will present all available packages in fzf for you to fuzzy find the one you need. Pressing enter on the result will install the package.

Similarly, we can list all installed packages in fzf for easy removal:

sh
# Arch
remove() {
package=$(paru -Qq | fzf --preview 'paru -Qi {1}') && paru -Rns --noconfirm "$package"
}
# Debian
remove() {
package=$(dpkg --get-selections | awk '{print $1}' | fzf --preview 'apt-cache show {1}') && sudo apt remove --purge "$package"
}

If fills me with no joy to admit that more often than I’d like, I need to debug tests or commands that only fail sometimes.
This is always a pain in the ass, but something like this can make it easier:

sh
loop() {
local cmd=("${@:2}")
for i in {1.."$1"}; do
eval "$cmd"
if [[ $? != 0 ]]; then
echo "\nCommand failed on run $i"
return 1
fi
done
echo "\nCommand never failed"
}

Now, I can run loop 5 make flaky_tests once and have make flaky_tests run 5 (or 5000) times, reporting the failed iteration if present.

You can see how this can get out of hand fast.
Beware of complexity!

Global aliases

Some pipes and/or redirections are used quite often, but aliasing them won’t work as you’d expect.

You can use -g to declare them as global aliases (as in they can be placed anywhere in the command, not just the beginning):

sh
alias -g C="| xclip -sel clipboard" # or wl-copy, pbcopy, ...
alias -g NOER="2> /dev/null"
alias -g NOOUT="> /dev/null 2>&1"
alias -g S="| sort"
alias -g SU="| sort -u"

Now I don’t need to remember how to send the output to my clipboard.

Shell setup

There are some other minor tweaks you can do to your shell config to make it nicer.
For starters, chose a text editor and set it as default, so you don’t unexpectedly get thrown into an unfamiliar environment:

sh
export EDITOR=nvim

Explicitly set your desired terminal emulator so things don’t open in some odd default terminal:

sh
export TERMINAL=kitty
export TERM=kitty

Don’t ask why you need to set it twice…

Also, you can ensure the command history behaves sensibly:

sh
# Number of commands to store in memory
export HISTSIZE=10000
# Number of commands to store in disk
export SAVEHIST=10000
# Ignore duplicated commands during session
setopt HIST_IGNORE_ALL_DUPS
# Ignore duplicated commands when saving to hist file
setopt HIST_SAVE_NO_DUPS
# Append commands to history file instead of overwriting it
setopt append_history
# Append commands to history file as soon as they are run (instead of when the session ends)
setopt inc_append_history

Key bindings

With zsh, we can assign any function to a key combination by first turning it into a widget:

sh
custom_func() {
echo "Hello from custom function!"
}
zle -N custom_func
bindkey '^H' custom_func

Here we use zle -N to register a function as a widget, and bindkey to assign it to Ctrl+H.
A widget is, keeping it simple, any ZLE (Zsh Line Editor) compatible command, which is what bindkey expects.

There’s much more you can do with widgets (this is a great place to fall down a rabbit hole!) and zsh comes with a bunch of useful ones built-in.

There are two in particular that I find super useful when going up and down the command history: up-line-or-beginning-search and down-line-or-beginning-search.

By default, pressing the up and down arrow keys allows you to scroll up/down the command history.
These two widgets will scroll based on what you already typed.
So if I type e and then the up arrow, only commands starting with e will be shown (so echo would appear but ls would be ignored).

These need to be loaded in memory before being registered, which then allows you to bind them:

sh
autoload -U up-line-or-beginning-search down-line-or-beginning-search
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
bindkey $key[Up] up-line-or-beginning-search
bindkey $key[Down] down-line-or-beginning-search

This binding syntax is slightly different from the previous one.
Special keys (arrows, backspace, tab, delete, etc.) are handled this way.

Hopefully this helps you enjoy your time in the command line a bit more!


Other posts you might like