环境变量缺失引导规范

当脚本缺少必要的环境变量,或环境变量指向的文件不存在/格式无效时,按以下设计总则实现引导用户补充的完整流程。

参考实现:script-qbase/env_variables/env_check.sh

1. 问题域定义

1.1 什么算”缺失”

一个环境变量可能处于以下任一”非可用”状态:

状态 含义 检测条件
未设置 env var 不存在或为空 值为空
占位值 env var 的值仍是占位符,未被替换 值 == 占位符
文件不存在 env var 指向的路径不是有效的文件 值非空但文件不存在
格式无效 文件内容不符合预期的格式(如 JSON) 文件存在但解析失败

1.2 解决目标

让调用方总能拿到一个可用的值,且后续不再重复引导。


2. 架构原则

2.1 三模块职责划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph LR
subgraph 调用层
CALLER[调用者]
end

subgraph 本方案
CHECK[检测模块<br>判断状态<br>输出结构化响应<br>无副作用]
GUIDE[交互引导模块<br>展示信息<br>获取用户选择<br>返回目标值]
PERSIST[持久化模块<br>写入配置<br>打开 profile<br>生效 env var]
end

CALLER -->|委托检测| CHECK
CHECK -->|JSON 状态| CALLER
CALLER -->|委托引导| GUIDE
GUIDE -->|用户选择的值| CALLER
CALLER -->|委托写入| PERSIST

2.2 关键约束

  • 检测模块不应有交互副作用:不要一边检查一边弹菜单
  • 交互模块不应直接写入系统配置:写配置是持久化模块的职责
  • 每个模块应可独立测试:给定输入,断言输出和副作用

2.3 通信通道约定

通道 用途 消费者 约束
stdout 机器可读的结构化数据 调用方(捕获) 必须是结构化格式
stderr 人类可读的交互信息 终端用户 颜色、格式随意
exit code 执行结果状态 调用方(判断) 语义化枚举
  • 检测模块的响应为 {"status_type": "<枚举值>", "message": "<描述>"}
  • 所有交互信息必须输出到 stderr,禁止 stdout 输出来自人类的交互文本

2.4 两阶段占位符策略

问题:env var 为空时若直接打开 profile 让用户编辑,编辑期间其他程序可能读到空值。

方案:先写入占位符 → 再引导用户替换为实际值。

1
2
阶段一:env_var = your_ENV_NAME_value(占位符)
阶段二:引导用户将占位符替换为实际值

好处:中间状态可识别、避免空值竞态、调用方可阶段一后继续、无需等用户编辑。


3. 概要流程

架构原则描述了”谁做什么”,核心流程是完整的状态机。概要流程是中间的线性主干——忽略细节分支,只展示一次完整调用从开始到结束的必经步骤:

3.1 无环境变量维护列表的场景

choices JSON 文件尚不存在,首次设置环境变量的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
graph LR
S1[1. 检测] --> S2{2. 判断状态}
S2 -->|成功| S3[3. 直接取值]
S2 -->|非成功| S4[4. 展示参考示例]
S4 --> S5[5. 显示操作菜单: ①复制示例 ②手动输入]
S5 --> S6[6. 执行菜单选择并验证]
S6 --> S7{7. 是否自动设置?}
S7 -->|是| S8a[8a. 自动写入并生效]
S7 -->|否| S8b[8b. 引导手动设置]
S8a --> S9[9. 输出值给调用方]
S8b --> S9
S3 --> S9
S9 --> S10[10. 调用方继续]

3.2 有环境变量维护列表的场景

choices JSON 文件已存在,用户多一个”从表选择”选项,设置后可选择将新值注册回列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
graph LR
S1[1. 检测] --> S2{2. 判断状态}
S2 -->|成功| S3[3. 直接取值]
S2 -->|非成功| S4[4. 展示参考示例]
S4 --> S5[5. 显示操作菜单: ①复制示例 ②手动输入 ③从表选择]
S5 --> S6[6. 执行菜单选择并验证]
S6 --> S7{7. 是否自动设置?}
S7 -->|是| S8a[8a. 自动写入并生效]
S7 -->|否| S8b[8b. 引导手动设置]
S8a --> S9[9. 输出值给调用方]
S8b --> S9
S3 --> S9
S9 --> T1{10. 是否加入维护列表?}
T1 -->|是| T1a[10a. 写入维护列表]
T1 -->|否| T1b[10b. 跳过]
T1a --> S10[11. 调用方继续]
T1b --> S10

3.3 步骤说明

步骤 职责模块 说明
1. 检测 检测模块 检查 env var 当前状态(未设置、占位符、文件不存在、JSON 格式无效等)
2. 判断状态 调用方 根据返回的 status_type 决定走成功分支或引导分支
3. 直接取值 调用方 status 为 env_success:直接读 env var 当前值
4. 展示参考示例 引导模块 展示示例文件内容(如 JSON 结构),帮助用户理解所需格式
5. 显示操作菜单 引导模块 显示菜单供用户选择如何提供 env var 的值:① 复制示例文件到目标目录、② 手动输入已有文件路径、③ 从环境变量维护列表中选择(仅在有 choices 文件时显示)
6. 执行菜单选择并验证 引导模块 按用户选择执行:① 复制示例 → 返回目标路径、② 手动输入 → 验证文件存在/格式有效、③ 从表选择 → 返回选中值
7. 是否自动设置 引导模块 询问用户:自动写入 system profile or 手动引导
8a. 自动写入并生效 持久化模块 写入 profile + source 生效
8b. 引导手动设置 引导模块 显示 export 模板 + 打开 profile + 提示 source
9. 输出值 引导模块 stdout 输出目标路径,供调用方捕获
10. 是否加入维护列表 调用方 仅 3.2:询问用户是否将当前值注册到 choices 列表
10a. 写入维护列表 调用方 仅 3.2:调用 env_var_add_to_choices.sh 写入 choices JSON
10b. 跳过 调用方 仅 3.2:不加入
11. 调用方继续 调用方 捕获 stdout 拿到值,继续执行业务逻辑

4. 核心流程(状态机)

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
graph TD
%% ===== 入口 =====
START[开始] --> PARSE[解析命名参数]
PARSE --> CHECK_PARAM{必要参数是否完整?}
CHECK_PARAM -->|否| ERR_MISS_PARAM[输出错误 → exit 1]
CHECK_PARAM -->|是| CALL_CHECK[委托检测模块]

%% ===== 检测模块 =====
subgraph DETECT [检测模块 - 无副作用]
CALL_CHECK --> CHECK{检查 env var 当前值}

CHECK -->|值为空| NOT_SET[status: env_not_set]
NOT_SET --> ADD_PLACEHOLDER{两阶段策略:<br>是否先设占位符?}
ADD_PLACEHOLDER -->|是| DO_ADD_PLACE[调用持久化模块写入 env_var=占位符]
DO_ADD_PLACE --> PLACE_RESULT{写入成功?}
PLACE_RESULT -->|失败| PLACE_FAIL[status: env_placeHolder_set_failure]
PLACE_RESULT -->|成功| PLACE_OK[status: env_placeHolder_set_success]

CHECK -->|值为占位符| IS_PLACE[status: env_is_placeholder]
IS_PLACE --> ADD_PLACEHOLDER

CHECK -->|有非占位值| CHECK_FILE{需检查文件?}
CHECK_FILE -->|否| SUCCESS[status: env_success]

CHECK_FILE -->|是 File| CHECK_EXIST{文件存在?}
CHECK_EXIST -->|否| FILE_MISS[status: env_file_noexsit]
CHECK_EXIST -->|是| CHECK_JSON{需验证 JSON 格式?}
CHECK_JSON -->|否| SUCCESS
CHECK_JSON -->|是| JSON_VALID{内容为有效 JSON?}
JSON_VALID -->|否| JSON_INVALID[status: env_file_not_json]
JSON_VALID -->|是| SUCCESS
end

%% ===== 路由 =====
SUCCESS --> ROUTE_STATUS{路由 status_type}
PLACE_FAIL --> ROUTE_STATUS
PLACE_OK --> ROUTE_STATUS
FILE_MISS --> ROUTE_STATUS
JSON_INVALID --> ROUTE_STATUS

ROUTE_STATUS -->|env_success| DIRECT_OUTPUT[直接输出当前值 → exit 0]

ROUTE_STATUS -->|env_placeHolder_set_success| GUIDE
ROUTE_STATUS -->|env_is_placeholder| GUIDE
ROUTE_STATUS -->|env_not_set| GUIDE
ROUTE_STATUS -->|env_file_noexsit| GUIDE
ROUTE_STATUS -->|env_file_not_json| GUIDE
ROUTE_STATUS -->|env_placeHolder_set_failure| FAIL_EXIT[输出错误 → exit 1]

%% ===== 引导模块 =====
subgraph GUIDE_MOD [交互引导模块]
GUIDE --> SHOW_EXAMPLE[展示参考示例]
SHOW_EXAMPLE --> MENU{显示操作菜单}

MENU --> OPT1[1. 复制示例到目标目录]
MENU --> OPT2[2. 手动输入已有文件路径]
MENU --> OPTQ[Q/q → exit 2]

OPT1 --> CHECK_OVERWRITE{目标文件已存在?}
CHECK_OVERWRITE -->|是| ASK_OVERWRITE{询问是否覆盖?}
ASK_OVERWRITE -->|y| DO_COPY[chmod +w → cp]
ASK_OVERWRITE -->|Q/q| EXIT_GUIDE[exit 2]
ASK_OVERWRITE -->|其他| ASK_OVERWRITE
CHECK_OVERWRITE -->|否| DO_COPY

DO_COPY --> COPY_CHECK{copy 成功?}
COPY_CHECK -->|否| COPY_FAIL[提示权限错误 → 返回 MENU]
COPY_CHECK -->|是| DECIDE_SET{是否自动设置环境变量?<br>y: 自动写入 / n: 手动引导}

OPT2 --> INPUT_PATH[提示输入路径]
INPUT_PATH --> INPUT_EMPTY{输入为空或 Q?}
INPUT_EMPTY -->|Q/q| EXIT_GUIDE
INPUT_EMPTY -->|否| INPUT_EXIST{文件存在?}
INPUT_EXIST -->|否| INPUT_RETRY[提示不存在 → 重新输入]
INPUT_EXIST -->|是| INPUT_VALID{文件格式正确?<br>可含值结构验证}
INPUT_VALID -->|否| INPUT_INVALID[提示参考示例 → 返回 MENU]
INPUT_VALID -->|是| DECIDE_SET

INPUT_RETRY --> INPUT_PATH
COPY_FAIL --> MENU
INPUT_INVALID --> MENU
end

%% ===== 设置决策 =====
DECIDE_SET -->|y| AUTO_SET[自动设置:<br>调用持久化模块写入 env_var=实际值<br>调用生效模块使配置生效]
AUTO_SET --> AUTO_CHECK{设置成功?}
AUTO_CHECK -->|否| SET_FAIL[提示失败 → exit 2]
AUTO_CHECK -->|是| OUTPUT_PATH[通过 stdout 输出目标路径]

DECIDE_SET -->|n| MANUAL_GUIDE[手动引导:<br>显示 export 模板<br>打开 profile<br>提示 source]
MANUAL_GUIDE --> OUTPUT_PATH

OUTPUT_PATH --> DONE[return 0 → 调用方继续]

4.1 状态枚举

status_type 含义 后续动作
env_success env var 正常可用 直接输出值 → exit 0
env_not_set env var 未设置 两阶段:先设占位符,再引导替换
env_is_placeholder env var 仍是占位值 引导替换为实际值
env_placeHolder_set_success 占位符已成功写入 引导替换为实际值
env_placeHolder_set_failure 占位符写入失败 输出错误 → exit 1
env_file_noexsit 文件不存在 引导修改路径
env_file_not_json 文件不是有效 JSON 引导修复文件

4.2 Exit Code 约定

Code 含义
0 成功:值已就绪,stdout 包含结果
1 参数错误或系统失败
2 用户主动退出(Q/q)或操作失败