环境变量与配置文件
本节目标
区分普通 Shell 变量与已导出的环境变量,在子 Bash 中验证继承边界,并把两个明确允许的练习配置写入 dev-lab 后逐行解析。
开始前状态
你已完成 LINUX-04:notes/linux-notes.md 精确包含前四节,原有 Shell checkpoint 文件、目录和权限均保持规范。本节只新增 config/env.dev-lab 并原子推进 Linux 笔记,不改 config/bashrc.dev-lab,也不读取 HOME 中的配置。
必要原理
Shell 变量只属于当前 shell;export 后,它才会作为环境变量进入随后启动的子进程。子进程不能反向修改父进程,所以隔离练习适合放在子 Bash 或函数子 shell 中。
项目配置文件不是 shell 程序。直接 source 或执行未知配置会把其中的命令语法交给 shell。本节只接受 DEVLAB_PROFILE 与 DEVLAB_REGION 两个键,逐行检查键、值、重复项和格式,再把值作为普通文本使用。
真实项目的 .env、代理设置、令牌和账号变量不在本练习范围内。不要把真实凭据复制进 dev-lab,也不要为了课程改写真实 rc 文件。
动手完成
先推进受控状态。迁移会验证完整 LINUX-04 路径集合、精确文件字节和权限;只有全部匹配,才在同目录创建两个非链接草稿,以可移植的符号模式规范化权限,精确比较后分别原子替换目标。
运行位置: 任意 Bash 工作目录,DEVLAB_DIR 指向完成 LINUX-04 的 dev-lab。预期输出: 无输出;新增精确的 config/env.dev-lab,笔记只新增“环境变量与配置”一节。失败处理: 任一检查失败都保持原 snapshot 与外部文件不变;恢复规范前置状态并确认草稿名没有被占用后重试。
advance_linux_environment_state() (
set -euo pipefail
devlab_input="${DEVLAB_DIR:-}"
[[ -n "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
[[ -d "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是真实目录。\n'; exit 1; }
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
[[ "$devlab_logical" == "$devlab_dir" && ! -L "$devlab_input" ]] || {
printf 'DEVLAB_DIR 不得经过符号链接。\n'
exit 1
}
[[ "$devlab_dir" != / ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
home_dir="$(cd -- "$HOME" && pwd -P)" || exit 1
[[ "$devlab_dir" != "$home_dir" ]] || { printf 'DEVLAB_DIR 不得是用户主目录。\n'; exit 1; }
fi
for required_directory in config data notes output scripts; do
[[ -d "$devlab_dir/$required_directory" && ! -L "$devlab_dir/$required_directory" ]] || {
printf '前置目录必须是真实目录:%s\n' "$required_directory"
exit 1
}
done
expected_paths='README.md
config
config/bashrc.dev-lab
data
data/access.log
notes
notes/linux-notes.md
notes/shell-notes.md
output
output/summary.txt
scripts
scripts/permission-demo.sh
scripts/report.sh'
actual_paths="$(cd "$devlab_dir" && find . -mindepth 1 -print | sed 's#^\./##' | LC_ALL=C sort)"
[[ "$actual_paths" == "$expected_paths" ]] || { printf 'LINUX-04 路径集合不匹配。\n'; exit 1; }
require_exact() {
local relative_path="$1"
local expected_content="$2"
[[ -f "$devlab_dir/$relative_path" && ! -L "$devlab_dir/$relative_path" ]] \
&& cmp -s "$expected_content" "$devlab_dir/$relative_path" \
|| { printf '前置文件不匹配:%s\n' "$relative_path"; exit 1; }
}
has_any_execute_bit() {
local mode
mode="$(LC_ALL=C ls -ld "$1")"
mode="${mode%% *}"
[[ "$mode" == *[xstST]* ]]
}
file_identity() {
local target="$1"
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$target" 2>/dev/null)"; then
:
else
identity="$(LC_ALL=C stat -L -f '%d:%i' "$target")" || exit 1
fi
printf '%s' "$identity"
}
fd_matches_identity() {
local fd_identity
fd_identity="$(file_identity "$1")" || return 1
[[ "$fd_identity" == "$2" || "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin \
&& "${fd_identity##*:}" == "${2##*:}" ]]
}
remove_if_owned() {
local owned_path="$1" owned_identity="$2" parent_path="${1%/*}" owned_name="${1##*/}"
local parent_identity=''
[[ -n "$owned_identity" ]] || return 0
case "$parent_path" in
"$devlab_dir/config") parent_identity="$config_dir_identity" ;;
"$devlab_dir/notes") parent_identity="$notes_dir_identity" ;;
*) return 1 ;;
esac
(
cd -P -- "$parent_path" || exit 1
cleanup_owned_boundary="$owned_name"
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ ! -e "./$owned_name" && ! -L "./$owned_name" ]] && exit 0
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name" 2>/dev/null)" == "$owned_identity" ]] || exit 1
/bin/rm -f -- "./$owned_name"
)
}
create_owned_file() {
local owned_path="$1" parent_path="$2" parent_identity="$3" mode="$4" identity_variable="$5"
local owned_name="${owned_path##*/}" caller_directory="$PWD" created_identity=''
cd -P -- "$parent_path" || return 1
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || return 1
set -o noclobber
exec 6> "./$owned_name" || { set +o noclobber; return 1; }
set +o noclobber
created_identity="$(file_identity "./$owned_name")" || { exec 6>&-; return 1; }
printf -v "$identity_variable" '%s' "$created_identity"
owned_post_create_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
/bin/cat >&6 || { exec 6>&-; return 1; }
owned_post_write_boundary="$owned_name"
/bin/chmod "$mode" /dev/fd/6 || { exec 6>&-; return 1; }
owned_post_mode_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$created_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || { exec 6>&-; return 1; }
exec 6>&-
cd -P -- "$caller_directory" || return 1
}
publish_owned_file() {
local owned_path="$1" owned_identity="$2" final_path="$3" parent_identity="$4" expected_final_identity="${5:-}"
local parent_path="${owned_path%/*}" owned_name="${owned_path##*/}" final_name="${final_path##*/}"
(
cd -P -- "$parent_path" || exit 1
publication_boundary="$owned_name->$final_name"
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" ]] || exit 1
if [[ -n "$expected_final_identity" ]]; then
[[ -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$expected_final_identity" ]] || exit 1
publication_commit_boundary="$owned_name->$final_name"
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" \
&& -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$expected_final_identity" ]] || exit 1
if [[ "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin ]]; then
/bin/mv -f -h -- "./$owned_name" "./$final_name" || exit 1
else
/bin/mv -f -T -- "./$owned_name" "./$final_name" || exit 1
fi
else
[[ ! -e "./$final_name" && ! -L "./$final_name" ]] || exit 1
publication_commit_boundary="$owned_name->$final_name"
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" \
&& ! -e "./$final_name" && ! -L "./$final_name" ]] || exit 1
if [[ "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin ]]; then
/bin/mv -n -h -- "./$owned_name" "./$final_name" || exit 1
else
/bin/mv -n -T -- "./$owned_name" "./$final_name" || exit 1
fi
fi
[[ ! -e "./$owned_name" && ! -L "./$owned_name" \
&& -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$owned_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]]
)
}
require_exact 'README.md' <(printf '%s\n' \
'# dev-lab Shell checkpoint' \
'' \
'这是终端与 Shell 板块完成后的权威参考状态。它保留固定访问日志、确定性摘要、隔离 Bash 配置和只会写入明确输出文件的报告脚本。' \
'' \
'请在此目录的父目录运行 `check-shell.sh`;checker 只读取目标,不会 source 配置或执行报告脚本。')
require_exact 'config/bashrc.dev-lab' <(printf '%s\n' \
"export DEVLAB_MODE='practice'" \
'' \
'devlab-root() {' \
' printf '\''%s\n'\'' "${DEVLAB_DIR:?set DEVLAB_DIR before using devlab-root}"' \
'}' \
'' \
'devlab-summary() {' \
' bash "${DEVLAB_DIR:?set DEVLAB_DIR first}/scripts/report.sh" "$DEVLAB_DIR"' \
'}')
require_exact 'data/access.log' <(printf '%s\n' \
'2026-08-12T09:00:00Z GET / 200' \
'2026-08-12T09:00:01Z GET /docs/devenv/welcome 200' \
'2026-08-12T09:00:02Z GET /docs/devenv/module-route 200' \
'2026-08-12T09:00:03Z GET /missing 404' \
'2026-08-12T09:00:04Z POST /practice 200' \
'2026-08-12T09:00:05Z GET /old-link 404')
require_exact 'notes/shell-notes.md' <(printf '%s\n' \
'# Shell 练习笔记' \
'' \
'历史记录可能包含参数、路径或令牌;先检查再分享或清理。Tab 补全帮助确认命令和文件名,但不代替阅读实际路径。' \
'' \
'本 checkpoint 的 Bash 配置仅用于 `DEVLAB_DIR` 指定的隔离练习目录。')
require_exact 'output/summary.txt' <(printf '%s\n' \
'total requests: 6' \
'successful requests: 4' \
'not found: 2')
require_exact 'scripts/report.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'[[ "$#" -eq 1 ]] || { printf '\''usage: report.sh DEV_LAB_DIR\n'\'' >&2; exit 64; }' \
'devlab_dir="${1:?usage: report.sh DEV_LAB_DIR}"' \
'input="$devlab_dir/data/access.log"' \
'output="$devlab_dir/output/summary.txt"' \
'[[ -f "$input" ]] || { printf '\''missing input: %s\n'\'' "$input" >&2; exit 1; }' \
'' \
'total="$(wc -l < "$input" | tr -d '\'' '\'')"' \
'successful="$(grep -c '\'' 200$'\'' "$input" || true)"' \
'not_found="$(grep -c '\'' 404$'\'' "$input" || true)"' \
'' \
'{' \
' printf '\''total requests: %s\n'\'' "$total"' \
' printf '\''successful requests: %s\n'\'' "$successful"' \
' printf '\''not found: %s\n'\'' "$not_found"' \
'} > "$output"')
require_exact 'scripts/permission-demo.sh' <(printf '%s\n' \
'#!/usr/bin/env bash' \
'set -euo pipefail' \
'' \
'printf '\''permission demo: ok\n'\''')
require_exact 'notes/linux-notes.md' <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。')
[[ -x "$devlab_dir/scripts/report.sh" && -x "$devlab_dir/scripts/permission-demo.sh" ]] \
|| { printf '前置脚本必须可执行。\n'; exit 1; }
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/scripts/report.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/scripts/permission-demo.sh"
BASH_ENV= ENV= bash --noprofile --norc -n "$devlab_dir/config/bashrc.dev-lab"
for relative_path in README.md config/bashrc.dev-lab data/access.log notes/shell-notes.md notes/linux-notes.md output/summary.txt; do
! has_any_execute_bit "$devlab_dir/$relative_path" || { printf '前置文件不得可执行:%s\n' "$relative_path"; exit 1; }
done
config_target="$devlab_dir/config/env.dev-lab"
config_draft="$devlab_dir/config/.env.dev-lab.next"
notes_target="$devlab_dir/notes/linux-notes.md"
notes_draft="$devlab_dir/notes/.linux-notes.md.next"
[[ ! -e "$config_target" && ! -L "$config_target" ]] || { printf '配置目标已存在,停止并检查。\n'; exit 1; }
[[ ! -e "$config_draft" && ! -L "$config_draft" ]] || { printf '配置草稿路径已存在,停止并检查。\n'; exit 1; }
[[ ! -e "$notes_draft" && ! -L "$notes_draft" ]] || { printf '笔记草稿路径已存在,停止并检查。\n'; exit 1; }
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
config_dir_identity="$(file_identity "$devlab_dir/config")" || exit 1
notes_dir_identity="$(file_identity "$devlab_dir/notes")" || exit 1
notes_target_identity="$(file_identity "$notes_target")" || exit 1
config_draft_identity=''
notes_draft_identity=''
config_published=0
config_published_identity=''
cleanup_linux_environment_drafts() {
local status="$?" cleanup_failed=0
trap - EXIT
remove_if_owned "$config_draft" "$config_draft_identity" || cleanup_failed=1
remove_if_owned "$notes_draft" "$notes_draft_identity" || cleanup_failed=1
if [[ "$config_published" -eq 1 ]]; then
if [[ -f "$config_target" && ! -L "$config_target" \
&& "$(file_identity "$config_target")" == "$config_published_identity" ]] \
&& cmp -s "$config_target" <(printf '%s\n' 'DEVLAB_PROFILE=practice' 'DEVLAB_REGION=local'); then
remove_if_owned "$config_target" "$config_published_identity" || cleanup_failed=1
else
cleanup_failed=1
fi
fi
[[ "$cleanup_failed" -eq 0 ]] || exit 1
exit "$status"
}
trap cleanup_linux_environment_drafts EXIT
create_owned_file "$config_draft" "$devlab_dir/config" "$config_dir_identity" u=rw,go=r config_draft_identity <<'EXPECTED'
DEVLAB_PROFILE=practice
DEVLAB_REGION=local
EXPECTED
create_owned_file "$notes_draft" "$devlab_dir/notes" "$notes_dir_identity" u=rw,go=r notes_draft_identity <<'EXPECTED'
# Linux 基础练习笔记
## 文件系统
- 系统目录只观察;所有练习写入明确的 dev-lab。
- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。
## 用户、用户组与权限
- 先确认目标是非符号链接普通文件,再只增加用户执行位。
## 进程、任务与信号
- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。
## 软件包与命令
- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。
## 环境变量与配置
- 子进程只继承已导出的变量;项目配置只解析允许的键和值。
EXPECTED
[[ -f "$config_draft" && ! -L "$config_draft" && -f "$notes_draft" && ! -L "$notes_draft" ]] \
|| { printf '草稿必须是普通文件。\n'; exit 1; }
cmp -s "$config_draft" <(printf '%s\n' 'DEVLAB_PROFILE=practice' 'DEVLAB_REGION=local') \
|| { printf '配置草稿内容不匹配。\n'; exit 1; }
cmp -s "$notes_draft" <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。' \
'' \
'## 环境变量与配置' \
'- 子进程只继承已导出的变量;项目配置只解析允许的键和值。') \
|| { printf '笔记草稿内容不匹配。\n'; exit 1; }
! has_any_execute_bit "$config_draft" && ! has_any_execute_bit "$notes_draft" \
|| { printf '普通草稿不得可执行。\n'; exit 1; }
[[ "$(file_identity "$devlab_dir/config")" == "$config_dir_identity" \
&& "$(file_identity "$config_draft")" == "$config_draft_identity" ]] || exit 1
config_published_identity="$config_draft_identity"
publish_owned_file "$config_draft" "$config_draft_identity" "$config_target" "$config_dir_identity"
config_published=1
[[ "$(file_identity "$config_target")" == "$config_published_identity" \
&& "$(file_identity "$devlab_dir/notes")" == "$notes_dir_identity" \
&& "$(file_identity "$notes_draft")" == "$notes_draft_identity" ]] || exit 1
publish_owned_file "$notes_draft" "$notes_draft_identity" "$notes_target" "$notes_dir_identity" "$notes_target_identity"
[[ "$(file_identity "$notes_target")" == "$notes_draft_identity" ]] || exit 1
config_published=0
trap - EXIT
)
advance_linux_environment_state
transition_status="$?"
unset -f advance_linux_environment_state
[[ "$transition_status" -eq 0 ]] || exit "$transition_status"
现在在函数子 shell 中演示继承并解析刚创建的配置。第一轮子 Bash 看不到未导出的同名变量,第二轮在 export 后看到固定值;函数返回后,调用它的父 shell 同名变量保持原值。配置先逐行拒绝未知键、重复键、空值和额外 shell 语法,再运行固定的允许列表解析核心。
运行位置: 任意 Bash 工作目录,DEVLAB_DIR 指向刚完成本节迁移的 dev-lab。预期输出: 精确为 environment practice: ok。失败处理: 非零退出时不要执行配置;检查文件是否仍是两个精确允许行,并从规范 LINUX-04 状态重新迁移。
linux_environment_practice() (
set -euo pipefail
devlab_input="${DEVLAB_DIR:-}"
[[ -n "$devlab_input" && -d "$devlab_input" && ! -L "$devlab_input" ]] || exit 1
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
[[ "$devlab_logical" == "$devlab_dir" && "$devlab_dir" != / ]] || exit 1
if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
home_dir="$(cd -- "$HOME" && pwd -P)" || exit 1
[[ "$devlab_dir" != "$home_dir" ]] || exit 1
fi
DEVLAB_DIR="$devlab_dir"
config_file="$DEVLAB_DIR/config/env.dev-lab"
[[ -f "$config_file" && ! -L "$config_file" ]] || exit 1
seen_profile=0
seen_region=0
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
DEVLAB_PROFILE=practice)
[[ "$seen_profile" -eq 0 ]] || { printf '配置键重复:DEVLAB_PROFILE\n' >&2; exit 1; }
seen_profile=1
;;
DEVLAB_REGION=local)
[[ "$seen_region" -eq 0 ]] || { printf '配置键重复:DEVLAB_REGION\n' >&2; exit 1; }
seen_region=1
;;
DEVLAB_PROFILE=*|DEVLAB_REGION=*) printf '配置值不在允许列表。\n' >&2; exit 1 ;;
*=*) printf '未知配置键:%s\n' "${line%%=*}" >&2; exit 1 ;;
*) printf '配置行格式无效。\n' >&2; exit 1 ;;
esac
done < "$config_file"
[[ "$seen_profile" -eq 1 && "$seen_region" -eq 1 ]] || { printf '配置键不完整。\n' >&2; exit 1; }
profile=''
region=''
while IFS='=' read -r key value; do
case "$key" in
DEVLAB_PROFILE) profile=$value ;;
DEVLAB_REGION) region=$value ;;
*) printf '未知配置键:%s\n' "$key" >&2; exit 1 ;;
esac
done < "$DEVLAB_DIR/config/env.dev-lab"
test "$profile" = practice
test "$region" = local
export -n DEVLAB_INHERITANCE_PROBE 2>/dev/null || true
DEVLAB_INHERITANCE_PROBE='child-value'
BASH_ENV= ENV= bash --noprofile --norc -c 'test -z "${DEVLAB_INHERITANCE_PROBE+x}"'
export DEVLAB_INHERITANCE_PROBE
BASH_ENV= ENV= bash --noprofile --norc -c 'test "$DEVLAB_INHERITANCE_PROBE" = child-value'
printf 'environment practice: ok\n'
)
linux_environment_practice
practice_status="$?"
unset -f linux_environment_practice
[[ "$practice_status" -eq 0 ]] || exit "$practice_status"
观察结果
config/env.dev-lab 精确包含两个允许键。普通变量只有当前 shell 可见,导出后才进入新子进程;练习结束后,父 shell 的同名变量、真实配置与代理状态都没有改变。
常见问题
- 能否把配置直接 source? 不能。配置是数据,必须先逐行验证键和值。
- 为什么拒绝重复键? 后写覆盖前写会隐藏歧义;唯一键让结果可复查。
- 能否放真实令牌? 不能。本练习只使用公开固定值,不处理任何真实秘密。
完成检查
-
config/env.dev-lab只有DEVLAB_PROFILE=practice与DEVLAB_REGION=local。 - 继承练习只输出
environment practice: ok,父 shell 同名变量不变。 -
notes/linux-notes.md精确包含前五节,两个草稿路径均不存在。
下一步
下一节只在 loopback 动态端口上启动本练习自己的临时服务;若本机没有课程支持的运行时,则使用固定访问日志完成离线诊断。