Shell脚本基础

青青子衿 0 阅读 linux服务

Shell脚本基础

导读:如果说Linux命令是"一块块砖头",那Shell脚本就是把这些砖头砌成"一面墙"的砂浆。学会Shell脚本,你就能把几十条命令组合成一个自动化程序,让重复性工作一键完成。本章从零基础讲起,带你写出自己的第一个脚本。


1 什么是Shell?

生活类比

Shell就像你和Linux内核之间的"翻译官"。你不可能直接跟内核说机器语言,而是通过Shell把你的命令翻译成内核能理解的指令。就像你不会直接操作厨房的灶台电路,而是通过"菜谱"(命令)告诉"厨师"(Shell)来做菜。

sh / bash / zsh 的区别

Shell全称特点适用场景
shBourne Shell最原始的Shell,功能最基础编写需要最大兼容性的脚本
bashBourne Again Shellsh的增强版,Linux默认Shell最常用,绝大多数服务器默认
zshZ Shell功能最丰富,补全最强大交互式使用,macOS默认Shell
# 查看当前使用的Shell
echo $SHELL

# 查看系统中安装了哪些Shell
cat /etc/shells

# 临时切换到zsh
zsh

# 永久更改默认Shell
chsh -s /bin/zsh

互动提问:bash 和 sh 是什么关系?在Linux上用sh执行脚本,实际上用的是什么?

答案:在大多数Linux发行版上,sh 是一个指向 bash(或 dash)的软链接。用 sh 执行脚本时,bash会以"POSIX兼容模式"运行,部分bash特有功能会被禁用。写脚本建议明确用 #!/bin/bash


2 第一个脚本:Hello World

shebang——脚本的"身份证"

#!/bin/bash
# 上面这行叫 shebang(#! + 解释器路径)
# 它告诉系统:用 /bin/bash 来执行这个脚本

echo "Hello, World!"
echo "当前时间:$(date)"
echo "当前用户:$(whoami)"
echo "当前目录:$(pwd)"

什么是shebang? #! 是两个字符:#(井号)和 !(感叹号)。它必须出现在脚本文件的第一行第一个字符,后面跟上解释器的完整路径。没有shebang,系统不知道该用哪个程序来执行脚本。

脚本编写规范

#!/bin/bash
# ============================================
# 脚本名称:hello.sh
# 脚本功能:打印欢迎信息
# 作    者:张三
# 创建日期:2024-01-01
# ============================================

# 这是注释,用来说明代码的作用
echo "Hello, World!"    # 行尾注释也可以

3 脚本的执行方式

四种执行方式对比

# 方式一:用 sh 执行(不识别bash特有语法,如 [[ ]] 和数组)
sh script.sh

# 方式二:用 bash 执行(推荐,明确使用bash解释器)
bash script.sh

# 方式三:用 ./ 执行(需要脚本有执行权限 + shebang)
chmod +x script.sh      # 先给执行权限
./script.sh             # 然后执行

# 方式四:用 source 执行(在当前Shell环境中执行,不是子Shell)
source script.sh        # 或 . script.sh

关键区别

执行方式子Shell?需要权限?影响当前环境?
sh script.sh不需要不影响
bash script.sh不需要不影响
./script.sh需要+x不影响
source script.sh不需要会影响

互动提问:如果你在脚本里用 export MY_VAR=hello,用 bash script.shsource script.sh 分别执行后,在当前终端能访问 $MY_VAR 吗?

答案:bash script.sh 在子Shell中执行,export 的变量只存在于子Shell,当前终端无法访问source script.sh 在当前Shell执行,当前终端可以访问 $MY_VAR


4 变量

生活类比

变量就像一个"贴了标签的盒子"——你给盒子起个名字(变量名),往里面放东西(赋值),以后用名字就能取出来用(引用)。

自定义变量

#!/bin/bash

# 定义变量(等号两边不能有空格!)
name="张三"
age=18
school="清华大学"

# 引用变量(用 $ 符号)
echo "姓名:$name"
echo "年龄:${age}岁"          # 用花括号明确变量边界
echo "学校:$school"

# 拼接字符串
greeting="你好,${name}同学!欢迎来到${school}。"
echo "$greeting"

易错点name = "张三" 会报错!Shell中赋值等号两边不能有空格name = "张三" 会被解释为执行命令 name,参数是 ="张三"

系统变量(预定义变量)

#!/bin/bash

echo "脚本名称:$0"            # myscript.sh
echo "第一个参数:$1"          # 第一个位置参数
echo "所有参数:$@"            # 所有位置参数
echo "参数个数:$#"            # 位置参数的个数
echo "上个命令退出码:$?"      # 0=成功,非0=失败
echo "当前进程PID:$$"         # 当前脚本的进程ID
echo "后台进程PID:$!"         # 最近一个后台进程的PID

只读变量

readonly PI=3.14159
PI=3.14    # 报错!不能修改只读变量

readonly CONFIG_FILE="/etc/myapp.conf"
# 此后 CONFIG_FILE 不可更改,防止脚本运行中意外修改关键路径

read——从键盘读取输入

#!/bin/bash

# 基本读取
echo "请输入你的名字:"
read name
echo "你好,$name!"

# 带提示符读取(-p)
read -p "请输入你的年龄:" age
echo "你今年${age}岁"

# 静默读取(-s,输入不回显,适合密码)
read -s -p "请输入密码:" password
echo ""    # 输入完补一个换行
echo "密码已收到(不告诉你是什么)"

# 限时读取(-t,超时秒数)
read -t 10 -p "你有10秒时间输入答案:" answer
if [ -z "$answer" ]; then
    echo "超时了!"
fi

# 读取多个值
read -p "请输入姓名和年龄(空格分隔):" name age
echo "姓名:$name,年龄:$age"

变量的作用域

#!/bin/bash

# 局部变量(只在当前脚本/函数中有效)
local_var="我是局部的"

# 环境变量(export后,子进程也能用)
export GLOBAL_VAR="我是全局的"

# 演示
bash child.sh    # child.sh 中可以用 $GLOBAL_VAR,但用不到 $local_var

5 引号的区别

三种引号对比

name="张三"
age=18

# 1. 单引号 ''——完全原样输出,变量不展开
echo '你好 $name'
# 输出:你好 $name          ($name没有被替换)

# 2. 双引号 ""——变量会被展开替换
echo "你好 $name"
# 输出:你好 张三           ($name被替换为值)

# 3. 反引号 `` 或 $()——执行命令,取输出结果
echo "今天是 `date`"
echo "今天是 $(date)"        # 推荐用 $(),可以嵌套
# 输出:今天是 Mon Jan  1 10:00:00 CST 2024

对比表

引号类型变量替换?命令替换?转义字符?用途
单引号 ''不替换不替换不转义原样输出、正则表达式
双引号 ""替换替换转义包含变量的字符串
反引号 ``执行命令获取命令输出(推荐用 $()

互动提问:以下代码输出什么?

name="world"
echo '$name'
echo "$name"
echo "$name says hello"

答案:第一行 $name,第二行 world,第三行 world says hello

$() 的嵌套优势

# 反引号嵌套很痛苦
files=`ls -l \`find . -name "*.txt"\``

# $() 嵌套很清晰
files=$(ls -l $(find . -name "*.txt"))

6 算术运算

四种运算方式

#!/bin/bash

a=10
b=3

# 方式一:$[] (较旧,但仍可用)
echo "a+b = $[a+b]"         # 13
echo "a-b = $[a-b]"         # 7
echo "a*b = $[a*b]"         # 30
echo "a/b = $[a/b]"         # 3(整数除法)
echo "a%b = $[a%b]"         # 1(取余)

# 方式二:$(()) (推荐)
echo "a+b = $((a+b))"       # 13
echo "a-b = $((a-b))"       # 7
echo "a*b = $((a*b))"       # 30
echo "a/b = $((a/b))"       # 3
echo "a%b = $((a%b))"       # 1
echo "a**b = $((a**b))"     # 1000(幂运算)

# 方式三:expr(需要空格,*要转义)
echo "a+b = $(expr $a + $b)"
echo "a*b = $(expr $a \* $b)"     # * 需要 \ 转义

# 方式四:bc(支持小数运算)
echo "scale=2; 10/3" | bc         # 3.33
echo "$a + $b" | bc               # 13
echo "scale=4; 22/7" | bc         # 3.1428

整数 vs 小数

# Shell内置只支持整数运算
echo $((10/3))                # 输出 3(不是3.33)

# 需要小数必须借助 bc 或 awk
echo "scale=2; 10/3" | bc     # 输出 3.33
awk 'BEGIN{printf "%.2f\n", 10/3}'  # 输出 3.33

互动提问echo $((7/2))echo "7/2" | bc 的结果一样吗?

答案:不一样$((7/2)) 输出 3(整数除法),echo "7/2" | bc 也输出 3(bc默认也是整数)。但 echo "scale=1; 7/2" | bc 输出 3.5


7 test 判断——条件测试

生活类比

test 就像"问问题":这个文件存在吗?这个变量是空的吗?这个数字大于10吗?根据回答(真/假)来决定下一步做什么。

文件测试

#!/bin/bash

file="/etc/passwd"
dir="/var/log"

# 使用 test 命令
test -f "$file" && echo "$file 是普通文件"
test -d "$dir" && echo "$dir 是目录"

# 使用 [ ] (等价于test,注意空格!)
[ -f "$file" ] && echo "文件存在"
[ -d "$dir" ] && echo "目录存在"

# 使用 [[ ]] (bash扩展,功能更强大)
[[ -f "$file" ]] && echo "文件存在"
测试操作符含义示例
-f是普通文件?[ -f /etc/passwd ]
-d是目录?[ -d /var/log ]
-e文件/目录存在?[ -e /tmp/test ]
-r可读?[ -r /etc/shadow ]
-w可写?[ -w /tmp/test ]
-x可执行?[ -x /usr/bin/ls ]
-s文件大小不为0?[ -s file.txt ]
-L是符号链接?[ -L /usr/bin/python ]

整数比较

a=10
b=20

[ $a -eq $b ]    # 等于(equal)
[ $a -ne $b ]    # 不等于(not equal)
[ $a -gt $b ]    # 大于(greater than)
[ $a -ge $b ]    # 大于等于(greater or equal)
[ $a -lt $b ]    # 小于(less than)
[ $a -le $b ]    # 小于等于(less or equal)

注意:整数比较必须用 -eq-gt 这些操作符,不能用 ==>> 在Shell中是重定向符号!

字符串测试

name="hello"

[ -z "$name" ]     # 字符串为空?(zero length)
[ -n "$name" ]     # 字符串不为空?(non-zero length)
[ "$name" = "hello" ]    # 字符串相等(注意空格和引号)
[ "$name" != "world" ]   # 字符串不等

# [[ ]] 中可以使用正则和模式匹配
[[ "$name" == h* ]]      # 以h开头?
[[ "$name" =~ ^he ]]     # 正则匹配

逻辑组合

# 与(AND)
[ -f /etc/passwd ] && [ -r /etc/passwd ]   # 文件存在且可读
[ -f /etc/passwd -a -r /etc/passwd ]       # 同上(-a = AND)
[[ -f /etc/passwd && -r /etc/passwd ]]     # [[ ]] 中用 &&

# 或(OR)
[ -f /tmp/a ] || [ -f /tmp/b ]             # a或b存在
[ -f /tmp/a -o -f /tmp/b ]                 # 同上(-o = OR)
[[ -f /tmp/a || -f /tmp/b ]]              # [[ ]] 中用 ||

# 非(NOT)
[ ! -f /etc/passwd ]                       # 文件不存在
[[ ! -f /etc/passwd ]]

实用示例

#!/bin/bash

# 检查文件是否存在,不存在则创建
config="/etc/myapp.conf"
if [ ! -f "$config" ]; then
    echo "配置文件不存在,正在创建..."
    touch "$config"
    echo "# MyApp Configuration" > "$config"
    echo "配置文件已创建:$config"
fi

# 检查目录是否有写权限
if [ -d /data && -w /data ]; then
    echo "/data 目录可写"
fi

8 if / elif / else 条件判断

基本语法

#!/bin/bash

# 单分支
if [ 条件 ]; then
    命令
fi

# 双分支
if [ 条件 ]; then
    条件为真时执行
else
    条件为假时执行
fi

# 多分支
if [ 条件1 ]; then
    条件1为真时执行
elif [ 条件2 ]; then
    条件2为真时执行
elif [ 条件3 ]; then
    条件3为真时执行
else
    以上条件都不满足时执行
fi

实战示例

#!/bin/bash
# score.sh - 成绩评级程序

read -p "请输入分数(0-100):" score

# 输入验证
if ! [[ "$score" =~ ^[0-9]+$ ]]; then
    echo "错误:请输入一个数字!"
    exit 1
fi

if [ "$score" -gt 100 ] || [ "$score" -lt 0 ]; then
    echo "错误:分数必须在0-100之间!"
    exit 1
elif [ "$score" -ge 90 ]; then
    echo "优秀!A等级"
elif [ "$score" -ge 80 ]; then
    echo "良好!B等级"
elif [ "$score" -ge 70 ]; then
    echo "中等!C等级"
elif [ "$score" -ge 60 ]; then
    echo "及格!D等级"
else
    echo "不及格!F等级"
fi

case语句(多值判断的替代方案)

#!/bin/bash

read -p "请输入选项(start/stop/restart/status):" action

case "$action" in
    start)
        echo "正在启动服务..."
        ;;
    stop)
        echo "正在停止服务..."
        ;;
    restart)
        echo "正在重启服务..."
        ;;
    status)
        echo "服务正在运行"
        ;;
    *)
        echo "用法:$0 {start|stop|restart|status}"
        exit 1
        ;;
esac

for循环

#!/bin/bash

# 遍历列表
for name in Alice Bob Charlie; do
    echo "你好,$name!"
done

# C风格循环
for ((i=1; i<=5; i++)); do
    echo "第 $i 次循环"
done

# 遍历文件
for file in /etc/*.conf; do
    echo "配置文件:$file"
done

while循环

#!/bin/bash

# 计数器循环
count=1
while [ $count -le 5 ]; do
    echo "第 $count 次"
    count=$((count+1))
done

# 读取文件每一行
while read line; do
    echo "行内容:$line"
done < /etc/passwd

# 无限循环(守护进程常用)
while true; do
    echo "检查服务状态..."
    sleep 60    # 每60秒检查一次
done

9 位置参数——脚本的"输入参数"

生活类比

位置参数就像函数的"参数"——执行脚本时可以传入数据,脚本内部通过 $1$2 等来引用。

#!/bin/bash
# greet.sh

echo "脚本名称:$0"         # greet.sh
echo "第一个参数:$1"       # Alice
echo "第二个参数:$2"       # 20
echo "第三个参数:$3"       # 北京
echo "所有参数:$@"         # Alice 20 北京
echo "所有参数:$*"         # Alice 20 北京(作为单个字符串)
echo "参数个数:$#"         # 3

执行:

bash greet.sh Alice 20 北京

@ 和 * 的区别

#!/bin/bash

# "$@" 把每个参数当作独立的字符串(推荐)
for arg in "$@"; do
    echo "参数:[$arg]"
done
# 执行 bash test.sh "hello world" foo
# 输出:参数:[hello world]
#       参数:[foo]

# "$*" 把所有参数当作一个字符串
for arg in "$*"; do
    echo "参数:[$arg]"
done
# 输出:参数:[hello world foo]    (合并为一个字符串)

最佳实践:绝大多数情况下使用 "$@" 而不是 "$*""$@" 保留了参数的原始边界,不会因为参数中含空格而出错。

参数默认值

#!/bin/bash

# 如果 $1 未传入或为空,使用默认值
name=${1:-"匿名用户"}
port=${2:-8080}
logdir=${3:-"/var/log"}

echo "用户名:$name"
echo "端口号:$port"
echo "日志目录:$logdir"
语法含义
${var:-default}var为空/未设置时,使用default
${var:=default}var为空/未设置时,使用default并赋值给var
${var:+value}var不为空时,使用value;为空时返回空
${var:?message}var为空时,输出message并退出脚本

10 环境变量

常用环境变量

echo $PATH        # 命令搜索路径
echo $HOME        # 当前用户主目录
echo $USER        # 当前用户名
echo $SHELL       # 当前Shell
echo $PWD         # 当前工作目录
echo $HOSTNAME    # 主机名
echo $LANG        # 语言环境
echo $PS1         # 命令提示符格式
echo $RANDOM      # 随机数(0-32767)

PATH——命令搜索路径

# 查看当前PATH
echo $PATH
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

# 临时添加(当前终端有效)
export PATH=$PATH:/opt/myapp/bin

# 永久添加(写入 ~/.bashrc 或 /etc/profile)
echo 'export PATH=$PATH:/opt/myapp/bin' >> ~/.bashrc
source ~/.bashrc    # 立即生效

PS1——命令提示符

# 默认提示符
echo $PS1
# [\u@\h \W]\$     →  root@server ~#

# 自定义提示符
export PS1='[\u@\h \w]\$ '    # \w显示完整路径
export PS1='[\u@\h \W \$?]\$ ' # 显示上条命令的退出码
转义符含义示例
\u用户名root
\h主机名server01
\w完整工作目录/var/log/nginx
\W当前目录名nginx
\d日期Mon Jan 01
\t时间14:30:00
\$提示符(root为#,普通用户为$)# 或 $

export——导出环境变量

#!/bin/bash

# 定义局部变量
LOCAL_VAR="只在当前Shell有效"

# 导出为环境变量
export GLOBAL_VAR="子Shell也能访问"

# 验证
bash -c 'echo $GLOBAL_VAR'    # 输出:子Shell也能访问
bash -c 'echo $LOCAL_VAR'     # 输出:(空)

11 函数

#!/bin/bash

# 定义函数
log_info() {
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] [INFO] $1"
}

log_error() {
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] [ERROR] $1" >&2    # 输出到stderr
}

# 调用函数
log_info "脚本开始执行"
log_error "发生了错误!"

# 带返回值的函数
add() {
    local result=$(($1 + $2))
    echo $result    # 通过echo返回结果
}

sum=$(add 10 20)
echo "10 + 20 = $sum"

12 实战脚本示例

脚本一:系统信息采集脚本

#!/bin/bash
# ============================================
# 脚本名称:sysinfo.sh
# 脚本功能:一键采集系统关键信息
# 用法:bash sysinfo.sh
# ============================================

# 分隔线
LINE="================================================"

echo "$LINE"
echo "           系统信息采集报告"
echo "           采集时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "$LINE"

# 1. 主机信息
echo ""
echo "【主机信息】"
echo "  主机名:$(hostname)"
echo "  内核版本:$(uname -r)"
echo "  系统版本:$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
echo "  运行时间:$(uptime -p 2>/dev/null || uptime)"

# 2. CPU信息
echo ""
echo "【CPU信息】"
echo "  CPU型号:$(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)"
echo "  CPU核心数:$(grep -c 'processor' /proc/cpuinfo)"
echo "  系统负载:$(cat /proc/loadavg | awk '{print $1, $2, $3}')"

# 3. 内存信息
echo ""
echo "【内存信息】"
free -h | awk '/Mem:/{printf "  总内存:%s  已用:%s  可用:%s\n", $2, $3, $7}'
free -h | awk '/Swap:/{printf "  总交换:%s  已用:%s  空闲:%s\n", $2, $3, $4}'

# 4. 磁盘信息
echo ""
echo "【磁盘使用】"
df -hT | grep -vE 'tmpfs|devtmpfs|overlay' | awk 'NR>1{printf "  %-20s 类型:%-6s 总量:%-6s 已用:%-6s 可用:%-6s 使用率:%s\n", $7, $2, $3, $4, $5, $6}'

# 5. 网络信息
echo ""
echo "【网络信息】"
echo "  IP地址:"
ip addr show 2>/dev/null | grep 'inet ' | grep -v '127.0.0.1' | awk '{print "    "$2" ("$NF")"}'
echo "  默认网关:$(ip route show default 2>/dev/null | awk '{print $3}' | head -1)"
echo "  DNS服务器:$(grep nameserver /etc/resolv.conf | head -2 | awk '{print $2}' | tr '\n' ', ' | sed 's/,$//')"

# 6. 登录用户
echo ""
echo "【当前登录用户】"
who | awk '{printf "  用户:%-10s 终端:%-8s 登录时间:%s %s\n", $1, $2, $3, $4}'

# 7. 进程统计
echo ""
echo "【进程统计】"
total_proc=$(ps aux | wc -l)
echo "  总进程数:$total_proc"
echo "  占用内存TOP5:"
ps aux --sort=-%mem | head -6 | awk 'NR>1{printf "    %-15s PID:%-8s CPU:%-5s MEM:%-5s %s\n", $1, $2, $3, $4, $11}'

echo ""
echo "$LINE"
echo "           报告采集完毕"
echo "$LINE"

脚本二:批量创建用户脚本

#!/bin/bash
# ============================================
# 脚本名称:batch_users.sh
# 脚本功能:批量创建用户并设置初始密码
# 用法:sudo bash batch_users.sh
# 用户列表:通过参数文件或交互输入指定
# ============================================

# 检查是否以root运行
if [ "$(id -u)" -ne 0 ]; then
    echo "错误:请使用root权限运行此脚本!"
    echo "用法:sudo bash $0"
    exit 1
fi

# 日志文件
LOG_FILE="/var/log/batch_users_$(date +%Y%m%d_%H%M%S).log"

log() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    echo "$msg"
    echo "$msg" >> "$LOG_FILE"
}

# 显示使用帮助
usage() {
    echo "用法:"
    echo "  1. 从文件读取用户名:sudo bash $0 -f users.txt"
    echo "  2. 交互式输入:       sudo bash $0 -i"
    echo "  3. 指定前缀和数量:   sudo bash $0 -p student -n 30"
    echo ""
    echo "文件格式(每行一个用户名):"
    echo "  alice"
    echo "  bob"
    echo "  charlie"
    exit 1
}

# 创建单个用户的函数
create_user() {
    local username=$1
    local password=$2

    # 检查用户是否已存在
    if id "$username" &>/dev/null; then
        log "跳过:用户 $username 已存在"
        return 1
    fi

    # 创建用户(带主目录)
    useradd -m -s /bin/bash "$username"

    if [ $? -ne 0 ]; then
        log "失败:无法创建用户 $username"
        return 1
    fi

    # 设置密码
    echo "${username}:${password}" | chpasswd

    # 设置密码过期(强制首次登录修改密码)
    # chage -d 0 "$username"    # 取消注释可启用

    log "成功:用户 $username 创建完毕,初始密码:$password"
    return 0
}

# 生成随机密码
gen_password() {
    local length=${1:-8}
    tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length"
}

# 主逻辑
SUCCESS=0
FAIL=0
SKIP=0

if [ "$1" = "-f" ] && [ -n "$2" ]; then
    # 模式一:从文件读取
    if [ ! -f "$2" ]; then
        echo "错误:文件 $2 不存在!"
        exit 1
    fi

    log "开始从文件 $2 批量创建用户..."
    while IFS= read -r username || [ -n "$username" ]; do
        # 跳过空行和注释
        [[ -z "$username" || "$username" =~ ^# ]] && continue
        # 去除空格
        username=$(echo "$username" | xargs)

        password=$(gen_password 8)
        if create_user "$username" "$password"; then
            SUCCESS=$((SUCCESS+1))
        else
            SKIP=$((SKIP+1))
        fi
    done < "$2"

elif [ "$1" = "-i" ]; then
    # 模式二:交互式输入
    log "交互式创建用户模式(输入 q 结束)"
    while true; do
        read -p "请输入用户名(q退出):" username
        [ "$username" = "q" ] && break
        [[ -z "$username" ]] && continue

        password=$(gen_password 8)
        if create_user "$username" "$password"; then
            SUCCESS=$((SUCCESS+1))
        else
            SKIP=$((SKIP+1))
        fi
    done

elif [ "$1" = "-p" ] && [ -n "$2" ] && [ "$3" = "-n" ] && [ -n "$4" ]; then
    # 模式三:前缀+数量
    prefix=$2
    count=$4
    log "批量创建用户:前缀=$prefix,数量=$count"

    for i in $(seq 1 "$count"); do
        username="${prefix}$(printf '%02d' $i)"
        password=$(gen_password 8)
        if create_user "$username" "$password"; then
            SUCCESS=$((SUCCESS+1))
        else
            SKIP=$((SKIP+1))
        fi
    done

else
    usage
fi

# 输出统计
echo ""
echo "========================================"
echo "  创建完毕!"
echo "  成功:${SUCCESS} 个用户"
echo "  跳过:${SKIP} 个用户"
echo "  日志:$LOG_FILE"
echo "========================================"

使用方法:

# 方式一:从文件批量创建
cat > users.txt << 'EOF'
alice
bob
charlie
david
EOF
sudo bash batch_users.sh -f users.txt

# 方式二:交互式创建
sudo bash batch_users.sh -i

# 方式三:按前缀和数量创建(如 student01 到 student30)
sudo bash batch_users.sh -p student -n 30

13 脚本调试技巧

# 开启调试模式(显示每条执行的命令)
bash -x script.sh

# 只检查语法不执行
bash -n script.sh

# 在脚本内部局部开启调试
set -x          # 开启调试
# ... 需要调试的代码 ...
set +x          # 关闭调试

# 遇到错误立即退出
set -e          # 任何命令返回非0就退出脚本
set -euo pipefail  # 更严格:未定义变量报错、管道失败也退出

互动提问set -eset -x 分别用于什么场景?

答案:set -e 用于"严格模式"——任何命令失败就立即停止脚本,避免错误级联;set -x 用于"调试模式"——打印每条命令及其执行结果,方便排查问题。


运维常用命令精选

# ===== 脚本执行 =====
bash script.sh                   # 用bash执行脚本
bash -x script.sh                # 调试模式执行
bash -n script.sh                # 只检查语法
chmod +x script.sh && ./script.sh  # 赋权后执行
source script.sh                 # 在当前Shell中执行

# ===== 变量操作 =====
echo $PATH                       # 查看命令搜索路径
echo $HOME                       # 查看主目录
export MY_VAR="hello"            # 导出环境变量
env                              # 查看所有环境变量

# ===== 文本处理(脚本常用) =====
grep "pattern" file              # 搜索文本
awk '{print $1}' file            # 按列处理
sed 's/old/new/g' file           # 替换文本
cut -d: -f1 /etc/passwd          # 按分隔符切割
sort file                        # 排序
uniq                             # 去重
wc -l file                       # 统计行数

# ===== 脚本中常用的命令组合 =====
$(command)                       # 命令替换(取命令的输出)
${var:-default}                  # 变量为空时使用默认值
${var%suffix}                    # 删除变量末尾的后缀
${var#prefix}                    # 删除变量开头的前缀
$((a+b))                         # 算术运算

趣味命令

# 1. 用Shell画一个进度条
for i in $(seq 1 100); do
    printf "\r[%-50s] %d%%" "$(printf '#%.0s' $(seq 1 $((i/2))))" "$i"
    sleep 0.05
done
echo ""

# 2. 实时监控脚本执行过程(watch命令)
watch -n 1 'df -h | head -5'    # 每秒刷新磁盘使用情况
watch -n 2 'ps aux --sort=-%cpu | head -6'  # 实时监控CPU占用

# 3. 用Shell生成随机密码
tr -dc 'A-Za-z0-9!@#' < /dev/urandom | head -c 16; echo

# 4. 一行命令实现倒计时
for i in $(seq 10 -1 1); do echo -ne "\r倒计时:$i 秒"; sleep 1; done; echo -e "\r时间到!       "

# 5. 用Shell画一颗圣诞树(圣诞节用!)
for i in $(seq 1 2 20); do
    printf "%*s" $((10-i/2)) ""
    printf "*%.0s" $(seq 1 $i)
    echo ""
done
printf "%9s\n" "*"
printf "%9s\n" "***"

# 6. 显示当前终端的彩色banner
figlet "Hello Shell" 2>/dev/null || echo "安装figlet可以显示ASCII艺术字:yum install figlet"

# 7. 用Shell统计代码行数
find . -name "*.sh" -exec wc -l {} + | tail -1

# 8. 让终端"说话"(如果安装了espeak)
# echo "Hello World" | espeak

小结:Shell脚本是运维自动化的基石。记住三个要点:(1) 第一行写 #!/bin/bash;(2) 变量赋值等号两边不能有空格;(3) 条件判断用 [ ][[ ]]方括号内侧必须有空格。写脚本时养成加注释、写日志的好习惯,三个月后的你会感谢现在的自己。