DEV Community

Cover image for Fuzzy business: shadowing ssh
Ben Sinclair
Ben Sinclair

Posted on

Fuzzy business: shadowing ssh

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.

The fuzzy matches from my SSH config with the term 'government' entered as an example. The results are unimportant.

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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


  1. COMMAAAAAAAAND. 

  2. Yes, I'm aware you can use some kind of autocomplete in zsh with a combination of asterisks and tabs and bears. 

Top comments (1)

Collapse
 
rossangus profile image
Ross Angus

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.