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:
- Its tab completion is much better than
bash
- It’s
bash
compatible (unlike some other shells) - It’s designed to be extensible via plugins
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:
some --cool command | xclip -sel clipboard # or wl-copy, pbcopy, ...
Or pipe the content of your clipboard into a command:
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:
!! # 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:
echo one && echo two # runs the second command only if the first one succeedsecho 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 cd
ing to directories like these:
cd ~/Documents/repos/work/legacycd ~/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:
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:
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 ripgrepexport 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.
File management
mmv
allows you to do bulk rename of files:
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
.
If your struggle to handle git though the command line, lazygit
might help.
Same goes for docker and lazydocker
.
Aliases
Having gone through those tools, you can see how I cope with my crippling allergy to unnecessary typing:
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
:
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?
mkd() { mkdir -p "$1" && cd "$1" }
Or cd
into a directory and instantly run ls
?
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:
vo() { file="$(fzf)" && nvim "$file" }
Or fuzzy find your zsh
history to look for that command you barely remember:
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:
# Archinstall() { package=$(paru -Slq | fzf --preview 'paru -Si {1}') && paru -S --skipreview "$package"}
# Debianinstall() { 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:
# Archremove() { package=$(paru -Qq | fzf --preview 'paru -Qi {1}') && paru -Rns --noconfirm "$package"}
# Debianremove() { 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:
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):
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:
export EDITOR=nvim
Explicitly set your desired terminal emulator so things don’t open in some odd default terminal:
export TERMINAL=kittyexport TERM=kitty
Don’t ask why you need to set it twice…
Also, you can ensure the command history behaves sensibly:
# Number of commands to store in memoryexport HISTSIZE=10000# Number of commands to store in diskexport SAVEHIST=10000# Ignore duplicated commands during sessionsetopt HIST_IGNORE_ALL_DUPS# Ignore duplicated commands when saving to hist filesetopt HIST_SAVE_NO_DUPS# Append commands to history file instead of overwriting itsetopt 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:
custom_func() { echo "Hello from custom function!"}zle -N custom_funcbindkey '^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:
autoload -U up-line-or-beginning-search down-line-or-beginning-searchzle -N up-line-or-beginning-searchzle -N down-line-or-beginning-searchbindkey $key[Up] up-line-or-beginning-searchbindkey $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!