Full Code of vaniacer/sshto for AI

master 1eb51f406049 cached
5 files
37.2 KB
10.8k tokens
1 requests
Download .txt
Repository: vaniacer/sshto
Branch: master
Commit: 1eb51f406049
Files: 5
Total size: 37.2 KB

Directory structure:
gitextract_10rhdvse/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE.md
├── README.md
└── sshto

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: ['paypal.me/sshto']


================================================
FILE: .gitignore
================================================
.idea
/install.sh


================================================
FILE: LICENSE.md
================================================
License: MIT
	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:
	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.
	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.


================================================
FILE: README.md
================================================
# sshto
<a href="https://t.me/sshtobash"><img src="https://telegram.org/img/website_icon.svg" width="21"></a>
[![Twitter Follow](https://img.shields.io/twitter/follow/Vaniacer?style=social)](https://twitter.com/Vaniacer)
[![paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/sshto?locale.x=en_US) <sup>Feel free to support the project!)</sup></br>

Small bash script that builds a menu (via dialog) from your ~/.ssh/config.</br>
![screeenshot](https://user-images.githubusercontent.com/18072680/60570513-69e99f00-9d7a-11e9-916d-48b74fa7585a.png)
</br>
Allows you to connect to your servers or run commands from menu. Available commands:</br>
![cmds](https://user-images.githubusercontent.com/18072680/211161226-1c5eec5a-634b-4902-90cd-5947dd95083e.png)
</br>
Your commands can be easily added to this list. Just edit this part of the script:
<pre>
cmdlist=(
    #Command#    #Description#
    "${slct[@]}" #De/Select command
    "Username"   "Change ssh username to \Z1$GUEST\Z0"
    "Add tab"    "Add terminal tab with \Z1sshto\Z0 for \Z4$target\Z0"
    "Ssh tab"    "Add terminal tab with \Z1ssh\Z0 to \Z4$target\Z0"
    ''           ''
    "ls -lah"    "List Files"
    "free -h"    "Show free memory"
    "df  -ih"    "Show free inodes"
    "df   -h"    "Show free disk space"
    "Custom"     "Run custom command on \Z4$target\Z0"
    "Script"     "Run custom script on \Z4$target\Z0"
    ''           ''
    'Yes'        "Say 'yes' to SSH"
    "Info"       "Full system info"
    'Fix_id'     "Update host in known_hosts"
    "Sshkey"     "Add my ssh key to \Z4$target\Z0"
    "Alias"      "Add my useful aliases to \Z4$target\Z0"
    "Copy"       "Copy selected file or dir to \Z4$target\Z0"
    ''           ''
    "Home"       "Change home folder \Z4$home\Z0 on local server"
    "Dest"       "Change destination folder \Z4$DEST\Z0 on \Z4$target\Z0"
    "Upload"     "Upload file or folder from \Z4$home\Z0 to \Z4$target:${DEST}\Z0"
    "Download"   "Download file or folder from \Z4$target:${DEST}\Z0 to \Z4$home\Z0"
    "Mount"      "Mount remote folder \Z4$target:$DEST\Z0 to \Z4$home\Z0"
    "Unmount"    "Unmount remote folder \Z4$target:$DEST\Z0 from \Z4$home\Z0"
    ''           ''
    "Local"      "Change local  port \Z1$LOCAL\Z0"
    "Remote"     "Change remote port \Z1$REMOTE\Z0"
    "Tunnel"     "Start portunneling from \Z4$target:$REMOTE\Z0 to \Z4localhost:$LOCAL\Z0"
    ''           ''
    "ShowConf"   "Show ssh config for this host"
    "EditConf"   "Edit ssh config for this host"
)
</pre>
First collumn - command, second - description.</br>
Simple commands like `ls -la` could be added as is.</br>
A list of commands or a complicated logic should be added via function.</br>
Empty values(`''`) could be used as a delimiter.</br>
You can quick jump to the selected server via <i>CONNECT</i> button.</br>
To close <i>ssh</i> session press <kbd>CTRL</kbd>+<kbd>D</kbd> or run `exit` command, it'll bring you back to <i>sshto</i> commands section.</br>
</br>
Optional hosts description could be added like this:</br>
<pre>
Host server1 #Description, it could be more than one word
HostName 192.168.0.1
Port 22
User admin
</pre>
Optional start menu delimiters '---{ Group Name }---' could be added like this:</br>
<pre>
#Host DUMMY #Group Name#
</pre>
If you are unhappy with this 'DUMMY' group name template, or you actually have a host named 'dummy',
you can change this template by ajusting this variable `group_id=dummy`. </br>
All these additions won't break your *ssh configs* coz they are considered as comments.  

------
~/.ssh/config example:
<pre>
#Host DUMMY #Rybinsk#

Host rybserver1 #First server
HostName localhost

Host rybserver2 #Second server
HostName localhost

Host rybserver3 #Third server
HostName localhost

#Host DUMMY #Moscow#

Host moserver1 #First server
HostName localhost

Host moserver2 #Second server
HostName localhost

Host moserver3 #Third server
HostName localhost
</pre>
Script greps data from multiple config files via pattername `config*` in `~/.ssh` dir.</br>
So you can split config to multiple files and use them with <i>Include</i> directive, example:
<pre>
Include config_moscow
Include config_rybinsk
Include config*
</pre>
All preset variables and functions could be tweaked via `~/.sshtorc` config file:
<pre>
echo "REMOTE=9000  # Remote port for tunneling." >> ~/.sshtorc
</pre>

You can customize dialog itself a bit by creating and editing its config file:
<pre>
dialog --create-rc ~/.dialogrc
nano ~/.dialogrc
</pre>

If you don't have dialog and don't want(or can't) to install it, there is a dialog-less version of sshto
in my new project [bashui](https://github.com/vaniacer/bashui) here is how it looks:
![bashui-hosts](https://habrastorage.org/getpro/habr/upload_files/024/c74/38e/024c7438e6429f5e37a8a71d98bf7edb.png)

![bashui-commands](https://habrastorage.org/getpro/habr/upload_files/495/2cc/526/4952cc52616db16acfa7b2fd9e8d366f.png)

Try it [bashui-sshto](https://github.com/vaniacer/bashui/blob/master/demo_sshto)!

# How to install
Clone\download this project, go to it's folder and run:
<pre>sudo cp sshto /usr/bin/

#and to unistall
sudo rm /usr/bin/sshto
</pre>

<a href="https://asciinema.org/a/PQMuRvfmxlHUc4oZMN76LY2V4">See how it works at asciinema</a></br>

<a href="http://www.youtube.com/watch?feature=player_embedded&v=FhnsVH8t96Q
" target="_blank"><img src="http://img.youtube.com/vi/FhnsVH8t96Q/0.jpg" 
alt="ssh config guide" width="240" height="180" border="10"/></a></br>
Tom Lawrens video guide about ssh config and sshto

<a href="https://t.me/sshtobash"><img src="https://telegram.org/img/website_icon.svg" width="21"></a>
[![Twitter Follow](https://img.shields.io/twitter/follow/Vaniacer?style=social)](https://twitter.com/Vaniacer)
[![paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/sshto?locale.x=en_US) <sup>Feel free to support the project!)</sup></br>


================================================
FILE: sshto
================================================
#!/usr/bin/env bash
#--------------------------{ Default values }---------------------------------------------------------------------------
     OPT=$1     # Options.
  sshdir=~/.ssh # Ssh configfiles folder
     KEY="$sshdir/id_rsa.pub" # SSH key to use in command Sshkey(Add my ssh key to host).
  REMOTE=8080   # Remote port for tunneling.
   LOCAL=18080  # Local  port for tunneling.
   GUEST=$USER  # Alternative username to login with.
    DEST='~'    # Destination folder on target server to download\upload files and sshfs mount.
    TIME=60     # Timer for tunneling command. Tunnel will be closed after 60 seconds, but it will stay open if used.
  EDITOR=nano   # SSH confile editor.
  LSEXIT=true   # Perform ls on exit true|false.
    home=$PWD   # Destination folder on local  server to download\upload files and sshfs mount.
sshfsopt=       # Sshfs options if needed
group_id=dummy  # Group separator identification template, 'dummy' by default, don't ask why, historically)
rsyncopt='-Pgzortl'            # Upload/download rsync options
knwhosts="$sshdir"/known_hosts # Path to known_hosts file.
 confile=~/.sshtorc            # Path to config file, have to be chmod'ed to 600.
 tmpfile=/tmp/sshtorc-${USER}  # Path to tmp file to save selected context.
sshto_script[0]=~              # Path to sshto script
sshto_script[1]=.sshto_script  # Sshto script filename
sshto_script[2]="${sshto_script[0]}/${sshto_script[1]}" # Sshto script full
#------------------------{ Add some tabs }------------------------------------------------------------------------------
tabbed(){ target=$target gnome-terminal --title=$target --tab -qe "${1/_target_/$target}"; } # Terminal command for tabs

#------------------------{ Add your commands to this lists }------------------------------------------------------------
cmdlist_renew(){
    cmdlist=(
        #Command#    #Description#
        "${slct[@]}" #De/Select command
        "Username"   "Change ssh username to \Z1$GUEST\Z0"
        "Add tab"    "Add terminal tab with \Z1sshto\Z0 for \Z4$target\Z0"
        "Ssh tab"    "Add terminal tab with \Z1ssh\Z0 to \Z4$target\Z0"
        ''           ''
        "ls -lah"    "List Files"
        "free -h"    "Show free memory"
        "df  -ih"    "Show free inodes"
        "df   -h"    "Show free disk space"
        "Custom"     "Run custom command on \Z4$target\Z0"
        "Script"     "Run custom script on \Z4$target\Z0"
        ''           ''
        'Yes'        "Say 'yes' to SSH"
        "Info"       "Full system info"
        'Fix_id'     "Update host in known_hosts"
        "Sshkey"     "Add my ssh key to \Z4$target\Z0"
        "Alias"      "Add my useful aliases to \Z4$target\Z0"
        "Copy"       "Copy selected file or dir to \Z4$target\Z0"
        ''           ''
        "Home"       "Change home folder \Z4$home\Z0 on local server"
        "Dest"       "Change destination folder \Z4$DEST\Z0 on \Z4$target\Z0"
        "Upload"     "Upload file or folder from \Z4$home\Z0 to \Z4$target:${DEST}\Z0"
        "Download"   "Download file or folder from \Z4$target:${DEST}\Z0 to \Z4$home\Z0"
        "Mount"      "Mount remote folder \Z4$target:$DEST\Z0 to \Z4$home\Z0"
        "Unmount"    "Unmount remote folder \Z4$target:$DEST\Z0 from \Z4$home\Z0"
        ''           ''
        "Local"      "Change local  port \Z1$LOCAL\Z0"
        "Remote"     "Change remote port \Z1$REMOTE\Z0"
        "Tunnel"     "Start portunneling from \Z4$target:$REMOTE\Z0 to \Z4localhost:$LOCAL\Z0"
        ''           ''
        "ShowConf"   "Show ssh config for this host"
        "EditConf"   "Edit ssh config for this host"
    )
    cmdlist_group=(
        #Command#    #Description#
        "${slct_grp[@]}" #De/Select command
        "Username"   "Change ssh username to \Z1$GUEST\Z0"
        "Add tabs"   "Add terminal tabs with \Z1sshto\Z0 for hosts in \Z4$group\Z0 group"
        "Ssh tabs"   "Add terminal tabs with \Z1ssh\Z0 to hosts from \Z4$group\Z0 group"
        ''           ''
        "ls  -la"    "List Files"
        "free -h"    "Show free memory"
        "df  -ih"    "Show free inodes"
        "df   -h"    "Show free disk space"
        "Custom"     "Run custom command on \Z4$group\Z0"
        "Script"     "Run custom script on \Z4$group\Z0"
        ''           ''
        'Yes'        "Say 'yes' to SSH"
        'Fix_id'     "Update hosts in known_hosts"
        "Info"       "Full system info"
        "Alias"      "Add my useful aliases to \Z4$group\Z0"
        "Copy"       "Copy selected file or dir to \Z4$group\Z0"
        ''           ''
        "Home"       "Change home folder \Z4$home\Z0 on local server"
        "Dest"       "Change destination folder \Z4$DEST\Z0 on \Z4$group\Z0"
        "Upload"     "Upload file or folder from \Z4$home\Z0 to \Z4$group:${DEST}\Z0"
        ''           ''
        "EditConf"   "Edit ssh config for this group"
    )
}
#--------------------------------------------------------------------+--------------------------------------------------
#Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}"Hello!)"${DEF}|
#-------------------------+--------------------------------+---------+
#       Text color        |       Background color         |         |
#-----------+-------------+--------------+-----------------+         |
# Base color|Lighter shade|  Base color  | Lighter shade   |         |
#-----------+-------------+--------------+-----------------+         |
BLK='\e[30m'; blk='\e[90m'; BBLK='\e[40m'; bblk='\e[100m' #| Black   |
RED='\e[31m'; red='\e[91m'; BRED='\e[41m'; bred='\e[101m' #| Red     |
GRN='\e[32m'; grn='\e[92m'; BGRN='\e[42m'; bgrn='\e[102m' #| Green   |
YLW='\e[33m'; ylw='\e[93m'; BYLW='\e[43m'; bylw='\e[103m' #| Yellow  |
BLU='\e[34m'; blu='\e[94m'; BBLU='\e[44m'; bblu='\e[104m' #| Blue    |
MGN='\e[35m'; mgn='\e[95m'; BMGN='\e[45m'; bmgn='\e[105m' #| Magenta |
CYN='\e[36m'; cyn='\e[96m'; BCYN='\e[46m'; bcyn='\e[106m' #| Cyan    |
WHT='\e[37m'; wht='\e[97m'; BWHT='\e[47m'; bwht='\e[107m' #| White   |
#----------------------------------------------------------+---------+
# Effects                                                            |
#--------------------------------------------------------------------+
DEF='\e[0m'   #Default color and effects                             |
BLD='\e[1m'   #Bold\brighter                                         |
DIM='\e[2m'   #Dim\darker                                            |
CUR='\e[3m'   #Italic font                                           |
UND='\e[4m'   #Underline                                             |
INV='\e[7m'   #Inverted                                              |
COF='\e[?25l' #Cursor Off                                            |
CON='\e[?25h' #Cursor On                                             |
#--------------------------------------------------------------------+
# Text positioning, usage: XY 10 10 'Hello World!'                   |
XY(){ printf "\e[$2;${1}H$3"; }                                     #|
# Print line, usage: line - 10 | line -= 20 | line 'Hello World!' 20 |
line(){ printf -v _L %$2s; printf -- "${_L// /$1}"; }               #|
# Create sequence like {0..(X-1)}, usage: que 10                     |
que(){ printf -v _N %$1s; _N=(${_N// / 1}); printf "${!_N[*]}"; }   #|
#------------{ Check that dialog and gawk are installed }------------+
install_help="
${BLD}sshto$DEF requires that the package '${GRN}%b$DEF' is installed.
Type this into the terminal and press return:

    ${BLD}%b$DEF

Then run ${BLD}sshto$DEF again
"
how_to_install(){
    local package=$1
    which yum     &> /dev/null && installer="yum -y install $package"
    which brew    &> /dev/null && installer="brew install $package"
    which apt-get &> /dev/null && installer="apt-get install -y $package"
    printf -- "$install_help" "$package" "$installer"
    [[ $2 ]] && exit $2
}

for package in dialog gawk; { which $package &> /dev/null || how_to_install $package 1; }
#------------------------{check bash version }--------------------------------------------------------------------------
tversion=4.2
cversion=${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}
gawk '{if($1>$2){exit 1}}' <<< "$tversion $cversion" || {
    printf "\nBASH version ${BLD}$tversion+$DEF required to run ${BLD}sshto$DEF, your is - $BLD$BASH_VERSION$DEF\n"
    exit 1
}
#------------------------{ Waiting animation }--------------------------------------------------------------------------
cursor () {
    case $1 in
         on) stty  echo; printf "$CON";;
        off) stty -echo; printf "$COF";;
    esac
}

   x=$[COLUMNS/2-3]
   y=$[  LINES/2-3]
sand=( ⠁  ⠂  ⠄  ' ' )
#  {   small digits    }
sd=(₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉)
bs='⠴⠷⠦' # bottom sand pile
ts='⠖'    #  top  sand pile
WAIT(){
    clear; cursor off; i=0; start=$SECONDS
    XY $[x-1]  $y    $UND$BLD$RED'       '$DEF                     # _______
    XY $[x-1] $[y+1]         $RED'╲'$DIM$UND'     '$DEF$red'╱'$DEF # ╲_____╱
    XY  $x    $[y+2]         $BLU'(  '$BLD$WHT'•'$BLD$BLU')'$DEF   #  (  •)
    XY  $x    $[y+3]         $BLU' ╲'$YLW"$ts"$BLD$BLU'╱'$DEF      #   ╲⠖╱
    XY  $x    $[y+4]         $BLU" ╱$YLW${sand[$i]}$BLD$BLU╲"$DEF  #   ╱⠂╲
    XY  $x    $[y+5]         $BLU'('$YLW"$bs"$BLD$BLU')'$DEF       #  (⠴⠷⠦)
    XY $[x-1] $[y+6]         $RED'╱'$RED'‾‾‾‾‾'$BLD$RED'╲'$DEF     # ╱‾‾‾‾‾╲
    XY $[x-1] $[y+7]     $DIM$RED'‾‾‾‾‾‾‾'$DEF                     # ‾‾‾‾‾‾‾
    ( while true; do sleep 0.07
        printf -v counter "%03d" $[SECONDS-start]
        small="${sd[${counter:0:1}]}${sd[${counter:1:1}]}${sd[${counter:2:1}]}"
        XY $[x-1] $[y+1] $RED'╲'$DIM$UND" $small "$DEF$red'╱'$DEF
        XY  $x    $[y+4] $BLU" ╱$YLW${sand[$i]}$BLD$BLU╲"$DEF
        ((i++)); (($i==${#sand[@]})) && i=0;
    done ) & waiter=$!
}

GO() { [[ -e /proc/$waiter ]] && kill $waiter; cursor on; clear; }

check_confile(){
    [[ -e $confile  ]] || return
    [[ $(stat -c "%a %U %G" "$confile") == "600 $USER $USER" ]] && . "$confile"
}

#------------------------{ Pause function }-----------------------------------------------------------------------------
pause(){
    local  mess=${1:-'press any key to continue'}
    printf "\n$COF$BLD$mess\n"
    read   -srn1
    printf "\n$DEF$CON"
}

#------------------------{ Yes to ssh }---------------------------------------------------------------------------------
ssh_yes(){
    local hostname=${hostnames["$target"]}
    local fprint=($(ssh-keyscan -H "$hostname" 2>/dev/null))
    grep -q "${fprint[2]}" "$knwhosts" || echo "${fprint[@]}" >> "$knwhosts"
}

fix_id(){
    local hostname=${hostnames["$target"]}
    local address=$(dig  +short   $hostname)
    ssh-keygen -f "$knwhosts" -R "$hostname"
    ssh-keygen -f "$knwhosts" -R "$address"
    ssh_yes
}
#------------------------{ System Info commands }-----------------------------------------------------------------------
system_info(){
    ssh $SSH_OPT $target "
        printf '\n${BLD}Hostname:${DEF}\n'
        hostname

        printf '\n${BLD}Interfaces:${DEF}\n'
        ip a

        printf '\n${BLD}Memory:${DEF}\n'
        LANG=Us free --si -h

        printf '\n${BLD}CPU:${DEF}\n'
        lscpu

        printf '\n${BLD}Disk:${DEF}\n'
        df -h; echo; df -ih; echo; lsblk

        printf '\n${BLD}Software:${DEF}\n'
        uname -a; echo
        [[ -e /usr/bin/lsb_release ]] && { lsb_release -a; echo; }
        [[ -e /usr/bin/java        ]] && { java  -version; echo; }
        [[ -e /usr/bin/psql        ]] && { psql  -V      ; echo; }
        [[ -e /usr/sbin/nginx      ]] && { nginx -v      ; echo; }

        printf '${BLD}Logged in Users:${DEF}\n'
        who

        printf '\n${BLD}Port usage info:${DEF}\n'
        netstat -tulpn 2> /dev/null

        printf '\n${BLD}Processes:${DEF}\n'
        top -bcn1 | head -n30
    "
}

#------------------------{ Show\Edit ssh config }-----------------------------------------------------------------------
show_conf(){ clear; ssh -G $target; pause; }
edit_conf(){
    local new_group search=$target new_filename group_list name confs data
    [[ $group ]] && search="$group_id[[:space:]]*#$group#"
    [[ $group =~ Selected_hosts|Filtered_hosts ]] && {
        dialog --yesno 'Save this group as permanent config file?' 5 45 || return
        new_group=$(D "RUN" '' "BACK" '' --inputbox "Set new group(file) name:" 8 80 "$new_group")
        case $new_group:$? in
                     '':0) return;;
                      *:0) new_filename=~/.ssh/config_"${new_group// /_}"
                           new_filename=${new_filename,,}
                           [[ -f $new_filename ]] && {
                                dialog --defaultno --yesno 'File exists, overwrite?' 5 45 || return
                           }
                           data="#Host DUMMY #$new_group#"
                           group_list=("${selected_list[@]:2}")
                           for ((i=0;  i<${#group_list[@]}; i+=2)); do
                                name="${group_list[$i]}"
                                data+="$(
                                    echo; echo
                                    gawk -vname="$name" '
                                        BEGIN{IGNORECASE=1}
                                        {
                                            if ($1 == "host" && $2 == name){start=1}
                                            if (start){if ($0 ~ /^#|^$/){exit}; print }
                                        }
                                    '   $CONFILES
                                )"
                           done
                           echo "$data" > "$new_filename"
                           return;;
                      *:*) return;;
        esac
    }
    confs=($(grep -rilE "Host[[:space:]]*$search" $CONFILES)) || { clear; echo 'Config file not found'; pause; return; }
    $EDITOR "${confs[@]}"
}

#------------------------{ SSH to target server }-----------------------------------------------------------------------
go_to_target(){ clear; ssh $SSH_OPT $target || pause; }

#------------------------{ Add aliases }--------------------------------------------------------------------------------
add_aliases(){
    scp $SSH_OPT ~/.bash_aliases $target:~
    ssh $SSH_OPT $target "grep '. ~/.bash_aliases' .bashrc || echo '. ~/.bash_aliases' >> .bashrc"
}

#------------------------{ Run function on a group of servers }---------------------------------------------------------
group_run(){
    local func group_list data
    func="$1"
    group_list=("${list[@]:2}")
    SSH_OPT_CUR="$SSH_OPT"
    SSH_OPT="$SSH_OPT -o ConnectTimeout=10 -o BatchMode=true"
    case $func in tabbed*)
        for ((i=0; i<${#group_list[@]}; i+=2)); do
              target="${group_list[$i]}"
              tabbed "${2/_target_/$target}"
        done
        return;;
    esac
    WAIT
    data=$(
        for ((i=0; i<${#group_list[@]}; i+=2)); do
            target="${group_list[$i]}"
            [[ $target =~ ^-+.*-+$ ]] && continue
            (
                code="$BLD$GRN"
                data=$($func 2>&1 | sed ':a;N;$!ba;s/\n/\\n/g'; exit ${PIPESTATUS[0]}) || code="$BLD$RED"
                echo "$code----{ $target }----$DEF\\n${data:-Command did not output anything.}\\n"
            )   &
        done
    )
    GO; printf -- '%b' "$(sort -Vk2 <<< "$data")"
    SSH_OPT="$SSH_OPT_CUR"
}

#------------------------{ Run command/script }-------------------------------------------------------------------------
run_command(){ ssh $SSH_OPT $target $command; }
run_script (){
    scp -r $SSH_OPT "${sshto_script[2]}" $target:~/ || return 1
    ssh    $SSH_OPT "$target" "~/${sshto_script[1]}"
}

#------------------------{ Add ssh key }--------------------------------------------------------------------------------
add_sshkey(){ clear; ssh_yes > /dev/null; ssh-copy-id -i $KEY $SSH_OPT $target; }

#------------------------{ Tunnelling command}--------------------------------------------------------------------------
portunneling(){ ssh $SSH_OPT $target -f -L 127.0.0.1:$LOCAL:127.0.0.1:$REMOTE sleep $TIME; }

#------------------------{ Exit function }------------------------------------------------------------------------------
bye(){
    printf "\n$DEF$CON"
    clear
    $LSEXIT || exit 0
    lsopts='--color=auto'
    [[ $(uname -s) == "Darwin" ]] && lsopts='-G'
    ls $lsopts
    exit 0
};  trap bye INT

#============================>-{ Dialog functions }-<===================================================================
do='--output-fd 1 --colors --no-collapse' # dialog common options
eb='--extra-button'                       # extra
hb='--help-button'                        # buttons
cl='--cancel-label'                       # and
el='--extra-label'                        # short
hl='--help-label'                         # label
ol='--ok-label'                           # names

# Dialog buttons order and exit codes
#<OK> <Extra> <Cancel> <Help>
# 0      3       1       2

D(){ # dialog creator
    local opts=()
    [[ $1 ]] && opts+=("$ol" "$1")
    [[ $2 ]] && opts+=("$el" "$2" "$eb")
    [[ $3 ]] && opts+=("$cl" "$3")
    [[ $4 ]] && opts+=("$hl" "$4" "$hb")
    shift 4
    dialog "${opts[@]}" $do  "$@"
}

#------------------------{ Change alternative username }----------------------------------------------------------------
username(){
    new_user=$(D "CHANGE" '' "BACK" '' --max-input 20 --inputbox 'Change alternative username' 10 30 $GUEST)
	case $new_user:$? in
                 *:0) GUEST=${new_user:-$GUEST}; SSH_OPT="-oUser=$GUEST"; USERNOTE="Username changed to \Z2$GUEST\Z0.";;
                 *:*) return;;
	esac
}

#------------------------{ Create custom command/script }---------------------------------------------------------------
custom(){
    local runner=
    [[ $group ]] && runner='group_run'
    new_command=$(D "RUN" '' "BACK" '' --inputbox "Write down your command here:" 8 120 "$new_command")
	case $new_command:$? in
	               '':0) custom;;
                    *:0) command=$new_command; clear; $runner run_command; pause;;
                    *:*) return;;
	esac
}

script(){
    [[ -f ${sshto_script[2]} ]] || {
        echo  -e '#!/bin/bash\necho "Running sshto script"' > "${sshto_script[2]}"
        chmod +x "${sshto_script[2]}"
    }

    script_text=$(cat "${sshto_script[2]}")
    D "RUN" "EDIT" '' "BACK" --msgbox "$script_text" 40 120
    case $? in
         0) [[ $script_text ]] || script; clear; $runner run_script; pause;;
         3) $EDITOR "${sshto_script[2]}"; script;;
         2) second_dialog;;
	esac
}

#------------------------{ Change local port for tunnelling }-----------------------------------------------------------
local_port(){
    new_local=$(D "CHANGE" '' "BACK" '' --max-input 5 --inputbox 'Change local port' 10 30 $LOCAL)
    LOCAL=${new_local:-$LOCAL}
}

#------------------------{ Change remote port for tunnelling }----------------------------------------------------------
remote_port(){
    new_remote=$(D "CHANGE" '' "BACK" '' --max-input 5 --inputbox 'Change remote port' 10 30 $REMOTE)
    REMOTE=${new_remote:-$REMOTE}
}

#------------------------{ Upload\Download and mount dialogs }----------------------------------------------------------
downpath(){
    new_path=$(D "CHANGE" '' "BACK" '' --max-input 100 --inputbox 'Change download folder' 10 50 $DEST)
    DEST=${new_path:-$DEST}
    dfilelist=
}

homepath(){
    new_path=$(D "CHANGE" '' "BACK" '' --max-input 100 --inputbox 'Change home folder' 10 50 $home)
    home=${new_path:-$home}
}

uploader(){
    printf "Uploading $BLD$ufilename$DEF\n"
    rsync $rsyncopt --rsh="ssh $SSH_OPT" $ufilename $target:"$DEST/"
}

mountdest(){
    which  sshfs &> /dev/null || { clear; how_to_install sshfs; pause; return; }
    clear; sshfs $sshfsopt "$target":"$DEST" "$home" || pause
}

unmountdest(){ mount | grep -q "$home" && umount "$home"; }

copy_files(){
    local runner=
    [[ $group ]] && runner='group_run'
    ufilename=$(D "COPY" '' "BACK" '' --fselect $PWD/ 10 80)
	case $ufilename:$? in
         $PWD|$PWD/:0) return;;
                  *:0) clear; $runner uploader; pause;;
                  *:*) return;;
	esac           ;   copy_files
}

upload(){
    local runner=
    [[ $group ]] && runner='group_run'
    ufilelist=( $(ls -sh1 $home | awk '{print $2,$1}') )
	ufilename=$(D "UPLOAD" '' "BACK" '' --menu "Select file\folder to upload:" 0 0 0 "${ufilelist[@]:2}")
	case $? in
         0) [[ $ufilename ]] || upload
            clear; $runner uploader; pause;;
         *) return;;
	esac;   upload
}

download(){
    [[ $dfilelist ]] || {
        WAIT
        dfilelist=$(ssh $SSH_OPT $target ls -sh1 $DEST 2>&1) \
            && dfilelist=( $(awk '{print $2,$1}' <<< "$dfilelist") ) \
            || {
                clear
                echo "$dfilelist"
                pause
                dfilelist=
                second_dialog
            }
        GO
    }
	dfilename=$(D "DOWNLOAD" '' "BACK" '' --menu "Select file\folder to download:" 0 0 0 "${dfilelist[@]:2}")
	case $? in
         0) [[ $dfilename ]] || download
            clear
            printf "Downloading $BLD$dfilename$DEF\n"
            rsync $rsyncopt --rsh="ssh $SSH_OPT" $target:"$DEST/$dfilename" . || pause;;
         *) return;;
	esac;   download
}

#------------------------{ Switch menu mode to contents view or full list }---------------------------------------------
save_tmp(){ echo "$1" > "$tmpfile"; chmod 600 "$tmpfile"; }
new_list(){
    list=(); match=
    for item in "${selected_list[@]}" "${filtered_list[@]}" "${fullist[@]}"; {
        case         $item:$match    in
                 *{\ *\ }*:1) break  ;;
           *{\ $filter\ }*:*) match=1;;
        esac
        [[ $match ]] && list+=( "$item" )
    }

    [[ $filter =~ Selected ]] && return
    [[ ${list[*]} ]] && save_tmp "filter='$filter'" || { list=( "${fullist[@]}" ); rm "$tmpfile"; }
}

contents_menu(){
    local filter_tmp=$filter selected= filtered=
    [[ ${selected_list[@]} ]] && selected='Selected_hosts'
    [[ ${filtered_list[@]} ]] && filtered='Filtered_hosts'
    local btns=('SELECT' 'RUN COMMAND' 'RELOAD' 'BACK')
	filter=$(D "${btns[@]}" --no-items --menu "Select list of hosts:" 0 0 0 "All" $selected $filtered "${content[@]}" '' "$qcmd" "$qbut1")
	case $filter:$? in
	          "":[03]) contents_menu;;
	     "$qcmd":[03]) contents_menu;;
        "$qbut1":[03]) filter_items 'first_dialog';;
             All:0) list=( "${selected_list[@]}" "${fullist[@]}" )
                    save_tmp       "filter=";;
               *:3) second_dialog "$filter" ;;
               *:2) filter=$filter_tmp;;
               *:1) restart; contents_menu;;
               *:0) new_list;;
	esac        ;   first_dialog
}
# Dialog buttons order and exit codes
#<OK> <Extra> <Cancel> <Help>
# 0      3       1       2
#------------------------{ Selector }-----------------------------------------------------------------------------------
declare -A slctd_hosts
declare -A slctd_groups

gen_selected_list(){
    local k
    selected_list=('-----------{ Selected_hosts }-----------' '_LINE_')
    for k in "${!slctd_hosts[@]}"; { selected_list+=("$k" "${slctd_hosts[$k]}"); }
}

slct_dslct(){
    local desc k v

    # remove from selection
    [[ ${slctd_hosts[$target]} && $1 != "select" ]] && {
        unset slctd_hosts[$target]
        gen_selected_list
        ((${#selected_list[@]}==2)) && unset selected_list
        return
    }

    [[ $1 == "deselect" ]] && return
    # add to selection
    for ((k=0,v=1; k<N; k++,v++)); { [[ ${fullist[k]} =~ $target ]] && { desc=${fullist[v]}; break; }; }
    slctd_hosts[$target]=$desc
    gen_selected_list
}

slct_dslctgroup_run(){
    group_list=("${list[@]:2}")
    for ((i=0;  i<${#group_list[@]}; i+=2)); do
        target="${group_list[$i]}"
        slct_dslct "${command,,}"
    done

    [[ ${slctd_groups["$group"]} ]] && unset slctd_groups["$group"] || slctd_groups["$group"]=$group
}

#------------------------{ Items filter }-------------------------------------------------------------------------------
filter_items(){
    local n d
    declare -A filtered_hosts
    filtered_list=('-----------{ Filtered_hosts }-----------' "$descline")
    items_filter=$(D 'FILTER' 'CLEAR' 'BACK' '' --max-input 60 --inputbox 'Enter filtering pattern' 10 60 $items_filter)
	case $items_filter:$? in
                '':0|*:3) unset filtered_list items_filter; contents_menu;;
                     *:0) items_filter=${items_filter,,}
                          for ((n=0,d=1; n<N; n+=2,d+=2)); do
                              filter_by=${fullist[n]}
                              [[ $items_filter  =~ ^\#.*$             ]] && filter_by=${fullist[d]}
                              [[ ${filter_by,,} =~ ^-+.*-+$           ]] && continue
                              [[ ${filter_by,,} =~ ${items_filter/\#} ]] && {
                                  [[ ${filtered_hosts[${fullist[n]}]} ]] || { # to remove dupes
                                      filtered_list+=("${fullist[n]}" "${fullist[d]}")
                                        filtered_hosts[${fullist[n]}]="${fullist[d]}"
                                  }
                              }
                          done
                          ((${#filtered_list[@]}==2)) && { clear; pause 'Nothing found'; $1; }
                          list=( "${filtered_list[@]}" );;
                     *:1) $1;;
	esac;                 $1
}

#------------------------{ First dialog - Select target host }----------------------------------------------------------
first_dialog(){
    local btns
    group= dfilelist=
    [[ $OPT =~ name ]] && btns=('GET NAME' '' 'EXIT' 'CONTENTS') || btns=('CONNECT/SELECT' 'RUN COMMAND' 'EXIT' 'CONTENTS')
	target=$(D "${btns[@]}" --menu "Select host to connect to. $USERNOTE" 0 0 0 "${list[@]//_LINE_/$descline}" "${quick_butt[@]}")
	case $target:$? in
	          "":0) first_dialog;;
	     "$qcmd":0) first_dialog;;
	    "$qbut1":[03]) filter_items 'first_dialog';;
       *{\ *\ }*:0) filter=${target//*\{ }; filter=${filter// \}*}; new_list; first_dialog ;;
       *{\ *\ }*:3) filter=${target//*\{ }; filter=${filter// \}*}; second_dialog "$filter";;
               *:0) [[ $OPT =~ name ]] && return || { go_to_target; first_dialog; };;
      	       *:1) bye;;
               *:2) contents_menu;;
      	       *:3) second_dialog;;
               *:*) contents_menu;;
  	esac
}

gsel(){
    [[ $group == "Selected_hosts" ]] && { slct_grp=(); return 1; }
    [[ ${slctd_groups["$group"]}  ]] &&                return 0
}
#------------------------{ Second dialog - Select command }-------------------------------------------------------------
second_dialog(){
    local headings    commands                    singleornot         runner             connect
        group="$1"    commands='cmdlist[@]'       singleornot='host'  runner=''          connect='CONNECT'
    [[ $group ]] && { commands='cmdlist_group[@]' singleornot='group' runner='group_run' connect=''  filter="$group"
                      slct_grp=(Select   "Add all hosts of \Z4$group\Z0 to tmp group \Z4Selected_hosts\Z0"      '' '')
              gsel && slct_grp=(Deselect "Remove all hosts of \Z4$group\Z0 from tmp group \Z4Selected_hosts\Z0" '' '')
    }

    headings="Select command to run on $singleornot \Z4${group:-$target}\Z0. $USERNOTE"
                                       slct=(Select   "Add \Z4$target\Z0 to tmp group \Z4Selected_hosts\Z0"      '' '')
    [[ ${slctd_hosts["$target"]} ]] && slct=(Deselect "Remove \Z4$target\Z0 from tmp group \Z4Selected_hosts\Z0" '' '')

    new_list; cmdlist_renew
	command=$(D 'RUN' "$connect" 'BACK' 'CONTENTS' --menu "$headings" 0 0 0 "${!commands}")
	case $command:$? in
	           '':0) :;;
	     *'elect':0) slct_dslct$runner  ;;
       "Add tab"*:0) $runner tabbed "$0";;
       "Ssh tab"*:0) $runner tabbed "ssh $SSH_OPT _target_";;
            Alias:0) clear; $runner add_aliases; pause;;
             Info:0) clear; $runner system_info; pause;;
              Yes:0) clear; $runner ssh_yes    ; pause;;
           Fix_id:0) clear; $runner fix_id     ; pause;;
           Sshkey:0) add_sshkey  ;;
             Copy:0) copy_files  ;;
           Upload:0) upload      ;;
           Custom:0) custom      ;;
           Script:0) script      ;;
         Username:0) username    ;;
             Dest:0) downpath    ;;
             Home:0) homepath    ;;
            Mount:0) mountdest   ;;
          Unmount:0) unmountdest ;;
         Download:0) download    ;;
            Local:0) local_port  ;;
           Remote:0) remote_port ;;
           Tunnel:0) portunneling;;
         ShowConf:0) show_conf   ;;
         EditConf:0) edit_conf   ;;
                *:0) clear; $runner run_command; pause;;
                *:2) contents_menu;;
                *:3) go_to_target ;;
                *:*) first_dialog ;;
	esac         ;   second_dialog "$group"
}

#-------------{ Create the list of hosts. Get hosts and descriptions from ~/.ssh/config* }------------------------------
declare -A hostnames=()
desclength=20
restart(){
    fullist=()
    content=()
    hostnames=()
    selected_list=()
    CONFILES=$( # SSH confiles list.
        shopt -s nullglob
        echo  "$sshdir"/config*[!~]
        [[ -f "$sshdir"/config ]] && echo "$sshdir"/config
    )

    while read -r name hostname desc; do
        case    ${name,,} in
            'group_name') name="{ $desc }"
                          name_length=${#name}
                          name_left=$[(40-name_length)/2]
                          name_right=$[40-(name_left+name_length)]
                          printf -v tmp "%${name_left}s_NAME_%${name_right}s"
                          tmp=${tmp// /-}  name=${tmp//_NAME_/$name}
                          content+=( "$desc" );  desc='_LINE_';;
                    '#'*) continue;;
        esac
        ((${#desc}>desclength)) && desclength=${#desc}
        hostnames["$name"]=$hostname #Create host:hostname pairs in hostnames array
        fullist+=("$name" "$desc")   #Add Host and Description to the list
    done < <(gawk '
    BEGIN{IGNORECASE=1}
    /Host /{
        strt=1
        host=$2
        desc=gensub(/^.*Host .* #(.*)/, "\\1", "g", $0)
        desc=gensub(/(.*)#.*/,          "\\1", "g", desc)
        next
    }
    strt && host == "'"$group_id"'"{
        print "group_name", "dummy", desc
        strt=0
    }
    strt && /HostName /{
        hostname=$2
        print host, hostname, desc
        strt=0
    }'  $CONFILES)

    descline=$(line - $desclength)
    list=( "${fullist[@]}" "${quick_butt[@]}" )
    N=${#fullist[@]}
     qcmd='-----------{ Quick commands }-----------'
    qbut1='1             Filter items'
    quick_butt=(
        ''        ''
        "$qcmd"   "$descline"
        "$qbut1"  "Create list of hosts filtered by pattern"
    )
}

#--{ Go baby, GO!) }--
check_confile;  restart
[[ -e $tmpfile  ]] && . "$tmpfile"
[[    $filter   ]] &&    new_list
[[ $target      ]] || first_dialog
[[ $OPT =~ name ]] && { echo $target; exit; }
[[ $target      ]] && second_dialog

bye #:)
Download .txt
gitextract_10rhdvse/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE.md
├── README.md
└── sshto
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (40K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 28,
    "preview": "custom: ['paypal.me/sshto']\n"
  },
  {
    "path": ".gitignore",
    "chars": 18,
    "preview": ".idea\n/install.sh\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1049,
    "preview": "License: MIT\n\tPermission is hereby granted, free of charge, to any person obtaining a copy\n\tof this software and associa"
  },
  {
    "path": "README.md",
    "chars": 5930,
    "preview": "# sshto\n<a href=\"https://t.me/sshtobash\"><img src=\"https://telegram.org/img/website_icon.svg\" width=\"21\"></a>\n[![Twitter"
  },
  {
    "path": "sshto",
    "chars": 31096,
    "preview": "#!/usr/bin/env bash\n#--------------------------{ Default values }-------------------------------------------------------"
  }
]

About this extraction

This page contains the full source code of the vaniacer/sshto GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (37.2 KB), approximately 10.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!