BusyBox 搭建 mini rootfs 完全指南 / 第 7 章:ash Shell
第 7 章:ash Shell
7.1 ash Shell 概述
7.1.1 ash 简介
ash(Almquist Shell)是 BusyBox 默认的 Shell 实现,源自 Kenneth Almquist 的轻量级 Shell。它是嵌入式 Linux 和容器环境中最常用的 Shell。
7.1.2 Shell 对比
| 特性 | ash | bash | dash | sh (POSIX) |
|---|---|---|---|---|
| 大小 | ~100KB | ~1MB | ~120KB | ~100KB |
| 启动速度 | 快 | 较慢 | 快 | 快 |
| 数组支持 | ✗ | ✓ | ✗ | ✗ |
| 函数 | ✓ | ✓ | ✓ | ✓ |
| 交互式 | 一般 | 优秀 | 一般 | 一般 |
| POSIX 兼容 | ✓ | 超集 | ✓ | ✓ |
| 嵌入式适用 | ✓ | ✗ | ✓ | ✓ |
7.1.3 启动 ash
# 直接启动 ash
$ ash
# 通过符号链接启动
$ sh
# 如果 /bin/sh -> busybox,则启动的是 ash
# 检查当前 Shell
$ echo $0
/bin/sh
# 查看 Shell 版本
$ busybox sh --help
BusyBox v1.36.1 (2024-01-01 10:00:00 CST) multi-call binary.
Usage: sh [-/+OPTIONS] [-/+o OPT]... [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]
7.2 ash 基本特性
7.2.1 变量操作
# 变量赋值(注意:等号两边不能有空格)
$ NAME="BusyBox"
$ VERSION=1.36.1
# 变量引用
$ echo $NAME
BusyBox
$ echo "${NAME} v${VERSION}"
BusyBox v1.36.1
# 只读变量
$ readonly PI=3.14
$ PI=3.15
/bin/sh: can't create PI: Read-only file system
# 删除变量
$ unset NAME
# 环境变量
$ export MYVAR="hello"
$ env | grep MYVAR
MYVAR=hello
# 特殊变量
$ echo $# # 参数个数
$ echo $@ # 所有参数
$ echo $* # 所有参数(字符串形式)
$ echo $? # 上一个命令的退出码
$ echo $$ # 当前 Shell PID
$ echo $! # 后台进程 PID
$ echo $0 # 脚本名称
$ echo $1 # 第一个参数
$ echo $HOME # 主目录
$ echo $PATH # 路径
$ echo $PWD # 当前目录
7.2.2 字符串操作
# 字符串长度
$ STR="Hello"
$ echo ${#STR}
5
# 子字符串
$ STR="Hello World"
$ echo ${STR:0:5}
Hello
$ echo ${STR:6}
World
# 默认值
$ echo ${UNSET:-default}
default
# 赋默认值
$ echo ${UNSET:=default}
default
$ echo $UNSET
default
# 替换
$ STR="hello-world-hello"
$ echo ${STR/hello/HELLO}
HELLO-world-hello
$ echo ${STR//hello/HELLO}
HELLO-world-HELLO
# 删除前缀
$ FILE="/path/to/file.tar.gz"
$ echo ${FILE##*/}
file.tar.gz
# 删除后缀
$ echo ${FILE%%.tar.gz}
/path/to/file
# 删除最短后缀
$ echo ${FILE%.gz}
/path/to/file.tar
7.2.3 条件表达式
# if 语句
if [ "$1" = "start" ]; then
echo "Starting..."
elif [ "$1" = "stop" ]; then
echo "Stopping..."
else
echo "Usage: $0 {start|stop}"
fi
# 注意:[ 和 ] 与变量之间必须有空格
# 正确: [ "$VAR" = "value" ]
# 错误: ["$VAR"="value"]
# test 命令(等价于 [])
if test -f /etc/passwd; then
echo "File exists"
fi
# 文件测试
[ -f file ] # 文件存在且是普通文件
[ -d dir ] # 目录存在
[ -e path ] # 路径存在
[ -r file ] # 可读
[ -w file ] # 可写
[ -x file ] # 可执行
[ -s file ] # 文件非空
[ -L file ] # 是符号链接
[ file1 -nt file2 ] # file1 比 file2 新
# 字符串测试
[ -z "$str" ] # 字符串为空
[ -n "$str" ] # 字符串非空
[ "$a" = "$b" ] # 字符串相等
[ "$a" != "$b" ] # 字符串不等
# 数值测试
[ "$a" -eq "$b" ] # 等于
[ "$a" -ne "$b" ] # 不等于
[ "$a" -gt "$b" ] # 大于
[ "$a" -lt "$b" ] # 小于
[ "$a" -ge "$b" ] # 大于等于
[ "$a" -le "$b" ] # 小于等于
# 逻辑运算
[ "$a" = "1" ] && [ "$b" = "2" ] # AND
[ "$a" = "1" ] || [ "$b" = "2" ] # OR
[ ! "$a" = "1" ] # NOT
7.2.4 循环结构
# for 循环
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# 文件遍历
for file in /tmp/*.log; do
[ -f "$file" ] || continue
echo "Processing: $file"
done
# 范围遍历
for i in $(seq 1 10); do
echo $i
done
# while 循环
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# 读取文件
while read line; do
echo "Line: $line"
done < /etc/passwd
# until 循环
until ping -c 1 8.8.8.8 >/dev/null 2>&1; do
echo "Waiting for network..."
sleep 1
done
echo "Network available!"
# break 和 continue
for i in 1 2 3 4 5; do
[ "$i" = "3" ] && continue
[ "$i" = "5" ] && break
echo $i
done
7.2.5 case 语句
# 基本 case
case "$1" in
start)
echo "Starting service"
;;
stop)
echo "Stopping service"
;;
restart)
echo "Restarting service"
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
# 模式匹配
case "$filename" in
*.tar.gz) echo "tar.gz archive" ;;
*.tar.bz2) echo "tar.bz2 archive" ;;
*.tar.xz) echo "tar.xz archive" ;;
*.zip) echo "zip archive" ;;
*) echo "Unknown format" ;;
esac
# 多模式匹配
case "$char" in
[a-z]) echo "Lowercase letter" ;;
[A-Z]) echo "Uppercase letter" ;;
[0-9]) echo "Digit" ;;
*) echo "Other" ;;
esac
7.3 函数
7.3.1 函数定义和调用
# 方式一(推荐)
greet() {
local name="$1"
echo "Hello, $name!"
}
# 方式二
function greet {
echo "Hello, $1!"
}
# 调用函数
greet "World"
# 输出: Hello, World!
# 函数返回值
is_running() {
pidof "$1" >/dev/null 2>&1
return $?
}
if is_running "sshd"; then
echo "sshd is running"
fi
7.3.2 函数参数和局部变量
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message"
}
log "INFO" "System started"
# [2024-01-01 12:00:00] [INFO] System started
7.3.3 常用函数库
# /etc/init.d/functions - 系统函数库
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
# PID 管理
PIDFILE="/var/run/myapp.pid"
start_daemon() {
local daemon="$1"
shift
if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
log_warning "$daemon is already running"
return 1
fi
$daemon "$@" &
echo $! > "$PIDFILE"
log_success "Started $daemon (PID: $!)"
}
stop_daemon() {
local daemon="$1"
if [ ! -f "$PIDFILE" ]; then
log_warning "$daemon is not running"
return 1
fi
local pid=$(cat "$PIDFILE")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
log_success "Stopped $daemon"
else
log_warning "$daemon PID $pid not found"
fi
rm -f "$PIDFILE"
}
7.4 ash 与 Bash 的差异
7.4.1 不支持的特性
# ❌ 数组(ash 不支持)
arr=(1 2 3) # Bash ✓, ash ✗
echo ${arr[0]} # Bash ✓, ash ✗
# ✅ 替代方案:使用字符串
arr="1 2 3"
for item in $arr; do
echo $item
done
# ❌ [[ ]] 双括号(ash 不支持)
[[ "$a" == "$b" ]] # Bash ✓, ash ✗
[[ -f "$file" && -r "$file" ]] # Bash ✓, ash ✗
# ✅ 替代方案:使用 [ ] 和 && ||
[ "$a" = "$b" ] && echo "equal"
[ -f "$file" ] && [ -r "$file" ] && echo "readable"
# ❌ 算术展开 $(( ))(ash 支持有限)
$((a + b)) # ash ✓
$((a++)) # ash ✗(自增运算不支持)
# ❌ heredoc 中的变量展开(ash 支持有限)
cat << EOF
Hello $NAME # ash ✓
EOF
cat << 'EOF'
Hello $NAME # 不展开变量
EOF
# ❌ process substitution(ash 不支持)
diff <(cmd1) <(cmd2) # Bash ✓, ash ✗
# ✅ 替代方案:使用临时文件
cmd1 > /tmp/out1
cmd2 > /tmp/out2
diff /tmp/out1 /tmp/out2
7.4.2 兼容性编写指南
#!/bin/sh
# 可移植 Shell 脚本编写指南
# ✅ 使用 POSIX 兼容语法
# 变量引用始终加双引号
echo "$HOME"
echo "${PATH}"
# ✅ 使用 [ ] 替代 [[ ]]
if [ "$a" = "$b" ]; then
echo "equal"
fi
# ✅ 使用 $() 替代反引号
files=$(ls /tmp)
# ✅ 使用 $(( )) 进行算术运算
count=$((count + 1))
# ✅ 使用函数返回值替代数组
get_item() {
case "$1" in
0) echo "first" ;;
1) echo "second" ;;
2) echo "third" ;;
esac
}
# ✅ 使用 case 替代正则
case "$email" in
*@*.*) echo "Valid email format" ;;
*) echo "Invalid email format" ;;
esac
7.4.3 常见兼容性问题
# 问题 1: 字符串比较
# Bash 允许
[ $a = $b ] # 可能出错如果 $a 为空
# POSIX 推荐
[ "$a" = "$b" ] # 始终加引号
# 问题 2: 赋值语句
# Bash 允许
local arr=(1 2 3) # ash 不支持
# POSIX 替代
local arr="1 2 3"
# 问题 3: 函数定义
# Bash 允许
function myfunc { # ash 不支持这种写法
echo "hello"
}
# POSIX 兼容
myfunc() {
echo "hello"
}
# 问题 4: 条件表达式
# Bash 允许
[[ $a == *pattern* ]] # ash 不支持
# POSIX 替代
case "$a" in *pattern*) echo "match" ;; esac
7.5 脚本编写最佳实践
7.5.1 脚本模板
#!/bin/sh
# /usr/bin/myscript - 描述脚本功能
# Usage: myscript [options] <arguments>
set -e # 遇错退出
set -u # 未定义变量报错
# 版本和配置
VERSION="1.0.0"
CONFIG_FILE="/etc/myscript.conf"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# 日志函数
log() {
echo -e "${GREEN}[INFO]${NC} $*"
}
error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
die() {
error "$@"
exit 1
}
# 使用说明
usage() {
cat << EOF
Usage: $(basename $0) [OPTIONS] <arguments>
Options:
-h, --help Show this help
-v, --verbose Enable verbose output
-q, --quiet Suppress output
-n, --dry-run Dry run mode
Examples:
$(basename $0) --help
$(basename $0) -v input.txt
EOF
exit 0
}
# 参数解析
VERBOSE=0
QUIET=0
DRYRUN=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage ;;
-v|--verbose) VERBOSE=1; shift ;;
-q|--quiet) QUIET=1; shift ;;
-n|--dry-run) DRYRUN=1; shift ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
# 检查必要参数
[ $# -lt 1 ] && die "Missing required argument. See --help"
# 主逻辑
main() {
local input="$1"
[ ! -f "$input" ] && die "File not found: $input"
log "Processing: $input"
# ... 业务逻辑 ...
log "Done."
}
# 执行主函数
main "$@"
7.5.2 参数解析模板
#!/bin/sh
# 命令行参数解析
parse_args() {
local verbose=0
local output=""
local input=""
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
verbose=$((verbose + 1))
shift
;;
-o|--output)
[ -z "$2" ] && die "Option $1 requires argument"
output="$2"
shift 2
;;
-o=*|--output=*)
output="${1#*=}"
shift
;;
-h|--help)
usage
;;
--)
shift
break
;;
-*)
die "Unknown option: $1"
;;
*)
break
;;
esac
done
# 剩余参数作为输入文件
input="$@"
# 导出变量
VERBOSE=$verbose
OUTPUT=$output
INPUT=$input
}
# 使用
parse_args "$@"
echo "Verbose: $VERBOSE"
echo "Output: $OUTPUT"
echo "Input: $INPUT"
7.6 内置命令
7.6.1 常用内置命令
# 命令执行
$ command ls # 跳过函数/别名
$ builtin echo # 强制使用内置版本
$ exec /bin/sh # 替换当前 Shell
$ eval "echo hello" # 执行字符串命令
# 变量操作
$ export VAR=value # 导出变量
$ unset VAR # 删除变量
$ readonly VAR=value # 只读变量
$ declare -i VAR=0 # 声明整数(ash 不支持)
# 目录操作
$ cd /tmp # 切换目录
$ pushd /var # 压入目录栈(部分 ash 不支持)
$ popd # 弹出目录栈
$ pwd # 打印当前目录
# 作业控制
$ command & # 后台运行
$ jobs # 列出后台任务
$ fg %1 # 前台运行
$ bg %1 # 后台继续
$ wait # 等待所有后台任务
$ wait %1 # 等待特定任务
# 信号处理
$ trap 'cleanup' EXIT # 退出时执行
$ trap 'reload' HUP # 收到 HUP 时执行
$ trap '' INT # 忽略 INT 信号
$ trap - INT # 恢复默认处理
7.6.2 I/O 重定向
# 输出重定向
$ echo "hello" > file.txt # 覆盖写入
$ echo "hello" >> file.txt # 追加写入
# 输入重定向
$ cat < file.txt
# 错误重定向
$ command 2> error.log # 错误输出到文件
$ command 2>&1 # 错误输出到 stdout
$ command > all.log 2>&1 # 所有输出到文件
# 合并输出
$ command > /dev/null 2>&1 # 丢弃所有输出
# Here Document
$ cat << EOF
Line 1
Line 2
EOF
# Here String(ash 不支持)
$ cat <<< "string" # ash ✗
$ echo "string" | cat # 替代方案
# 文件描述符
$ exec 3> /tmp/fd3.txt # 打开 fd 3
$ echo "hello" >&3 # 写入 fd 3
$ exec 3>&- # 关闭 fd 3
7.6.3 管道和逻辑运算
# 管道
$ cat /etc/passwd | grep root | cut -d: -f1
# 逻辑与
$ mkdir -p /tmp/test && cd /tmp/test
# 逻辑或
$ command || echo "Command failed"
# 组合
$ mkdir -p /tmp/test && cd /tmp/test || exit 1
# 管道与逻辑
$ cat file | grep pattern && echo "Found" || echo "Not found"
7.7 Shell 配置文件
7.7.1 启动文件
# /etc/profile - 系统级配置
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
export HOSTNAME=$(hostname)
export HOME=/root
export PS1='[\u@\h \W]\$ '
export TZ='CST-8'
# 别名
alias ll='ls -la'
alias la='ls -A'
alias l='ls -CF'
# /etc/profile.d/*.sh - 模块化配置
# /etc/profile.d/aliases.sh
alias cp='cp -i'
alias mv='mv -i'
alias rm='rm -i'
# ~/.profile - 用户级配置
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
7.7.2 ash 特定配置
# ash 不读取 .bashrc
# 配置通过 /etc/profile 和 ~/.profile
# 设置 ash 提示符
PS1='$ ' # 简单提示符
PS1='[\u@\h \W]\$ ' # 类 bash 提示符(需要 busybox 支持 \u \h \W)
# 启用别名支持
# 编译 BusyBox 时启用:
# Shells → ash → [*] Alias support
7.8 调试脚本
# 方式一:使用 -x 选项
$ sh -x script.sh
+ echo 'Starting...'
Starting...
+ count=0
# 方式二:脚本内部启用
#!/bin/sh
set -x # 启用调试
# ... 代码 ...
set +x # 禁用调试
# 方式三:部分调试
#!/bin/sh
debug() {
[ "$DEBUG" = "1" ] && echo "[DEBUG] $*"
}
DEBUG=1
debug "Variable X=$X"
# 方式四:使用 trap
#!/bin/sh
trap 'echo "Line $LINENO: exit code $?"' ERR
7.9 本章小结
| 概念 | 说明 |
|---|---|
| ash | BusyBox 默认 Shell,POSIX 兼容 |
| 变量 | 使用 ${} 引用,始终加双引号 |
| 函数 | 使用 name() {} 语法 |
| 无数组 | 使用字符串和 for 循环替代 |
| POSIX 兼容 | 编写可移植脚本的最佳实践 |
| set -e | 遇错退出,增强脚本健壮性 |
扩展阅读
上一章: 第 6 章 — 网络工具
下一章: 第 8 章 — 核心工具