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

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 对比

特性ashbashdashsh (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 本章小结

概念说明
ashBusyBox 默认 Shell,POSIX 兼容
变量使用 ${} 引用,始终加双引号
函数使用 name() {} 语法
无数组使用字符串和 for 循环替代
POSIX 兼容编写可移植脚本的最佳实践
set -e遇错退出,增强脚本健壮性

扩展阅读


上一章: 第 6 章 — 网络工具
下一章: 第 8 章 — 核心工具