diff --git a/scripts/install.sh b/scripts/install.sh index e4553710..74adbbcb 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -66,6 +66,11 @@ while [[ $# -gt 0 ]]; do shift ;; --repo=*) OFFICER_REPO="${1#--repo=}" ;; + # Every question that HAS a default answers itself. The ones with no possible + # default still ask — see the note above the run below. + --unattended | -y) + export UNATTENDED=1 ASSUME_YES=1 + ;; -h | --help) say "usage: install.sh [--machine-only | --officer-only] [--repo ]" say "" @@ -73,9 +78,16 @@ while [[ $# -gt 0 ]]; do say " --machine-only stop after the machine is provisioned" say " --officer-only the platform only, on a machine you already trust" say " --repo clone the platform from here instead of the default" + say " --unattended take the default for every question that has one (-y)" say "" say " The default is a private Gitea over SSH, which only authenticates on a" say " machine whose key it already knows. Pass an https URL on a fresh box." + say "" + say " --unattended still asks the questions that have no possible default:" + say " the username, the Tailscale control plane / login server / auth key," + say " an SSH public key when the account has none, and the git identity." + say " Answer those ahead of time with SETUP_USERNAME, TS_LOGIN_SERVER," + say " TS_AUTHKEY and TIMEZONE to reduce it further." exit 0 ;; *) die "unknown option: $1" ;; @@ -131,6 +143,8 @@ if [[ "$KERNEL" != "Darwin" && "$EUID" -ne 0 ]]; then SETUP_USERNAME="${SETUP_USERNAME:-}" \ MACHINE_ROLE="${MACHINE_ROLE:-}" \ REPORT_FILE="${REPORT_FILE:-}" \ + UNATTENDED="${UNATTENDED:-}" \ + ASSUME_YES="${ASSUME_YES:-}" \ OFFICER_REPO="${OFFICER_REPO:-}" \ bash "$SELF" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"} fi diff --git a/scripts/setup/machine-setup/lib/base.sh b/scripts/setup/machine-setup/lib/base.sh index 57caa162..cf5b804a 100644 --- a/scripts/setup/machine-setup/lib/base.sh +++ b/scripts/setup/machine-setup/lib/base.sh @@ -285,6 +285,37 @@ page() { # deliberate keystroke would train people to hold the y key down. # # ASSUME_YES=1 answers all of them, for an unattended run. + +# A numbered menu's answer, or its own default when running unattended. +# +# menu_answer DNS_CHOICE " Which one? (1-5) [1]: " +# +# ── Why empty, rather than a default passed in ── +# +# Every menu in this script reads its choice and then consumes it as +# `${CHOICE:-}`, so the default already lives at the point of use — which is the +# right place, next to the options it selects between. Setting the variable EMPTY is +# therefore exactly what pressing Enter does, and it cannot drift from the default +# the prompt advertises the way a second copy passed in here would. +# +# `read <<<''` rather than `eval` or `declare -g`: no eval, and `declare -g` is bash +# 4.2+, which rules out the bash 3.2 that macOS still ships. +# +# The prompt is still printed, with the reason, because a transcript that silently +# skips a question reads as a question that was never asked. +menu_answer() { + local var="$1" prompt="$2" + if [[ "${UNATTENDED:-}" == "1" ]]; then + printf '%s%s\n' "$prompt" "— unattended, taking the default" + read -r "$var" <<<'' + return 0 + fi + read -rp "$prompt" "$var" || { + echo "" + fail "No answer." + } +} + confirm() { local message="${1:-Proceed?}" # Second argument flips the default. Most questions here are "do the thing you diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index 9913204f..b91b39ab 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -18,6 +18,12 @@ ANSWERS_FILE="$SCRIPT_DIR/.setup-answers" # still runs, because every section needs what it establishes — the system, the # role, the account and its home. ONLY_STEP="" + +# Kept before the loop consumes them: this script re-executes itself through sudo +# below and was passing `"$@"`, which `shift` had already emptied — so `--only` and +# `--reask` silently stopped existing the moment it escalated. +ORIGINAL_ARGS=(${@+"$@"}) + while [[ $# -gt 0 ]]; do case "$1" in --only) @@ -32,12 +38,25 @@ while [[ $# -gt 0 ]]; do RE_ASK=1 shift ;; + # Every question that HAS a default answers itself; the ones with none still ask. + # ASSUME_YES drives confirm(), UNATTENDED drives the numbered menus and the + # free-text prompts that carry a default. + --unattended | -y) + export UNATTENDED=1 ASSUME_YES=1 + shift + ;; -l | --list) grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}" exit 0 ;; -h | --help) - echo "usage: machine-setup.sh [--only ] [--reask] [--list]" + echo "usage: machine-setup.sh [--only ] [--reask] [--list] [--unattended]" + echo "" + echo " --unattended take the default for every question that has one (-y)." + echo " Still asks the ones with no possible default: the" + echo " username, the Tailscale control plane / login server /" + echo " auth key, an SSH public key when the account has none," + echo " and the git identity." exit 0 ;; *) echo "unknown option: $1" >&2 && exit 2 ;; @@ -155,7 +174,9 @@ elif [[ "$EUID" -ne 0 ]]; then SETUP_USERNAME="${SETUP_USERNAME:-}" \ MACHINE_ROLE="${MACHINE_ROLE:-}" \ REPORT_FILE="${REPORT_FILE:-}" \ - bash "$SCRIPT_DIR/machine-setup.sh" "$@" + UNATTENDED="${UNATTENDED:-}" \ + ASSUME_YES="${ASSUME_YES:-}" \ + bash "$SCRIPT_DIR/machine-setup.sh" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"} fi # On macOS the account running the script IS the account, and there is nothing to @@ -761,10 +782,9 @@ if ! skip; then echo "" while [[ -z "${TIMEZONE:-}" ]]; do - if ! read -rp " Pick a number, or type a zone name — Enter keeps ${CURRENT_TZ:-the current one}: " TZ_CHOICE; then - echo "" - fail "No answer. Set TIMEZONE= to answer this ahead of time." - fi + # Unattended keeps the current zone, which is what Enter does here. TIMEZONE= + # in the environment answers it ahead of time and skips this block entirely. + menu_answer TZ_CHOICE " Pick a number, or type a zone name — Enter keeps ${CURRENT_TZ:-the current one}: " if [[ -z "$TZ_CHOICE" ]]; then TIMEZONE="$CURRENT_TZ" @@ -918,10 +938,7 @@ elif ! skip; then BALLAST_FILE="" while [[ -z "$BALLAST_FILE" ]]; do - if ! read -rp " Which one? (1/2/3) [1]: " BALLAST_WHERE; then - echo "" - fail "No answer." - fi + menu_answer BALLAST_WHERE " Which one? (1/2/3) [1]: " case "${BALLAST_WHERE:-1}" in 1) BALLAST_FILE="${USER_HOME}/${BALLAST_NAME}" ;; 2) BALLAST_FILE="${OFFICER_ROOT}/${BALLAST_NAME}" ;; @@ -961,10 +978,7 @@ elif ! skip; then BALLAST_PCT="" while [[ -z "$BALLAST_PCT" ]]; do - if ! read -rp " Which one? (1/2/3) [2]: " BALLAST_SIZE_CHOICE; then - echo "" - fail "No answer." - fi + menu_answer BALLAST_SIZE_CHOICE " Which one? (1/2/3) [2]: " case "${BALLAST_SIZE_CHOICE:-2}" in 1) BALLAST_PCT=5 ;; 2) BALLAST_PCT=10 ;; @@ -1339,10 +1353,7 @@ if ! skip; then DNS_FALLBACK="" DNS_CHOSEN="" while [[ -z "$DNS_CHOSEN" ]]; do - if ! read -rp " Which one? (1-5) [1]: " DNS_CHOICE; then - echo "" - fail "No answer." - fi + menu_answer DNS_CHOICE " Which one? (1-5) [1]: " case "${DNS_CHOICE:-1}" in 1) DNS_CHOSEN="keep" ;; 2) @@ -1449,10 +1460,7 @@ elif ! skip; then NET_CHOICE="" while [[ -z "$NET_CHOICE" ]]; do - if ! read -rp " Which one? (1/2/3) [1]: " NET_ANSWER; then - echo "" - fail "No answer." - fi + menu_answer NET_ANSWER " Which one? (1/2/3) [1]: " case "${NET_ANSWER:-1}" in 1 | 2 | 3) NET_CHOICE="${NET_ANSWER:-1}" ;; *) warn "Pick 1, 2 or 3." ;; @@ -1817,10 +1825,7 @@ if ! skip; then DOCKER_ACCESS="" while [[ -z "$DOCKER_ACCESS" ]]; do - if ! read -rp " Which one? (1/2/3) [1]: " DOCKER_CHOICE; then - echo "" - fail "No answer." - fi + menu_answer DOCKER_CHOICE " Which one? (1/2/3) [1]: " case "${DOCKER_CHOICE:-1}" in 1 | 2 | 3) DOCKER_ACCESS="${DOCKER_CHOICE:-1}" ;; *) warn "Pick 1, 2 or 3." ;; @@ -1928,10 +1933,7 @@ if ! skip; then NVIM_REPO="" NVIM_PICK="" while [[ -z "$NVIM_PICK" ]]; do - if ! read -rp " Which one? (1/2/3) [1]: " NVIM_CHOICE; then - echo "" - fail "No answer." - fi + menu_answer NVIM_CHOICE " Which one? (1/2/3) [1]: " case "${NVIM_CHOICE:-1}" in 1) NVIM_REPO="https://github.com/LazyVim/starter" @@ -2337,10 +2339,7 @@ EOF EDITOR_PICK="" while [[ -z "$EDITOR_PICK" ]]; do - if ! read -rp " Which one? (1-${#EDITORS[@]}) [1]: " EDITOR_CHOICE; then - echo "" - fail "No answer." - fi + menu_answer EDITOR_CHOICE " Which one? (1-${#EDITORS[@]}) [1]: " EDITOR_CHOICE="${EDITOR_CHOICE:-1}" if [[ "$EDITOR_CHOICE" =~ ^[0-9]+$ ]] && ((EDITOR_CHOICE >= 1 && EDITOR_CHOICE <= ${#EDITORS[@]})); then EDITOR_PICK="${EDITORS[$((EDITOR_CHOICE - 1))]}" diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index 8ba22663..3402d782 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -45,14 +45,19 @@ while [[ $# -gt 0 ]]; do OFFICER_REPO="${1#*=}" shift ;; + --unattended | -y) + export UNATTENDED=1 ASSUME_YES=1 + shift + ;; -l | --list) grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}" exit 0 ;; -h | --help) - echo "usage: officer-setup.sh [--only ] [--list] [--repo ]" + echo "usage: officer-setup.sh [--only ] [--list] [--repo ] [--unattended]" echo "" echo " --only run one step; --list names them" + echo " --unattended take the default for every question that has one (-y)" echo " --repo clone from here instead of the default, which is a" echo " private Gitea over SSH and only authenticates on a" echo " machine whose key it already knows. Same as exporting" @@ -118,6 +123,8 @@ elif [[ "$EUID" -ne 0 ]]; then SETUP_USERNAME="${SETUP_USERNAME:-}" \ MACHINE_ROLE="${MACHINE_ROLE:-}" \ REPORT_FILE="${REPORT_FILE:-}" \ + UNATTENDED="${UNATTENDED:-}" \ + ASSUME_YES="${ASSUME_YES:-}" \ OFFICER_REPO="${OFFICER_REPO:-}" \ bash "$SCRIPT_DIR/officer-setup.sh" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"} fi diff --git a/scripts/setup/officer-setup/lib/base.sh b/scripts/setup/officer-setup/lib/base.sh index 4cbcff12..6f519e1c 100644 --- a/scripts/setup/officer-setup/lib/base.sh +++ b/scripts/setup/officer-setup/lib/base.sh @@ -113,6 +113,14 @@ confirm() { ask_required() { local __var="$1" message="$2" default="$3" answer="" + # Unattended takes the default where there IS one. Where there is not — the owning + # account on a machine that machine-setup never ran on — it still asks, because + # there is nothing to fall back to and a guess would install as the wrong user. + if [[ "${UNATTENDED:-}" == "1" && -n "$default" ]]; then + printf ' %s [%s] — unattended, taking the default\n' "$message" "$default" + printf -v "$__var" '%s' "$default" + return 0 + fi while [[ -z "$answer" ]]; do if ! read -rp " ${message}${default:+ [$default]}: " answer; then echo ""