Linux Shell脚本编写的简易指南,使用Bash

曾经想过学习更多关于Linux shell脚本编程的知识,但不确定从何开始吗?您是否是Unix类操作系统的新手,想要扩展自己的技能以进行一些基本的shell编程?这个面向初学者的教程将介绍Linux shell脚本编程的基础知识,包括创建和运行脚本,以及处理字符串和循环。

Shell脚本用于自动化常见的管理任务

无论操作系统如何,shell脚本都用于自动化重复的管理任务。例如,在Windows中,您可以使用文件资源管理器重命名文件。但如果您需要重命名许多文件,使用图形shell将是一项耗时的任务。PowerShell允许您自动化任务并可靠地重复执行它。

广告

在基于Linux的操作系统中,Bash和其他shell用于自动化任务,如处理文件、修改系统配置以及执行许多其他通过输入单个命令完成的任务。

学习Bash shell脚本编程所需

要编写和运行Bash脚本,您只需要三样东西:

  • 任何纯文本编辑器,例如记事本、文本编辑器、TextEdit、vi、emacs或Visual Studio Code。
  • terminal emulator, an application that comes preinstalled with most operating systems and is often called Terminal, Console, or Command Prompt.
  • Bash本身。

终端模拟器是您将输入命令并通过按Enter或Return键来运行它们的地方。至于Bash,无论您是否预先安装它,都将取决于您的平台:

  • macOS上,Bash是预装的。在较新版本中,Z shell(zsh)是默认shell,这没关系。只要安装了Bash,您也可以从zsh运行Bash脚本。
  • Linux发行版通常都安装了Bash。(您可以通过查看系统是否包含/bin/bash文件来检查。)Android是一个特殊情况,它没有随附Bash。有一些步骤可以将Bash安装到Android上,本文将不涉及这些步骤。
  • Windows没有捆绑Bash。 PowerShell是Windows中的默认命令行shell。您需要在Windows子系统Linux(WSL)下安装Linux发行版才能运行Bash。

要查找Bash版本,请运行bash –version命令。即使是更旧的Bash版本也具有很强大的功能,但Bash 3和4都引入了一些基本命令的简写符号。如果某个命令需要这些Bash版本中的一个,将在下面提到。

广告

什么是shell?

在计算世界中,shell是一个为底层操作系统提供接口的程序。shell可以是图形用户界面(GUI),如Windows shell。

Shell脚本语言

然而,人们通常使用这个术语来特指命令行界面(CLI)——一个由文本行组成的界面,您只能使用键盘与之交互。以下是一些*nix操作系统的shell脚本语言示例:

在这里,我们将重点介绍Bash shell。这是一个流行的免费Unix shell,在大多数Linux发行版和macOS上预装。

什么是shell脚本?

Shell有自己的编程语言。您可以使用这种语言向shell发送命令,然后shell执行这些命令。您可以直接在shell中键入这些命令,也可以将它们保存到一个文件(一个脚本),然后从shell中执行该文件。在这两种情况下,编写命令的语法是相同的。

Advertisement

本文将介绍shell脚本的基础知识,以创建这个文件。

基本shell脚本

让我们从一些基本的shell脚本开始。要编写一个简单的脚本,我们将学习一些Linux中的简单shell脚本命令:

  1. 在文本编辑器中创建一个新的空文本文件
  2. #!/bin/bash写在第一行。
  3. 在下面输入你的命令
  4. 保存该文件,最好使用“.sh”扩展名或者没有任何扩展名。

第一行的#!/bin/bash被称为“shebang”。它告诉你的shell应该在Bash中执行此脚本,并且应该是脚本中的第一行。如果你切换到不同的shell,你的脚本仍然会在Bash中运行。

要自己尝试这个过程,请在你的主目录中创建一个名为‘hello_world’的文件:

#!/bin/bash
echo "hello world"

就是这样 — 你已经创建了一个Bash脚本!

Our “hello world” script is just a simple text file

在运行之前,你可能需要更改文件的权限。

使用chmod设置运行shell脚本的权限

要修改我们的‘hello_world‘文件的权限,你可以在终端模拟器中运行这个特定命令。这样会给文件的所有者执行文件的权限。

chmod u+x 'hello_world'
Running our script without vs. with the “execute” permission

如果你只想运行你的shell脚本,你可以直接跳到下一节。对于那些对chmod命令感兴趣的人chmod是“change mode”的缩写,在Unix中用于更改文件的“模式”(或权限)。在类Unix操作系统中,你可以为3类用户设置文件权限:

  • 拥有文件的用户(用u表示)。
  • 拥有文件的组(g)。
  • 其他人(o)。

chmod命令,你也可以使用a来表示所有这些。

每个文件有3种类型的权限(或“模式”):

  • 读取(r
  • 写入(w
  • 执行(x

你还可以添加(+)或移除(-)权限。

chmod的第一个参数是这三个的组合 — 用户首先,动作其次,模式第三。这里有一些命令示例:

  • chmod gu+rw 'hello_world'将为所有者和拥有组添加读取和写入权限。
  • chmod a-x 'hello_world'将移除所有人的可执行权限。
  • chmod u+rwx 'hello_world' 'hello_world_2' 将赋予所有者读取、写入和执行“hello_world”和“hello_world_2”文件的权限。

我们仅介绍了chmod命令的基础知识。还有一种更复杂但更简洁的定义这些模式的方法(“数字表示法”),以及另一种命令可以用来查看文件的权限(ls -l)。我们不会在这里详细介绍这些话题。

执行Shell脚本

是时候执行我们的第一个脚本了。通常,要运行脚本,只需在终端模拟器中键入其路径并按回车键。

./hello_world

您可以使用相对路径或绝对路径。当使用相对路径时,始终在命令开头使用./:这会告诉终端在当前文件夹中查找(由'.'表示),而不是在PATH环境变量中定义的目录中查找。

Just typing the script name doesn’t work, but running its relative or absolute paths does

使用注释注解您的脚本

在Bash脚本中,#后面的单行文字被视为注释。这可以帮助说明复杂行的作用或概述脚本中更大部分的功能。

例如:

#!/bin/bash

#
# This shell script prints "hello world".
#

echo "hello world" # This line prints "hello world".

变量简介

在编写脚本时,定义变量是很有用的。在Bash中,您可以通过输入变量名和值,并用等号分隔来做到这一点:VARIABLENAME='VALUE'

不要在等号旁边加空格 — Bash 会认为您想要运行一个进程。

使用单引号来包围值,以防止Bash 将其解释为其他内容。在Bash中,变量没有类型 — 一切基本上都是字符串。Bash程序会将字符串解析为不同类型,例如数字。

要引用变量的值,请在变量名前加上美元符号:$VARIABLENAME

要尝试实践这一点,您可以将脚本更改为以下内容:

#!/bin/bash
HELLO="hello variable world"
echo $HELLO # should print "hello variable world"

接收参数

在键入命令时,您写下的各个单词被称为参数。在我们的chmod u+x 'hello_world'示例中,chmodu+x'hello_world'是三个不同的参数。chmod是命令名,而u+xhello_world被称为参数 — 提供额外信息给命令的参数。

在您的脚本中,您可以通过变量来访问这些参数。为了避免与本地变量冲突,这些变量使用数字命名 — $0是命令名,$1是接下来的参数,$2是其后一个参数,以此类推。

让我们试一下:

#!/bin/bash
HELLO="hello $1 world"
echo $HELLO

现在,用这些参数运行这个脚本:

./hello_world bash script

输出应该是 hello bash world,第一个参数被使用,第二个被忽略。

如果你希望 bash script 被看作一个参数,你需要用引号括起来:

./hello_world 'bash script'
Words separated by a space are considered as several arguments, except when in quotes

使用 if 语句有条件地运行代码

程序员在脚本中的核心需求之一是只有在满足特定条件时才运行一段代码。Bash 使用 if 语句实现这一点:

NUM=$RANDOM
if (( $NUM % 2 )) # if CONDITION
then
    echo "$NUM is odd"
fi # this is how you end an if statement

提示:从现在开始,这些示例被假定为较大脚本的一部分,并且省略了一开始的 #!/bin/bash。但是请不要忘记将其作为脚本的第一行!

你也可以在 if 语句内使用 else来指定如果条件不满足要执行什么操作,或使用 elif(缩写形式为“else if“)语句来指定第一个条件不被满足时的另一个条件:

NUM=$RANDOM
if [ $NUM -eq 12 ]
then
    echo "$NUM is my favorite number"
elif (( $NUM % 2 ))
then
    echo "$NUM is odd"
else
    echo "$NUM is even"fi

fi‘用来结束 if 语句。

提示:如果您不确定如何编写条件本身,请查看test,方括号([])和双括号((()))表示法。

The output of our script depends on the value of a random variable

使用for循环重复一系列命令

既然我们已经介绍了有条件地运行代码,让我们看看如何在满足条件的情况下运行一定次数的代码。

 for循环非常适合这样的任务,特别是其中的“三表达式语法”。其背后的思想是为循环分配一个特定的变量,并逐渐更改它,直到满足某个条件。下面是它的结构:

for (( ASSIGNMENT_EXPRESSION ; CONDITION_EXPRESSION ; UPDATE_EXPRESSION ))
do
    COMMANDS
done

例如,如果您想要一个循环运行10次,其中i的值从0到9变化,您的for循环可能是这样的:

for (( i=0; i<10; i++ ))
do
    echo $i
done

让我们逐步解释:

  • i=0 is the assignment expression here. It’s run only once before the loop is executed, which is why it’s useful for initializing a variable.
  • i<10 is our condition expression. This expression is evaluated before each iteration of a loop. If it is equal to zero (which means the same as “true” in Bash), the next iteration is not run.
  • i++ is our update expression. It’s run after each iteration of a loop.
The structure of our for loop

遍历列表中的元素

除了三表达式语法,您还可以使用in关键字来定义for循环。这种替代语法用于迭代一系列项。

最基本的示例就是在in关键字之后列出您要迭代的一组项,用空格分隔。例如:

for i in 0 1 2 3 4 5 6 7 8 9 # space-separated list items
do
    echo $i
done

您还可以通过命令输出的项目进行迭代:

for i in $(seq 0 1 9)

通用的命令替换符号$()用于命令替换 —— 执行一个命令并将其输出用作其周围父命令的输入。

如果要遍历整数,最好使用Bash内置的范围语法,比seq命令更高效。然而,这种语法仅在较新的Bash版本中可用:

  • for i in {0..9},适用于Bash 3。
  • for i in {0..9..1},适用于Bash 4,其中最后一个数字表示增量。

同样,您也可以遍历字符串:

for s in 'item1' 'item2' 'item3'

使用通配符获取与模式匹配的文件

在前一节讨论的for循环中,迭代单个文件是更常见的用例之一。

为了解决这个问题,我们需要先介绍所谓的“通配符扩展”。这是Bash中的一个功能,让您可以使用模式匹配来指定文件名。有一些特殊字符称为通配符,您可以用来定义这些模式。

在深入探讨之前,让我们看几个具体的例子:

  • echo *:一个返回当前目录中所有文件名称的命令,除了隐藏的文件。
  • echo *.txt:一个返回当前目录中所有没有隐藏的、带有 txt 扩展名的文件名称的命令。
  • echo ????:一个返回当前目录中所有四个字符文件名的命令。

我们这里使用的是 * 和 ? 通配符。还有一个我们没有使用的通配符。以下是一个概述:

  • 星号(*)代表文件或目录名称中的任意数量的字符(包括0)。
  • 问号(?)代表文件或目录名称中的单个字符。
  • 双星号(**)代表完整文件路径中的任意数量字符。这是Bash 4及更高版本中支持的功能,需要通过运行 shopt -s globstar 启用。
  • 方括号([])用于表示文件或目录名称中一组符号中的字符。例如,[st]ake 将找到名称为 saketake 的文件,但不包括 stake

请注意,在使用通配符展开时,所有隐藏文件(即文件名以点 . 开头的文件)都会被忽略。

方括号表示法允许更复杂的操作。

  • 范围由第一个和最后一个值定义 – 例如[1-8]
  • 通过在括号内使用!作为第一个字符来消除某些字符 – 例如[!3]

要将其中一个特殊字符视为没有任何含义的普通字符,只需在其前面加上反斜杠 – 例如\?

A few examples of globbing

使用for循环迭代文件

现在我们已经介绍了glob扩展的基础知识,让我们看看如何使用它来迭代文件。

我们可以在for循环本身中简单地使用glob运算符。以下是一个简单示例,循环打印当前目录中每个文件的名称:

for f in *
do
    echo $f
done

要打印当前目录中每个文件以及其子目录的名称,请检查您是否运行Bash 4.0或更高版本,方法是运行bash --version,然后您可以运行以下命令:

shopt -s globstar # enables using **
for f in **
do
    echo $f
done

提示:如果您运行的是较旧版本的Bash,则无法在此使用for循环进行globbing。您最好的做法是使用find命令,但我们不会在本文中详细介绍这一点。

这些当然只是您可以运行的一些最简单的循环,但您可以做的还有很多。例如,要将文件夹中的所有JPG文件重命名为具有一致顺序文件名的文件,您可以运行:

i=1
for f in *.jpg
do
    mv -i -- "$f" "image_$i.jpg"
    let i=i+1
done
Checking the Bash version, then running our script

在条件为true时运行代码

for循环不是Bash中我们可以使用的唯一一种循环类型 — 我们还有while:这种类型的循环会在特定条件为true时运行。

语法与for循环语法类似:

while CONDITION
do
    COMMANDS
done

举个实际的例子,这是如何逐行读取文件(除了前导或尾随空格)直到文件结束的:

while read -r line
do
    echo "$line"
done < FILENAME # Replace FILENAME with the path to a text file you'd like to read
Using a while loop inside our script to have it print itself

您也可以用while循环替换for循环,例如,从0到9进行迭代:

i=0
while [ $i -lt 10 ]
do
    echo $i
    let i=i+1
done

您还可以从理论上永远运行一个循环。这是一个命令示例:

while true
do
    echo "running forever"
done

提示: 要终止脚本,只需按下Ctrl+C

虽然这种无限循环乍看起来毫无用处,但实际上它可能非常有用,特别是当与break语句结合使用时。

跳出循环

break语句用于跳出循环。

这允许您运行一个无限循环,并在出现任何跳出条件时跳出它。

举个简单的例子,我们可以用类似这样的无限循环来复制我们从0到9运行的循环:

i=0while true
do
    if [ $i -eq 10 ]
    then
        break
    fi
    echo $i
    let i=i+1
done

如果你有几个嵌套的while循环,你可以在break语句后面加上一个数字,来指定从哪一层循环中跳出:break 1相当于break,会跳出最接近的外层循环,break 2会跳出上一级的循环,以此类推。

让我们看一个快速的例子,这次使用for循环,遍历每一个四个字母的组合,直到碰到单词“bash”为止:

for l4 in {a..z}
do
    for l3 in {a..z}
    do
        for l2 in {a..z}
        do
            for l1 in {a..z}
            do
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done

A related keyword that’s also worth a mention is continue, which skips to the next iteration of the loop. Just like break, it also takes an optional numeric argument that corresponds to the loop level.

这里有一个愚蠢的例子,我们跳过在我们缩短的四个字母单词列表中带有字母 E 的所有单词:

for l4 in {a..z}
do
    if [ $l4 = "e" ]
    then
        continue
    fi
    for l3 in {a..z}
    do
        if [ $l3 = "e" ]
        then
            continue
        fi
        for l2 in {a..z}
        do
            if [ $l2 = "e" ]
            then
                continue
            fi
            for l1 in {a..z}
            do
                if [ $l1 = "e" ]
                then
                    continue
                fi
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done

我们也可以在最深层循环的级别执行所有这些continue语句:

for l4 in {a..z}
do
    for l3 in {a..z}
    do
        for l2 in {a..z}
        do
            for l1 in {a..z}
            do
                if [ $l4 = "e" ]
                then
                    continue 4
                fi
                if [ $l3 = "e" ]
                then
                    continue 3
                fi
                if [ $l2 = "e" ]
                then
                    continue 2
                fi
                if [ $l1 = "e" ]
                then
                    continue
                fi
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done
Our script stops once it generates the word “bash”

如何在shell脚本中获取用户输入

有时,您希望用户直接与您的脚本交互,而不仅仅是使用初始脚本参数。这就是read命令发挥作用的地方。

要获取用户输入并将其保存到名为NAME的变量中,您可以使用这个命令:

read NAME 

这是该命令的最简单形式,仅由命令名称和您想要保存输入的变量组成。

然而,更频繁地,您可能希望提示用户,以便他们知道该输入什么。您可以通过-p参数来实现这一点,之后编写您想要的提示。

这是您可能要求名称并将其分配给名为 NAME 的变量的方式:

read -p "Your name: " NAME
echo "Your name is $NAME." # this line is here just to show that the name has been saved to the NAME variable
The script saves our input to a variable and then prints it

在字符串中打印特殊字符

在Bash中有一些字符需要小心使用。例如,在文件名中的空格,字符串中的引号,或几乎任何地方的反斜杠。

要告诉Bash在某些地方忽略它们的特殊含义,您可以要么“转义”它们,要么将它们包装在文字引号中。

转义”一个特殊字符意味着告诉Bash将其视为没有任何特殊含义的字符。为此,请在该字符之前写一个反斜杠。

假设我们有一个名为img \ 01 *DRAFT* 的文件。在Bash中,我们可以这样引用它:

img\ \\\ 01\ \*DRAFT\*

以下是Bash中一些特殊字符的非详尽列表:

  • 空格:空格,制表符,空行
  • 引号:”,“”
  • 括号和方括号:(),{},[]
  • 管道和重定向:|,<,>
  • 通配符:*,?
  • 其他:!,#,;,=,&,~,`
  • 转义字符本身:\

但是,如果您对Bash是新手,记住哪些字符具有特殊含义可能很麻烦。因此,在许多情况下,使用文字引号可能更容易:只需用单引号括起包含任何特殊字符的文本,那些引号内的所有特殊字符都将被忽略。

这是我们示例的展示方式:

'img \ 01 *DRAFT*'

如果要使用一个特殊字符,但转义其他字符呢?您可以使用反斜杠转义每个字符,但也可以节省麻烦,将除了特殊字符以外的所有内容用字面引号括起来。

例如,假设您有几个文件名为等,您希望使用通配符扩展来匹配所有这些文件名。您可以这样写:

'img \ 01 *'*' DRAFT*'
This glob expansion finds all of our 3 oddly-named files

如果您需要写入的内容包含单引号 — 例如 — 您可以使用类似的策略,结合使用字面字符串和转义:

'img \ 01 '\''FINAL'\'

如何在Bash中连接字符串

假设您有两个或更多字符串变量 — 例如名字和姓氏:

FIRST_NAME="Johnny"LAST_NAME="Appleseed"

要将这些变量合并为一个字符串,也许使用定制分隔符,只需创建一个由这两个变量组成的新字符串:

NAME="$LAST_NAME"', '"$FIRST_NAME"

您还可以在其中使用双引号和内联变量,使用花括号将变量名与周围的文本分隔开:

NAME="${LAST_NAME}, ${FIRST_NAME}"

Bash 还允许使用 += 运算符将文本附加到字符串中,就像这样:

NAME='Appleseed'NAME+=', 'NAME+='Johnny'
Examples of different ways of stringing together text

在字符串中运行命令

Bash还允许您在字符串中使用命令的输出,也称为命令替换。只需用$()括起您的命令即可。例如,要打印当前时间戳,您可以运行:

echo "Current timestamp: $(date)"
Using command substitution to print the current timestamp

您可能还记得之前的for循环示例中使用过这种语法,当时我们遍历了从0到9的整数序列:

for i in $(seq 0 1 9)
do
    echo $i
done

替换的命令在子shell中运行。这意味着,例如,在命令期间创建的任何变量都不会影响您运行脚本的环境。

在shell脚本中设置和返回退出代码

对于更复杂的脚本,通常会让它返回一个退出 代码 — 一个介于0和255之间的数字,告诉人们脚本是否成功运行或遇到错误。

至于要使用哪些数字,官方的Bash手册指定了以下内容:

  • 0:程序成功执行。
  • 2:程序使用不正确(例如,由于参数无效或缺失)。
  • 1和3-124:用户定义的错误。
  • 126:命令不可执行。
  • 127:未找到命令。
  • 125和128-255:shell使用的错误状态。如果进程被信号N终止,则退出状态为128 + N。

正如您所看到的,除了0之外的所有数字均表示某种类型的错误。退出码1通常用于一般错误。在大多数情况下,您不需要使用任何高于2的退出码。

要从您的脚本中返回退出码,只需使用exit命令。例如,要以代码2退出,您将编写exit 2

如果您不在脚本中使用exit命令,或者使用命令而没有指定代码,则将返回脚本中最后执行的命令的退出状态。

要获取shell中上一个运行命令的退出码,请使用$?变量:echo $?

如何调用函数

在编写较长的脚本或有重复代码块的脚本时,您可能需要将一些代码分离为函数。有两种格式可用于定义函数:两种情况下,所有函数代码都包含在花括号内,只是函数声明不同。

更紧凑的格式使用跟随函数名称的括号声明函数:

function_name () {
    echo "This is where your function code goes"
}

另一种格式使用function关键字在函数名前面定义函数:

function function_name {
    echo "This is where your function code goes"
}

函数必须在脚本中调用之前声明。调用函数的方式类似执行常规命令,使用函数名称作为命令:

function_name

使用变量传递数据给函数

在Bash中,函数不能接受参数。要向函数传递信息,您需要在脚本中使用全局变量。

例如:

is_your_name_defined () {
    if [ -z "$YOUR_NAME" ]
    then
        echo "It doesn't seem like I have your name."
    else
        echo "Is this your name: ${YOUR_NAME}?"
    fi
}
read -p "Your name: " YOUR_NAME

is_your_name_defined

与其他语言中您可能习惯的不同,您在函数内定义的变量是全局的,并且对脚本外的代码可见。要定义仅在函数作用域内可访问的变量(即对其外部所有代码不可访问),请使用local关键字:例如local i=0

如果您想要从函数中返回值,您也需要使用全局变量。在Bash中,函数只能返回退出码,使用return关键字。让我们看一个例子:

is_your_name_defined () {
    if [ -z "$YOUR_NAME" ]
    then
        MESSAGE="It doesn't seem like I have your name."
    else
        MESSAGE="Is this your name: ${YOUR_NAME}?"
    fi
}
read -p "Your name: " YOUR_NAME

is_your_name_defined

echo $MESSAGE
The output of our script depends on the values entered

总结

Bash脚本允许您在基于UNIX的操作系统上执行许多操作。本文涵盖了Bash脚本的一些基础知识,包括创建和运行脚本、处理字符串以及在代码中使用循环。希望这将成为您编写功能强大的Bash脚本的良好起点,以满足您的需求。

当然,关于Bash还有很多值得学习的内容,包括一些最有用的命令、文件系统导航等。在评论中告诉我们接下来应该涵盖哪些主题。

相关文章:

Source:
https://petri.com/shell-scripting-bash/