#!/usr/bin/env sh # apply-config.sh — 把設定寫進 shell rc 檔或建立設定目錄(供 /jsc-cli:setup 呼叫)。 # 用法: # apply-config.sh set {KEY} {VALUE} # 寫入或更新一個環境變數(export) # apply-config.sh unset {KEY} # 從 jsc 段落移除一個環境變數 # apply-config.sh mkdir {PATH} # 建立目錄(值可帶 $VAR 與開頭的 ~) # apply-config.sh show # 印出目前 jsc 段落的內容(每行 KEYVALUE) # apply-config.sh rcfiles # 印出這次會寫入的 rc 檔路徑 # # 寫入位置:每個既有的 shell rc 檔(~/.bashrc、~/.zshrc、~/.config/fish/config.fish) # 都寫一份,全部不存在時才建立 ~/.bashrc。內容一律收在標記段落之間: # # jsc-config # export KEY='值' # # /jsc-config # 段落整段重寫,重跑只取代不疊加。段落外的內容一律不動——rc 檔是使用者自己的檔案, # jsc 只負責自己那一段。 # # 動到任何檔案之前先原樣備份到 $JSC_HOME/backup/config/{yyyyMMdd_HHmmss}/,備份失敗就不寫入。 # fish 的語法與 POSIX shell 不同,寫進去的是 set -gx KEY 值。 # # 結束碼: 0=成功 2=用法錯誤 4=備份或寫入失敗 set -eu JSC_HOME="${JSC_HOME:-$HOME/.jsc}" MARK_OPEN='# jsc-config' MARK_SHUT='# /jsc-config' TAB=$(printf '\t') usage() { echo "用法:apply-config.sh {set {KEY} {VALUE}|unset {KEY}|mkdir {PATH}|show|rcfiles}" >&2 exit 2 } cmd="${1:-}" case "$cmd" in set|unset|mkdir|show|rcfiles) ;; *) usage ;; esac rc_files() { found="" for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do [ -f "$f" ] && { printf '%s\n' "$f"; found=1; } done [ -n "$found" ] || printf '%s\n' "$HOME/.bashrc" } if [ "$cmd" = rcfiles ]; then rc_files; exit 0; fi # 目前段落裡的設定,格式 KEYVALUE。取第一個既有 rc 檔為準:寫入時每個檔案內容相同。 read_pairs() { for f in $(rc_files); do [ -f "$f" ] || continue awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" ' $0==o { inb=1; next } $0==s { inb=0; next } inb { line=$0 sub(/^export /, "", line) # POSIX shell sub(/^set -gx /, "", line) # fish if (line ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { eq=index(line, "="); k=substr(line, 1, eq-1); v=substr(line, eq+1) } else { sp=index(line, " "); if (sp==0) next k=substr(line, 1, sp-1); v=substr(line, sp+1) } gsub(/^'"'"'|'"'"'$/, "", v) if (k != "") print k "\t" v } ' "$f" return 0 done } if [ "$cmd" = show ]; then read_pairs; exit 0; fi if [ "$cmd" = mkdir ]; then raw="${2:-}"; [ -n "$raw" ] || usage case "$raw" in "~/"*) raw="$HOME/${raw#\~/}" ;; esac path=$( set +u; eval "printf '%s' \"$raw\"" ) [ -n "$path" ] || { echo "路徑展開後是空的:${2:-}" >&2; exit 4; } mkdir -p "$path" 2>/dev/null || { echo "無法建立目錄:$path" >&2; exit 4; } printf 'created\t%s\n' "$path" exit 0 fi key="${2:-}" [ -n "$key" ] || usage case "$key" in [A-Za-z_]*) ;; *) echo "變數名不合法:$key" >&2; exit 2 ;; esac value="${3:-}" if [ "$cmd" = set ] && [ -z "$value" ]; then echo "set 需要值" >&2; exit 2; fi # 合併:既有設定 + 這次的異動,重寫整段。少了這一步,寫第二個變數會蓋掉第一個。 pairs=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 4; } read_pairs > "$pairs" 2>/dev/null || true merged=$(mktemp) || { rm -f "$pairs"; echo "無法建立暫存檔" >&2; exit 4; } while IFS="$TAB" read -r k v; do [ -n "${k:-}" ] || continue [ "$k" = "$key" ] && continue printf '%s\t%s\n' "$k" "$v" >> "$merged" done < "$pairs" rm -f "$pairs" if [ "$cmd" = set ]; then printf '%s\t%s\n' "$key" "$value" >> "$merged"; fi # 備份:動到的每個檔案先原樣複製一份,備份不了就整個不寫。 stamp=$(date +%Y%m%d_%H%M%S) backup_dir="$JSC_HOME/backup/config/$stamp" mkdir -p "$backup_dir" 2>/dev/null || { rm -f "$merged"; echo "無法建立備份目錄:$backup_dir" >&2; exit 4; } # 依 rc 檔語法組出段落內容。fish 用 set -gx,其餘用 export。 block_for() { _f="$1" while IFS="$TAB" read -r k v; do [ -n "${k:-}" ] || continue case "$_f" in *config.fish) printf "set -gx %s '%s'\n" "$k" "$v" ;; *) printf "export %s='%s'\n" "$k" "$v" ;; esac done < "$merged" } rc_list=$(mktemp) || { rm -f "$merged"; echo "無法建立暫存檔" >&2; exit 4; } rc_files > "$rc_list" rc=0 while IFS= read -r f; do [ -n "$f" ] || continue if [ -f "$f" ]; then cp -p "$f" "$backup_dir/$(basename "$f")" 2>/dev/null \ || { echo "無法備份 $f,沒有備份就不寫入" >&2; rc=4; continue; } else mkdir -p "$(dirname "$f")" 2>/dev/null || { echo "無法建立 $(dirname "$f")" >&2; rc=4; continue; } touch "$f" 2>/dev/null || { echo "無法建立 $f" >&2; rc=4; continue; } fi content=$(block_for "$f") block=$(printf '%s\n%s\n%s' "$MARK_OPEN" "$content" "$MARK_SHUT") if grep -qF "$MARK_OPEN" "$f" 2>/dev/null; then awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" -v b="$block" ' $0==o { print b; skip=1; next } $0==s { skip=0; next } skip { next } { print } ' "$f" > "$f.jsc-tmp" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法改寫 $f" >&2; rc=4; continue; } mv "$f.jsc-tmp" "$f" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法覆寫 $f" >&2; rc=4; continue; } else ( printf '\n%s\n' "$block" >> "$f" ) 2>/dev/null || { echo "無法寫入 $f" >&2; rc=4; continue; } fi # 寫完重讀驗證:說寫好了卻沒寫進去,是最難查的失敗 if [ "$cmd" = set ]; then grep -qF "$key" "$f" 2>/dev/null || { echo "$f 寫入後讀不到 $key" >&2; rc=4; continue; } fi printf 'wrote\t%s\n' "$f" done < "$rc_list" rm -f "$rc_list" "$merged" printf 'backup\t%s\n' "$backup_dir" [ "$rc" = 0 ] || exit 4 printf 'note\t%s\n' "新的設定要開新的 shell 或重新 source rc 檔才生效;本輪工作階段可先手動 export" exit 0