Skip to content

Instantly share code, notes, and snippets.

@superyngo
Last active February 23, 2026 02:52
Show Gist options
  • Select an option

  • Save superyngo/bed6b95a32cfef619949c409797c0237 to your computer and use it in GitHub Desktop.

Select an option

Save superyngo/bed6b95a32cfef619949c409797c0237 to your computer and use it in GitHub Desktop.
sync ssh config
$SSHOpts = @('-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes')
$DefaultParallel = 5
function Show-Usage {
Write-Host @"
用法: sync_ssh_config.ps1 [選項]
將本機 ~/.ssh/config 同步到所有 SSH config 中定義的遠端主機。
選項:
-DryRun 僅列出同步目標,不執行
-Exclude h1,h2 排除指定 host(逗號分隔)
-Parallel N 並行數(預設 5,設 1 為循序執行)
-Help 顯示此說明
"@
}
function Sync-SSHConfig {
[CmdletBinding()]
param(
[switch]$DryRun,
[string]$Exclude = '',
[int]$Parallel = $DefaultParallel,
[switch]$Help
)
if ($Help) {
Show-Usage
return
}
$configFile = Join-Path $env:USERPROFILE ".ssh\config"
# 檢查 config 檔案是否存在
if (-not (Test-Path $configFile)) {
Write-Error "錯誤: $configFile 不存在"
exit 1
}
# 安全的暫存目錄(用於收集並行結果)
$resultDir = Join-Path ([System.IO.Path]::GetTempPath()) "sync_ssh_$(Get-Random)"
New-Item -ItemType Directory -Path $resultDir -Force | Out-Null
try {
# 取得本機 hostname 用於排除自身
$localHostname = $env:COMPUTERNAME
$localFqdn = try { [System.Net.Dns]::GetHostEntry('').HostName } catch { '' }
# 建立排除清單
$excluded = @{}
if ($Exclude -ne '') {
$Exclude -split ',' | ForEach-Object { $excluded[$_.Trim()] = $true }
}
# 解析 SSH config
$targets = @()
$currentHost = $null
$currentHostName = $null
$currentUser = $env:USERNAME
Get-Content $configFile | ForEach-Object {
$line = $_.Trim()
if ($line -match '^Host\s+(.+)$') {
# 儲存前一個 Host(如果有)
if ($currentHost) {
$targets += [PSCustomObject]@{
Alias = $currentHost
HostName = $currentHostName
User = $currentUser
Target = "$currentUser@$currentHostName"
}
}
$hostPattern = $Matches[1]
if ($hostPattern -ne '*') {
$currentHost = $hostPattern
$currentHostName = $hostPattern
$currentUser = $env:USERNAME
} else {
$currentHost = $null
}
}
elseif ($currentHost -and $line -match '^\s*HostName\s+(.+)$') {
$currentHostName = $Matches[1]
}
elseif ($currentHost -and $line -match '^\s*User\s+(.+)$') {
$currentUser = $Matches[1]
}
}
# 處理最後一個 Host
if ($currentHost) {
$targets += [PSCustomObject]@{
Alias = $currentHost
HostName = $currentHostName
User = $currentUser
Target = "$currentUser@$currentHostName"
}
}
# 過濾排除項目與本機
$filteredTargets = $targets | Where-Object {
$h = $_.HostName
$a = $_.Alias
# 排除指定 host
if ($excluded.ContainsKey($a)) { return $false }
# 排除本機自身
if ($h -in @('localhost', '127.0.0.1', '::1', $localHostname, $localFqdn)) { return $false }
return $true
}
$total = @($filteredTargets).Count
if ($total -eq 0) {
Write-Host "沒有需要同步的目標主機"
return
}
# dry-run 模式
if ($DryRun) {
Write-Host "=== Dry Run: 將同步到以下 $total 台主機 ===" -ForegroundColor Yellow
foreach ($t in $filteredTargets) {
Write-Host " $($t.Alias) ($($t.Target))"
}
return
}
Write-Host "開始同步 $total 台主機(並行數: $Parallel)"
Write-Host ""
# 同步單一 host 的 ScriptBlock
$syncJob = {
param($Target, $Label, $ConfigFile, $SSHOpts, $ResultFile)
# 確保遠端 .ssh 目錄存在(相容 Windows PowerShell 與 Unix shell)
& ssh @SSHOpts $Target "test -d ~/.ssh || mkdir ~/.ssh; chmod 700 ~/.ssh 2>/dev/null; exit 0" 2>$null
# 複製 config 檔案
& scp @SSHOpts -q $ConfigFile "${Target}:~/.ssh/config" 2>$null
if ($LASTEXITCODE -eq 0) {
# 設定正確權限(Windows 上 chmod 無效但不影響)
& ssh @SSHOpts $Target "chmod 600 ~/.ssh/config 2>/dev/null; exit 0" 2>$null
"ok" | Out-File -FilePath $ResultFile -NoNewline
Write-Output " ✓ $Label ($Target)"
} else {
"fail" | Out-File -FilePath $ResultFile -NoNewline
Write-Output " ✗ $Label ($Target)"
}
}
# 並行執行
$jobs = @()
$idx = 0
foreach ($t in $filteredTargets) {
$resultFile = Join-Path $resultDir "$idx"
$jobs += Start-Job -ScriptBlock $syncJob -ArgumentList $t.Target, $t.Alias, $configFile, $SSHOpts, $resultFile
$idx++
# 控制並行數
while (@($jobs | Where-Object { $_.State -eq 'Running' }).Count -ge $Parallel) {
Start-Sleep -Milliseconds 200
}
}
# 等待所有 job 完成並輸出結果
$jobs | Wait-Job | ForEach-Object {
Receive-Job -Job $_ | Write-Host
}
$jobs | Remove-Job -Force
# 統計結果
$count = 0
$failed = 0
Get-ChildItem $resultDir -File | ForEach-Object {
if ((Get-Content $_.FullName -Raw) -eq 'ok') {
$count++
} else {
$failed++
}
}
Write-Host ""
Write-Host "==========================================" -ForegroundColor Yellow
Write-Host "同步完成: 成功 $count 台,失敗 $failed 台(共 $total 台)" -ForegroundColor Yellow
Write-Host "==========================================" -ForegroundColor Yellow
# 返回值反映失敗狀態
if ($failed -gt 0) { exit 1 }
}
finally {
# 清理暫存目錄
if (Test-Path $resultDir) {
Remove-Item -Recurse -Force $resultDir
}
}
}
# 執行
Sync-SSHConfig @args
#!/bin/bash
SSH_OPTS="-o ConnectTimeout=5 -o BatchMode=yes"
DEFAULT_PARALLEL=5
usage() {
cat <<'EOF'
用法: sync_ssh_config.sh [選項]
將本機 ~/.ssh/config 同步到所有 SSH config 中定義的遠端主機。
選項:
--dry-run 僅列出同步目標,不執行
--exclude h1,h2 排除指定 host(逗號分隔)
--parallel N 並行數(預設 5,設 1 為循序執行)
-h, --help 顯示此說明
EOF
}
sync_ssh_config() {
local dry_run=false
local exclude_list=""
local parallel=$DEFAULT_PARALLEL
# 解析參數
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--exclude) exclude_list="$2"; shift 2 ;;
--parallel) parallel="$2"; shift 2 ;;
-h|--help) usage; return 0 ;;
*) echo "未知參數: $1"; usage; return 1 ;;
esac
done
local config_file="$HOME/.ssh/config"
# 檢查 config 檔案是否存在
if [[ ! -f "$config_file" ]]; then
echo "錯誤: $config_file 不存在"
return 1
fi
# 安全的暫存檔
local temp_hosts
temp_hosts=$(mktemp)
local result_dir
result_dir=$(mktemp -d)
trap 'rm -f "$temp_hosts"; rm -rf "$result_dir"' EXIT INT TERM
# 取得本機 hostname 用於排除自身
local local_hostname
local_hostname=$(hostname)
local local_fqdn
local_fqdn=$(hostname -f 2>/dev/null || echo "")
# 建立排除清單的 associative array
declare -A excluded
if [[ -n "$exclude_list" ]]; then
IFS=',' read -ra ex_arr <<< "$exclude_list"
for e in "${ex_arr[@]}"; do
excluded["$e"]=1
done
fi
# 從 config 提取所有 Host 和對應的 HostName/User
awk '
/^Host / {
host = $2
if (host != "*") hosts[host] = host
}
/^[[:space:]]*HostName / {
if (host && host != "*") hostnames[host] = $2
}
/^[[:space:]]*User / {
if (host && host != "*") users[host] = $2
}
END {
for (h in hosts) {
user = (users[h] ? users[h] : ENVIRON["USER"])
hostname = (hostnames[h] ? hostnames[h] : h)
print h "\t" user "@" hostname "\t" hostname
}
}
' "$config_file" > "$temp_hosts"
# 過濾排除項目與本機
local targets=()
local labels=()
while IFS=$'\t' read -r host_alias target actual_host; do
# 排除指定 host
if [[ -n "${excluded[$host_alias]+_}" ]]; then
continue
fi
# 排除本機自身
if [[ "$actual_host" == "localhost" || "$actual_host" == "127.0.0.1" || "$actual_host" == "::1" \
|| "$actual_host" == "$local_hostname" || "$actual_host" == "$local_fqdn" ]]; then
continue
fi
targets+=("$target")
labels+=("$host_alias")
done < "$temp_hosts"
local total=${#targets[@]}
if [[ $total -eq 0 ]]; then
echo "沒有需要同步的目標主機"
return 0
fi
# dry-run 模式
if $dry_run; then
echo "=== Dry Run: 將同步到以下 $total 台主機 ==="
for i in "${!targets[@]}"; do
echo " ${labels[$i]} (${targets[$i]})"
done
return 0
fi
echo "開始同步 $total 台主機(並行數: $parallel)"
echo ""
# 單一 host 的同步函數(子程序用)
sync_one() {
local idx="$1"
local target="$2"
local label="$3"
local result_file="$result_dir/$idx"
# 確保遠端 .ssh 目錄存在(相容 Windows PowerShell 與 Unix shell)
# shellcheck disable=SC2086
ssh $SSH_OPTS "$target" "test -d ~/.ssh || mkdir ~/.ssh; chmod 700 ~/.ssh 2>/dev/null; exit 0" 2>/dev/null
# 複製 config 檔案
# shellcheck disable=SC2086
if scp $SSH_OPTS -q "$config_file" "$target:~/.ssh/config" 2>/dev/null; then
# 設定正確權限(Windows 上 chmod 無效但不影響)
# shellcheck disable=SC2086
ssh $SSH_OPTS "$target" "chmod 600 ~/.ssh/config 2>/dev/null; exit 0" 2>/dev/null
echo "ok" > "$result_file"
echo " ✓ $label ($target)"
return
fi
echo "fail" > "$result_file"
echo " ✗ $label ($target)"
}
# 並行執行
local running=0
for i in "${!targets[@]}"; do
sync_one "$i" "${targets[$i]}" "${labels[$i]}" &
((running++))
if [[ $running -ge $parallel ]]; then
wait -n 2>/dev/null || wait
((running--))
fi
done
wait
# 統計結果
local count=0
local failed=0
for f in "$result_dir"/*; do
[[ -f "$f" ]] || continue
if [[ "$(cat "$f")" == "ok" ]]; then
((count++))
else
((failed++))
fi
done
echo ""
echo "=========================================="
echo "同步完成: 成功 $count 台,失敗 $failed 台(共 $total 台)"
echo "=========================================="
# 返回值反映失敗狀態
[[ $failed -eq 0 ]]
}
# 執行函數,傳遞所有命令列參數
sync_ssh_config "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment