Shadowing commands
I'm going to show you how to shadow a "real" application so you can bolt things on after the fact.
I'm going to use shell functions to do it.
Functions in the command line are like more powerful aliases.
bUt YoU dOnT lIke aLiAsEs
Yeah. I posted about that. I know.
Everything I said there stands.
We're not adding a subcommand here this time, or forcing default flags. We're going to be overriding one specific scenario, which is when the user types ssh
on its own. Normally ssh
would give you its usage message, and we don't care about that too much. ssh
will never be run without arguments in a script, so there's no worry we'll mess up between environments, and the user can still see the usage information if they mess up, or explicitly by running command ssh
.
This is basically monkey patching as used by such peers as Ruby On Rails, and 1990s virus authors.
By your command
So how can you tell if a command is shadowed? Can you run the "real" command?
You can use the command -v <command>
command on the command to see what your command is. And you can command your shell to use the "real" command by prefixing it with command
, like, command <command>
.1
For example, command -v ssh
will show you what's going to run when you type ssh
into the shell. I'll use it in a minute to stop my script in its tracks in the case where fzf
isn't available on the system.
Ok, I'm sold, what are we going to add?
A fuzzy host matcher2 that gets a friendly description of the host from comments in your SSH config file.
So you'll need fzf installed for this example to do anything interesting.
Important bits of the function
It's not massively important how this thing works, except for checking whether any arguments have been passed and falling back to the "real" command if we don't need to call our code.
# If any arguments were provided, use the original ssh command instead.
if [ $# -ne 0 ]; then
command ssh "$@"
return
fi
return
on its own will return the last exit code, so for example, if someone runs ssh -plop
, it will return 255 because lop
isn't a port number.
Note that we're using return
rather than exit
here, because otherwise we'd immediately log the user out and that's probably not what they're expecting!
If you're confused by exit
vs. return
, check my "sourcery" post mentioned later.
The full function
ssh() {
# Path to your SSH config file in case it's different.
local SSH_CONFIG="$HOME/.ssh/config"
local RESET=$(tput sgr0)
local BOLD=$(tput bold)
local CYAN=$(tput setaf 6)
# If any arguments were provided, use the original ssh command instead.
if [ $# -ne 0 ]; then
command ssh "$@"
# "return" on its own will pass the last exit code.
return
fi
if ! command -v fzf > /dev/null; then
printf "Shadowed ssh command will not run without fzf installed.\n" >&2
return 1
fi
if [ ! -f "$SSH_CONFIG" ]; then
printf "SSH config file not found at %s\n" "$SSH_CONFIG" >&2
return 1
fi
local hosts_with_descriptions=$(awk -v bold="$BOLD" -v cyan="$CYAN" -v reset="$RESET" '
BEGIN { max_host_length = 0 }
/^#/ {
desc = substr($0, 2) # Remove the # from the comment
gsub(/^[[:space:]]+|[[:space:]]+$/, "", desc) # Trim whitespace
}
/^Host / {
host = $2
if (length(host) > max_host_length) max_host_length = length(host)
if (desc == "") desc = "No description"
hosts[host] = desc
desc = ""
}
END {
for (host in hosts) {
printf "%s%-*s%s │ %s%s%s\n", bold, max_host_length, host, reset, cyan, hosts[host], reset
}
}
' "$SSH_CONFIG" | sort)
local selected_line=$(echo "$hosts_with_descriptions" | fzf --height 40% --reverse --prompt="Select SSH host: " --ansi)
local selected_host=$(echo "$selected_line" | awk '{print $1}' | sed "s/\x1B\[[0-9;]*[mK]//g")
if [ -n "$selected_host" ]; then
echo "Connecting to $selected_host..."
command ssh -F "$SSH_CONFIG" "$selected_host"
else
echo "No host selected."
return 0
fi
}
Whoa, you say. What's that crazy awk
script in the middle doing there? Well I'll tell you what it's doing there. I cba to do that bit myself so I got AI to do it for me. I know, I know, I'm a bad person. It's not relevant to this blog post though.
And why am I explicitly passing a config file path to ssh
? Because you might not keep it in ~/.ssh/config
. I don't, and as for why, well, I'll probably write a post about that later.
Hook me up
You'll need to load this function before you can use it. That's typically when you start your shell, so you could paste it into your .zshrc
or .bashrc
, or whatever. You can't run it as a standalone script by adding a shebang, though. I wrote about that, too:
(Hacker voice) I'm in
Yeah, you are!
Cover image by Photo by Marco Bianchetti on Unsplash
Top comments (1)
I'm not clever enough to understand this post but my take away is "it's possible to have multi-part posts on dev.to", which is great news and new to me.