Last active
February 23, 2026 02:52
-
-
Save superyngo/bed6b95a32cfef619949c409797c0237 to your computer and use it in GitHub Desktop.
sync ssh config
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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