improve linux-ssh-operator-skill: bug fixes, rsync support, tunneling docs

- Fix dry-run bug in ssh_alias_setup.sh (pub key check skipped when key was never generated)
- Remove dead code in ssh_run.sh (-- check after host arg was never reachable)
- Add --rsync/--delete flags to ssh_copy.sh for incremental rsync transfers
- Add -C/--compress flag to ssh_run.sh for slow-link connections
- Expand SKILL.md/SKILL.zh-CN.md: tunneling, jump host, SSH multiplexing, Docker ops
- Expand ssh-playbook.md: port forwarding, bastion, ControlMaster, structured troubleshooting table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 17:26:47 +08:00
parent dea5db60d9
commit 766eb935a9
6 changed files with 363 additions and 91 deletions

View File

@@ -1,13 +1,14 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
---
# Linux SSH Operator
## Overview
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login).
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login, tunnels).
## Workflow
@@ -99,29 +100,85 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
```
### Transfer files
Upload:
Upload (scp):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
Download:
Download (scp):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
Incremental sync (rsync — preferred for directories or large files):
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
```
### SSH Tunneling / Port Forwarding
Local forward (access remote service on a local port):
```bash
# Access remote MySQL (3306) at localhost:13306
ssh -L 13306:127.0.0.1:3306 my-server -N
```
Remote forward (expose a local port on the server):
```bash
# Make local :8080 accessible on server's :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
Jump host (bastion → target):
```bash
ssh -J bastion-user@bastion:22 target-user@target-host
```
Or configure in `~/.ssh/config`:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
### SSH Multiplexing (faster repeated connections)
Add to `~/.ssh/config` to reuse the same TCP connection:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
The first `ssh my-server` opens a master connection; subsequent connections reuse it without re-handshaking.
## Common Ops (snippets)
- Disk: `df -h`, `du -sh /path/* | sort -h`
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
- Services: `systemctl status|restart|stop SERVICE`
- Services: `systemctl status|restart|stop|enable SERVICE`
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
- Docker: `docker ps`, `docker logs -f CONTAINER`, `docker exec -it CONTAINER bash`
- Config test: `nginx -t`, `sshd -T`
- Open files: `lsof -i :PORT`, `fuser PORT/tcp`
## Safety
@@ -136,5 +193,5 @@ Download:
## Scripts
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
- `scripts/ssh_run.sh`: run remote commands with consistent options.
- `scripts/ssh_copy.sh`: push/pull files via scp with consistent options.
- `scripts/ssh_run.sh`: run remote commands with consistent options (supports `--compress`, `--tty`, `--sudo`).
- `scripts/ssh_copy.sh`: push/pull files via scp or rsync (use `--rsync` for incremental transfers).

View File

@@ -1,6 +1,7 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、SSH 隧道/端口转发、跳板机、内网穿透、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件、SSH 隧道、端口转发、跳板机、堡垒机、服务器上看一下、帮我连上服务器、查看服务器日志 等场景时使用。
---
# Linux SSH 运维助手(中文说明)
@@ -14,9 +15,10 @@ description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、
- 执行远程命令
- 查看系统与服务日志
- 管理 `systemd` 服务
- 上传和下载文件
- 上传和下载文件scp 或 rsync
- 配置 SSH 别名
- 安装公钥并启用免密登录
- SSH 隧道 / 端口转发 / 跳板机
- 进行常见运维排障
适合以下场景:
@@ -25,9 +27,11 @@ description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、
- 在服务器上运行命令
- 查看 `journalctl` / 应用日志
- 启动、停止、重启服务
- 把文件部署到服务器
- 把文件部署到服务器scp 或 rsync 增量同步)
- 从服务器拉取日志、配置或产物文件
-别名 + IP + 密码快速配置一键 SSH 免密登录
-"别名 + IP + 密码"快速配置一键 SSH 免密登录
- 配置端口转发访问内网服务
- 通过跳板机/堡垒机连接内网服务器
## 工作流程
@@ -71,7 +75,7 @@ ssh my-server
### 一键配置别名 + 免密登录
当用户提供别名、主机/IP、密码,并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
当用户提供"别名、主机/IP、密码",并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
1. 使用辅助脚本写入或更新 SSH 别名:
@@ -118,22 +122,78 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --compress my-server -- cat /var/log/big.log
```
### 传输文件
上传文件:
上传文件scp
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
下载文件:
下载文件scp
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
增量同步rsync推荐用于目录或大文件
```bash
# 上传目录
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
# 上传并删除远端多余文件
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
# 下载目录
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh --rsync pull my-server /var/log/ ./logs/
```
### SSH 隧道 / 端口转发
本地转发(将远程服务映射到本地端口):
```bash
# 把远程 MySQL(3306) 映射到本地 13306
ssh -L 13306:127.0.0.1:3306 my-server -N
```
远程转发(将本地服务暴露到服务器端口):
```bash
# 把本地 :8080 映射到服务器 :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
跳板机 / 堡垒机:
```bash
ssh -J bastion-user@bastion:22 target-user@target-host
```
或在 `~/.ssh/config` 中配置:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
### SSH 多路复用(加速重复连接)
`~/.ssh/config` 中添加,复用同一 TCP 连接:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
第一次 `ssh my-server` 建立主连接,后续连接直接复用,无需重新握手,速度更快。
## 常见运维操作
- 磁盘空间:`df -h`
@@ -142,8 +202,11 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
- 进程查看:`ps aux --sort=-%mem | head`
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
- 日志跟踪:`tail -n 200 -f /path/log`
- 服务管理:`systemctl status|restart|stop SERVICE`
- 服务管理:`systemctl status|restart|stop|enable SERVICE`
- 网络排查:`ss -lntp``ip a``ip r``curl -v http://127.0.0.1:PORT/`
- Docker`docker ps``docker logs -f CONTAINER``docker exec -it CONTAINER bash`
- 配置测试:`nginx -t``sshd -T`
- 端口占用:`lsof -i :PORT``fuser PORT/tcp`
## 安全注意事项
@@ -170,7 +233,8 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
- 指定用户、端口、私钥
- 设置连接超时
- 启用 TTY
- 启用 TTY`--tty`
- 启用 SSH 压缩(`--compress`,适合慢速链路)
- 使用 `sudo``sudo -n`
- 使用 `--accept-new` 处理首次连接主机
- 使用 `--dry-run` 预览实际将执行的命令
@@ -184,11 +248,13 @@ ssh -tt my-server sudo systemctl status nginx --no-pager
### `scripts/ssh_copy.sh`
用于通过 `scp` 上传或下载文件,支持:
用于通过 `scp``rsync` 上传或下载文件,支持:
- `push` 上传文件到远程服务器
- `pull` 从远程服务器下载文件
- 递归复制目录
- `--rsync`:使用 rsync 增量同步(显示进度,适合目录和大文件)
- `--delete`:配合 `--rsync push` 删除远端多余文件
- 递归复制目录scp 模式用 `-r`
- 指定用户、端口、私钥
- 使用 `--accept-new`
- 使用 `--dry-run` 预览命令

View File

@@ -60,6 +60,7 @@ ssh my-server "du -sh /var/log/* | sort -h | tail"
```bash
ssh my-server "ps aux --sort=-%mem | head"
ssh my-server "ss -lntp"
ssh my-server "lsof -i :8080"
```
### Logs (systemd)
@@ -67,6 +68,7 @@ ssh my-server "ss -lntp"
```bash
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
ssh my-server "journalctl -u SERVICE -f --no-pager"
ssh my-server "journalctl -u SERVICE --since '1 hour ago' --no-pager"
```
### Services (systemd)
@@ -89,15 +91,137 @@ Non-interactive sudo (fails if a password prompt would be required):
ssh my-server "sudo -n systemctl restart SERVICE"
```
### Docker
```bash
ssh my-server "docker ps"
ssh my-server "docker logs --tail=200 -f CONTAINER"
ssh my-server "docker exec -it CONTAINER bash"
ssh my-server "docker stats --no-stream"
```
### File editing shortcuts
```bash
# View a config file
ssh my-server "cat /etc/nginx/nginx.conf"
# Quick in-place line replacement
ssh my-server "sed -i 's/old/new/g' /path/to/file"
# Append a line
ssh my-server "echo 'new line' >> /path/to/file"
```
## SSH Tunneling / Port Forwarding
### Local forward
Access a remote service on a local port (server-side port does not need to be publicly open):
```bash
# remote MySQL → localhost:13306
ssh -L 13306:127.0.0.1:3306 my-server -N
# remote Redis → localhost:16379
ssh -L 16379:127.0.0.1:6379 my-server -N
```
Add `-f` to background the tunnel:
```bash
ssh -fNL 13306:127.0.0.1:3306 my-server
```
### Remote forward
Expose a local service on the server (useful for demos or webhooks):
```bash
# local :8080 → server :18080
ssh -R 18080:127.0.0.1:8080 my-server -N
```
### Jump host / bastion
```bash
# Single jump
ssh -J bastion user@target
# Multi-hop
ssh -J jump1,jump2 user@target
```
`~/.ssh/config` version:
```sshconfig
Host target-internal
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
```
## SSH Multiplexing
Reuse the same TCP connection for multiple SSH sessions — avoids repeated handshakes:
```sshconfig
Host *
ControlMaster auto
ControlPath ~/.ssh/ctrl-%h-%p-%r
ControlPersist 60s
```
Check active master sockets:
```bash
ls ~/.ssh/ctrl-*
```
Close a master socket manually:
```bash
ssh -O exit my-server
```
## Safer host key handling
- Prefer verifying the host key fingerprint out-of-band on first connect.
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
Remove a stale known_hosts entry:
```bash
ssh-keygen -R SERVER_IP
```
## Troubleshooting quick hits
- `Permission denied (publickey)`: wrong user, wrong key, server missing your public key, or `sshd` settings.
- `Connection timed out`: routing/firewall/security group, wrong port, server down.
- `No route to host`: network path missing (VPN, subnet, ACL).
| Symptom | Likely cause | Fix |
|---|---|---|
| `Permission denied (publickey)` | Wrong user/key, pub key not on server, sshd config | Check `~/.ssh/authorized_keys` on server; verify key matches |
| `Connection timed out` | Firewall/security group, wrong port, server down | Check port, security group, ping |
| `No route to host` | Network path missing (VPN, ACL, subnet) | Check VPN, routing table |
| `Host key changed` warning | Server rebuilt, or MITM | Verify with out-of-band channel; `ssh-keygen -R HOST` if expected |
| `Too many authentication failures` | SSH agent offering too many keys | Use `-o IdentitiesOnly=yes -i ~/.ssh/id_ed25519` |
| `sudo: a terminal is required` | No TTY allocated | Use `ssh -tt` or pass `-t` to wrapper |
### Debug connection verbosely
```bash
ssh -vvv my-server
```
### Check sshd config on server
```bash
ssh my-server "sudo sshd -T | grep -E 'pubkeyauthentication|passwordauthentication|permitrootlogin'"
```
### Check authorized_keys permissions (common cause of publickey failure)
```bash
ssh my-server "ls -la ~/.ssh/ && cat ~/.ssh/authorized_keys"
# .ssh should be 700, authorized_keys should be 600
```

View File

@@ -118,7 +118,7 @@ if [[ ! -f "$key" ]]; then
fi
fi
if [[ ! -f "$key_pub" ]]; then
if ! $dry_run && [[ ! -f "$key_pub" ]]; then
echo "Public key not found: $key_pub" >&2
exit 1
fi

View File

@@ -3,7 +3,7 @@ set -euo pipefail
usage() {
cat <<'USAGE'
Copy files via scp with consistent options.
Copy files via scp or rsync with consistent options.
Usage:
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
@@ -13,9 +13,11 @@ Options:
-u, --user USER Override SSH user (or set REMOTE_USER)
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-r, --recursive Copy directories recursively
-r, --recursive Copy directories recursively (scp mode only; rsync always recurses)
--rsync Use rsync instead of scp (incremental, shows progress)
--delete With --rsync push: delete remote files not present in source
--accept-new Set StrictHostKeyChecking=accept-new
--dry-run Print the scp command that would run
--dry-run Print the command that would run
-h, --help Show help
Environment defaults:
@@ -23,6 +25,8 @@ Environment defaults:
Examples:
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/
ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
USAGE
}
@@ -32,47 +36,28 @@ user="${REMOTE_USER:-}"
key="${REMOTE_KEY:-}"
recursive=false
use_rsync=false
delete_mode=false
accept_new=false
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
-r|--recursive)
recursive=true
shift
;;
--accept-new)
accept_new=true
shift
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
-u|--user) user="${2:-}"; shift 2 ;;
-p|--port) port="${2:-}"; shift 2 ;;
-i|--key) key="${2:-}"; shift 2 ;;
-r|--recursive) recursive=true; shift ;;
--rsync) use_rsync=true; shift ;;
--delete) delete_mode=true; shift ;;
--accept-new) accept_new=true; shift ;;
--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
*) break ;;
esac
done
@@ -81,11 +66,8 @@ if [[ $# -lt 4 ]]; then
exit 2
fi
direction="$1"
shift
host="$1"
shift
direction="$1"; shift
host="$1"; shift
dest_host="$host"
if [[ -n "$user" ]]; then
@@ -93,34 +75,70 @@ if [[ -n "$user" ]]; then
dest_host="${user}@${host_no_user}"
fi
scp_opts=(-P "$port" -p)
if [[ -n "$key" ]]; then
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $recursive; then
scp_opts+=(-r)
fi
if $accept_new; then
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
if $use_rsync; then
if ! command -v rsync >/dev/null 2>&1; then
echo "rsync is required but not installed." >&2
exit 1
fi
ssh_parts=("ssh" "-p" "$port")
if [[ -n "$key" ]]; then
ssh_parts+=("-i" "$key" "-o" "IdentitiesOnly=yes")
fi
if $accept_new; then
ssh_parts+=("-o" "StrictHostKeyChecking=accept-new")
fi
rsync_opts=(-avz --progress -e "${ssh_parts[*]}")
case "$direction" in
push)
local_path="$1"
remote_path="$2"
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
case "$direction" in
push)
local_path="$1"
remote_path="$2"
if $delete_mode; then
rsync_opts+=(--delete)
fi
full_cmd=(rsync "${rsync_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(rsync "${rsync_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
else
scp_opts=(-P "$port" -p)
if [[ -n "$key" ]]; then
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $recursive; then
scp_opts+=(-r)
fi
if $accept_new; then
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
case "$direction" in
push)
local_path="$1"
remote_path="$2"
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
fi
if $dry_run; then
printf '%q ' "${full_cmd[@]}"

View File

@@ -14,6 +14,7 @@ Options:
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
-C, --compress Enable SSH compression (useful on slow links)
--accept-new Set StrictHostKeyChecking=accept-new
--sudo Prefix command with sudo --
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
@@ -36,6 +37,7 @@ key="${REMOTE_KEY:-}"
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
tty=false
compress=false
accept_new=false
sudo_mode=""
dry_run=false
@@ -58,6 +60,10 @@ while [[ $# -gt 0 ]]; do
tty=true
shift
;;
-C|--compress)
compress=true
shift
;;
--accept-new)
accept_new=true
shift
@@ -126,14 +132,15 @@ if $accept_new; then
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
if $compress; then
ssh_opts+=(-C)
fi
if $tty; then
ssh_opts+=(-tt)
fi
cmd=("$@")
if [[ ${#cmd[@]} -gt 0 && "${cmd[0]}" == "--" ]]; then
cmd=("${cmd[@]:1}")
fi
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
if [[ "$sudo_mode" == "sudo-n" ]]; then
cmd=("sudo" "-n" "--" "${cmd[@]}")