命令串联与脚本


Linux 启明道场.

在管道模块中,你已经尝试将多条命令组合起来,让数据通过管道在它们之间流动,从而完成比单条命令稍复杂的任务。 当然,即使不传递数据,也可以采用这种组合思路:有时,你会希望连续执行多条命令,让它们共同完成一个目标。

本模块将介绍除管道之外的几种命令串联方式。 学完后,你就将开始编写shell 脚本了!


串联命令最简单的方式,就是使用 ;。 在大多数场景中,; 用于分隔命令,类似于按 Enter 分隔各行。 因此,下面的操作:

hacker@dojo:~$ echo COLLEGE > pwn
hacker@dojo:~$ cat pwn
COLLEGE
hacker@dojo:~$

与下面的操作基本相同:

hacker@dojo:~$ echo COLLEGE > pwn; cat pwn
COLLEGE
hacker@dojo:~$

通常,按下 Enter 后,shell 会执行你输入的命令。等命令结束,再显示提示符,让你输入下一条命令。 分号的作用类似,只是中间不会显示提示符,而且两条命令都在执行开始前一次输入。

现在试试看!在这一关中,你必须先运行 /challenge/pwn,再运行 /challenge/college,并用分号将它们串联起来。

你已经在进程模块中学习了退出码。 现在,用它们来串联命令吧!

&& 运算符只在第一条命令成功时才执行第二条命令(按照 Linux 的惯例,成功意味着退出码为 0)。 它被称为“AND”(逻辑与)运算符,因为两个条件要依次成立:第一条命令必须成功,然后才会执行第二条命令。 在复杂的命令行工作流程中,如果某些操作依赖其他操作成功完成,这就特别有用。

语法如下:

hacker@dojo:~$ command1 && command2

它的含义是:“执行 command1;如果它成功,再执行 command2。”

看几个例子:

hacker@dojo:~$ touch /home/hacker/file && echo "this will run"
success
this will run
hacker@dojo:~$ touch /file && echo "this will NOT run"
touch: cannot touch '/file': Permission denied
hacker@dojo:~$

第二次调用 touch 失败了,因为 hacker 用户没有对 /file 的写入权限,因此 echo 没有运行。

在这一关中,你需要使用 && 运算符,将 /challenge/first-success 和 /challenge/second 两个程序串联起来。 先试着分别运行每条命令,看看会发生什么(你会发现这样拿不到 flag)。 但如果用 && 串联它们,flag 就会出现!

你刚学过 && 运算符,它只在第一条命令成功时执行第二条命令。 现在来看看相反的情况:|| 运算符只在第一条命令失败时(以非零退出码结束)才执行第二条命令。 它被称为“OR”(逻辑或)运算符,因为要么第一条命令成功,要么执行第二条命令。

语法如下:

hacker@dojo:~$ command1 || command2

它的含义是:“执行 command1;如果它失败,再执行 command2。”

看几个例子:

hacker@dojo:~$ touch /file || echo "touch failed, so this runs"
touch: cannot touch '/file': Permission denied
touch failed, so this runs
hacker@dojo:~$ touch /home/hacker/file || echo "this will NOT run"
hacker@dojo:~$

|| 运算符非常适合提供备用命令或进行错误处理!

在这一关中,你需要使用 || 运算符,将 /challenge/first-failure 和 /challenge/second 串联起来。 动手吧!

为了实现复杂的效果,你会组合越来越多的命令,而越来越长的命令行很快就会变得难以管理。 这时,你可以把这些命令放进一个文件中,这样的文件叫作 shell 脚本;执行文件,就能运行其中的命令! 例如,看看之前使用分号的方法:

hacker@dojo:~$ echo COLLEGE > pwn; cat pwn
COLLEGE
hacker@dojo:~$

我们可以创建一个名为 pwn.sh 的 shell 脚本(按照惯例,shell 脚本经常使用 sh 后缀):

echo COLLEGE > pwn
cat pwn

然后,把这个文件作为参数传给一个新启动的 shell(bash),就能执行它! 以这种方式调用 shell 时,它会从文件中读取命令,而不是等待用户输入命令。

hacker@dojo:~$ ls
hacker@dojo:~$ bash pwn.sh
COLLEGE
hacker@dojo:~$ ls
pwn
hacker@dojo:~$

可以看到,shell 脚本执行了两条命令,创建了 pwn 文件,并输出了文件内容。

现在轮到你了! 和上一关一样,先运行 /challenge/pwn,再运行 /challenge/college,但这次要把它们写入一个名为 x.sh 的 shell 脚本,然后用 bash 运行!


注意:我们还没有介绍 Linux 中那些功能强大的命令行文件编辑器。 目前,你可以使用桌面模式中的 Text Editor(文本编辑器)应用(Applications->Accessories->Text Editor),或者 VSCode 工作区中的默认编辑器!

来尝试一点更有挑战性的内容! 你已经用 | 在程序之间传递输出,但到目前为止,只是把一条命令的输出交给另一条命令作为输入。 如果想把多个程序的输出一起交给一条命令呢? 有几种方法可以做到。这里先介绍一种简单的方法:重定向脚本的输出!

对 shell 来说,脚本只是一条普通命令。 这意味着,你可以像在管道模块中处理命令那样,重定向脚本的输入和输出! 例如,将输出写入一个文件:

hacker@dojo:~$ cat script.sh
echo PWN
echo COLLEGE
hacker@dojo:~$ bash script.sh > output
hacker@dojo:~$ cat output
PWN
COLLEGE
hacker@dojo:~$

各种重定向方法都适用:> 用于标准输出,2> 用于标准错误,< 用于标准输入,>> 和 2>> 用于追加模式的重定向,>& 用于重定向到其他文件描述符,| 用于通过管道传给另一条命令。

本关将练习通过管道(|)把脚本输出交给另一个程序。 可执行脚本稍后才会介绍,因此在传递脚本输出时,请用 bash 运行脚本。 和之前一样,你需要创建一个 .sh 脚本,先调用 /challenge/pwn,再调用 /challenge/college,然后把脚本的输出通过管道传给 /challenge/solve,且该命令只调用一次。

你已经写出了第一个 shell 脚本,但每次都用 bash script.sh 调用它,有些麻烦。 为什么需要这个 bash 呢?

执行 bash script.sh 时,你其实是在启动 bash 命令,并传入 script.sh 参数。 这会让 bash 从 script.sh 读取命令,而不是从标准输入读取,因此你的 shell 脚本得以执行。

其实,你可以省去手动调用 bash 这一步。 只要 shell 脚本文件是可执行的(回顾一下文件权限),就可以直接通过相对路径或绝对路径调用它! 例如,在主目录中创建 script.sh,并赋予它执行权限,之后就可以通过 /home/hacker/script.sh、~/script.sh,或者(当前工作目录为 /home/hacker 时)./script.sh 调用它。

在这里试试看! 创建一个调用 /challenge/solve 的 shell 脚本,赋予它执行权限,然后直接运行它,无需显式调用 bash!

你已经在成为 shell 脚本编写者的道路上迈出了扎实的一步! 不过,到目前为止,你的 shell 脚本只能从 shell 中启动。 上一关运行顺利,是因为你从 bash shell 中调用了脚本;如果脚本由一个 Python(或其他语言)编写的程序来调用,就无法这样运行了。

在 Linux 中调用程序时,Linux 内核会先检查文件,确定应该如何运行它。 内核并不根据扩展名来判断(所以 shell 脚本不一定非要使用 .sh 扩展名,Python 脚本也不一定要使用 .py 扩展名,其他脚本同理)。 Linux 实际上会查看文件开头的几个字节,从中获取这些信息。

程序有很多种类型。如果程序文件以 #! 开头(通常称为“shebang”),Linux 就会将该文件视为解释型程序,并把这一行剩余的内容作为解释器的路径。 然后,它会调用这个解释器,将程序文件的路径作为唯一参数传入。

看看下面这个 shell 脚本:

#!/bin/bash

echo "Hello Hackers!"

可以这样执行它:

hacker@dojo:~$ chmod a+x script.sh
hacker@dojo:~$ ./script.sh
Hello Hackers!
hacker@dojo:~$

执行 ./script.sh 时,Linux 会打开文件,读取第一行,提取 /bin/bash 作为解释器,然后执行 /bin/bash ./script.sh 来启动脚本!

注意,shebang 必须位于文件的第一行,前面不能有空行或空格!

在这一关中,请在 /home/hacker/solve.sh 创建一个脚本,设置正确的 shebang,并输出 "hack the planet"。 记得赋予它执行权限,然后运行 /challenge/run,检查脚本是否正常工作!


小知识: 你可能会见到以下常见的 shebang:

  • #!/bin/bash:用于 bash 脚本
  • #!/usr/bin/python3:用于 Python 脚本
  • #!/bin/sh:用于 POSIX shell 脚本。这种 shell 是 bash 的早期前身,功能较少,但与非 Linux 系统的兼容性更好!

你已经学会创建 shell 脚本,但到目前为止,它们只是命令的集合。 当脚本能够接收参数时,功能就会强大得多! 调用方式可能是这样的:

hacker@dojo:~$ bash myscript.sh hello world

脚本可以通过特殊变量访问这些参数:

  • $1 保存第一个参数("hello")
  • $2 保存第二个参数("world")
  • $3 保存第三个参数(如果传入了第三个参数)
  • 后续参数依次类推

下面是一个简单的例子:

hacker@dojo:~$ cat myscript.sh
#!/bin/bash
echo "First argument: $1"
echo "Second argument: $2"
hacker@dojo:~$ bash myscript.sh hello world
First argument: hello
Second argument: world
hacker@dojo:~$

在这一关中,你需要在 /home/hacker/solve.sh 编写一个脚本,满足以下要求:

  1. 接收两个参数
  2. 按相反顺序输出它们(先输出第二个参数,再输出第一个参数)

例如:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn college
college pwn
hacker@dojo:~$

脚本能够正常工作后,运行 /challenge/run,获取 flag!

现在,你已经能在脚本中使用参数了。接下来,用条件逻辑让脚本更灵活!

在 bash 中,可以使用 if 语句进行判断:

if [ "$1" == "ping" ]
then
    echo "pong"
fi

上面的逻辑用自然语言表达,就是 如果第一个参数是 "ping",就输出 "pong"。 这段语法对格式的要求比较严格。 首先,你必须在 if 后面(这与 C 等语言不同)、[ 后面,以及 ] 前面留出空格。 其次,if、then 和 fi 必须各占一行(或者用分号分隔),不能挤在同一条语句中。 还有一点有些特别:if 语句的结束标记不是 endif、end 之类的词,而是 fi(即 if 倒过来写)。 你需要记住这一点。

在这一关中,请在 /home/hacker/solve.sh 编写一个脚本,满足以下要求:

  • 接收一个参数
  • 如果参数是 "pwn",输出 "college"
  • 对于其他任何输入,不输出任何内容

示例:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh foo
hacker@dojo:~$

脚本能够正常工作后,运行 /challenge/run,获取 flag!


注意: 除了字符串是否相等,你还想知道条件中能检查什么吗? 使用 help test 了解完整说明吧!

到目前为止,你的 if 语句处理的都是特定情况,那么其他情况怎么办? 这就需要 else 了!

当 if 条件为假时,else 分支就会执行:

if [ "$1" == "hello" ]
then
    echo "Hi there!"
else
    echo "I don't understand"
fi

注意,else 没有条件:它负责处理前面没有匹配到的所有情况。 它后面也不需要 then。 最后,fi 要放在 else 代码块之后,表示整个复合语句结束! else 分支是可选的:上一关就没有使用它,只有需要处理其他情况时才要加上。

下面是一个实际例子:

if [ "$1" == "start" ]
then
    echo "Starting the service..."
else
    echo "Unknown command. Use 'start' to begin."
fi

在这一关中,请在 /home/hacker/solve.sh 编写一个脚本,满足以下要求:

  • 接收一个参数
  • 如果参数是 "pwn",输出 "college"
  • 对于其他任何输入,输出 "nope"

示例:

hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh hack
nope
hacker@dojo:~$ bash /home/hacker/solve.sh anything
nope
hacker@dojo:~$

脚本能够正常工作后,运行 /challenge/run,获取 flag!

你已经学会用一条 if 语句检查条件。 如果需要检查多个条件呢? 可以使用 elif(else if 的缩写):

if [ "$1" == "one" ]
then
    echo "1"
elif [ "$1" == "two" ]
then
    echo "2"
elif [ "$1" == "three" ]
then
    echo "3"
else
    echo "unknown"
fi

注意,elif 后面也需要有 then,和 if 一样。 和之前一样,末尾的 else 会处理所有未匹配的情况。

在这一关中,请在 /home/hacker/solve.sh 编写一个脚本,满足以下要求:

  • 接收一个参数
  • 如果参数是 "hack",输出 "the planet"
  • 如果参数是 "pwn",输出 "college"
  • 如果参数是 "learn",输出 "linux"
  • 对于其他任何输入,输出 "unknown"

示例:

hacker@dojo:~$ bash /home/hacker/solve.sh hack
the planet
hacker@dojo:~$ bash /home/hacker/solve.sh pwn
college
hacker@dojo:~$ bash /home/hacker/solve.sh learn
linux
hacker@dojo:~$ bash /home/hacker/solve.sh foo
unknown
hacker@dojo:~$

脚本能够正常工作后,运行 /challenge/run,获取 flag!


注意: 编写脚本时,请严格按照示例中的空格格式书写。 与许多其他语言不同,bash 要求 [ 和 ] 都必须用空格与其他字符隔开,否则无法解析条件。

编写 shell 脚本的可不只有你! 它们很适合完成简单的“系统级”任务,是开发者和系统管理员常用的工具。 事实上,典型 Linux 计算机中有相当多的程序都是 shell 脚本,这个比例可能会让你意外。

本关将学习阅读 shell 脚本。 /challenge/run 是一个 shell 脚本,要求你输入一个秘密密码,但这个密码就直接写在脚本里! 读取脚本(例如使用 cat),找出密码,获取 flag!


注意: 你也可以尝试阅读其他挑战的代码! 阅读代码是学习新技能的重要方法,因为你可以看到某项功能是如何实现的,并在自己的脚本中复用这些方法。 不过要注意:某些程序文件包含机器码,人类无法直接阅读。 你可以用 file 命令区分文件类型,但 Linux Luminarium 中几乎所有挑战都是用 shell 脚本实现的,可以放心用 cat 查看。


30天排行榜:

此排行榜反映本模块在道场上线后各挑战的解出情况。

排名 黑客 徽章 得分
暂无解题排行数据 —— 注册账号并完成挑战即可登上榜单!