What: 新增 jsc-cli:setup,讀 doctor 的待修清單逐項確認後修復,並新增 tools/apply-config.sh 負責實際寫入。 Why: 體檢找得出問題,修還是得靠人一個一個查文件。修法又分三種:算得出來的、要人給值的、只能手動的,混在一起講不清楚。 How: 依規格表的 fix 欄分流,auto 直接寫、ask 先用決策樹問到值、manual 印步驟。複合修復交回原主(deploy、hooks-install、models)。apply-config.sh 只動 rc 檔的 # jsc-config 標記段落,寫前備份到 $JSC_HOME/backup/config/,寫後重讀驗證,fish 自動改用 set -gx 語法。 Who: 體檢與修復流程,接在 jsc-cli:doctor 之後。
159 lines
6.0 KiB
Bash
Executable File
159 lines
6.0 KiB
Bash
Executable File
#!/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 段落的內容(每行 KEY<TAB>VALUE)
|
||
# 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
|
||
|
||
# 目前段落裡的設定,格式 KEY<TAB>VALUE。取第一個既有 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
|