My Favourite Terminal Workflow Tricks
October 29, 2022
The following are my 4 favourite terminal scripts and shortcuts that I have found useful for optimizing processes and procedures that arise frequently during my day job as a Machine Learning Engineer. These tricks are mostly applicable for zsh / tmux users although the Command line copier is more general. Accusations of over optimization are definitely valid! I have included all the required code along with links to the ‘live’ versions that live in my dotfiles repo on github.
Command line copier
Starting with my favourite of the list, this script streamlines the process of copying text from the terminal. It uses the osc52 escape sequence which is supported by most terminals (I use iterm) and allows you to copy text from anywhere, including from remote ssh sessions. This script is quite simple, it reads from stdin (or any number of files given as arguments), wraps the received text contents in the osc52 sequence and then prints it back to the terminal with the result that the text contents is copied to your local clipboard. There exists an excellent vim plugin that I use vim-oscyank which does a similar thing but for yanking inside vim.
#!/bin/bash
# yk (yank) script - copy stdin to clipboard using osc52 escape sequence
set -euo pipefail
function print_OSC52() {
esc="\033]52;c;$( printf %s "$1" | head -c $maxlen | base64 | tr -d '\r\n')\a"
printf $esc
}
buf=$(cat "$@")
buf_len=${#buf}
maxlen=74994
if [[ $buf_len -gt $maxlen ]]; then
>&2 echo "Input length ($buf_len) longer than max length ($maxlen), \
output will be truncated"
fi
print_OSC52 "$buf"
>&2 echo "Copied $buf_len characters to clipboard"
Note to get this to work inside tmux you need to include set -g set-clipboard on
in your tmux.conf
, which allows tmux to use your terminal clipboard. I have this script named yk
visible in my $PATH
so it can be used as follows:
echo "why hello there" | yk
# cmd-v / ctrl-v / tmux paste results in:
why hello there
# Alternatively
yk file1 file2 file3
Perhaps the most useful application of this script is to augment the readlink
command which I very frequently use to get path names (e.g. to use as an argument to a script). I already have the alias rl="readlink -f"
to get path names, but by including the script below as rlc
in my $PATH
I can now automatically copy the path to my clipboard with the command rlc
.
#!/bin/bash
# rlc script - like readlink -f but copy the result to the clipboard
set -euo pipefail
path=$(readlink -f $@)
echo $path | yk
echo $path
As I use vi mode in zsh and I also use this script such that if I yank something in visual mode it also ends up on my local system clipboard.
# Activate vim mode.
bindkey -v
# Yank to the system clipboard
function vi-yank-yk {
zle vi-yank
echo "$CUTBUFFER" | yk 2> /dev/null
}
zle -N vi-yank-yk
bindkey -M vicmd 'y' vi-yank-yk
Fzf vim opener
I am a big fan of fzf and use it various ways throughout my workflow. In fact both this and the next trick utilize fzf!
Fzf comes with the default binding ctrl-t
which enables you to complete a command with a path by fuzzy searching the current directory and children. One thing that I want to do often is open a single file in a repository, make some edits and save. This keybinding helps speed up that process by using a modified version of fzf’s ctrl-T binding to open a file with vim. I have this command bound to ctrl-v
in my .zshrc
and is pretty much the only way I open files with vim.
# .zshrc example
function __fsel_files() {
setopt localoptions pipefail no_aliases 2> /dev/null
eval find ./ -type f -print | fzf -m "$@" | while read item; do
echo -n "${(q)item} "
done
local ret=$?
echo
return $ret
}
function fzf-vim {
selected=$(__fsel_files)
if [[ -z "$selected" ]]; then
zle redisplay
return 0
fi
zle push-line # Clear buffer
BUFFER="nvim $selected";
zle accept-line
}
zle -N fzf-vim
bindkey "^v" fzf-vim
This shortcut works in two parts, first a combination of the find
command and fzf is used to select a file path. Then nvim <selected filepath>
is inserted into your terminal buffer (as if you had typed it in) and that line is accepted (equivalent to pressing enter on it). The main benefit of setting this up as a zle widget and explicitly writing to your terminal buffer is that the command and the selected filename are then stored in your history which you wouldn’t get if you were to simply use vim $(fzf)
.
Example after pressing ctrl-v
in my terminal:
I’ve included my default fzf options below to show how this prompt is achieved.
# in .zshrc
export FZF_DEFAULT_OPTS='--color=16,bg:-1,bg+:15,hl:4,hl+:4,fg:-1,fg+:-1,gutter:-1,pointer:-1,marker:-1,prompt:1 --height 60% --reverse --color border:46 --border=sharp --prompt="➤ " --pointer="➤ " --marker="➤ "'
FZF tmux session switcher
This one is heavily inspired by ThePrimageons “tmux sessionizer”. I use tmux heavily for work and I like to use tmux ‘sessions’ to isolate different projects that I am concurrently working on. I use git worktrees to work on multiple branches of our main repo at once and this fits nicely with that pattern.
E.g. using this script I can get a list of possible tmux sessions based on directories in my ~git/
directory (and any others I want to include), selecting one will make a new tmux session at that directory with an informative session name. Below is an example of the end result.
I have this script named tsesh
also on my $PATH
. If I call tsesh
with no arguments it will open the fzf window shown above (including the --popup
flag will open the fzf inside a tmux popup window). I have this bound to <leader>tab
in my tmux.conf, and to ctrl-a
in my .zshrc
(ctrl-a
is my tmux leader when inside tmux)
# In tmux.conf
bind Tab run-shell "tsesh --popup"
# In .zshrc
bindkey -s '^a' "tsesh\n"
Full script below with an explanation of the key functions after:
#!/bin/bash
USAGE=$(cat <<-END
Usage: tsesh <PATH>
Tmux session switcher, if a PATH argument is provided will either switch to or
start a new session named after the basename of that path and in that location.
If no PATH is provided a fzf window will be presented from common options.
OPTIONS:
--popup [use if called from a tmux popup window, changes some fzf options]
END
)
POSITIONAL_ARGS=()
while (( "$#" )); do
case "$1" in
-h|--help)
echo "$USAGE" && exit 1 ;;
--popup)
POPUP=true && shift ;;
--) # end argument parsing
shift && break ;;
-*|--*=) # unsupported flags
echo "Error: Unsupported flag $1" >&2 && exit 1 ;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift ;;
esac
done
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
# Is tmux running and arje we inside a tmux session
is_running=$(ps aux | grep '[t]mux new-session')
if tmux info &> /dev/null; then
is_inside="true"
fi
# Selection using fzf ---
dir2name(){
# Set of rules to transform directories into sesh_names
name="$1"
sesh_name=$(basename "$name")
if [[ $name == *"/git/aladdin/"* ]] ; then
sesh_name="aladdin-$sesh_name"
fi
echo "$sesh_name"
}
find_directories(){
# Responsible for chosing which directories to display
possible_options="$HOME/git"
if [ $LOC == "remote" ]; then
possible_options+=" $HOME/git/aladdin"
fi
find $possible_options -mindepth 1 -maxdepth 1 -type d -not -path '*/.*';
}
tabulate() {
# Reads a list of directories from find command and processes them to
# a table for fzf to display, it also prepends all the running tmux sessions
# to the list so they can also be selected, outputs a colored table
if [[ $is_inside == "true" ]]; then
current_session=$(tmux display-message -p '#S')
fi
printf "\x1b[1mSession Directory Status\x1b[0m\n" #Headings
{
for name in $(tmux list-sessions | awk '{print $1}' | sed 's/:$//'); do
status="\x1b[32mrunning\x1b[0m"
line="-"
if [[ ! $name == $current_session ]]; then
printf "\x1b[33m$name \x1b[34m$line\x1b[0m $status\n"
fi
done
while IFS=$'\n' read -r line; do
name="$(dir2name $line)"
if ! tmux has-session -t="$name" 2> /dev/null ; then
# Don't process running ones
status="create?"
printf "\x1b[33m$name \x1b[34m$line\x1b[0m $status\n"
fi
done | sort -k1,1
}
}
# This section gets the selected and sesh_name from cmd line of from fzf
# if no cmd line args are provided
if [[ $# -eq 1 ]]; then
selected=$1
sesh_name=$(basename $selected)
if [[ ! -d $selected ]]; then
selected=$HOME
fi
else
fzf_bin="fzf"
fzf_args="--header-lines=1 --ansi"
if [[ ! -z $POPUP && $TERM_PROGRAM == "tmux" ]] ; then
fzf_bin="fzf-tmux -p 50%,50%"
fzf_args+=" --border=none --height=100%"
fi
selected=$(find_directories | tabulate | column -t -s' '\
| sed '1s/^/ /' | $fzf_bin $fzf_args )
sesh_name=$(echo $selected | awk '{ print $1 }')
selected=$(echo $selected | awk '{ print $2 }')
fi
if [[ -z $selected ]]; then
exit 0
fi
# Session switching ---
# If tmux is not running at all
if [[ -z $TMUX ]] && [[ -z $is_running ]]; then
tmux new-session -s $sesh_name -c $selected
exit 0
fi
# If tmux is running but does not have a session with that name
# create new session but dont attach
if ! tmux has-session -t=$sesh_name 2> /dev/null; then
tmux new-session -ds $sesh_name -c $selected
fi
if [[ $is_inside == true ]]; then
# Switching from one session to another when already inside tmux
tmux switch-client -t $sesh_name
else
# If tmux is running, has a session with that name but we are not currently attached
tmux attach -t $sesh_name
fi
This is quite a long one but there are few key functions:
find_directories
prints out all the possible directories that I can start a tmux session in by default this is just everything in~git/
directory and every thing I have in my~git/aladdin/
directory (which are git worktrees for my repository calledaladdin
). I use the$LOC
environment variable, which is set in my.zshrc
to distinguish my remote and local machine as I have folders on my remote machine I don’t have on my local onedir2name
extracts the session name from the directory name, mostly just the basename of the directory but there is additional logic for my git worktreestabulate
converts the directory names fromfind_directories
into a table that is sent to fzf using thedir2name
function. It also appends any running tmux sessions (e.g. that might have been created manually)- The final section ensures the script works wherever you are calling it from and if tmux is already running or not.
Custom completions
Zsh allows you to add custom completions to functions and aliases which I have found to be very effective at streamlining my workflow. The following discusses a particular example of using custom completions I have found to be useful.
All our compute jobs are based around a queue system (we use SGE 😩) the up shot of this is that many times throughout the day I run commands that take job ids as inputs. E.g. qtail
will tail -f
the log file of a job whilst qdel
will delete a job. The standard workflow to run any of these would be to first list all running jobs, then find the job id we want to issue the command to, then copy it and the run the relevant command. By adding completions to these queue commands that whole process can be reduced to just one step.
#compdef qstat
# AUTOCOMPLETION FOR ZSH
# Reference: https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html
local -a opts
list="$(/usr/bin/qstat | tail -n +3 | awk '{print $1}' | uniq)"
setopt shwordsplit
for item in $list; do
# Strip the description of the leading /exp/edwardr/ as
# all of my jobs have that
description=${$(qlog $item)#"/exp/edwardr/"}
opts+=("$item:$description")
done
_describe 'command' opts
The snippet above adds completion to the qstat
function (which many of the queue commands are based on). The first step of this process is getting a list of job ids by running qstat
itself. Then for each job id I also collect the path to the log file (with the qlog
command) enabling me to identify what the job is running.
The _describe
function is provided by zsh and takes the opts
argument which is an array of possible completions. The format $item:$description
means that $item
is the thing to be completed and $description
provides extra info, in this case the log path so that I can identify the job. You could add any potential description here that you wanted. An example of tab completing the qtail
function is shown below:
With this setup I save the the compdef snippet above into a file called _qstat
(name of command to be completed) in a directory called completions
which I then add to my fpath
(where ZSH searches for completions) by including fpath=(<path to>/completions $fpath)
in my .zshrc
. Any other commands that I want the same completions for I can add by using the builtin zsh compdef
function.
# The functions below are defined in my .zshrc but here I set
# them to use the same completions as qstat defined above
compdef qlog=qstat
compdef qcat=qstat
compdef qexp=qstat
compdef qrecycle=qstat
compdef qtail=qstat
That’s a wrap! I hope you found this list useful or interesting and hope that it might give some inspiration to fellow over-optimizers on how to tinker your setup for maximum speed!