Shell脚本基础

Shell脚本是Linux系统管理的利器,通过编写脚本可以自动化完成复杂的系统管理任务。本章将介绍Shell脚本的基础知识和编写规范。


一、Shell脚本概述

1.1 什么是Shell脚本

1.1.1 Shell脚本定义

Shell脚本:包含一系列命令的文本文件,按顺序执行。

Shell脚本特点

  • 解释型语言
  • 无需编译
  • 跨平台
  • 易于调试

东巴文理解:Shell脚本就像菜谱,按步骤执行一系列操作,自动完成复杂任务。

1.1.2 Shell脚本用途

常见用途

  • 系统管理任务
  • 自动化部署
  • 日志分析
  • 数据备份
  • 批量操作

东巴文应用场景

  • 定时备份数据库
  • 批量创建用户
  • 监控系统状态
  • 自动化部署应用

1.2 第一个Shell脚本

1.2.1 创建脚本文件

创建hello.sh脚本

#!/bin/bash
# 第一个Shell脚本
# 作者:东巴文
# 日期:2024-01-01

echo "Hello, World!"
echo "欢迎使用东巴文Linux教程"
echo "当前用户:$USER"
echo "当前目录:$PWD"
echo "当前时间:$(date)"

脚本说明

  • #!/bin/bash:Shebang,指定解释器
  • #:注释符号
  • echo:输出命令
  • $USER$PWD:环境变量
  • $(date):命令替换

东巴文提示:Shebang必须放在第一行,告诉系统使用哪个Shell解释器。

1.2.2 执行脚本

方法一:使用bash命令

# 使用bash执行
bash hello.sh

# 输出:
# Hello, World!
# 欢迎使用东巴文Linux教程
# 当前用户:user
# 当前目录:/home/user
# 当前时间:2024年 01月 01日 星期一 10:00:00 CST

方法二:赋予执行权限

# 赋予执行权限
chmod +x hello.sh

# 执行脚本
./hello.sh

# 输出:
# Hello, World!
# 欢迎使用东巴文Linux教程
# 当前用户:user
# 当前目录:/home/user
# 当前时间:2024年 01月 01日 星期一 10:00:00 CST

方法三:使用source命令

# 使用source执行
source hello.sh

# 或使用点命令
. hello.sh

# 输出:
# Hello, World!
# 欢迎使用东巴文Linux教程
# 当前用户:user
# 当前目录:/home/user
# 当前时间:2024年 01月 01日 星期一 10:00:00 CST

东巴文区别

  • bash:启动新Shell执行脚本
  • ./:启动新Shell执行脚本
  • source:在当前Shell中执行脚本

二、Shell脚本结构

2.1 脚本基本结构

2.1.1 标准脚本模板

#!/bin/bash
#===============================================================================
# 脚本名称:script_name.sh
# 脚本功能:脚本功能描述
# 作者:东巴文
# 创建日期:2024-01-01
# 最后修改:2024-01-01
# 版本:1.0
# 使用方法:./script_name.sh [参数]
# 注意事项:注意事项说明
#===============================================================================

# 设置变量
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)

# 设置错误处理
set -e  # 遇到错误立即退出
set -u  # 使用未定义变量时报错
set -o pipefail  # 管道中的错误也会导致脚本退出

# 定义函数
function print_info() {
    echo "[INFO] $1"
}

function print_error() {
    echo "[ERROR] $1" >&2
}

# 主函数
function main() {
    print_info "脚本开始执行..."
    
    # 脚本主要逻辑
    print_info "当前目录:$PWD"
    print_info "当前用户:$USER"
    
    print_info "脚本执行完成"
}

# 执行主函数
main "$@"

东巴文最佳实践:使用标准模板,让脚本更规范、更易维护。

2.1.2 Shebang详解

常见Shebang

#!/bin/bash          # 使用bash
#!/bin/sh            # 使用sh
#!/usr/bin/env bash  # 使用env查找bash
#!/usr/bin/env python # Python脚本
#!/usr/bin/env perl  # Perl脚本

东巴文推荐:使用#!/usr/bin/env bash,更通用。

2.2 注释规范

2.2.1 单行注释

# 这是单行注释
echo "Hello"  # 行尾注释

# 注释应该解释"为什么",而不是"是什么"
# 例如:
# 检查文件是否存在,避免重复下载
if [ ! -f "$FILE" ]; then
    wget "$URL"
fi

2.2.2 多行注释

方法一:使用多个单行注释

# 这是多行注释的第一行
# 这是多行注释的第二行
# 这是多行注释的第三行
echo "Hello"

方法二:使用冒号

: '
这是多行注释
可以写很多内容
不会被解释执行
'
echo "Hello"

方法三:使用Here Document

: <<'EOF'
这是多行注释
可以写很多内容
不会被解释执行
EOF
echo "Hello"

东巴文提示:推荐使用方法一,更清晰易读。

2.3 脚本参数

2.3.1 位置参数

#!/bin/bash
# 参数示例脚本

echo "脚本名称:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "第三个参数:$3"
echo "参数个数:$#"
echo "所有参数:$@"
echo "所有参数:$*"

# 执行脚本
# ./params.sh a b c d

# 输出:
# 脚本名称:./params.sh
# 第一个参数:a
# 第二个参数:b
# 第三个参数:c
# 参数个数:4
# 所有参数:a b c d
# 所有参数:a b c d

特殊变量

变量 说明
$0 脚本名称
11-9 第1-9个参数
${10} 第10个参数
$# 参数个数
$@ 所有参数(数组)
$* 所有参数(字符串)
$? 上个命令的退出状态
$$ 当前进程PID
$! 后台进程PID

2.3.2 参数处理

检查参数

#!/bin/bash

# 检查参数个数
if [ $# -lt 1 ]; then
    echo "使用方法:$0 <参数1> [参数2]"
    echo "示例:$0 hello world"
    exit 1
fi

# 检查参数是否为空
if [ -z "$1" ]; then
    echo "错误:参数不能为空"
    exit 1
fi

echo "参数检查通过"
echo "第一个参数:$1"

参数默认值

#!/bin/bash

# 设置默认值
NAME=${1:-"东巴文"}  # 如果$1为空,使用"东巴文"
AGE=${2:-18}         # 如果$2为空,使用18

echo "姓名:$NAME"
echo "年龄:$AGE"

# 执行脚本
# ./default.sh
# 输出:
# 姓名:东巴文
# 年龄:18

# ./default.sh 张三 25
# 输出:
# 姓名:张三
# 年龄:25

2.3.3 选项参数

使用getopts处理选项

#!/bin/bash
# 选项参数示例

# 显示帮助信息
function show_help() {
    echo "使用方法:$0 [选项]"
    echo "选项:"
    echo "  -h    显示帮助信息"
    echo "  -f    指定文件"
    echo "  -v    显示详细信息"
    echo "  -n    指定名称"
}

# 初始化变量
FILE=""
VERBOSE=false
NAME=""

# 解析选项
while getopts "hvf:n:" opt; do
    case $opt in
        h)
            show_help
            exit 0
            ;;
        f)
            FILE="$OPTARG"
            ;;
        v)
            VERBOSE=true
            ;;
        n)
            NAME="$OPTARG"
            ;;
        \?)
            echo "无效选项:-$OPTARG" >&2
            exit 1
            ;;
        :)
            echo "选项 -$OPTARG 需要参数" >&2
            exit 1
            ;;
    esac
done

# 显示结果
echo "文件:$FILE"
echo "名称:$NAME"
echo "详细模式:$VERBOSE"

# 执行脚本
# ./options.sh -h
# ./options.sh -f test.txt -n "东巴文" -v

东巴文理解:getopts专门用于处理命令行选项,让脚本更专业。


三、变量与数据类型

3.1 变量定义

3.1.1 变量命名规则

命名规则

  • 只能包含字母、数字、下划线
  • 不能以数字开头
  • 区分大小写
  • 不能使用关键字

正确示例

# 正确的变量名
NAME="东巴文"
age=25
_user="user"
MAX_VALUE=100

错误示例

# 错误的变量名
1name="error"      # 不能以数字开头
my-name="error"    # 不能包含减号
my name="error"    # 不能包含空格

东巴文规范

  • 环境变量:大写(如PATH、HOME)
  • 局部变量:小写(如name、age)
  • 常量:大写(如MAX_VALUE)

3.1.2 变量赋值

基本赋值

# 赋值时等号两边不能有空格
name="东巴文"

# 错误示例(有空格)
name = "东巴文"  # 错误!

# 多个变量赋值
a=1 b=2 c=3

# 使用命令结果赋值
current_date=$(date)
current_user=$USER

# 使用算术表达式赋值
count=$((5 + 3))

变量引用

name="东巴文"

# 使用变量
echo $name
echo ${name}

# 变量拼接
echo "欢迎${name}使用Linux"
echo "欢迎$name使用Linux"

# 变量边界
echo "${name}is good"  # 正确
echo "$nameis good"    # 错误,找不到变量nameis

# 花括号的作用
file="test"
echo "${file}_name"  # 输出:test_name
echo "$file_name"    # 输出空,变量file_name不存在

东巴文提示:推荐使用${变量},更清晰,避免歧义。

3.2 数据类型

3.2.1 字符串

字符串定义

# 单引号字符串
str1='Hello World'

# 双引号字符串
str2="Hello World"

# 无引号字符串
str3=Hello

# 多行字符串
str4="第一行
第二行
第三行"

单引号vs双引号

name="东巴文"

# 单引号:原样输出,不解析变量和命令
echo 'Hello $name'
# 输出:Hello $name

# 双引号:解析变量和命令
echo "Hello $name"
# 输出:Hello 东巴文

# 双引号中执行命令
echo "当前时间:$(date)"
# 输出:当前时间:2024年 01月 01日 星期一 10:00:00 CST

# 单引号中不能使用转义字符
echo 'Hello\nWorld'
# 输出:Hello\nWorld

# 双引号中可以使用转义字符
echo -e "Hello\nWorld"
# 输出:
# Hello
# World

东巴文理解

  • 单引号:所见即所得
  • 双引号:解析变量和命令

字符串操作

str="Hello World"

# 字符串长度
echo ${#str}
# 输出:11

# 提取子串
echo ${str:0:5}   # 从第0个开始,提取5个字符
# 输出:Hello

echo ${str:6}     # 从第6个开始到结尾
# 输出:World

# 查找子串
expr index "$str" "W"
# 输出:7(W的位置)

# 字符串替换
echo ${str/World/东巴文}
# 输出:Hello 东巴文

# 删除字符串
echo ${str#Hello }  # 删除开头匹配的字符串
# 输出:World

echo ${str%World}   # 删除结尾匹配的字符串
# 输出:Hello 

# 大小写转换
echo ${str^^}  # 转大写
# 输出:HELLO WORLD

echo ${str,,}  # 转小写
# 输出:hello world

3.2.2 数值

整数定义

# 十进制
num1=10

# 八进制(以0开头)
num2=012

# 十六进制(以0x开头)
num3=0xFF

数值运算

# 使用$(())
a=$((5 + 3))
echo $a
# 输出:8

# 使用$[]
b=$[5 + 3]
echo $b
# 输出:8

# 使用expr
c=$(expr 5 + 3)
echo $c
# 输出:8

# 使用let
let d=5+3
echo $d
# 输出:8

# 使用bc(支持浮点数)
e=$(echo "5.5 + 3.3" | bc)
echo $e
# 输出:8.8

东巴文推荐:使用$(()),简洁高效。

运算符

# 算术运算符
echo $((5 + 3))   # 加法:8
echo $((5 - 3))   # 减法:2
echo $((5 * 3))   # 乘法:15
echo $((5 / 3))   # 除法:1
echo $((5 % 3))   # 取余:2
echo $((5 ** 3))  # 幂运算:125

# 自增自减
a=5
echo $((a++))  # 输出5,然后a变为6
echo $((a--))  # 输出6,然后a变为5
echo $((++a))  # a先变为6,然后输出6
echo $((--a))  # a先变为5,然后输出5

# 位运算
echo $((5 & 3))   # 按位与:1
echo $((5 | 3))   # 按位或:7
echo $((5 ^ 3))   # 按位异或:6
echo $((~5))      # 按位取反:-6
echo $((5 << 1))  # 左移:10
echo $((5 >> 1))  # 右移:2

3.2.3 数组

数组定义

# 方式一:括号定义
arr1=(1 2 3 4 5)

# 方式二:逐个定义
arr2[0]=1
arr2[1]=2
arr2[2]=3

# 方式三:定义特定元素
arr3=([0]=1 [2]=3 [4]=5)

# 字符串数组
names=("东巴文" "张三" "李四" "王五")

数组操作

arr=(1 2 3 4 5)

# 获取数组长度
echo ${#arr[@]}
# 输出:5

echo ${#arr[*]}
# 输出:5

# 获取单个元素
echo ${arr[0]}
# 输出:1

# 获取所有元素
echo ${arr[@]}
# 输出:1 2 3 4 5

echo ${arr[*]}
# 输出:1 2 3 4 5

# 获取数组索引
echo ${!arr[@]}
# 输出:0 1 2 3 4

# 数组切片
echo ${arr[@]:1:3}
# 输出:2 3 4

# 添加元素
arr+=(6 7)
echo ${arr[@]}
# 输出:1 2 3 4 5 6 7

# 删除元素
unset arr[2]
echo ${arr[@]}
# 输出:1 2 4 5 6 7

# 遍历数组
for i in "${arr[@]}"; do
    echo $i
done

东巴文提示:数组索引从0开始,使用${数组[@]}获取所有元素。

3.3 特殊变量

3.3.1 环境变量

常见环境变量

echo $HOME      # 用户主目录
echo $USER      # 当前用户名
echo $PWD       # 当前目录
echo $PATH      # 命令搜索路径
echo $SHELL     # 当前Shell
echo $LANG      # 语言设置
echo $HOSTNAME  # 主机名
echo $RANDOM    # 随机数
echo $UID       # 用户ID
echo $GROUPS    # 用户所属组

设置环境变量

# 临时设置
export MY_VAR="东巴文"

# 永久设置(写入~/.bashrc)
echo 'export MY_VAR="东巴文"' >> ~/.bashrc
source ~/.bashrc

# 删除环境变量
unset MY_VAR

3.3.2 特殊符号变量

#!/bin/bash

echo "脚本名称:$0"
echo "进程ID:$$"
echo "参数个数:$#"
echo "所有参数:$@"
echo "所有参数:$*"

# 上个命令的退出状态
ls /nonexistent
echo "退出状态:$?"
# 输出:退出状态:2

# 后台进程ID
sleep 10 &
echo "后台进程ID:$!"

*@@和的区别

#!/bin/bash
# 区别演示脚本

echo "使用\$@:"
for arg in "$@"; do
    echo "参数:$arg"
done

echo "使用\$*:"
for arg in "$*"; do
    echo "参数:$arg"
done

# 执行脚本
# ./test.sh "a b" c d

# 输出:
# 使用$@:
# 参数:a b
# 参数:c
# 参数:d
# 使用$*:
# 参数:a b c d

东巴文理解

  • $@:保留参数边界
  • $*:合并所有参数为一个字符串

四、运算符

4.1 算术运算符

4.1.1 基本运算

#!/bin/bash

a=10
b=3

# 加法
echo "a + b = $((a + b))"
# 输出:a + b = 13

# 减法
echo "a - b = $((a - b))"
# 输出:a - b = 7

# 乘法
echo "a * b = $((a * b))"
# 输出:a * b = 30

# 除法
echo "a / b = $((a / b))"
# 输出:a / b = 3

# 取余
echo "a % b = $((a % b))"
# 输出:a % b = 1

# 幂运算
echo "a ** b = $((a ** b))"
# 输出:a ** b = 1000

4.1.2 赋值运算符

#!/bin/bash

a=10

# 简单赋值
a=10
echo "a = $a"
# 输出:a = 10

# 加等于
((a += 5))
echo "a += 5, a = $a"
# 输出:a += 5, a = 15

# 减等于
((a -= 3))
echo "a -= 3, a = $a"
# 输出:a -= 3, a = 12

# 乘等于
((a *= 2))
echo "a *= 2, a = $a"
# 输出:a *= 2, a = 24

# 除等于
((a /= 4))
echo "a /= 4, a = $a"
# 输出:a /= 4, a = 6

# 取余等于
((a %= 4))
echo "a %= 4, a = $a"
# 输出:a %= 4, a = 2

4.2 比较运算符

4.2.1 数值比较

#!/bin/bash

a=10
b=20

# 等于
if [ $a -eq $b ]; then
    echo "$a 等于 $b"
else
    echo "$a 不等于 $b"
fi
# 输出:10 不等于 20

# 不等于
if [ $a -ne $b ]; then
    echo "$a 不等于 $b"
fi
# 输出:10 不等于 20

# 大于
if [ $a -gt $b ]; then
    echo "$a 大于 $b"
else
    echo "$a 不大于 $b"
fi
# 输出:10 不大于 20

# 小于
if [ $a -lt $b ]; then
    echo "$a 小于 $b"
fi
# 输出:10 小于 20

# 大于等于
if [ $a -ge $b ]; then
    echo "$a 大于等于 $b"
else
    echo "$a 小于 $b"
fi
# 输出:10 小于 20

# 小于等于
if [ $a -le $b ]; then
    echo "$a 小于等于 $b"
fi
# 输出:10 小于等于 20

数值比较运算符

运算符 说明 英文
-eq 等于 equal
-ne 不等于 not equal
-gt 大于 greater than
-lt 小于 less than
-ge 大于等于 greater or equal
-le 小于等于 less or equal

4.2.2 字符串比较

#!/bin/bash

str1="hello"
str2="world"
str3="hello"

# 等于
if [ "$str1" = "$str3" ]; then
    echo "str1 等于 str3"
fi
# 输出:str1 等于 str3

# 不等于
if [ "$str1" != "$str2" ]; then
    echo "str1 不等于 str2"
fi
# 输出:str1 不等于 str2

# 字符串长度为0
empty=""
if [ -z "$empty" ]; then
    echo "empty是空字符串"
fi
# 输出:empty是空字符串

# 字符串长度不为0
if [ -n "$str1" ]; then
    echo "str1不是空字符串"
fi
# 输出:str1不是空字符串

# 字符串是否为空
if [ "$str1" ]; then
    echo "str1不为空"
fi
# 输出:str1不为空

字符串比较运算符

运算符 说明
= 字符串相等
!= 字符串不相等
-z 字符串长度为0
-n 字符串长度不为0

东巴文提示:字符串比较时,变量要用双引号括起来,避免空格导致错误。

4.3 逻辑运算符

4.3.1 逻辑运算

#!/bin/bash

a=10
b=20

# 逻辑与
if [ $a -gt 5 ] && [ $b -gt 15 ]; then
    echo "两个条件都成立"
fi
# 输出:两个条件都成立

# 逻辑或
if [ $a -gt 15 ] || [ $b -gt 15 ]; then
    echo "至少一个条件成立"
fi
# 输出:至少一个条件成立

# 逻辑非
if [ ! $a -gt 15 ]; then
    echo "a不大于15"
fi
# 输出:a不大于15

# 使用[[ ]]支持&&和||
if [[ $a -gt 5 && $b -gt 15 ]]; then
    echo "两个条件都成立"
fi
# 输出:两个条件都成立

# 使用(( ))支持C风格语法
if (( a > 5 && b > 15 )); then
    echo "两个条件都成立"
fi
# 输出:两个条件都成立

逻辑运算符

运算符 说明
&& 逻辑与
|| 逻辑或
! 逻辑非

4.4 文件测试运算符

4.4.1 文件类型测试

#!/bin/bash

file="/etc/passwd"
dir="/home"

# 文件是否存在
if [ -e "$file" ]; then
    echo "$file 存在"
fi
# 输出:/etc/passwd 存在

# 是否为普通文件
if [ -f "$file" ]; then
    echo "$file 是普通文件"
fi
# 输出:/etc/passwd 是普通文件

# 是否为目录
if [ -d "$dir" ]; then
    echo "$dir 是目录"
fi
# 输出:/home 是目录

# 是否为块设备文件
if [ -b "/dev/sda" ]; then
    echo "/dev/sda 是块设备"
fi

# 是否为字符设备文件
if [ -c "/dev/tty" ]; then
    echo "/dev/tty 是字符设备"
fi

# 是否为符号链接
if [ -L "/bin/sh" ]; then
    echo "/bin/sh 是符号链接"
fi

# 是否为管道文件
if [ -p "/tmp/pipe" ]; then
    echo "/tmp/pipe 是管道文件"
fi

# 是否为套接字文件
if [ -S "/tmp/sock" ]; then
    echo "/tmp/sock 是套接字文件"
fi

4.4.2 文件权限测试

#!/bin/bash

file="/etc/passwd"

# 是否可读
if [ -r "$file" ]; then
    echo "$file 可读"
fi
# 输出:/etc/passwd 可读

# 是否可写
if [ -w "$file" ]; then
    echo "$file 可写"
else
    echo "$file 不可写"
fi
# 输出:/etc/passwd 不可写(普通用户)

# 是否可执行
if [ -x "/bin/ls" ]; then
    echo "/bin/ls 可执行"
fi
# 输出:/bin/ls 可执行

# 是否设置了SUID
if [ -u "/usr/bin/passwd" ]; then
    echo "/usr/bin/passwd 设置了SUID"
fi

# 是否设置了SGID
if [ -g "/usr/bin/write" ]; then
    echo "/usr/bin/write 设置了SGID"
fi

# 是否设置了粘滞位
if [ -k "/tmp" ]; then
    echo "/tmp 设置了粘滞位"
fi

4.4.3 文件属性测试

#!/bin/bash

file1="/etc/passwd"
file2="/etc/shadow"

# 文件是否为空
if [ -s "$file" ]; then
    echo "$file 不为空"
fi
# 输出:/etc/passwd 不为空

# 文件是否被修改过(比另一个文件新)
if [ "$file1" -nt "$file2" ]; then
    echo "$file1$file2 新"
fi

# 文件是否比另一个文件旧
if [ "$file1" -ot "$file2" ]; then
    echo "$file1$file2 旧"
fi

# 两个文件是否为同一个文件(inode相同)
if [ "$file1" -ef "$file2" ]; then
    echo "$file1$file2 是同一个文件"
fi

文件测试运算符总结

运算符 说明
-e 文件存在
-f 普通文件
-d 目录
-r 可读
-w 可写
-x 可执行
-s 文件不为空
-L 符号链接
-b 块设备
-c 字符设备
-p 管道文件
-S 套接字文件
-nt 文件比另一个新
-ot 文件比另一个旧
-ef 两个文件相同

五、输入输出

5.1 输出命令

5.1.1 echo命令

#!/bin/bash

# 基本输出
echo "Hello World"
# 输出:Hello World

# 输出变量
name="东巴文"
echo "欢迎$name"
# 输出:欢迎东巴文

# 不换行输出
echo -n "不换行"
echo "输出"
# 输出:不换行输出

# 启用转义字符
echo -e "第一行\n第二行"
# 输出:
# 第一行
# 第二行

# 输出到标准错误
echo "错误信息" >&2

# 输出到文件
echo "写入文件" > file.txt

# 追加到文件
echo "追加内容" >> file.txt

# 显示命令执行结果
echo "当前目录:$(pwd)"
echo "当前用户:$USER"

echo选项

选项 说明
-n 不换行
-e 启用转义字符
-E 禁用转义字符(默认)

转义字符

字符 说明
\n 换行
\t 制表符
\ 反斜杠
\a 警报
\r 回车

5.1.2 printf命令

#!/bin/bash

# 基本格式化输出
printf "Hello, %s\n" "World"
# 输出:Hello, World

# 格式化字符串
name="东巴文"
age=25
printf "姓名:%s,年龄:%d\n" "$name" "$age"
# 输出:姓名:东巴文,年龄:25

# 格式化数字
printf "整数:%d\n" 123
printf "浮点数:%f\n" 3.14
printf "科学计数:%e\n" 1000000

# 宽度和对齐
printf "|%10s|\n" "hello"   # 右对齐,宽度10
printf "|%-10s|\n" "hello"  # 左对齐,宽度10
printf "|%10d|\n" 123       # 数字右对齐
printf "|%010d|\n" 123      # 用0填充

# 输出到文件
printf "写入文件\n" > file.txt

printf格式符

格式符 说明
%s 字符串
%d 整数
%f 浮点数
%c 字符
%x 十六进制
%o 八进制
%e 科学计数法

东巴文理解:printf比echo更强大,支持格式化输出。

5.2 输入命令

5.2.1 read命令

#!/bin/bash

# 基本输入
echo "请输入您的姓名:"
read name
echo "您好,$name!"

# 带提示的输入
read -p "请输入您的年龄:" age
echo "您的年龄是:$age"

# 输入密码(不显示)
read -s -p "请输入密码:" password
echo
echo "密码已接收"

# 限时输入(5秒)
read -t 5 -p "请在5秒内输入:" input
echo "您输入的是:$input"

# 限制输入字符数
read -n 1 -p "请输入一个字符:" char
echo
echo "您输入的是:$char"

# 读取多个变量
read -p "请输入姓名和年龄:" name age
echo "姓名:$name,年龄:$age"

# 读取到数组
read -a arr -p "请输入多个单词:"
echo "第一个单词:${arr[0]}"
echo "所有单词:${arr[@]}"

# 读取整行
read -p "请输入一行文字:" line
echo "您输入的是:$line"

read选项

选项 说明
-p 提示信息
-s 静默模式(不显示输入)
-t 超时时间(秒)
-n 读取指定字符数
-a 读取到数组
-d 指定分隔符
-r 不处理转义字符

5.2.2 交互式脚本示例

#!/bin/bash
# 用户信息收集脚本

echo "===== 用户信息收集 ====="
echo

# 输入姓名
read -p "请输入姓名:" name

# 输入年龄
while true; do
    read -p "请输入年龄:" age
    if [[ $age =~ ^[0-9]+$ ]]; then
        break
    else
        echo "错误:年龄必须是数字"
    fi
done

# 输入邮箱
while true; do
    read -p "请输入邮箱:" email
    if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        break
    else
        echo "错误:邮箱格式不正确"
    fi
done

# 输入密码
while true; do
    read -s -p "请输入密码:" password1
    echo
    read -s -p "请再次输入密码:" password2
    echo
    if [ "$password1" = "$password2" ]; then
        break
    else
        echo "错误:两次密码不一致"
    fi
done

# 显示信息
echo
echo "===== 用户信息 ====="
echo "姓名:$name"
echo "年龄:$age"
echo "邮箱:$email"
echo "信息已保存"

5.3 重定向

5.3.1 标准输入输出

文件描述符

描述符 名称 说明
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误

输出重定向

# 输出到文件(覆盖)
echo "Hello" > file.txt

# 输出到文件(追加)
echo "World" >> file.txt

# 标准输出重定向
ls > output.txt

# 标准错误重定向
ls /nonexistent 2> error.txt

# 标准输出和错误重定向到同一文件
ls /nonexistent > all.txt 2>&1

# 或使用&>(bash特有)
ls /nonexistent &> all.txt

# 丢弃输出
ls /nonexistent 2>/dev/null

# 标准输出和错误分别重定向
ls / > stdout.txt 2> stderr.txt

输入重定向

# 从文件读取输入
wc -l < file.txt

# Here Document
cat << EOF
第一行
第二行
第三行
EOF

# Here String
grep "pattern" <<< "search in this string"

5.3.2 管道

# 基本管道
ls | grep ".txt"

# 多个管道
cat file.txt | grep "error" | wc -l

# 管道与重定向结合
ls | grep ".txt" > txt_files.txt

# tee命令(同时输出到屏幕和文件)
ls | tee files.txt

# tee追加模式
ls | tee -a files.txt

东巴文理解:管道就像流水线,将一个命令的输出作为另一个命令的输入。


六、脚本调试

6.1 调试方法

6.1.1 echo调试

#!/bin/bash

name="东巴文"
age=25

# 使用echo输出变量值
echo "调试:name=$name, age=$age"

# 使用echo输出执行步骤
echo "开始执行第一步..."
# 第一步代码
echo "第一步完成"

echo "开始执行第二步..."
# 第二步代码
echo "第二步完成"

6.1.2 set命令调试

#!/bin/bash

# 显示执行的每条命令
set -x

name="东巴文"
echo "Hello, $name"

# 关闭调试
set +x

echo "调试已关闭"

# 输出示例:
# + name=东巴文
# + echo 'Hello, 东巴文'
# Hello, 东巴文
# + set +x
# 调试已关闭

set选项

选项 说明
-x 显示执行的命令
-v 显示读取的行
-e 遇到错误立即退出
-u 使用未定义变量时报错
-o pipefail 管道中的错误也会导致脚本退出

6.1.3 bash调试选项

# 执行时显示命令
bash -x script.sh

# 执行时显示读取的行
bash -v script.sh

# 执行时显示命令和读取的行
bash -xv script.sh

# 检查语法错误
bash -n script.sh

6.2 错误处理

6.2.1 退出状态码

#!/bin/bash

# 执行命令
ls /nonexistent

# 检查退出状态
if [ $? -eq 0 ]; then
    echo "命令执行成功"
else
    echo "命令执行失败"
    exit 1
fi

# 常见退出状态码
# 0   成功
# 1   一般错误
# 2   误用Shell命令
# 126 命令无法执行
# 127 命令未找到
# 128 退出参数无效
# 130 Ctrl+C中断
# 255 退出码超出范围

6.2.2 错误处理函数

#!/bin/bash

# 错误处理函数
function error_exit() {
    echo "[ERROR] $1" >&2
    exit 1
}

# 使用错误处理
cd /nonexistent || error_exit "无法进入目录"

# 检查文件是否存在
[ -f "/etc/passwd" ] || error_exit "文件不存在"

# 检查命令是否成功
if ! command -v git &> /dev/null; then
    error_exit "git命令未安装"
fi

echo "所有检查通过"

6.2.3 trap命令

#!/bin/bash

# 清理函数
function cleanup() {
    echo "执行清理操作..."
    rm -f /tmp/tempfile
    echo "清理完成"
}

# 捕获退出信号
trap cleanup EXIT

# 捕获中断信号(Ctrl+C)
trap 'echo "用户中断"; exit 1' INT

# 捕获错误
trap 'echo "发生错误"; exit 1' ERR

# 创建临时文件
touch /tmp/tempfile

# 脚本主要逻辑
echo "脚本执行中..."

# 脚本正常退出时,会自动执行cleanup函数

常见信号

信号 说明
EXIT 脚本退出
INT 中断(Ctrl+C)
TERM 终止
HUP 挂起
ERR 错误

东巴文最佳实践:使用trap确保脚本退出时执行清理操作。


七、实战案例

7.1 系统信息脚本

#!/bin/bash
#===============================================================================
# 脚本名称:sysinfo.sh
# 脚本功能:显示系统信息
# 作者:东巴文
# 创建日期:2024-01-01
#===============================================================================

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 显示标题
function show_header() {
    echo -e "${BLUE}================================${NC}"
    echo -e "${GREEN}    系统信息收集脚本${NC}"
    echo -e "${BLUE}================================${NC}"
    echo
}

# 显示系统信息
function show_system_info() {
    echo -e "${YELLOW}[系统信息]${NC}"
    echo "主机名:$(hostname)"
    echo "系统:$(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
    echo "内核:$(uname -r)"
    echo "架构:$(uname -m)"
    echo
}

# 显示CPU信息
function show_cpu_info() {
    echo -e "${YELLOW}[CPU信息]${NC}"
    echo "CPU型号:$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d':' -f2 | xargs)"
    echo "CPU核心数:$(nproc)"
    echo "CPU使用率:$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1"%"}')"
    echo
}

# 显示内存信息
function show_memory_info() {
    echo -e "${YELLOW}[内存信息]${NC}"
    free -h | awk 'NR==1{printf "%-10s %10s %10s %10s\n", $1, $2, $3, $4}NR==2{printf "%-10s %10s %10s %10s\n", "内存", $2, $3, $4}NR==3{printf "%-10s %10s %10s %10s\n", "交换", $2, $3, $4}'
    echo
}

# 显示磁盘信息
function show_disk_info() {
    echo -e "${YELLOW}[磁盘信息]${NC}"
    df -h | awk '$NF=="/"{printf "根分区:%s 总大小 %s 已使用 %s 可用 %s 使用率 %s\n", $NF, $2, $3, $4, $5}'
    echo
}

# 显示网络信息
function show_network_info() {
    echo -e "${YELLOW}[网络信息]${NC}"
    echo "IP地址:$(hostname -I | awk '{print $1}')"
    echo "MAC地址:$(ip link show | grep -E "ether" | awk '{print $2}' | head -1)"
    echo
}

# 显示用户信息
function show_user_info() {
    echo -e "${YELLOW}[用户信息]${NC}"
    echo "当前用户:$USER"
    echo "登录用户数:$(who | wc -l)"
    echo
}

# 主函数
function main() {
    show_header
    show_system_info
    show_cpu_info
    show_memory_info
    show_disk_info
    show_network_info
    show_user_info
    echo -e "${GREEN}系统信息收集完成${NC}"
}

# 执行主函数
main

7.2 文件备份脚本

#!/bin/bash
#===============================================================================
# 脚本名称:backup.sh
# 脚本功能:文件备份脚本
# 作者:东巴文
# 创建日期:2024-01-01
#===============================================================================

# 设置变量
SOURCE_DIR="/home/user/data"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
LOG_FILE="/var/log/backup.log"

# 日志函数
function log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 检查源目录
if [ ! -d "$SOURCE_DIR" ]; then
    log "错误:源目录 $SOURCE_DIR 不存在"
    exit 1
fi

# 创建备份目录
if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    log "创建备份目录:$BACKUP_DIR"
fi

# 开始备份
log "开始备份:$SOURCE_DIR"
log "备份文件:$BACKUP_DIR/$BACKUP_FILE"

# 执行备份
tar -czf "$BACKUP_DIR/$BACKUP_FILE" "$SOURCE_DIR" 2>&1 | tee -a "$LOG_FILE"

# 检查备份结果
if [ $? -eq 0 ]; then
    log "备份成功"
    log "备份文件大小:$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)"
else
    log "备份失败"
    exit 1
fi

# 删除旧备份(保留最近7个)
cd "$BACKUP_DIR"
ls -t backup_*.tar.gz | tail -n +8 | xargs rm -f
log "清理旧备份完成"

# 发送通知(可选)
# echo "备份完成:$BACKUP_FILE" | mail -s "备份通知" admin@example.com

log "备份任务完成"

7.3 批量创建用户脚本

#!/bin/bash
#===============================================================================
# 脚本名称:create_users.sh
# 脚本功能:批量创建用户
# 作者:东巴文
# 创建日期:2024-01-01
#===============================================================================

# 检查是否为root用户
if [ "$EUID" -ne 0 ]; then
    echo "请使用root用户执行此脚本"
    exit 1
fi

# 用户列表文件
USER_FILE="users.txt"

# 检查用户文件是否存在
if [ ! -f "$USER_FILE" ]; then
    echo "错误:用户文件 $USER_FILE 不存在"
    echo "请创建用户文件,格式:用户名 密码"
    exit 1
fi

# 创建用户
while read username password; do
    # 跳过空行和注释
    if [ -z "$username" ] || [[ "$username" =~ ^# ]]; then
        continue
    fi
    
    # 检查用户是否已存在
    if id "$username" &>/dev/null; then
        echo "用户 $username 已存在,跳过"
        continue
    fi
    
    # 创建用户
    useradd -m -s /bin/bash "$username"
    
    # 设置密码
    echo "$username:$password" | chpasswd
    
    # 设置首次登录修改密码
    chage -d 0 "$username"
    
    echo "用户 $username 创建成功"
    
done < "$USER_FILE"

echo "所有用户创建完成"

八、本章小结

8.1 核心要点

✅ 理解Shell脚本基本概念 ✅ 掌握脚本编写规范 ✅ 学会变量与数据类型 ✅ 熟练使用运算符 ✅ 掌握输入输出操作 ✅ 学会脚本调试技巧

8.2 验证清单

完成本章学习后,请确认您能够:

  • 编写简单的Shell脚本
  • 使用变量和运算符
  • 处理脚本参数
  • 进行文件测试
  • 调试Shell脚本

东巴文(db-w.cn) - 让Linux学习更简单