Docu review done: Fri 26 Jan 2024 04:38:40 PM CET

Table of content

General

This is an extention of ssht which you might have seen already in this documentation.

If not, it is worth to have a look at it ;)

This (sshc) allows you to kind of the same what cssh does but in tmux. Why kind of, well we just started the developing on it, so a bunch of things are still not working a 100% ;)

If you are using zsh, we found out, that the ordering of the parameters is now allowing any kind of order. So if you want to run e.g. only a bash without any switch user (-N -s '/bin/bash' -S <server1> <server2> ...) you have to perform it in exactly this order, otherwiese it will not work out.

By default, when tmux starts, the synchronize-panes is set to off. But you can use the keybindiung <prefix> + _ (or to what ever you defined it with parameter -z to togle it

Parameters

ParameterDescription
-l [ssh username]Defines the username which is used for ssh connection like ssh -l (default: whoami)
-L [tmux layoutname]Defines the layout used by tmux (default: main-vertical)
-s [switch user command]Defines switch user command like su - or sudo su - or sudo -i … (default: su -)
-S [list of servers]After -S we expect a list of servers, just write it like this (then tabcomletion works as well) -S host1 host2 host3
-NDisables the password fetching, can be usefull if you have NOPASSWD (not recomended) in your sudors conf
-P [absolut path to pwd bin]Defines the path to the bin/scirpt for fetching the pwd e.g. with vault -P vault read
-r [resize value]Defines resize used for main-vertical and main-horizontal (default: 30)
-z [letter]Defines key for toggle sync panes (default: prefix>+ _)
*This is a placeholder for hostnames, if you dont specify any parameter and only add hostnames it will work as well.
-h:help! if you konw what I mean

Requirements

To make use of sshc, you some applications or functions are needed in your system

  • tmux
  • Password store with CLI access e.g. pass
  • Being able to generate a list of hosts you want to ssh and fetch passwords e.g. your ~/.ssh/config

Installation

The installation is as simple as it can be (expected that you have tmux, pwd store and your host list already in place.

Go to a place on your system where you store your .rc files which get sourced by your shell and either add there a new file or copy/past the below functions into an existing one.

The function below expects that your ssh host list comes from ~/.ssh/config, if you dont have it like that, just search for it and replace it with the commands you need.

############################################
#
# TMUX ssh root login
#

function _tmux_login_usage() {
    echo "-l <local tmux socket name>   # as it says"
    echo "-t <tmux connection string>   # <session>:<window>(.<pane>)"
    echo "-s <ssh remote server>        # as it says"
    echo "-p <match string>             # string that is shown when requesting pwd (default: 'Password:')"
    echo "-P <absolut path to pwd bin>  # defines the absolutpath to the binary for fetching the pwd"
    echo "-h                            # hows this help"
    return 64
}

function tmux_login() {
    local optstring="l:t:s:p:P:h"
    tmux_socket_name=""
    tmux_connect=""
    ssh_server_name=""
    pwd_question_matcher="Password:"
    password_bin=""

    while getopts ${optstring} c; do
        case ${c} in
            l)  tmux_socket_name="${OPTARG}" ;;
            t)  tmux_connect="${OPTARG}" ;;
            s)  ssh_server_name="${OPTARG}" ;;
            p)  pwd_question_matcher="${OPTARG}" ;;
            P)  password_bin="${OPTARG}" ;;
            h)  _tmux_login_usage ; return 64 ;;
            *)  _tmux_login_usage ; return 64 ;;
        esac
    done

    local pw
    if pw=$("${password_bin}" "${ssh_server_name}") ; then
        while_counter=20
        while [[ $(tmux -L "${tmux_socket_name}" capture-pane -p -t "${tmux_connect}" | grep -v "^$" | tail -n 1) != "${pwd_question_matcher}" ]]; do
            if (( while_counter > 0 )); then
                sleep 0.3
                let while_counter-=1
            else
                return 1
            fi
        done
        &>/dev/null tmux -L "${tmux_socket_name}" send-keys -t "${tmux_connect}" "$pw" C-m
        &>/dev/null tmux -L "${tmux_socket_name}" send-keys -t "${tmux_connect}" "printf '\033]2;${ssh_server_name}\033\\' ; clear" C-m
    else
        echo "failed to get pwd for ${ssh_server_name}"
    fi
}

function _sshc_compl_zsh() {
    ssh_conf_hosts="$(sed -E 's/\*//g;s/ /:ssh hosts;/g' <<< ${=${${${${${(f)"$(cat ~/.ssh/config(n) /dev/null)"}##[A-Z][a-z]* }##  *[A-Z][a-z]* *}%%[# ]*}%%--*}//,/ }):ssh hosts"
    parameter_compl=('-l:ssh user name' '-s:specify switch user command' '-S:specify ssh target hosts (only needed if parameter are used)' '-N:disable password parsing' '-P:specify the absolutpath to bin for fetching pwds' '-h:shows help')
    hosts_compl=(${(@s:;:)ssh_conf_hosts})
    _describe '_sshc' parameter_compl -- hosts_compl
}

function _ssht_compl_bash() {
    if [ "${#COMP_WORDS[@]}" != "2" ]; then
        return
    fi

    local IFS=$'\n'
    local suggestions=($(compgen -W "$(sed -E '/^Host +[a-zA-Z0-9]/!d;s/Host //g' ~/.ssh/config | sort -u)" -- "${COMP_WORDS[1]}"))

    if [ "${#suggestions[@]}" == "1" ]; then
        local onlyonesuggestion="${suggestions[0]/%\ */}"
        COMPREPLY=("${onlyonesuggestion}")
    else
        for i in "${!suggestions[@]}"; do
            suggestions[$i]="$(printf '%*s' "-$COLUMNS"  "${suggestions[$i]}")"
        done

        COMPREPLY=("${suggestions[@]}")
    fi
}

function _sshc_usage() {
    echo "-l <ssh username>             # default: $(whoami)"
    echo "-L <tmux layoutname>          # specify starting layout of panes (default: main-vertical)"
    echo "-s <switch user command>      # default: 'su -'"
    echo "-S <list of servers>          # -S server1 server2 ..."
    echo "-N                            # set if no pwd is needed"
    echo "-P <absolut path to pwd bin>  # defines the absolutpath to the binary for fetching the pwd"
    echo "-r <resice value>             # defines resize used for main-vertical and main-horizontal (default: 30)"
    echo "-z <letter>                   # defines key for toggle sync panes (default: <prefix> + '_')"
    echo "*                             # hostnames for remote servers after -S"
    echo ""
    echo "If no parameter is used, function assumes all arguments are hostnames"
    echo ""
    echo "-h                            # hows this help"
    return 64
}

function _sshc() {
    ssh_user=$(whoami)
    su_command="su -"
    local optstring="l:L:s:S:P:z:hN"
    val_args=$(sed -E 's/://g' <<<"${optstring}")
    servers=""
    fetch_pwd=true
    password_bin="<DEFAULT PWD FETCH COMMAND>"
    tmux_layout="main-vertical"
    tmux_pane_resize_r="30"
    tmux_sync_toggle_key="_"
    window_bg_colour_inact='colour236'
    window_bg_colour_act='black'
    pane_border_fg_colour_act='colour51'
    tmux_socket_name="sshc_root_sessions"

    if grep -q -E "^-[${val_args}]  *[a-zA-Z0-9_-]|^-h *$" <<<"${@}" ; then
        while getopts ${optstring} c; do
            case ${c} in
                l)  ssh_user="${OPTARG}" ;;
                L)  tmux_layout="${OPTARG}" ;;
                s)  su_command="${OPTARG}" ;;
                S)  servers=$(sed -E 's/^.* -S ([a-zA-Z0-9_ .-]+)( -.*){,1}/\1/g;s/ -.*//g' <<<"${@}") ;;
                N)  fetch_pwd=false ;;
                P)  password_bin="${OPTARG}" ;;
                r)  tmux_pane_resize_r="${OPTARG}" ;;
                z)  tmux_sync_toggle_key="${OPTARG}" ;;
                h)  _sshc_usage ; return 64 ;;
                *)  _sshc_usage ; return 64 ;;
            esac
        done
    else
        servers="${@}"
    fi

    if ( $fetch_pwd ) ; then
        if ! [ -x "${password_bin}" ]; then
            echo "${password_bin} is not executeable for your use or does not exist"
            return 1
        fi
    fi

    server_first=$(cut -f1 -d\ <<<"${servers}")
    servers_ex_first=$(sed -e "s/^${server_first} //g" <<<"${servers}")
    session_name="c_$(( ( RANDOM % 9999 ) + 1))"
    window_name="$(( ( RANDOM % 9999 )  + 1 ))-$(sed -E 's/\./_/g' <<<"${servers}")"
    pane_counter=0

    if ( $fetch_pwd ) ; then
        ( &>/dev/null tmux_login -s "${server_first}" -t "${session_name}:${window_name}.${pane_counter}" -l "${tmux_socket_name}" -P "${password_bin}" & )
    fi

    &>/dev/null tmux -L "${tmux_socket_name}" new -s "${session_name}" -n "${window_name}" -d "TERM=rxvt ssh -t -l ${ssh_user} ${server_first} \"${su_command}\""
    &>/dev/null tmux -L "${tmux_socket_name}" select-layout -t "${session_name}:${window_name}" "tiled"
    &>/dev/null tmux -L "${tmux_socket_name}" set window-style "bg=${window_bg_colour_inact}"
    &>/dev/null tmux -L "${tmux_socket_name}" set window-active-style "bg=${window_bg_colour_act}"
    &>/dev/null tmux -L "${tmux_socket_name}" set pane-active-border-style "fg=${pane_border_fg_colour_act}"
    &>/dev/null tmux -L "${tmux_socket_name}" bind-key $tmux_sync_toggle_key set-window-option synchronize-panes
    &>/dev/null tmux -L "${tmux_socket_name}" setw window-status-current-format '#{?pane_synchronized,#[bg=red],}#I:#W'
    &>/dev/null tmux -L "${tmux_socket_name}" setw window-status-format '#{?pane_synchronized,#[bg=red],}#I:#W'

    if ! ( $fetch_pwd ) ; then
        ( &>/dev/null tmux -L "${tmux_socket_name}" send-keys -t "${session_name}:${window_name}" "printf '\033]2;${server_first}\033\\' ; clear" C-m &)
    fi

    pane_counter=1

    for server in $(echo $servers_ex_first); do
        &>/dev/null tmux -L "${tmux_socket_name}" select-layout -t "${session_name}:${window_name}" "tiled"

        if ( $fetch_pwd ) ; then
            ( &>/dev/null tmux_login -s "${server}" -t "${session_name}:${window_name}.${pane_counter}" -l "${tmux_socket_name}" -P "${password_bin}" & )
        fi

        &>/dev/null tmux -L "${tmux_socket_name}" split-window -t "${session_name}:${window_name}" "TERM=rxvt ssh -t -l ${ssh_user} ${server} \"${su_command}\""

        if ! ( $fetch_pwd ) ; then
            ( &>/dev/null tmux -L "${tmux_socket_name}" send-keys -t "${session_name}:${window_name}.${pane_counter}" "printf '\033]2;${server}\033\\' ; clear" C-m & )
        fi

        pane_counter=$((pane_counter + 1))
    done

    &>/dev/null tmux -L "${tmux_socket_name}" set -g pane-border-status
    &>/dev/null tmux -L "${tmux_socket_name}" select-layout -t "${session_name}:${window_name}" "${tmux_layout}"
    &>/dev/null tmux -L "${tmux_socket_name}" select-pane -t "${session_name}:${window_name}.0"

    case "${tmux_layout}" in
        "main-vertical"   ) &>/dev/null tmux -L "${tmux_socket_name}" resize-pane -t "${session_name}:${window_name}.0" -R "${tmux_pane_resize_r}" ;;
        "main-horizontal" ) &>/dev/null tmux -L "${tmux_socket_name}" resize-pane -t "${session_name}:${window_name}.0" -D "${tmux_pane_resize_r}" ;;
    esac

    &>/dev/null tmux -L "${tmux_socket_name}" attach -t "${session_name}"
}

current_shell="$(ps -o comm -p $$ | tail -1)"
alias sshc="_sshc"

if [[ "${current_shell}" == "zsh" ]]; then
    compdef _sshc_compl_zsh _sshc
elif [[ "${current_shell}" == "bash" ]]; then
    complete -F _ssht_compl_bash sshc
fi

As an well trained engineer, you saw ofcourse that a different tmux config was used, ~/.tmux_ssh.conf

Why do we load an additional tmux config? Easy to say, this ensures that mod+b gets unset, so if you run tmux on the destination server you will controle the remote one and not yours. It will also sets alertings to inform the shell/terminal about alerts, the status bar will be removed to have the full view of your terminal and the scallback buffer got increased. Also the title is getting enabled, as the above function will replace it so that your terminal title gets the hostname from the server. This can help you for faster switching to the open connection for example.

This is what it contains:

#tmux source-file .tmux_ssh.conf
unbind C-b

#Set alert if something happens
setw -g monitor-activity on
set -g visual-activity on

# scrollback buffer n lines
set -g history-limit 99999

# enable wm window titles
set -g set-titles on

# center align the window list
set -g status off

# Enable mouse mode to avoide scroll back issues
set -g mouse off

Now the time has tome to source it to your .rc file.

Usage

To use it, you just have to run sshc followed either by <tab><tab> or type the full or beginning of the hostname + tab till you have the full name displayed in your shell, or use the parameters which allow you to modify the behafoir of the script.

Guess what, now just hit the enter key ;)

Small difference to ssht is, that it starts in the background (detached) and after all commands have been executed, it will show up and you will see your tmux with splited panes.

We are still working on the bach completion, to get the parameters in there as well

Sorry, we will work on that ;)

And the nice thing is, if you time (does not matter in which pane) it will do the same actions on all the other panes as well, like cssh.

When you close now one the connection, all other will be closed too and it will look like before you perfmored the ssht command. And we are done.

If you want to know how the tab completion works, have a look at

Sample bin bash

sshc

Sample sudo

sshc_sudo