一、Shell 是什么?
简单理解:
- Shell 是 Linux 的"命令解释器",你敲的命令它帮你传给系统执行
- Shell 脚本 是把一堆命令写到文件里,让它自动按顺序跑
常见的 Shell 类型:
bash
# 查看当前系统用的什么 Shell
echo $SHELL
# 输出:/bin/bash
# 查看系统有哪些 Shell
cat /etc/shells
目前最常用的是 Bash(Bourne Again Shell),大多数 Linux 发行版默认就是它。
二、第一个脚本
创建一个文件 hello.sh:
bash
#!/bin/bash
# 这是我的第一个 Shell 脚本
echo "Hello, World!"
echo "当前时间是: $(date)"
echo "当前用户是: $(whoami)"
运行方式:
bash
# 方式一:给执行权限
chmod +x hello.sh
./hello.sh
# 方式二:用 bash 直接执行
bash hello.sh
注意 :#!/bin/bash 叫做 Shebang,告诉系统用哪个解释器来跑这个脚本。写脚本第一行最好都加上。
三、变量
定义变量
bash
# 定义变量(等号两边不能有空格!)
name="Linux"
version=22
# 使用变量
echo "学习 $name, 版本 $version"
echo "学习 ${name}, 版本 ${version}" # 推荐用花括号,更清晰
踩坑点 :等号两边不能加空格,name = "Linux" 会报错。
变量类型
bash
# 字符串
str="hello"
# 字符串拼接
greeting="Hello, ${str}!"
echo $greeting
# 字符串长度
echo ${#str}
# 只读变量
readonly PI=3.14
# PI=3.15 # 这行会报错,只读变量不能修改
# 删除变量(只读变量不能删除)
unset str
环境变量 vs 局部变量
bash
# 局部变量:只在当前 Shell 有效
local_var="我在局部"
# 环境变量:子进程也能访问
export GLOBAL_VAR="我是全局的"
# 查看所有环境变量
env
# 或
printenv
四、字符串操作
bash
str="Hello Shell World"
# 单引号:原样输出,不解析变量
echo 'Hello $str' # 输出: Hello $str
# 双引号:会解析变量
echo "Hello $str" # 输出: Hello Hello Shell World
# 字符串切片
echo ${str:0:5} # 输出: Hello(从位置0开始,取5个字符)
echo ${str:6} # 输出: Shell World(从位置6到结尾)
# 字符串替换
echo ${str/Shell/Bash} # 输出: Hello Bash World(替换第一个匹配)
echo ${str//l/L} # 输出: HeLLo SheLL WorLd(替换所有匹配)
# 字符串长度
echo ${#str} # 输出: 17
五、数组
bash
# 定义数组
fruits=("apple" "banana" "cherry" "date")
# 访问单个元素(下标从0开始)
echo ${fruits[0]} # 输出: apple
echo ${fruits[2]} # 输出: cherry
# 所有元素
echo ${fruits[@]} # 输出: apple banana cherry date
# 数组长度
echo ${#fruits[@]} # 输出: 4
# 添加元素
fruits[4]="elderberry"
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
六、条件判断
if 语句
bash
#!/bin/bash
age=20
if [ $age -ge 18 ]; then
echo "成年人"
elif [ $age -ge 12 ]; then
echo "青少年"
else
echo "儿童"
fi
注意 :[ 后面和 ] 前面都要有空格,这是最容易忘的地方。
比较运算符
bash
# 数值比较
# -eq 等于
# -ne 不等于
# -gt 大于
# -ge 大于等于
# -lt 小于
# -le 小于等于
a=10
b=20
if [ $a -eq $b ]; then
echo "相等"
fi
if [ $a -lt $b ]; then
echo "a 小于 b"
fi
字符串比较
bash
str1="hello"
str2="world"
# = 判断是否相等
# != 判断是否不等
# -z 判断字符串是否为空
# -n 判断字符串是否非空
if [ "$str1" = "$str2" ]; then
echo "字符串相等"
fi
if [ -z "$str1" ]; then
echo "字符串为空"
fi
文件判断
bash
# -f 是否为普通文件
# -d 是否为目录
# -e 是否存在
# -r 是否可读
# -w 是否可写
# -x 是否可执行
# -s 文件是否非空
file="/etc/passwd"
if [ -f "$file" ]; then
echo "$file 是一个普通文件"
fi
if [ -r "$file" ]; then
echo "$file 可读"
fi
逻辑运算
bash
age=25
score=85
# && 逻辑与
if [ $age -ge 18 ] && [ $score -ge 60 ]; then
echo "成年人且及格"
fi
# || 逻辑或
if [ $age -lt 18 ] || [ $score -ge 90 ]; then
echo "未成年或优秀"
fi
# ! 逻辑非
if [ ! $age -lt 18 ]; then
echo "不是未成年人"
fi
七、循环
for 循环
bash
# 遍历列表
for fruit in apple banana cherry; do
echo "水果: $fruit"
done
# C风格for循环
for ((i=1; i<=5; i++)); do
echo "第 $i 次"
done
# 遍历文件
for file in /tmp/*.log; do
echo "日志文件: $file"
done
# 遍历数字序列
for i in {1..5}; do
echo "数字: $i"
done
# 步长
for i in {0..10..2}; do
echo "偶数: $i" # 输出 0 2 4 6 8 10
done
while 循环
bash
# 基础while
count=1
while [ $count -le 5 ]; do
echo "第 $count 次"
count=$((count + 1))
done
# 读取文件每一行
while IFS= read -r line; do
echo "内容: $line"
done < /etc/passwd
# 无限循环
while true; do
echo "按 Ctrl+C 退出"
sleep 1
done
until 循环
bash
# until:条件为假时执行,条件为真时停止(和while相反)
count=1
until [ $count -gt 5 ]; do
echo "第 $count 次"
count=$((count + 1))
done
break 和 continue
bash
# break:跳出整个循环
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
# 输出 1 2 3 4
# continue:跳过本次循环
for i in {1..5}; do
if [ $i -eq 3 ]; then
continue
fi
echo $i
done
# 输出 1 2 4 5
八、函数
定义和调用
bash
# 方式一
greet() {
echo "Hello, $1!"
}
# 方式二
function say_hi {
echo "Hi, $1!"
}
# 调用函数
greet "Linux" # 输出: Hello, Linux!
say_hi "Shell" # 输出: Hi, Shell!
参数和返回值
bash
# 函数参数
add() {
local sum=$(($1 + $2)) # local 定义局部变量
return $sum
}
# 调用并获取返回值
add 3 5
echo "3 + 5 = $?" # $? 获取上一个命令的返回值
# 更推荐用 echo 输出,用 $() 获取
add() {
echo $(($1 + $2))
}
result=$(add 3 5)
echo "3 + 5 = $result"
实用函数示例
bash
# 检查命令是否存在
check_command() {
if command -v $1 &> /dev/null; then
echo "$1 已安装"
return 0
else
echo "$1 未安装"
return 1
fi
}
check_command docker
check_command kubectl
# 带颜色的输出
red() {
echo -e "\033[31m$1\033[0m"
}
green() {
echo -e "\033[32m$1\033[0m"
}
yellow() {
echo -e "\033[33m$1\033[0m"
}
green "操作成功"
red "操作失败"
yellow "警告信息"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log "开始执行任务"
log "任务完成"
九、输入输出
读取用户输入
bash
# 基础读取
echo "请输入你的名字:"
read name
echo "你好, $name!"
# 带提示的读取
read -p "请输入年龄: " age
echo "你 $age 岁了"
# 静默输入(比如密码)
read -sp "请输入密码: " password
echo
echo "密码长度: ${#password}"
# 读取多个变量
read -p "输入姓名和年龄: " name age
echo "$name 今年 $age 岁"
输出重定向
bash
# 标准输出重定向
echo "hello" > output.txt # 覆盖写入
echo "world" >> output.txt # 追加写入
# 错误输出重定向
ls /nonexistent 2> error.log
# 同时重定向标准输出和错误输出
command > output.txt 2>&1
# 或者更简洁
command &> output.txt
# 丢弃输出
command > /dev/null 2>&1
管道
bash
# 管道:把前一个命令的输出作为后一个命令的输入
ps aux | grep nginx
cat /etc/passwd | wc -l
ls -la | sort -k5 -n # 按文件大小排序
# 多级管道
ps aux | grep nginx | grep -v grep | awk '{print $2}'
十、实战脚本
脚本一:系统信息查看
bash
#!/bin/bash
echo "========== 系统信息 =========="
echo "主机名: $(hostname)"
echo "系统版本: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
echo "内核版本: $(uname -r)"
echo "CPU: $(grep 'model name' /proc/cpuinfo | head -1 | cut -d':' -f2)"
echo "内存: $(free -h | grep Mem | awk '{print $2}')"
echo "磁盘使用: $(df -h / | tail -1 | awk '{print $5}')"
echo "当前用户: $(whoami)"
echo "系统运行时间: $(uptime -p)"
echo "=============================="
脚本二:批量创建用户
bash
#!/bin/bash
# 从文件读取用户名,批量创建
USER_FILE="users.txt"
if [ ! -f "$USER_FILE" ]; then
echo "错误: $USER_FILE 不存在"
exit 1
fi
while IFS= read -r username; do
if [ -z "$username" ]; then
continue
fi
if id "$username" &>/dev/null; then
echo "用户 $username 已存在,跳过"
else
useradd -m "$username"
echo "用户 $username 创建成功"
fi
done < "$USER_FILE"
脚本三:日志清理
bash
#!/bin/bash
# 清理指定天数前的日志文件
LOG_DIR="/var/log/myapp"
DAYS=7
if [ ! -d "$LOG_DIR" ]; then
echo "目录 $LOG_DIR 不存在"
exit 1
fi
echo "清理 $DAYS 天前的日志..."
find "$LOG_DIR" -name "*.log" -type f -mtime +$DAYS -exec rm -f {} \;
echo "清理完成"
脚本四:服务健康检查
bash
#!/bin/bash
# 检查服务是否运行
check_service() {
local service_name=$1
if systemctl is-active --quiet "$service_name"; then
echo "[✓] $service_name 运行中"
return 0
else
echo "[✗] $service_name 未运行"
return 1
fi
}
# 检查端口是否监听
check_port() {
local port=$1
local service_name=$2
if ss -tlnp | grep -q ":$port"; then
echo "[✓] $service_name (端口 $port) 监听中"
return 0
else
echo "[✗] $service_name (端口 $port) 未监听"
return 1
fi
}
echo "========== 服务状态检查 =========="
check_service nginx
check_service mysql
check_service redis
check_port 80 "HTTP"
check_port 443 "HTTPS"
check_port 3306 "MySQL"
check_port 6379 "Redis"
echo "=================================="
脚本五:自动备份
bash
#!/bin/bash
# 配置
BACKUP_DIR="/backup"
SOURCE_DIR="/var/www/html"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
KEEP_DAYS=30
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 执行备份
echo "开始备份 $SOURCE_DIR ..."
tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" "$SOURCE_DIR"
if [ $? -eq 0 ]; then
echo "备份成功: ${BACKUP_FILE}"
else
echo "备份失败"
exit 1
fi
# 清理旧备份
echo "清理 ${KEEP_DAYS} 天前的备份..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$KEEP_DAYS -delete
echo "备份完成"
十一、常用技巧
脚本调试
bash
# 方式一:使用 -x 参数(显示执行的每一行)
bash -x script.sh
# 方式二:在脚本中开启调试
#!/bin/bash
set -x # 开启调试
# ... 脚本内容 ...
set +x # 关闭调试
# 方式三:使用 -e 参数(遇到错误立即退出)
#!/bin/bash
set -e # 遇到错误退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任何命令失败则整体失败
常用内置变量
bash
$0 # 脚本名称
$1-$9 # 第1-9个参数
$# # 参数个数
$@ # 所有参数(作为独立字符串)
$* # 所有参数(作为单个字符串)
$$ # 当前脚本的PID
$? # 上一个命令的返回值
$! # 后台运行的最后一个进程的PID
参数处理
bash
#!/bin/bash
# 简单的参数处理
if [ $# -lt 1 ]; then
echo "用法: $0 <参数>"
exit 1
fi
echo "参数1: $1"
echo "参数个数: $#"
echo "所有参数: $@"
# 遍历所有参数
for arg in "$@"; do
echo "参数: $arg"
done
条件表达式简写
bash
# 传统写法
if [ -f "/etc/passwd" ]; then
echo "文件存在"
fi
# 简写(双括号更现代)
if [[ -f "/etc/passwd" ]]; then
echo "文件存在"
fi
# 三元运算风格
[[ -f "/etc/passwd" ]] && echo "存在" || echo "不存在"
# 默认值
name=${1:-"默认名称"}
echo "名字: $name"
十二、学习心得
今天学完 Shell 脚本,有几个感受:
-
Shell 不难,但细节很多 :比如
[后面要有空格,变量赋值不能有空格,这些小细节不注意就会报错 -
实践比看书重要:看文档觉得自己会了,一写就出错。今天写了十几个小脚本,踩了不少坑,但印象很深
-
Shell 是运维的基本功:之前觉得会敲命令就行,现在发现,能把重复性工作写成脚本,才是真正的效率提升
-
学会看报错信息:Shell 的报错信息有时候很不直观,但仔细看 usually 能找到问题所在