Skip to content

Latest commit

 

History

History
1203 lines (850 loc) · 64.6 KB

File metadata and controls

1203 lines (850 loc) · 64.6 KB

十二、在脚本中使用管道和重定向

在本章中,我们将解释 Bash 的一个非常重要的方面:重定向。我们将首先描述不同类型的输入和输出重定向,以及它们与 Linux 文件描述符的关系。在介绍了重定向的基础知识之后,我们将继续一些高级用法。

接下来是管道,这是一个在 shell 脚本中大量使用的概念。我们举几个管道的实际例子。最后,我们展示一下在这里是如何记录的,它也有一些很好的用途。

本章将介绍以下命令:diffgccfallocatetrchpasswdteebc

本章将涵盖以下主题:

  • 输入/输出重定向
  • 管道
  • 这里有文件

技术要求

本章的所有脚本都可以在 GitHub 上找到,链接如下:https://GitHub . com/tam mert/learn-Linux-shell-scripting/tree/master/chapter _ 12。对于所有其他练习,你的 Ubuntu 18.04 虚拟机仍然是你最好的朋友。

输入/输出重定向

在本章中,我们将详细讨论 Linux 中的重定向。

简而言之,重定向就像这个词暗示的那样:将某样东西重定向到某样东西。例如,我们已经看到,我们可以使用管道将一个命令的输出用作下一个命令的输入。管道在 Linux 中使用|符号实现。

然而,这可能会提出一个问题:Linux 首先是如何处理输入和输出的?我们将从文件描述符的一些理论开始我们的重定向之旅,这些理论使所有的重定向成为可能!

文件描述符

你可能听腻了,但它仍然是正确的:在 Linux 中,一切都是文件。我们已经看到,文件是文件,目录是文件,甚至硬盘也是文件;但是现在,我们将更进一步:您用于输入的键盘也是一个文件!

作为补充,你的终端,命令用作输出,是,猜猜是什么:一个文件。

您可以在 Linux 文件系统树中找到这些文件,就像大多数特殊文件一样。让我们检查一下我们的虚拟机:

reader@ubuntu:~$ cd /dev/fd/
reader@ubuntu:/dev/fd$ ls -l
total 0
lrwx------ 1 reader reader 64 Nov  5 18:54 0 -> /dev/pts/0
lrwx------ 1 reader reader 64 Nov  5 18:54 1 -> /dev/pts/0
lrwx------ 1 reader reader 64 Nov  5 18:54 2 -> /dev/pts/0
lrwx------ 1 reader reader 64 Nov  5 18:54 255 -> /dev/pts/0

在我们找到的四个文件中,有三个很重要:/dev/fd/0/dev/fd/1/dev/fd/2

从本文的标题可以看出, fd 代表 f 文件 d 脚本。这些文件描述符在内部用于将用户的输入和输出绑定到终端。你实际上可以看到这是如何用文件描述符完成的:它们象征性地链接到/dev/pts/0

在这种情况下, pts 代表伪终端从机,这是 SSH 连接的定义。看看当我们从三个不同的位置观察/dev/fd时会发生什么:

# SSH connection 1
reader@ubuntu:~/scripts/chapter_12$ ls -l /dev/fd/
total 0
lrwx------ 1 reader reader 64 Nov  5 19:06 0 -> /dev/pts/0
lrwx------ 1 reader reader 64 Nov  5 19:06 1 -> /dev/pts/0
lrwx------ 1 reader reader 64 Nov  5 19:06 2 -> /dev/pts/0

# SSH connection 2
reader@ubuntu:/dev/fd$ ls -l
total 0
lrwx------ 1 reader reader 64 Nov  5 18:54 0 -> /dev/pts/1
lrwx------ 1 reader reader 64 Nov  5 18:54 1 -> /dev/pts/1
lrwx------ 1 reader reader 64 Nov  5 18:54 2 -> /dev/pts/1

# Virtual machine terminal
reader@ubuntu:/dev/fd$ ls -l
total 0
lrwx------ 1 reader reader 64 Nov  5 19:08 0 -> /dev/tty/1
lrwx------ 1 reader reader 64 Nov  5 19:08 1 -> /dev/tty/1
lrwx------ 1 reader reader 64 Nov  5 19:08 2 -> /dev/tty/1

每个连接都有自己的/dev/底座(属于udev类型,存储在内存中),这就是为什么我们看不到从一个连接到另一个连接的输出。

现在,我们已经讨论了输入和输出。但是,正如您无疑已经看到的,在前面的示例中分配了三个文件描述符。在 Linux(或类似 Unix/Unix 的系统)中,有三个默认的,默认情况下通过文件描述符公开:

  • 标准输入流,stdin,默认绑定到/dev/fd/0
  • 标准输出流,stdout,默认绑定到/dev/fd/1
  • 标准错误流,stderr,默认绑定到/dev/fd/2

就这三个流而言,stdinstdout应该相当简单:输入和输出。然而,正如您可能已经推断的那样,输出实际上分为正常输出和错误输出。正常输出发送到stdout文件描述符,而错误输出通常发送到stderr

因为这两者都象征性地与终端相联系,所以无论如何你都会在那里看到它们。然而,正如我们将在本章后面看到的,一旦我们开始重定向,这种差异就变得很重要。

You might see some other file descriptors, such as the 255 in the first example. Besides their use in supplying input and output to the Terminal, file descriptors are also used when Linux opens a file in the filesystem. This other use of file descriptors is outside of the scope for this book; we have, however, included a link in the Further reading section for those interested.

在正常交互中,您在终端中键入的文本会被写入/dev/fd/0上的stdin,命令可以读取该文本。使用这个输入,命令通常会做一些事情(否则,我们就不需要命令了!)并将输出写入stdoutstderr。在那里它将被终端读取并显示给你。简而言之:

  • A 终端T5】将写入stdinstdoutstderr读取
  • 一条命令 stdin读取,并写入stdoutstderr

Besides the file descriptors Linux uses internally, there are also a few file descriptors reserved for when you want to create really advanced scripts; these are 3 through 9. Any others might be used by the system, but these are guaranteed free for your use. As this is, as stated, very advanced and not used too often, we will not go into detail. However, we've found some further reading which might be interesting, which is included at the end of this chapter.

重定向输出

现在,关于输入、输出和文件描述符的理论应该清楚了,我们将看到如何在我们的命令行和脚本冒险中使用这些技术。

事实上,在不使用重定向的情况下编写 shell 脚本相当困难;在本章之前,我们实际上已经在书中使用了几次重定向,因为当时我们真的需要它来完成我们的工作(例如,第 8 章变量和用户输入中的file-create.sh)。

现在,让我们获得一些重定向的真实体验!

标准输出

命令的大部分输出将是标准输出,写入/dev/fd/1上的stdout。通过使用>符号,我们可以使用以下语法将其重定向出来:

command > output-file

重定向总是指向一个文件(然而,正如我们所知,并非所有文件都是相同的,所以在常规示例之后,我们将向您展示一些涉及非常规文件的 Bash 魔法)。如果文件不存在,将创建它。如果存在,将被覆盖

以最简单的形式,通常打印到终端的所有内容都可以重定向到一个文件:

reader@ubuntu:~/scripts/chapter_12$ ls -l /var/log/dpkg.log 
-rw-r--r-- 1 root root 737150 Nov  5 18:49 /var/log/dpkg.log
reader@ubuntu:~/scripts/chapter_12$ cat /var/log/dpkg.log > redirected-file.log
reader@ubuntu:~/scripts/chapter_12$ ls -l
total 724
-rw-rw-r-- 1 reader reader 737150 Nov  5 19:45 redirected-file.log

如您所知,cat将整个文件内容打印到您的终端。实际上它实际上是把整个内容发送到stdout,绑定到/dev/fd/1,绑定到你的终端;这就是你看到它的原因。

现在,如果我们将文件的内容重定向回另一个文件,我们基本上已经做了很大的努力...复制文件!从文件大小可以看出,它实际上是同一个文件。如果不确定,可以使用diff命令查看文件是否相同:

reader@ubuntu:~/scripts/chapter_12$ diff /var/log/dpkg.log redirected-file.log 
reader@ubuntu:~/scripts/chapter_12$ echo $?
0

如果diff没有返回任何输出,并且它有一个退出代码0,则文件中没有差异。

回到重定向的例子。我们使用>将输出重定向到文件。实际上,>1>的简称。你可能会认出这个1:它指的是文件描述符/dev/fd/1。正如我们将在处理stderr时看到的,它在/dev/fd/2上,我们将使用2>而不是1>>

但是,首先,让我们构建一个简单的脚本来进一步说明这一点:

reader@ubuntu:~/scripts/chapter_12$ vim redirect-to-file.sh 
reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-05
# Description: Redirect user input to file.
# Usage: ./redirect-to-file.sh
#####################################

# Capture the users' input.
read -p "Type anything you like: " user_input

# Save the users' input to a file.
echo ${user_input} > redirect-to-file.txt

现在,当我们运行这个时,read会提示我们输入一些文本。这将保存在user_input变量中。然后,我们将使用echouser_input变量的内容发送到stdout。但是,不是通过/dev/fd/1到达/dev/pts/0上的终端,而是重定向到redirect-to-file.txt文件。

总之,它看起来像这样:

reader@ubuntu:~/scripts/chapter_12$ bash redirect-to-file.sh 
Type anything you like: I like dogs! And cats. Maybe a gecko?
reader@ubuntu:~/scripts/chapter_12$ ls -l
total 732
-rw-rw-r-- 1 reader reader 737150 Nov  5 19:45 redirected-file.log
-rw-rw-r-- 1 reader reader    383 Nov  5 19:58 redirect-to-file.sh
-rw-rw-r-- 1 reader reader     38 Nov  5 19:58 redirect-to-file.txt
reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt
I like dogs! And cats. Maybe a gecko?

现在,这和广告宣传的一样。但是,如果我们再次运行它,我们会看到这个脚本可能会出现两个问题:

reader@ubuntu:~/scripts$ bash chapter_12/redirect-to-file.sh
Type anything you like: Hello
reader@ubuntu:~/scripts$ ls -l
<SNIPPED>
drwxrwxr-x 2 reader reader 4096 Nov  5 19:58 chapter_12
-rw-rw-r-- 1 reader reader    6 Nov  5 20:02 redirect-to-file.txt
reader@ubuntu:~/scripts$ bash chapter_12/redirect-to-file.sh
Type anything you like: Bye
reader@ubuntu:~/scripts$ ls -l
<SNIPPED>
drwxrwxr-x 2 reader reader 4096 Nov  5 19:58 chapter_12
-rw-rw-r-- 1 reader reader    4 Nov  5 20:02 redirect-to-file.txt

我们之前已经警告过,第一件出错的事情是相对路径可能会在写入文件的地方出错。

您可能已经预见到文件就在脚本旁边创建;只有当您的当前工作目录在脚本所在的目录中时,才会发生这种情况。因为我们从树的较低位置调用它,所以输出被写到那里(因为那是当前的工作目录)。

另一个问题是,每次我们输入内容时,我们都会删除文件的旧内容!在我们键入Hello之后,我们看到文件是六个字节(每个字符一个字节,加上一个换行符),在我们键入Bye之后,我们现在看到文件只有四个字节(三个字符加上换行符)。

这可能是期望的行为,但是如果输出被附加到文件而不是替换它,通常会好得多。

让我们在新版本的脚本中解决这两个问题:

reader@ubuntu:~/scripts$ vim chapter_12/redirect-to-file.sh 
reader@ubuntu:~/scripts$ cat chapter_12/redirect-to-file.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.1.0
# Date: 2018-11-05
# Description: Redirect user input to file.
# Usage: ./redirect-to-file.sh
#####################################

# Since we're dealing with paths, set current working directory.
cd $(dirname $0)

# Capture the users' input.
read -p "Type anything you like: " user_input

# Save the users' input to a file. > for overwrite, >> for append.
echo ${user_input} >> redirect-to-file.txt

现在,如果我们运行它(从任何地方),我们将看到新的文本被附加到第一句话中,/home/reader/chapter_12/redirect-to-file.txt文件中的I like dogs! And cats. Maybe a gecko?:

reader@ubuntu:~/scripts$ cd /tmp/
reader@ubuntu:/tmp$ cat /home/reader/scripts/chapter_12/redirect-to-file.txt 
I like dogs! And cats. Maybe a gecko?
reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_12/redirect-to-file.sh
Type anything you like: Definitely a gecko, those things are awesome!
reader@ubuntu:/tmp$ cat /home/reader/scripts/chapter_12/redirect-to-file.txt 
I like dogs! And cats. Maybe a gecko?
Definitely a gecko, those things are awesome!

所以,cd $(dirname $0)帮助我们找到了相对路径,一个>>而不是>保证了追加而不是覆盖。正如你所料,>>又是1>>的缩写,我们将在稍后开始重定向stderr流时看到。

不久前,我们答应给你一些巴什魔法。虽然不完全是魔法,但可能会伤到你的头一点点:

reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt 
I like dogs! And cats. Maybe a gecko?
Definitely a gecko, those things are awesome!
reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/pts/0
I like dogs! And cats. Maybe a gecko?
Definitely a gecko, those things are awesome!
reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/fd/1
I like dogs! And cats. Maybe a gecko?
Definitely a gecko, those things are awesome!
reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/fd/2
I like dogs! And cats. Maybe a gecko?
Definitely a gecko, those things are awesome!

因此,我们总共使用cat打印了四次文件。你可能会想,我们也可以用for做到这一点,但教训不是我们打印信息的次数,而是我们是如何做到的!

第一,我们刚刚用了cat;那里没什么特别的。接下来,我们将catstdout重定向到我们的终端/dev/pts/0结合使用。再次打印消息。

第三次和第四次,我们把cat的重定向stdout发送到/dev/fd/1/dev/fd/2。由于这些符号与/dev/pts/0相关联,因此这些符号出现在我们的终端上也就不足为奇了。

那么我们实际上如何区分stdoutstderr

标准错误

如果你被前面的例子弄糊涂了,那可能是因为你误解了stderr消息所带的流程(我们不怪你,我们在那里把自己搞糊涂了!).当我们将cat命令的输出发送到/dev/fd/2时,我们使用了>,它发送stdout而不是stderr

所以在我们的例子中,我们只是滥用stderr文件描述符打印到终端;糟糕的做法。我们保证不再做了。那么,我们怎样才能让实际上处理stderr消息呢?

reader@ubuntu:/tmp$ cat /root/
cat: /root/: Permission denied
reader@ubuntu:/tmp$ cat /root/ 1> error-file
cat: /root/: Permission denied
reader@ubuntu:/tmp$ ls -l
-rw-rw-r-- 1 reader reader    0 Nov  5 20:35 error-file
reader@ubuntu:/tmp$ cat /root/ 2> error-file
reader@ubuntu:/tmp$ ls -l
-rw-rw-r-- 1 reader reader   31 Nov  5 20:35 error-file
reader@ubuntu:/tmp$ cat error-file 
cat: /root/: Permission denied

这种互动应该能说明一些事情。第一,当cat /root/抛出Permission denied错误时,发送到stderr而不是stdout。我们可以看到这一点,因为当我们执行相同的命令,但我们试图用1> error-file重定向标准 输出时,我们仍然在终端中看到输出*,我们还看到error-file为空。*

当我们改为使用2> error-file,它重定向stderr而不是常规的stdout,我们不再在我们的终端中看到错误信息。

更好的是,我们现在看到error-file有 31 字节的内容,当我们用cat打印时,我们又一次看到了我们重定向的错误消息!如前所述,本着与1>>相同的精神,如果您想将追加到而不是覆盖到一个文件中,请使用2>>

现在,因为很难找到一个同时打印stdoutstderr的命令,我们将创建自己的命令:一个非常简单的 C 程序,打印两行文本,一行到stdout,一行到stderr

作为编程和编译的预演,看看这个(如果你不完全理解,不要担心):

reader@ubuntu:~/scripts/chapter_12$ vim stderr.c 
reader@ubuntu:~/scripts/chapter_12$ cat stderr.c 
#include <stdio.h>
int main()
{
  // Print messages to stdout and stderr.
  fprintf(stdout, "This is sent to stdout.\n");
  fprintf(stderr, "This is sent to stderr.\n");
  return 0;
}

reader@ubuntu:~/scripts/chapter_12$ gcc stderr.c -o stderr
reader@ubuntu:~/scripts/chapter_12$ ls -l
total 744
-rw-rw-r-- 1 reader reader 737150 Nov  5 19:45 redirected-file.log
-rw-rw-r-- 1 reader reader    501 Nov  5 20:09 redirect-to-file.sh
-rw-rw-r-- 1 reader reader     84 Nov  5 20:13 redirect-to-file.txt
-rwxrwxr-x 1 reader reader   8392 Nov  5 20:46 stderr
-rw-rw-r-- 1 reader reader    185 Nov  5 20:46 stderr.c

gcc stderr.c -o stderr命令将stderr.c中的源代码编译成二进制stderr

gcc是 GNU 编译器集合,默认情况下并不总是安装。如果你想继续这个例子,并且你得到一个关于找不到gcc的错误,使用sudo apt install gcc -y安装它。

如果我们运行我们的程序,我们会得到两行输出。因为这不是 Bash 脚本,所以我们不能用bash stderr来执行。我们需要用chmod制作二进制可执行文件,并用./stderr运行它:

reader@ubuntu:~/scripts/chapter_12$ bash stderr
stderr: stderr: cannot execute binary file
reader@ubuntu:~/scripts/chapter_12$ chmod +x stderr
reader@ubuntu:~/scripts/chapter_12$ ./stderr 
This is sent to stdout.
This is sent to stderr.

现在,让我们看看当我们开始重定向部分输出时会发生什么:

reader@ubuntu:~/scripts/chapter_12$ ./stderr > /tmp/stdout
This is sent to stderr.
reader@ubuntu:~/scripts/chapter_12$ cat /tmp/stdout 
This is sent to stdout.

因为我们只将stdout(最后提醒:>等于1>)重定向到完全合格的文件/tmp/stdout,所以stderr消息仍然被打印到终端。

反过来给出类似的结果:

reader@ubuntu:~/scripts/chapter_12$ ./stderr 2> /tmp/stderr
This is sent to stdout.
reader@ubuntu:~/scripts/chapter_12$ cat /tmp/stderr 
This is sent to stderr.

现在,当我们仅使用2> /tmp/stderr重定向stderr时,我们看到stdout消息出现在我们的终端中,并且stderr被正确重定向到/tmp/stderr文件。

我肯定你现在在问自己这个问题:我们如何将所有输出重定向到一个文件,包括stdoutstderr?如果这是一本关于 Bash 3.x 的书,我们会有一个艰难的对话。那次对话需要我们将stderr重定向到stdout,之后我们可以使用>将所有输出(因为我们已经首先将stderr重定向到stdout到一个文件。

尽管这是合乎逻辑的方式,但是stderrstdout的重定向实际上出现在命令的末尾。命令的结尾是这样的:./stderr > /tmp/output 2>&1。不是太复杂,而是够难的,你永远不会真的一口气记住它(这一点你可以相信我们)。

幸运的是,在 Bash 4.x 中,我们有了一个新的重定向命令,可以做同样的事情,但是方式更容易理解:&>

重定向所有输出

在大多数情况下,发送到stderr而不是stdout的输出将包含明确表示您正在处理错误的单词。这将包括例如permission deniedcannot execute binary filesyntax error near unexpected token等等。

正因为如此,通常没有必要将输出分成stdoutstderr(但是,很明显,有时会有很好的功能)。在这种情况下,Bash 4.x 的加入让我们可以用一个命令重定向stdoutstderr是完美的。这个重定向可以和语法&>一起使用,它和我们之前看到的例子没有什么不同。

让我们回顾一下之前的例子,看看这是如何让我们的生活变得更轻松的:

reader@ubuntu:~/scripts/chapter_12$ ./stderr
This is sent to stdout.
This is sent to stderr.
reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /tmp/output
reader@ubuntu:~/scripts/chapter_12$ cat /tmp/output
This is sent to stderr.
This is sent to stdout.

太棒了!有了这个语法,我们不再需要担心不同的输出流。这在使用新命令时尤其实用;在这种情况下,您可能会错过有趣的错误消息,因为它们在stderr流未保存时丢失了。

冒着听起来重复的风险,将stdoutstderr追加到文件中的语法也是额外的> : &>>

继续,用前面的例子试试。我们不会在这里打印它,因为现在应该很明显这是如何工作的。

Unsure about whether to redirect all output, or just stdout or stderr? Our advice: start with redirecting both to the same file. If in your use case this gives too much noise (either masking errors or normal log messages), you could always decide to redirect either of them to a file, and get the other printed in your Terminal. Often, in practice, stderr messages need the context provided by stdout messages to make sense of the error anyway, so you may as well have them conveniently located in the same file!

特殊输出重定向

虽然发送所有输出通常是一件好事,但您会发现自己经常做的另一件事是将错误(在某些命令中可能会出现)重定向到一个特殊设备:/dev/null

null有点放弃功能:它在垃圾桶和黑洞之间的某个地方。

/开发/空

实际上,发送(实际上,写入)到/dev/null的所有数据都将被丢弃,但是仍然会生成一个写操作成功返回到调用命令。在这种情况下,这将是重定向。

这很重要,因为看看重定向无法成功完成时会发生什么:

reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /root/file
-bash: /root/file: Permission denied
reader@ubuntu:~/scripts/chapter_12$ echo $?
1

此操作失败(因为reader用户显然无法写入root超级用户的主目录)。

看看当我们用/dev/null做同样的事情时会发生什么:

reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /dev/null 
reader@ubuntu:~/scripts/chapter_12$ echo $?
0
reader@ubuntu:~/scripts/chapter_12$ cat /dev/null 
reader@ubuntu:~/scripts/chapter_12$

么事儿啦在那里。所有输出都消失了(由于&>重定向,stdoutstderr都消失了),但是命令仍然报告了0的理想退出状态。当我们确定数据没有了,我们就使用cat /dev/null,这不会产生任何结果。

我们将向您展示一个实际的例子,您无疑会发现自己经常在脚本中使用这个例子:

reader@ubuntu:~/scripts/chapter_12$ vim find.sh 
reader@ubuntu:~/scripts/chapter_12$ cat find.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-06
# Description: Find a file.
# Usage: ./find.sh <file-name>
#####################################

# Check for the current number of arguments.
if [[ $# -ne 1 ]]; then
  echo "Wrong number of arguments!"
  echo "Usage: $0 <file-name>"
  exit 1
fi

# Name of the file to search for.
file_name=$1

# Redirect all errors to /dev/null, so they don't clutter the terminal.
find / -name "${file_name}" 2> /dev/null

除了stderr/dev/null重定向之外,这个脚本只包含我们之前介绍过的构造。虽然这个find.sh脚本实际上只不过是一个简单的find命令的包装器,但它有很大的不同。

看看我们用find找文件find.sh文件会怎么样(因为为什么不呢!):

reader@ubuntu:~/scripts/chapter_12$ find / -name find.sh
find: ‘/etc/ssl/private’: Permission denied
find: ‘/etc/polkit-1/localauthority’: Permission denied
<SNIPPED>
find: ‘/sys/fs/pstore’: Permission denied
find: ‘/sys/fs/fuse/connections/48’: Permission denied
/home/reader/scripts/chapter_12/find.sh
find: ‘/data/devops-files’: Permission denied
find: ‘/data/dev-files’: Permission denied
<SNIPPED>

我们已经削减了大约 95%的产出,因为你可能会同意五页的Permission denied错误没有多少价值。因为我们以普通用户的身份运行find,所以我们无法访问系统的许多部分。这些错误反映了这一点。

正如前面强调的,我们确实找到了我们的脚本,但是在您遇到它之前,它可能需要几分钟的滚动时间。这正是我们所说的错误输出淹没相关输出的意思。

现在,让我们用包装脚本来寻找相同的文件:

reader@ubuntu:~/scripts/chapter_12$ bash find.sh find.sh
/home/reader/scripts/chapter_12/find.sh

开始了。同样的结果,但没有那些讨厌的错误迷惑我们。由于Permission denied错误被发送到stderr流,我们find命令后使用2> /dev/null删除了错误。

这实际上把我们带到了另一点:您也可以使用重定向来使命令静音。我们已经看到了许多包含--quiet-q标志的命令。有些命令,例如find,没有这个标志。

你可以说find有这个标志会很奇怪(不想知道文件在哪里,为什么还要搜索文件,对吧?),但可能还有其他命令,其中退出代码提供了足够的信息,但没有--quiet标志;这些都是将一切重新导向/dev/null的绝佳人选。

All commands are different. While most have an available --quiet flag by now, there will always be cases in which this does not work for you. Perhaps the --quiet flag only silences stdout and not stderr, or perhaps it only reduces output. In any case, knowledge about redirecting all output to /dev/null when you're really not interested in that output (only in the exit status) is a very good thing to have!

/dev/zero

我们可以使用的另一种特殊装置是/dev/zero。当我们将输出重定向到/dev/zero时,它的作用与/dev/null完全相同:数据消失。然而,在实践中,/dev/null最常用于此目的。

那么,为什么会有这种特殊的装置呢?因为/dev/zero也可以用来读取空字节。在所有可能的 256 个字节中,空字节是第一个:十六进制00。例如,空字节通常用于表示命令的终止。

现在,我们还可以使用这些空字节向磁盘分配字节:

reader@ubuntu:/tmp$ ls -l
-rw-rw-r-- 1 reader reader   48 Nov  6 19:26 output
reader@ubuntu:/tmp$ head -c 1024 /dev/zero > allocated-file
reader@ubuntu:/tmp$ ls -l
-rw-rw-r-- 1 reader reader 1024 Nov  6 20:09 allocated-file
-rw-rw-r-- 1 reader reader   48 Nov  6 19:26 output
reader@ubuntu:/tmp$ cat allocated-file 
reader@ubuntu:/tmp$ 

通过使用head -c 1024,我们指定我们想要的第一个 1024 个字符来自 /dev/zero。因为/dev/zero只提供空字节,这些都是一样的,但是我们肯定会有1024的。

我们使用stdout重定向将它们重定向到一个文件,然后我们看到一个大小为 1024 字节的文件(多么令人惊讶)。现在,如果我们cat这个文件,我们什么也看不见!同样,这不应该是一个惊喜,因为空字节就是:空,空,空。终端没有办法表示它们,所以它没有。

如果您需要在脚本中这样做,还有另一种选择:fallocate:

reader@ubuntu:/tmp$ fallocate --length 1024 fallocated-file
reader@ubuntu:/tmp$ ls -l
-rw-rw-r-- 1 reader reader 1024 Nov  6 20:09 allocated-file
-rw-rw-r-- 1 reader reader 1024 Nov  6 20:13 fallocated-file
-rw-rw-r-- 1 reader reader   48 Nov  6 19:26 output
reader@ubuntu:/tmp$ cat fallocated-file 
reader@ubuntu:/tmp$ 

从前面的输出中可以看到,这个命令确实完成了我们已经完成的/dev/zero读取和重定向(如果fallocate实际上是一个包装从/dev/zero读取的花哨包装,我们不会感到惊讶,但是我们不能确定这一点)。

输入重定向

另外两个著名的特殊设备/dev/random/dev/urandom最好与下一个重定向一起讨论:输入重定向

输入通常来自键盘,由终端传递给命令。最简单的例子是read命令:它从stdin开始读取,直到遇到一个换行符(当按下回车键时),然后将输入保存到REPLY变量(或者任何自定义的,如果你给了那个参数的话)。看起来有点像这样:

reader@ubuntu:~$ read -p "Type something: " answer
Type something: Something
reader@ubuntu:~$ echo ${answer}
something

别紧张。现在,假设我们以非交互方式运行该命令,这意味着我们不能使用键盘和终端来提供信息(这不是read的真实用例,但这是一个很好的例子)。

在这种情况下,我们可以使用stdin的输入重定向来将输入提供给read。这是通过<字符实现的,它是<0的简写。还记得stdin文件描述符是/dev/fd/0吗?不是巧合。

让我们通过重定向stdin以非交互方式使用read来读取文件,而不是终端:

reader@ubuntu:/tmp$ echo "Something else" > answer-file
reader@ubuntu:/tmp$ read -p "Type something: " new_answer < answer-file
reader@ubuntu:/tmp$ echo ${new_answer}
Something else

为了表明我们没有欺骗和重复使用已经存储在${answer}变量中的答案,我们将存储read回复的变量重命名为${new_answer}

现在,在命令的末尾,我们从answer-file文件重定向stdin,这是我们首先使用echo +重定向stdout创建的。这就像在命令后面加上< answer-file一样简单。

这种重定向使得read从文件中读取,直到遇到换行符(这是echo总是以此结束字符串的便利之处)。

现在输入重定向的基础应该已经清楚了,让我们回到我们的特殊设备:/dev/random/dev/urandom。这两个特殊的文件是伪随机数发生器,这是一个复杂的词,表示几乎产生随机数据的东西。

在这些特殊设备的情况下,它们从设备驱动程序、鼠标移动和其他大部分随机的事物中收集(类似随机性的复杂词)。

/dev/random/dev/urandom略有不同:当系统熵不够时,/dev/random停止产生随机输出,/dev/urandom继续前进。

如果你真的需要全熵,/dev/random可能是更好的选择(老实说,在这种情况下,你可能会采取其他措施),但大多数情况下,/dev/urandom是你的脚本中更好的选择,因为阻塞会产生难以置信的等待时间。这来自第一手经验,可能会非常不方便!

举个例子,我们只展示/dev/urandom/dev/random的输出类似。

实际上,/dev/urandom随机地吐出字节*。有些字节在可打印的 ASCII 字符范围内(1-9、a-z、A-Z),其他字节用于空格(0x20)或换行符(0x0A)。*

*使用head -1/dev/urandom中抓取“第一行”可以看出随机性。由于一行以换行符结束,命令head -1 /dev/urandom将打印所有内容,直到第一个换行符:可以是几个或很多个字符;

reader@ubuntu:/tmp$ head -1 /dev/urandom 
~d=G1���RB�Ҫ��"@
                F��OJ2�%�=�8�#,�t�7���M���s��Oѵ�w��k�qݙ����W��E�h��Q"x8��l�d��P�,�.:�m�[Lb/A�J�ő�M�o�v��
                                                                                                        �
reader@ubuntu:/tmp$ head -1 /dev/urandom 
��o�u���'��+�)T�M���K�K����Y��G�g".!{R^d8L��s5c*�.đ�

我们运行的第一个实例比第二个实例打印了更多的字符(不是所有字符都可读);这可以直接与生成的字节的随机性联系起来。第二次运行head -1 /dev/urandom时,我们遇到了换行字节 0x0A,比第一次迭代要快。

生成密码

现在,您可能想知道随机字符可能会有什么用途。一个主要的例子是生成密码。长的随机密码总是好的;它们能抵抗蛮力攻击,无法被猜到,而且如果不被重用的话,非常安全。老实说,使用自己的 Linux 系统中的熵来生成随机密码有多酷?

更好的是,我们可以使用来自/dev/urandom的输入重定向以及tr命令来实现这一点。一个简单的脚本如下所示:

reader@ubuntu:~/scripts/chapter_12$ vim password-generator.sh 
reader@ubuntu:~/scripts/chapter_12$ cat password-generator.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-06
# Description: Generate a password.
# Usage: ./password-generator.sh <length>
#####################################

# Check for the current number of arguments.
if [[ $# -ne 1 ]]; then
  echo "Wrong number of arguments!"
  echo "Usage: $0 <length>"
  exit 1
fi

# Verify the length argument.
if [[ ! $1 =~ ^[[:digit:]]+$ ]]; then
  echo "Please enter a length (number)."
  exit 1
fi

password_length=$1

# tr grabs readable characters from input, deletes the rest.
# Input for tr comes from /dev/urandom, via input redirection.
# echo makes sure a newline is printed.
tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c ${password_length}
echo

标题和输入检查,甚至是用正则表达式检查一个数字的检查,现在应该都清楚了。

接下来,我们使用tr命令和来自/dev/urandom的重定向输入来抓取我们的 a-z、A-Z 和 0-9 集合中的可读字符。这些是从到head管道(本章稍后将详细介绍管道),这将导致第一个 x 字符被打印给用户(如脚本参数中所指定的)。

为了确保终端格式正确,我们在没有参数的情况下快速插入echo;这只是打印一个换行符。就这样,我们构建了自己的私有安全离线密码生成器。甚至使用输入重定向!

高级重定向

我们现在已经看到了输入和输出重定向,以及两者的一些实际用途。然而,我们还没有将这两种形式的重定向结合起来,这是非常可能的!

不过,您可能不会经常使用它;大多数命令接受输入作为参数,并且通常提供一个标志,允许您指定要输出到的文件。但是知识就是力量,如果你遇到一个没有这些论点的命令,你知道你可以自己解决这个问题。

在命令行中尝试以下操作,并尝试理解为什么会得到您看到的结果:

reader@ubuntu:~/scripts/chapter_12$ cat stderr.c 
#include <stdio.h>
int main()
{
  // Print messages to stdout and stderr.
  fprintf(stdout, "This is sent to stdout.\n");
  fprintf(stderr, "This is sent to stderr.\n");
  return 0;
}

reader@ubuntu:~/scripts/chapter_12$ grep 'stderr' < stderr.c 
  // Print messages to stdout and stderr.
  fprintf(stderr, "This is sent to stderr.\n");
reader@ubuntu:~/scripts/chapter_12$ grep 'stderr' < stderr.c > /tmp/grep-file
reader@ubuntu:~/scripts/chapter_12$ cat /tmp/grep-file 
  // Print messages to stdout and stderr.
  fprintf(stderr, "This is sent to stderr.\n");

如您所见,我们可以在同一行使用<>来重定向输入和输出。首先,我们在grep 'stderr' < stderr.c命令中使用带有输入重定向的grep(这在技术上也是grep 'stderr' stderr.c所做的)。我们在终端中看到输出。

接下来,我们在该命令后面添加> /tmp/grep-file,这意味着我们将把stdout重定向到那个/tmp/grep-file文件。我们不再在终端中看到输出,但是当我们cat文件时,我们得到它,所以它被成功写入文件。

由于我们在本章的高级部分,我们将演示输入重定向放在哪里并不重要:

reader@ubuntu:~/scripts/chapter_12$ < stderr.c grep 'stdout' > /tmp/grep-file-stdout
reader@ubuntu:~/scripts/chapter_12$ cat /tmp/grep-file-stdout 
 // Print messages to stdout and stderr.
 fprintf(stdout, "This is sent to stdout.\n");

这里,我们在命令的开头指定了输入重定向。对我们来说,当您考虑流程时,这感觉像是更符合逻辑的方法,但这会导致实际命令(grep)出现在命令的大致中间,这会弄乱可读性。

这主要是一个没有实际意义的问题,因为在实践中,我们发现输入和输出重定向的用处都很小;即使在这个例子中,我们也只是将命令写成grep 'stdout' stderr.c > /tmp/grep-file-stdout,混乱的结构就消失了。

但是真正了解输入和输出是怎么回事,以及一些命令是如何为你做一些繁重的工作的,是值得你花时间的!这些正是您在更复杂的脚本中会遇到的问题,完全理解这一点将为您节省大量的故障排除时间。

重定向重定向

我们已经给你一个重定向过程的预览。最著名的例子是将stderr流重定向到stdout流,这个例子在 Bash 4.x 之前使用最多。通过这样做,您可以仅使用>语法重定向所有输出。

你可以这样实现:

reader@ubuntu:/tmp$ cat /etc/shadow
cat: /etc/shadow: Permission denied
reader@ubuntu:/tmp$ cat /etc/shadow > shadow
cat: /etc/shadow: Permission denied
reader@ubuntu:/tmp$ cat shadow 
#Still empty, since stderr wasn't redirected to the file.
reader@ubuntu:/tmp$ cat /etc/shadow > shadow 2>&1 
#Redirect fd2 to fd1 (stderr to stdout).
reader@ubuntu:/tmp$ cat shadow 
cat: /etc/shadow: Permission denied

请记住,您不再需要 Bash 4.x 的这种语法,但是如果您想使用自己的自定义文件描述符作为输入/输出流,这将是有用的知识。通过以2>&1结束命令,我们将所有stderr输出(2>)写入stdout描述符(&1)。

我们也可以反过来做:

reader@ubuntu:/tmp$ head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash
reader@ubuntu:/tmp$ head -1 /etc/passwd 2> passwd
root:x:0:0:root:/root:/bin/bash
reader@ubuntu:/tmp$ cat passwd
#Still empty, since stdout wasn't redirected to the file.
reader@ubuntu:/tmp$ head -1 /etc/passwd 2> passwd 1>&2
#Redirect fd1 to fd2 (stdout to stderr).
reader@ubuntu:/tmp$ cat passwd 
root:x:0:0:root:/root:/bin/bash

所以现在,我们将stderr流重定向到passwd文件。然而,head -1 /etc/passwd命令只传送一个stdout流;我们看到它被打印到终端,而不是文件。

当我们使用1>&2(也可以写成>&2)时,我们将stdout重定向到stderr。现在它被写入文件,我们可以在那里cat它!

Remember, this is advanced information, which is mostly useful for your theoretical understanding and when you start working with your own custom file descriptors. For all other output redirections, play it safe and use the &> syntax as we discussed earlier.

命令替换

虽然不是严格意义上的 Linux 重定向,但是命令替换在我们看来是一种功能重定向的形式:您使用一个命令的输出作为另一个命令的参数。如果我们需要使用输出作为下一个命令的输入,我们会使用管道(正如我们将在几页中看到的),但是有时我们只需要在命令中非常具体的位置使用输出。

这是使用命令替换的地方。我们已经在一些脚本中看到了命令替换:cd $(dirname $0)。简单来说,这和dirname $0的结果有点像cd

dirname $0返回脚本所在的目录(因为$0是脚本的完全限定路径),所以当我们将这个用于脚本时,我们将确保所有操作总是相对于脚本所在的目录执行。

如果没有命令替换,我们需要将输出存储在某个地方,然后才能再次使用它:

dirname $0 > directory-file
cd < directory-file
rm directory-file

虽然这个有时会起作用,但这里有一些陷阱:

  • 你需要在你有写权限的地方写一个文件
  • cd后需要清理文件
  • 您需要确保该文件不会与其他脚本冲突

长话短说,这远远不是一个理想的解决方案,最好避免。而且由于 Bash 提供了命令替换,所以使用它没有真正的缺点。正如我们所看到的,cd $(dirname $0)中的命令替换为我们处理这个,不需要我们跟踪文件或变量或任何其他复杂的构造。

命令替换实际上在 Bash 脚本中使用得相当多。看看下面的例子,其中我们使用命令替换来实例化和填充一个变量:

reader@ubuntu:~/scripts/chapter_12$ vim simple-password-generator.sh 
reader@ubuntu:~/scripts/chapter_12$ cat simple-password-generator.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-10
# Description: Use command substitution with a variable.
# Usage: ./simple-password-generator.sh
#####################################

# Write a random string to a variable using command substitution.
random_password=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 20)

echo "Your random password is: ${random_password}"

reader@ubuntu:~/scripts/chapter_12$ bash simple-password-generator.sh 
Your random password is: T3noJ3Udf8a2eQbqPiad
reader@ubuntu:~/scripts/chapter_12$ bash simple-password-generator.sh 
Your random password is: wu3zpsrusT5zyvbTxJSn

对于这个例子,我们重用了我们早期password-generator.sh脚本中的逻辑。这一次,我们没有给用户提供长度的选项;我们保持简单,假设长度为 20(至少在 2018 年,这是一个相当不错的密码长度)。

我们使用命令替换将结果(随机密码)写入变量,然后将变量echo发送给用户。

我们其实可以用一行代码来完成:

reader@ubuntu:~/scripts/chapter_12$ echo "Your random password is: $(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 20)"
Your random password is: REzCOa11pA2846fvxsa

然而,正如我们到现在已经讨论了很多次的那样,可读性很重要(仍然!).我们觉得,在实际使用变量之前,先用描述性名称编写变量,可以增加脚本的可读性。

此外,如果我们想多次使用相同的随机值,我们无论如何都需要一个变量。所以在这种情况下,我们脚本中额外的冗长对我们有帮助,也是可取的。

The predecessor to $(..) was the use of backticks, which is the ```sh character (found next to the 1 on English-International keyboards). `$(cd dirname $0)` was previously written as cd dirname $0. While this mostly does the same as the newer (and better) `$(..)` syntax, there are two things that were often an issue with backticks: word splitting and newlines. These are both issues that are caused by whitespace. It is much easier to use the new syntaxes and not have to worry about things like this!

过程替代

与命令替代密切相关的是*过程替代。*语法如下:

<(command)
```sh

它的工作原理与命令替换非常相似,但是您可以将输出作为文件引用,而不是将命令的输出作为字符串发送到某个地方。这意味着一些不需要字符串而是引用文件的命令也可以用于动态输入。

虽然过于高级,无法详细讨论,但这里有一个简单的例子,应该可以说明问题:

reader@ubuntu:~/scripts/chapter_12$ diff <(ls /tmp/) <(ls /home/) 1,11c1 < directory-file < grep-file < grep-file-stdout < passwd < shadow

reader

`diff`命令通常比较两个文件并打印它们的差异。现在,我们不再使用文件,而是使用过程替换让`diff`使用`<(ls /tmp/)`语法比较来自`ls /tmp/``ls /home/`的结果。

# 管道

最后,我们都在等待的时刻:**管道**。这些近乎神奇的构造在 Linux/Bash 中被大量使用,每个人都应该知道它们。任何比单个命令更复杂的事情几乎总是使用管道来获得解决方案。

现在最大的启示是:管道真正做的只是将一个命令的`stdout`连接到另一个命令的`stdin`。

等等什么?!

# 将标准输出绑定到标准输入

是的,事实上就是这样。现在您已经了解了输入和输出重定向的所有知识,这可能有点令人失望。然而,仅仅因为概念简单,并不意味着管道不是**极其强大的**并且应用非常广泛。

让我们看一个例子,它展示了我们如何用管道替换输入/输出重定向:

reader@ubuntu:/tmp$ echo 'Fly into the distance' > file reader@ubuntu:/tmp$ grep 'distance' < file Fly into the distance reader@ubuntu:/tmp$ echo 'Fly into the distance' | grep 'distance'Fly into the distance

对于正常的重定向,我们首先将一些文本写入一个文件(使用输出重定向),然后将它用作`grep`的输入。接下来,我们做完全相同的功能,但是没有文件作为中间步骤。

基本上,管道语法如下:

command-with-output | command-using-input

您可以在一条线上使用多个管道,也可以使用管道和输入/输出重定向的任何组合,只要它有意义。

通常,当您到达两个以上的管道/重定向点时,您可以用一个额外的行来增加可读性,也许可以使用命令替换将中间结果写入变量。但是,从技术上来说,你可以让它变得像你想要的那样*复杂*;保持警惕,不要把事情弄得太复杂**

 *如前所述,管道将`stdout``stdin`绑定在一起。你可能对即将到来的问题有个想法:`stderr`!看看这个输出分离成`stdout``stderr`如何影响管道的例子:

reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow | grep 'denied' cat: /etc/shadow: Permission denied reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow | grep 'denied' > /tmp/empty-file cat: /etc/shadow: Permission denied #Printed to stderr on terminal. reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow | grep 'denied' 2> /tmp/error-file cat: /etc/shadow: Permission denied #Printed to stderr on terminal. reader@ubuntu:/scripts/chapter_12$ cat /tmp/empty-file reader@ubuntu:~/scripts/chapter_12$ cat /tmp/error-file

最初,这个例子可能会让你感到困惑。让我们一步一步来弄清楚。

第一,`cat /etc/shadow | grep 'denied'`。我们试着把`grep``cat /etc/shadow``stdout`换成`denied`这个词。我们实际上并没有找到它,但我们看到它印在我们的终端上。为什么呢?因为即使`stdout`被输送到`grep`,但是`stderr`被直接送到我们的终端(而**不是**`grep`)。

如果你是通过 SSH 连接到 Ubuntu 18.04,当`grep`成功的时候,默认应该会看到颜色高亮;在本例中,您不会遇到这种情况。

下一个命令`cat /etc/shadow | grep 'denied' > /tmp/empty-file`**`grep`**`stdout`重定向到一个文件。由于`grep`没有处理错误信息,文件保持为空。

即使我们试图在最后重定向`stderr`,正如在`cat /etc/shadow | grep 'denied' 2> /tmp/error-file`命令中可以看到的,我们仍然没有在文件中获得任何输出。这是因为重定向**是顺序的**:输出重定向只适用于`grep`,不适用`cat`。

现在,以同样的方式,输出重定向有一种重定向`stdout``stderr`的方法,带有`|&`语法的管道也是如此。再看同一个例子,现在使用正确的重定向:

reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' cat: /etc/shadow: Permission denied reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' > /tmp/error-file reader@ubuntu:/scripts/chapter_12$ cat /tmp/error-file cat: /etc/shadow: Permission denied reader@ubuntu:/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' 2> /tmp/error-file cat: /etc/shadow: Permission denied reader@ubuntu:~/scripts/chapter_12$ cat /tmp/error-file

对于第一个命令,如果您启用了颜色语法,您将看到单词`denied`是粗体和彩色的(在我们的例子中,红色)。这意味着现在我们使用`|&``grep`确实成功处理了输出。

接下来,当我们使用`grep``stdout`重定向时,我们看到我们成功地将输出写入了一个文件。如果我们试图用`2>`重定向它,我们会再次看到它被打印在终端中,但不是文件中。这是因为重定向的顺序性:一旦`grep`成功处理了输入(来自`stderr`),`grep`将其输出到`stdout``grep`实际上并不知道输入原来是一个`stderr`流;就其而言,只是`stdin`来处理。既然`grep`的成功过程去了`stdout`,那就是我们最终找到它的地方!

如果我们想要安全并且不需要分离`stdout``stderr`的功能,最安全的方法是使用如下命令:`cat /etc/shadow |& grep 'denied' &> /tmp/file`。因为管道和输出重定向都要处理`stdout``stderr`,所以我们总是会将所有输出放在我们想要的地方。

# 实例

因为管道的理论现在应该相对简单了(当我们讨论输入和输出重定向时,我们已经把大部分内容排除在外),所以我们将给出一些实际的例子来说明管道的力量。

记住管道只对接受`stdin`输入的命令起作用是很好的;不是所有人都这样。如果您将某些东西传递给完全忽略该输入的命令,您可能会对结果感到失望。

既然我们现在已经介绍了管道,我们将在本书的其余部分更广泛地使用它们。虽然这些例子将展示一些使用管道的方法,但本书的其余部分将包含更多内容!

# 又一个密码生成器

因此,我们已经创建了两个密码生成器。由于 3 是一个神奇的数字,这是一个很好的例子来演示链接管道,我们将再创建一个(最后一个,promise):

reader@ubuntu:/scripts/chapter_12$ vim piped-passwords.sh reader@ubuntu:/scripts/chapter_12$ cat piped-passwords.sh #!/bin/bash

#####################################

Author: Sebastiaan Tammer

Version: v1.0.0

Date: 2018-11-10

Description: Generate a password, using only pipes.

Usage: ./piped-passwords.sh

#####################################

password=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c20)

echo "Your random password is: ${password}"

首先,我们从`/dev/urandom`抓取前 10 行(默认行为为`head`)。我们将这个发送到`tr`,它将它修剪成我们想要的字符集(因为它也输出不可读的字符)。然后,当我们有一个可以使用的字符集时,我们再次使用`head`从该字符集中抓取前 20 个字符。

如果只是`head /dev/urandom | tr -dc 'a-zA-Z0-9'`跑几次,就会看到长短不一;这是因为换行符字节的随机性。通过从`/dev/urandom`中抓取 10 行,没有足够的可读字符来创建 20 个字符的密码的机会非常小。

(对读者的挑战:创建一个循环的脚本,这样做足够长的时间来遇到这种情况!)

这个例子说明了一些事情。首先,我们经常可以用一些智能管道实现很多我们想做的事情。其次,多次使用同一个命令并不罕见。顺便说一下,我们也可以选择`tail -c20`作为链中的最终命令,但是这与整个命令有很好的对称性!

最后,我们看到了三种不同的密码生成器,它们实际上做着同样的事情。一如既往,在 Bash 中,有许多方法可以实现相同的目标;由你来决定哪一个最适用。就我们而言,可读性和性能应该是这个决定的两个主要因素。

# 在脚本中设置密码

您可能发现自己想要编写脚本的另一项任务是为本地用户设置密码。虽然从安全角度来看,这并不总是好的做法(尤其是对于个人用户帐户),但它是用于功能帐户(对应于软件的用户,如运行`httpd`进程的 Apache 用户)的做法。

这些用户大多不需要密码,但有时他们需要。在这种情况下,我们可以使用带有`chpasswd`命令的管道来设置它们的密码:

reader@ubuntu:/scripts/chapter_12$ vim password-setter.sh reader@ubuntu:/scripts/chapter_12$ cat password-setter.sh #!/bin/bash

#####################################

Author: Sebastiaan Tammer

Version: v1.0.0

Date: 2018-11-10

Description: Set a password using chpasswd.

Usage: ./password-setter.sh

#####################################

NEW_USER_NAME=bob

Verify this script is run with root privileges.

if [[ $(id -u) -ne 0 ]]; then echo "Please run as root or with sudo!" exit 1 fi

We only need exit status, send all output to /dev/null.

id ${NEW_USER_NAME} &> /dev/null

Check if we need to create the user.

if [[ $? -ne 0 ]]; then

User does not exist, create the user.

useradd -m ${NEW_USER_NAME} fi

Set the password for the user.

echo "${NEW_USER_NAME}:password" | chpasswd

在运行此脚本之前,请记住,这将使用非常简单(错误)的密码将用户添加到您的系统中。我们为这个脚本更新了一点输入卫生:我们使用命令替换来查看脚本是否以 root 权限运行。因为`id -u`返回用户的数字 ID,在 root 用户或者 sudo 权限的情况下应该是 0,所以我们可以使用`-ne 0`进行比较。

如果我们运行脚本,而用户不存在,我们会在为该用户设置密码之前创建用户。这是通过管道将`username:password`发送到`chpasswd``stdin`来完成的。请注意,我们使用了`-ne 0`两次,但用于非常不同的事情:第一次用于比较用户标识,第二次用于比较退出状态。

你可能会想到这个脚本的多种改进。例如,能够同时指定用户名和密码而不是这些硬编码的伪值可能是件好事。另外,在`chpasswd`命令后进行一次理智检查绝对是个好主意。在当前迭代中,脚本没有给**任何**反馈给用户;非常糟糕的做法。

看看能不能解决这些问题,一定要记住用户指定的任何输入都要彻底检查*!如果你真的想要一个挑战,通过从一个文件中抓取输入,在`for`循环中为多个用户做这个。*

*An important thing to note is that a process, when running, is visible to any user on the system. This is often not that big a problem, but if you're providing usernames and passwords directly to the script as arguments, those are visible to everyone as well. This is often only for a very short time, but they will be visible nonetheless. Always keep security in mind when dealing with sensitive issues such as passwords.

# 球座

似乎是为了与管道协同工作而创建的命令是`tee`。手册页上的描述应该讲述了大部分故事:

tee - read from standard input and write to standard output and files

所以,本质上,发送东西到`tee`的`stdin`(通过管道!)允许我们同时将输出保存到您的终端和一个文件中。

这在使用交互式命令时通常最有用;它允许您实时跟踪输出,但也可以将其写入(日志)文件供以后查看。更新系统为`tee`用例提供了一个很好的例子:

sudo apt upgrade -y | tee /tmp/upgrade.log

我们可以通过将*所有*输出发送到`tee`,包括`stderr`,让它变得更好:

sudo apt upgrade -y |& tee /tmp/upgrade.log

输出如下所示:

reader@ubuntu:/scripts/chapter_12$ sudo apt upgrade -y |& tee /tmp/upgrade.log WARNING: apt does not have a stable CLI interface. Use with caution in scripts. Reading package lists... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. reader@ubuntu:/scripts/chapter_12$ cat /tmp/upgrade.log WARNING: apt does not have a stable CLI interface. Use with caution in scripts. Reading package lists... 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

终端输出和日志文件的第一行都是`WARNING`,发送到`stderr`;如果你用`|`代替`|&`,那就不会写入日志文件,只会在屏幕上显示。如果你按照建议使用`|&`,你会看到你屏幕上的输出和文件的内容完全匹配。

默认情况下,`tee`覆盖目标文件。像所有形式的重定向一样,`tee`也有一种追加而不是覆盖的方式:`--append` ( `-a`)标志。根据我们的经验,这通常是一个谨慎的选择,与`|&`并无不同。

While `tee` is a great asset for your command-line arsenal, it most definitely has its place in scripting as well. Once your scripts get more complex, you might want to save parts of the output to a file for later review. However, to keep the user updated on the status of a script, printing some to the Terminal might also be a good idea. If these two scenarios overlap, you'll need to use `tee` to get the job done!

# 这里有文件

我们将在本章中介绍的最后一个概念是的*文档。这里的文档,也称为 heredocs,用于向某些命令提供输入,与`stdin`重定向略有不同。值得注意的是,向命令提供多行输入是一种简单的方法。它使用以下语法:*

cat << EOF input more input the last input EOF

如果您在终端中运行此程序,您将看到以下内容:

reader@ubuntu:~/scripts/chapter_12$ cat << EOF

input more input the last input EOF input more input the last input

`<<`语法让 Bash 知道您想要使用一个 heredoc。紧接着,您提供了一个*定界标识符*。这可能看起来很复杂,但它实际上意味着您提供了一个将终止输入的字符串。因此,在我们的示例中,我们提供了常用的`EOF`(T4 end**o**f**f**ile 的缩写)。

现在,如果 heredoc 在输入中遇到与定界标识符完全匹配的行,它将停止侦听进一步的输入。下面是另一个例子,更详细地说明了这一点:

reader@ubuntu:~/scripts/chapter_12$ cat << end-of-file

The delimiting identifier is end-of-file But it only stops when end-of-file is the only thing on the line end-of-file does not work, since it has text after it end-of-file The delimiting identifier is end-of-file But it only stops when end-of-file is the only thing on the line end-of-file does not work, since it has text behind it

虽然用`cat`来说明这一点,但并不是一个很实际的例子。然而`wall`命令是。`wall`让您向连接到服务器的每个人,向他们的终端广播消息。与 heredoc 结合使用时,它看起来有点像这样:

reader@ubuntu:~/scripts/chapter_12$ wall << EOF

Hi guys, we're rebooting soon, please save your work! It would be a shame if you lost valuable time... EOF

Broadcast message from reader@ubuntu (pts/0) (Sat Nov 10 16:21:15 2018):

Hi guys, we're rebooting soon, please save your work! It would be a shame if you lost valuable time...

在这种情况下,我们接收自己的广播。但是,如果您与您的用户多次连接,您将看到广播也进入其中。

尝试同时使用终端控制台连接和 SSH 连接;如果你亲眼看到,你会更好地理解它。

# 这里有文档和变量

使用 heredocs 时,混淆的一个来源通常是使用变量。默认情况下,变量在 heredoc 中解析,如下例所示:

reader@ubuntu:~/scripts/chapter_12$ cat << EOF

Hi, this is $USER! EOF Hi, this is reader!

然而,这可能并不总是理想的功能。您可能想用它来写一个文件,变量应该在以后解析。

在这种情况下,我们可以引用 EOF 的定界标识符来防止变量被替换:

reader@ubuntu:~/scripts/chapter_12$ cat << 'EOF'

Hi, this is $USER! EOF Hi, this is $USER!

# 使用此文档进行脚本输入

由于 heredocs 允许我们简单地将换行符分隔的输入传递给一个命令,我们可以使用它以非交互方式运行一个交互脚本!我们已经在实践中使用了这一点,例如,在只能交互运行的数据库安装程序脚本中。但是,一旦您知道了问题的顺序和您想要提供的输入,您就可以使用 heredoc 将这些输入提供给交互式脚本。

更好的是,我们已经创建了一个使用交互式输入的脚本,`/home/reader/scripts/chapter_11/while-interactive.sh`,我们可以用它来展示这个功能:

reader@ubuntu:/tmp$ head /home/reader/scripts/chapter_11/while-interactive.sh #!/bin/bash

#####################################

Author: Sebastiaan Tammer

Version: v1.1.0

Date: 2018-10-28

Description: A simple riddle in a while loop.

Usage: ./while-interactive.sh

#####################################

reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_11/while-interactive.sh << EOF a mouse #Try 1. the sun #Try 2. keyboard #Try 3. EOF

Incorrect, please try again. #Try 1. Incorrect, please try again. #Try 2. Correct, congratulations! #Try 3. Now we can continue after the while loop is done, awesome!

我们知道剧本一直持续到得到正确的答案,要么是`keyboard`要么是`Keyboard`。我们使用此文档依次向脚本发送三个答案:`a mouse``the sun`,最后是`keyboard`。我们可以很容易地将输出与输入对应起来。

为了更详细,运行带有`bash -x`的 heredoc 输入的脚本,它将明确地向您展示这个谜语有三种尝试。

You might want to use a here document within a nested function (which will be explained in the next chapter) or within a loop. In both cases, you should already be using indentation to improve readability. However, this impacts your heredoc, because the whitespace is considered part of the input. If you find yourself in that situation, heredocs have an extra option: `<<-` instead of `<<`. When supplying the extra `-`, all *tab characters* are ignored. This allows you to indent the heredoc construction with tabs, which maintains both readability and function.

# 这里是字符串

本章我们最不想讨论的就是*这里的字符串*。它非常类似于这里的文档(因此得名),但它处理的是一个字符串,而不是一个文档(谁能想到呢!).

这种使用`<<<`语法的构造可用于向命令提供文本输入,该命令通常可能只接受来自`stdin`或文件的输入。一个很好的例子是`bc`,这是一个简单的计算器(GNU 项目的一部分)。

通常,您可以通过两种方式之一使用它:通过管道向`stdin`发送输入,或者通过将`bc`指向文件:

reader@ubuntu:/tmp$ echo "2^8" | bc 256

reader@ubuntu:/tmp$ echo "4*4" > math reader@ubuntu:/tmp$ bc math bc 1.07.1 Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. 16 ^C (interrupt) use quit to exit. quit

`stdin`一起使用时,`bc`返回计算结果。与文件一起使用时,`bc`会打开一个交互会话,我们需要通过输入`quit`来手动关闭。对于我们想要实现的目标来说,这两种方式似乎都有些力不从心。

让我们看看这里的字符串是如何修复这个问题的:

reader@ubuntu:/tmp$ bc <<< 2^8 256


开始了。这里只是一个简单的字符串作为输入(它被发送到命令的`stdin`),我们得到了与带有管道的`echo`相同的功能。然而,现在它只是一个单一的命令,而不是一个链条。简单却有效,正是我们喜欢的方式!

# 摘要

本章解释了关于 Linux 上*重定向*的几乎所有知识。我们从什么是重定向,以及如何使用*文件描述符*来促进重定向的一般描述开始。我们了解到文件描述符 0、1 和 2 分别用于`stdin`、`stdout`和`stderr`。

然后我们熟悉了重定向的语法。这包括`>`、`2>`、`&>`和`<`及其附加语法、`>>`、`2>>`、`&>>`和`<<`。

我们讨论了一些特殊的 Linux 设备,`/dev/null`、`/dev/zero`和`/dev/urandom`。我们展示了如何使用这些设备来移除输出、生成空字节和生成随机数据的示例。在高级重定向部分,我们展示了我们可以将`stdout`绑定到`stderr`,反之亦然。

此外,我们还学习了*命令替换*和*过程替换*,这允许我们将一个命令的结果用于另一个命令的参数中,或者作为一个文件。

接下来是*管道*。管道很简单,但是非常强大,Bash 构造,用于将一个命令的`stdout`(可能还有`stderr`)连接到另一个命令的`stdin`。这允许我们链接命令,在我们前进的过程中进一步操纵数据流,通过我们想要的任意数量的命令。

我们还引入了`tee`,它允许我们向我们的终端和文件发送一个流,这是一个经常用于日志文件的结构。

最后,我们解释了这里的*文档*和这里的*字符串*。这些概念允许我们将多行和单行输入直接从终端发送到其他命令的`stdin`中,否则这些命令将需要`echo`或`cat`。

本章介绍了以下命令:`diff`、`gcc`、`fallocate`、`tr`、`chpasswd`、`tee`和`bc`。

# 问题

1.  什么是文件描述符?
2.  术语`stdin`、`stdout`和`stderr`是什么意思?
3.  `stdin`、`stdout`和`stderr`如何映射到默认文件描述符?
4.  输出重定向`>`、`1>`和`2>`有什么区别?
5.  `>`和`>>`有什么区别?
6.  如何同时重定向`stdout`和`stderr`?
7.  哪些特殊设备可以作为输出黑洞?
8.  关于重定向,管道有什么作用?
9.  我们如何向终端和日志文件发送输出?
10.  这里字符串的典型用例是什么?

# 进一步阅读

*   **点击以下链接**阅读更多关于文件描述符的信息:[https://linuxmerkat . WordPress . com/2011/12/02/file-descriptor-explained/](https://linuxmeerkat.wordpress.com/2011/12/02/file-descriptors-explained/)。

*   **在以下链接**中查找带有文件描述符的高级脚本的信息:[https://bash . cyberiti . biz/guide/Reads _ from _ file _ descriptor _(FD)](https://bash.cyberciti.biz/guide/Reads_from_the_file_descriptor_(fd))。
*   **在以下链接**阅读更多关于命令替换的信息:[http://www.tldp.org/LDP/abs/html/commandsub.html](http://www.tldp.org/LDP/abs/html/commandsub.html)。
*   **点击以下链接**:[https://www.tldp.org/LDP/abs/html/here-docs.html](https://www.tldp.org/LDP/abs/html/here-docs.html)查找本文档信息。***