
当服务器从 3 台变成 30 台,ssh 这件事就开始变得难受了。IP 记不住,密钥和密码混着来,有的机器登进去还得手动 sudo -i。我一直想要一个足够简单的命令:记住一个短别名,敲一下就进去。
于是有了 s——一个不到 600 行的 bash 脚本,把日常 SSH 运维压缩成一个字母开头的命令。这篇文章记录它的设计取舍、完整功能,以及几个写它时踩到的坑。
一分钟感受
$ s
别名 目标 端口 登录 描述
hk-01 root@1.2.3.4 22 密钥 香港 主力节点
db-1 deploy@5.6.7.8 2222 密钥 业务库
win-box admin@9.9.9.9 22 密码 ⇒root 跳板机
$ s hk-01 # 直接连,密钥/密码自动判断
$ s ping # 全部主机一眼看在线/离线
$ s run 'db-*' 'df -h /' # 一批机器上跑同一条命令一个核心决定:不发明新格式,就用 ~/.ssh/config
市面上的 SSH 管理器大多有自己的一套配置文件/数据库。我不想要——那意味着 scp、rsync、VS Code Remote 这些工具都得再教一遍。
s 的所有主机就是标准的 ~/.ssh/config 条目:
Host hk-01
HostName 1.2.3.4
User root
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes好处是白拿的:
s hk-01能连,原生ssh hk-01也能连;scp file hk-01:/tmp/、rsync ... hk-01:...全部自动认识这个别名;- 别的工具、别的机器只要有这份 config 就通用,
s只是个更顺手的入口,不是一道锁。
那些 s 自己需要、而 SSH 又不认识的元数据(描述、登录方式、是否自动切 root),我用注释行存在同一个 Host 块里:
Host win-box
#s-desc 跳板机
#s-auth password
#s-root yes
HostName 9.9.9.9
User adminssh 会忽略所有 # 开头的行,所以这些标记对原生 ssh 完全透明,而 s 用 awk 把它们读出来。一份配置,两套解读,互不打架:
~/.ssh/config
┌───────────────────────┐
│ Host win-box │
#s-* 注释 │ #s-desc 跳板机 │ ← ssh 视而不见
只有 s 读 │ #s-auth password │ (# 开头一律忽略)
│ #s-root yes │
│ HostName 9.9.9.9 │ ← ssh 和 s 都读
标准字段 │ User admin │
└───────────┬───────────┘
│
┌───────────┴───────────┐
▼ ▼
原生 ssh / scp s(多读几行注释,
rsync / VS Code 知道要走密码、切 root)
(只认标准字段)密钥与密码:密码不落明文
加机器就一条命令,密钥、密码都支持:
s add hk-01 root@1.2.3.4 -i ~/.ssh/id_ed25519 -d "香港 主力节点"
s add win-box admin@9.9.9.9 --pass -d "跳板机" # --pass 会提示输入密码密钥登录没什么好说的,交给 ssh 就行。密码登录麻烦在两点:一是明文存哪,二是怎么喂给 ssh。
~/.ssh/config 里只留一行 #s-auth password 做标记,真正的密码用 security add-generic-password 写入钥匙串;连接时再 security find-generic-password 取出,通过 sshpass 喂给 ssh。配置文件即使被人翻到,也拿不到密码。连接时 s 看到 #s-auth password 就自动走 sshpass,你完全无感:
s win-box # 自动取出密码登录,你什么都不用输改密码、把密码机升级成免密,也各有一条命令:
s passwd win-box # 只更新 Keychain 里的密码
s cp win-box # 把本机公钥拷过去(ssh-copy-id),之后就能免密了登录后自动切 root
有些机器登进去是普通用户,第一件事永远是 sudo -i。把它交给 s:
s root win-box on # 开启:以后 s win-box 连上就自动 sudo -i对密码机还有个细节:sudo 通常要再输一次密码,而这个密码一般和登录密码是同一个。所以 s 会把 Keychain 里那把密码顺手喂给 sudo——真正做到一条命令直达 root shell,中间一次都不用输密码。喂失败(比如两个密码确实不一样)就自动回退成手动输入,不会卡住。
s win-box
# → 自动填 SSH 密码 → 自动用同一密码过 sudo → 直接落在 root@win-box列表、传输、连通性、改配置
日常琐事都收敛成子命令:
s ls # 列出所有主机(裸 s 也一样)
s ping # TCP 探测,标注每台在线/离线
s ping 'db-*' win-box # 只查一部分,支持通配符
s put ./app.tar.gz db-1:/root/ # 上传(密码机自动走 sshpass)
s get db-1:/var/log/app.log ./ # 下载
s set hk-01 host 1.2.3.9 # 改 IP(云主机重启后公网 IP 变了很常见)
s desc hk-01 "香港 主力节点" # 改描述
s edit # 直接 $EDITOR 打开 configput/get 会自动从参数里认出 别名:路径 那部分,判断该走原生 scp 还是 sshpass scp,你不用关心目标机是密钥还是密码登录。
三个让 fleet 变顺手的命令
机器一多,光能连还不够。
fzf 模糊选机——别名记不全时,模糊搜索:
s pick # 弹出 fzf 列表,输关键字回车直接连端口转发——把远端只监听在本机的服务(数据库、内部 API 等)拉到本地直连:
s fwd db-1 8123 # 简写:本地:8123 → 远端 localhost:8123
s fwd jump 5432:10.0.0.100:5432 # 借 jump 当跳板,连它内网里的库5432:10.0.0.100:5432 中间那个地址是从服务器角度解析的,不是你本机。写 localhost 指的是「SSH 连上的那台服务器自己」,写内网 IP 则是让那台服务器当跳板去连它内网里的第三台机器——后者能帮你够到公网根本访问不到的机器。批量执行——一条命令扫一批机器,天然适合集群:
s run 'k8s-*' 'kubectl get nodes' # 通配符选一组
s run 'db-1,win-box' 'df -h /' # 逗号分隔多个选择器逐台执行、带标题分隔、末尾汇总成功/失败数,某台挂了不影响其它。
Tab 补全
作为一个每天要敲很多次的命令,补全是刚需。给 zsh 写了个补全函数丢进 fpath:
s <Tab>列出所有别名 + 子命令;s hk<Tab>补成hk-01;s rm <Tab>/s ping <Tab>/s fwd <Tab>补别名。
候选是实时从 ~/.ssh/config 读的——所以 s add 新加的机器会自动出现在补全里,不用维护第二份清单。
几个踩到的坑
写这种「看起来很简单」的小工具,坑往往在细节里。挑几个有代表性的。
1. 中文变量名后面跟全角括号,会报 unbound variable。 开了 set -u 后,"未找到别名:$alias(...)" 这一句直接崩:
line 147: alias�: unbound variable原因是 bash 把 $alias 后面紧跟的全角括号 ( 的字节吞进了变量名,去找一个不存在的变量。解法是老老实实用花括号界定:${alias}。
2. 中文对齐不能用 printf %-Ns。 printf 按字符个数补空格,但中文在终端里占两格,于是带中文的列全歪了。得按显示宽度补齐。有个不用起子进程的小技巧——对「纯 ASCII + 三字节中文」的字符串,显示宽度正好等于 (字符数 + 字节数) / 2:
dwidth() { local s="$1" c b; c=${#s}; local LC_ALL=C; b=${#s}; echo $(( (c + b) / 2 )); }${#s} 在 UTF-8 locale 下数的是字符数,临时切到 LC_ALL=C 再数一次就是字节数,两者一平均就得到显示宽度。
3. 密码对了却 Permission denied。 强制 PreferredAuthentications=password 时,某些带 PAM 的服务器其实走的是 keyboard-interactive,于是密码正确也被拒。把两种都允许即可:password,keyboard-interactive。
4. set -e 会让批量执行第一台失败就整体退出。 因为 set -e 遇到非零退出就中止。解法是把 ssh 放进 if 条件里——条件语境下的命令失败不触发 set -e:
if ssh ... "$alias" "$cmd"; then rc=0; else rc=$?; fi5. 密码机交互登录,连上却「秒退、无输出、退出码 0」。 这个最迷惑。原因是 sshpass 介入后,ssh 的 stdin 不再被识别成终端,于是它不向远端申请伪终端;远端 shell 一起来就读到 EOF,立刻正常退出——所以退出码是 0,什么都没留下。解法是交互登录时显式 -t 强制分配伪终端:
sshpass -e ssh -t "$alias" # 少了这个 -t,交互 shell 会瞬间关掉6. 密码机第一次连,sshpass 直接以退出码 6 失败。 因为这台的主机公钥还不在 known_hosts 里,ssh 想弹「是否信任该主机」的确认,而 sshpass 没法回答这个交互提示,只好放弃。解法是加 StrictHostKeyChecking=accept-new:首次自动接受新主机密钥,但主机密钥若变更仍然拒绝——安全性上是个合理折中。
小结
s 没做什么高深的事,它只是把「一堆 SSH 服务器」这件事里所有重复动作——记 IP、切密钥密码、切 root、传文件、查状态、批量跑命令——各压成一条短命令,同时坚持一个原则:底层永远是标准 ~/.ssh/config,绝不发明新格式,绝不把自己变成一道锁。
如果你也管着一堆机器,不妨拿去改。代码已经放到 GitHub,clone 下来 ./install.sh 就能用;完整源码也附在文末。
完整源码 s(放进 PATH,chmod +x 即用;密码功能需要 sshpass,fzf 选机需要 fzf)
#!/usr/bin/env bash
# s — 极简 SSH 连接管理器(基于标准 ~/.ssh/config)
#
# s 列出所有主机
# s <别名> 连接(自动判断密钥/密码登录)
# s add <别名> <user@host> [-p PORT] [-i KEYFILE] [--pass] [-d "描述"] [--root]
# s rm <别名> 删除一台主机(含 Keychain 里的密码)
# s set <别名> host|user|port <值> 快速改某个字段
# s desc <别名> [描述…] 设置/修改描述
# s root <别名> [on|off] 开/关"登录后自动 sudo -i 切 root"
# s passwd <别名> 修改密码登录主机在 Keychain 里的密码
# s put/get [-r] … 文件传输(密码机自动走 sshpass)
# s ping [别名/通配符…] TCP 连通性检查
# s pick fzf 模糊选机并连接
# s fwd <别名> <spec…> 端口转发
# s run <选择器> <命令…> 批量执行
# s edit / s cp <别名> 编辑 config / 拷公钥升级免密
set -euo pipefail
CONFIG="$HOME/.ssh/config"
KC_SERVICE="ssh-s-tool"
# ---- 颜色 ----
if [ -t 1 ]; then
B=$'\033[1m'; G=$'\033[32m'; Y=$'\033[33m'; C=$'\033[36m'; R=$'\033[31m'; D=$'\033[2m'; N=$'\033[0m'
else
B=""; G=""; Y=""; C=""; R=""; D=""; N=""
fi
err() { printf "%s✗%s %s\n" "$R" "$N" "$*" >&2; }
info() { printf "%s•%s %s\n" "$C" "$N" "$*"; }
# 终端显示宽度(ASCII=1,中文=2):纯 ASCII+3字节中文时 宽度=(字符数+字节数)/2
dwidth() { local s="$1" c b; c=${#s}; local LC_ALL=C; b=${#s}; echo $(( (c + b) / 2 )); }
pad() { local s="$1" w="$2" n; n=$(( w - $(dwidth "$s") )); (( n < 0 )) && n=0; printf "%s%*s" "$s" "$n" ""; }
ensure_config() {
mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh" 2>/dev/null || true
[ -f "$CONFIG" ] || { : >"$CONFIG"; }
chmod 600 "$CONFIG" 2>/dev/null || true
}
host_exists() { # $1=alias
awk -v a="$1" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { for (i=2;i<=NF;i++) if ($i==a) { found=1; exit } }
END { exit(found?0:1) }
' "$CONFIG" 2>/dev/null
}
host_field() { # $1=alias $2=field
awk -v a="$1" -v f="$2" '
BEGIN { fl=tolower(f) }
/^[[:space:]]*[Hh]ost[[:space:]]/ { inblk=0; for (i=2;i<=NF;i++) if ($i==a) inblk=1; next }
inblk && tolower($1)==fl { print $2; exit }
' "$CONFIG" 2>/dev/null
}
is_password_auth() { # $1=alias
awk -v a="$1" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { inblk=0; for (i=2;i<=NF;i++) if ($i==a) inblk=1; next }
inblk && $1=="#s-auth" && $2=="password" { found=1; exit }
END { exit(found?0:1) }
' "$CONFIG" 2>/dev/null
}
is_auto_root() { # $1=alias
awk -v a="$1" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { inblk=0; for (i=2;i<=NF;i++) if ($i==a) inblk=1; next }
inblk && $1=="#s-root" && $2=="yes" { found=1; exit }
END { exit(found?0:1) }
' "$CONFIG" 2>/dev/null
}
host_desc() { # $1=alias
awk -v a="$1" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { inblk=0; for (i=2;i<=NF;i++) if ($i==a) inblk=1; next }
inblk && $1=="#s-desc" { line=$0; sub(/^[[:space:]]*#s-desc[[:space:]]*/,"",line); print line; exit }
' "$CONFIG" 2>/dev/null
}
# ---- Keychain ----
kc_set() { security add-generic-password -U -s "$KC_SERVICE" -a "$1" -w "$2" >/dev/null 2>&1; }
kc_get() { security find-generic-password -s "$KC_SERVICE" -a "$1" -w 2>/dev/null; }
kc_del() { security delete-generic-password -s "$KC_SERVICE" -a "$1" >/dev/null 2>&1 || true; }
cmd_list() {
ensure_config
if ! grep -qE '^[[:space:]]*[Hh]ost[[:space:]]' "$CONFIG" 2>/dev/null; then
info "还没有任何主机,用 ${B}s add${N} 添加一台。"; return 0
fi
printf "%s%s %s %s %s %s%s\n" "$B" \
"$(pad 别名 20)" "$(pad 目标 24)" "$(pad 端口 6)" "$(pad 登录 6)" "描述" "$N"
awk '/^[[:space:]]*[Hh]ost[[:space:]]/ { for (i=2;i<=NF;i++) print $i }' "$CONFIG" \
| grep -vE '[*?]' | while read -r alias; do
[ -z "$alias" ] && continue
local_host="$(host_field "$alias" HostName)"; [ -z "$local_host" ] && local_host="$alias"
user="$(host_field "$alias" User)"; [ -z "$user" ] && user="(默认)"
port="$(host_field "$alias" Port)"; [ -z "$port" ] && port="22"
desc="$(host_desc "$alias")"
if is_password_auth "$alias"; then acolor="$Y"; aname="密码"; else acolor="$G"; aname="密钥"; fi
local roottag=""; is_auto_root "$alias" && roottag="${G}⇒root${N} "
printf "%s%s%s %s %s %s%s%s %b%s%s%s\n" \
"$C" "$(pad "$alias" 20)" "$N" \
"$(pad "${user}@${local_host}" 24)" "$(pad "$port" 6)" \
"$acolor" "$(pad "$aname" 6)" "$N" "$roottag" "$D" "$desc" "$N"
done
}
cmd_connect() { # $1=alias, 其余透传给 ssh
ensure_config
local alias="$1"; shift || true
if ! host_exists "$alias"; then
err "未找到别名:${alias}(用 ${B}s${N} 查看全部,或 ${B}s add${N} 添加)"; exit 1
fi
local auto_root=0
is_auto_root "$alias" && [ $# -eq 0 ] && auto_root=1
if is_password_auth "$alias"; then
local pw; pw="$(kc_get "$alias")"
[ -z "$pw" ] && { err "别名 $alias 标记为密码登录,但 Keychain 里没有密码。"; exit 1; }
# accept-new:首次自动接受新主机密钥(sshpass 无法回答交互提示,否则退出码 6 秒退)
local sshopts=(-o PreferredAuthentications=password,keyboard-interactive -o PubkeyAuthentication=no -o StrictHostKeyChecking=accept-new)
if [ "$auto_root" = 1 ]; then
# 用同一密码喂 sudo(base64 传输避开特殊字符);喂失败则回退手动输入
local b64; b64="$(printf '%s' "$pw" | base64 | tr -d '\n')"
exec env SSHPASS="$pw" sshpass -e ssh "${sshopts[@]}" -t "$alias" \
"echo $b64 | base64 -d 2>/dev/null | sudo -S -v 2>/dev/null; exec sudo -i"
elif [ $# -eq 0 ]; then
# 交互登录:sshpass 下必须显式 -t,否则远端不分配伪终端、shell 秒退
exec env SSHPASS="$pw" sshpass -e ssh "${sshopts[@]}" -t "$alias"
else
exec env SSHPASS="$pw" sshpass -e ssh "${sshopts[@]}" "$alias" "$@"
fi
else
if [ "$auto_root" = 1 ]; then exec ssh -t "$alias" sudo -i; else exec ssh "$alias" "$@"; fi
fi
}
append_host() { # alias host user port keyfile is_pass desc auto_root
local alias="$1" host="$2" user="$3" port="$4" keyfile="$5" is_pass="$6" desc="${7:-}" auto_root="${8:-0}"
{
printf "\nHost %s\n" "$alias"
[ -n "$desc" ] && printf " #s-desc %s\n" "$desc"
[ "$auto_root" = "1" ] && printf " #s-root yes\n"
printf " HostName %s\n" "$host"
[ -n "$user" ] && printf " User %s\n" "$user"
[ -n "$port" ] && [ "$port" != "22" ] && printf " Port %s\n" "$port"
if [ "$is_pass" = "1" ]; then printf " #s-auth password\n"
elif [ -n "$keyfile" ]; then printf " IdentityFile %s\n IdentitiesOnly yes\n" "$keyfile"; fi
} >>"$CONFIG"
chmod 600 "$CONFIG" 2>/dev/null || true
}
cmd_add() {
ensure_config
local alias="" target="" user="" host="" port="22" keyfile="" is_pass=0 desc="" auto_root=0
if [ $# -ge 2 ]; then
alias="$1"; target="$2"; shift 2
if [[ "$target" == *@* ]]; then user="${target%@*}"; host="${target#*@}"; else host="$target"; fi
while [ $# -gt 0 ]; do
case "$1" in
-p|--port) port="$2"; shift 2 ;;
-i|--identity) keyfile="$2"; shift 2 ;;
--pass|--password) is_pass=1; shift ;;
-d|--desc|--description) desc="$2"; shift 2 ;;
--root) auto_root=1; shift ;;
*) err "未知参数: $1"; exit 1 ;;
esac
done
else
printf "%s别名%s (连接时敲的短名): " "$B" "$N"; read -r alias
[ -z "$alias" ] && { err "别名不能为空"; exit 1; }
printf "%s主机%s (IP 或域名): " "$B" "$N"; read -r host
[ -z "$host" ] && { err "主机不能为空"; exit 1; }
printf "%s用户%s [root]: " "$B" "$N"; read -r user; [ -z "$user" ] && user="root"
printf "%s端口%s [22]: " "$B" "$N"; read -r port; [ -z "$port" ] && port="22"
printf "%s描述%s (可留空): " "$B" "$N"; read -r desc
printf "%s登录方式%s 1) 密钥(默认) 2) 密码 : " "$B" "$N"; read -r m
if [ "$m" = "2" ]; then is_pass=1
else printf "%s指定私钥%s (留空=默认 ~/.ssh/id_*): " "$B" "$N"; read -r keyfile; fi
printf "%s登录后自动 sudo -i 切 root?%s [y/N]: " "$B" "$N"; read -r r
[[ "$r" =~ ^[Yy] ]] && auto_root=1
fi
if host_exists "$alias"; then err "别名 $alias 已存在。"; exit 1; fi
local pw=""
if [ "$is_pass" = "1" ]; then
printf "%s密码%s (输入时不显示): " "$B" "$N"; read -rs pw; echo
[ -z "$pw" ] && { err "密码不能为空"; exit 1; }
fi
append_host "$alias" "$host" "$user" "$port" "$keyfile" "$is_pass" "$desc" "$auto_root"
[ "$is_pass" = "1" ] && kc_set "$alias" "$pw"
echo
if [ "$is_pass" = "1" ]; then printf "%s✓%s 已添加 %s%s%s(密码登录,密码存于 Keychain)\n" "$G" "$N" "$C" "$alias" "$N"
else printf "%s✓%s 已添加 %s%s%s(密钥登录)\n" "$G" "$N" "$C" "$alias" "$N"; fi
printf " 现在可以:%ss %s%s\n" "$B" "$alias" "$N"
}
cmd_rm() {
ensure_config
local alias="${1:-}"; [ -z "$alias" ] && { err "用法:s rm <别名>"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
local tmp; tmp="$(mktemp)"
awk -v a="$alias" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { drop=0; if (NF==2 && $2==a) drop=1 }
!drop { print }
' "$CONFIG" >"$tmp"
awk 'NF||p{print} {p=NF}' "$tmp" >"$CONFIG"
rm -f "$tmp"; chmod 600 "$CONFIG" 2>/dev/null || true
kc_del "$alias"
printf "%s✓%s 已删除 %s%s%s\n" "$G" "$N" "$C" "$alias" "$N"
}
cmd_cp() {
ensure_config
local alias="${1:-}"; [ -z "$alias" ] && { err "用法:s cp <别名>"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
if is_password_auth "$alias"; then
local pw; pw="$(kc_get "$alias")"
info "用密码把公钥拷到 $alias …"
env SSHPASS="$pw" sshpass -e ssh-copy-id \
-o PreferredAuthentications=password,keyboard-interactive -o PubkeyAuthentication=no "$alias"
else
ssh-copy-id "$alias"
fi
}
cmd_passwd() {
ensure_config
local alias="${1:-}"; [ -z "$alias" ] && { err "用法:s passwd <别名>"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
is_password_auth "$alias" || { err "$alias 不是密码登录的主机。"; exit 1; }
local pw; printf "%s新密码%s (输入时不显示): " "$B" "$N"; read -rs pw; echo
[ -z "$pw" ] && { err "密码不能为空"; exit 1; }
kc_set "$alias" "$pw"
printf "%s✓%s 已更新 %s%s%s 的密码\n" "$G" "$N" "$C" "$alias" "$N"
}
cmd_desc() {
ensure_config
local alias="${1:-}"; shift || true
[ -z "$alias" ] && { err "用法:s desc <别名> <描述>(留空则清除)"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
local desc="$*" tmp; tmp="$(mktemp)"
awk -v a="$alias" -v d="$desc" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { intgt=(NF==2 && $2==a)?1:0; print; if (intgt && d!="") print " #s-desc " d; next }
intgt && $1=="#s-desc" { next }
{ print }
' "$CONFIG" >"$tmp"
mv "$tmp" "$CONFIG"; chmod 600 "$CONFIG" 2>/dev/null || true
[ -n "$desc" ] && printf "%s✓%s %s%s%s 的描述:%s\n" "$G" "$N" "$C" "$alias" "$N" "$desc" \
|| printf "%s✓%s 已清除 %s%s%s 的描述\n" "$G" "$N" "$C" "$alias" "$N"
}
cmd_root() {
ensure_config
local alias="${1:-}" want="${2:-}"
[ -z "$alias" ] && { err "用法:s root <别名> [on|off]"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
local on=0
case "$want" in
on|yes|1) on=1 ;; off|no|0) on=0 ;;
"") if is_auto_root "$alias"; then on=0; else on=1; fi ;;
*) err "参数只能是 on / off"; exit 1 ;;
esac
local tmp; tmp="$(mktemp)"
awk -v a="$alias" -v on="$on" '
/^[[:space:]]*[Hh]ost[[:space:]]/ { intgt=(NF==2 && $2==a)?1:0; print; if (intgt && on=="1") print " #s-root yes"; next }
intgt && $1=="#s-root" { next }
{ print }
' "$CONFIG" >"$tmp"
mv "$tmp" "$CONFIG"; chmod 600 "$CONFIG" 2>/dev/null || true
[ "$on" = "1" ] && printf "%s✓%s %s%s%s 登录后将自动 sudo -i\n" "$G" "$N" "$C" "$alias" "$N" \
|| printf "%s✓%s %s%s%s 已关闭自动 sudo -i\n" "$G" "$N" "$C" "$alias" "$N"
}
cmd_scp() {
ensure_config
[ $# -lt 2 ] && { err "用法:s put <本地…> <别名:路径> 或 s get <别名:路径> <本地>"; exit 1; }
local a cand host_alias=""
for a in "$@"; do
if [[ "$a" == *:* ]]; then cand="${a%%:*}"; if host_exists "$cand"; then host_alias="$cand"; break; fi; fi
done
[ -z "$host_alias" ] && { err "参数里没找到已知别名(格式 别名:远程路径)"; exit 1; }
if is_password_auth "$host_alias"; then
local pw; pw="$(kc_get "$host_alias")"
exec env SSHPASS="$pw" sshpass -e scp \
-o PreferredAuthentications=password,keyboard-interactive -o PubkeyAuthentication=no "$@"
else
exec scp "$@"
fi
}
cmd_set() {
ensure_config
local alias="${1:-}" field="${2:-}" value="${3:-}"
[ -z "$alias" ] || [ -z "$field" ] || [ -z "$value" ] && { err "用法:s set <别名> <host|user|port> <值>"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
local directive
case "$field" in
host|hostname|ip|HostName) directive="HostName" ;;
user|User) directive="User" ;;
port|Port) directive="Port" ;;
*) err "字段只能是 host / user / port"; exit 1 ;;
esac
local tmp; tmp="$(mktemp)"
awk -v a="$alias" -v d="$directive" -v v="$value" '
BEGIN { dl=tolower(d) }
/^[[:space:]]*[Hh]ost[[:space:]]/ { intgt=(NF==2 && $2==a)?1:0; print; if (intgt) print " " d " " v; next }
intgt && tolower($1)==dl { next }
{ print }
' "$CONFIG" >"$tmp"
mv "$tmp" "$CONFIG"; chmod 600 "$CONFIG" 2>/dev/null || true
printf "%s✓%s %s%s%s 的 %s 已设为 %s\n" "$G" "$N" "$C" "$alias" "$N" "$directive" "$value"
}
all_aliases() {
awk '/^[[:space:]]*[Hh]ost[[:space:]]/ { for (i=2;i<=NF;i++) print $i }' "$CONFIG" 2>/dev/null | grep -vE '[*?]'
}
expand_aliases() {
local -a out=(); local pat alias matched
if [ $# -eq 0 ]; then all_aliases; return 0; fi
for pat in "$@"; do
matched=0
while IFS= read -r alias; do
[ -z "$alias" ] && continue
case "$alias" in $pat) out+=("$alias"); matched=1 ;; esac
done < <(all_aliases)
[ "$matched" = 0 ] && err "没有匹配的别名:$pat"
done
[ ${#out[@]} -eq 0 ] && return 0
printf "%s\n" "${out[@]}" | awk '!seen[$0]++'
}
cmd_ping() {
ensure_config
local -a targets=()
while IFS= read -r a; do [ -n "$a" ] && targets+=("$a"); done < <(expand_aliases "$@")
[ ${#targets[@]} -eq 0 ] && { err "没有可检查的主机"; exit 1; }
printf "%s%s %s %s%s\n" "$B" "$(pad 别名 20)" "$(pad 目标 26)" "状态" "$N"
local alias local_host port st
for alias in "${targets[@]}"; do
local_host="$(host_field "$alias" HostName)"; [ -z "$local_host" ] && local_host="$alias"
port="$(host_field "$alias" Port)"; [ -z "$port" ] && port="22"
if nc -z -G 3 "$local_host" "$port" >/dev/null 2>&1; then st="${G}● 在线${N}"; else st="${R}○ 离线${N}"; fi
printf "%s%s%s %s %b\n" "$C" "$(pad "$alias" 20)" "$N" "$(pad "${local_host}:${port}" 26)" "$st"
done
}
cmd_pick() {
ensure_config
command -v fzf >/dev/null 2>&1 || { err "未安装 fzf"; exit 1; }
local line alias
line="$(all_aliases | while IFS= read -r a; do
[ -z "$a" ] && continue
printf "%s\t%s@%s\t%s\n" "$a" "$(host_field "$a" User)" "$(host_field "$a" HostName)" "$(host_desc "$a")"
done | column -t -s $'\t' | fzf --height=40% --reverse --prompt='ssh> ' --header='选机器回车连接')"
[ -z "$line" ] && exit 0
alias="${line%% *}"
cmd_connect "$alias"
}
cmd_fwd() {
ensure_config
local alias="${1:-}"; shift || true
[ -z "$alias" ] && { err "用法:s fwd <别名> <本地端口|本地:远端主机:远端口> …"; exit 1; }
if ! host_exists "$alias"; then err "未找到别名:$alias"; exit 1; fi
[ $# -eq 0 ] && { err "至少给一个转发规则,如 8123 或 15432:10.0.0.5:5432"; exit 1; }
local spec; local -a lopts=()
for spec in "$@"; do
case "$spec" in
*:*:*) lopts+=(-L "$spec") ;;
*:*) lopts+=(-L "$spec") ;;
*) lopts+=(-L "$spec:localhost:$spec") ;;
esac
done
info "隧道建立中(${alias}):$* —— 保持此窗口,Ctrl-C 断开"
if is_password_auth "$alias"; then
local pw; pw="$(kc_get "$alias")"
exec env SSHPASS="$pw" sshpass -e ssh -N \
-o PreferredAuthentications=password,keyboard-interactive -o PubkeyAuthentication=no "${lopts[@]}" "$alias"
else
exec ssh -N "${lopts[@]}" "$alias"
fi
}
cmd_run() {
ensure_config
local sel="${1:-}"; shift || true
[ -z "$sel" ] || [ $# -eq 0 ] && { err "用法:s run <别名/通配符[,…]> <命令…>"; exit 1; }
local cmd="$*"
local -a pats=(); local IFS_OLD="$IFS"; IFS=','; read -ra pats <<< "$sel"; IFS="$IFS_OLD"
local -a targets=()
while IFS= read -r a; do [ -n "$a" ] && targets+=("$a"); done < <(expand_aliases "${pats[@]}")
[ ${#targets[@]} -eq 0 ] && { err "没有可执行的主机"; exit 1; }
local alias ok=0 fail=0 rc
for alias in "${targets[@]}"; do
printf "\n%s%s─── %s ───%s\n" "$B" "$C" "$alias" "$N"
rc=0
if is_password_auth "$alias"; then
local pw; pw="$(kc_get "$alias")"
if env SSHPASS="$pw" sshpass -e ssh \
-o PreferredAuthentications=password,keyboard-interactive -o PubkeyAuthentication=no \
-o ConnectTimeout=10 "$alias" "$cmd"; then rc=0; else rc=$?; fi
else
if ssh -o BatchMode=yes -o ConnectTimeout=10 "$alias" "$cmd"; then rc=0; else rc=$?; fi
fi
if [ "$rc" -eq 0 ]; then ok=$((ok+1)); else fail=$((fail+1)); printf "%s✗ %s 失败(退出码 %s)%s\n" "$R" "$alias" "$rc" "$N"; fi
done
printf "\n%s完成%s:%s成功 %d%s,%s失败 %d%s\n" "$B" "$N" "$G" "$ok" "$N" "$R" "$fail" "$N"
}
cmd_edit() { ensure_config; "${EDITOR:-vi}" "$CONFIG"; }
usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; }
main() {
local cmd="${1:-ls}"
case "$cmd" in
ls|list) cmd_list ;;
add) shift; cmd_add "$@" ;;
rm|del|remove) shift; cmd_rm "$@" ;;
cp|copy-id) shift; cmd_cp "$@" ;;
passwd|pw) shift; cmd_passwd "$@" ;;
desc) shift; cmd_desc "$@" ;;
root) shift; cmd_root "$@" ;;
put|get|scp) shift; cmd_scp "$@" ;;
set) shift; cmd_set "$@" ;;
ping) shift; cmd_ping "$@" ;;
pick|p) cmd_pick ;;
fwd|tunnel|forward) shift; cmd_fwd "$@" ;;
run|exec) shift; cmd_run "$@" ;;
edit) cmd_edit ;;
-h|--help|help) usage ;;
"") cmd_list ;;
*) cmd_connect "$@" ;;
esac
}
main "$@"
还没有评论
欢迎留下你的观点,保持交流的清晰和友好。
写下评论