当脚本缺少必要的环境变量,或环境变量指向的文件不存在/格式无效时,按以下设计总则实现引导用户补充的完整流程。
参考实现: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)或操作失败 |