[
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: ['paypal.me/sshto']\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n/install.sh\n"
  },
  {
    "path": "LICENSE.md",
    "content": "License: MIT\n\tPermission is hereby granted, free of charge, to any person obtaining a copy\n\tof this software and associated documentation files (the \"Software\"), to deal\n\tin the Software without restriction, including without limitation the rights\n\tto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n\tcopies of the Software, and to permit persons to whom the Software is\n\tfurnished to do so, subject to the following conditions:\n\tThe above copyright notice and this permission notice shall be included in all\n\tcopies or substantial portions of the Software.\n\tTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n\tIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n\tFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n\tAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n\tLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n\tOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n\tSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# sshto\n<a href=\"https://t.me/sshtobash\"><img src=\"https://telegram.org/img/website_icon.svg\" width=\"21\"></a>\n[![Twitter Follow](https://img.shields.io/twitter/follow/Vaniacer?style=social)](https://twitter.com/Vaniacer)\n[![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>\n\nSmall bash script that builds a menu (via dialog) from your ~/.ssh/config.</br>\n![screeenshot](https://user-images.githubusercontent.com/18072680/60570513-69e99f00-9d7a-11e9-916d-48b74fa7585a.png)\n</br>\nAllows you to connect to your servers or run commands from menu. Available commands:</br>\n![cmds](https://user-images.githubusercontent.com/18072680/211161226-1c5eec5a-634b-4902-90cd-5947dd95083e.png)\n</br>\nYour commands can be easily added to this list. Just edit this part of the script:\n<pre>\ncmdlist=(\n    #Command#    #Description#\n    \"${slct[@]}\" #De/Select command\n    \"Username\"   \"Change ssh username to \\Z1$GUEST\\Z0\"\n    \"Add tab\"    \"Add terminal tab with \\Z1sshto\\Z0 for \\Z4$target\\Z0\"\n    \"Ssh tab\"    \"Add terminal tab with \\Z1ssh\\Z0 to \\Z4$target\\Z0\"\n    ''           ''\n    \"ls -lah\"    \"List Files\"\n    \"free -h\"    \"Show free memory\"\n    \"df  -ih\"    \"Show free inodes\"\n    \"df   -h\"    \"Show free disk space\"\n    \"Custom\"     \"Run custom command on \\Z4$target\\Z0\"\n    \"Script\"     \"Run custom script on \\Z4$target\\Z0\"\n    ''           ''\n    'Yes'        \"Say 'yes' to SSH\"\n    \"Info\"       \"Full system info\"\n    'Fix_id'     \"Update host in known_hosts\"\n    \"Sshkey\"     \"Add my ssh key to \\Z4$target\\Z0\"\n    \"Alias\"      \"Add my useful aliases to \\Z4$target\\Z0\"\n    \"Copy\"       \"Copy selected file or dir to \\Z4$target\\Z0\"\n    ''           ''\n    \"Home\"       \"Change home folder \\Z4$home\\Z0 on local server\"\n    \"Dest\"       \"Change destination folder \\Z4$DEST\\Z0 on \\Z4$target\\Z0\"\n    \"Upload\"     \"Upload file or folder from \\Z4$home\\Z0 to \\Z4$target:${DEST}\\Z0\"\n    \"Download\"   \"Download file or folder from \\Z4$target:${DEST}\\Z0 to \\Z4$home\\Z0\"\n    \"Mount\"      \"Mount remote folder \\Z4$target:$DEST\\Z0 to \\Z4$home\\Z0\"\n    \"Unmount\"    \"Unmount remote folder \\Z4$target:$DEST\\Z0 from \\Z4$home\\Z0\"\n    ''           ''\n    \"Local\"      \"Change local  port \\Z1$LOCAL\\Z0\"\n    \"Remote\"     \"Change remote port \\Z1$REMOTE\\Z0\"\n    \"Tunnel\"     \"Start portunneling from \\Z4$target:$REMOTE\\Z0 to \\Z4localhost:$LOCAL\\Z0\"\n    ''           ''\n    \"ShowConf\"   \"Show ssh config for this host\"\n    \"EditConf\"   \"Edit ssh config for this host\"\n)\n</pre>\nFirst collumn - command, second - description.</br>\nSimple commands like `ls -la` could be added as is.</br>\nA list of commands or a complicated logic should be added via function.</br>\nEmpty values(`''`) could be used as a delimiter.</br>\nYou can quick jump to the selected server via <i>CONNECT</i> button.</br>\nTo 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>\n</br>\nOptional hosts description could be added like this:</br>\n<pre>\nHost server1 #Description, it could be more than one word\nHostName 192.168.0.1\nPort 22\nUser admin\n</pre>\nOptional start menu delimiters '---{ Group Name }---' could be added like this:</br>\n<pre>\n#Host DUMMY #Group Name#\n</pre>\nIf you are unhappy with this 'DUMMY' group name template, or you actually have a host named 'dummy',\nyou can change this template by ajusting this variable `group_id=dummy`. </br>\nAll these additions won't break your *ssh configs* coz they are considered as comments.  \n\n------\n~/.ssh/config example:\n<pre>\n#Host DUMMY #Rybinsk#\n\nHost rybserver1 #First server\nHostName localhost\n\nHost rybserver2 #Second server\nHostName localhost\n\nHost rybserver3 #Third server\nHostName localhost\n\n#Host DUMMY #Moscow#\n\nHost moserver1 #First server\nHostName localhost\n\nHost moserver2 #Second server\nHostName localhost\n\nHost moserver3 #Third server\nHostName localhost\n</pre>\nScript greps data from multiple config files via pattername `config*` in `~/.ssh` dir.</br>\nSo you can split config to multiple files and use them with <i>Include</i> directive, example:\n<pre>\nInclude config_moscow\nInclude config_rybinsk\nInclude config*\n</pre>\nAll preset variables and functions could be tweaked via `~/.sshtorc` config file:\n<pre>\necho \"REMOTE=9000  # Remote port for tunneling.\" >> ~/.sshtorc\n</pre>\n\nYou can customize dialog itself a bit by creating and editing its config file:\n<pre>\ndialog --create-rc ~/.dialogrc\nnano ~/.dialogrc\n</pre>\n\nIf you don't have dialog and don't want(or can't) to install it, there is a dialog-less version of sshto\nin my new project [bashui](https://github.com/vaniacer/bashui) here is how it looks:\n![bashui-hosts](https://habrastorage.org/getpro/habr/upload_files/024/c74/38e/024c7438e6429f5e37a8a71d98bf7edb.png)\n\n![bashui-commands](https://habrastorage.org/getpro/habr/upload_files/495/2cc/526/4952cc52616db16acfa7b2fd9e8d366f.png)\n\nTry it [bashui-sshto](https://github.com/vaniacer/bashui/blob/master/demo_sshto)!\n\n# How to install\nClone\\download this project, go to it's folder and run:\n<pre>sudo cp sshto /usr/bin/\n\n#and to unistall\nsudo rm /usr/bin/sshto\n</pre>\n\n<a href=\"https://asciinema.org/a/PQMuRvfmxlHUc4oZMN76LY2V4\">See how it works at asciinema</a></br>\n\n<a href=\"http://www.youtube.com/watch?feature=player_embedded&v=FhnsVH8t96Q\n\" target=\"_blank\"><img src=\"http://img.youtube.com/vi/FhnsVH8t96Q/0.jpg\" \nalt=\"ssh config guide\" width=\"240\" height=\"180\" border=\"10\"/></a></br>\nTom Lawrens video guide about ssh config and sshto\n\n<a href=\"https://t.me/sshtobash\"><img src=\"https://telegram.org/img/website_icon.svg\" width=\"21\"></a>\n[![Twitter Follow](https://img.shields.io/twitter/follow/Vaniacer?style=social)](https://twitter.com/Vaniacer)\n[![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>\n"
  },
  {
    "path": "sshto",
    "content": "#!/usr/bin/env bash\n#--------------------------{ Default values }---------------------------------------------------------------------------\n     OPT=$1     # Options.\n  sshdir=~/.ssh # Ssh configfiles folder\n     KEY=\"$sshdir/id_rsa.pub\" # SSH key to use in command Sshkey(Add my ssh key to host).\n  REMOTE=8080   # Remote port for tunneling.\n   LOCAL=18080  # Local  port for tunneling.\n   GUEST=$USER  # Alternative username to login with.\n    DEST='~'    # Destination folder on target server to download\\upload files and sshfs mount.\n    TIME=60     # Timer for tunneling command. Tunnel will be closed after 60 seconds, but it will stay open if used.\n  EDITOR=nano   # SSH confile editor.\n  LSEXIT=true   # Perform ls on exit true|false.\n    home=$PWD   # Destination folder on local  server to download\\upload files and sshfs mount.\nsshfsopt=       # Sshfs options if needed\ngroup_id=dummy  # Group separator identification template, 'dummy' by default, don't ask why, historically)\nrsyncopt='-Pgzortl'            # Upload/download rsync options\nknwhosts=\"$sshdir\"/known_hosts # Path to known_hosts file.\n confile=~/.sshtorc            # Path to config file, have to be chmod'ed to 600.\n tmpfile=/tmp/sshtorc-${USER}  # Path to tmp file to save selected context.\nsshto_script[0]=~              # Path to sshto script\nsshto_script[1]=.sshto_script  # Sshto script filename\nsshto_script[2]=\"${sshto_script[0]}/${sshto_script[1]}\" # Sshto script full\n#------------------------{ Add some tabs }------------------------------------------------------------------------------\ntabbed(){ target=$target gnome-terminal --title=$target --tab -qe \"${1/_target_/$target}\"; } # Terminal command for tabs\n\n#------------------------{ Add your commands to this lists }------------------------------------------------------------\ncmdlist_renew(){\n    cmdlist=(\n        #Command#    #Description#\n        \"${slct[@]}\" #De/Select command\n        \"Username\"   \"Change ssh username to \\Z1$GUEST\\Z0\"\n        \"Add tab\"    \"Add terminal tab with \\Z1sshto\\Z0 for \\Z4$target\\Z0\"\n        \"Ssh tab\"    \"Add terminal tab with \\Z1ssh\\Z0 to \\Z4$target\\Z0\"\n        ''           ''\n        \"ls -lah\"    \"List Files\"\n        \"free -h\"    \"Show free memory\"\n        \"df  -ih\"    \"Show free inodes\"\n        \"df   -h\"    \"Show free disk space\"\n        \"Custom\"     \"Run custom command on \\Z4$target\\Z0\"\n        \"Script\"     \"Run custom script on \\Z4$target\\Z0\"\n        ''           ''\n        'Yes'        \"Say 'yes' to SSH\"\n        \"Info\"       \"Full system info\"\n        'Fix_id'     \"Update host in known_hosts\"\n        \"Sshkey\"     \"Add my ssh key to \\Z4$target\\Z0\"\n        \"Alias\"      \"Add my useful aliases to \\Z4$target\\Z0\"\n        \"Copy\"       \"Copy selected file or dir to \\Z4$target\\Z0\"\n        ''           ''\n        \"Home\"       \"Change home folder \\Z4$home\\Z0 on local server\"\n        \"Dest\"       \"Change destination folder \\Z4$DEST\\Z0 on \\Z4$target\\Z0\"\n        \"Upload\"     \"Upload file or folder from \\Z4$home\\Z0 to \\Z4$target:${DEST}\\Z0\"\n        \"Download\"   \"Download file or folder from \\Z4$target:${DEST}\\Z0 to \\Z4$home\\Z0\"\n        \"Mount\"      \"Mount remote folder \\Z4$target:$DEST\\Z0 to \\Z4$home\\Z0\"\n        \"Unmount\"    \"Unmount remote folder \\Z4$target:$DEST\\Z0 from \\Z4$home\\Z0\"\n        ''           ''\n        \"Local\"      \"Change local  port \\Z1$LOCAL\\Z0\"\n        \"Remote\"     \"Change remote port \\Z1$REMOTE\\Z0\"\n        \"Tunnel\"     \"Start portunneling from \\Z4$target:$REMOTE\\Z0 to \\Z4localhost:$LOCAL\\Z0\"\n        ''           ''\n        \"ShowConf\"   \"Show ssh config for this host\"\n        \"EditConf\"   \"Edit ssh config for this host\"\n    )\n    cmdlist_group=(\n        #Command#    #Description#\n        \"${slct_grp[@]}\" #De/Select command\n        \"Username\"   \"Change ssh username to \\Z1$GUEST\\Z0\"\n        \"Add tabs\"   \"Add terminal tabs with \\Z1sshto\\Z0 for hosts in \\Z4$group\\Z0 group\"\n        \"Ssh tabs\"   \"Add terminal tabs with \\Z1ssh\\Z0 to hosts from \\Z4$group\\Z0 group\"\n        ''           ''\n        \"ls  -la\"    \"List Files\"\n        \"free -h\"    \"Show free memory\"\n        \"df  -ih\"    \"Show free inodes\"\n        \"df   -h\"    \"Show free disk space\"\n        \"Custom\"     \"Run custom command on \\Z4$group\\Z0\"\n        \"Script\"     \"Run custom script on \\Z4$group\\Z0\"\n        ''           ''\n        'Yes'        \"Say 'yes' to SSH\"\n        'Fix_id'     \"Update hosts in known_hosts\"\n        \"Info\"       \"Full system info\"\n        \"Alias\"      \"Add my useful aliases to \\Z4$group\\Z0\"\n        \"Copy\"       \"Copy selected file or dir to \\Z4$group\\Z0\"\n        ''           ''\n        \"Home\"       \"Change home folder \\Z4$home\\Z0 on local server\"\n        \"Dest\"       \"Change destination folder \\Z4$DEST\\Z0 on \\Z4$group\\Z0\"\n        \"Upload\"     \"Upload file or folder from \\Z4$home\\Z0 to \\Z4$group:${DEST}\\Z0\"\n        ''           ''\n        \"EditConf\"   \"Edit ssh config for this group\"\n    )\n}\n#--------------------------------------------------------------------+--------------------------------------------------\n#Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}\"Hello!)\"${DEF}|\n#-------------------------+--------------------------------+---------+\n#       Text color        |       Background color         |         |\n#-----------+-------------+--------------+-----------------+         |\n# Base color|Lighter shade|  Base color  | Lighter shade   |         |\n#-----------+-------------+--------------+-----------------+         |\nBLK='\\e[30m'; blk='\\e[90m'; BBLK='\\e[40m'; bblk='\\e[100m' #| Black   |\nRED='\\e[31m'; red='\\e[91m'; BRED='\\e[41m'; bred='\\e[101m' #| Red     |\nGRN='\\e[32m'; grn='\\e[92m'; BGRN='\\e[42m'; bgrn='\\e[102m' #| Green   |\nYLW='\\e[33m'; ylw='\\e[93m'; BYLW='\\e[43m'; bylw='\\e[103m' #| Yellow  |\nBLU='\\e[34m'; blu='\\e[94m'; BBLU='\\e[44m'; bblu='\\e[104m' #| Blue    |\nMGN='\\e[35m'; mgn='\\e[95m'; BMGN='\\e[45m'; bmgn='\\e[105m' #| Magenta |\nCYN='\\e[36m'; cyn='\\e[96m'; BCYN='\\e[46m'; bcyn='\\e[106m' #| Cyan    |\nWHT='\\e[37m'; wht='\\e[97m'; BWHT='\\e[47m'; bwht='\\e[107m' #| White   |\n#----------------------------------------------------------+---------+\n# Effects                                                            |\n#--------------------------------------------------------------------+\nDEF='\\e[0m'   #Default color and effects                             |\nBLD='\\e[1m'   #Bold\\brighter                                         |\nDIM='\\e[2m'   #Dim\\darker                                            |\nCUR='\\e[3m'   #Italic font                                           |\nUND='\\e[4m'   #Underline                                             |\nINV='\\e[7m'   #Inverted                                              |\nCOF='\\e[?25l' #Cursor Off                                            |\nCON='\\e[?25h' #Cursor On                                             |\n#--------------------------------------------------------------------+\n# Text positioning, usage: XY 10 10 'Hello World!'                   |\nXY(){ printf \"\\e[$2;${1}H$3\"; }                                     #|\n# Print line, usage: line - 10 | line -= 20 | line 'Hello World!' 20 |\nline(){ printf -v _L %$2s; printf -- \"${_L// /$1}\"; }               #|\n# Create sequence like {0..(X-1)}, usage: que 10                     |\nque(){ printf -v _N %$1s; _N=(${_N// / 1}); printf \"${!_N[*]}\"; }   #|\n#------------{ Check that dialog and gawk are installed }------------+\ninstall_help=\"\n${BLD}sshto$DEF requires that the package '${GRN}%b$DEF' is installed.\nType this into the terminal and press return:\n\n    ${BLD}%b$DEF\n\nThen run ${BLD}sshto$DEF again\n\"\nhow_to_install(){\n    local package=$1\n    which yum     &> /dev/null && installer=\"yum -y install $package\"\n    which brew    &> /dev/null && installer=\"brew install $package\"\n    which apt-get &> /dev/null && installer=\"apt-get install -y $package\"\n    printf -- \"$install_help\" \"$package\" \"$installer\"\n    [[ $2 ]] && exit $2\n}\n\nfor package in dialog gawk; { which $package &> /dev/null || how_to_install $package 1; }\n#------------------------{check bash version }--------------------------------------------------------------------------\ntversion=4.2\ncversion=${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}\ngawk '{if($1>$2){exit 1}}' <<< \"$tversion $cversion\" || {\n    printf \"\\nBASH version ${BLD}$tversion+$DEF required to run ${BLD}sshto$DEF, your is - $BLD$BASH_VERSION$DEF\\n\"\n    exit 1\n}\n#------------------------{ Waiting animation }--------------------------------------------------------------------------\ncursor () {\n    case $1 in\n         on) stty  echo; printf \"$CON\";;\n        off) stty -echo; printf \"$COF\";;\n    esac\n}\n\n   x=$[COLUMNS/2-3]\n   y=$[  LINES/2-3]\nsand=( ⠁  ⠂  ⠄  ' ' )\n#  {   small digits    }\nsd=(₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉)\nbs='⠴⠷⠦' # bottom sand pile\nts='⠖'    #  top  sand pile\nWAIT(){\n    clear; cursor off; i=0; start=$SECONDS\n    XY $[x-1]  $y    $UND$BLD$RED'       '$DEF                     # _______\n    XY $[x-1] $[y+1]         $RED'╲'$DIM$UND'     '$DEF$red'╱'$DEF # ╲_____╱\n    XY  $x    $[y+2]         $BLU'(  '$BLD$WHT'•'$BLD$BLU')'$DEF   #  (  •)\n    XY  $x    $[y+3]         $BLU' ╲'$YLW\"$ts\"$BLD$BLU'╱'$DEF      #   ╲⠖╱\n    XY  $x    $[y+4]         $BLU\" ╱$YLW${sand[$i]}$BLD$BLU╲\"$DEF  #   ╱⠂╲\n    XY  $x    $[y+5]         $BLU'('$YLW\"$bs\"$BLD$BLU')'$DEF       #  (⠴⠷⠦)\n    XY $[x-1] $[y+6]         $RED'╱'$RED'‾‾‾‾‾'$BLD$RED'╲'$DEF     # ╱‾‾‾‾‾╲\n    XY $[x-1] $[y+7]     $DIM$RED'‾‾‾‾‾‾‾'$DEF                     # ‾‾‾‾‾‾‾\n    ( while true; do sleep 0.07\n        printf -v counter \"%03d\" $[SECONDS-start]\n        small=\"${sd[${counter:0:1}]}${sd[${counter:1:1}]}${sd[${counter:2:1}]}\"\n        XY $[x-1] $[y+1] $RED'╲'$DIM$UND\" $small \"$DEF$red'╱'$DEF\n        XY  $x    $[y+4] $BLU\" ╱$YLW${sand[$i]}$BLD$BLU╲\"$DEF\n        ((i++)); (($i==${#sand[@]})) && i=0;\n    done ) & waiter=$!\n}\n\nGO() { [[ -e /proc/$waiter ]] && kill $waiter; cursor on; clear; }\n\ncheck_confile(){\n    [[ -e $confile  ]] || return\n    [[ $(stat -c \"%a %U %G\" \"$confile\") == \"600 $USER $USER\" ]] && . \"$confile\"\n}\n\n#------------------------{ Pause function }-----------------------------------------------------------------------------\npause(){\n    local  mess=${1:-'press any key to continue'}\n    printf \"\\n$COF$BLD$mess\\n\"\n    read   -srn1\n    printf \"\\n$DEF$CON\"\n}\n\n#------------------------{ Yes to ssh }---------------------------------------------------------------------------------\nssh_yes(){\n    local hostname=${hostnames[\"$target\"]}\n    local fprint=($(ssh-keyscan -H \"$hostname\" 2>/dev/null))\n    grep -q \"${fprint[2]}\" \"$knwhosts\" || echo \"${fprint[@]}\" >> \"$knwhosts\"\n}\n\nfix_id(){\n    local hostname=${hostnames[\"$target\"]}\n    local address=$(dig  +short   $hostname)\n    ssh-keygen -f \"$knwhosts\" -R \"$hostname\"\n    ssh-keygen -f \"$knwhosts\" -R \"$address\"\n    ssh_yes\n}\n#------------------------{ System Info commands }-----------------------------------------------------------------------\nsystem_info(){\n    ssh $SSH_OPT $target \"\n        printf '\\n${BLD}Hostname:${DEF}\\n'\n        hostname\n\n        printf '\\n${BLD}Interfaces:${DEF}\\n'\n        ip a\n\n        printf '\\n${BLD}Memory:${DEF}\\n'\n        LANG=Us free --si -h\n\n        printf '\\n${BLD}CPU:${DEF}\\n'\n        lscpu\n\n        printf '\\n${BLD}Disk:${DEF}\\n'\n        df -h; echo; df -ih; echo; lsblk\n\n        printf '\\n${BLD}Software:${DEF}\\n'\n        uname -a; echo\n        [[ -e /usr/bin/lsb_release ]] && { lsb_release -a; echo; }\n        [[ -e /usr/bin/java        ]] && { java  -version; echo; }\n        [[ -e /usr/bin/psql        ]] && { psql  -V      ; echo; }\n        [[ -e /usr/sbin/nginx      ]] && { nginx -v      ; echo; }\n\n        printf '${BLD}Logged in Users:${DEF}\\n'\n        who\n\n        printf '\\n${BLD}Port usage info:${DEF}\\n'\n        netstat -tulpn 2> /dev/null\n\n        printf '\\n${BLD}Processes:${DEF}\\n'\n        top -bcn1 | head -n30\n    \"\n}\n\n#------------------------{ Show\\Edit ssh config }-----------------------------------------------------------------------\nshow_conf(){ clear; ssh -G $target; pause; }\nedit_conf(){\n    local new_group search=$target new_filename group_list name confs data\n    [[ $group ]] && search=\"$group_id[[:space:]]*#$group#\"\n    [[ $group =~ Selected_hosts|Filtered_hosts ]] && {\n        dialog --yesno 'Save this group as permanent config file?' 5 45 || return\n        new_group=$(D \"RUN\" '' \"BACK\" '' --inputbox \"Set new group(file) name:\" 8 80 \"$new_group\")\n        case $new_group:$? in\n                     '':0) return;;\n                      *:0) new_filename=~/.ssh/config_\"${new_group// /_}\"\n                           new_filename=${new_filename,,}\n                           [[ -f $new_filename ]] && {\n                                dialog --defaultno --yesno 'File exists, overwrite?' 5 45 || return\n                           }\n                           data=\"#Host DUMMY #$new_group#\"\n                           group_list=(\"${selected_list[@]:2}\")\n                           for ((i=0;  i<${#group_list[@]}; i+=2)); do\n                                name=\"${group_list[$i]}\"\n                                data+=\"$(\n                                    echo; echo\n                                    gawk -vname=\"$name\" '\n                                        BEGIN{IGNORECASE=1}\n                                        {\n                                            if ($1 == \"host\" && $2 == name){start=1}\n                                            if (start){if ($0 ~ /^#|^$/){exit}; print }\n                                        }\n                                    '   $CONFILES\n                                )\"\n                           done\n                           echo \"$data\" > \"$new_filename\"\n                           return;;\n                      *:*) return;;\n        esac\n    }\n    confs=($(grep -rilE \"Host[[:space:]]*$search\" $CONFILES)) || { clear; echo 'Config file not found'; pause; return; }\n    $EDITOR \"${confs[@]}\"\n}\n\n#------------------------{ SSH to target server }-----------------------------------------------------------------------\ngo_to_target(){ clear; ssh $SSH_OPT $target || pause; }\n\n#------------------------{ Add aliases }--------------------------------------------------------------------------------\nadd_aliases(){\n    scp $SSH_OPT ~/.bash_aliases $target:~\n    ssh $SSH_OPT $target \"grep '. ~/.bash_aliases' .bashrc || echo '. ~/.bash_aliases' >> .bashrc\"\n}\n\n#------------------------{ Run function on a group of servers }---------------------------------------------------------\ngroup_run(){\n    local func group_list data\n    func=\"$1\"\n    group_list=(\"${list[@]:2}\")\n    SSH_OPT_CUR=\"$SSH_OPT\"\n    SSH_OPT=\"$SSH_OPT -o ConnectTimeout=10 -o BatchMode=true\"\n    case $func in tabbed*)\n        for ((i=0; i<${#group_list[@]}; i+=2)); do\n              target=\"${group_list[$i]}\"\n              tabbed \"${2/_target_/$target}\"\n        done\n        return;;\n    esac\n    WAIT\n    data=$(\n        for ((i=0; i<${#group_list[@]}; i+=2)); do\n            target=\"${group_list[$i]}\"\n            [[ $target =~ ^-+.*-+$ ]] && continue\n            (\n                code=\"$BLD$GRN\"\n                data=$($func 2>&1 | sed ':a;N;$!ba;s/\\n/\\\\n/g'; exit ${PIPESTATUS[0]}) || code=\"$BLD$RED\"\n                echo \"$code----{ $target }----$DEF\\\\n${data:-Command did not output anything.}\\\\n\"\n            )   &\n        done\n    )\n    GO; printf -- '%b' \"$(sort -Vk2 <<< \"$data\")\"\n    SSH_OPT=\"$SSH_OPT_CUR\"\n}\n\n#------------------------{ Run command/script }-------------------------------------------------------------------------\nrun_command(){ ssh $SSH_OPT $target $command; }\nrun_script (){\n    scp -r $SSH_OPT \"${sshto_script[2]}\" $target:~/ || return 1\n    ssh    $SSH_OPT \"$target\" \"~/${sshto_script[1]}\"\n}\n\n#------------------------{ Add ssh key }--------------------------------------------------------------------------------\nadd_sshkey(){ clear; ssh_yes > /dev/null; ssh-copy-id -i $KEY $SSH_OPT $target; }\n\n#------------------------{ Tunnelling command}--------------------------------------------------------------------------\nportunneling(){ ssh $SSH_OPT $target -f -L 127.0.0.1:$LOCAL:127.0.0.1:$REMOTE sleep $TIME; }\n\n#------------------------{ Exit function }------------------------------------------------------------------------------\nbye(){\n    printf \"\\n$DEF$CON\"\n    clear\n    $LSEXIT || exit 0\n    lsopts='--color=auto'\n    [[ $(uname -s) == \"Darwin\" ]] && lsopts='-G'\n    ls $lsopts\n    exit 0\n};  trap bye INT\n\n#============================>-{ Dialog functions }-<===================================================================\ndo='--output-fd 1 --colors --no-collapse' # dialog common options\neb='--extra-button'                       # extra\nhb='--help-button'                        # buttons\ncl='--cancel-label'                       # and\nel='--extra-label'                        # short\nhl='--help-label'                         # label\nol='--ok-label'                           # names\n\n# Dialog buttons order and exit codes\n#<OK> <Extra> <Cancel> <Help>\n# 0      3       1       2\n\nD(){ # dialog creator\n    local opts=()\n    [[ $1 ]] && opts+=(\"$ol\" \"$1\")\n    [[ $2 ]] && opts+=(\"$el\" \"$2\" \"$eb\")\n    [[ $3 ]] && opts+=(\"$cl\" \"$3\")\n    [[ $4 ]] && opts+=(\"$hl\" \"$4\" \"$hb\")\n    shift 4\n    dialog \"${opts[@]}\" $do  \"$@\"\n}\n\n#------------------------{ Change alternative username }----------------------------------------------------------------\nusername(){\n    new_user=$(D \"CHANGE\" '' \"BACK\" '' --max-input 20 --inputbox 'Change alternative username' 10 30 $GUEST)\n\tcase $new_user:$? in\n                 *:0) GUEST=${new_user:-$GUEST}; SSH_OPT=\"-oUser=$GUEST\"; USERNOTE=\"Username changed to \\Z2$GUEST\\Z0.\";;\n                 *:*) return;;\n\tesac\n}\n\n#------------------------{ Create custom command/script }---------------------------------------------------------------\ncustom(){\n    local runner=\n    [[ $group ]] && runner='group_run'\n    new_command=$(D \"RUN\" '' \"BACK\" '' --inputbox \"Write down your command here:\" 8 120 \"$new_command\")\n\tcase $new_command:$? in\n\t               '':0) custom;;\n                    *:0) command=$new_command; clear; $runner run_command; pause;;\n                    *:*) return;;\n\tesac\n}\n\nscript(){\n    [[ -f ${sshto_script[2]} ]] || {\n        echo  -e '#!/bin/bash\\necho \"Running sshto script\"' > \"${sshto_script[2]}\"\n        chmod +x \"${sshto_script[2]}\"\n    }\n\n    script_text=$(cat \"${sshto_script[2]}\")\n    D \"RUN\" \"EDIT\" '' \"BACK\" --msgbox \"$script_text\" 40 120\n    case $? in\n         0) [[ $script_text ]] || script; clear; $runner run_script; pause;;\n         3) $EDITOR \"${sshto_script[2]}\"; script;;\n         2) second_dialog;;\n\tesac\n}\n\n#------------------------{ Change local port for tunnelling }-----------------------------------------------------------\nlocal_port(){\n    new_local=$(D \"CHANGE\" '' \"BACK\" '' --max-input 5 --inputbox 'Change local port' 10 30 $LOCAL)\n    LOCAL=${new_local:-$LOCAL}\n}\n\n#------------------------{ Change remote port for tunnelling }----------------------------------------------------------\nremote_port(){\n    new_remote=$(D \"CHANGE\" '' \"BACK\" '' --max-input 5 --inputbox 'Change remote port' 10 30 $REMOTE)\n    REMOTE=${new_remote:-$REMOTE}\n}\n\n#------------------------{ Upload\\Download and mount dialogs }----------------------------------------------------------\ndownpath(){\n    new_path=$(D \"CHANGE\" '' \"BACK\" '' --max-input 100 --inputbox 'Change download folder' 10 50 $DEST)\n    DEST=${new_path:-$DEST}\n    dfilelist=\n}\n\nhomepath(){\n    new_path=$(D \"CHANGE\" '' \"BACK\" '' --max-input 100 --inputbox 'Change home folder' 10 50 $home)\n    home=${new_path:-$home}\n}\n\nuploader(){\n    printf \"Uploading $BLD$ufilename$DEF\\n\"\n    rsync $rsyncopt --rsh=\"ssh $SSH_OPT\" $ufilename $target:\"$DEST/\"\n}\n\nmountdest(){\n    which  sshfs &> /dev/null || { clear; how_to_install sshfs; pause; return; }\n    clear; sshfs $sshfsopt \"$target\":\"$DEST\" \"$home\" || pause\n}\n\nunmountdest(){ mount | grep -q \"$home\" && umount \"$home\"; }\n\ncopy_files(){\n    local runner=\n    [[ $group ]] && runner='group_run'\n    ufilename=$(D \"COPY\" '' \"BACK\" '' --fselect $PWD/ 10 80)\n\tcase $ufilename:$? in\n         $PWD|$PWD/:0) return;;\n                  *:0) clear; $runner uploader; pause;;\n                  *:*) return;;\n\tesac           ;   copy_files\n}\n\nupload(){\n    local runner=\n    [[ $group ]] && runner='group_run'\n    ufilelist=( $(ls -sh1 $home | awk '{print $2,$1}') )\n\tufilename=$(D \"UPLOAD\" '' \"BACK\" '' --menu \"Select file\\folder to upload:\" 0 0 0 \"${ufilelist[@]:2}\")\n\tcase $? in\n         0) [[ $ufilename ]] || upload\n            clear; $runner uploader; pause;;\n         *) return;;\n\tesac;   upload\n}\n\ndownload(){\n    [[ $dfilelist ]] || {\n        WAIT\n        dfilelist=$(ssh $SSH_OPT $target ls -sh1 $DEST 2>&1) \\\n            && dfilelist=( $(awk '{print $2,$1}' <<< \"$dfilelist\") ) \\\n            || {\n                clear\n                echo \"$dfilelist\"\n                pause\n                dfilelist=\n                second_dialog\n            }\n        GO\n    }\n\tdfilename=$(D \"DOWNLOAD\" '' \"BACK\" '' --menu \"Select file\\folder to download:\" 0 0 0 \"${dfilelist[@]:2}\")\n\tcase $? in\n         0) [[ $dfilename ]] || download\n            clear\n            printf \"Downloading $BLD$dfilename$DEF\\n\"\n            rsync $rsyncopt --rsh=\"ssh $SSH_OPT\" $target:\"$DEST/$dfilename\" . || pause;;\n         *) return;;\n\tesac;   download\n}\n\n#------------------------{ Switch menu mode to contents view or full list }---------------------------------------------\nsave_tmp(){ echo \"$1\" > \"$tmpfile\"; chmod 600 \"$tmpfile\"; }\nnew_list(){\n    list=(); match=\n    for item in \"${selected_list[@]}\" \"${filtered_list[@]}\" \"${fullist[@]}\"; {\n        case         $item:$match    in\n                 *{\\ *\\ }*:1) break  ;;\n           *{\\ $filter\\ }*:*) match=1;;\n        esac\n        [[ $match ]] && list+=( \"$item\" )\n    }\n\n    [[ $filter =~ Selected ]] && return\n    [[ ${list[*]} ]] && save_tmp \"filter='$filter'\" || { list=( \"${fullist[@]}\" ); rm \"$tmpfile\"; }\n}\n\ncontents_menu(){\n    local filter_tmp=$filter selected= filtered=\n    [[ ${selected_list[@]} ]] && selected='Selected_hosts'\n    [[ ${filtered_list[@]} ]] && filtered='Filtered_hosts'\n    local btns=('SELECT' 'RUN COMMAND' 'RELOAD' 'BACK')\n\tfilter=$(D \"${btns[@]}\" --no-items --menu \"Select list of hosts:\" 0 0 0 \"All\" $selected $filtered \"${content[@]}\" '' \"$qcmd\" \"$qbut1\")\n\tcase $filter:$? in\n\t          \"\":[03]) contents_menu;;\n\t     \"$qcmd\":[03]) contents_menu;;\n        \"$qbut1\":[03]) filter_items 'first_dialog';;\n             All:0) list=( \"${selected_list[@]}\" \"${fullist[@]}\" )\n                    save_tmp       \"filter=\";;\n               *:3) second_dialog \"$filter\" ;;\n               *:2) filter=$filter_tmp;;\n               *:1) restart; contents_menu;;\n               *:0) new_list;;\n\tesac        ;   first_dialog\n}\n# Dialog buttons order and exit codes\n#<OK> <Extra> <Cancel> <Help>\n# 0      3       1       2\n#------------------------{ Selector }-----------------------------------------------------------------------------------\ndeclare -A slctd_hosts\ndeclare -A slctd_groups\n\ngen_selected_list(){\n    local k\n    selected_list=('-----------{ Selected_hosts }-----------' '_LINE_')\n    for k in \"${!slctd_hosts[@]}\"; { selected_list+=(\"$k\" \"${slctd_hosts[$k]}\"); }\n}\n\nslct_dslct(){\n    local desc k v\n\n    # remove from selection\n    [[ ${slctd_hosts[$target]} && $1 != \"select\" ]] && {\n        unset slctd_hosts[$target]\n        gen_selected_list\n        ((${#selected_list[@]}==2)) && unset selected_list\n        return\n    }\n\n    [[ $1 == \"deselect\" ]] && return\n    # add to selection\n    for ((k=0,v=1; k<N; k++,v++)); { [[ ${fullist[k]} =~ $target ]] && { desc=${fullist[v]}; break; }; }\n    slctd_hosts[$target]=$desc\n    gen_selected_list\n}\n\nslct_dslctgroup_run(){\n    group_list=(\"${list[@]:2}\")\n    for ((i=0;  i<${#group_list[@]}; i+=2)); do\n        target=\"${group_list[$i]}\"\n        slct_dslct \"${command,,}\"\n    done\n\n    [[ ${slctd_groups[\"$group\"]} ]] && unset slctd_groups[\"$group\"] || slctd_groups[\"$group\"]=$group\n}\n\n#------------------------{ Items filter }-------------------------------------------------------------------------------\nfilter_items(){\n    local n d\n    declare -A filtered_hosts\n    filtered_list=('-----------{ Filtered_hosts }-----------' \"$descline\")\n    items_filter=$(D 'FILTER' 'CLEAR' 'BACK' '' --max-input 60 --inputbox 'Enter filtering pattern' 10 60 $items_filter)\n\tcase $items_filter:$? in\n                '':0|*:3) unset filtered_list items_filter; contents_menu;;\n                     *:0) items_filter=${items_filter,,}\n                          for ((n=0,d=1; n<N; n+=2,d+=2)); do\n                              filter_by=${fullist[n]}\n                              [[ $items_filter  =~ ^\\#.*$             ]] && filter_by=${fullist[d]}\n                              [[ ${filter_by,,} =~ ^-+.*-+$           ]] && continue\n                              [[ ${filter_by,,} =~ ${items_filter/\\#} ]] && {\n                                  [[ ${filtered_hosts[${fullist[n]}]} ]] || { # to remove dupes\n                                      filtered_list+=(\"${fullist[n]}\" \"${fullist[d]}\")\n                                        filtered_hosts[${fullist[n]}]=\"${fullist[d]}\"\n                                  }\n                              }\n                          done\n                          ((${#filtered_list[@]}==2)) && { clear; pause 'Nothing found'; $1; }\n                          list=( \"${filtered_list[@]}\" );;\n                     *:1) $1;;\n\tesac;                 $1\n}\n\n#------------------------{ First dialog - Select target host }----------------------------------------------------------\nfirst_dialog(){\n    local btns\n    group= dfilelist=\n    [[ $OPT =~ name ]] && btns=('GET NAME' '' 'EXIT' 'CONTENTS') || btns=('CONNECT/SELECT' 'RUN COMMAND' 'EXIT' 'CONTENTS')\n\ttarget=$(D \"${btns[@]}\" --menu \"Select host to connect to. $USERNOTE\" 0 0 0 \"${list[@]//_LINE_/$descline}\" \"${quick_butt[@]}\")\n\tcase $target:$? in\n\t          \"\":0) first_dialog;;\n\t     \"$qcmd\":0) first_dialog;;\n\t    \"$qbut1\":[03]) filter_items 'first_dialog';;\n       *{\\ *\\ }*:0) filter=${target//*\\{ }; filter=${filter// \\}*}; new_list; first_dialog ;;\n       *{\\ *\\ }*:3) filter=${target//*\\{ }; filter=${filter// \\}*}; second_dialog \"$filter\";;\n               *:0) [[ $OPT =~ name ]] && return || { go_to_target; first_dialog; };;\n      \t       *:1) bye;;\n               *:2) contents_menu;;\n      \t       *:3) second_dialog;;\n               *:*) contents_menu;;\n  \tesac\n}\n\ngsel(){\n    [[ $group == \"Selected_hosts\" ]] && { slct_grp=(); return 1; }\n    [[ ${slctd_groups[\"$group\"]}  ]] &&                return 0\n}\n#------------------------{ Second dialog - Select command }-------------------------------------------------------------\nsecond_dialog(){\n    local headings    commands                    singleornot         runner             connect\n        group=\"$1\"    commands='cmdlist[@]'       singleornot='host'  runner=''          connect='CONNECT'\n    [[ $group ]] && { commands='cmdlist_group[@]' singleornot='group' runner='group_run' connect=''  filter=\"$group\"\n                      slct_grp=(Select   \"Add all hosts of \\Z4$group\\Z0 to tmp group \\Z4Selected_hosts\\Z0\"      '' '')\n              gsel && slct_grp=(Deselect \"Remove all hosts of \\Z4$group\\Z0 from tmp group \\Z4Selected_hosts\\Z0\" '' '')\n    }\n\n    headings=\"Select command to run on $singleornot \\Z4${group:-$target}\\Z0. $USERNOTE\"\n                                       slct=(Select   \"Add \\Z4$target\\Z0 to tmp group \\Z4Selected_hosts\\Z0\"      '' '')\n    [[ ${slctd_hosts[\"$target\"]} ]] && slct=(Deselect \"Remove \\Z4$target\\Z0 from tmp group \\Z4Selected_hosts\\Z0\" '' '')\n\n    new_list; cmdlist_renew\n\tcommand=$(D 'RUN' \"$connect\" 'BACK' 'CONTENTS' --menu \"$headings\" 0 0 0 \"${!commands}\")\n\tcase $command:$? in\n\t           '':0) :;;\n\t     *'elect':0) slct_dslct$runner  ;;\n       \"Add tab\"*:0) $runner tabbed \"$0\";;\n       \"Ssh tab\"*:0) $runner tabbed \"ssh $SSH_OPT _target_\";;\n            Alias:0) clear; $runner add_aliases; pause;;\n             Info:0) clear; $runner system_info; pause;;\n              Yes:0) clear; $runner ssh_yes    ; pause;;\n           Fix_id:0) clear; $runner fix_id     ; pause;;\n           Sshkey:0) add_sshkey  ;;\n             Copy:0) copy_files  ;;\n           Upload:0) upload      ;;\n           Custom:0) custom      ;;\n           Script:0) script      ;;\n         Username:0) username    ;;\n             Dest:0) downpath    ;;\n             Home:0) homepath    ;;\n            Mount:0) mountdest   ;;\n          Unmount:0) unmountdest ;;\n         Download:0) download    ;;\n            Local:0) local_port  ;;\n           Remote:0) remote_port ;;\n           Tunnel:0) portunneling;;\n         ShowConf:0) show_conf   ;;\n         EditConf:0) edit_conf   ;;\n                *:0) clear; $runner run_command; pause;;\n                *:2) contents_menu;;\n                *:3) go_to_target ;;\n                *:*) first_dialog ;;\n\tesac         ;   second_dialog \"$group\"\n}\n\n#-------------{ Create the list of hosts. Get hosts and descriptions from ~/.ssh/config* }------------------------------\ndeclare -A hostnames=()\ndesclength=20\nrestart(){\n    fullist=()\n    content=()\n    hostnames=()\n    selected_list=()\n    CONFILES=$( # SSH confiles list.\n        shopt -s nullglob\n        echo  \"$sshdir\"/config*[!~]\n        [[ -f \"$sshdir\"/config ]] && echo \"$sshdir\"/config\n    )\n\n    while read -r name hostname desc; do\n        case    ${name,,} in\n            'group_name') name=\"{ $desc }\"\n                          name_length=${#name}\n                          name_left=$[(40-name_length)/2]\n                          name_right=$[40-(name_left+name_length)]\n                          printf -v tmp \"%${name_left}s_NAME_%${name_right}s\"\n                          tmp=${tmp// /-}  name=${tmp//_NAME_/$name}\n                          content+=( \"$desc\" );  desc='_LINE_';;\n                    '#'*) continue;;\n        esac\n        ((${#desc}>desclength)) && desclength=${#desc}\n        hostnames[\"$name\"]=$hostname #Create host:hostname pairs in hostnames array\n        fullist+=(\"$name\" \"$desc\")   #Add Host and Description to the list\n    done < <(gawk '\n    BEGIN{IGNORECASE=1}\n    /Host /{\n        strt=1\n        host=$2\n        desc=gensub(/^.*Host .* #(.*)/, \"\\\\1\", \"g\", $0)\n        desc=gensub(/(.*)#.*/,          \"\\\\1\", \"g\", desc)\n        next\n    }\n    strt && host == \"'\"$group_id\"'\"{\n        print \"group_name\", \"dummy\", desc\n        strt=0\n    }\n    strt && /HostName /{\n        hostname=$2\n        print host, hostname, desc\n        strt=0\n    }'  $CONFILES)\n\n    descline=$(line - $desclength)\n    list=( \"${fullist[@]}\" \"${quick_butt[@]}\" )\n    N=${#fullist[@]}\n     qcmd='-----------{ Quick commands }-----------'\n    qbut1='1             Filter items'\n    quick_butt=(\n        ''        ''\n        \"$qcmd\"   \"$descline\"\n        \"$qbut1\"  \"Create list of hosts filtered by pattern\"\n    )\n}\n\n#--{ Go baby, GO!) }--\ncheck_confile;  restart\n[[ -e $tmpfile  ]] && . \"$tmpfile\"\n[[    $filter   ]] &&    new_list\n[[ $target      ]] || first_dialog\n[[ $OPT =~ name ]] && { echo $target; exit; }\n[[ $target      ]] && second_dialog\n\nbye #:)\n"
  }
]