强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 章 — 核心工具