#!/usr/bin/env bash # # myssh.sh — MobaXterm / Linux 通用纯 bash不依赖 Python。 # 默认地址簿: /.myssh/book.tsv可用 MYSSH_DB 或 -f 指定 # 每行: userTABhostTABlabelTAB密码的base64 # # 用法: # ./myssh.sh # ./myssh.sh -l 关键字 # ./myssh.sh userhost [-l label] # # MobaXterm: 若从 Windows 拷贝脚本请保持 LF 换行CRLF 会导致 shebang 失效sh/dash 跑不起来。 # 若仍用 sh 执行下面会尝试自动切到 bash。 # if [ -z ${BASH_VERSION:-} ]; then exec bash $0 $ fi set -euo pipefail DEFAULT_DB/.myssh/book.tsv die() { echo myssh: $* 2; exit 1; } b64_encode() { # Must never fail under set -e: pipefail failing base64/openssl would otherwise exit the whole script. local out out$(printf %s $1 | base64 -w0 2/dev/null) || true if [[ -n ${out:-} ]]; then printf %s $out; return 0; fi out$(printf %s $1 | openssl base64 -e -A 2/dev/null) || true if [[ -n ${out:-} ]]; then printf %s $out; return 0; fi out$(printf %s $1 | base64 2/dev/null | tr -d \n\r) || true if [[ -n ${out:-} ]]; then printf %s $out; return 0; fi die cannot base64-encode password (need GNU/base64 or openssl in PATH) } b64_decode() { local out out$(printf %s $1 | base64 -d 2/dev/null) || true if [[ -n ${out:-} ]]; then printf %s $out; return 0; fi out$(printf %s $1 | openssl base64 -d -A 2/dev/null) || true if [[ -n ${out:-} ]]; then printf %s $out; return 0; fi printf } usage() { cat EOF myssh.sh — bash SSH helper with local address book (no Python). Usage: myssh.sh myssh.sh -l KEYWORD myssh.sh userhost [-l LABEL] Options: -l, --label LABEL With userhost: save label. Alone: filter list by label substring. -f, --file PATH Book file (default: ~/.myssh/book.tsv or MYSSH_DB). -h, --help This help. MobaXterm: run inside local bash; chmod x myssh.sh; optional sshpass for saved passwords. EOF } TARGET LABEL_SET0 LABEL_VAL DB_PATH while [[ $# -gt 0 ]]; do case $1 in -h|--help) usage exit 0 ;; -l|--label) [[ $# -ge 2 ]] || die missing value for $1 LABEL_SET1 LABEL_VAL$2 shift 2 ;; -f|--file) [[ $# -ge 2 ]] || die missing value for $1 DB_PATH$2 shift 2 ;; -*) die unknown option: $1 ;; *) [[ -z $TARGET ]] || die unexpected extra argument: $1 TARGET$1 shift ;; esac done DB${DB_PATH:-${MYSSH_DB:-$DEFAULT_DB}} mkdir -p $(dirname $DB) 2/dev/null || true umask 077 # --- 读写记录TSV--- load_all_lines() { [[ -f $DB ]] || return 0 # 跳过空行 grep -v ^[[:space:]]*$ $DB || true } write_all_lines() { local tmp tmp$(mktemp ${TMPDIR:-/tmp}/myssh.XXXXXX) if [[ $# -eq 0 ]]; then : $tmp else printf %s\n -- $ $tmp fi chmod 600 $tmp 2/dev/null || true mv -f $tmp $DB chmod 600 $DB 2/dev/null || true } find_record_pw_label() { # args: user host - sets FOUND_PW FOUND_LABEL via nameref or globals local u$1 h$2 FOUND_PW FOUND_LABEL local line user host label pwb64 while IFS$\t read -r user host label pwb64 || [[ -n ${user:-} ]]; do [[ -z ${user:-} ]] continue if [[ $user $u $host $h ]]; then FOUND_PW$(b64_decode ${pwb64:-}) FOUND_LABEL${label:-} return 0 fi done (load_all_lines) return 1 } merge_save() { local u$1 h$2 pw$3 lab$4 local pwb64 new_line user host label oldb pwb64$(b64_encode $pw) new_line$(printf %s\t%s\t%s\t%s $u $h $lab $pwb64) local -a out() local found0 while IFS$\t read -r user host label oldb || [[ -n ${user:-} ]]; do [[ -z ${user:-} ]] continue if [[ $user $u $host $h ]]; then out($new_line) found1 else out($(printf %s\t%s\t%s\t%s $user $host ${label:-} ${oldb:-})) fi done (load_all_lines) if [[ $found -eq 0 ]]; then out($new_line) fi if [[ ${#out[]} -eq 0 ]]; then write_all_lines else write_all_lines ${out[]} fi } label_contains() { local hay$1 nd$2 local lh lnd lh$(printf %s $hay | tr [:upper:] [:lower:]) lnd$(printf %s $nd | tr [:upper:] [:lower:]) [[ $lh *$lnd* ]] } run_ssh() { local u$1 h$2 pw$3 local t${u}${h} if [[ -n $pw ]] command -v sshpass /dev/null 21; then SSHPASS$pw sshpass -e ssh $t return $? fi if [[ -n $pw ]] ! command -v sshpass /dev/null 21; then echo myssh: sshpass not found; running plain ssh (install sshpass for saved password). 2 fi ssh $t } pick_ssh_from_lines() { # Arg $1: path to TSV file (do not feed list on stdin — stdin must stay the terminal for read). local book${1:?pick_ssh_from_lines: missing book file} [[ -f $book ]] || { echo (empty) 2; return 1; } local -a users() hosts() pws() local i0 user host label pwb64 pw while IFS$\t read -r user host label pwb64 || [[ -n ${user:-} ]]; do [[ -z ${user:-} ]] continue pw$(b64_decode ${pwb64:-}) i$((i 1)) users($user) hosts($host) pws($pw) printf %3d [%s] %s%s\n $i ${label:-} $user $host done $book if [[ $i -eq 0 ]]; then echo (empty) 2 return 1 fi local raw echo -n Select 1-${i} (Enter to cancel): 2 if [[ -r /dev/tty ]]; then read -r raw /dev/tty || true else read -r raw || true fi raw${raw:-} [[ -n $raw ]] || return 1 [[ $raw ~ ^[0-9]$ ]] || { echo myssh: invalid selection 2; return 1; } if [[ $raw -lt 1 || $raw -gt $i ]]; then echo myssh: out of range 2 return 1 fi SSH_PICK_U${users[$((raw - 1))]} SSH_PICK_H${hosts[$((raw - 1))]} SSH_PICK_PW${pws[$((raw - 1))]} return 0 } MYSSH_PW_RESULT read_password() { local prompt$1 MYSSH_PW_RESULT if [[ ! -t 0 ]]; then if [[ ${MYSSH_PASSWORDx} x ]]; then echo myssh: stdin not a TTY; using MYSSH_PASSWORD from environment. 2 MYSSH_PW_RESULT${MYSSH_PASSWORD-} return 0 fi die stdin is not a TTY. Use a real terminal, SSH keys (empty password), or export MYSSH_PASSWORD for this session. fi echo myssh: password is hidden (no stars); type then press Enter. 2 # Avoid $(read_password): command substitution runs in a subshell and can break TTY reads on some terminals. read -r -s -p $prompt MYSSH_PW_RESULT || true echo 2 } # ----- 主流程 ----- if [[ -z $TARGET $LABEL_SET -eq 1 ]]; then echo Entries matching label ${LABEL_VAL}: tmpf$(mktemp ${TMPDIR:-/tmp}/myssh.f.XXXXXX) while IFS$\t read -r user host label pwb64 || [[ -n ${user:-} ]]; do [[ -z ${user:-} ]] continue if label_contains ${label:-} $LABEL_VAL; then printf %s\t%s\t%s\t%s\n $user $host ${label:-} ${pwb64:-} fi done (load_all_lines) $tmpf if ! pick_ssh_from_lines $tmpf; then rm -f $tmpf exit 0 fi rm -f $tmpf run_ssh $SSH_PICK_U $SSH_PICK_H $SSH_PICK_PW exit $? fi if [[ -z $TARGET ]]; then echo Saved entries: tmpf$(mktemp ${TMPDIR:-/tmp}/myssh.f.XXXXXX) load_all_lines $tmpf if ! pick_ssh_from_lines $tmpf; then rm -f $tmpf exit 0 fi rm -f $tmpf run_ssh $SSH_PICK_U $SSH_PICK_H $SSH_PICK_PW exit $? fi # userhost (use RUSER/RHOST: avoid clobbering login USER / rare HOST env quirks) [[ $TARGET ** ]] || die expected userhost, got: $TARGET RUSER${TARGET%*} RHOST${TARGET#*} [[ -n $RUSER -n $RHOST ]] || die invalid userhost: $TARGET HAVE0 PREV_PW PREV_LABEL if find_record_pw_label $RUSER $RHOST; then HAVE1 PREV_PW$FOUND_PW PREV_LABEL$FOUND_LABEL fi if [[ $HAVE -eq 1 ]]; then read_password Password [Enter to keep saved, or type new]: PW$MYSSH_PW_RESULT [[ -n $PW ]] || PW$PREV_PW else read_password Password (empty for key-only; entry still saved): PW$MYSSH_PW_RESULT fi if [[ $LABEL_SET -eq 1 ]]; then EFFECTIVE_LABEL$LABEL_VAL elif [[ $HAVE -eq 1 ]]; then EFFECTIVE_LABEL$PREV_LABEL else EFFECTIVE_LABEL fi echo myssh: saving book - $DB 2 merge_save $RUSER $RHOST $PW $EFFECTIVE_LABEL echo myssh: starting ssh... 2 run_ssh $RUSER $RHOST $PW exit $?