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

文章目录

  • [1. 初识 Shell 解释器](#1. 初识 Shell 解释器)
    • [1.1 Shell 类型](#1.1 Shell 类型)
    • [1.2 Shell 的父子关系](#1.2 Shell 的父子关系)
  • [2. 编写第一个 Shell 脚本](#2. 编写第一个 Shell 脚本)
  • [3. Shell 脚本语法](#3. Shell 脚本语法)
    • [3.1 脚本格式](#3.1 脚本格式)
    • [3.2 注释](#3.2 注释)
      • [3.2.1 单行注释](#3.2.1 单行注释)
      • [3.2.2 多行注释](#3.2.2 多行注释)
    • [3.3 Shell 变量](#3.3 Shell 变量)
      • [3.3.1 系统预定义变量(环境变量)](#3.3.1 系统预定义变量(环境变量))
        • [printenv 查看所有环境变量](#printenv 查看所有环境变量)
        • [set 查看所有 Shell 变量](#set 查看所有 Shell 变量)
      • [3.3.2 声明变量](#3.3.2 声明变量)
      • [3.3.3 引用变量](#3.3.3 引用变量)
      • [3.3.4 变量的作用域](#3.3.4 变量的作用域)
      • [3.3.5 撤销变量](#3.3.5 撤销变量)

1. 初识 Shell 解释器

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

1.1 Shell 类型

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

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

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

bash 复制代码
cat /etc/shells

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

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

echo $SHELL

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

bash 复制代码
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 会按顺序逐个执行命令,例如:

bash 复制代码
命令1; 命令2; 命令3

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

bash 复制代码
命令1 && 命令2 && 命令3

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

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

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

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

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

    shell 复制代码
    sh hello_world.sh
  4. 运行后,应该会看到以下输出:

3. Shell 脚本语法

3.1 脚本格式

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

shell 复制代码
#!/bin/bash

3.2 注释

3.2.1 单行注释

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

bash 复制代码
# 这是一个注释
echo "Hello, World!"  # 输出文本

3.2.2 多行注释

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

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

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

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

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

shell 复制代码
: '
这是注释的部分。
可以有多行内容。
'

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. 基本用法

    bash 复制代码
    printenv

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

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

    bash 复制代码
    printenv VARIABLE_NAME

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

    bash 复制代码
    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. 显示所有变量

    bash 复制代码
    set

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

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

    bash 复制代码
    set VARIABLE_NAME=value

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

    bash 复制代码
    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 中,变量默认被视为字符串类型,支持使用单引号 ' 和双引号 " 来定义字符串:

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

    shell 复制代码
    my_string='Hello, $USER!'
    echo "$my_string"  # 输出: Hello, $USER!

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

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

    shell 复制代码
    my_string="Hello, $USER!"
    echo "$my_string"  # 输出: Hello, Alice!

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

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

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

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

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

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

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

    shell 复制代码
    my_string="Hello, World!"
    new_string=${my_string/World/Bash}
    echo "$new_string"  # 输出: Hello, Bash!

Tips:

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

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

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

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

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

    shell 复制代码
    my_array=(element1 element2 element3)
  2. 使用 declare -a 定义数组:

    shell 复制代码
    declare -a my_array
    my_array=(element1 element2 element3)

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

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

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

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

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

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

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

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

Tips:

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

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

bash 复制代码
declare -A fruits
fruits=(["apple"]="red" ["banana"]="yellow" ["cherry"]="red")

也可以在声明时初始化:

bash 复制代码
declare -A fruits=(["apple"]="red" ["banana"]="yellow" ["cherry"]="red")

常用操作如下:

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

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

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

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

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

    shell 复制代码
    map[key]="new_value"
  6. 新增元素:

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

    shell 复制代码
    unset map[key]

3.3.3 引用变量

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

  • 使用 $variable 形式引用变量

    sh 复制代码
    echo $PATH
  • 使用花括号 ${variable} 形式引用的变量

    sh 复制代码
    echo "Value: ${my_var}"
    echo "Value: ${my_var}123"  # 输出:Value: Hello, World!123

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

Tips:

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

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

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

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

3.3.4 变量的作用域

全局变量

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

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

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

局部变量

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

bash 复制代码
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)使用。

只读变量

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

bash 复制代码
# 定义只读变量
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.5 撤销变量

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

bash 复制代码
MY_VAR="Some value"  # 定义变量
unset MY_VAR        # 撤销变量

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

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

相关推荐
hero_th8 分钟前
[Ubuntu] 文件/目录权限更改
linux·ubuntu
花花少年18 分钟前
pip在ubuntu下换源
linux·ubuntu·pip
y0ungsheep37 分钟前
[GXYCTF 2019]Ping Ping Ping 题解(多种解题方式)
linux·web安全·网络安全·php
海绵波波10741 分钟前
Webserver(1.6)Linux系统IO函数
linux·运维·服务器
czme1 小时前
线程和进程
linux·数据结构·计算机网络
华东设计之美1 小时前
etcd多实例配置
linux·服务器·etcd
dessler1 小时前
Linux系统-开关机
linux·运维·云计算
致奋斗的我们2 小时前
RHCE的学习(7)
linux·服务器·网络·学习·redhat·rhce·rhcsa
昨天今天明天好多天3 小时前
【Linux】ClickHouse 部署
linux·服务器·clickhouse
taolichao303 小时前
架设一台NFS服务器,按照要求配置
linux·运维·服务器