Shell脚本基础
Shell脚本基础
导读:如果说Linux命令是"一块块砖头",那Shell脚本就是把这些砖头砌成"一面墙"的砂浆。学会Shell脚本,你就能把几十条命令组合成一个自动化程序,让重复性工作一键完成。本章从零基础讲起,带你写出自己的第一个脚本。
1 什么是Shell?
生活类比
Shell就像你和Linux内核之间的"翻译官"。你不可能直接跟内核说机器语言,而是通过Shell把你的命令翻译成内核能理解的指令。就像你不会直接操作厨房的灶台电路,而是通过"菜谱"(命令)告诉"厨师"(Shell)来做菜。
sh / bash / zsh 的区别
| Shell | 全称 | 特点 | 适用场景 |
|---|---|---|---|
| sh | Bourne Shell | 最原始的Shell,功能最基础 | 编写需要最大兼容性的脚本 |
| bash | Bourne Again Shell | sh的增强版,Linux默认Shell | 最常用,绝大多数服务器默认 |
| zsh | Z 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.sh和source 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 -e和set -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) 条件判断用[ ]或[[ ]],方括号内侧必须有空格。写脚本时养成加注释、写日志的好习惯,三个月后的你会感谢现在的自己。