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脚本重在实用,掌握基础语法和文本处理工具是关键。