MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Bash脚本编程基础语法

2023-01-217.9k 阅读

1. Bash脚本的基本结构

Bash脚本是由一系列的Bash命令组成的文本文件。通常,一个Bash脚本以#!/bin/bash开头,这一行被称为shebang,它告诉系统使用/bin/bash来解释执行这个脚本。

例如,创建一个简单的脚本hello.sh

#!/bin/bash
echo "Hello, World!"

在上述脚本中,#!/bin/bash指定了使用Bash解释器,echo命令用于输出文本到标准输出。

2. 变量

2.1 变量的定义

在Bash中,变量无需事先声明类型。定义变量的语法为:变量名=变量值。注意,等号两边不能有空格。

name="John"
age=30

这里定义了一个字符串变量name和一个数值变量age

2.2 变量的引用

引用变量时,在变量名前加$符号。例如:

name="Alice"
echo "My name is $name"

上述代码会输出My name is Alice

2.3 环境变量

Bash中有许多预定义的环境变量,例如$PATH,它包含了系统查找可执行文件的路径。可以使用export命令将自定义变量提升为环境变量。

my_var="test"
export my_var

这样其他子进程就可以访问my_var变量。

3. 数据类型

3.1 字符串

字符串是Bash中最常用的数据类型之一。可以使用单引号或双引号来定义字符串。

单引号定义的字符串,其中的变量不会被替换,双引号定义的字符串,其中的变量会被替换。

name="Bob"
str1='My name is $name'
str2="My name is $name"
echo $str1
echo $str2

上述代码输出结果为:

My name is $name
My name is Bob

3.2 数值

Bash支持整数运算。可以使用let命令或((...))结构进行数值计算。

a=5
b=3
let result=a+b
echo $result

((result=a*b))
echo $result

上述代码分别输出815

4. 运算符

4.1 算术运算符

Bash支持常见的算术运算符,如+-*/%(取模)等。除了前面提到的let((...)),还可以使用expr命令,但expr要求运算符与操作数之间有空格。

a=10
b=3
result=$(expr $a + $b)
echo $result

4.2 比较运算符

用于比较数值或字符串。比较数值的运算符有-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)、-ge(大于等于)、-le(小于等于)。

a=5
b=3
if [ $a -gt $b ]; then
    echo "$a is greater than $b"
fi

比较字符串的运算符有=(等于)、!=(不等于)。

str1="hello"
str2="world"
if [ $str1 != $str2 ]; then
    echo "Strings are not equal"
fi

4.3 逻辑运算符

逻辑与(&&)、逻辑或(||)和逻辑非(!)。

a=5
b=3
if [ $a -gt 0 ] && [ $b -gt 0 ]; then
    echo "Both a and b are positive"
fi

5. 控制结构

5.1 if语句

基本语法为:

if [ 条件判断 ]; then
    命令序列1
else
    命令序列2
fi

例如:

age=20
if [ $age -ge 18 ]; then
    echo "You are an adult"
else
    echo "You are a minor"
fi

5.2 case语句

用于多分支选择,语法如下:

case 变量 in
模式1)
    命令序列1
    ;;
模式2)
    命令序列2
    ;;
*)
    命令序列3
    ;;
esac

示例:

fruit="apple"
case $fruit in
apple)
    echo "It's an apple"
    ;;
banana)
    echo "It's a banana"
    ;;
*)
    echo "Unknown fruit"
    ;;
esac

5.3 for循环

有两种常见的形式,一种是传统的C风格,另一种是基于列表的。

C风格:

for (( i = 1; i <= 5; i++ )); do
    echo $i
done

基于列表的:

fruits=("apple" "banana" "cherry")
for fruit in ${fruits[@]}; do
    echo $fruit
done

5.4 while循环

只要条件为真,就会一直执行循环体。

count=1
while [ $count -le 5 ]; do
    echo $count
    ((count++))
done

5.5 until循环

while相反,只要条件为假,就执行循环体。

count=1
until [ $count -gt 5 ]; do
    echo $count
    ((count++))
done

6. 函数

6.1 函数定义

语法为:

函数名() {
    命令序列
    [return 返回值]
}

例如:

add() {
    result=$(( $1 + $2 ))
    echo $result
}
sum=$(add 3 5)
echo $sum

6.2 函数参数

在函数内部,可以通过$1$2等变量来访问传递给函数的参数。$0表示脚本本身的名称,$#表示参数的个数,$*$@都表示所有参数,但在处理带空格的参数时有细微差别。

print_args() {
    echo "Number of arguments: $#"
    echo "All arguments: $*"
    echo "Each argument:"
    for arg in "$@"; do
        echo $arg
    done
}
print_args one "two three" four

7. 文件操作

7.1 文件测试

可以使用[ -条件 文件名 ]的形式来测试文件的属性。例如:

file="test.txt"
if [ -e $file ]; then
    echo "File exists"
fi

常见的文件测试条件有:

  • -e:文件或目录是否存在
  • -f:是否是普通文件
  • -d:是否是目录
  • -r:文件是否可读
  • -w:文件是否可写
  • -x:文件是否可执行

7.2 文件读取

可以使用read命令从文件中读取内容。例如:

while read line; do
    echo $line
done < test.txt

上述代码会逐行读取test.txt的内容并输出。

7.3 文件写入

使用>进行覆盖写入,>>进行追加写入。

echo "This is a test" > test.txt
echo "Another line" >> test.txt

8. 输入输出重定向

8.1 标准输出重定向

将命令的输出结果重定向到文件。例如:

ls -l > file_list.txt

8.2 标准错误输出重定向

将命令的错误信息重定向到文件。例如:

ls non_existent_file 2> error.txt

8.3 同时重定向标准输出和标准错误输出

ls non_existent_file &> combined.txt

8.4 输入重定向

将文件内容作为命令的输入。例如:

wc -l < test.txt

9. 管道

管道(|)用于将一个命令的输出作为另一个命令的输入。

ls -l | grep "txt"

上述代码先列出当前目录下的详细文件列表,然后通过管道将输出传递给grep命令,筛选出文件名中包含txt的行。

10. 数组

10.1 数组的定义

可以使用括号来定义数组。例如:

nums=(1 2 3 4 5)

也可以逐个赋值:

fruits[0]="apple"
fruits[1]="banana"
fruits[2]="cherry"

10.2 数组的访问

通过索引访问数组元素,索引从0开始。

nums=(1 2 3 4 5)
echo ${nums[0]}

获取数组所有元素:

fruits=("apple" "banana" "cherry")
echo ${fruits[@]}

获取数组长度:

nums=(1 2 3 4 5)
echo ${#nums[@]}

11. 特殊变量

11.1 $?

表示上一个命令的退出状态码。0表示命令成功执行,非0表示有错误发生。

ls
echo $?

11.2 $$

表示当前脚本的进程ID。

echo "My PID is $$"

11.3 $!

表示上一个在后台运行的进程ID。

sleep 10 &
echo "Background process ID: $!"

12. 调试Bash脚本

可以在脚本开头添加set -x来开启调试模式,它会在执行每条命令前打印出该命令及其参数。调试结束后,可以使用set +x关闭调试模式。

#!/bin/bash
set -x
a=5
b=3
result=$(( a + b ))
echo $result
set +x

此外,还可以使用bash -n来检查脚本语法错误,而不实际执行脚本。

bash -n my_script.sh

13. 与其他程序交互

Bash脚本可以调用外部程序,并处理它们的输出。例如,调用curl获取网页内容:

content=$(curl -s https://www.example.com)
echo $content

还可以调用Python脚本:

python_result=$(python -c "print(2 + 3)")
echo $python_result

14. 处理命令行参数

在Bash脚本中,可以通过$1$2等变量来访问命令行参数。例如,创建一个脚本args.sh

#!/bin/bash
echo "First argument: $1"
echo "Second argument: $2"

运行脚本时传递参数:

./args.sh hello world

输出:

First argument: hello
Second argument: world

还可以使用getopts来处理带选项的参数。例如:

#!/bin/bash
while getopts ":a:b:c" opt; do
    case $opt in
    a)
        var_a=$OPTARG
        ;;
    b)
        var_b=$OPTARG
        ;;
    c)
        echo "Option -c was specified"
        ;;
    \?)
        echo "Invalid option: -$OPTARG" >&2
        ;;
    :)
        echo "Option -$OPTARG requires an argument." >&2
        ;;
    esac
done
shift $((OPTIND - 1))
echo "var_a: $var_a"
echo "var_b: $var_b"
echo "Remaining arguments: $*"

运行脚本:

./script.sh -a value1 -b value2 other_arg1 other_arg2

输出:

var_a: value1
var_b: value2
Remaining arguments: other_arg1 other_arg2

15. 脚本的执行权限

为了能够执行Bash脚本,需要赋予脚本可执行权限。可以使用chmod命令来实现。

chmod +x my_script.sh

这样就可以直接通过./my_script.sh来执行脚本了。如果脚本没有可执行权限,可能会收到Permission denied的错误。

16. 信号处理

Bash脚本可以捕获和处理系统信号。使用trap命令来设置信号处理函数。

例如,捕获SIGINT(通常由Ctrl+C产生)信号:

#!/bin/bash
trap 'echo "Caught SIGINT, exiting gracefully..."' SIGINT
while true; do
    echo "Running..."
    sleep 1
done

当用户在脚本运行时按下Ctrl+C,脚本会捕获到SIGINT信号并执行相应的处理函数,输出提示信息后退出。

常见的信号有:

  • SIGINT:中断信号,通常由Ctrl+C产生
  • SIGTERM:终止信号,通常由kill命令发送
  • SIGHUP:挂起信号,通常在终端关闭时发送

17. 环境配置与脚本的可移植性

在编写Bash脚本时,考虑可移植性很重要。不同的系统可能使用不同版本的Bash,甚至可能使用其他Shell。

为了提高可移植性,可以:

  1. 使用标准的Shell语法:避免使用特定版本Bash的扩展语法。
  2. 检查环境变量:例如,检查$PATH是否包含必要的工具。
  3. 使用相对路径:尽量避免使用绝对路径,除非必要。

另外,在脚本开头使用#!/bin/sh而不是#!/bin/bash/bin/sh通常是一个POSIX兼容的Shell,能在更多系统上运行。但要注意,sh的功能可能比bash有限,需要根据实际需求选择。

18. 脚本的模块化与复用

对于大型的Bash脚本项目,将脚本模块化可以提高代码的可维护性和复用性。

可以将常用的功能封装成函数,放在一个单独的文件中,然后在其他脚本中通过source命令引入。

例如,创建一个common_functions.sh文件:

# common_functions.sh
add() {
    result=$(( $1 + $2 ))
    echo $result
}
subtract() {
    result=$(( $1 - $2 ))
    echo $result
}

然后在另一个脚本main.sh中使用:

#!/bin/bash
source common_functions.sh
sum=$(add 3 5)
echo "Sum: $sum"
diff=$(subtract 5 3)
echo "Difference: $diff"

这样,不同的脚本可以复用common_functions.sh中的函数,减少重复代码。

19. 日志记录

在Bash脚本中进行日志记录有助于调试和监控脚本的运行。可以使用echo配合输出重定向将日志信息写入文件。

例如:

#!/bin/bash
log_file="script.log"
echo "$(date): Starting script" >> $log_file
# 脚本主体部分
echo "$(date): Finishing script" >> $log_file

为了使日志更具可读性,可以添加时间戳等信息。还可以根据脚本的运行状态记录不同级别的日志,例如:

#!/bin/bash
log_file="script.log"
log() {
    local level=$1
    local message=$2
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "$timestamp [$level] $message" >> $log_file
}
log "INFO" "Starting script"
# 脚本主体部分
log "ERROR" "An error occurred"
log "INFO" "Finishing script"

这样可以清晰地区分不同类型的日志信息。

20. 性能优化

对于复杂的Bash脚本,性能优化很重要。以下是一些优化技巧:

  1. 减少外部命令调用:外部命令调用会消耗系统资源和时间,尽量使用Bash内置命令。例如,使用((...))进行数值计算比调用expr效率更高。
  2. 避免不必要的循环:如果可以通过一次性操作完成任务,就不要使用循环。
  3. 优化I/O操作:减少文件的读写次数,批量处理数据。

例如,假设要对一个文件中的所有数字求和,一种低效的方法是逐行读取并求和:

sum=0
while read num; do
    ((sum += num))
done < numbers.txt
echo $sum

一种更高效的方法是使用awk命令一次性处理:

sum=$(awk '{s+=$1} END {print s}' numbers.txt)
echo $sum

通过合理运用这些优化技巧,可以显著提高Bash脚本的执行效率。