文章目录
- bash 概念与学习目的
- 第一个 bash 脚本
- bash 语法
- 变量的使用
- 位置参数
- 管道符号(过滤条件)
- 重定向符号
- 条件测试命令
- 条件语句
- case 条件分支
- Array
- for 循环
- 函数
- exit 关键字
- bash 脚本
- 记录历史命令
- 查询文件
- 分发内容
bash 概念与学习目的
bash(Bourne Again Shell)是一种广泛使用的 Unix/Linux Shell(命令行界面)。它是由 Brian Fox 为 GNU 项目开发的,是 Bourne Shell(sh)的增强版本,因此得名 “Bourne Again”。
Bash 是大多数 Linux 发行版(如 Ubuntu、CentOS)和 macOS 的默认 Shell,具有如 命令历史记录、管道、重定向、变量操作以及脚本编写能力
相比于 bash , Python 或是 Ansible 脚本更易读写,一般是更好的选择,但是学习 bash 只需要了解基础知识就在可以在生产环境中有更多的选择
bash 初级学习需要少量的 Linux command (vim、cat、echo等)与权限概念基础
第一个 bash 脚本
- 创建一个 bash 脚本
vim bash-demo1.sh
- 编写第一个 bash 脚本
脚本文件中写入以下内容
#!/bin/bash
echo Hello World!
其中,第一行注解告诉 Linux 操作系统采用哪一个 bash 解释器(默认选项,可省略),第二行会回显输出 Hello World
退出插入模式 :wq
(或是键盘大写后按键 ZZ)保存退出
- 为脚本添加可执行权限
执行脚本首先需要先赋予脚本执行权限
ls -l
ls 命令的 long 格式查看脚本是否具有执行权限
可以看到 root用户、root同组用户、其他用户 都没有 x
- 执行权限
# 为当前用户添加执行权限
chmod u+x bash-demo1.sh
# 为所有用户添加执行权限
chmod +x bash-demo1.sh
作者的当前用户为 root ,可以看到 root 权限栏出现 x 执行权限
前三个字母为 root 用户读写执行权限,中间三个字母为 root同组用户读写执行权限、最后三个字母为其他用户的读写执行权限
- 执行 bash 脚本
./bash-demo1.sh
# 或是在不更改脚本执行权限的情况下
bash bash-demo1.sh
bash 语法
变量的使用
- 变量在路径中的使用
此处编写一个可用于文件复制的 bash shell
#!/bin/bash
cp /home/fishpie/workspace/bash-shell/tempdir01/demo1.txt /home/fishpie/workspace/bash-shell/tempdir02/
cp /home/fishpie/workspace/bash-shell/tempdir02/demo2.txt /home/fishpie/workspace/bash-shell/tempdir01/CPfile.txt
可以发现当文件路较长时可读性很差,可以使用变量来解决这个问题
#!/bin/bash
# 变量
MY_LOCATION_FROM=/home/fishpie/workspace/bash-shell/tempdir01
MY_LOCATION_TO=/home/fishpie/workspace/bash-shell/tempdir02
# 显示变量
echo $MY_LOCATION_FROM
echo $MY_LOCATION_TO
# 使用变量
cp "$MY_LOCATION_FROM/demo1.txt" "$MY_LOCATION_TO/"
cp "$MY_LOCATION_TO/demo2.txt" "$MY_LOCATION_FROM/CPfile.txt"
对于变量的使用(引用)需要使用到
$
推荐使用
""
包裹路径,如果变量的值(比如 $MY_LOCATION_TO 或 $MY_LOCATION_FROM)包含空格,Bash 会将空格视为参数的分隔符,导致命令解析错误
- 读取输入的变量
读取用户输入,比如常见的 yum 安装软件包过程中的 Y/n 用户输入等需要用户交互的地方
读取用户输入并设置为变量的关键字为 read
,此处编写一个读取用户名称的脚本
#!/bin/bash
# 提示用户输入名字和姓氏
echo What is your first name?
read FIRST_NAME
echo What is your last name?
read LAST_NAME# 提示用户输入性别选择
echo "Are you male? (Y/n): "
read gender_choice# 根据用户输入判断性别并显示结果
if [ "$gender_choice" = "Y" ] || [ "$gender_choice" = "y" ]; thenecho "Name: $FIRST_NAME $LAST_NAME, Gender: Male"
elif [ "$gender_choice" = "N" ] || [ "$gender_choice" = "n" ]; thenecho "Name: $FIRST_NAME $LAST_NAME, Gender: Female"
elseecho "Invalid input! Please enter Y/y for male or N/n for female."
fi
位置参数
在 Linux command 中,所有命令的开头(0号位置,$0
)是为 shell 本身保留,是脚本名称或解释器名称
如果我们想将参数等信息通过 空格 传入,则可以使用位置参数 $1
、$2
、$3
…
#!/bin/bashecho Hello $1 $2
管道符号(过滤条件)
管道符号 | 用于将一个命令的输出传递给另一个命令作为输入,是 Bash(及其他 shell)中的一种进程间通信机制
管道符号 | 会将左侧命令的标准输出(stdout)作为右侧命令的标准输入(stdin) (标准流)
- 标准输出(stdout):命令执行后显示在终端的结果
- 标准输入(stdin):命令从外部接收的数据(比如键盘输入或管道传递的数据)
管道的本质是:避免中间结果保存到文件,直接在内存中传递数据,提高效率
管道在内存中操作,效率高,但大量数据时可能会占用较多内存
右侧的命令必须能从 stdin 接收输入。例如,
ls | cp
是无效的,因为 cp 不支持从 stdin 读取数据
示例:
# 查看本机网络中的 80 端口服务
netstat -tunlp | grep ":80"# 统计系统中正在运行的进程数量
ps aux | wc -l# 找出占用 CPU 最多的前 5 个进程
ps aux | sort -k 3 -nr | head -n 5# 查看系统中所有用户的唯一用户名并排序
cat /etc/passwd | cut -d: -f1 | sort | uniq
重定向符号
重定向符号 >
和 >>
可以将命令的标准输出(stdout)或标准错误(stderr)从默认的终端重定向到文件或其他地方
和管道符号 |
都是非常常用的符号
- > 将命令的输出写入指定文件,如果文件已存在,则覆盖原有内容
- >> 将命令的输出追加到指定文件末尾,如果文件已存在,则不覆盖,而是在末尾添加内容
<
将文件内容作为命令的输入
示例:
# 统计当前目录中的所有 txt 文件
ls -l | grep "txt" >> txt_files.txt# 追加日期到日志文件
date +"%Y-%m-%d" >> log.txt# 清空文件
> something.txt# 统计文件行数
wc -l < file.txt# 将筛选出的 bash 进程信息保存到 processes.txt
ps aux | grep "bash" > processes.txt
- 进阶用法
这里再次提到标准流
- 标准输入(stdin,文件描述符 0):命令接收的数据,默认来自键盘
- 标准输出(stdout,文件描述符 1):命令的正常输出,默认显示在终端
- 标准错误(stderr,文件描述符 2):命令的错误信息,默认也显示在终端
示例1:
# 重定向标准错误,将标准错误重定向到标准输出的位置
ls tempdir03/ tempdir05/ 2>&1 > output.txt
如果不加入 &1
参数
ls tempdir03/ tempdir05/ 2> output.txt
可见不会显示错误信息
示例2:
在组合重定向时,顺序很重要
ls > file.txt 2>&1 # 正确
ls 2>&1 > file.txt # 错误:stderr 不会进入 file.txt
条件测试命令
test
或者 [ ]
,空格是必须的,用于条件判断,通常与 if、while 等流程控制语句搭配使用,来检查文件状态、比较数值或字符串等
[ ]
的正式名称:test
命令,其实 [ ]
就是 test
命令的符号化
[ -f /etc/passwd ]
# 等价于
test -f /etc/passwd
echo $?
用于查看上一个命令的退出状态,输出 0 则为真,输出1 则为 假
常见用法与分类
- 文件测试
测试选项 | 含义 | 示例 |
---|---|---|
-e | 文件是否存在 | [ -e /etc/passwd ] |
-f | 是否为普通文件(非目录) | [ -f /etc/passwd ] |
-d | 是否为目录 | [ -d /home ] |
-r | 是否可读 | [ -r file.txt ] |
-w | 是否可写 | [ -w file.txt ] |
-x | 是否可执行 | [ -x script.sh ] |
-s | 文件是否非空(大小大于 0) | [ -s log.txt ] |
# 判断 /etc/passwd 是否为一个文件
if [ -f /etc/passwd ]; thenecho "passwd file exists and is a regular file"
fi
- 字符串比较
测试选项 | 含义 | 示例 |
---|---|---|
= 或 == | 字符串是否相等 | [ “$str” = “hello” ] |
!= | 字符串是否不相等 | [ “$str” != “hello” ] |
-z | 字符串是否为空 | [ -z “$str” ] |
-n | 字符串是否非空 | [ -n “$str” ] |
# 判断字符串是否相等
name="Alice"
if [ "$name" = "Alice" ]; thenecho "Hello, Alice!"
fi
【注】:变量需要用双引号 “$name” 包裹,避免变量为空时语法错误
例如:
var=""
[ $var = "" ] # 出错,解析为 [ = "" ]
[ "$var" = "" ] # 正确
- 数值比较
测试选项 | 含义 | 示例 |
---|---|---|
-eq | 等于 | [ 5 -eq 5 ] |
-ne | 不等于 | [ 5 -ne 6 ] |
-gt | 大于 | [ 10 -gt 5 ] |
-lt | 小于 | [ 5 -lt 10 ] |
-ge | 大于等于 | [ 10 -ge 10 ] |
-le | 小于等于 | [ 5 -le 6 ] |
# 判断年龄是否大于 18 岁
age=20
if [ "$age" -gt 18 ]; thenecho "You are an adult."
fi
- 逻辑运算
操作符 | 含义 | 示例 |
---|---|---|
-a | 与(AND) | [ -f file.txt -a -r file.txt ] |
-o | 或(OR) | [ “ a g e " − g t 18 − o " age" -gt 18 -o " age"−gt18−o"age” -eq 18 ] |
! | 非(NOT) | [ ! -d /tmp ] |
注意空格的使用!
# 判断 file.txt 是否为普通文件,是否有写权限
if [ -f file.txt -a -w file.txt ]; thenecho "file.txt exists and is writable"
fi
可能会注意到,如果遇到了需要正则表达式,如判断一个变量是否符合某个格式
Bash 中还有一个增强版 [[ ]],功能更强大,支持**正则匹配(=~)**等,且对未定义变量更宽容
# 检查 $var 是否为数字
[[ $var =~ ^[0-9]+$ ]]
# 正则匹配(=~)
条件语句
if
elif
else
,注意不是 elsif
与条件测试命令搭配使用
基本语法:
if [ 条件 ]; then# 条件为真时执行的代码
elif [ 条件 ]; then# 上一个条件为假,此条件为真时执行的代码
else# 所有条件都为假时执行的代码
fi
此处编写一个根据用户输入的数字判断其范围的脚本:
#!/bin/bashecho "Please enter a number: "
read numif [ "$num" -gt 0 ]; thenecho "$num is positive."
elif [ "$num" -lt 0 ]; thenecho "$num is negative."
elseecho "$num is zero."
fi
- 多条件组合
使用逻辑运算符(如 -a、-o)或 &&、|| 组合条件
# 判断 85 分是一个什么样的等级
score=85
if [ "$score" -ge 90 ]; thenecho "Grade: A"
elif [ "$score" -ge 80 ] && [ "$score" -lt 90 ]; thenecho "Grade: B"
elif [ "$score" -ge 70 ]; thenecho "Grade: C"
elseecho "Grade: D"
fi
- 嵌套结构
在 if
内部嵌套另一个 if
,特别注意脚本的结构问题
# 判断 25 岁是一个什么样的年龄
age=25
if [ "$age" -gt 18 ]; thenif [ "$age" -lt 30 ]; thenecho "Young adult"elseecho "Adult"fi
elseecho "Minor"
fi
- 正则表达式作为判断条件
# 判断 名字是否以 F 开头
name="FISHPIE"
if [[ "$name" =~ ^F ]]; thenecho "Name starts with F"
elseecho "Name does not start with F"
fi
示例脚本:
- 用户输入输出处理
#!/bin/bashread -p "Enter Y/N: " choice
if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; thenecho "Yes"
elif [ "$choice" = "N" ] || [ "$choice" = "n" ]; thenecho "No"
elseecho "Invalid input"
fi
- 文件检查
#!/bin/bashif [ -f "data.txt" ]; thenecho "File exists"
elif [ -d "data.txt" ]; thenecho "It's a directory"
elseecho "File does not exist"
fi
case 条件分支
case 语句是一种条件分支结构,类似于 if-elif-else,但它更适合处理多分支选择,尤其是需要根据变量值匹配多个模式时
基本语法:
case 表达式 in模式1)# 匹配模式1时执行的代码;;模式2)# 匹配模式2时执行的代码;;*)# 默认情况(可选);;
esac
- case 表达式:指定要匹配的值(通常是变量)
- in:开始模式匹配部分
- 模式):定义匹配的模式,后面跟 )
- ;;:表示该分支的代码结束,类似 break
- *****:通配符,表示默认分支(当没有模式匹配时执行)
- esac:case 的结束标志(case 反过来)
对格式的要求相对严格
示例:
- 用户输入匹配
#!/bin/bashecho "Enter a fruit: "
read fruitcase "$fruit" in"apple")echo "You chose an apple.";;"banana")echo "You chose a banana.";;"orange")echo "You chose an orange.";;*)echo "Unknown fruit: $fruit";;
esac
- 多模式匹配与命令结合
#!/bin/bash# case 中可以直接匹配命令的输出
case $(uname) in"Linux")echo "Running on Linux";;"Darwin")echo "Running on macOS";;*)echo "Unknown system: $(uname)";;
esac# 有限可写条件中的一种
echo "Enter a day (mon/tue/wed/etc): "
read daycase "$day" in"mon"|"tue"|"wed"|"thu"|"fri")echo "Weekday";;"sat"|"sun")echo "Weekend";;*)echo "Invalid day: $day";;
esac
Array
数组(Array) 是一种数据结构,用于存储多个有序的值(元素),这些值可以通过索引(下标)访问,虽然不如其他编程语言那样灵活强大,但也是 bash 中不可或缺的一部分
- 数组的使用
# 直接赋值
array_name=(value1 value2 value3)
# 逐个赋值
array[0]="value1"
array[1]="value2"
array[2]="value3"、
# 获取数组长度
echo ${#array[@]}# 切片
array=(1 2 3 4 5)
echo ${array[@]:1:3} # 从索引 1 开始,取 3 个元素
for 循环
用于逐个处理一组数据(如列表、数组、文件等)。它特别适合在已知迭代次数或需要遍历集合时使用
基本语法:
- 传统派:
for 变量 in 列表; do# 执行的代码
done
- 现代派:
在 Bash 3.0+ 中可以采用 C 语言风格编写,类似 C/C++ 的 for (i=0; i<5; i++)
for (( 初始值; 条件; 步进 )); do# 执行的代码
done
示例:
- 批量复制数组中存在的文件
#!/bin/bashfiles=("file1.txt" "file2.txt" "file3.txt")for file in "${files[@]}"; doif [ -f "$file" ]; thenecho "$file exists, copying..."cp "$file" "/tmp/"elseecho "$file does not exist"fi
done
- 通过直接遍历命令结果来查找当前目录下的 txt 文件
#!/bin/bashfor file in *.txt; doecho "Found TXT file: $file"
done
- 检查多个主机是否在线
#!/bin/bashhosts=("192.168.1.1" "google.com" "8.8.8.8")for host in "${hosts[@]}"; doping -c 2 "$host" > /dev/nullif [ $? -eq 0 ]; thenecho "$host is online"elseecho "$host is offline"fi
done
ping -c 2 发送 2 个数据包,$? 检查命令退出状态
- 批量生成文件
#!/bin/bashfor (( i=1; i<=3; i++ )); dotouch "file$i.txt"echo "Created file$i.txt"
done#或是
for i in {1..3}; dotouch "file$i.txt"echo "Created file$i.txt"
done
【注】:遍历列表中的元素如果带有 空格 则该元素需要使用 " " 包裹**
循环之间的比较
循环类型 | 适用场景 | 示例 |
---|---|---|
for | 遍历列表、数组、范围 | for i in 1 2 3; do |
while | 条件不确定时 | while [ $x -lt 5 ]; do |
until | 条件为假时循环 | until [ $x -eq 0 ]; do |
函数
在 Bash 脚本中,函数(Function) 是一种将一组命令封装成可重用代码块的方法。定义一个函数后,可以通过调用它的名称来执行其中的代码,并且可以传递参数给函数以增加灵活性
解耦合,模块化,精简代码
基本语法:
- 使用
function
关键字
function 函数名 {# 函数体
}
- 精简写法
函数名() {# 函数体
}
示例:
#!/bin/bashgreet() {echo "Hello,$1! You are $2 years old,The time now is $(date +"%Y-%m-%d")"
}# 调用函数并传递参数
greet "Tom" 21
- 局部变量
默认情况,函数内的变量是全局的,可以用 local
关键字定义局部变量,避免污染外部环境
#!/bin/bashmy_function() {local name="$1" # 局部变量echo "Inside function: $name"
}name="Global"
my_function "Local"
echo "Outside function: $name"
- 返回值
- 当 Bash 函数没有直接返回值,则可以通过
return
返回退出状态(0~255,0表示成功)
#!/bin/bashcheck_number() {if [ "$1" -gt 0 ]; thenreturn 0 # 成功elsereturn 1 # 失败fi
}check_number 5
if [ $? -gt 0 ]; thenecho "Positive number"
elseecho "Non-positive number"
fi
$? 获取上一个命令(函数)的退出状态
- 通过
echo
返回值
#!/bin/bashadd() {
# 在 (()) 内计算 $1 + $2 的和
# 通过 $() 将计算结果捕获为字符串
# echo 将这个结果输出echo $(($1 + $2))
}# = 是用于判断字符串是否相同
# $() 将计算结果捕获为字符串
result=$(add 3 4)
echo "Sum is: $result"
在 (()) 内部,可以直接进行数学运算(如加减乘除),无需额外的命令(如 expr)
$1 + $2 表示将函数的第一个参数 $1 和第二个参数 $2 相加
exit 关键字
只需要记住: exit 命令会立即结束当前脚本的执行,并返回一个状态码给调用它的环境
退出状态码:
- 0:表示成功(默认值)
- 非0:表示失败或某种错误,通常由开发者定义具体含义
#!/bin/bashecho "Script starts"
exit
echo "This won't run"
- 带状态码退出
#!/bin/bash# 检查是否提供了参数
if [ $# -ne 1 ]; thenecho "Usage: $0 <filename>"exit 1
fi# 获取传入的文件名参数
filename="$1"echo "Checking file: $filename"
if [ -f "$filename" ]; thenecho "File exists"exit 0 # 成功退出
elseecho "File not found"exit 1 # 失败退出
fi
$# 表示参数个数,-ne 1 表示 “不等于 1"
如果参数数量不对,提示用法并退出(状态码 1)
- exit 与 return 的区别
特征 | exit | return |
---|---|---|
作用范围 | 终止整个脚本 | 仅退出当前函数 |
使用场景 | 脚本级别退出 | 函数级别返回 |
状态码 | 返回给父进程 | 返回给调用函数的地方 |
推荐文章:
Linux常用工具(LTS)_linux lts-CSDN博客
Linux手记(LTS)_linux lts-CSDN博客
bash 脚本
记录历史命令
#!/bin/bashLOG_DIR="$HOME/command_logs"
LOG_FILE="$LOG_DIR/command_history.log"if [ ! -d "$LOG_DIR" ]; thenmkdir -p "$LOG_DIR" || { echo "Error: Failed to create directory"; exit 1; }
fi# 确保历史文件存在
HISTFILE=${HISTFILE:-"$HOME/.bash_history"}# 强制写入当前会话历史
history -atimestamp=$(date "+%Y-%m-%d %H:%M:%S")# 从 $HISTFILE 获取前 50 条命令
commands=$(tail -n 50 "$HISTFILE")if [ -z "$commands" ]; thenecho "No commands found in history file $HISTFILE."exit 1
fiecho "Logging commands at $timestamp:" >> "$LOG_FILE"
echo "$commands" | while IFS= read -r cmd; doecho "[$timestamp] $cmd" >> "$LOG_FILE"
doneecho "Commands logged to $LOG_FILE"
exit 0
命令执行日志
cat ~/command_logs/command_history.log
查询文件
根据指定模式查询所有已连接服务器中包含 xx 内容或名称的文件或目录
#!/bin/bash# 检查是否提供了搜索模式
if [ $# -lt 1 ]; thenecho "Usage: $0 <pattern> [content_search]"echo " <pattern>: File or directory name pattern (e.g., '*.txt')"echo " [content_search]: Optional, search for this string in file contents"exit 1
fi# 获取参数
pattern="$1" # 文件或目录名称模式
content="$2" # 可选的内容搜索字符串# 定义已连接的服务器列表(假设通过 SSH 访问)
servers=("server1.example.com" "server2.example.com" "server3.example.com")
# 替换为实际的服务器地址或从配置文件读取# 本地搜索路径(可根据需要修改)
search_path="/home/user"# 循环遍历每个服务器
for server in "${servers[@]}"; doecho "Searching on $server..."# 如果没有指定内容搜索,则只查找文件名或目录名if [ -z "$content" ]; thenssh "$server" "find $search_path -name \"$pattern\"" 2>/dev/nullelse# 如果指定了内容搜索,则查找文件并检查内容ssh "$server" "find $search_path -name \"$pattern\" -type f -exec grep -l \"$content\" {} +" 2>/dev/nullfi
doneecho "Search completed."
exit 0
$1:文件名或目录名的模式(如 *.txt)
$2(可选):文件内容中要搜索的字符串(如 xx)servers 数组中列出目标服务器,需替换为实际地址
假设使用 SSH 访问,需配置免密登录
使用示例:
# 查找所有 .txt 文件
./search_servers.sh "*.txt"
# 查找包含 "xx" 的 .txt 文件
./search_servers.sh "*.txt" "xx"
分发内容
分发当前文件到指定服务器目录,可选择是否替换指定服务器原有的分发文件
#!/bin/bash# 检查参数数量
if [ $# -lt 2 ]; thenecho "Usage: $0 <file> <dest_path> [replace]"echo " <file>: File to distribute"echo " <dest_path>: Destination directory on servers (e.g., /home/user/files)"echo " [replace]: 'yes' to replace existing files, omit or any other value for no"exit 1
fi# 获取参数
file="$1" # 要分发的文件
dest_path="$2" # 目标路径
replace="$3" # 是否替换(yes 或其他)# 检查文件是否存在
if [ ! -f "$file" ]; thenecho "Error: File '$file' does not exist."exit 1
fi# 定义目标服务器列表
servers=("server1.example.com" "server2.example.com" "server3.example.com")
# 替换为实际服务器地址# 分发文件
for server in "${servers[@]}"; doecho "Distributing to $server..."# 检查目标路径是否已有同名文件if ssh "$server" "[ -f \"$dest_path/$(basename "$file")\" ]" 2>/dev/null; thenif [ "$replace" = "yes" ]; thenecho "Replacing existing file on $server..."scp "$file" "$server:$dest_path/" 2>/dev/nullelseecho "File exists on $server, skipping (use 'yes' to replace)."fielse# 文件不存在,直接分发scp "$file" "$server:$dest_path/" 2>/dev/nullif [ $? -eq 0 ]; thenecho "Successfully distributed to $server."elseecho "Failed to distribute to $server."fifi
doneecho "Distribution completed."
exit 0
$1:要分发的文件名
$2:目标服务器上的目录路径
$3(可选):yes 表示替换现有文件,否则跳过servers 数组中列出目标服务器,需替换为实际地址
假设使用 SSH 访问,需配置免密登录
使用示例:
# 分发文件,不替换已有文件
./distribute_file.sh myfile.txt /home/user/files
# 分发文件并替换已有文件
./distribute_file.sh myfile.txt /home/user/files yes