Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active August 23, 2026 13:44
Show Gist options
  • Select an option

  • Save lbussy/928efaa37157e57690361ce4cf19a059 to your computer and use it in GitHub Desktop.

Select an option

Save lbussy/928efaa37157e57690361ce4cf19a059 to your computer and use it in GitHub Desktop.
Interactive and Safer SSH Key Setup

Interactive Raspberry Pi SSH Key Setup

ssh-copy asks for a Raspberry Pi's short hostname, selects a public key, handles the common host-key change caused by re-imaging, installs the key when needed, verifies login, and creates the SSH alias when it is missing.

Intended environment: This convenience utility is designed for personally managed Raspberry Pis that are frequently re-imaged. Automatic removal of a changed saved host key may be inappropriate or unsafe in production, shared, regulated, or high-assurance environments. Use formal identity verification and change control there.

Workflow

  1. Enter the short hostname, such as wspr2; the script uses wspr2.local.
  2. Reuse an existing alias of that name or prepare a new alias automatically.
  3. Select a username; pressing Enter defaults to pi.
  4. Select a public key. The safest valid key is listed first and selected by pressing Enter.
  5. Compare saved server host-key fingerprints with the keys currently offered by the Pi.
  6. If they differ, back up known_hosts and automatically remove only the stale target entry.
  7. Let OpenSSH provide the single prompt to verify and accept a first-seen or replacement host key.
  8. Skip ssh-copy-id when the selected key already works; otherwise install and verify it.
  9. Save a missing alias after successful verification.
  10. Finish with Ready: ssh HOSTNAME.

Host-key safety

ssh-keyscan is used only to display and compare currently offered keys. Its output is never appended to known_hosts and is not proof of identity.

For this personal re-image workflow, a differing saved key is treated as stale: the script creates a timestamped backup of ~/.ssh/known_hosts, removes only the selected host token, and relies on OpenSSH's normal StrictHostKeyChecking=ask prompt for the new key.

A changed host key can also indicate interception. Read the displayed fingerprints and do not accept OpenSSH's prompt unless the change is expected. Production and high-assurance environments should not use this automatic stale-key removal policy.

Local safety behavior

  • The hostname prompt accepts only a short hostname without .local; shell metacharacters and dotted names are rejected.
  • An existing alias matching the short hostname is resolved with ssh -G; otherwise HOSTNAME.local, port 22, and a new alias are prepared.
  • The username menu includes every unique User value found in SSH config, the local user, and Other; pressing Enter defaults to pi.
  • Public-key paths are displayed. The strongest valid existing pair is physically first and is the Enter default: Ed25519 security key, Ed25519, ECDSA security key, ECDSA, RSA, then legacy or unknown types.
  • Existing key pairs must be regular, non-symlink files, and new keys never overwrite a pair.
  • The selected key is tested first when the saved host key matches. If it works, ssh-copy-id is skipped.
  • Only the public key is passed to ssh-copy-id.
  • ssh-copy-id status messages are shown without executable-path prefixes, indentation, or blank spacer lines. Status tags retain terminal colors; NO_COLOR or redirected output remains plain. Message content and exit status are preserved.
  • A new alias is written only after selected-key login succeeds; existing config is backed up and replaced from a same-directory temporary file.
  • Host discovery and SSH operations have bounded timeouts.
  • Dry run performs no scan, connection, key generation, remote modification, host-key replacement, or config write.

Requirements

  • Bash 3.2 or newer
  • ssh, ssh-keygen, ssh-keyscan, and ssh-copy-id
  • awk, sort, cmp, sed, and mktemp
  • Terminal access for interactive choices
  • Existing password or other authentication accepted by the remote account

One-line install - skips review

This revision-free command installs the current remote script directly as /usr/local/bin/ssh-copy with mode 0755:

curl -fsSL https://gist.githubusercontent.com/lbussy/928efaa37157e57690361ce4cf19a059/raw/ssh_copy.sh | sudo bash -c 'set -euo pipefail; install_tmp=$(mktemp); trap "rm -f -- \"$install_tmp\"" EXIT; cat > "$install_tmp"; test -s "$install_tmp"; bash -n "$install_tmp"; install -m 0755 "$install_tmp" /usr/local/bin/ssh-copy'

This deliberately skips review and follows the current unpinned Gist revision. Anyone controlling the served content could install code with elevated privileges. Use it only when you accept that risk; otherwise use the inspect-first method below.

Install: download and inspect first

This revision-free URL always retrieves the current Gist:

curl -fsSL https://gist.githubusercontent.com/lbussy/928efaa37157e57690361ce4cf19a059/raw/ssh_copy.sh -o ssh_copy.sh
less ssh_copy.sh
bash ssh_copy.sh --install
rm -f ssh_copy.sh

Because the URL is unpinned, its content can change. Inspect it before installation. The explicit installer places the reviewed file at /usr/local/bin/ssh-copy with mode 0755; it uses sudo only for that installation when required.

Usage

Run the interactive workflow after installation:

ssh-copy

Preview selections and local saved-key information without scanning or contacting the host:

ssh-copy --dry-run

Show help:

ssh-copy --help

Unreachable or powered-off hosts

Host-key discovery has a hard 10-second process deadline in addition to ssh-keyscan connection timing. If DNS or mDNS resolution, routing, or the SSH service is unavailable, the script exits with a clear error before ssh-copy-id runs.

For troubleshooting, check the host and SSH path directly:

ping wspr2.local
ssh -vvv -o ConnectTimeout=10 wspr2

The deadline can be temporarily adjusted from 1 through 60 seconds:

SSH_COPY_CONTACT_TIMEOUT_SECONDS=15 ssh-copy

The same deadline is passed to key installation and final verification along with one connection attempt and bounded server-alive settings.

Important limitations and risks

  • A changed saved host key is removed automatically after backup. This is convenient for expected Pi re-images but unsafe when the change is unexpected.
  • ssh-copy-id modifies the remote account's authorization file, normally ~/.ssh/authorized_keys.
  • A successful key installation is not rolled back automatically if later verification fails.
  • SSH config can inherit behavior from wildcard blocks and Include files. Review ssh -G ALIAS, especially proxy and identity settings.
  • New alias entries are appended as explicit blocks; earlier matching wildcard settings can still affect the effective configuration.
  • The interactive hostname path intentionally supports short .local hostnames only.
  • Generated private keys should use a strong passphrase and must be protected.
  • Backups contain sensitive connection metadata and should retain restrictive permissions.
  • Disabling password authentication is a separate server-administration task. Preserve console or alternate access before changing it.

License

MIT License. Use at your own risk.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
umask 077
SSH_DIR="$HOME/.ssh"
SSH_CONFIG="$SSH_DIR/config"
KNOWN_HOSTS="$SSH_DIR/known_hosts"
DRY_RUN=false
CONTACT_TIMEOUT_SECONDS="${SSH_COPY_CONTACT_TIMEOUT_SECONDS:-10}"
IS_NEW_HOST=false
HOST_KEY_STATE="unknown"
SSH_ALIAS=""
SSH_HOST=""
SSH_PORT="22"
SSH_USER=""
PRIVATE_KEY=""
PUBLIC_KEY=""
declare -a TEMP_FILES=()
cleanup() {
(( ${#TEMP_FILES[@]} == 0 )) || rm -f -- "${TEMP_FILES[@]}"
}
trap cleanup EXIT
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
warn() { printf 'Warning: %s\n' "$*" >&2; }
run_with_deadline() {
local timeout_seconds="$1"
shift
local pid deadline status
"$@" &
pid=$!
deadline=$((SECONDS + timeout_seconds))
while kill -0 "$pid" 2>/dev/null; do
if (( SECONDS >= deadline )); then
kill -TERM "$pid" 2>/dev/null || true
sleep 0.2
kill -KILL "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
return 124
fi
sleep 0.1
done
wait "$pid"
status=$?
return "$status"
}
usage() {
cat <<'EOF'
Usage: ssh_copy.sh [--dry-run] [--help]
ssh_copy.sh --install
Interactive workflow:
1. Select an SSH alias or enter a new host.
2. Confirm user, port, and alias.
3. Select or generate a client key pair.
4. Inspect saved and currently offered server host-key fingerprints.
5. Optionally replace a verified old host key.
6. Install the public key and verify key-only login.
7. Save a new SSH alias after successful verification.
EOF
}
expand_path() {
local path="$1" prefix=$'\x7e/'
if [[ "$path" == "~" ]]; then
printf '%s\n' "$HOME"
elif [[ "${path:0:2}" == "$prefix" ]]; then
printf '%s/%s\n' "$HOME" "${path:2}"
else
printf '%s\n' "$path"
fi
}
validate_alias() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] ||
die "Alias contains unsafe characters."
}
validate_host() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]] ||
die "HostName must be a DNS name or IPv4 address without shell metacharacters."
}
validate_user() {
[[ "$1" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] ||
die "User contains unsafe characters."
}
validate_port() {
if [[ ! "$1" =~ ^[0-9]+$ ]] || (( 10#$1 < 1 || 10#$1 > 65535 )); then
die "Port must be an integer from 1 through 65535."
fi
}
prompt_value() {
local prompt="$1" default="$2" value
printf '%s [%s]: ' "$prompt" "$default" >&2
read -r value < /dev/tty || die "Could not read terminal input."
printf '%s\n' "${value:-$default}"
}
check_tools() {
local command
for command in ssh ssh-keygen ssh-keyscan ssh-copy-id awk sort cmp mktemp; do
command -v "$command" >/dev/null 2>&1 ||
die "Required command is missing: $command"
done
if [[ "$DRY_RUN" == false ]]; then
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
elif [[ ! -d "$SSH_DIR" ]]; then
warn "SSH directory does not exist; dry run will not create it."
fi
if [[ ! -e "$SSH_CONFIG" ]]; then
if [[ "$DRY_RUN" == true ]]; then
warn "SSH config does not exist; dry run will not create it."
else
: > "$SSH_CONFIG"
chmod 600 "$SSH_CONFIG"
fi
fi
[[ ! -e "$SSH_CONFIG" || ( -f "$SSH_CONFIG" && ! -L "$SSH_CONFIG" ) ]] ||
die "SSH config must be a regular, non-symlink file."
}
config_aliases() {
[[ -r "$SSH_CONFIG" ]] || return 0
awk '
tolower($1) == "host" {
for (i=2; i<=NF; i++)
if ($i !~ /[*?!]/) print $i
}
' "$SSH_CONFIG" | sort -u
}
resolved_value() {
local alias="$1" key="$2"
ssh -G "$alias" 2>/dev/null |
awk -v wanted="$key" '$1 == wanted { print $2; exit }'
}
select_host() {
local short_name existing configured_user="" found=false
printf 'Raspberry Pi hostname (without .local): '
read -r short_name < /dev/tty || die "Could not read hostname."
[[ "$short_name" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]] ||
die "Enter only the short hostname, without .local."
SSH_ALIAS="$short_name"
SSH_HOST="$short_name.local"
SSH_PORT="22"
while IFS= read -r existing; do
[[ "$existing" != "$SSH_ALIAS" ]] || { found=true; break; }
done < <(config_aliases)
if [[ "$found" == true ]]; then
local resolved_host resolved_port
resolved_host=$(resolved_value "$SSH_ALIAS" hostname)
resolved_port=$(resolved_value "$SSH_ALIAS" port)
configured_user=$(resolved_value "$SSH_ALIAS" user)
[[ -z "$resolved_host" ]] || SSH_HOST="$resolved_host"
[[ -z "$resolved_port" ]] || SSH_PORT="$resolved_port"
else
IS_NEW_HOST=true
fi
validate_alias "$SSH_ALIAS"
validate_host "$SSH_HOST"
validate_port "$SSH_PORT"
select_username "$configured_user"
}
config_users() {
[[ -r "$SSH_CONFIG" ]] || return 0
awk 'tolower($1) == "user" && NF >= 2 { print $2 }' "$SSH_CONFIG" | sort -u
}
select_username() {
local configured_user="${1:-}" candidate choice index=1
local -a users=("pi")
while IFS= read -r candidate; do
[[ -n "$candidate" ]] || continue
validate_user "$candidate" || continue
local seen=false existing
for existing in "${users[@]}"; do
[[ "$existing" != "$candidate" ]] || { seen=true; break; }
done
[[ "$seen" == true ]] || users+=("$candidate")
done < <({
[[ -z "$configured_user" ]] || printf '%s\n' "$configured_user"
config_users
printf '%s\n' "${USER:-}"
} | awk 'NF' | sort -u)
printf '\nAvailable SSH usernames (press Enter for pi):\n'
for candidate in "${users[@]}"; do
if [[ "$candidate" == "pi" ]]; then
printf ' %d) %s [default]\n' "$index" "$candidate"
else
printf ' %d) %s\n' "$index" "$candidate"
fi
((index += 1))
done
printf ' %d) Other\n' "$index"
printf 'Selection: '
read -r choice < /dev/tty || die "Could not read username selection."
if [[ -z "$choice" ]]; then
SSH_USER="pi"
return
fi
[[ "$choice" =~ ^[0-9]+$ ]] || die "Invalid username selection."
if (( choice == index )); then
printf 'SSH username: '
read -r SSH_USER < /dev/tty || die "Could not read SSH username."
validate_user "$SSH_USER"
elif (( choice >= 1 && choice < index )); then
SSH_USER="${users[choice-1]}"
else
die "Username selection is out of range."
fi
}
generate_key() {
local default_path path
default_path="$SSH_DIR/id_ed25519"
path=$(prompt_value "New private-key path" "$default_path")
path=$(expand_path "$path")
[[ "$path" != *.pub ]] || die "Enter a private-key path, not .pub."
[[ ! -e "$path" && ! -L "$path" && ! -e "$path.pub" && ! -L "$path.pub" ]] ||
die "Refusing to overwrite an existing key pair."
[[ "$DRY_RUN" == false ]] || die "Dry run cannot generate a key."
mkdir -p "$(dirname "$path")"
printf 'Generating Ed25519 key; a passphrase is recommended.\n'
ssh-keygen -t ed25519 -a 64 -f "$path"
PRIVATE_KEY="$path"
}
key_rank() {
local public_key="$1" details type
details=$(ssh-keygen -lf "$public_key" 2>/dev/null) || { printf '99\n'; return; }
type="${details##* (}"
type="${type%)}"
case "$type" in
ED25519-SK) printf '10\n' ;;
ED25519) printf '20\n' ;;
ECDSA-SK) printf '30\n' ;;
ECDSA) printf '40\n' ;;
RSA) printf '50\n' ;;
*) printf '90\n' ;;
esac
}
select_client_key() {
local -a keys=() ordered=()
local key choice index=1 default_index=0 default_rank=999 rank
for key in "$SSH_DIR"/id_*; do
[[ -f "$key" && ! -L "$key" && "$key" != *.pub &&
-f "$key.pub" && ! -L "$key.pub" ]] || continue
ssh-keygen -lf "$key.pub" >/dev/null 2>&1 || continue
keys+=("$key")
rank=$(key_rank "$key.pub")
if (( rank < default_rank )); then
default_rank=$rank
default_index=${#keys[@]}
elif (( rank == default_rank )) && [[ "${key##*/}" == "id_ed25519" ]]; then
default_index=${#keys[@]}
fi
done
if (( default_index > 0 )); then
ordered+=("${keys[default_index-1]}")
for key in "${keys[@]}"; do
[[ "$key" == "${keys[default_index-1]}" ]] || ordered+=("$key")
done
fi
printf '\nAvailable public keys:\n'
for key in "${ordered[@]}"; do
if (( index == 1 )); then
printf ' %d) %s.pub [default]\n' "$index" "$key"
else
printf ' %d) %s.pub\n' "$index" "$key"
fi
((index += 1))
done
printf ' %d) Generate a new Ed25519 key\n' "$index"
if (( ${#ordered[@]} > 0 )); then
printf 'Selection (press Enter for 1): '
else
printf 'Selection (press Enter to generate): '
fi
read -r choice < /dev/tty || die "Could not read key selection."
if [[ -z "$choice" ]]; then
if (( ${#ordered[@]} > 0 )); then
PRIVATE_KEY="${ordered[0]}"
else
generate_key
fi
elif [[ "$choice" =~ ^[0-9]+$ ]] && (( choice == index )); then
generate_key
elif [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice < index )); then
PRIVATE_KEY="${ordered[choice-1]}"
else
die "Invalid key selection."
fi
PUBLIC_KEY="$PRIVATE_KEY.pub"
ssh-keygen -lf "$PUBLIC_KEY" >/dev/null || die "Invalid public key: $PUBLIC_KEY"
}
host_token() {
if [[ "$SSH_PORT" == 22 ]]; then
printf '%s\n' "$SSH_HOST"
else
printf '[%s]:%s\n' "$SSH_HOST" "$SSH_PORT"
fi
}
fingerprints_from_file() {
local file="$1"
[[ -s "$file" ]] || { printf ' none\n'; return; }
ssh-keygen -lf "$file" 2>/dev/null | sed 's/^/ /' ||
printf ' unable to parse fingerprints\n'
}
inspect_host_key() {
local token known_file scan_file known_keys scan_keys backup
token=$(host_token)
known_file=$(mktemp)
scan_file=$(mktemp)
TEMP_FILES+=("$known_file" "$scan_file")
ssh-keygen -F "$token" -f "$KNOWN_HOSTS" 2>/dev/null |
awk '!/^#/ && NF >= 3 { print $2, $3 }' > "$known_file" || true
if [[ "$DRY_RUN" == true ]]; then
printf '\nSaved host-key fingerprints for %s:\n' "$token"
fingerprints_from_file "$known_file"
printf 'Dry run: current host keys were not scanned.\n'
return
fi
if ! run_with_deadline "$CONTACT_TIMEOUT_SECONDS" \
ssh-keyscan -T 5 -p "$SSH_PORT" "$SSH_HOST" > "$scan_file" 2>/dev/null; then
die "Could not contact $SSH_HOST:$SSH_PORT within $CONTACT_TIMEOUT_SECONDS seconds. Check power, name resolution, and SSH."
fi
[[ -s "$scan_file" ]] || die "The host offered no SSH host keys."
printf '\nSaved host-key fingerprints:\n'
fingerprints_from_file "$known_file"
printf 'Currently offered fingerprints (not yet trusted):\n'
fingerprints_from_file "$scan_file"
if [[ ! -s "$known_file" ]]; then
HOST_KEY_STATE="new"
return
fi
known_keys=$(awk '{print $1, $2}' "$known_file" | sort -u)
scan_keys=$(awk 'NF >= 3 {print $2, $3}' "$scan_file" | sort -u)
if [[ "$known_keys" == "$scan_keys" ]]; then
HOST_KEY_STATE="known"
return
fi
backup="$KNOWN_HOSTS.backup.$(date +%Y%m%d%H%M%S)"
cp -p "$KNOWN_HOSTS" "$backup" ||
die "Could not back up known_hosts; the old entry was not removed."
ssh-keygen -R "$token" -f "$KNOWN_HOSTS" >/dev/null ||
die "Could not remove the old host-key entry. Backup: $backup"
HOST_KEY_STATE="replaced"
printf '\nHost key changed for %s.\n' "$token"
printf 'Expected after re-imaging; otherwise this can indicate interception.\n'
printf 'Backed up known_hosts: %s\n' "$backup"
printf 'Removed the old entry; OpenSSH will ask you to verify the new key.\n'
}
show_plan() {
printf '\nSSH setup plan:\n'
printf ' Alias: %s%s\n' "$SSH_ALIAS" "$([[ "$IS_NEW_HOST" == true ]] && printf ' (new)' || true)"
printf ' Destination: %s@%s\n' "$SSH_USER" "$SSH_HOST"
printf ' Port: %s\n' "$SSH_PORT"
printf ' Public key: %s\n' "$PUBLIC_KEY"
printf ' Fingerprint: %s\n' "$(ssh-keygen -lf "$PUBLIC_KEY")"
}
normalize_ssh_copy_output() {
local info_color="" warning_color="" error_color="" reset_color=""
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
info_color=$'\033[36m'
warning_color=$'\033[33m'
error_color=$'\033[31m'
reset_color=$'\033[0m'
fi
awk -v info_color="$info_color" -v warning_color="$warning_color" \
-v error_color="$error_color" -v reset_color="$reset_color" '
{
sub(/^([^:]*\/)?ssh-copy-id: /, "")
if ($0 ~ /^[[:space:]]+\(/) {
sub(/^[[:space:]]+/, "")
}
sub(/^INFO:/, info_color "INFO:" reset_color)
sub(/^WARNING:/, warning_color "WARNING:" reset_color)
sub(/^ERROR:/, error_color "ERROR:" reset_color)
}
/^[[:space:]]*$/ { next }
{ print }
'
}
install_client_key() {
local destination="$SSH_USER@$SSH_HOST" copy_status
show_plan
inspect_host_key
[[ "$DRY_RUN" == false ]] || {
printf 'Dry run: no connection or change occurred.\n'
return 0
}
if [[ "$HOST_KEY_STATE" == "known" ]] &&
ssh -p "$SSH_PORT" -o BatchMode=yes -o IdentitiesOnly=yes \
-o ConnectTimeout="$CONTACT_TIMEOUT_SECONDS" -o ConnectionAttempts=1 \
-o StrictHostKeyChecking=yes -i "$PRIVATE_KEY" "$destination" true \
>/dev/null 2>&1; then
printf 'Selected public key already works; nothing to copy.\n'
return 0
fi
set +e
ssh-copy-id -i "$PUBLIC_KEY" -p "$SSH_PORT" \
-o ConnectTimeout="$CONTACT_TIMEOUT_SECONDS" -o ConnectionAttempts=1 \
-o ServerAliveInterval=5 -o ServerAliveCountMax=1 \
-o StrictHostKeyChecking=ask "$destination" 2>&1 |
normalize_ssh_copy_output
copy_status=${PIPESTATUS[0]}
set -e
(( copy_status == 0 )) || die "ssh-copy-id failed with status $copy_status."
ssh -p "$SSH_PORT" -o BatchMode=yes -o IdentitiesOnly=yes \
-o ConnectTimeout="$CONTACT_TIMEOUT_SECONDS" -o ConnectionAttempts=1 \
-o ServerAliveInterval=5 -o ServerAliveCountMax=1 \
-o StrictHostKeyChecking=yes -i "$PRIVATE_KEY" "$destination" true ||
die "Key installation returned, but selected-key login verification failed."
printf 'Public key installed and verified.\n'
}
save_new_alias() {
[[ "$IS_NEW_HOST" == true && "$DRY_RUN" == false ]] || return 0
local temp backup
temp=$(mktemp "$SSH_DIR/config.tmp.XXXXXX")
if [[ -e "$SSH_CONFIG" ]]; then
cp -p "$SSH_CONFIG" "$temp" || die "Could not prepare SSH config update."
backup="$SSH_CONFIG.backup.$(date +%Y%m%d%H%M%S)"
cp -p "$SSH_CONFIG" "$backup" || die "Could not back up SSH config."
fi
{
printf '\nHost %s\n' "$SSH_ALIAS"
printf ' HostName %s\n' "$SSH_HOST"
printf ' User %s\n' "$SSH_USER"
printf ' Port %s\n' "$SSH_PORT"
printf ' IdentityFile %s\n' "$PRIVATE_KEY"
printf ' StrictHostKeyChecking ask\n'
} >> "$temp"
chmod 600 "$temp"
mv -f "$temp" "$SSH_CONFIG"
printf 'Saved SSH alias %s in %s.\n' "$SSH_ALIAS" "$SSH_CONFIG"
}
install_self() {
local source target="/usr/local/bin/ssh-copy"
source="${BASH_SOURCE[0]}"
[[ "$source" != "-" && -f "$source" && ! -L "$source" ]] ||
die "--install requires an inspected local, non-symlink script file."
source="$(cd "$(dirname "$source")" && pwd -P)/$(basename "$source")"
command -v install >/dev/null 2>&1 || die "The install command is required."
if (( EUID == 0 )); then
install -d -m 0755 /usr/local/bin
install -m 0755 "$source" "$target"
else
command -v sudo >/dev/null 2>&1 || die "sudo is required to install in /usr/local/bin."
sudo install -d -m 0755 /usr/local/bin
sudo install -m 0755 "$source" "$target"
fi
printf 'Installed %s. Run it with: ssh-copy\n' "$target"
}
main() {
while (( $# )); do
case "$1" in
--install) install_self; exit 0 ;;
--dry-run) DRY_RUN=true ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "Unknown argument: $1" ;;
esac
shift
done
if [[ ! "$CONTACT_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] ||
(( CONTACT_TIMEOUT_SECONDS < 1 || CONTACT_TIMEOUT_SECONDS > 60 )); then
die "SSH_COPY_CONTACT_TIMEOUT_SECONDS must be from 1 through 60."
fi
check_tools
select_host
select_client_key
install_client_key
save_new_alias
[[ "$DRY_RUN" == true ]] ||
printf 'Ready: ssh %s\n' "$SSH_ALIAS"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment