一站式学习 Shell 脚本语法与编程技巧,踏出自动化的第一步

📢该篇博客已经完结,欢迎您的持续关注📢

关于Shell脚本语法的这篇博客到这里就完结了,非常感谢大家的阅读。如果对您有所帮助,请持续关注我的更新。

同时,欢迎您继续阅读我的另一篇文章:Linux常用命令详解,线上问题排查必备。我将持续更新线上问题排查及日常使用的一些Linux常用命令的使用教程,帮助大家更高效地解决问题和提升操作技能。期待与您在下一篇文章中再见!

1. 初识 Shell 解释器

在 Linux 系统中,Shell 是用户与操作系统之间的接口,它充当了命令行界面(CLI,Command Line Interface)和操作系统内核之间的桥梁。用户通过 Shell 输入命令,Shell 则将这些命令解析并执行。Shell 解释器不仅支持用户通过 CLI 输入单个命令,还可以将多个命令放入文件中作为程序执行,这就是 Shell 脚本的基础。

1.1 Shell 类型

Linux 的发行版众多,大多数都会包含多个 Shell,但通常会采用其中一个作为默认 Shell。常见的 Shell 解释器包括:

Shell 名称描述
ShBourne Shell,早期 Unix 系统中的标准 Shell
BashBourne Again Shell,最流行的 Shell,许多 Linux 发行版的默认 Shell
ZshZ-Shell,功能强大,提供用户友好的特性,Bash 的扩展版本
CshC-Shell,语法类似C语言,适合程序员使用
KshKorn Shell,结合了 Bourne Shell 和 C Shell 的特点。
FishFriendly Interactive Shell,专为用户友好性设计
DashDebian Almquist Shell,轻量级的 Shell,启动速度快,资源占用低,常用于 Debian 系统中的系统脚本和启动脚本

以 CentOS7.8 为例,我们要查看系统支持的所有 Shell 类型时,可以通过以下命令查看 /etc/shells 文件:

cat /etc/shells

该文件包含了系统上安装的所有可用 Shell 以及对应的路径:

image-20241022215748426

如果要查看当前用户默认使用的 Shell,可以通过环境变量 $SHELL 来查看:

echo $SHELL

image-20241022220259511

也可以通过查看 /etc/passwd 文件,/etc/passwd 文件中包含了系统中每个用户的信息,在用户ID记录的第7个字段中列出了默认的 Shell 程序。使用 grep 命令查找当前用户的条目:

grep "^$(whoami):" /etc/passwd

该命令会输出类似于以下格式的信息:

在这里插入图片描述

其中最后一个字段(/bin/bash)就是当前用户的默认 Shell。

1.2 Shell 的父子关系

Shell 不仅是一种命令行界面(CLI),它还是一个持续运行的复杂交互式程序。当用户登录到系统或打开一个终端窗口时,默认的 Shell(如 BashZsh 等)会被启动,系统会自动运行默认的 Shell 程序,这个 Shell 作为 父Shell 持续运行并等待用户的命令输入。

当用户在 父Shell 中输入 /bin/bash 或其他等效的命令时,系统会创建一个新的Shell实例,这个新的 Shell 称为 子Shell

子Shell的特性如下:

  • 独立性:子Shell相对于父Shell是独立的,它可以有自己的环境变量、目录、作业控制等。
  • 层次结构:可以创建多个子Shell,每个子Shell又可以创建自己的子Shell,从而形成一个层次结构。父子关系可以用树状结构表示。
  • 退出行为:当子Shell退出时,它会将控制权返回给父Shell,父Shell可以继续执行后续的命令。如果子Shell 在后台运行,则父Shell 也可以继续运行其他命令而不受影响。

父Shell子Shell的流程演示:

在这里插入图片描述

  1. 当我们登录到系统时,Linux 会启动一个默认的 Shell 程序(例如 Bash),这个 Shell 就是 父Shell。通过 ps -f 命令可以查看到它的进程ID是16229(PID),运行的是 bash shell 程序(CMD)。

  2. 父Shell 中输入 /bin/bash 命令后,系统会创建一个新的Shell实例,这个新的 Shell 称为 子Shell 。尽管窗口没有变化,但实际上我们已经进入了一个新的 Shell 环境,再次使用 ps -f 命令,可以查看到 子Shell 的进程信息(第二行):进程 ID 是16299(PID),运行的是 bash shell 程序(CMD)。

  3. 使用 echo $PPID 命令,可以看到 子Shell 的父进程ID(PPID)与 父Shell 的 PID 相同,这确认了 子Shell 是由 父Shell 创建的。

  4. 如果我们想返回到 父Shell ,只需在 子Shell 中输入 exit 命令, 子Shell 就会终止。此时, 父Shell 继续运行,等待我们的输入。

  5. 如果在 父Shell 中输入 exit 命令,父Shell 将会终止,整个终端会话结束,用户将被登出控制台终端。

2. 编写第一个 Shell 脚本

Tips:Shell 可以将多个命令串起来,一次执行完成。如果要多个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开。

对 Shell 解释器有了基本的认识后,我们就可以编写第一个 Shell 脚本了。

创建 Shell 脚本最基本的方式是使用分号(;)将多个命令串在一起,Shell 会按顺序逐个执行命令,例如:

命令1; 命令2; 命令3

这样,Shell 会先执行命令1,完成后再执行命令2,最后执行命令3。此外,如果我们希望在前一个命令成功执行后再执行下一个命令,可以使用 && 连接:

命令1 && 命令2 && 命令3

这样,只有当命令1成功执行(返回状态为0)时,命令2才会执行。这种办法只要不超过最大命令行字符数(255)限制,就能将任意多个命令串连在一起使用。

此外,我们可以将多个命令写入到文本文件中,每次执行时,直接执行文件即可,不需要每次手动输入这些命令。例如:

  1. 使用 vi 或其他文本编辑器创建脚本文件:

    vi hello_world.sh
    
  2. 编辑脚本,在文件中输入以下内容,按 Esc 键,输入 :wq 然后按 Enter 保存并退出。

    #!/bin/bash
    echo 'HelloWOlrd'
    
  3. 通过以下命令运行脚本:

    sh hello_world.sh
    
  4. 运行后,应该会看到以下输出:

    image-20241023220400134

3. Shell 脚本基本语法

3.1 脚本格式

每个 Shell 脚本文件的第一行以 #! 开头,后跟解释器的路径,例如 /bin/bash/bin/sh,这告诉系统用哪个 Shell 解释器来执行脚本,格式为:

#!/bin/bash

3.2 注释

3.2.1 单行注释

Shell 脚本中单行注释以 # 开头,表示注释一行,直到行尾。示例:

# 这是一个注释
echo "Hello, World!"  # 输出文本

3.2.2 多行注释

Shell 脚本没有专门的多行注释语法,要实现多行注释,可以使用 # 注释每一行:

# 这是一段多行注释
# 可以写很多内容
# 每一行前都要加 #

还可以通过 Here Document (here doc) 方式实现多行注释的效果,格式如下:

command <<DELIMITER
多行文本
...
DELIMITER
  • command 是你想要执行的命令。
  • DELIMITER 是一个自定义的标识符,可以是任意字符串,用于标记文本块的开始和结束。

由于冒号 : 是一个空命令,可以优化为 : + 空格 + 单引号 的格式实现多行注释:

: '
这是注释的部分。
可以有多行内容。
'

3.3 Shell 变量

Shell 脚本中的变量包括系统预定义变量(环境变量)和用户自定义变量;按照类型又分为局部变量、全局变量和只读变量。

3.3.1 系统预定义变量(环境变量)

系统预定义变量是由 Linux 操作系统提供的变量,包含了系统的配置信息和环境设置。这些变量可以直接在 Shell 脚本和命令行中使用。常见的环境变量如下:

环境变量说明
XDG_SESSION_ID当前用户会话的唯一标识符,可用于跟踪用户会话
HOSTNAME当前系统的主机名,用于在网络中识别计算机,可以通过修改 /etc/hostname 文件来更改它
TERM当前终端类型,例如 xterm-256color,影响终端的功能和外观
SHELL显示用户的默认 Shell 程序的路径,例如 /bin/bash
HISTSIZE控制命令历史记录的大小,通常为1000
USER当前登录用户的用户名,例如 root
PATH定义 Shell 查找可执行程序的路径。当用户输入命令时,系统会在这些路径中查找相应的可执行文件
PWD显示当前工作目录的绝对路径
LANG系统的语言设置,影响程序的输出和本地化设置,例如 zh_CN.UTF-8
SHLVL用于指示当前的 Shell 嵌套级别。每当启动一个新的 Shell 实例,该值就会增加
HOME当前用户的主目录,例如 /root
SSH_CONNECTION当通过SSH连接到远程服务器时,提供有关连接的信息,包括客户端和服务器的IP地址及端口
LESSOPEN用于设置 less 命令的预处理器,允许在查看文件时进行处理,例如文本高亮
SSH_CLIENT类似于 SSH_CONNECTION,SSH客户端的IP地址、源端口和服务端口
printenv 查看所有环境变量

在 Linux 操作系统中可以使用 printenv 命令以键值对的形式输出当前所有的环境变量。

  1. 基本用法

    printenv
    

    运行这个命令会列出所有当前的环境变量及其值,以键值对的形式显示。例如:

    USER=john
    HOME=/home/john
    SHELL=/bin/bash
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    
  2. 查看特定变量

    printenv VARIABLE_NAME
    

    可以通过指定变量名来查看某个特定环境变量的值。例如:

    printenv PATH
    

    这将只输出 PATH 环境变量的值:

    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    
set 查看所有 Shell 变量

set 命令是 Linux 的一个 Shell 内建命令,用于设置、显示或取消 Shell 的环境变量和 Shell 选项。 Shell 变量是指在 Shell 环境中定义的任何变量。它们可以是系统预定义变量(如 $PATH)或者用户自定义变量。

  1. 显示所有变量

    set
    

    运行此命令会列出当前 Shell 环境中的所有变量(包括环境变量和用户自定义变量)及其值。输出通常包含各个变量的名称和对应的值。例如:

    BASH=/bin/bash
    BASH_VERSION='5.0.17(1)-release'
    USER=john
    ...
    
  2. 设置变量

    set VARIABLE_NAME=value
    

    直接使用 set 可以为变量赋值,但这种方式只在当前 Shell 会话中有效。示例:

    set myvar=Hello
    echo $myvar  # 输出 Hello
    

3.3.2 声明变量

用户可以在 Shell 脚本中自定义变量存储数据、结果或其他信息,并在脚本上下文中引用。

Shell 脚本声明变量需要遵循以下规则:

  • 格式:变量名和等号之间不能有空格。例如,VAR=value 是正确的,而 VAR = value 是错误的。

  • 命名规则

    • 只包含字母、数字和下划线:变量名可以包含字母(大小写敏感)、数字和下划线(_),但不能包含其他特殊字符。
    • 不能以数字开头:变量名不能以数字开头,但可以包含数字。例如,VAR1 是有效的,而 1VAR 是无效的。
    • 避免使用 Shell 关键字:不应使用 Shell 的关键字(如 ifthenelsefiforwhile 等)作为变量名,以免引起混淆。
    • 使用大写字母表示常量:常量的变量名通常使用大写字母,例如 PI=3.14
    • 避免使用特殊符号:尽量避免在变量名中使用特殊符号,因为它们可能与 Shell 的语法产生冲突。
    • 避免使用空格:变量名中不应包含空格,因为空格通常用于分隔命令和参数。
字符串变量

在 Shell 中,变量默认被视为字符串类型,支持使用单引号 ' 和双引号 " 来定义字符串:

  • 使用单引号 ' 定义:单引号中的所有字符都被视为字面值,完全不进行变量替换或转义

    my_string='Hello, $USER!'
    echo "$my_string"  # 输出: Hello, $USER!
    

    在这个例子中,$USER 被视为普通文本,不会被替换为实际的用户名。

  • 使用双引号 " 定义:双引号中的变量会被替换为对应的变量值,某些转义字符(如 \n\t 等)也会被解析

    my_string="Hello, $USER!"
    echo "$my_string"  # 输出: Hello, Alice!
    

对字符串变量常用的操作如下:

  1. 双引号 " 实现多个字符串拼接:

    greeting="Hello"
    name="Alice"
    full_greeting="$greeting, $name!"
    echo "$full_greeting"	# 输出: Hello, Alice!
    
  2. 使用 ${#var} 来获取字符串的长度:

    length=${#my_string}
    echo "字符串长度: $length"  # 输出: 字符串长度: 13
    
  3. 使用 ${var:start:length} 截取字符串:

    substring=${my_string:7:5}  # 从索引 7 开始,长度为 5
    echo "$substring"  # 输出: World
    

    如果需要从指定位置截取到末尾,可以使用 ${var:start} 语法,而不需要指定长度:

    my_string="Hello, World!"
    substring=${my_string:7}  # 从索引 7 开始截取到末尾
    echo "$substring"  # 输出: World!
    
  4. 使用 ${var/old/new} 来替换字符串中的部分内容:

    my_string="Hello, World!"
    new_string=${my_string/World/Bash}
    echo "$new_string"  # 输出: Hello, Bash!
    

Tips:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;

  • 在单引号字符串中,不能直接包含单独的单引号。如果需要在字符串中使用单引号,可以成对出现,或通过拼接来实现。比如:

    echo 'It'\''s a test'  # 输出: It's a test
    
    1. ':先结束当前的单引号字符串。
    2. \':转义单引号,插入一个字面值的单引号。
    3. ':再开始一个新的单引号字符串。
数组变量

Shell 只支持一维数组(不支持多维数组),初始化时不需要指定数组大小,元素的下标由 0 开始,其声明的方式如下:

  1. 使用括号 () 定义数组:

    my_array=(element1 element2 element3)
    
  2. 使用 declare -a 定义数组:

    declare -a my_array
    my_array=(element1 element2 element3)
    

数组支持的常用操作如下:

  1. 通过 ${array[index]} 来访问指定下标的数组元素:

    echo "${my_array[0]}"  # 输出: element1
    
  2. 使用 ${#array[@]} 获取数组的长度:

    length=${#my_array[@]}
    echo "数组长度: $length"  # 输出: 数组长度: 3
    
  3. 使用 @* 可以获取数组中的所有元素:

    echo "${my_array[@]}"  # 输出: element1 element2 element3
    
  4. 使用 for 循环遍历数组的所有元素:

    for item in "${my_array[@]}"; do
        echo "$item"
    done
    
  5. 通过索引直接修改数组的某个元素:

    my_array[1]="new_value"  # 修改第二个元素
    echo "${my_array[1]}"  # 输出: new_value
    
  6. 使用 += 添加新元素:

    my_array+=("element4")  # 添加新元素
    echo "${my_array[@]}"  # 输出: element1 element2 element3 element4
    

Tips:

  • 在定义数组时,数组元素之间需要以空格进行分隔
  • 访问或打印数组时,元素之间会自动以空格拆分
关联数组(Map)变量

关联数组(也称为字典或哈希表)允许使用非整数索引来存储键值对。使用 declare -A 命令来定义:

declare -A fruits
fruits=(["apple"]="red" ["banana"]="yellow" ["cherry"]="red")

也可以在声明时初始化:

declare -A fruits=(["apple"]="red" ["banana"]="yellow" ["cherry"]="red")

常用操作如下:

  1. 通过 ${map[key]} 来访问关联数组中指定 Key 对应的 Value:

    echo "${fruits[apple]}"  # 输出: red
    
  2. ${!map[@]} 在数组前添加感叹号 ! 可以获取数组的所有键:

    echo "所有水果: ${!fruits[@]}"  # 输出: apple banana cherry
    
  3. ${map[@]} 使用 @* 可以获取数组中的所有元素:

    echo "${fruits[@]}" # 输出: red yellow red
    
  4. 使用 for 循环遍历关联数组的所有键及其对应的值:

    for key in "${!fruits[@]}"; do
        echo "$key: ${fruits[$key]}"
    done
    
  5. 修改元素:

    map[key]="new_value"
    
  6. 新增元素:

    map[new_key]="new_value"
    
  7. 使用 unset 命令删除关联数组中的某个元素:

    unset map[key]
    
只读变量

只读变量是定义后无法被修改的变量。使用 readonly 命令可以将变量设置为只读,这样在脚本的后续部分尝试修改该变量将会报错。

# 定义只读变量
readonly MY_READONLY_VAR="This variable is read-only"

# 输出变量的值
echo $MY_READONLY_VAR                                     

# 尝试修改只读变量,将报错:cannot assign to read-only variable
MY_READONLY_VAR="New Value"

3.3.3 引用变量

在脚本上下文中,我们可以在变量名称之前加上美元符($)来引用声明的变量以及系统环境变量。以下是几种常用的引用方法:

  • 使用 $variable 形式引用变量

    echo $PATH
    
  • 使用花括号 ${variable} 形式引用的变量

    echo "Value: ${my_var}"
    echo "Value: ${my_var}123"  # 输出:Value: Hello, World!123
    

    当变量后面紧跟其他字符时,花括号 {} 可以使变量边界更清晰。

Tips:

  • 对一个变量进行赋值时不使用 $ 符号
  • 引用一个变量值时必须使用美元符($
  • 在赋值语句中使用其他变量的值时,必须使用 $ 符号
#!/bin/bash

# 1. 赋值时不使用美元符
value1="Hello"
value2="World"

# 2. 在赋值语句中使用 value1 的值时,必须使用美元符
greeting="$value1, $value2!"

# 3. 引用变量值时使用美元符
echo "$greeting"

3.3.4 变量的作用域

全局变量

全局变量是指允许在整个脚本中(以及在其启动的所有子进程(即子Shell)中)访问的变量,全局变量可以直接在脚本的顶部或函数外部定义:

MY_GLOBAL_VAR="Hello, World!"  # 定义全局变量
echo $MY_GLOBAL_VAR              # 输出变量的值

可以使用 export 命令将其导出为环境变量,以便在子进程(即子Shell)中访问。

局部变量

局部变量是在特定的作用域内定义的变量,通常是在函数内定义:

my_function() {
    local MY_LOCAL_VAR="This is a local variable"  # 定义局部变量
    echo $MY_LOCAL_VAR                                # 在函数内访问
}

my_function            # 调用函数
echo $MY_LOCAL_VAR     # 输出为空,无法访问局部变量

当函数执行完毕后,局部变量会被销毁,无法在函数外部访问。

Tips:使用 local 定义的变量是局部的,只在其所在的函数内有效,无法通过 export 导出为环境变量供子进程(即子Shell)使用。

3.3.5 撤销变量

后续不再需要声明的变量时,可以使用 unset 命令来撤销已经声明的变量:

MY_VAR="Some value"  # 定义变量
unset MY_VAR        # 撤销变量

echo $MY_VAR        # 不会输出任何内容

撤销后,该变量不再存在,尝试引用将不会返回任何值,unset 命令不能删除只读变量。

3.4 Shell 脚本传递参数

在执行 Shell 脚本时,我们可以通过以下形式向脚本传递参数:

./example.sh 参数1 参数2 参数3

在脚本内通过位置参数 $n 来获取参数的值,例如,$0 :执行的文件名(包含文件路径),$1 表示第一个参数,$2 表示第二个参数,当 n>=10 时,需要使用大括号 ${n} 来获取参数。

Shell 脚本还支持如下的特殊变量来操作传递参数:

符号说明
$n引用参数
$@获取所有参数(每个参数作为单独的字符串),适合在循环中使用
$#获取传递给脚本的参数个数
$*将所有参数作为一个字符串处理
$$当前脚本的进程 ID
$?上一个命令的退出状态码,也可以获取上一个函数返回的结果

示例如下:

  1. 创建一个 Shell 脚本 example.sh,添加以下内容:

    #!/bin/bash
    
    echo "第一个参数是: $1"
    echo "第二个参数是: $2"
    echo "第三个参数是: $3"
    
    # 通过 $@ 获取所有参数
    echo "所有参数: $@"
    
    # 通过 $# 获取参数的数量
    echo "参数的数量: $#"
    
  2. 运行脚本并传递参数:

    ./example.sh 参数1 参数2 参数3
    
  3. 输出结果:

    第一个参数是: hello
    第二个参数是: world
    第三个参数是: 123
    所有参数: hello world 123
    参数的数量: 3
    

3.5 命令替换

命令替换是 Shell 脚本中最有用的特性之一,它允许将一个命令的输出作为另一个命令的输入。在 Shell 脚本或命令行中,可以使用命令替换来动态地获取数据并将其赋给变量,之后就可以随意在脚本中使用,相较于直接在命令行中输入多个命令,命令替换使得脚本更简洁明了。

命令替换有两种主要的写法:

  1. 使用反引号(command ):

    result=`command`
    
    • result:用于存储命令的输出
    • command:希望执行的命令
  2. 使用美元符号和括号($()),这是更推荐的方式:

    result=$(command)
    

命令替换示例如下:

  1. 获取当前日期:

    current_date=$(date)
    echo "当前日期和时间: $current_date"
    
  2. 获取当前用户:

    current_user=$(whoami)
    echo "当前用户: $current_user"
    
  3. 计算文件数量

    file_count=$(ls | wc -l)
    echo "当前目录下的文件数量: $file_count"
    
  4. 嵌套命令:

    file_count=$(ls $(pwd) | wc -l)
    echo "当前目录中的文件数量: $file_count"
    

    内层的 ls $(pwd) 命令的输出被传递给 wc -l 命令,以计算当前目录中的文件数量。

Tips:命令替换会创建一个子 Shell 来运行对应的命令。子 Shell 是由运行该脚本的 Shell 所创建出来的一个独立的子 Shell 。正因如此,由该子 Shell 所执行命令无法使用脚本中所创建的变量。

3.6 运算符

运算符用于执行各种操作,Shell 脚本中的运算符分为多种类型,包括算术运算符、关系运算符、逻辑运算符、位运算符、字符串运算符和文件测试运算符等。

3.6.1 特殊运算符

[](测试命令)

[] 是 Shell 中的测试命令,通常用于条件判断。它也称为“test”命令。这个结构用于检查条件表达式的结果,如比较数值、字符串或文件属性。

基本语法如下:

if [ condition ]; then
    # 条件为真时执行的命令
fi

Tips:在方括号内外,操作数和运算符之间必须有空格。例如 [ -f file.txt ] 是有效的,而 [ -ffile.txt ] 是无效的。

[[]](测试命令扩展)

[[ ]] 是 Shell 脚本中进行条件测试的推荐方式,比传统的 [ ] 具有更多的功能和灵活性,[[ ]] 支持更复杂的条件表达式,比如正则表达式匹配:

#!/bin/bash

input="abc123"

if [[ $input =~ [a-z]+[0-9]+ ]]; then
    echo "Input matches the regex"
fi
$(())(算术扩展)

$(()) 是用于进行算术运算的结构,它允许在 Shell 脚本中直接进行数学计算。这个结构会返回运算的结果,可以直接用于变量赋值或其他表达式中。

基本语法:

result=$(( 算术表达式 ))

3.6.2 算术运算符

算术运算符用于执行基本的数学运算。

运算符描述示例
+加法echo $((5 + 3))
-减法echo $((5 - 3))
*乘法echo $((5 * 3))
/除法echo $((6 / 3))
%取模(余数)echo $((5 % 3))

3.6.3 关系运算符

关系运算符用于比较两个值,返回真或假。

运算符描述示例
-eq等于[ $a -eq $b ]
-ne不等于[ $a -ne $b ]
-gt大于[ $a -gt $b ]
-lt小于[ $a -lt $b ]
-ge大于等于[ $a -ge $b ]
-le小于等于[ $a -le $b ]

3.6.4 逻辑运算符

逻辑运算符用于执行布尔逻辑操作。

运算符描述示例
&&逻辑与command1 && command2
||逻辑或command1 || command2
!逻辑非if [ ! -f file.txt ]; then ...

3.6.5 位运算符

位运算符用于按位操作整数值。

运算符描述示例
&按位与expr $a & $b
|按位或expr $a | $b
^按位异或expr $a ^ $b
~按位取反expr ~ $a
<<左移expr $a << 1
>>右移expr $a >> 1

3.6.6 文件测试运算符

文件测试运算符用于检查文件的属性。

运算符描述示例
-e检查文件是否存在[ -e filename ]
-f检查是否为普通文件[ -f filename ]
-d检查是否为目录[ -d dirname ]
-r检查文件是否可读[ -r filename ]
-w检查文件是否可写[ -w filename ]
-x检查文件是否可执行[ -x filename ]

3.6.7 字符串运算符

字符串运算符用于比较字符串。

运算符描述示例
=检查字符串是否相等[ "$str1" = "$str2" ]
!=检查字符串是否不等[ "$str1" != "$str2" ]
-z检查字符串是否为空[ -z "$str" ]
-n检查字符串是否非空[ -n "$str" ]

3.6.8 赋值运算符

赋值运算符用于将值赋给变量。

运算符描述示例
=赋值a=10
+=追加赋值a+=5a 的值变为 105

4. Shell 脚本流程控制

与 Java、PHP 等语言不同的是,Shell 中的每个控制结构必须包含可执行的代码块,不能为空。

4.1 条件语句

4.1.1 if-then

最基本的条件判断结构就是 if-then 语句,if-then 语句基本语法如下:

if command
then
    commands
fi

Bash 中的 if 语句与许多其他编程语言有所不同。在 Bash 中,if 语句会运行 if 后面的那个命令,命令执行后会返回一个退出状态码:

  • 退出状态码为 0:表示命令成功执行,此时会执行 then 部分的命令。
  • 退出状态码为非 0:表示命令执行失败,此时会执行 else(如果有)部分的命令。

示例:

#!/bin/bash

# 检查目录是否存在
if [ -d "/path/to/directory" ]
then
    echo "Directory exists."
fi

我们也可以在条件表达式的尾部添加分号;,就可以将 then 关键字放在同一行上了:

#!/bin/bash

# 检查目录是否存在
if [ -d "/path/to/directory" ]; then
    echo "Directory exists."
fi

4.1.2 if-then-else

这个结构增加了一个 else 分支,用于处理条件为假的情况。

基本语法:

if [ 条件 ]; then
    # 条件为真时执行的代码
else
    # 条件为假时执行的代码
fi

示例:

#!/bin/bash

num=-1

if [ $num -gt 0 ]; then
    echo "$num is positive"
else
    echo "$num is not positive"
fi

4.1.3 if-elif

if-elif 结构允许检查多个条件,每个 elif 可以有不同的条件,类似 Java 中的 if-else if-else

基本语法:

if [ 条件1 ]; then
    # 条件1为真时执行的代码
elif [ 条件2 ]; then
    # 条件2为真时执行的代码
else
    # 条件都不满足时执行的代码
fi

示例:

#!/bin/bash

num=0

if [ $num -gt 0 ]; then
    echo "$num is positive"
elif [ $num -eq 0 ]; then
    echo "$num is zero"
else
    echo "$num is negative"
fi

4.2 循环

Shell 脚本中提供了 forwhileuntil 命令实现循环处理重复的逻辑。

4.2.1 for 循环

for 循环用于遍历一个集合(例如列表、数组、字符串等)中的每个元素,并对每个元素执行一段代码,比如处理某个目录下的所有文件、系统上的所有用户或是某个文本文件中的所有行。for 循环非常适合处理已知数量的迭代。基本语法如下:

for variable in list; do
    commands
done
  • variable 是一个变量,用于存储当前迭代的元素。
  • list 是要遍历的元素列表,可以是静态值命令输出范围

使用示例如下:

  1. 静态值遍历数字:

    #!/bin/bash
    
    for i in 1 2 3 4 5; do
        echo "当前数字是 $i"
    done
    
  2. 遍历当前目录下的所有 .txt 文件名:

    #!/bin/bash
    
    for file in *.txt; do
        echo "处理文件: $file"
    done
    
  3. 使用命令输出作为列表:

    #!/bin/bash
    
    for file in $(ls); do
        echo "文件: $file"
    done
    

    使用 $(ls) 获取当前目录下的所有文件名。

  4. 使用 {start..end} 语法生成一个范围:

    #!/bin/bash
    
    for i in {1..5}; do
        echo "数字: $i"
    done
    
  5. 遍历数组

    #!/bin/bash
    
    array=(apple banana cherry)
    
    for fruit in "${array[@]}"; do
        echo "水果: $fruit"
    done
    
  6. 嵌套 for 循环

    #!/bin/bash
    
    for i in {1..2}; do
        for j in {1..3}; do
            echo "外循环: $i, 内循环: $j"
        done
    done
    

4.2.2 while 循环

while 循环在条件为真时持续执行,常用于读取用户输入、读取文件内容。语法如下:

while [ condition ]; do
    commands
done

使用示例如下:

  1. 计数到 5:

    #!/bin/bash
    
    count=1
    while [ $count -le 5 ]; do
        echo "计数: $count"
        ((count++))  # 递增计数
    done
    
  2. 逐行读取文件内容并输出到指定文件:

    #!/bin/bash
    
    # 读取文件的每一行
    filename="example.txt"
    
    while read -r line; do
        echo "当前行: $line"
    done < "$filename"
    
  3. 读取用户输入,直到满足某个条件为止:

    #!/bin/bash
    
    # 初始化变量
    input=""
    
    # 读取用户输入
    while [ "$input" != "exit" ]; do
        read -p "请输入一个命令(输入 'exit' 退出): " input
        echo "你输入的命令是: $input"
    done
    
    echo "退出循环"
    

4.2.3 until 循环

until 循环与 while 循环相反,当条件为假时执行。语法如下:

until [ condition ]; do
    commands
done

4.2.4 退出循环和跳过当前迭代

和 Java 一样,Shell 也支持在循环体中使用 continue 跳过当前迭代,break 退出循环。

使用 break 退出循环

#!/bin/bash

for i in {1..5}; do
    if [ $i -eq 3 ]; then
        echo "遇到 3,退出循环"
        break  # 退出循环
    fi
    echo "处理 $i"
done

输出:

处理 1
处理 2
遇到 3,退出循环

使用 continue 跳过当前迭代

#!/bin/bash

for i in {1..5}; do
    if [ $i -eq 3 ]; then
        echo "跳过 $i"
        continue  # 跳过当前迭代
    fi
    echo "处理 $i"
done

输出:

处理 1
处理 2
跳过 3
处理 4
处理 5

5. Shell 自定义函数

Shell 支持用户将一系列命令封装成一个自定义函数,然后再脚本中重复使用。

5.1 定义函数

定义函数的基本语法如下:

[function] function_name() {
    # commands
}

定义函数时可以省略 function 关键字,直接使用 function_name() 格式定义:

my_function() {
    echo "Hello World!"
}

5.2 调用函数

函数必须在声明之后才能被调用。如果在函数声明之前调用,Shell 会提示找不到该命令:

image-20241103200005485

定义好函数后,可以直接使用函数名调用:

#!/bin/bash

my_function() {
    echo "Hello World!"
}

my_function # 输出: Hello World!

5.3 函数传数

在函数调用时可以向函数传递参数,只需要在函数内使用位置参数 $n 来访问传入的参数:

#!/bin/bash

my_function() {
    local param1=$1  # 第一个参数
    local param2=$2  # 第二个参数
    echo "Param 1: $param1"
    echo "Param 2: $param2"
}

my_function "Hello" "World"

image-20241103200305102

也可以使用参数替换的方式为参数设置默认值,三种格式如下:

  • ${parameter:-default}:如果 parameter 未设置或为空,则使用 default 值。
  • ${parameter-default}:如果 parameter 未设置(即没有定义),使用 default 值,但如果 parameter 是空字符串,则仍然使用其原值。
  • ${parameter:+value}:如果 parameter 被设置且非空,则使用 value

使用样例如下:

  1. 使用 $n 访问多个参数:

    sum_numbers() {
        local sum=$(( $1 + $2 + $3 ))  # 计算前三个参数的和
        echo "The sum is: $sum"
    }
    
    sum_numbers 5 10 15  # 输出: The sum is: 30
    
  2. 使用 $@$* 访问所有传递给函数的参数:

    print_all_params() {
        echo "All parameters using \$@:"
        for param in "$@"; do
            echo "$param"
        done
    }
    
    print_all_params "one" "two" "three"
    
  3. 使用 $# 来获取传递给函数的参数个数:

    count_params() {
        echo "Number of parameters: $#"
    }
    
    count_params "param1" "param2"  # 输出: Number of parameters: 2
    
  4. 为参数设置默认值:

    greet() {
        local name=${1:-"Guest"}  # 如果第一个参数未提供,使用"Guest"
        echo "Hello, $name!"
    }
    
    # 调用函数
    greet         # 输出: Hello, Guest!
    greet "Alice" # 输出: Hello, Alice!
    
    • ${1:-"Guest"}:检查第一个参数 $1 是否为空。如果为空,则使用默认值 "Guest"
    • 如果传入一个参数(如 "Alice"),那么 name 将会被赋值为 "Alice"

5.4 函数返回值

函数的返回值可以通过两种主要方式来处理:返回状态码和输出结果。

5.4.1 使用 return 返回状态码

函数可以使用 return 命令来返回一个状态码(0-255),在调用函数只能通过 $? 来获取 return 命令返回的状态码。

return 命令只能返回一个介于 0 到 255 之间的整数,因此通常用来表示函数的执行结果。例如,返回 0 表示成功,非零值表示失败。如果不显示的调用 return 命令,将以函数的最后一条命令的运行结果,作为返回值。

使用示例:

my_function() {
    if [ "$1" -gt 0 ]; then
        return 0  # 成功
    else
        return 1  # 失败
    fi
}

# 调用函数并检查返回值
my_function 5
echo $?  # 输出0,表示成功

5.4.2 使用 $() 捕获 echo 的输出结果

echo 命令可以将函数的计算结果、变量的值等直接打印在标准输出(通常是屏幕上),我们可以在调用函数时使用命令替换来捕获结果。

使用示例如下:

add_numbers() {
    local sum=$(( $1 + $2 ))
    echo $sum  # 输出结果
}

# 捕获函数输出
result=$(add_numbers 5 10)
echo "The sum is: $result"  # 输出: The sum is: 15

6. 输入/输出重定向

在 Linux 系统中,每个命令的执行都会涉及输入和输出的处理。每个命令在运行时通常会打开三个文件:

  • 标准输入(STDIN):默认来源是终端(键盘),用于接收用户输入。
  • 标准输出 (STDOUT):命令的正常输出结果,默认返回到终端(显示屏)。
  • 标准错误(STDERR):用于输出错误信息和警告,通常也会显示在终端,以帮助用户识别问题。

通过输入和输出重定向,我们可以灵活地控制命令执行时的输入来源和输出目标。这在处理文件和日志记录时尤为重要。

6.1 基本重定向命令

基本重定向命令可以控制命令输入、输出的来源和去向,例如将命令的输出写入文件、从文件中读取输入或将输出追加到已有文件中。

命令说明
command > filecommand 的标准输出重定向到 file,如果 file 已存在,则覆盖其内容。
command < filefile 的内容作为 command 的标准输入。
command >> filecommand 的标准输出追加到 file 的末尾,如果 file 不存在,则创建它。

使用示例:

  1. ls 的输出重定向到文件 list.txt:

    ls > list.txt
    
  2. 将 list.txt 的内容作为 cat 的输入:

    cat < list.txt
    
  3. echo 的输出追加到 log.txt:

    echo "This is a log entry." >> log.txt
    

Tips:使用 > 时会覆盖文件内容,而 >> 则是追加到文件末尾。

6.2. 文件描述符重定向

除了基本的重定向命令,我们还可以使用文件描述符重定向更灵活地管理输入和输出。标准输入、标准输出和标准错误的描述符如下:

文件描述符描述默认用途
0标准输入 (stdin)接收输入(通常来自键盘)
1标准输出 (stdout)输出结果(默认到终端)
2标准错误 (stderr)输出错误信息(默认到终端)

文件描述符重定向命令格式如下:

命令说明
n > file将文件描述符 n 的输出重定向到 file
n >> file将文件描述符 n 的输出追加到 file
n >& m将文件描述符 n 的输出重定向到文件描述符 m,合并它们的输出。
n <& m将文件描述符 m 的输入重定向到文件描述符 n,合并它们的输入。

使用示例:

  1. 将错误信息写入 error.log 文件:

    ls file.txt 2> error.log
    
  2. 合并输出和错误,将标准输出和标准错误都重定向到 output.txt 文件:

    command > output.txt 2>&1
    
  3. 将标准输出和标准错误分别重定向到不同的文件:

    command > output.txt 2> error.txt
    

7. Shell 脚本执行

执行 Shell 脚本的方法有三种主要形式,每种形式在执行环境、变量作用域、退出状态和权限要求等方面都有显著的不同。

7.1 直接调用脚本(子Shell)

直接调用脚本是一种常见的方式,该方式会在一个子 Shell 中运行脚本,因此脚本内定义的变量在外部是不可见的。

通过直接调用脚本的方式运行脚本只需在命令行输入以下格式的命令:

./script_name.sh
  • ./ 表示当前目录。如果脚本位于其他目录,需要提供相应的路径,比如 /path/to/script_name.sh
  • script_name.sh 脚本文件名,后缀是 .sh

这种方式在执行之前,需要确保脚本文件具有可执行权限,可以通过以下命令来设置:

chmod +x script_name.sh

7.2 指定 Shell 解释器(子Shell)

直接调用脚本执行时 Shell 解释器使用的是当前用户默认的解释器,如果我们希望执行时使用指定的 Shell 解释器,通常是使用以下命令:

bash script_name.sh

或者可以使用其他 Shell,例如:

sh script_name.sh    # 使用 Bourne Shell
zsh script_name.sh   # 使用 Z Shell
ksh script_name.sh   # 使用 Korn Shell

与直接调用脚本不同,这种方法不要求脚本具有可执行权限,可以直接运行。此外,脚本会在一个子进程中执行,定义的变量不会影响到当前的 Shell 环境。

7.3 使用 source 或 . 命令执行

除了上述两种方式,还可以使用 source. 命令来执行脚本,格式如下:

source myscript.sh
# 或
. myscript.sh

这种方式的显著特点是脚本在当前的 Shell 环境中执行,而不是在子 Shell 中。即我们在脚本中定义的变量在当前的 Shell 中是可见的,我们可以通过这种方式使用脚本来修改当前 Shell 的环境,例如定义环境变量、函数或别名。

同时,使用 source 执行脚本时,脚本的退出状态也会影响到当前的 Shell。例如在顶级父 Shell 中使用 source. 执行脚本,而脚本中包含 exit 命令,脚本的执行将会导致整个顶级父 Shell 退出。

#!/bin/bash

exit # 退出当前 Shell

image-20241103222702826

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@赵士杰

如果对你有用,可以进行打赏,感

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值