01-Shell基础
Shell是Linux/Unix的命令解释器,既是用户界面也是编程语言。Shell脚本是运维自动化的核心工具。
Shell简介
常见Shell类型
# 查看可用Shell
cat /etc/shells
# 常见Shell
/bin/sh # Bourne Shell(POSIX标准)
/bin/bash # Bourne Again Shell(最流行)
/bin/zsh # Z Shell(macOS默认)
/bin/fish # Friendly Interactive Shell
/bin/dash # Debian Almquist Shell(轻量)
# 查看当前Shell
echo $SHELL
# 查看脚本使用的Shell
ps -p $$
Bash vs Zsh
特性 |
Bash |
Zsh |
|---|---|---|
兼容性 |
POSIX兼容,广泛支持 |
Bash兼容+增强 |
补全 |
基础补全 |
智能补全,路径展开 |
主题 |
无内置 |
Oh My Zsh丰富主题 |
性能 |
启动快 |
稍慢但功能强 |
默认系统 |
大部分Linux |
macOS 10.15+ |
第一个Shell脚本
基本结构
#!/bin/bash
# 这是注释
# Shebang指定解释器
echo "Hello, Shell!"
# 执行权限
chmod +x script.sh
./script.sh
# 或直接用bash执行
bash script.sh
Shebang详解
#!/bin/bash # 使用bash
#!/bin/sh # 使用sh(可能是bash或dash)
#!/usr/bin/env bash # 推荐:自动查找bash路径
#!/usr/bin/env python3 # Python脚本
变量
变量定义和使用
# 定义变量(=两边无空格)
name="Alice"
age=25
readonly PI=3.14159 # 只读变量
# 使用变量
echo $name
echo ${name} # 推荐:明确边界
echo "Hello, $name"
echo 'Hello, $name' # 单引号不解析变量
# 变量拼接
fullname="${name} Smith"
# 删除变量
unset name
特殊变量
$0 # 脚本名
$1-$9 # 第1-9个参数
${10} # 第10个及以上参数
$# # 参数个数
$@ # 所有参数(分开)
$* # 所有参数(合并)
$? # 上一命令退出状态(0=成功)
$$ # 当前Shell的PID
$! # 最后一个后台进程的PID
# 示例
#!/bin/bash
echo "脚本名: $0"
echo "第一个参数: $1"
echo "参数个数: $#"
echo "所有参数: $@"
echo "退出状态: $?"
# 执行:./script.sh arg1 arg2
变量作用域
# 局部变量(当前Shell)
name="local"
# 环境变量(子进程可见)
export PATH=$PATH:/usr/local/bin
# 函数内局部变量
function test() {
local var="local" # 仅函数内可见
global="global" # 全局
}
命令替换
# 反引号(旧)
result=`date`
# $() 推荐
result=$(date)
count=$(ls | wc -l)
# 示例
today=$(date +%Y-%m-%d)
echo "Today is $today"
数据类型
字符串
# 单引号:原样输出
str='Hello $USER' # 输出:Hello $USER
# 双引号:解析变量
str="Hello $USER" # 输出:Hello alice
# 字符串长度
echo ${#str}
# 字符串截取
str="Hello World"
echo ${str:0:5} # Hello(从0开始,长度5)
echo ${str:6} # World(从6到末尾)
# 字符串替换
echo ${str/World/Shell} # 替换第一个
echo ${str//o/O} # 替换所有
# 字符串删除
file="script.sh.bak"
echo ${file%.bak} # script.sh(删除最短后缀)
echo ${file%.*} # script.sh(删除最短.后缀)
echo ${file%%.*} # script(删除最长.后缀)
echo ${file#script.} # sh.bak(删除最短前缀)
# 字符串拼接
str1="Hello"
str2="World"
result="$str1 $str2"
result="${str1}${str2}"
数组
# 定义数组
arr=(1 2 3 4 5)
arr[0]=10
# 访问元素
echo ${arr[0]} # 第一个元素
echo ${arr[@]} # 所有元素
echo ${arr[*]} # 所有元素(合并)
echo ${#arr[@]} # 数组长度
# 切片
echo ${arr[@]:1:3} # 从索引1开始,3个元素
# 添加元素
arr+=(6 7 8)
# 遍历数组
for item in "${arr[@]}"; do
echo $item
done
# 关联数组(Bash 4+)
declare -A map
map["name"]="Alice"
map["age"]=25
echo ${map["name"]}
echo ${!map[@]} # 所有键
echo ${map[@]} # 所有值
运算符
算术运算
# let命令
let a=5+3
let "a = 5 + 3"
# (()) 推荐
a=$((5 + 3))
((a++))
((a += 5))
# expr(旧)
a=`expr 5 + 3`
# 运算符
$((a + b)) # 加
$((a - b)) # 减
$((a * b)) # 乘
$((a / b)) # 除
$((a % b)) # 取模
$((a ** b)) # 幂
# 自增自减
((i++))
((i--))
((++i))
((--i))
# 示例
num=10
result=$((num * 2 + 5))
echo $result # 25
比较运算
# 整数比较
-eq # 等于
-ne # 不等于
-gt # 大于
-ge # 大于等于
-lt # 小于
-le # 小于等于
# 示例
if [ $a -eq $b ]; then
echo "equal"
fi
# (()) 支持 C 风格
if ((a == b)); then
echo "equal"
fi
# 字符串比较
= # 等于(或 ==)
!= # 不等于
-z # 字符串为空
-n # 字符串非空
< # 小于(需转义)
> # 大于(需转义)
# 示例
if [ "$str1" = "$str2" ]; then
echo "equal"
fi
if [ -z "$str" ]; then
echo "empty"
fi
# 文件测试
-e file # 文件存在
-f file # 常规文件
-d file # 目录
-r file # 可读
-w file # 可写
-x file # 可执行
-s file # 文件非空
-L file # 符号链接
# 示例
if [ -f "file.txt" ]; then
echo "file exists"
fi
逻辑运算
# [ ] 中
-a # AND
-o # OR
! # NOT
if [ $a -gt 0 -a $b -gt 0 ]; then
echo "both positive"
fi
# [[ ]] 中(推荐)
&& # AND
|| # OR
! # NOT
if [[ $a > 0 && $b > 0 ]]; then
echo "both positive"
fi
# 命令级
cmd1 && cmd2 # cmd1成功才执行cmd2
cmd1 || cmd2 # cmd1失败才执行cmd2
# 示例
[ -f file.txt ] && cat file.txt
[ -f file.txt ] || touch file.txt
输入输出
echo vs printf
# echo:简单输出
echo "Hello World"
echo -n "no newline" # 不换行
echo -e "tab:\t newline:\n" # 解析转义
# printf:格式化输出(推荐)
printf "Name: %s, Age: %d\n" "Alice" 25
printf "%10s %5d\n" "Alice" 25 # 右对齐
printf "%-10s %-5d\n" "Alice" 25 # 左对齐
printf "%.2f\n" 3.14159 # 保留2位小数
读取输入
# read命令
read name
read -p "Enter name: " name # 提示符
read -s password # 隐藏输入
read -t 5 name # 5秒超时
read -n 1 key # 读1个字符
read -a arr # 读入数组
# 读取文件
while IFS= read -r line; do
echo "$line"
done < file.txt
# 读取多个变量
read -p "Enter name and age: " name age
重定向
# 输出重定向
cmd > file # 覆盖写入
cmd >> file # 追加写入
cmd 2> error.log # 错误输出
cmd > output.log 2>&1 # 合并标准输出和错误
cmd &> all.log # 合并(简写)
# 输入重定向
cmd < file # 从文件读取
cmd << EOF # Here Document
line1
line2
EOF
cmd <<< "string" # Here String
# 管道
cmd1 | cmd2 # cmd1的输出作为cmd2的输入
ls -l | grep ".sh" | wc -l
# /dev/null(黑洞)
cmd > /dev/null # 丢弃输出
cmd 2> /dev/null # 丢弃错误
cmd &> /dev/null # 全丢弃
流程控制
if语句
# 基本语法
if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fi
# 示例
if [ $1 -gt 100 ]; then
echo "greater than 100"
elif [ $1 -gt 50 ]; then
echo "greater than 50"
else
echo "less than or equal 50"
fi
# [[ ]] 增强(支持正则、通配符)
if [[ $str =~ ^[0-9]+$ ]]; then
echo "number"
fi
if [[ $file == *.sh ]]; then
echo "shell script"
fi
# 多条件
if [ -f file.txt ] && [ -r file.txt ]; then
cat file.txt
fi
case语句
case $var in
pattern1)
commands
;;
pattern2|pattern3)
commands
;;
*)
default commands
;;
esac
# 示例
case $1 in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
restart)
echo "Restarting..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
# 通配符
case $file in
*.sh)
echo "Shell script"
;;
*.py)
echo "Python script"
;;
*)
echo "Unknown type"
;;
esac
for循环
# 遍历列表
for item in 1 2 3 4 5; do
echo $item
done
# 遍历数组
for item in "${arr[@]}"; do
echo $item
done
# 遍历文件
for file in *.sh; do
echo $file
done
# C风格
for ((i=0; i<10; i++)); do
echo $i
done
# 遍历命令输出
for user in $(cat users.txt); do
echo $user
done
# seq命令
for i in $(seq 1 10); do
echo $i
done
# Brace扩展
for i in {1..10}; do
echo $i
done
for i in {1..10..2}; do # 步长2
echo $i
done
while循环
# 基本语法
while [ condition ]; do
commands
done
# 示例:计数器
count=1
while [ $count -le 5 ]; do
echo $count
((count++))
done
# 无限循环
while true; do
echo "infinite loop"
sleep 1
done
# 读取文件
while IFS= read -r line; do
echo "$line"
done < file.txt
until循环
# 直到条件为真
until [ condition ]; do
commands
done
# 示例
count=1
until [ $count -gt 5 ]; do
echo $count
((count++))
done
break和continue
# break:跳出循环
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
# continue:跳过当前迭代
for i in {1..10}; do
if [ $i -eq 5 ]; then
continue
fi
echo $i
done
函数
函数定义
# 方式1
function_name() {
commands
}
# 方式2
function function_name {
commands
}
# 示例
greet() {
echo "Hello, $1"
}
greet "Alice" # 调用
参数和返回值
# 函数参数
add() {
local a=$1
local b=$2
echo $((a + b))
}
result=$(add 3 5)
echo $result # 8
# return(0-255)
is_even() {
local num=$1
if [ $((num % 2)) -eq 0 ]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if is_even 4; then
echo "even"
fi
# echo返回字符串
get_date() {
echo $(date +%Y-%m-%d)
}
today=$(get_date)
局部变量
global_var="global"
test_func() {
local local_var="local" # 局部变量
global_var="modified" # 修改全局
echo "$local_var"
}
test_func
echo "$global_var" # modified
# echo "$local_var" # 错误:未定义
调试
调试选项
# -x:打印执行的命令
bash -x script.sh
# 脚本内启用
#!/bin/bash
set -x # 开启
commands
set +x # 关闭
# -e:遇到错误立即退出
set -e
# -u:使用未定义变量时报错
set -u
# 组合使用
set -eux
set -euo pipefail # 管道中任一命令失败都退出
常用调试技巧
# 检查语法
bash -n script.sh
# 打印调试信息
echo "DEBUG: var=$var" >&2
# 条件调试
DEBUG=1
[ "$DEBUG" = 1 ] && echo "Debug info"
# trap捕获错误
trap 'echo "Error at line $LINENO"' ERR
最佳实践
Shebang和选项
#!/usr/bin/env bash
# 严格模式
set -euo pipefail
# -e: 命令失败立即退出
# -u: 使用未定义变量报错
# -o pipefail: 管道中任一失败都失败
变量命名
# 小写:局部变量
local_var="value"
# 大写:环境变量/常量
export PATH=$PATH:/usr/local/bin
readonly MAX_COUNT=100
# 引号保护
echo "$var" # 推荐
echo "${var}" # 明确边界
echo "${var:-default}" # 默认值
错误处理
# 检查命令是否存在
if ! command -v git &> /dev/null; then
echo "git not found"
exit 1
fi
# 检查上一命令状态
if [ $? -ne 0 ]; then
echo "Command failed"
exit 1
fi
# 更优雅
if ! some_command; then
echo "Failed"
exit 1
fi
代码组织
#!/usr/bin/env bash
set -euo pipefail
# 常量
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/script.log"
# 函数
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
cleanup() {
log "Cleaning up..."
# 清理操作
}
main() {
trap cleanup EXIT # 退出时清理
log "Starting..."
# 主逻辑
}
# 入口
main "$@"
核心: Shell脚本重在实用,掌握基础语法和文本处理工具是关键。