跳到主要内容

软件包与命令安装

本节目标

判断命令来自 shell 内建功能还是可执行文件,查看版本,并在不安装软件、不修改软件源、不依赖公网的前提下观察已有包管理器的本地包状态。

开始前状态

你已完成 LINUX-03:进程练习留下的瞬时状态已清理,dev-lab 的规范笔记精确包含前三节。本节的命令观察不写入 checkpoint;状态迁移仍先验证完整前置状态。

运行位置: 任意 Bash 工作目录;这些命令只观察当前 shell 和 PATH。预期输出: cd 的类型、Bash 的命令路径和版本信息。失败处理: 若 Bash 不在预期 PATH,先检查当前 shell 与环境配置,不要立刻改动系统软件。

type cd
command -v bash
bash --version

必要原理

type 能区分 shell 内建功能、别名、函数与外部命令;command -v 适合脚本判断 PATH 中将运行哪个命令;许多外部命令通过 --version 报告版本,但具体选项仍应以该命令帮助为准。

包管理器维护的是本地包状态与可用元数据。apt-get --simulate 只计算计划,不执行真实变更;即使如此,它仍可能因为本地索引缺失而失败。已有 Homebrew 的 macOS 环境可以用 brew info 查看信息。本节把两者都当作可选观察:没有包管理器,或本地状态不足,都不阻塞 checkpoint。

真实安装属于课后可选操作,本页不提供可自动执行的安装命令,也不指导安装 Homebrew。先理解命令来源、版本、平台差异与变更范围,再根据自己的系统文档决定是否安装。

动手完成

必做块先检查命令来源和版本,再按 PATH 选择一个分支:已有 apt-get 时只做模拟,只有 brew 时只查看信息,两者都没有时跳过。所有工具输出被隐藏,因此成功 stdout 固定;任何包管理器观察失败也不会改变或阻塞规范笔记。

运行位置: 任意 Bash 工作目录;无需进入 dev-lab。预期输出: 精确为 package practice: observed失败处理: Bash 自身检查失败时停止并修复 PATH;包管理器因本地状态不足而失败时保持不安装,直接继续 checkpoint 迁移。

linux_package_practice() (
set -euo pipefail

type cd >/dev/null
command -v bash >/dev/null
bash --version >/dev/null

if command -v apt-get >/dev/null 2>&1; then
apt-get --simulate install curl >/dev/null 2>&1 || true
elif command -v brew >/dev/null 2>&1; then
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_FROM_API=1 brew info curl >/dev/null 2>&1 || true
else
printf '未检测到 apt-get 或 Homebrew;跳过包管理器观察。\n' >/dev/null
fi
printf 'package practice: observed\n'
)

linux_package_practice
package_status="$?"
unset -f linux_package_practice
[[ "$package_status" -eq 0 ]]

观察结束后只推进规范笔记。迁移拒绝空路径、根目录、用户主目录、符号链接目标或经过符号链接的父路径,并逐一验证 LINUX-03 的完整路径集合、文件字节和执行权限。

运行位置: DEVLAB_DIR 指向完成 LINUX-03 的 dev-lab。预期输出: 无输出,Linux 笔记只原子新增“软件包与命令”一节。失败处理: 失败时 snapshot 保持不变;如果普通草稿曾创建会被清理,符号链接草稿则原样拒绝且不跟随。

advance_linux_package_notes() (
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-03 路径集合不匹配。\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]* ]]
}
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" ]] || { printf 'report.sh 必须可执行。\n'; exit 1; }
[[ -x "$devlab_dir/scripts/permission-demo.sh" ]] || { printf 'permission-demo.sh 必须可执行。\n'; exit 1; }
bash -n "$devlab_dir/scripts/report.sh"
bash -n "$devlab_dir/scripts/permission-demo.sh"
bash -n "$devlab_dir/config/bashrc.dev-lab"
for relative_path in README.md config/bashrc.dev-lab data/access.log notes/shell-notes.md output/summary.txt notes/linux-notes.md; do
! has_any_execute_bit "$devlab_dir/$relative_path" || { printf '前置文件不得可执行:%s\n' "$relative_path"; exit 1; }
done

current_notes="$devlab_dir/notes/linux-notes.md"
notes_draft="$devlab_dir/notes/.linux-notes.md.next"
notes_dir="$devlab_dir/notes"
file_identity() {
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$1" 2>/dev/null)"; then :; else
identity="$(LC_ALL=C stat -L -f '%d:%i' "$1")" || return 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" owned_name="${1##*/}"
[[ -n "$owned_identity" ]] || return 0
(
cd -P -- "$notes_dir" || exit 1
cleanup_owned_boundary="$owned_name"
[[ "$(file_identity .)" == "$notes_dir_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
[[ -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
[[ ! -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" ]]
)
}
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
notes_dir_identity="$(file_identity "$notes_dir")" || exit 1
current_notes_identity="$(file_identity "$current_notes")" || exit 1
[[ ! -e "$notes_draft" && ! -L "$notes_draft" ]] || { printf '笔记草稿路径已存在,停止并检查。\n'; exit 1; }
notes_draft_identity=''
cleanup_linux_package_draft() {
local status="$?"
trap - EXIT
remove_if_owned "$notes_draft" "$notes_draft_identity" || exit 1
exit "$status"
}
trap cleanup_linux_package_draft EXIT

create_owned_file "$notes_draft" "$notes_dir" "$notes_dir_identity" u=rw,go=r notes_draft_identity <<'EXPECTED'
# Linux 基础练习笔记

## 文件系统
- 系统目录只观察;所有练习写入明确的 dev-lab。
- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。

## 用户、用户组与权限
- 先确认目标是非符号链接普通文件,再只增加用户执行位。

## 进程、任务与信号
- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。

## 软件包与命令
- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。
EXPECTED
cmp -s <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。' \
'' \
'## 用户、用户组与权限' \
'- 先确认目标是非符号链接普通文件,再只增加用户执行位。' \
'' \
'## 进程、任务与信号' \
'- 只终止本练习刚启动并保存 PID 的进程,随后 wait 完成回收。' \
'' \
'## 软件包与命令' \
'- 先确认命令来源和包管理器;模拟或查看信息不等于真实安装。') \
"$notes_draft" || exit 1
[[ "$(file_identity "$notes_dir")" == "$notes_dir_identity" \
&& "$(file_identity "$notes_draft")" == "$notes_draft_identity" ]] || exit 1
publish_owned_file "$notes_draft" "$notes_draft_identity" "$current_notes" "$notes_dir_identity" "$current_notes_identity"
[[ "$(file_identity "$current_notes")" == "$notes_draft_identity" ]] || exit 1
)

advance_linux_package_notes
package_transition_status="$?"
unset -f advance_linux_package_notes
[[ "$package_transition_status" -eq 0 ]]

观察结果

三种 PATH 情况都得到同一个确定性完成标记:优先模拟 Debian 系包管理计划,其次查看既有 Homebrew 信息,否则安全跳过。练习不会新增软件、缓存或 checkpoint 文件;只有单独的状态迁移会更新规范笔记。

常见问题

为什么模拟命令仍可能失败? 模拟需要读取本地索引、依赖关系和配置;本地包状态不完整时无法形成计划,但没有发生真实安装。

为什么找到命令还要看来源? 同名命令可能是 shell 内建功能、函数、别名或不同目录里的可执行文件。先用 typecommand -v 确认来源,才能正确解释版本与行为。

完成检查

  • 能用 type 区分内建功能与外部命令;
  • 能用 command -v 确认 PATH 解析结果,并查看 --version
  • 理解 apt-get --simulatebrew info 都不是安装;
  • 没有可用包管理器时仍能安全完成练习;
  • 成功 stdout 精确为 package practice: observed
  • dev-lab 只原子新增规范第四节。

下一步

下一节会学习环境变量与隔离配置,继续保持“真实系统只观察,确定性结果才写入 dev-lab”的边界。