-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·1210 lines (1053 loc) · 42.1 KB
/
setup.sh
File metadata and controls
executable file
·1210 lines (1053 loc) · 42.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#
# 自适应 Shell 环境配置脚本,支持 bash 和 zsh
# 自动检测执行脚本的 shell 并安装相应配置
#
set -e
###
# 检测安装模式:本地或在线
###
# 本地模式:通过 ./install.sh 调用时,第二个参数为 "local"
# 在线模式:通过 curl 管道调用时,第二个参数为空或不传递
INSTALL_MODE_PARAM="${2:-}"
if [[ "$INSTALL_MODE_PARAM" == "local" ]]; then
INSTALL_MODE="local"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DOTFILES_DIR="$SCRIPT_DIR"
else
INSTALL_MODE="online"
DOTFILES_DIR=""
fi
###
# 辅助函数:获取配置文件
###
get_config_file() {
local file_path="$1"
local dest_file="$2"
if [[ "$INSTALL_MODE" == "local" ]]; then
# 本地模式:从本地仓库复制
if [[ -f "$DOTFILES_DIR/$file_path" ]]; then
cp "$DOTFILES_DIR/$file_path" "$dest_file"
return 0
else
echo "⚠ 本地文件未找到: $file_path"
return 1
fi
else
# 在线模式:从 GitHub 下载
curl -sSL "https://raw.githubusercontent.com/eallion/dotfiles/refs/heads/main/$file_path" -o "$dest_file"
return $?
fi
}
###
# 辅助函数:命令行多选工具
###
select_tools() {
local title="$1"
shift
local options=("$@")
local selected=()
local i
local all_idx=-1
local skip_idx=-1
for ((i=0; i<${#options[@]}; i++)); do
selected[i]=0
if [[ "${options[i]}" == "ALL" ]]; then all_idx=$i; fi
if [[ "${options[i]}" == "SKIP" ]]; then skip_idx=$i; fi
done
# 默认选中 ALL (如果存在)
if (( all_idx >= 0 )); then
selected[all_idx]=1
fi
local current=0
local key=""
echo "------------------------------" >&2
echo "▶ $title (↑/↓移动,空格选择/取消,回车确认):" >&2
# 隐藏光标
tput civis >&2 || true
# 打印空行撑开菜单区域,防止回退覆盖其他内容
for ((i=0; i<${#options[@]}; i++)); do
echo "" >&2
done
# 光标回到菜单顶部
echo -en "\033[${#options[@]}A" >&2
while true; do
for ((i=0; i<${#options[@]}; i++)); do
local prefix=" "
if [[ $i == $current ]]; then
prefix=" > "
fi
local check="[ ]"
if [[ ${selected[i]} == 1 ]]; then
check="[*]"
fi
if [[ $i == $current ]]; then
# \r 归位首列,\033[K 清除到行尾
echo -e "\r\033[1;32m$prefix$check ${options[i]}\033[0m\033[K" >&2
else
echo -e "\r$prefix$check ${options[i]}\033[K" >&2
fi
done
# 光标移回菜单顶部进行下一次重绘
echo -en "\033[${#options[@]}A\r" >&2
IFS= read -rsn1 key < /dev/tty
if [[ $key == $'\x1b' ]]; then
# 读取转义序列
read -rsn2 key < /dev/tty
if [[ $key == "[A" || $key == "OA" || $key == "A" ]]; then # 上
((current--))
if ((current < 0)); then current=$((${#options[@]} - 1)); fi
elif [[ $key == "[B" || $key == "OB" || $key == "B" ]]; then # 下
((current++))
if ((current >= ${#options[@]})); then current=0; fi
fi
elif [[ $key == " " ]]; then # 空格
if [[ ${selected[current]} == 1 ]]; then
selected[current]=0
else
selected[current]=1
# 互斥处理
if [[ $current == $all_idx ]]; then
for ((i=0; i<${#options[@]}; i++)); do
if [[ $i != $all_idx ]]; then selected[i]=0; fi
done
elif [[ $current == $skip_idx ]]; then
for ((i=0; i<${#options[@]}; i++)); do
if [[ $i != $skip_idx ]]; then selected[i]=0; fi
done
else
if (( all_idx >= 0 )); then selected[all_idx]=0; fi
if (( skip_idx >= 0 )); then selected[skip_idx]=0; fi
fi
fi
elif [[ "$key" == "" ]]; then # Enter
break
fi
done
# 循环结束,光标移动到选项下方恢复正常输出
echo -en "\033[${#options[@]}B\r" >&2
tput cnorm >&2 || true
local results=()
for ((i=0; i<${#options[@]}; i++)); do
if [[ ${selected[i]} == 1 ]]; then
results+=("${options[i]}")
fi
done
if [[ ${#results[@]} -eq 0 ]]; then
if (( skip_idx >= 0 )); then
echo "SKIP"
else
echo "ALL"
fi
else
echo "${results[*]}"
fi
}
###
# 第一步:用户检查与处理(Root 用户处理)
###
if [[ "$EUID" -eq 0 ]]; then
echo "▶ 检测到当前为 root 用户。"
echo "为了安全性,建议创建/使用一个普通用户来运行此脚本。"
printf "是否创建/切换到普通用户? (y/N) "
if read -t 10 CREATE_USER < /dev/tty; then :; else CREATE_USER="N"; echo ""; fi
if [[ "$CREATE_USER" =~ ^[Yy]$ ]]; then
printf "请输入用户名: "
read -t 30 USERNAME < /dev/tty
if [[ -z "$USERNAME" ]]; then
echo "❌ 用户名不能为空"
exit 1
fi
# 检查用户是否存在
if id "$USERNAME" &>/dev/null 2>&1; then
echo "✔ 用户 $USERNAME 已存在,将切换到该用户"
else
echo "▶ 用户 $USERNAME 不存在,正在创建..."
useradd -m -s /bin/bash "$USERNAME"
echo "✔ 用户 $USERNAME 创建成功"
fi
# 在切换前以 root 身份直接写入 sudoers,新用户无需输入任何密码
echo "▶ 正在为 $USERNAME 配置 sudo 免密权限..."
mkdir -p /etc/sudoers.d
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-"$USERNAME"
chmod 440 /etc/sudoers.d/90-"$USERNAME"
echo "✔ sudo 免密权限配置完成"
# 切换到该用户并重新运行脚本
# 兼容 curl 管道模式:$0 可能是 "bash",此时用 /proc/self/fd/0 读取
echo "▶ 切换到用户 $USERNAME 并重新运行脚本..."
SCRIPT_PATH="${BASH_SOURCE[0]}"
if [[ -f "$SCRIPT_PATH" ]]; then
su - "$USERNAME" -c "bash '$SCRIPT_PATH' $*"
else
echo "⚠ 在线安装模式:请切换到 $USERNAME 后重新运行安装命令。"
echo " su - $USERNAME"
fi
exit 0
fi
# 如果不创建新用户,作为 root 继续
SUDO=""
fi
# 优先检查显式传递的参数
if [[ -n "$1" ]]; then
if [[ "$1" == "zsh" ]]; then
INSTALL_SHELL="zsh"
elif [[ "$1" == "bash" ]]; then
INSTALL_SHELL="bash"
else
echo "⚠ 未知参数: $1,将尝试自动检测..."
fi
fi
# 如果未通过参数指定,则尝试检测
if [[ -z "$INSTALL_SHELL" ]]; then
# 尝试使用 ps 检测 (需处理 ps 命令不存在的情况)
if command -v ps &> /dev/null; then
if CURRENT_SHELL=$(ps -p $$ -o comm= 2>/dev/null); then
DETECTED_SHELL=$(basename "$CURRENT_SHELL")
else
DETECTED_SHELL="unknown"
fi
else
DETECTED_SHELL="unknown"
fi
echo "▶ 自动检测到的 Shell: $DETECTED_SHELL"
if [[ "$DETECTED_SHELL" == "zsh" ]]; then
INSTALL_SHELL="zsh"
elif [[ "$DETECTED_SHELL" == "bash" || "$DETECTED_SHELL" == "sh" ]]; then
# 即使在 bash/sh 下运行,也检查 $0 是否暗示了 zsh (虽然前面 $1 已经检查过了,这里保留作为 fallback)
if [[ "$0" == "zsh" ]]; then
INSTALL_SHELL="zsh"
else
INSTALL_SHELL="bash"
fi
else
# 无法检测且未指定参数,默认为 bash
echo "⚠ 无法检测到 Shell 类型 (可能缺少 ps 命令),且未指定参数。"
echo "⚠ 默认使用 bash 安装。"
INSTALL_SHELL="bash"
fi
fi
echo "=== 正在开始配置 $INSTALL_SHELL 环境 ==="
echo "脚本将自动备份你的配置文件"
echo "------------------------------"
cd ~
echo "------------------------------"
###
# 第二步:普通用户处理(如果不是 root)
###
if [[ "$EUID" -ne 0 ]]; then
echo "▶ 检测到当前为普通用户。安装命令将使用 sudo 运行。"
SUDO="sudo"
# 检查 sudo 是否安装
if ! command -v sudo &> /dev/null; then
echo "❌ 未检测到 sudo 命令。"
echo "你需要安装 sudo 才能继续。"
printf "是否尝试使用 su 切换到 root 自动安装 sudo?(y/N) "
if read -t 10 INSTALL_SUDO < /dev/tty; then :; else INSTALL_SUDO="N"; echo ""; fi
if [[ "$INSTALL_SUDO" =~ ^[Yy]$ ]]; then
echo "请输入 root 密码以继续:"
su -c "apt-get update && apt-get install -y sudo"
if ! command -v sudo &> /dev/null; then
echo "❌ sudo 安装失败。请手动解决或以 root 运行。"
exit 1
fi
else
echo "请手动安装 sudo (apt install sudo) 或以 root 运行此脚本。"
exit 1
fi
fi
# 检查当前用户是否在 sudoers 中
CURRENT_USER=$(whoami)
if ! $SUDO -n true 2>/dev/null; then
echo "❌ 当前用户 $CURRENT_USER 没有 sudo 权限。"
echo ""
printf "是否通过 su 自动配置 sudoers(需要输入 root 密码)? (y/N) "
if read -t 15 AUTO_SUDO < /dev/tty; then :; else AUTO_SUDO="N"; echo ""; fi
if [[ "$AUTO_SUDO" =~ ^[Yy]$ ]]; then
echo "▶ 请输入 root 密码(以下操作以 root 身份执行):"
SUDOERS_CMD="mkdir -p /etc/sudoers.d && echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$CURRENT_USER && chmod 440 /etc/sudoers.d/90-$CURRENT_USER"
if su root -c "$SUDOERS_CMD" < /dev/tty; then
echo "✔ sudoers 配置成功!"
# sudoers 文件写入后无需重登录,立即验证
if ! $SUDO -n true 2>/dev/null; then
echo "⚠ 配置已写入,但当前会话尚未生效,请重新登录后再运行本脚本。"
exit 0
fi
echo "✔ sudo 权限验证通过,继续执行..."
else
echo "❌ 自动配置失败(root 密码错误或 su 不可用)。"
echo ""
echo "请手动以 root 身份执行以下命令后,重新运行本脚本:"
echo " echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$CURRENT_USER"
exit 1
fi
else
echo ""
echo "请手动以 root 身份执行以下任一命令后,重新运行本脚本:"
echo " 方法一: usermod -aG sudo $CURRENT_USER"
echo " 方法二: echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$CURRENT_USER"
echo ""
echo "或者直接以 root 运行本脚本,脚本会引导你创建/切换用户。"
exit 1
fi
fi
echo "▶ 正在配置 sudo 免密码..."
# 尝试配置免密
if echo "$CURRENT_USER ALL=(ALL) NOPASSWD:ALL" | $SUDO tee /etc/sudoers.d/90-$CURRENT_USER > /dev/null 2>&1; then
echo "✔ sudo 免密码配置成功!"
else
echo "⚠ sudo 免密码配置失败,脚本将继续运行,后续可能需要手动输入密码。"
fi
else
SUDO=""
echo "▶ 当前为 root 用户,所有安装命令将直接运行。"
fi
echo "------------------------------"
# --- 备份文件 ---
printf "是否备份现有的配置文件 (.zshrc, .bashrc 等)? (y/N) "
if read -t 10 BACKUP_FILES < /dev/tty; then :; else BACKUP_FILES="N"; echo ""; fi
if [[ "$BACKUP_FILES" =~ ^[Yy]$ ]]; then
# 备份现有的配置文件
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
backup_files=(
".bashrc"
".bash_aliases"
".zshrc"
".gitconfig"
".p10k.zsh"
)
for file in "${backup_files[@]}"; do
if [ -f "$HOME/$file" ]; then
mv "$HOME/$file" "$HOME/${file}.bak_$TIMESTAMP"
echo "✔ 备份 ~/$file 为 ~/${file}.bak_$TIMESTAMP"
fi
done
else
echo "⏭ 跳过备份。"
fi
echo "------------------------------"
###
# 第三步:优先安装 ZSH(最重要的基础)
###
echo "▶ 检查 ZSH 安装状态..."
if [[ "$INSTALL_SHELL" == "zsh" ]]; then
if ! command -v zsh &> /dev/null; then
echo "❌ 未检测到 zsh,这是目标 shell 所需的。"
echo "请先手动安装 zsh 后再运行此脚本。"
exit 1
else
echo "✔ zsh 已安装。"
fi
else
echo "▶ 目标 shell 为 bash,跳过 zsh 安装检查。"
fi
echo "------------------------------"
# --- 更新包管理器与安装基础工具 ---
echo "▶ 选择要安装的基础工具:"
basic_tools=("ALL" "curl" "wget" "git" "ca-certificates" "jq" "neovim" "SKIP")
selected_basic=$(select_tools "基础工具安装" "${basic_tools[@]}")
if [[ "$selected_basic" == *"SKIP"* ]] || [[ -z "$selected_basic" ]]; then
echo "⏭ 跳过基础工具安装。"
else
# 检查是否选择了 ALL
if [[ "$selected_basic" == *"ALL"* ]]; then
tools_to_install=("curl" "wget" "git" "ca-certificates" "jq" "neovim")
else
tools_to_install=()
for tool in $selected_basic; do
if [[ "$tool" != "ALL" && "$tool" != "SKIP" ]]; then
tools_to_install+=("$tool")
fi
done
fi
if [[ ${#tools_to_install[@]} -gt 0 ]]; then
# --- 更新包管理器 ---
echo "▶ 正在更新包管理器..."
$SUDO apt-get update
echo "✔ 包管理器更新完成"
# --- 安装选中的工具 ---
echo "▶ 正在安装选中的基础工具: ${tools_to_install[*]}"
$SUDO apt-get install -y "${tools_to_install[@]}"
echo "✔ 基础工具安装完成"
else
echo "⏭ 没有选择任何工具,跳过安装。"
fi
fi
echo "------------------------------"
# --- [新增] 如果目标 shell 是 zsh,优先安装 zsh ---
if [[ "$INSTALL_SHELL" == "zsh" ]]; then
echo "▶ 目标 shell 为 zsh,已检查 zsh 是否安装。"
if ! command -v zsh &> /dev/null; then
echo "❌ 未检测到 zsh,脚本无法继续。"
echo "请先手动安装 zsh 后再运行此脚本。"
exit 1
fi
echo "------------------------------"
fi
# --- 安装 Starship ---
printf "是否安装 Starship 及其配置? (y/N) "
if read -t 10 INSTALL_STARSHIP < /dev/tty; then :; else INSTALL_STARSHIP="N"; echo ""; fi
if [[ "$INSTALL_STARSHIP" =~ ^[Yy]$ ]]; then
echo "▶ 正在安装 Starship..."
if ! command -v starship &> /dev/null; then
bash -lc "curl -sS https://starship.rs/install.sh | sh -s -- --yes"
echo "✔ Starship 安装成功!"
else
echo "✔ Starship 已经安装,跳过安装。"
fi
echo "------------------------------"
# --- 配置 Starship ---
echo "▶ 正在配置 Starship ..."
mkdir -p ~/.config
get_config_file ".config/starship.toml" "$HOME/.config/starship.toml"
echo "✔ Starship 配置成功!"
else
echo "⏭ 跳过 Starship 安装与配置。"
fi
echo "------------------------------"
# --- 安装字体 ---
printf "是否安装 JetBrains Maple Mono 字体? (y/N) "
if read -t 10 INSTALL_FONTS < /dev/tty; then :; else INSTALL_FONTS="N"; echo ""; fi
if [[ "$INSTALL_FONTS" =~ ^[Yy]$ ]]; then
echo "▶ 正在安装 JetBrains Maple Mono 字体..."
wget https://github.com/SpaceTimee/Fusion-JetBrainsMapleMono/releases/latest/download/JetBrainsMapleMono-NF-XX-XX-XX.zip
unzip -o JetBrainsMapleMono-NF-XX-XX-XX.zip
mkdir -p ~/.fonts
mv *.ttf ~/.fonts/
rm JetBrainsMapleMono-NF-XX-XX-XX.zip LICENSE.txt
if command -v fc-cache &> /dev/null; then
fc-cache -f -v
fi
echo "✔ JetBrains Maple Mono 字体安装成功!"
else
echo "⏭ 跳过字体安装。"
fi
echo "------------------------------"
# 根据 shell 类型执行相应安装
if [[ "$INSTALL_SHELL" == "zsh" ]]; then
echo "=== 开始安装 ZSH 配置 ==="
# --- [可选] 安装 Zsh 现代 CLI 工具 ---
echo "▶ 选择要安装的现代 CLI 工具:"
cli_tools=("ALL" "unzip" "eza" "bat" "fzf" "zoxide" "ripgrep" "fd-find" "gh" "trash-cli" "btop" "SKIP")
selected_cli=$(select_tools "现代 CLI 工具安装 (ZSH)" "${cli_tools[@]}")
if [[ "$selected_cli" == *"SKIP"* ]] || [[ -z "$selected_cli" ]]; then
echo "⏭ 跳过现代 CLI 工具安装。"
else
# 检查是否选择了 ALL
if [[ "$selected_cli" == *"ALL"* ]]; then
tools_to_install=("unzip" "eza" "bat" "fzf" "zoxide" "ripgrep" "fd-find" "gh" "trash-cli" "btop")
else
tools_to_install=()
for tool in $selected_cli; do
if [[ "$tool" != "ALL" && "$tool" != "SKIP" ]]; then
tools_to_install+=("$tool")
fi
done
fi
if [[ ${#tools_to_install[@]} -gt 0 ]]; then
echo "▶ 正在安装选中的 CLI 工具: ${tools_to_install[*]}"
$SUDO apt-get install -y "${tools_to_install[@]}" || echo "⚠ 部分工具安装失败,可能仓库中不存在 (如 eza),脚本将继续。"
# Debian/Ubuntu 上 bat 包名安装为 bat,而可执行文件为 batcat,创建本地软链接以保持一致性
if [[ " ${tools_to_install[*]} " =~ " bat " ]] && command -v batcat &> /dev/null; then
mkdir -p "$HOME/.local/bin"
ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat"
echo "✔ 已创建 ~/.local/bin/bat -> $(command -v batcat)"
fi
# 创建 fd 符号链接( Debian/Ubuntu 中是 fd-find )
if [[ " ${tools_to_install[*]} " =~ " fd-find " ]] && command -v fdfind &> /dev/null && ! command -v fd &> /dev/null; then
$SUDO ln -s $(which fdfind) /usr/local/bin/fd
fi
else
echo "⏭ 没有选择任何工具,跳过安装。"
fi
fi
echo "------------------------------"
# --- 安装 ZSH 配置文件 ---
printf "是否安装 ZSH 配置文件 (.zshrc, .aliases, .login_info.sh)? (y/N) "
if read -t 10 INSTALL_ZSH_CONFIG < /dev/tty; then :; else INSTALL_ZSH_CONFIG="N"; echo ""; fi
if [[ "$INSTALL_ZSH_CONFIG" =~ ^[Yy]$ ]]; then
# 创建自定义别名文件
echo "▶ 正在配置 ZSH 自定义别名..."
# 下载 .aliases
get_config_file ".aliases" "$HOME/.aliases"
echo "✔ ZSH 自定义别名创建完成(.aliases)!"
# [新增] 如果安装了 neovim,添加别名
if command -v nvim &> /dev/null; then
echo "alias vim='nvim'" >> "$HOME/.aliases"
echo "✔ 已添加 alias vim=nvim 到 .aliases"
fi
echo "------------------------------"
# 下载并替换 .zshrc
echo "▶ 正在下载 .zshrc..."
get_config_file ".zshrc" "$HOME/.zshrc"
# 下载 login_info.sh
echo "▶ 正在下载 login_info.sh..."
get_config_file ".login_info.sh" "$HOME/.login_info.sh"
echo "✔ ZSH 配置已安装。"
else
echo "⏭ 跳过 ZSH 配置文件安装。"
fi
echo "------------------------------"
else
echo "=== 开始安装 BASH 配置 ==="
# --- [可选] 安装 Oh My Bash ---
printf "是否安装 Oh My Bash 框架? (y/N) "
if read -t 10 INSTALL_OMB < /dev/tty; then :; else INSTALL_OMB="N"; echo ""; fi
if [[ "$INSTALL_OMB" =~ ^[Yy]$ ]]; then
echo "▶ 正在安装 Oh My Bash..."
if [ ! -d "$HOME/.oh-my-bash" ]; then
bash -c "$(curl -fsSL https://raw.githubusercontent.com/ohmybash/oh-my-bash/master/tools/install.sh)" "" --unattended
echo "✔ Oh My Bash 安装成功!"
else
echo "✔ Oh My Bash 已经安装,跳过安装。"
fi
# 创建 Oh My Bash starship 插件
echo "▶ 正在配置 Oh My Bash starship 插件..."
mkdir -p ~/.oh-my-bash/custom/plugins/starship
cat > ~/.oh-my-bash/custom/plugins/starship/starship.plugin.sh << 'EOF'
#!/bin/bash
# Starship prompt for Oh My Bash
if command -v starship &> /dev/null; then
eval "$(starship init bash)"
fi
EOF
echo "✔ Oh My Bash starship 插件配置完成!"
else
echo "⏭ 跳过 Oh My Bash 安装。"
fi
echo "------------------------------"
# --- [可选] 安装 Bash 现代 CLI 工具 ---
echo "▶ 选择要安装的现代 CLI 工具:"
cli_tools=("ALL" "trash-cli" "ripgrep" "fd-find" "eza" "bat" "fzf" "zoxide" "gh" "btop" "SKIP")
selected_cli=$(select_tools "现代 CLI 工具安装 (Bash)" "${cli_tools[@]}")
if [[ "$selected_cli" == *"SKIP"* ]] || [[ -z "$selected_cli" ]]; then
echo "⏭ 跳过现代 CLI 工具安装。"
else
# 检查是否选择了 ALL
if [[ "$selected_cli" == *"ALL"* ]]; then
tools_to_install=("trash-cli" "ripgrep" "fd-find" "eza" "bat" "fzf" "zoxide" "gh" "btop")
else
tools_to_install=()
for tool in $selected_cli; do
if [[ "$tool" != "ALL" && "$tool" != "SKIP" ]]; then
tools_to_install+=("$tool")
fi
done
fi
if [[ ${#tools_to_install[@]} -gt 0 ]]; then
echo "▶ 正在安装选中的 CLI 工具: ${tools_to_install[*]}"
$SUDO apt-get install -y "${tools_to_install[@]}" || echo "⚠ 部分工具安装失败,可能仓库中不存在 (如 eza),脚本将继续。"
# 创建 fd 符号链接
if [[ " ${tools_to_install[*]} " =~ " fd-find " ]] && command -v fdfind &> /dev/null && ! command -v fd &> /dev/null; then
$SUDO ln -s $(which fdfind) /usr/local/bin/fd
fi
if [[ " ${tools_to_install[*]} " =~ " bat " ]] && command -v batcat &> /dev/null; then
mkdir -p "$HOME/.local/bin"
ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat"
echo "✔ 已创建 ~/.local/bin/bat -> $(command -v batcat)"
fi
else
echo "⏭ 没有选择任何工具,跳过安装。"
fi
fi
echo "------------------------------"
# --- 安装 Bash 配置文件 ---
printf "是否安装 Bash 配置文件 (.bashrc, .aliases, .login_info.sh)? (y/N) "
if read -t 10 INSTALL_BASH_CONFIG < /dev/tty; then :; else INSTALL_BASH_CONFIG="N"; echo ""; fi
if [[ "$INSTALL_BASH_CONFIG" =~ ^[Yy]$ ]]; then
# 下载并替换自定义点文件
echo "▶ 正在下载 .bashrc .aliases .login_info.sh..."
get_config_file ".bashrc" "$HOME/.bashrc"
get_config_file ".aliases" "$HOME/.aliases"
get_config_file ".login_info.sh" "$HOME/.login_info.sh"
# [新增] 如果安装了 neovim,添加别名
if command -v nvim &> /dev/null; then
echo "alias vim='nvim'" >> "$HOME/.aliases"
echo "✔ 已添加 alias vim=nvim 到 .aliases"
fi
echo "✔ Bash 配置已安装。"
else
echo "⏭ 跳过 Bash 配置文件安装。"
fi
echo "------------------------------"
fi
# --- 安装 FNM ---
printf "是否安装 FNM (Fast Node Manager)? (y/N) "
if read -t 10 INSTALL_FNM < /dev/tty; then :; else INSTALL_FNM="N"; echo ""; fi
if [[ "$INSTALL_FNM" =~ ^[Yy]$ ]]; then
echo "▶ 正在安装 FNM..."
# 安装 fnm
curl -fsSL https://fnm.vercel.app/install | bash
# 加载 fnm
export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(XDG_RUNTIME_DIR= fnm env)"
# 配置 shell 初始化
# 检测当前使用的 shell
if [[ -n "$BASH_VERSION" ]]; then
# Bash (Oh My Bash)
if ! grep -q 'fnm env' "$HOME/.bashrc" 2>/dev/null; then
echo '' >> "$HOME/.bashrc"
echo '# FNM' >> "$HOME/.bashrc"
echo 'export PATH="$HOME/.local/share/fnm:$PATH"' >> "$HOME/.bashrc"
echo 'eval "$(fnm env)"' >> "$HOME/.bashrc"
fi
echo "✔ 已配置 Bash FNM"
fi
if [[ -n "$ZSH_VERSION" ]]; then
# Zsh (Zinit)
if ! grep -q 'fnm env' "$HOME/.zshrc" 2>/dev/null; then
echo '' >> "$HOME/.zshrc"
echo '# FNM' >> "$HOME/.zshrc"
echo 'export PATH="$HOME/.local/share/fnm:$PATH"' >> "$HOME/.zshrc"
echo 'eval "$(fnm env)"' >> "$HOME/.zshrc"
fi
echo "✔ 已配置 Zsh FNM"
fi
echo "✔ FNM 安装完成!"
# --- 安装 Node.js ---
echo "▶ 选择要安装的 Node.js 版本:"
echo " 1) 最新 LTS 版本(推荐)"
echo " 2) 指定版本号"
echo " 3) 跳过"
printf "请选择 (1/2/3,默认 3): "
if read -t 15 NODE_CHOICE < /dev/tty; then :; else NODE_CHOICE="3"; echo ""; fi
NODE_VERSION=""
case "$NODE_CHOICE" in
1)
NODE_VERSION="lts"
;;
2)
printf "请输入版本号(如 20、22.0.0): "
if read -t 30 NODE_VERSION_INPUT < /dev/tty; then :; else NODE_VERSION_INPUT=""; echo ""; fi
if [[ -z "$NODE_VERSION_INPUT" ]]; then
echo "⚠ 未输入版本号,跳过 Node.js 安装。"
else
NODE_VERSION="$NODE_VERSION_INPUT"
fi
;;
*)
echo "⏭ 跳过 Node.js 安装。"
;;
esac
if [[ -n "$NODE_VERSION" ]]; then
export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(XDG_RUNTIME_DIR= fnm env)"
if [[ "$NODE_VERSION" == "lts" ]]; then
echo "▶ 正在安装 Node.js 最新 LTS..."
fnm install --lts
fnm default lts-latest
else
echo "▶ 正在安装 Node.js $NODE_VERSION..."
fnm install "$NODE_VERSION"
fnm default "$NODE_VERSION"
fi
# 重新加载 fnm 环境,使 node/npm 进入 PATH
eval "$(XDG_RUNTIME_DIR= fnm env)"
if command -v node &>/dev/null; then
echo "✔ Node.js $(node -v) 安装完成!"
else
echo "⚠ Node.js 安装可能失败,npm 不可用,跳过 pnpm 安装。"
NODE_VERSION=""
fi
fi
if [[ -n "$NODE_VERSION" ]] && command -v npm &>/dev/null; then
# --- 安装 pnpm ---
printf "是否安装 pnpm? (y/N) "
if read -t 10 INSTALL_PNPM < /dev/tty; then :; else INSTALL_PNPM="N"; echo ""; fi
if [[ "$INSTALL_PNPM" =~ ^[Yy]$ ]]; then
echo "▶ 正在安装 pnpm..."
npm i -g pnpm
echo "✔ pnpm 安装完成!"
else
echo "⏭ 跳过 pnpm 安装。"
fi
fi
else
echo "⏭ 跳过 FNM 安装。"
fi
echo "------------------------------"
# --- 下载其他配置文件 ---
echo "▶ 正在配置其他配置文件..."
# Git 配置
printf "是否安装 Git 配置 (.gitconfig)? (y/N) "
if read -t 10 INSTALL_GIT_CONFIG < /dev/tty; then :; else INSTALL_GIT_CONFIG="N"; echo ""; fi
if [[ "$INSTALL_GIT_CONFIG" =~ ^[Yy]$ ]]; then
echo "▶ 正在配置 Git 配置..."
get_config_file ".gitconfig" "$HOME/.gitconfig"
echo "✔ Git 配置已安装。"
else
echo "⏭ 跳过 Git 配置。"
fi
# Alacritty 配置
printf "是否安装 Alacritty 配置 (github_dark_dimmed.toml, alacritty.toml)? (y/N) "
if read -t 10 INSTALL_ALACRITTY < /dev/tty; then :; else INSTALL_ALACRITTY="N"; echo ""; fi
if [[ "$INSTALL_ALACRITTY" =~ ^[Yy]$ ]]; then
echo "▶ 正在下载 Alacritty 配置..."
mkdir -p "$HOME/.config/alacritty/themes/themes"
get_config_file ".config/alacritty/themes/themes/github_dark_dimmed.toml" "$HOME/.config/alacritty/themes/themes/github_dark_dimmed.toml"
get_config_file ".config/alacritty/alacritty.toml" "$HOME/.config/alacritty/alacritty.toml"
echo "✔ Alacritty 配置已安装。"
else
echo "⏭ 跳过 Alacritty 配置。"
fi
# Kitty 配置
printf "是否安装 Kitty 配置 (current-theme.conf, kitty.conf)? (y/N) "
if read -t 10 INSTALL_KITTY < /dev/tty; then :; else INSTALL_KITTY="N"; echo ""; fi
if [[ "$INSTALL_KITTY" =~ ^[Yy]$ ]]; then
echo "▶ 正在下载 Kitty 配置..."
mkdir -p "$HOME/.config/kitty"
get_config_file ".config/kitty/current-theme.conf" "$HOME/.config/kitty/current-theme.conf"
get_config_file ".config/kitty/kitty.conf" "$HOME/.config/kitty/kitty.conf"
echo "✔ Kitty 配置已安装。"
else
echo "⏭ 跳过 Kitty 配置。"
fi
# Ghostty 配置
printf "是否安装 Ghostty 配置? (y/N) "
if read -t 10 INSTALL_GHOSTTY < /dev/tty; then :; else INSTALL_GHOSTTY="N"; echo ""; fi
if [[ "$INSTALL_GHOSTTY" =~ ^[Yy]$ ]]; then
echo "▶ 正在下载 Ghostty 配置..."
mkdir -p "$HOME/.config/ghostty/auto"
# 下载主配置文件
get_config_file ".config/ghostty/config.ghostty" "$HOME/.config/ghostty/config.ghostty"
# 下载主题配置文件
get_config_file ".config/ghostty/auto/theme.ghostty" "$HOME/.config/ghostty/auto/theme.ghostty"
echo "✔ Ghostty 配置已安装。"
else
echo "⏭ 跳过 Ghostty 配置。"
fi
# Docker 守护进程配置
printf "是否配置 Docker 守护进程 (daemon.json)? (y/N) "
if read -t 10 INSTALL_DOCKER_CONFIG < /dev/tty; then :; else INSTALL_DOCKER_CONFIG="N"; echo ""; fi
if [[ "$INSTALL_DOCKER_CONFIG" =~ ^[Yy]$ ]]; then
echo "▶ 正在配置 Docker 守护进程..."
$SUDO mkdir -p /etc/docker
TEMP_JSON=$(mktemp)
# 复用统一函数:本地模式 cp,在线模式 curl
if ! get_config_file "etc/docker/daemon.json" "$TEMP_JSON"; then
echo "⚠ 获取 Docker 配置文件失败,跳过。"
TEMP_JSON=""
fi
if [[ -n "$TEMP_JSON" ]] && [[ -s "$TEMP_JSON" ]]; then
if [ -f "/etc/docker/daemon.json" ]; then
echo "ℹ 发现现有 daemon.json,正在合并配置..."
# 使用 jq 合并配置 (现有配置 * 远程配置 = 远程覆盖现有)
# 注意: 这里的 merge 策略是远程覆盖本地冲突项,但保留本地独有项
if command -v jq &> /dev/null; then
$SUDO cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%Y%m%d_%H%M%S)
$SUDO jq -s '.[0] * .[1]' /etc/docker/daemon.json "$TEMP_JSON" | $SUDO tee "$TEMP_JSON.merged" > /dev/null
$SUDO mv "$TEMP_JSON.merged" /etc/docker/daemon.json
else
echo "⚠ 未检测到 jq,无法合并 Docker 配置,备份原配置后直接替换。"
$SUDO cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%Y%m%d_%H%M%S)
$SUDO mv "$TEMP_JSON" /etc/docker/daemon.json
$SUDO chmod 644 /etc/docker/daemon.json
TEMP_JSON=""
fi
else
echo "ℹ 未发现 daemon.json,正在创建..."
$SUDO mv "$TEMP_JSON" /etc/docker/daemon.json
$SUDO chmod 644 /etc/docker/daemon.json
fi
echo "✔ Docker 守护进程配置已更新。"
else
echo "⚠ 无法下载 Docker 配置,跳过。"
fi
rm -f "$TEMP_JSON"
else
echo "⏭ 跳过 Docker 配置。"
fi
echo "------------------------------"
###
# SSH 配置(仅适用于 VPS 环境)
###
printf "是否配置 SSH 服务 (仅在 VPS 环境推荐配置)? (y/N) "
if read -t 10 CONFIGURE_SSH < /dev/tty; then :; else CONFIGURE_SSH="N"; echo ""; fi
if [[ "$CONFIGURE_SSH" =~ ^[Yy]$ ]]; then
echo "▶ 开始配置 SSH 服务..."
# 检查 SSH 服务状态
if ! systemctl is-active --quiet ssh; then
echo "▶ 启用 SSH 服务..."
$SUDO systemctl enable ssh
$SUDO systemctl start ssh
fi
# 检测系统版本以决定配置方式
if command -v lsb_release &> /dev/null; then
DISTRO=$(lsb_release -si)
VERSION=$(lsb_release -sr)
else
# 回退方法
if [ -f /etc/os-release ]; then
. /etc/os-release
DISTRO=$ID
VERSION=$VERSION_ID
else
DISTRO="unknown"
VERSION="unknown"
fi
fi
echo "▶ 检测到系统: $DISTRO $VERSION"
# 选择 SSH 端口
echo "请选择 SSH 端口配置方式:"
echo " 1) 输入自定义端口"
echo " 2) 使用 50000 以上的随机端口"
printf "请选择 (1/2): "
if read -t 30 PORT_CHOICE < /dev/tty; then :; else PORT_CHOICE="2"; echo ""; fi
case $PORT_CHOICE in
1)
printf "请输入 SSH 端口 (22-65535): "
read -t 30 SSH_PORT < /dev/tty
if ! [[ "$SSH_PORT" =~ ^[0-9]+$ ]] || [ "$SSH_PORT" -lt 22 ] || [ "$SSH_PORT" -gt 65535 ]; then
echo "❌ 无效端口,使用默认端口 22"
SSH_PORT=22
fi
;;
2)
SSH_PORT=$((50000 + RANDOM % 15536)) # 50000-65535
echo "▶ 随机生成端口: $SSH_PORT"
;;
*)
SSH_PORT=$((50000 + RANDOM % 15536))
echo "▶ 默认使用随机端口: $SSH_PORT"
;;
esac
# 是否禁用 root 直接登录
printf "是否禁用 root 用户直接 SSH 登录? (y/N) "
if read -t 10 DISABLE_ROOT_SSH < /dev/tty; then :; else DISABLE_ROOT_SSH="N"; echo ""; fi
# 是否禁止密码登录
printf "是否禁止密码登录 (推荐使用密钥登录)? (y/N) "
if read -t 10 DISABLE_PASSWORD_AUTH < /dev/tty; then :; else DISABLE_PASSWORD_AUTH="N"; echo ""; fi
SSH_KEY=""
if [[ "$DISABLE_PASSWORD_AUTH" =~ ^[Yy]$ ]]; then
echo "请粘贴你的 SSH 公钥 (以 ssh-rsa 或 ssh-ed25519 开头):"
echo "提示: 你可以在本地运行 'cat ~/.ssh/id_rsa.pub' 或 'cat ~/.ssh/id_ed25519.pub' 获取公钥"
read -t 60 SSH_KEY < /dev/tty
if [[ -z "$SSH_KEY" ]] || ! [[ "$SSH_KEY" =~ ^ssh-(rsa|ed25519|ecdsa) ]]; then
echo "❌ 无效的 SSH 公钥,跳过密钥配置"
SSH_KEY=""
DISABLE_PASSWORD_AUTH="N"
fi
fi
# 根据系统版本选择配置方式
if [[ "$DISTRO" == "Debian" && "$VERSION" =~ ^(12|13)$ ]] || [[ "$DISTRO" == "Ubuntu" && "$VERSION" =~ ^(22|24) ]]; then
# 现代系统使用 sshd_config.d/
echo "▶ 使用现代配置方式 (sshd_config.d/)..."
$SUDO mkdir -p /etc/ssh/sshd_config.d
# 创建自定义配置
CONFIG_FILE="/etc/ssh/sshd_config.d/99-custom.conf"
cat << EOF | $SUDO tee "$CONFIG_FILE" > /dev/null
# Custom SSH configuration added by dotfiles setup
Port $SSH_PORT
EOF
if [[ "$DISABLE_ROOT_SSH" =~ ^[Yy]$ ]]; then
echo "PermitRootLogin no" | $SUDO tee -a "$CONFIG_FILE" > /dev/null
fi
if [[ "$DISABLE_PASSWORD_AUTH" =~ ^[Yy]$ ]]; then
cat << EOF | $SUDO tee -a "$CONFIG_FILE" > /dev/null
PasswordAuthentication no
PubkeyAuthentication yes
EOF
fi
# 设置正确的权限
$SUDO chmod 644 "$CONFIG_FILE"
else
# 传统系统直接修改 sshd_config
echo "▶ 使用传统配置方式 (直接修改 sshd_config)..."
SSHD_CONFIG="/etc/ssh/sshd_config"
# 备份原配置
$SUDO cp "$SSHD_CONFIG" "${SSHD_CONFIG}.bak.$(date +%Y%m%d_%H%M%S)"
# 修改端口
$SUDO sed -i "s/^#*Port .*/Port $SSH_PORT/" "$SSHD_CONFIG"
if ! grep -q "^Port $SSH_PORT" "$SSHD_CONFIG"; then
echo "Port $SSH_PORT" | $SUDO tee -a "$SSHD_CONFIG" > /dev/null
fi
# 禁用 root 登录
if [[ "$DISABLE_ROOT_SSH" =~ ^[Yy]$ ]]; then
$SUDO sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' "$SSHD_CONFIG"
fi
# 禁用密码认证
if [[ "$DISABLE_PASSWORD_AUTH" =~ ^[Yy]$ ]]; then
$SUDO sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' "$SSHD_CONFIG"
$SUDO sed -i 's/^#*PubkeyAuthentication .*/PubkeyAuthentication yes/' "$SSHD_CONFIG"
fi
fi