feat(ff-ssh): add host from ssh args with ssh-copy-id

- ff ssh <ssh-args> parses target like ssh, runs ssh-copy-id, then
  writes ~/.ssh/config.d/<alias>.conf (HostName/User/Port/IdentityFile/
  ProxyJump/ProxyCommand); supports --no-copy, add and run subcommands
- record IdentityFile whenever -i / -o IdentityFile= is given and pass
  the same key to ssh-copy-id; omit it otherwise so ssh falls back to
  default identities
- ff ssh re-sources ff-ssh.fish each call, ff reload clears cached
  dotfile functions so edits take effect
- fix broken copy subcommand and ssh-arg parsing caused by
  --argument-names argv shadowing fish $argv
- fix ssh subcommand completion guards; wrap ssh completions for add/run
This commit is contained in:
2026-09-16 15:51:48 +08:00
parent 0b04a4de50
commit 0b2d2b6c6d
3 changed files with 327 additions and 22 deletions
+15 -6
View File
@@ -14,7 +14,7 @@ complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "s" -d "R
complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "help" -d "Show this help message" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "help" -d "Show this help message"
complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "region" -d "Toggle or set CN_REGION (mirror/proxy region)" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "region" -d "Toggle or set CN_REGION (mirror/proxy region)"
complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "ssh" -d "SSH init (keys + config.d) or copy config to remote" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "ssh" -d "SSH init / copy key + add host / copy config to remote"
complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "dirsize" -d "Show directory sizes" complete -c ff -n "not __fish_seen_subcommand_from $ff_subcommands" -a "dirsize" -d "Show directory sizes"
complete -c ff -n "__fish_seen_subcommand_from dirsize" -s a -l all -d "Recursively show all subdirectories" complete -c ff -n "__fish_seen_subcommand_from dirsize" -s a -l all -d "Recursively show all subdirectories"
@@ -24,9 +24,14 @@ complete -c ff -n "__fish_seen_subcommand_from dirsize" -s r -l reverse -d "Sort
complete -c ff -n "__fish_seen_subcommand_from dirsize" -s h -l help -d "Show help" complete -c ff -n "__fish_seen_subcommand_from dirsize" -s h -l help -d "Show help"
complete -c ff -n "__fish_seen_subcommand_from dirsize" -a "(__fish_complete_directories)" -d "Directory" complete -c ff -n "__fish_seen_subcommand_from dirsize" -a "(__fish_complete_directories)" -d "Directory"
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy help" -a init -d "Ensure SSH key and config.d Include" complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help; and test (count (commandline -opc)) -eq 2" -a init -d "Ensure SSH key and config.d Include"
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy help" -a copy -d "Copy specified SSH config file(s) to remote" complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help; and test (count (commandline -opc)) -eq 2" -a add -d "Copy key + add host from ssh-style args"
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy help" -a help -d "Show ssh subcommand help" complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help; and test (count (commandline -opc)) -eq 2" -a run -d "Copy key + add host, then connect"
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help; and test (count (commandline -opc)) -eq 2" -a copy -d "Copy specified SSH config file(s) to remote"
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help; and test (count (commandline -opc)) -eq 2" -a help -d "Show ssh subcommand help"
# 裸参数与 ssh 原生参数一致:复用 ssh 自带的补全(选项 + 主机名)
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy add run help" -w ssh
function __ff_ssh_config_d_files function __ff_ssh_config_d_files
for d in $HOME/.ssh/config.d $HOME/.dotfile/ssh/config.d for d in $HOME/.ssh/config.d $HOME/.dotfile/ssh/config.d
@@ -40,8 +45,12 @@ function __ff_ssh_config_d_files
end end
end end
complete -c ff -n "__fish_seen_subcommand_from ssh copy; and test (count (commandline -opc)) -eq 2" -a "(__fish_ssh_hosts 2>/dev/null; or __fish_print_hostnames)" -d "SSH host" complete -c ff -n "__fish_seen_subcommand_from ssh; and __fish_seen_subcommand_from copy; and test (count (commandline -opc)) -eq 3" -a "(__fish_print_hostnames)" -d "SSH host"
complete -c ff -n "__fish_seen_subcommand_from ssh copy; and test (count (commandline -opc)) -ge 3" -a "(__ff_ssh_config_d_files)" -d "Config file" complete -c ff -n "__fish_seen_subcommand_from ssh; and __fish_seen_subcommand_from copy; and test (count (commandline -opc)) -ge 4" -a "(__ff_ssh_config_d_files)" -d "Config file"
# add/run 使用与 ssh 相同的参数:直接复用 ssh 的选项与主机补全
complete -c ff -n "__fish_seen_subcommand_from ssh; and __fish_seen_subcommand_from add run" -w ssh
complete -c ff -n "__fish_seen_subcommand_from ssh; and not __fish_seen_subcommand_from init copy help" -l no-copy -d "Skip ssh-copy-id"
# # make 子命令的补全 - 直接使用脚本文件名 # # make 子命令的补全 - 直接使用脚本文件名
# complete -c ff -n "__fish_seen_subcommand_from make" -a "help" -d "Show all make targets" # complete -c ff -n "__fish_seen_subcommand_from make" -a "help" -d "Show all make targets"
+304 -12
View File
@@ -1,4 +1,4 @@
# ff ssh: local init (keys + config.d) and copy config to remote # ff ssh: local init (keys + config.d), copy key + add host, copy config to remote
function __ff_ssh_has_key function __ff_ssh_has_key
for f in $HOME/.ssh/id_* for f in $HOME/.ssh/id_*
@@ -95,7 +95,7 @@ function __ff_ssh_resolve_copy_src --argument file
return 1 return 1
end end
function __ff_ssh_copy_remote --argument-names argv function __ff_ssh_copy_remote
set -l host $argv[1] set -l host $argv[1]
set -l files $argv[2..-1] set -l files $argv[2..-1]
set -l ssh_dir "$HOME/.ssh" set -l ssh_dir "$HOME/.ssh"
@@ -158,24 +158,316 @@ function __ff_ssh_copy_remote --argument-names argv
echo "Done." echo "Done."
end end
function ff_ssh --argument-names argv # ---- ssh-style argument parsing -------------------------------------------
# ssh short options that consume a following value
function __ff_ssh_opt_takes_value --argument-names flag
contains -- $flag -b -c -D -E -e -F -I -i -J -L -l -m -O -o -p -Q -R -S -W -w
end
# First non-option argument of `ssh`-style args (the [user@]host)
function __ff_ssh_find_target
set -l n (count $argv)
set -l i 1
while test $i -le $n
set -l a $argv[$i]
if test "$a" = '--'
set i (math $i + 1)
if test $i -le $n
echo $argv[$i]
end
return 0
end
if string match -q -- '-*' $a
set -l flag (string sub -s 1 -l 2 -- $a)
if test (string length -- $a) -eq 2; and __ff_ssh_opt_takes_value $flag
set i (math $i + 2)
continue
end
set i (math $i + 1)
continue
end
echo $a
return 0
end
return 1
end
# Value of a short option, supporting both `-i file` and `-ifile`
function __ff_ssh_argv_value --argument-names flag
set -l args $argv[2..-1]
set -l n (count $args)
set -l flen (string length -- $flag)
set -l i 1
while test $i -le $n
set -l a $args[$i]
if test "$a" = "$flag"
set -l j (math $i + 1)
if test $j -le $n
echo $args[$j]
end
return 0
end
if test (string length -- $a) -gt $flen; and string match -q -- "$flag*" $a
string sub -s (math $flen + 1) -- $a
return 0
end
set i (math $i + 1)
end
return 1
end
# One value from `ssh -G` output (stdin)
function __ff_ssh_gval --argument-names key
while read -l line
set -l trimmed (string trim -- $line)
set -l k (string split -m1 ' ' -- $trimmed)[1]
if test (string lower -- $k) = (string lower -- $key)
string replace -r '^\S+\s+' '' -- $trimmed
return 0
end
end
end
# File that already declares `Host <alias>` (main config or config.d)
function __ff_ssh_find_alias_file --argument-names alias_name
set -l files $HOME/.ssh/config
if test -d $HOME/.ssh/config.d
set files $files $HOME/.ssh/config.d/*
end
for f in $files
test -f $f; or continue
if awk -v want=(string lower -- $alias_name) '
tolower($1) == "host" {
for (i = 2; i <= NF; i++)
if (tolower($i) == want) hit = 1
}
END { exit(hit ? 0 : 1) }
' $f
echo $f
return 0
end
end
return 1
end
# 复制公钥到远端(ssh-copy-id),失败不阻断配置写入
function __ff_ssh_copy_id
set -l target $argv[1]
set -l port $argv[2]
set -l identity $argv[3]
set -l proxyjump $argv[4]
set -l proxycommand $argv[5]
if not type -q ssh-copy-id
echo "ff ssh: 未找到 ssh-copy-id,跳过公钥复制(可手动执行 ssh-copy-id $target)" >&2
return 1
end
set -l cmd ssh-copy-id
if test -n "$port"; and test "$port" != 22
set cmd $cmd -p $port
end
if test -n "$identity"
set cmd $cmd -i $identity
end
if test -n "$proxyjump"
set cmd $cmd -o "ProxyJump=$proxyjump"
end
if test -n "$proxycommand"
set cmd $cmd -o "ProxyCommand=$proxycommand"
end
set cmd $cmd $target
echo "复制公钥: $cmd"
$cmd
end
# `ff ssh a@b.com` / `ff ssh -p 2222 -i key root@host`
# 确保本地 key -> ssh-copy-id -> 写 ~/.ssh/config.d/<alias>.conf
function __ff_ssh_add_host
set -l args
set -l do_copy 1
for a in $argv
if test "$a" = --no-copy
set do_copy 0
else
set args $args $a
end
end
set -l target (__ff_ssh_find_target $args)
if test -z "$target"
echo "ff ssh: 未找到 [user@]hostname 参数" >&2
return 1
end
# ssh://user@host:port/path -> user / host / port
set -l plain (string replace -r '^ssh://' '' -- $target)
set plain (string replace -r '/.*$' '' -- $plain)
set -l user ""
if string match -q '*@*' -- $plain
set user (string replace -r '@[^@]*$' '' -- $plain)
end
set -l hostport (string replace -r '^.*@' '' -- $plain)
set -l port ""
if string match -q '*:*' -- $hostport
set port (string replace -r '^.*:' '' -- $hostport)
set hostport (string replace -r ':[^:]*$' '' -- $hostport)
end
set -l host $hostport
if test -z "$host"
echo "ff ssh: 无法解析主机名: $target" >&2
return 1
end
test -n "$port"; or set port (__ff_ssh_argv_value -p $args)
set -l alias_name $host
if test -t 0
read -P "SSH 别名 [$host]: " answer
if test $status -eq 0; and test -n "$answer"
set alias_name $answer
end
end
if not string match -qr '^[A-Za-z0-9][A-Za-z0-9._-]*$' -- $alias_name
echo "ff ssh: 非法别名 '$alias_name'(只允许字母、数字、点、下划线、连字符)" >&2
return 1
end
set -l existing (__ff_ssh_find_alias_file $alias_name)
if test -n "$existing"
echo "SSH 别名 '$alias_name' 已存在于 $existing,跳过写入"
return 0
end
# 用 ssh -G 解析最终生效的 HostName/User/Port/Proxy*
set -l resolved (ssh -G $args 2>/dev/null)
set -l rhost (printf '%s\n' $resolved | __ff_ssh_gval hostname)
set -l ruser (printf '%s\n' $resolved | __ff_ssh_gval user)
set -l rport (printf '%s\n' $resolved | __ff_ssh_gval port)
set -l proxyjump (printf '%s\n' $resolved | __ff_ssh_gval proxyjump)
set -l proxycommand (printf '%s\n' $resolved | __ff_ssh_gval proxycommand)
test -n "$rhost"; or set rhost $host
test -n "$user"; or set user $ruser
test -n "$port"; or set port $rport
test -n "$port"; or set port 22
test "$proxyjump" = none; and set proxyjump ""
test "$proxycommand" = none; and set proxycommand ""
# IdentityFile: ssh -G 会列出所有默认密钥,仅在显式指定时记录
set -l identity ""
set -l explicit_identity (__ff_ssh_argv_value -i $args)
if test -n "$explicit_identity"
set identity $explicit_identity
else if string match -q '*IdentityFile*' -- $args
set identity (printf '%s\n' $resolved | __ff_ssh_gval identityfile)
end
if string match -q -- "$HOME/*" "$identity"
set identity "~/"(string replace -r "^$HOME/" '' -- $identity)
end
# ssh-copy-id 也要用同一把密钥(-i 或 -o IdentityFile=),否则会复制默认 key
set -l copy_identity $explicit_identity
if test -z "$copy_identity"; and test -n "$identity"
set copy_identity (string replace -r '^~/' "$HOME/" -- $identity)
end
# 先复制公钥,这样写完配置就能直接 ssh
if test "$do_copy" = 1
__ff_ssh_ensure_keys
__ff_ssh_copy_id "$target" "$port" "$copy_identity" "$proxyjump" "$proxycommand"
or echo "ff ssh: 公钥复制失败,已继续写入配置(可重试: ff ssh add $target)" >&2
end
set -l dir "$HOME/.ssh/config.d"
mkdir -p $dir
chmod 700 "$HOME/.ssh" $dir
set -l conf_file "$dir/$alias_name.conf"
if test -f $conf_file
cp -p $conf_file "$conf_file.backup."(date +%Y%m%d%H%M%S)
end
set -l tmpdir /tmp
test -n "$TMPDIR"; and set tmpdir $TMPDIR
set -l tmp (mktemp "$tmpdir/ff-ssh.XXXXXX")
begin
printf 'Host %s\n' $alias_name
printf ' HostName %s\n' $rhost
if test -n "$user"
printf ' User %s\n' $user
end
printf ' Port %s\n' $port
if test -n "$identity"
printf ' IdentityFile %s\n' $identity
end
if test -n "$proxyjump"
printf ' ProxyJump %s\n' $proxyjump
end
if test -n "$proxycommand"
printf ' ProxyCommand %s\n' $proxycommand
end
end > $tmp
chmod 600 $tmp
mv $tmp $conf_file
echo "已写入 SSH 配置: $conf_file"
echo "以后可直接运行: ssh $alias_name"
end
function __ff_ssh_usage
echo "用法: ff ssh [init | <ssh 参数> | add <ssh 参数> | run <ssh 参数> | copy <host> <file>... | help]"
echo ""
echo " (无参数) / init 确保本地 SSH key 存在,并启用 ~/.ssh/config.d Include"
echo " add <ssh 参数> 确保本地 key,ssh-copy-id 后写入 ~/.ssh/config.d/<别名>.conf"
echo " run <ssh 参数> 同 add,写完后立即连接(此时应已免密)"
echo " <ssh 参数> 裸参数等同 add(例: ff ssh -p 2222 root@1.2.3.4)"
echo " copy <host> <file> 检查远端 Include,然后 scp 指定配置文件到远端"
echo " help 显示此帮助"
echo ""
echo "选项:"
echo " --no-copy 只写配置,不执行 ssh-copy-id"
echo ""
echo "保留子命令: init / copy / add / run / help(同名主机请用 ssh 直接连接)"
echo ""
echo "示例:"
echo " ff ssh # 初始化本地 key 与 config.d"
echo " ff ssh a@b.com # 复制公钥 + 添加 Host 配置(交互式确认别名)"
echo " ff ssh -p 2222 -i ~/.ssh/id_x root@1.2.3.4"
echo " ff ssh --no-copy a@b.com # 只写配置,不复制公钥"
echo " ff ssh run a@b.com # 复制公钥 + 写配置并立即连接"
echo " ff ssh copy mynas 30-hanxi.conf # 推送配置片段到远端"
end
function ff_ssh
set -l sub (test (count $argv) -ge 1; and echo $argv[1]; or echo init) set -l sub (test (count $argv) -ge 1; and echo $argv[1]; or echo init)
switch $sub switch $sub
case init '' case init ''
__ff_ssh_ensure_keys __ff_ssh_ensure_keys
__ff_ssh_ensure_config_d __ff_ssh_ensure_config_d
case add
__ff_ssh_ensure_config_d
__ff_ssh_add_host $argv[2..-1]
case run
set -l run_args
for a in $argv[2..-1]
test "$a" = --no-copy; and continue
set run_args $run_args $a
end
__ff_ssh_ensure_config_d
__ff_ssh_add_host $argv[2..-1]; or return 1
command ssh $run_args
case copy case copy
__ff_ssh_copy_remote $argv[2..-1] __ff_ssh_copy_remote $argv[2..-1]
case help -h case help -h --help
echo "Usage: ff ssh [init|copy <host> <file>...|help]" __ff_ssh_usage
echo ""
echo " init Ensure SSH key and config.d Include (default)"
echo " copy <host> <file> Check remote Include, then scp specified config file(s)"
echo " help Show this help"
case '*' case '*'
echo "Unknown subcommand: $sub" # 裸参数与 ssh 原生参数一致:有主机就记录配置,否则透传给 ssh
echo "Run 'ff ssh help' for usage" if __ff_ssh_find_target $argv >/dev/null
return 1 __ff_ssh_ensure_config_d
__ff_ssh_add_host $argv
else
command ssh $argv
end
end end
end end
+7 -3
View File
@@ -14,7 +14,7 @@ function ff
"s|Alias for script" \ "s|Alias for script" \
"update|Update dotfile" \ "update|Update dotfile" \
"region|Toggle or set CN_REGION (mirror/proxy region)" \ "region|Toggle or set CN_REGION (mirror/proxy region)" \
"ssh|SSH init (keys + config.d) or copy config to remote" \ "ssh|SSH init, copy key + add host config, or copy config to remote" \
"dirsize|Show directory sizes (du wrapper)" \ "dirsize|Show directory sizes (du wrapper)" \
"help|Show this help message" "help|Show this help message"
@@ -28,6 +28,11 @@ function ff
switch $argv[1] switch $argv[1]
case reload case reload
# autoload 会缓存函数定义,先清掉 dotfile 里的函数再重载
for __f in ~/.dotfile/fish/functions/*.fish
functions -e (basename $__f .fish) 2>/dev/null
end
set -e __f
source ~/.config/fish/config.fish source ~/.config/fish/config.fish
echo "Fish configuration reloaded!" echo "Fish configuration reloaded!"
case edit case edit
@@ -95,9 +100,8 @@ function ff
return 1 return 1
end end
case ssh case ssh
if not functions -q ff_ssh # 每次都重新 source,避免 autoload 缓存旧定义
source ~/.dotfile/fish/functions/ff-ssh.fish source ~/.dotfile/fish/functions/ff-ssh.fish
end
ff_ssh $argv[2..-1] ff_ssh $argv[2..-1]
case dirsize case dirsize
set -l script_path ~/.dotfile/script/dirsize.sh set -l script_path ~/.dotfile/script/dirsize.sh