189. Linux中的命令之大杂烩

history

使用history命令可以使工作更高效。

随着我使用终端的时间越来越多,我一直在寻找一些能让我提高效率的命令。history 是一个可以改变我日常工作的命令。

history 命令保存了从终端会话开始后的所有其他命令的列表,允许你重新使用这些命令,而不是再次输入。如果你是一个经验丰富的终端用户,那么你应该知道 history,但是对于一些新的系统管理员来说,history是一个可以产生即时效益的工具。

首先,history 命令实际上不是一个真正的命令。你可以通过在系统中查找命令来验证这一点:

1
2
$ which history
which: no history in (/usr/local/bin:/usr/bin:/bin:/usr/games:/usr/local/sbin)

你会看到在计算机中找不到 history 命令,因为它是 shell 的一个内置关键字。也正是因为它是写在 shell 中的,所以根据你使用的是 Bash, tcsh, Zsh, dash, fish, ksh 等等,history 会有一些不同。本文是基于Bash来描述的,因此某些函数可能无法在其他 shell 中工作。但大多数的基本功能是相同的。

history 101

要查看 history 历史记录,在linux系统中打开终端,然后输入:

1
$ history

你会看到:

1
2
3
4
clear
ls -al
sudo dnf update -y
history

history 命令显示你启动终端会话以来所输入的命令列表。有意思的是,你可以使用以下命令来重新调用列表中的任何一个(历史命令):

1
$ !3

这个 !3 命令是告诉 shell 执行历史记录表中的第三行命令。此外也可以通过输入以下命令来访问该命令:

1
$ !sudo dnf

这个是让 history 搜索与你提供的表达式相匹配的最后一个命令(在上面这个例子中,这个表达式是dnf)并运行它。

查找 history

你还可以使用 history 来重新运行上次输入的命令,方法是键入:!!. 通过将它与 grep 配对,可以搜索与文本模式匹配的命令,或者通过将它与 tail 一起使用,可以找到最后执行的几个命令。例如:

1
2
3
$ history | grep dnf
3 sudo dnf update -y
5 history | grep dnf
1
2
3
4
$ history | tail -n 3
4 history
5 history | grep dnf
6 history | tail -n 3

获取此搜索功能的另一种方法是键入 Ctrl-R 以调用对命令历史记录的递归搜索。键入后,提示变为:

1
(reverse-i-search)`':

这样你可以通过点击回车(Enter)或者回退(Return)来选择匹配的命令。

修改和执行命令

你还可以通过 history 来修改命令以返回一个新的命令。比如我想要将 history | grep dnf 改为 history | grep ssh, 可以执行以下命令:

1
$ ^dnf^ssh^

命令将被执行,但会以ssh来替换dnf,也就是说,会执行以下命令:

1
$ history | grep ssh

删除 history

有时候你可能会要删除部分或者所有的历史命令。如果要删除部分历史命令,使用 history -d <line number>, 如果要清除所有的历史记录,可以执行 history -c.

history列表存储在一个用户可以修改的文件中。Bash用户可以在你的home路径中找到它,文件名是 .bash_history.

其他

你还可以用 history 来做一些其他事情:

  • 将 history 缓冲区的大小设置为某一数值;
  • 记录历史记录中每一行的日期和时间;
  • 阻止某些命令被记录在 history 记录中。

有关 history 命令的其他信息或者有意思的用法,可以看一下 Seth Kenlon 关于parsing history, history 搜索修饰符的文章,或者 GNU Bash 的操作手册。

reference:

shell中2>&1的含义解释

首先了解下1和2在Linux中代表什么

在Linux系统中0 1 2是一个文件描述符

名称 代码 操作符 Java中表示 Linux 下文件描述符(Debian 为例)
标准输入(stdin) 0 <<< System.in /dev/stdin -> /proc/self/fd/0 -> /dev/pts/0
标准输出(stdout) 1 >, >>, 1>1>> System.out /dev/stdout -> /proc/self/fd/1 -> /dev/pts/0
标准错误输出(stderr) 2 2> 或者 2>> System.error /dev/stderr -> /proc/self/fd/2 -> /dev/pts/0

从上表看的出来,我们平时使用的

1
echo "hello" > t.log

其实也可以写成

1
echo "hello" 1> t.log

关于2>&1的含义

(关于输入/输出重定向本文就不细说了,主要是要了解> < << >> <& >& 这6个符号的使用)

1)含义:将标准错误输出重定向到标准输出

2)符号>&是一个整体,不可分开,分开后就不是上述含义了。

比如有些人可能会这么想:2是标准错误输出,1是标准输出,>是重定向符号,那么”将标准错误输出重定向到标准输出”是不是就应该写成”2>1“就行了?是这样吗?

如果是尝试过,你就知道2>1的写法其实是将标准错误输出重定向到名为”1”的文件里去了

3)写成2&>1也是不可以的。

为什么2>&1要放在后面

考虑如下一条shell命令

1
nohup java -jar app.jar >log 2>&1 &

(最后一个&表示把条命令放到后台执行,不是本文重点,不懂的可以自行Google)

为什么2>&1一定要写到>log后面,才表示标准错误输出和标准输出都定向到log中?

我们不妨把1和2都理解是一个指针,然后来看上面的语句就是这样的:

  • 本来1—–>屏幕(1指向屏幕)
  • 执行>log后, 1—–>log (1指向log)
  • 执行2>&1后, 2—–>1 (2指向1,而1指向log,因此2也指向了log)

再来分析下

1
nohup java -jar app.jar 2>&1 >log&
  • 本来1—–>屏幕(1指向屏幕)
  • 执行2>&1后, 2—–>1 (2指向1,而1指向屏幕,因此2也指向了屏幕)
  • 执行>log后, 1—–>log (1指向log,2还是指向屏幕)

所以这就不是我们想要的结果。

简单做个试验测试下上面的想法:

java代码如下:

1
2
3
4
5
6
public class Htest {
publicstatic void main(String[] args) {
System.out.println("out1");
System.err.println("error1");
}
}

javac编译后运行下面指令:

1
java Htest 2>&1 > log

你会在终端上看到只输出了”error1”,log文件中则只有”out1”

每次都写”>log2>&1”太麻烦,能简写吗?

有以下两种简写方式

  • &>log
  • >&log

比如上面小节中的写法就可以简写为:

1
nohup java -jar app.jar &>log &

上面两种方式都和”>log 2>&1“一个语义。

那么上面两种方式中&>>&有区别吗?

语义上是没有任何区别的,但是第一种方式是最佳选择,一般使用第一种。

输入、输出和错误重定向

我们都知道 Linux 命令的功能,它接受一个输入,然后给你一个输出。在这里起作用的包含一些重要的角色,我们今天就来介绍一下这些角色。

stdin, stdout 和 stderr

当你运行 Linux 命令时,有三个数据流在其中起作用:

数据流 说明
stdin (Standard input,标准输入)是输入数据的源。
默认情况下,stdin 是从键盘输入的任何文本,它的流 ID(stream ID) 为 0;
stdout (Standard output,标准输出)是命令的输出结果。
默认情况下,它会显示在屏幕上,它的流 ID(stream ID) 为 1;
stderr (Standard error,标准错误)是命令产生的错误消息(如果有)。
默认情况下,屏幕上也会显示 stderr。它的流 ID(stream ID)是2。

这些流包含存储在内存缓冲区(buffer memory)中的纯文本数据。

把它想象成一个水流,你需要水源,比如水龙头,用管道连接到它,可以将其存储在水桶(文件)中,也可以给植物浇水(打印)。如果需要,还可以将其连接到另外一个水龙头上,也就是改变水的流向(重定向)。

Linux 中也有这种重定向的概念。你可以将 stdin, stdoutstderr 从其原本的目标,重定向到另一个文件或命令(甚至是打印机等外围设备)。

接下来我们来介绍一下重定向是如何工作的,以及如何使用它。

输出重定向

第一种也是最简单的重定向形式是输出重定向,也称为标准输出重定向

默认情况下,命令的输出是显示在屏幕上。比如,我使用 ls 命令列出当前目录下的所有文件:

1
2
[admin@fedora work]$ ls
appstxt new.txt static-ip.txt

通过输出重定向,可以将输出重定向到文件。如果此输出文件不存在,那么 shell 将创建它。

1
command > file

比如,我们将上述 ls 命令的输出保存到名为 output.txt 的文本文件中:

1
[admin@fedora work]$ ls > output.txt

输出文件是预先创建的

那么这个 output.txt 文件的内容是什么呢?我们用 cat 命令来看一下:

1
2
3
4
5
[admin@fedora work]$ cat output.txt 
appstxt
new.txt
output.txt
static-ip.txt

有没有注意到里面也包含 output.txt?将输出重定向输出到的文件(output.txt)是在运行预期命令之前创建的。为什么呢?因为它需要准备好输出的目的地,输出将被发送到该目的地。

追加而不是删除

一个经常被忽略的问题是,如果重定向到一个已经存在的文件,shell 将首先删除该文件。这意味着输出文件的现有内容将被删除,并替换为命令的输出。

如果不想删除原有的内容,而只是追加,那么可以使用 >> 重定向语法:

1
command >> file

你可以在当前的 shell 会话中禁止删除,使用 set -C

将输出重定向到文件,可以将输出的内容保存起来以供将来参考;另外如果输出的内容太多,占用了大篇幅的屏幕时,将内容保存到文件,就更方便查看和分析了,这就像收集日志文件一样。

管道重定向

在介绍 stdin 重定向之前,我们先来了解一下管道重定向,这是更加常见的,我们会经常使用管道重定向。

关于管道重定向,可以参阅我们先前的文章:管道pipe

通过管道重定向,可以将命令的标准输出发送到另一个命令的标准输入

1
command 1 | command 2

我们来举个例子,如果我们要查看当前目录中文件的数量,可以使用 ls -1(注意是数字1,不是字母L)来显示当前目录中的文件:

1
2
3
4
5
[admin@fedora work]$ ls -1
appstxt
new.txt
output.txt
static-ip.txt

我们知道 wc 命令用于计算文件中的行数,所以我们可以结合这个命令,如下:

1
2
[admin@fedora work]$ ls -1 | wc -l
4

使用管道,两个命令共享相同的内存缓冲区,第一个命令的输出存储在缓冲区中,然后该缓冲区将用作下一个命令的输入。

你将看到管道中最后一个命令的结果。这一点很明显,因为先前命令的 stdout 被重定向到下一个命令,而不是打印在屏幕上。

管道重定向或管道不限于仅连接两个命令,你也可以连接多个命令,只要一个命令的输出可用作下一个命令输入。

1
command_1 | command_2 | command_3 | command_4

请注意,stdout/stdin 是一块数据,而不是文件名

一些新的Linux用户在使用重定向时会感到困惑,如果命令返回一组文件名作为输出,则不能将这些文件名用作参数。

比如,使用 find 命令查找扩展名为 .txt 的文件,无法通过管道将查找到的这些文件移动到新的目录,不能这样操作:

1
find . -type f -name "*.txt" | mv destination_directory

这就是为什么我们经常会看到 find 命令与 exec 或 xargs 命令组合使用的原因。这些命令可以将大量的文本“文件名”转换为文件名,且可作为参数传递。

1
find . -type f -name "*.txt" | xargs -t -I{} mv {} ../new_dir

关于find 与 exec 或 xargs 命令组合使用的相关内容,可参考:使用xargs命令

输入重定向

使用 stdin 重定向可以将文本文件的内容传递给终端命令:

1
command < file

但是这种并不常用,因为大多数 Linux 命令都可以接受文件名作为参数,因此通常不需要 stdin 重定向。比如:

1
head < filename.txt

上面的命令可以直接写为:head filename.ext(也就是不用重定向符号 <)。

也不是说 stdin 重定向完全没有用,有些命令是依赖于它的。以 tr 命令为例,这个命令可以做很多事情,但在下面的例子中,它将输入文本从小写转为大写:

1
tr a-z A-Z < filename.txt

事实上,建议在管道上使用标准输入重定向,以避免不必要地使用 cat 命令。

比如上面的例子,很多人就习惯使用 cat,然而这里并没有必要使用cat

1
cat filename.txt | tr a-z A-Z

合并重定向

你可以根据需要组合使用 stdin,stdout 和管道重定向。

比如,下面的命令列出当前目录下所有的 txt 文件,然后统计一下文件的总数,并将这个结果保存到一个新文件中:

1
ls *.txt | wc -l > count.txt

错误重定向

有时候,当你运行一些命令或脚本时,会在屏幕上看到一条错误信息:

1
2
[admin@fedora work]$ ls -l ffffff > output.txt
ls: cannot access 'ffffff': No such file or directory

在本文的开头,我们提到过有三种数据流,stderr 是其中之一,它默认将输出显示在屏幕上。

你也可以重定向 stderr。由于它是一个输出数据流,因此可以使用与标准输出重定向相同的 >>> 重定向符号。

但是,当 stdout 和 stderr 都作为输出数据流时,如何区分它们呢?通过它们的流 ID(stream ID,也称为文件描述符)。

Data stream **stream ID **
stdin 0
stdout 1
stderror 2
-t -list
-u -update
-x –extract, –get
-j –bzip2
-z –gzip, –gunzip, –ungzip

默认情况下,当你使用输出重定向符号 > 时,它实际上是 1 >。换句话说,ID 为 1 的数据流是在这里输出。

所以当你要重定向 stderr 时,可以使用它的ID,比如 2> 或者 2>>。这表示输出重定向用于数据流 stderr(ID为2)。

stderr 重定向示例

我们来举几个例子。假如我们只想要保存错误信息,那么可以这样:

1
2
3
[admin@fedora work]$ ls fffff 2> error.txt
[admin@fedora work]$ cat error.txt
ls: cannot access 'fffff': No such file or directory

这个很简单。我们再来个稍微复杂一点的(并且很有用的):

1
2
3
4
5
[admin@fedora work]$ ls -l new.txt ffff > output.txt 2> error.txt 
[admin@fedora work]$ cat output.txt
-rw-rw-r-- 1 gliu gliu 0 May 5 10:25 new.txt
[admin@fedora work]$ cat error.txt
ls: cannot access 'ffff': No such file or directory

在上面的例子中,ls 命令尝试显示两个文件的信息。其中一个文件能成功,另一个会出错。所以我在这里做的是将 stdout 输出重定向到 output.txt 文件中,将 stderr 重定向到 error.txt 中(使用 2>)。

此外,我们还可以将 stdout 和 stderr 重定向到同一个文件,是有办法做到这一点的。

在下面的例子中,我使用追加模式(append mode)将 stderr 重定向到文件 combined.txt 中(使用 2>>);然后,同样使用追加模式将标准输出 stdout 重定向到同一个文件 combined.txt 中(使用 >>):

1
2
3
4
[admin@fedora work]$ ls -l new.txt fff 2>> combined.txt >> combined.txt 
[admin@fedora work]$ cat combined.txt
ls: cannot access 'fff': No such file or directory
-rw-rw-r-- 1 gliu gliu 0 May 5 10:25 new.txt

另一种方法,也是首选的方法,是使用类似于 2>&1 的东西,可以大致的翻译为“将 stderr 重定向到与 stdout 相同的地址中”。

我们以前面的示例为例,这次使用 2>&1 将 stdout 和 stderr 重定向到同一个文件:

1
2
3
4
[admin@fedora work]$ ls -l new.txt fff > output.txt 2>&1
[admin@fedora work]$ cat output.txt
ls: cannot access 'fff': No such file or directory
-rw-rw-r-- 1 gliu gliu 0 May 5 10:25 new.txt

在这里需要注意的一点是,不能想当然的以为 2>>&1 为追加模式,因为 2>&1 本身就是追加模式

此外,你也可以先使用 2>,然后使用 1>&2 将 stdout 重定向到与 stderr 相同的文件上。基本上,>& 是将一个输出数据流重定向到另一个上。

我们来总结一下

  1. 有三种数据流,其中一个是输入数据流 stdin(0),两个输出数据流为 stdout(1) 和 stderr(2);

  2. 键盘输入是默认的标准输入设备,屏幕是默认的输出设备;

  3. 输出重定向使用 > 或者 >>(用于追加模式);

  4. 输入重定向使用 <

  5. stderr 可以使用 2> 或者 2>>

  6. stderr 和 stdout 可以组合为:2>&1

find

find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。如果使用该命令时,不设置任何参数,则 find 命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示。

语法

1
find  path  -option  [  -print ]  [ -exec  -ok  command ]  {} \;

参数说明

find 根据下列规则判断 path 和 expression,在命令列上第一个 - ( ) ! 之前的部分为 path,之后的是 expression。如果 path 是空字串则使用目前路径,如果 expression 是空字串则使用 -print 为预设 expression。

expression 中可使用的选项有二三十个之多,在此只介绍最常用的部分。

参数 说明
-mount, -xdev 只检查和指定目录在同一个文件系统下的文件,避免列出其它文件系统中的文件
-amin n 在过去 n 分钟内被读取过
-anewer file 比文件 file 更晚被读取过的文件
-atime n 在过去n天内被读取过的文件
-cmin n 在过去 n 分钟内被修改过
-cnewer file 文件 file 更新的文件
-ctime n 在过去n天内被修改过的文件
-empty 空的文件-gid n or -group name : gid 是 n 或是 group 名称是 name
-ipath p, -path p 路径名称符合 p 的文件,ipath 会忽略大小写
-name name, -iname name 文件名称符合 name 的文件。iname 会忽略大小写
-size n 文件大小 是 n 单位,b 代表 512 位元组的区块,c 表示字元数,k 表示 kilo bytes,w 是二个位元组。
-type c 文件类型是 c 的文件。
d 目录
c 字型装置文件
b 区块装置文件
p 具名贮列
f 一般文件
l 符号连结
s socket
-pid n process id 是 n 的文件

你可以使用 () 将运算式分隔,并使用下列运算。

1
2
3
4
5
exp1 -and exp2
! expr
-not expr
exp1 -or exp2
exp1, exp2

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#将当前目录及其子目录下所有文件后缀为 .c 的文件列出来:
$ find . -name "*.c"


#将目前目录其其下子目录中所有一般文件列出
$ find . -type f


#将当前目录及其子目录下所有最近 20 天内更新过的文件列出:
$ find . -ctime -20


#查找 /var/log 目录中更改时间在 7 日以前的普通文件,并在删除之前询问它们:
$ find /var/log -type f -mtime +7 -ok rm {} \;


#查找当前目录中文件属主具有读、写权限,并且文件所属组的用户和其他用户具有读权限的文件:
$ find . -type f -perm 644 -exec ls -l {} \;


#查找系统中所有文件长度为 0 的普通文件,并列出它们的完整路径:
$ find / -type f -size 0 -exec ls -l {} \;

exec

有两种方法可以对find命令的结果执行其他命令:分别是使用 xargsexec 命令。

exec 与 find 命令一起使用

使用 exec 执行 find 命令的基本语法如下:

1
find [path] [arguments] -exec [command] {} \;

上述语法中:

参数 说明
[command] 是要对 find 命令的输出结果执行的操作(后续命令);
{} 是一个占位符,用于保存 find 命令输出的结果;
\; 表示对于每个找到的结果,都会执行一次[command],在这里需要对分号 ; 进行转义,因此需要在前面使用反斜杠 \

注意,在 {}\; 之间需要有一个空格。

还有另外一种用法,与上面的方式有所不同,如下:

1
find [path] [arguments] -exec [command] {} +

这里,加号 + 表示对于 find 命令的所有结果,[command] 只会执行一次,也就是所有结果都作为参数一起传递给 [command]。这里的加号 + 不需要使用反斜杠 \ 进行转义。

{} \; 如同下面的执行(为每一个结果都单独执行一次):

1
2
3
4
5
ls file1.txt

ls file2.txt

ls file3.txt

{} + 如下所示(使用所有的输出作为参数,执行命令一次):

1
ls file1.txt file2.txt file3.txt

通过如上表述,使用 {} + 似乎是个更好的选择,但事实恰恰相反。如果 find 命令有50个结果,就不能将它们作为参数一起传递,因为在命令行中有最大长度限制。

这就是为什么你最好使用 {} \; 的原因,除非你特别清楚将要执行的操作。

find 和 exec 命令一起使用的一些例子

接下来我们分享一些常用的关于 find 和 exec 命令结合使用的例子,以便大家能更好的理解它们。

查找和显示文件属性

下面的这个例子,将显示 /tmp 目录下所有的 lock 文件,并显示它们的属性:

1
sudo find /tmp/ -type f -name *lock -exec ls -l {} \;

结果如下:

1
2
3
sagar@LHB:~$ sudo find /tmp/ -type f -name *lock -exec ls -l {} \; 
-r--r--r-- 1 gdm gdm 11 Jul 17 08:01 /tmp/.X1024-lock
-r--r--r-- 1 gdm gdm 11 Jul 17 08:01 /tmp/.X1025-lock

查找并重命名文件

使用 find 和 exec 可以很方便的重命名文件(重命名文件,可以使用 mv 命令)。

1
sudo find /home/sagar/Downloads/ -type f -name 'ubuntu*' -exec mv {} {}_renamed \;

上面的命令查找以 ubuntu 开头的文件,并将其存储在占位符中。一旦该过程结束,它将在存储在占位符中的每个文件的末尾添加 “_renamed”。

收集并保存文件的大小

在本例中,我们将展示如何收集特定目录下可用文件的大小,并创建一个文件来保存给定的输出。

我们将在 /tmp 目录下收集每个文件的大小,并将输出保存在 /root 目录下,文件名为:du_data.out

1
sudo find /tmp/ -type f -exec du -sh {} \; > /root/du_data.out

那现在,我们在 /root 目录下查看一下创建的文件内容是什么:

使用特定参数删除文件

自动删除文件时请格外小心。如果你不注意,这可能是灾难性的。可以将交互式删除与 rm -i 一起使用,也可以先查看 find 命令的结果。

find 与 exec 命令组合使用的另一个常见例子是查找大于某个值的文件并将其删除。比如,清理一些大的日志文件。

下面这个例子,我们将会删除桌面上大于 100MB 的文件。

1
find ~/Desktop -size +100M -exec rm {} \;

类似,也可以根据文件创建的时间来删除,比如,我们删除超过10天的文件:

1
sudo find /tmp/ -type f -mtime +10 -exec rm {} \;

上述命令中的 -mtime 选项,表示文件的修改时间,后面的数字单位是+10表示超过10天。

使用特定的工具执行操作

在某些情况下,你可能希望在找到文件后,启动某些工具或软件包,来执行后续操作。

比如,我们搜索 mp3 文件,然后运行 id3v2,来显示 mp3 文件的详细信息:

1
find . -name "*.mp3" -exec id3v2 -l {} \;

id3v2 是一个包,它将显示有关 mp3 文件的详细信息,-l 用于显示与找到的 mp3 相关的每个 mp3 标记。

更改文件和目录的所有权

使用 find 和 exec 组合命令,还可以更改文件或目录的所有权,这也是一个很常见的应用场景。

下面的例子,我们查找用户 sagar 所拥有的文件,然后将其用户所有权更改为 milan:

1
sudo find /home/sagar/disk/Downloads -user sagar -type f -exec chown milan {} \;

更改文件权限

另外一个很常用的例子,就是更改文件的权限。

看下面的例子:

1
sudo find /home/sagar/disk/Downloads -type f -exec chmod 644 {} \;

上述命令,用于查找 Downloads 目录下所有的文件(使用 -type f 选项),然后将其权限更改为 644。

为每个文件收集md5sum

在下面的例子中,我们将演示如何为 /tmp 目录下的每个可用文件收集 md5sum 信息:

1
sudo find /tmp/ -type f -exec md5sum {} \;

如下图所示,上述命令会打印每个可用文件的名称和 md5sum 信息:

那么,怎么样才能将上面的输出保存到一个文件中呢?可使用如下命令:

1
sudo find /tmp/ -type f -exec md5sum {} \; > /Documents/checksumdata.out

将 exec 与 grep 命令组合使用

find 命令根据文件名查找,grep 命令可在文件内容中查找。

使用 findgrep 以及 exec 命令的组合,就可以得到一个功能强大的搜索工具。

比如下面的命令,搜索扩展名为 .hbs 的文件,然后使用 grep,将在这些文件中,搜索包含字符串 “excerpt” 的内容。

1
find . -type f -name "*.hbs" -exec grep -iH excerpt {} \;

使用 -H 选项,grep 命令将显示每个匹配的文件名。以下是输出:

1
2
3
4
5
6
7
sagar@LHB:~/Downloads/casper-hyvor$ find . -type f -name "*.hbs" -exec grep -iH excerpt {} \;
./author.hbs: <div class="post-card-excerpt">{{bio}}</div>
./partials/post-card.hbs: {{#if excerpt}}
./partials/post-card.hbs: <div class="post-card-excerpt">{{excerpt}}</div>
./post.hbs: {{#if custom_excerpt}}
./post.hbs: <p class="article-excerpt">{{custom_excerpt}}</p>
./tag.hbs: <div class="post-card-excerpt">

使用多个 exec 命令

是的,你可以在单个 find 命令中,组合使用多个 exec 命令。

我们对上一个命令做一下修改,使用两个 exec 命令:

1
find . -type f -name "*.hbs" -exec echo {} \; -exec grep excerpt {} \;

上述命令,首先搜索扩展名为 .hbs 的文件,然后第一个 exec 命令用于显示搜索到的文件名称;然后,第二个 exec 命令将在这些文件中搜索包含字符串 “excerpt” 的内容。

其输出与上一个命令略有不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sagar@LHB:~/Downloads/casper-hyvor$ find . -type f -name "*.hbs" -exec echo {} \; -exec grep excerpt {} \;
./index.hbs
./page.hbs
./default.hbs
./author.hbs
<div class="post-card-excerpt">{{bio}}</div>
./error-404.hbs
./error.hbs
./partials/icons/twitter.hbs
./partials/icons/fire.hbs
./partials/icons/lock.hbs
./partials/icons/loader.hbs
./partials/icons/rss.hbs
./partials/icons/avatar.hbs
./partials/icons/facebook.hbs
./partials/post-card.hbs
{{#if excerpt}}
<div class="post-card-excerpt">{{excerpt}}</div>
./post.hbs
{{#if custom_excerpt}}
<p class="article-excerpt">{{custom_excerpt}}</p>
./tag.hbs
<div class="post-card-excerpt">

find 是一个功能强大的命令,用于根据给定条件搜索文件,exec 命令可以让我们很方便的处理 find 命令的输出结果。我们上面介绍的也只是一个大概,在实际应用中使用 find 与 exec 命令的组合,为我们在 Linux 命令行中执行操作提供了无限可能。

管道pipe

我们在命令行中经常会用到类似 cmd0 | cmd1 | cmd2 的写法。其实,这是管道重定向(pipe redirection),用于将一个命令的输出作为输入重定向到下一个命令。

那么,你知道它具体是怎么工作的吗?今天我们来详细了解一下。

注:本文中会有多个地方使用 Unix 这个术语(而不是Linux),因为管道的概念起源于 Unix。

Linux 中的管道:总体思路

以下是关于“什么是 Unix 管道?”的内容:

Unix 管道是一种 IPC(Inter Process Communication,进程间通信)机制,它将一个程序的输出转发到另一程序的输入。

现在,我们换一种更加专业且易懂的语言重新解释一下:

Unix 管道是一种 IPC(Inter Process Communication,进程间通信)机制,它接收程序的标准输出(stdout),并通过缓冲区将其转发给另一个程序的标准输入(stdin)。

这样的描述,大家应该能理解了。参考下图可以了解管道的工作原理:

管道命令的最简单示例之一是将一些命令输出传递给 grep 命令以搜索特定字符串。

比如,我们可以搜索名称包含txt的文件,如下所示:

管道将标准输出重定向到标准输入,但不是作为命令参数

有个非常重要的一点需要注意,管道命令将标准输出(stdout)传递到另一个命令的标准输入(stdin),但不是作为参数。下面我们举个例子来说明这一点。

如果我们不带任何参数使用 cat 命令,它默认会从 stdin 读取内容。看下面的例子:

1
2
3
4
$ cat
Hello, my friend.
^D
Hello, my friend.

在上面的例子中,没有带任何参数使用了 cat,因此它默认会读取 stdin。接下来,我写了一行文字,然后按键 Ctrl+d 告诉它我写完了(Ctrl+d 表示 EOF 或文件结束)。随后,cat 命令读取 stdin,然后把之前我写的那行文字输出到了终端中。

现在,看如下命令:

1
echo hey | cat

管道右边的命令并不等于 cat hey。这里,标准输出(stdout)”hey” 被放在了缓冲区(buffer),并被传输到了 cat 命令的标准输入(stdin)。由于没有命令行参数,所以 cat 默认读取 stdin,而 stdin 中恰好有了内容(即“hey”),因此 cat 读取了这个内容,并将其打印到 stdout。

为了演示这个区别,我们可以创建一个名为 hey 的文件,并在其中添加一些文本。参见下图:

Linux 中的管道类型

Linux 中有两种类型的管道:

1)匿名管道,也就是未命名管道;

2)命名管道。

匿名管道

顾名思义,匿名管道就是没有名称。当你使用 | 符号时,它们就会由 Unix shell 动态创建了。

我们通常所说的管道,就是指的匿名管道。它用起来很方便,作为最终用户,我们不需要跟踪它的运行,shell 自动会处理这一切。

命名管道

这个稍有不同,命名管道在文件系统中确实存在。它们像普通文件一样存在,可以使用下面的命令创建命名管道:

1
$ mkfifo pipe

这将创建一个名为 pipe 的文件,执行以下命令:

1
2
$ ls -l pipe
prw-r--r--. 1 admin admin 0 Aug 4 17:23 pipe

请注意开头的“p”,这意味着该文件是一个管道。现在我们来使用这个管道。

如前所述,管道将命令的输出转发给另一个命令的输入。这就像快递服务,你把包裹从一个地址送到另一个地址。因此,第一步是提供包裹。

1
echo hey > pipe

我们会看到 echo 信息没有打印出来,看起来像是被挂起了。新打开一个终端,尝试读取该文件:

1
cat pipe

我们看下两个终端的输出结果,如下图所示:

惊讶吗?这两个命令同时完成了执行。

这是普通文件和命名管道之间的基本区别之一。在其他进程读取管道之前,不会将任何内容写入管道。

那么,为什么要使用命名管道呢?我们来看一下。

命名管道不会占用磁盘上的任何内存。

如果我们执行命令 du -s pipe,就会发现它不会占用任何空间。这是因为命名管道就像从内存缓冲区读写的端点。写入命名管道的任何内容实际上都存储在临时内存缓冲区中,当从另一个进程执行读取操作时,该缓冲区将被刷新。

节省 IO

因为写入命名管道意味着将数据存储到内存中的缓冲区中,因此如果涉及大文件的操作的话,就会大幅减少磁盘 I/O。

两个不同进程之间的通信

通过使用命名管道,可以高效地从另一个进程实时获取事件的输出。因为读和写同时发生,所以没有等待时间。

较低层次的管道理解(针对高级用户和开发人员)

接下来我们更深入的讨论一下管道,以及具体的实现。这些需要对以下内容有基本的了解:

  • C 程序工作原理;
  • 什么是系统调用;
  • 什么是进程;
  • 什么是文件描述符。

我们不会很详细的介绍这些概念,只讨论与管道相关的内容。对于大多数Linux用户来说,下面的内容可以选择性的阅读。

为了进行编译,在文章最后提供了一个示例 makefile。当然,这只是用来说明的伪代码。

看以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pipe.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <errno.h>

extern int errno;

int main(){
signed int fd[2];
pid_t pid;
static char input[50];
static char buf[50];

pipe(fd);

if((pid=fork())==-1){
int err=errno;
perror("fork failed");
exit(err);
}

if(pid){
close(fd[1]);
read(fd[0], buf, 50);
printf("The message read from child: %s\n", buf);
} else {
close(fd[0]);
printf("Enter a message from parent: ");
for(int i=0; (input[i]=getchar())!=EOF && input[i]!='\n' && i<49; i++)
write(fd[1], input, 50);
exit(0);
}
return 0;
}

在第16行,我使用 pipe() 函数创建了一个匿名管道,传递了一个长度为 2 的带符号整数数组。

这是因为管道只是一个包含两个无符号整数的数组,代表两个文件描述符。一个用于写,一个用于读。它们都指向内存上的缓冲区位置,通常为1mb。

这里我将变量命名为fd。fd[0] 是输入文件描述符,fd[1] 是输出文件描述符。在该程序中,一个进程将字符串写入 fd[1] 文件描述符,另一个进程从 fd[0] 文件描述符读取。

命名管道也一样,使用命名管道(而不是两个文件描述符),你可以从任何一个进程中打开一个文件,并像其他文件一样对其进行操作。同时应记住管道的特性。

下面是一个示例程序,它执行与前一个程序相同的操作,但它创建的不是匿名管道,而是命名管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// fifo.c
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

extern int errno;

#define fifo "npipe"

int main(void){
pid_t pid;
static char input[50];
static char buf[50];
signed int fd;

mknod(fifo, S_IFIFO|0700, 0);

if((pid=fork())<0){
int err=errno;
perror("Fork failed");
exit(err);
}

if(pid){
fd=open(fifo, O_RDONLY);
read(fd, buf, 50);
close(fd);
printf("The output is : %s", buf);
remove(fifo);
exit(0);
} else {
fd=open(fifo, O_WRONLY);
for(int i=0; (input[i]=getchar())!=EOF && input[i]!='\n' && i<49; i++);
write(fd, input, strlen(input));
close(fd);
exit(0);
}
return 0;
}

在这里,我使用 mknod 系统调用来创建命名管道。如你所见,虽然在完成时删除了管道,但你可以不使用它,只需要打开并写入本例中的 npipe 文件,就可以轻松的实现在不同进程之间的通信。

其实现实中,我们也不必创建两个管道来实现双向通信,匿名管道就是这样的。

以下是一个简单的 Makefile 的源代码示例(只是示例),将其与前面的程序放在同一个目录中(分别为 pipe.c 和 fifo.c)。

1
2
3
4
5
6
7
8
9
CFLAGS?=-Wall -g -O2 -Werror
CC?=clang

build:
$(CC) $(CFLAGS) -o pipe pipe.c
$(CC) $(CFLAGS) -o fifo fifo.c

clean:
rm -rf pipe fifo

xargs

什么是 xargs 命令

xargs命令从标准输入或另一个命令的输出中读取文本行,并将其转换为命令并执行。

我们经常会看到 xargs 命令与 find 命令一起使用。find 命令提供文件名列表,xargs 命令可以让我们逐个使用这些文件名,把它们当作是另一个命令的输入一样使用。

由于 rargs 会处理重定向,所以你需要提前了解关于标准输入、输出以及管道重定向相关的知识。关于管道重定向,可以参考这里

怎样使用 xargs 命令

xargs 命令的语法如下:

1
xargs [options] [command [initial-arguments]]

但一般我们不这样用,它的一个重要功能是将一个命令的输出组合到另一个命令中。我们看一个例子:

假如在当前路径下有一些txt文件,以各种鲜花名称命名,然后还有一个flowers.txt,记录了所有这些txt文件的名称:

1
2
3
4
5
6
7
8
[admin@fedora work]$ ls
flowers.txt lily.txt one_lotus.txt rose.txt three_lotus.txt two_lotus.txt
[admin@fedora work]$ cat flowers.txt
lily.txt
one_lotus.txt
rose.txt
three_lotus.txt
two_lotus.txt

现在我们的目标是查看 flowers.txt 中提到的所有文件的文件大小。根据以往的经验,我们可以使用 cat 命令来显示所有文件名,然后通过管道将其传输到 du 命令来检查文件大小。

但是如果我们直接使用管道的话,它不会给出 flowers.txt 中提到的每个文件的大小:

1
[admin@fedora work]$ du -h52K.[admin@fedora work]$ cat flowers.txt | du -h52K.

为什么呢?首先,du 命令不接受标准输入;其次,cat 命令的输出不是单个的文件名,而是由换行符隔开的一个文本。

而 xargs 命令的神奇之处在于,它将把这个由空格或换行符分割的文本转换为下一个命令的单独输入。

1
2
3
4
5
6
[admin@fedora work]$ cat flowers.txt | xargs du -h
4.0Klily.txt
4.0Kone_lotus.txt
16Krose.txt
4.0Kthree_lotus.txt
16Ktwo_lotus.txt

这相当于将这些文件名提供给 du 命令:

1
2
3
4
5
6
[admin@fedora work]$ du -h lily.txt one_lotus.txt rose.txt three_lotus.txt two_lotus.txt 
4.0Klily.txt
4.0Kone_lotus.txt
16Krose.txt
4.0Kthree_lotus.txt
16Ktwo_lotus.txt

怎么样,意识到 xargs 命令的强大了吗?

xargs 和 find:为彼此而存在

xargs 命令经常和 find 命令结合使用。

find 命令搜索文件和目录并返回它们的名称,有了 xargs 命令,你就可以将 find 命令的结果用于特定目的,如重命名、移动、删除等等。

比如,我们要搜索所有包含 red 一词的txt文件,可以在 xargs 的帮助下结合 find 和

1
2
3
4
5
grep 命令:
[admin@fedora work]$ find . -type f -name "*.txt" | xargs grep -l red
./three_lotus.txt
./two_lotus.txt
./rose.txt

find 与 exec 命令组合的工作原理类似。不过我们今天只集中讨论 xargs 命令。

处理文件名中带有空格的文件

如果文件名中有空格,会稍微麻烦点。比如我们将上面的文件three_lotus.txt重命名为 three lotus.txt,那么当使用 xargs 处理时,会被认为是两个独立的文件,three 和 lotus.txt:

1
2
3
4
5
[admin@fedora work]$ find . -type f -name "*.txt" | xargs grep -l red
./two_lotus.txt
grep: ./three: No such file or directory
grep: lotus.txt: No such file or directory
./rose.txt

在这种情况下,可以使用 find 命令的 -print0 选项,它使用 ASCII null 字符来换行,而不是换行符;同时,xargs 命令也需要带 -0 选项。

1
[admin@fedora work]$ find . -type f -print0 -name "*.txt" | xargs -0 grep -l red./two_lotus.txt./three lotus.txt./rose.txt

查看正在执行的命令

xargs 命令的 -t 选项,会打印正在执行的实际命令,所以可以用来查看正在执行的命令。

1
2
[admin@fedora work]$ find . -type f -name "*.txt" | xargs -t touch
touch ./three_lotus.txt ./two_lotus.txt ./lily.txt ./rose.txt

在运行命令之前,强制 xargs 提示确认

有些情况需要格外小心,比如删除文件,最好看看要执行什么命令,必要的话可以选择拒绝执行。

可以使用 -p 选项,在执行前进行确认:

1
2
[admin@fedora work]$ find . -type f -name "*.txt" | xargs -p rm
rm ./three_lotus.txt ./two_lotus.txt ./lily.txt ./rose.txt ?...n

结合占位符的使用

默认情况下,xargs命令将标准输入作为参数添加到命令末尾。当需要在最后一个参数之前使用时,这会产生问题。

比如,使用 move 命令,首先需要一个源,然后需要一个目标作为参数;如果要将找到的文件移动的目标文件,那么将不起作用:

1
2
3
[admin@fedora work]$ find . -type f -name "*.txt" | xargs -p mv new_dir
mv new_dir ./three_lotus.txt ./two_lotus.txt ./lily.txt ./rose.txt ?...y
mv: target './rose.txt' is not a directory

这时候,可以用 -I 选项来使用占位符:

1
2
3
4
5
[admin@fedora work]$ find . -type f -name "*.txt" | xargs -p -I {} mv {} new_dir
mv ./three_lotus.txt new_dir ?...n
mv ./two_lotus.txt new_dir ?...n
mv ./lily.txt new_dir ?...n
mv ./rose.txt new_dir ?...n

上述命令中,xargs 从 find 命令中获取所有的文件名,将其保存在 {} 中,然后转到 mv 命令并提供 {} 中的内容。

这里有一个主要的区别,它不是将所有文件名放在同一个命令中,而是逐个添加。这就是每个参数都会单独调用 mv 命令的原因。

注:上述命令中使用 {} 作为占位符,你可以使用其他字符作为占位符。{} 是安全的,易于理解和区分。

使用xargs运行多个命令

可以使用 xargs 的占位符来运行多个命令:

1
2
3
4
5
6
7
8
9
[admin@fedora work]$ find . -type f -name "*.txt" | xargs -I {} sh -c 'ls -l {}; du -h {}' 
-rw-rw-r-- 1 admin admin 0 May 28 17:02 ./three_lotus.txt
0./three_lotus.txt
-rw-rw-r-- 1 admin admin 0 May 28 17:02 ./two_lotus.txt
0./two_lotus.txt
-rw-rw-r-- 1 admin admin 0 May 28 17:02 ./lily.txt
0./lily.txt
-rw-rw-r-- 1 admin admin 0 May 28 17:02 ./rose.txt
0./rose.txt

这里需要注意下,占位符不会扩展到下一个管道重定向或者其他命令,这就是上述命令中为什么会使用 sh 命令的原因。

本文主要介绍了常用的 find 和 xargs 命令的使用,但 xargs 命令不是仅限于和find一起用。xargs 命令的一个很实际的例子是当要停止所有正在运行的 docker 容器时:

1
docker ps -q | xargs docker stop

与其他 Linux 命令一样,xargs 也有很多选项。关于详细信息,大家可以查阅 xargs 命令的 man 手册。

单引号与双引号

我们在 Linux 命令中经常会用到引号,比如处理文件名中的空格,或者处理特殊字符的时候。

本文将介绍不同类型的引号及其在 shell 脚本中的用法。

总结一下,大概有四种类型的引号:

  • 单引号:'
  • 双引号:"
  • 反斜杠:\
  • 反引号:`

除了反斜杠以外,其余三个都是成对出现。下面我们来详细看一下。

单引号

shell 会忽略单引号中所有的特殊字符,其中的所有内容都会被当作一个元素。

我们举个例子,假如有一个文本文件,里面记录了一些人名,如下:

1
2
3
4
5
6
$ cat cricket 
Allan Donald, South Africa
Steve Waugh, Australia
Mark Waugh, Australia
Henry Olonga, Zimbabwe
Sachin Tendulkar, India

现在我们使用 grep 命令在其中搜索 “Waugh”,会得到两个结果:

1
2
3
$ grep Waugh cricket
Steve Waugh, Australia
Mark Waugh, Australia

但是如果我们更精确一点,搜索 Steve Waugh,就会出现错误:

1
2
3
$ grep Steve Waugh cricket
grep: Waugh: No such file or directory
cricket:Steve Waugh, Australia

为什么会这样呢?因为空格在命令行中用于分割命令、选项和参数。在上面的例子中,我们原本是要搜索 Steve Waugh,但 shell 会将 Steve 作为 grep 的第一个参数,然后将 Waugh 和 cricket 作为被搜索的文件,很显然,没有 Waugh 这个文件,所以就报错了。同时,输出中显示了文件 cricket 的结果。

如果我们使用单引号,来搜索 ‘Steve Waugh’,它会被视为一个整体,那这样就得到了我们期望的搜索结果:

1
2
$ grep 'Steve Waugh' cricket
Steve Waugh, Australia

基本上,***当shell看到第一个单引号时,它会忽略任何特殊字符(空格也是一个特殊字符),直到找到另一个单引号(右引号)***。

忽略所有特殊字符

如果用单引号引起来,所有特殊字符都会失去其原本的意义。下面我们举几个例子。

我们在shell中声明一个变量,如果用$回显变量名,它将显示变量的值:

1
2
3
$ var=my_variable
$ echo $var
my_variable

但如果用单引号引起来,$ 将失去其特殊的功能。

1
$ echo '$var'$var

另一个例子,回车键(回车字符)也放在单引号里面:

1
2
3
4
$ echo 'how are
> you?'
how are
you?

双引号

双引号几乎与单引号相似。这里之所以说“几乎”是因为他们也会忽略所有特殊字符,除了:

  • 美元符号:$
  • 反引号:`
  • 反斜杠:\

由于美元符号 $ 不会被忽略,所以我们可以使用变量名,得到变量的值;但是单引号就不能这样:

1
2
3
4
5
$ var=my_variable
$ echo "$var"
my_variable
$ echo '$var'
$var

双引号还可用于在shell中转义单引号(即将其转义为普通字符):

1
2
3
4
5
$ var=My 'own villa' is yellow
own villa: command not found
$ var="My 'own villa' is yellow"
$ echo $var
My 'own villa' is yellow

反过来用(用单引号转义双引号),也可以:

1
2
3
4
5
$ var=he said, "Awesome!"
said,: command not found
$ var='he said, "Awesome!"'
$ echo $var
he said, "Awesome!"

反斜杠

反斜杠用于转义特殊字符。看下面的例子:

1
2
3
4
5
6
7
$ var=variable
$ echo \var
var
$ echo $var
variable
$ echo \$var
$var

因为 v 没有什么特别的含义,echo \var 只是简单的打印了 var。另一方面,当使用 $var 的时候,反斜杠转义了 $ 符号,所以打印出了 $var

用反斜杠转义换行符

我们可以使用反斜杠来转义换行符,这样就可以将单个命令换行继续编写

当命令太长或是多个命令的组合时,你会看到一些网站使用反斜杠在多行中显示单个命令。这可以使命令或代码更具可读性。

看下面的例子:

1
docker run --name server --network net -v html:/usr/share/nginx/html -v $PWD/custom-config.conf:/etc/nginx/nginx.conf -p 80:80 --restart on-failure -d nginx:latest

现在我们使用反斜杠,将上述命令分解为多行:

1
2
3
4
docker run --name server --network net \
-v html:/usr/share/nginx/html \
-v $PWD/custom-config.conf:/etc/nginx/nginx.conf \
-p 80:80 --restart on-failure -d nginx:latest

这样看起来容易多了。

双引号内的反斜杠

在双引号内,\ (反斜杠)、$ (美元符号)以及 ` (反引号)是不被转义的三个符号。

这样的话,我们就可以使用反斜杠来转义双引号中的美元符号、双引号以及反引号了。

看下面的例子,其中的 $5 会被认为是一个未声明的变量,且没有被赋值,所以在 echo 命令中会将其忽略:

1
2
$ echo "Meal costs $5.25"
Meal costs .25

要避免上述情况,可以使用 \ 来转义 $

1
2
$ echo "Meal costs \$5.25"
Meal costs $5.25

反引号

反引号( ` )有一个特殊含义,用于命令替换

Shell 具有此命令替换功能,其中指定的命令将替换为命令的输出。

在下面的例子中,date 会被替换为 date 命令的输出信息,即系统的当前日期和时间而输出:

1
2
$ echo The current date and time is `date`
The current date and time is Monday 23 August 2021 04:55:18 PM IST

需要注意的一点是,只有当反引号之间是一个命令的时候,才会进行替换,否则,将会按原内容显示

1
2
$ echo 'The current date and time is `late`'
The current date and time is `late`

如前文所述,将反引号放在双引号中,不会被转义,但是放在单引号中,会被转义为普通字符。

反引号不在建议使用

很长时间以来,反引号都用于 Shell 脚本中的命令替换,但是现在,现代 Unix 和 Linux 系统更提倡使用 $(command) 结构,比如:

1
2
$ echo The current date and time is $(date)
The current date and time is Monday 23 August 2021 05:55:47 PM IST

所以,反引号已经不再使用了。

环境变量

环境变量取决于某些特定的环境,是特定于当前系统环境的变量,比如,当前登录的用户存储在 USER 变量中。

什么是环境变量

环境变量与你的桌面环境无关。

HOSTNAME 是我们遇到的最基本的环境变量之一,一般情况下它的拼写字母是大写的,为什么会这样呢?

因为大多数环境变量都是由系统预定义的,并且是全局变量,所以它们通常都是用大写字母书写的。

那么,为什么首先要使用环境变量呢?

假设你是一名程序员,并且你的代码需要访问你的数据库密钥,该密钥不应公开共享。

那么,如果将代码共享到 git 上的时候,应该怎样避免密码泄露呢?一般的做法是,将数据库密钥封装为环境变量。

通过这种方式,将 git 上的指令设置为“如果要使代码正常运行,需要用数据库密钥来替换此变量”。

当然,这是使用环境变量的一种方式,下面是Linux系统中常见的一些环境变量:

注: 下面的变量不一定存在, 具体是什么, 使用 env 查看

**环境变量 ** **描述 **
HOME 显示当前用户主目录
HOSTHOSTNAME 系统的主机名, 通常存储在/etc/hostname文件中
USERLOGNAME 当前登录用户的用户名
LANG 指定系统默认的语言设置
PWD 显示当前工作目录的路径
UID 存储用户的唯一ID
SHELL 显示当前正在使用的 shell 路径
BASH_VERSION 当前使用的 bash 实例的版本
HISTFILE 保存命令历史记录的文件路径
TERM 显示正在使用的终端类型
PATH 显示已列分割的文件和目录的路径
TMPTEMP 指定临时文件的存储路径
LD_LIBRARY_PATH 指定动态链接库的搜索路径
PS1 定义命令行提示符的格式

打印环境变量

在 Linux 中有多种方法来打印环境变量,我们从最简单的方式开始。

使用 printenv 命令

printenv 程序用于打印当前 shell 的环境变量。

假如我们要使用 printenv 打印 USERNAME 变量的值,那么命令如下:

1
printenv USERNAME

同样,可以使用 printenv 打印多个环境变量,使用空格分割,比如,使用 printenv 打印 HOMEUSERNAME 的值:

1
printenv HOME USERNAME

上述命令,首先会打印当前登录用户的主目录,然后第二行显示主机名,如下图所示:

那么,如何打印当前 shell 中所有可用环境变量呢?只需要运行 printenv 命令即可:

1
printenv

使用 echo 命令

经常使用 Linux 的用户会比较习惯使用 echo 命令。比如,如下命令将会打印 USERNAME 的值:

1
echo $USERNAME

那么,使用 echo 命令打印多个环境变量该怎么做呢?使用如下语法:

1
echo -e "$<variable 1> \n<variable 2> \n<variable 3>"

比如,使用如下命令同时打印 HOME, USERNAME, HOSTNAME 的值:

1
echo -e "$USERNAME \n$HOME \n$HOSTNAME"

使用 env 命令

shell 脚本通常使用 env 命令启动正确的解释器,但我们也可以使用 env 指令列出可用的环境变量。

使用不带任何参数的 env 命令,会打印所有可用的环境变量:

1
env

那么,如果想要获取某个特定环境变量的值,需要怎样做呢?可以使用 grep 来对结果进行过滤。下面我们演示如何打印 HOME 的值:

1
env | grep HOME

使用 declare 命令

declare 命令用于声明和打印shell中变量的值。与上面介绍的其他命令一样,不带任何参数的使用 declare 命令会打印出所有可用的环境变量:

1
declare

前面我们使用过grep过滤结果,其实,可以使用它来过滤多个结果,语法如下:

1
declare | grep '<variable 1>\|<variable 2>\|<variable 3>'

下面命令打印 HOSTNAMEUSERNAME 的值:

1
declare | grep 'HOSTNAME\|USERNAME'

使用 set 命令

通常,set 命令用于在 shell 中设置或者取消设置配置项,以设置进程信息。除此以外,set 命令也可以用来打印当前 shell 的环境变量。如下命令:

1
set

同样可以使用 grep 来过滤 set 命令的结果。下面的例子展示如何使用 grep 命令打印多个环境变量:

1
set | grep 'HISTFILESIZE\|HISTFILE\|GNOME_SHELL_SESSION_MODE'

以上我们介绍了打印环境变量的多个方法,但是建议使用第一种方法,因为它的语法最简单。

使用 envsubst 命令替换环境变量

在写脚本或者运行脚本的时候,很多时候都会用到环境变量,不过有时候使用的变量可能会带来安全风险。所以这时候我们可以使用 envsubst 来代替环境变量。

envsubst 命令用于获取环境变量的替代项(可能这就是这个命令名称的由来:environment substitute)。

但是它不会直接改变环境变量,首先会根据模式查找变量(比如 $VARIABLE 或者 [$VARIABLE]);然后,它会用一个 bash 变量替换找到的环境变量。

注意:envsubst 命令只识别导出的变量(exported variables)。

使用 envsubst 命令替换环境变量,可参考如下语法模式:

1
envsubst [OPTION] [SHELL-FORMAT]

接下来我们来看看具体怎么做。

首先我们准备一个文本文件,比如将其命名为 confidential.txt,内容如下:

1
2
3
4
5
6
7
8
A sample file containg password and username!

And should not be shared by any means.

My loging credentials are:

username=$USERNAME
password=$PASSWORD

然后,为了替换 $USERNAME$PASSWORD,我们首先创建并导出两个变量:

1
2
export USERNAME=abhiman
export PASSWORD=strongphrase

导出变量值后,可以为创建的文件调用 envsubst 命令:

1
envsubst < confidential.txt

如上图所示,变量的值已经被改变了。

另外,还可以使用 unset 命令取消变量值的设置。首先,unset 使用 export 命令设置的变量:

1
unset USERNAME PASSWORD

现在,再次运行 envsubst 命令,可看到变量的值已经没有了:

因为 envsubst 命令只适用于导出的变量,当我们使用 unset 命令的时候,将变量的值设置为了 null。然后当 envsubst 命令找不到要替换的值的时候,就会出现空内容。

将输出重定向到特定文件

我们可以选择将输出重定向到某个文件,以保存输出内容,可使用重定向符号 > 来实现。比如,我们将上述输出重定向到 Output.txt:

1
envsubst < confidential.txt > Output.txt

以 shell 的方式替换 envsubst 命令中的特定变量

假如你导出了多个环境变量,但是只想替换其中的一小部分。这种情况下,可以使用 shell 的方式来实现。

其语法非常灵活,只需要在单引号 '' 中指定变量即可,可通过多种方式使用它,比如:

1
envsubst '$variable' > file

或者添加多个变量:

1
envsubst '$variable1 $variable1 $variable3' > file

也可以添加文本:

1
envsubst 'subsitute the $variable1 and $variable2' > file

下面这个例子,我们使用一个名为 Substitute.txt 的文本文件,其内容如下:

1
2
3
4
5
6
7
8
Hello, My name is $USER.

And these are login credentials for $SERVICE:

username=$USERNAME
password=$PASSWORD

Not meant for public use!

接下来,我将导出上面文件中使用的每个变量的值:

1
export USER=sagar export SERVICE=AWS export USERNAME=LHB export PASSWORD=randomphrase  

在不替换任何特定变量的情况下,它应该会得到以下输出:

如果我只希望 $USER$SERVICE 的值反映在输出中,可使用如下命令:

1
envsubst '$USER $SERVICE' < Substitute.txt

如上图所示,输出只显示了 $USER$SERVICE 的值,但是 $USERNAME$PASSWORD 仍旧保持原值。

PATH变量

无论您是使用Linux、BSD还是macOS,编辑 $PATH 对于任何 POSIX 初学者来说都是一项重要的技能。

当你在Linux系统的终端中输入一个命令的时候,所有你的操作都是运行一个程序。比如 ls, mkdir, rm 等,通常是存储在/usr/bin目录中的小程序。除此之外,其他地方也存储有可执行程序,比如:/usr/local/bin, /usr/local/sbin/usr/sbin。可执行程序为什么会在这些目录中不在本文的讨论范围内,但是你需要知道,可执行程序的存储位置不会局限于其中一个目录

当您在 Linux shell 中键入命令时,它不会在每个目录中查找是否有同名程序。它只看你指定的那些。它如何知道在上面提到的目录中查找?很简单:它们是名为 $PATH 的环境变量的一部分,您的shell检查该变量以知道在哪里查找。

查看 PATH

有时候,你可能希望将程序安装到计算机的其他位置,但是运行程序的时候不希望指定程序的确切位置。这时候可以通过在$PATH中添加一个环境变量来实现。要查看当前 $PATH 中的内容,需要在终端中输入:

1
echo $PATH

这样你可以在上面看到一些目录,这些目录就是保存可执行程序的位置,他们之间使用冒号分隔。那现在我们添加一些其他的目录。

设置临时 PATH 变量

假设你编写了一个名为hello.sh的shell脚本,并将其放在名为 /place/with/the/file 的目录中。这个脚本为当前目录中的所有文件提供了一些有用的函数,无论你当前在哪个目录中,都希望能够执行这些函数。

只需执行以下命令将 /place/with/the/file 添加到 $PATH 变量:

1
export PATH=$PATH:/place/with/the/file

现在,只要输入脚本的名称,就可以在系统的任何地方执行脚本,而不必在命令中包含完整路径。

设置永久 PATH 变量

以上命令是设置了一个临时的环境变量,当机器重启后,就不起作用了。变量 $PATH 由shell在每次启动时设置,但是你可以对其重新设置,以便每次打开新shell时都包含你设置的路径。具体的方法取决于你运行的是哪个shell。

如果你使用的是普通的Linux发行版,并且没有修改过默认值的话,一般运行的是bash。要查看运行的是哪个shell,可以通过以下命令:

1
$ echo $0

这样一个echo命令后面跟着一个 $0,如果是bash,你就会看到:

1
2
$ echo $0
-bash

对于bash,只需要将上面的 export PATH=$PATH:/place/with/the/file 添加到shell启动时要执行的文件,比如 ~/.bash_profile~/.bashrc~/.profile,如果你不确定放在那里,那就放在 ~/.bashrc 里面吧。

对于其他shell,您需要先找到适当的位置来配置;ksh配置通常出现在~/.kshrc中,zsh使用 ~/.zshrc 。可以先检查shell的说明文档来确定配置文件的位置。

apropos

使用 apropos 搜索可用的 Linux 命令.

如果你使用过某个命令,(当再次用的时候)却想不起具体名称了。

(这种情况下)可以在终端中使用 ctrl + r 快捷键,反向搜索 shell 历史记录。

如果你在同一个系统上使用过该命令,那么上述方法是可以的。但是,如果你在其他的Linux系统上使用的,或者只是在某个论坛或网站上偶然看到过,该怎么办呢?

好消息是,有一个专用的 Linux 命令,允许你在系统可用的命令中,使用字符串进行搜索。

使用 apropos 搜索 Linux 命令

apropos 命令可以让你使用关键词,在man手册的名称和简介中进行搜索。

大多数情况下,这都会帮助你查找到你想要的命令。

使用apropos命令很简单:

1
apropos [options] keyword

举一个简单的例子,假如你正在查找一个与 CPU 有关的命令,可以使用 apropos 这样查找:

1
apropos cpu

它会在 man 手册的名称和简介中查找所有包含 CPU 的命令:

1
2
3
4
5
6
7
8
root@learnubuntu:~# apropos cpu
chcpu (8) - configure CPUs
cpuid (4) - x86 CPUID access device
cpuset (7) - confine processes to processor and memory node subsets
lscpu (1) - display information about the CPU architecture
msr (4) - x86 CPU MSR access device
sched (7) - overview of CPU scheduling
taskset (1) - set or retrieve a process's CPU affinity

默认情况下,搜索不区分大小写,关键字可以是正则表达式。这就是为什么会看到很多返回结果,如CPUCPUID等。

如果需要精确匹配,可以使用选项 -e

1
2
3
4
5
root@learnubuntu:~# apropos -e cpu
lscpu (1) - display information about the CPU architecture
msr (4) - x86 CPU MSR access device
sched (7) - overview of CPU scheduling
taskset (1) - set or retrieve a process's CPU affinity

多个关键字

如果提供了多个关键字,apropos 将会返回所有与给定关键字匹配的结果。比如下面例子,会有307个条目与 network 或 pro 匹配:

1
2
root@learnubuntu:~# apropos network pro | wc -l
307

如果你搜索的单个命令包含多个词(单词之间有空格),那么可以使用引号来告诉apropos你搜索的是一整个命令而不是多个命令:

1
2
root@learnubuntu:~# apropos "network pro"
mtr-packet (8) - send and receive network probes

上面的示例要求你将所有关键字放在一起。可以使用 -a 选项,让条目以任何顺序匹配所有关键字。

1
2
3
root@learnubuntu:~# apropos -a network pro
ip-netns (8) - process network namespace management
mtr-packet (8) - send and receive network probes

仅搜索用户或系统命令

你可能发现了 apropos 命令会返回大量的结果,但并非所有的结果都是命令。

这是因为它搜索的是整个man手册。

如果你熟悉 man 手册,你会知道第 1 节包含有用户命令,第 8 节包含系统命令。如下表:

  • 用户命令(User Commands)
  • 系统调用(System Calls)
  • C库函数(C Library Functions)
  • 设备和特殊文件(Devices and Special Files)
  • 文件格式和约定(File Formats and Conventions)
  • 游戏等(Games etc)
  • 杂项(Miscellanea)
  • 系统管理工具和守护程序(System Administration tools and Daemons)

所以,当你搜索CPU时,它显示了所有部分的结果。注意每个“命令”后面的数字:

1
2
3
4
5
6
7
8
root@learnubuntu:~# apropos cpu
chcpu (8) - configure CPUs
cpuid (4) - x86 CPUID access device
cpuset (7) - confine processes to processor and memory node subsets
lscpu (1) - display information about the CPU architecture
msr (4) - x86 CPU MSR access device
sched (7) - overview of CPU scheduling
taskset (1) - set or retrieve a process's CPU affinity

你可以优化返回结果,(使用 -s 选项)只要指定的部分(sections):

1
2
3
4
root@learnubuntu:~# apropos -s 1,8 cpu
chcpu (8) - configure CPUs
lscpu (1) - display information about the CPU architecture
taskset (1) - set or retrieve a process's CPU affinity

有很多方式可以在 Linux 命令行中获得帮助,apropos 命令就是其中之一,不过似乎很少人知道这个。

tee

tee 命令从标准输入读取数据,同时写入到标准输出和文件中。

也就是说,tee 命令的结果是,你既可以在屏幕上看到命令的输出,同时还可以将输出保存到文件中。

换言之,你有一个输入,它会被引导到两个输出。

要理解这个问题,首先你需要知道 Linux 中重定向的概念。关于重定向,可以参阅:

该命令以电力、管道系统和其他行业中使用的 T 形接头命名,它们被称为“T”,因为它们类似于字母“T”。

tee 命令示例

tee命令有一个简单的语法:

1
tee [OPTION] [FILE]

tee 命令从标准输入读取数据,所以它总是和与另外一个命令一块被使用。

我们来看几个例子。

显示命令输出并将其保存到文件

比如我们想要计算某个文件中有多少行文字,我们要在屏幕上看到它到底有多少行,同时还想要把这个行数保存到另一个文件中。

1
2
[admin@fedora~]$ wc -l testfile.txt | tee count.txt
20 testfile.txt

文件 count.txt 原本并不存在,因此它将创建一个名为 count.txt 的新文件。如果你查看文件 count.txt 的内容,会发现其与显示在屏幕上的内容是相同的:

1
2
[admin@fedora~]$ cat count.txt 
20 testfile.txt

注意:默认情况下,tee 命令会覆盖原文件的内容。如果需要,可以使用 -a 选项来让其在文件中追加(而不是删除):

1
wc -l testfile.txt | tee -a count.txt

显示命令输出并将其保存到多个文件

如果要将命令输出保存到多个文件,也可以使用 tee 命令,只需指定文件即可:

1
2
[admin@fedora~]$ wc -l testfile.txt | tee count1.txt count2.txt
20 testfile.txt

可以验证上述两个文件中是否存储了相同的内容:

1
2
3
[admin@fedora~]$ cat count1.txt count2.txt 
20 testfile.txt
20 testfile.txt

在 Linux 中,是可以通过 cat 命令来查看多个文件的。

将命令输出解析到另一个命令,同时将其保存到文件中

我们并不总是需要查看命令输出,由于它是标准输出,因此可以通过管道将其传输到另一个命令。

看下面的例子:

1
2
[admin@fedora~]$ ls -l | tee count.txt | wc -l
7

上述命令中,ls -l 命令的输出通过管道传输到 tee 命令,tee 命令将 ls -l 的输出保存在 count.txt 文件中。然后,输出信息没有显示在屏幕上,而是又通过管道传输到了 wc -l 命令,该命令用于计算行数。

在本例中,我们通过屏幕看到的是 wc -l 命令的输出。ls -l 命令的输出被存储在了文件 count.txt 中。

1
2
3
4
5
6
7
8
[admin@fedora~]$ cat count.txt 
total 20
-r--r--r-- 1 admin admin 456 Dec 11 21:29 testfile.txt
-rw-r--r-- 1 admin admin 0 Jan 10 16:03 count.txt
-rw-r--r-- 1 admin admin 356 Dec 17 11:39 file1.txt
-rw-r--r-- 1 admin admin 356 Dec 17 09:59 file2.txt
-rw-r--r-- 1 admin admin 356 Dec 11 21:35 sherlock.txt
drwxr-xr-x 3 admin admin 4096 Jan 4 20:10 target

我们可以在平时工作中,依实际情况尽可能的发挥想象力,来使用 tee 命令。比如在分析一个很长的输出(比如日志文件)时,使用它会很方便。实时查看并将其存储在文件中以供将来参考也是非常有用的一个操作。

花括号扩展

花括号扩展(Brace expansion) {..} 是 Linux 中使用率很高的一个 shell 功能。你可以使用它打印数字或字母序列,将两个整数或字母使用花括号中的两个点分隔开,然后会看到神奇的结果。

我们举个最简单的例子,看如下代码:

1
echo {1..10}

上述一行 echo 代码的输出将会是什么呢?如下:

1
1 2 3 4 5 6 7 8 9 10

输出结果,会打印在同一行中,中间使用空格隔开。

为了让大家可以更好的理解它的功能,我们接下来列举几个其他例子。

一些例子

假如我们想要得到一个从 7 到 1 的数字序列:

1
2
$ echo {7..1}
7 6 5 4 3 2 1

可以添加前缀 0:

1
2
$ echo {01..10}
01 02 03 04 05 06 07 08 09 10

可以使用 {x..y..z} 形式的花括号来扩展生成从 x 到 y 的值,递增(步长)为 z。比如我们想要打印 15 以内的偶数序列,如下:

1
2
$ echo {0..15..2}
0 2 4 6 8 10 12 14

或者奇数序列:

1
2
$ echo {1..15..2}
1 3 5 7 9 11 13 15

步长可以指定为任何数值:

1
2
$ echo {100..1000..99}
100 199 298 397 496 595 694 793 892 991

也可以采用负数:

1
2
$ echo {3..-4}
3 2 1 0 -1 -2 -3 -4

注意:花括号中的点之间不能有空格,否则,扩展将不起作用。

使用字母序列

我们上面的例子,介绍的是使用数字序列。但是也可以使用它生成字母序列。看下面的例子:

1
2
$ echo {A..H}
A B C D E F G H

也可以将顺序反过来:

1
2
$ echo {H..A}
H G F E D C B A

或者指定步长:

1
2
$ echo {H..A..2}
H F D B

或者使用小写字母:

1
2
$ echo {a..f}
a b c d e f

实际应用

上面我们介绍了花括号扩展的具体用法,下面来介绍一些关于它的实际应用。

创建一系列文件

我们可以利用花括号扩展来创建一系列具有相同文件名规则的文件,看下面的例子:

1
2
3
4
$ touch file_{1..10}.txt
$ ls
file_10.txt file_2.txt file_4.txt file_6.txt file_8.txt
file_1.txt file_3.txt file_5.txt file_7.txt file_9.txt

创建备份文件

当我们在编辑配置文件时,一般建议先对其进行备份,按照大多数人的习惯,是在原始文件名中添加 .bak 扩展名,这表示它是对原文件的一个备份。

比如:

1
cp -p long_filename.txt long_filename.txt.bak

现在我们来使用花括号扩展来完成这个工作,如下代码:

1
cp -p long_filename.txt{,.bak}

是的,这里的 {,text} 不是上面的那种 {X..Y} 的模式,但是你需要知道有这样一个用法的存在。

1
2
3
$ cp -p long_filename.txt{,.bak}
$ ls
long_filename.txt long_filename.txt.bak

上述 cp 命令的 -p 选项表示需要保留原文件的属性,比如所有权、时间戳等。

使用多个花括号

可以使用多个花括号来创建具有相似名称和不同扩展名的文件,看下面的例子:

1
2
3
$ touch {a,b,c}.{hpp,cpp}
$ ls
a.cpp a.hpp b.cpp b.hpp c.cpp c.hpp

在路径中使用或括号扩展

假设有两个路径,其目录结构是类似的,只有很少的一部分不一样(比如中间某个文件夹名称不同),这个时候,花括号扩展会非常有用。看下面代码:

1
mv project/{new,old}/dir/file

其相当于:

1
mv project/new/dir/file project/old/dir/file

并非所有内容都可扩展

这是不言而喻的。如果你希望创建一个序列,给出的条件应该是可以创建成序列的东西。如果你使用的是一个很奇怪的组合,将不能被扩展。比如:

1
2
$ echo {1..Z}
{1..Z}

另外,也不能使用小数:

1
2
$ echo {1..5..0.5}
{1..5..0.5}

一些奇怪的组合,也会生成奇怪的结果:

1
2
$ echo {a..F}
a ` _ ^ ] [ Z Y X W V U T S R Q P O N M L K J I H G F

当你刚开始接触花括号扩展时,会觉得它很麻烦。但是,手动打字会更耗费时间。一旦你熟悉了它,那你的 Linux 技能会更上一个台阶。

分析二进制文件

“世界上有10种人:懂二进制的和不懂二进制的。”

我们每天都在处理二进制文件,但很多人对其了解的还不够多。这里说的二进制文件,是指可执行文件,包括通过命令行执行的,或者其他应用程序。

Linux 提供了很多工具,可以分析二进制文件。无论你的具体工作是什么,只要你在Linux系统上工作,了解这些工具会让你更好的理解Linux系统。

本文将会介绍一些关于解析二进制文件的比较流行的工具和命令,其中大多数是默认在系统中安装了的,还有一部分需要手动安装。

file

功能:帮助确定文件类型。

这个应该作为你分析二进制文件的起点。我们每天都会处理很多文件,并非所有的文件都是可执行的。文件有很多种,在开始分析之前,你需要首先确定文件的类型。它们是二进制文件,库文件,文本文件,视频文件,图片,PDF,还是数据库文件?

file 命令会帮助你确定要处理文件的文件类型。

1
2
3
4
5
6
$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=94943a89d17e9d373b2794dcb1f7e38c95b66c86, stripped


$ file /etc/passwd
/etc/passwd: ASCII text

ldd

功能:显示共享对象的依赖关系。

如果你在一个可执行的二进制文件上使用了 file 命令,那么应该会从输出内容中看到有“dynamically linked(动态链接)”的相关信息,这是什么意思呢?

在开发软件的时候,如果遇到一些常用功能且已经有现有工具的话,会使用现有的工具,就是“我们不会重新发明轮子”。大多数软件都会有一些常见的任务,比如打印输出,或者从标准输入读取、打开文件等等。这些常见的任务通常会被抽象为一组函数,所有人都能使用这些函数。这些函数通常会放在名为 libc 或 glibc 的库中。

如何知道可执行文件依赖于哪些库呢?使用 ldd 命令可以做到这一点,它可以显示二进制文件所有的依赖库及其路径。

1
2
3
4
5
6
7
8
9
10
11
$ ldd /bin/ls
linux-vdso.so.1 => (0x00007ffef5ba1000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fea9f854000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007fea9f64f000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007fea9f446000)
libc.so.6 => /lib64/libc.so.6 (0x00007fea9f079000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fea9ee17000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fea9ec13000)
/lib64/ld-linux-x86-64.so.2 (0x00007fea9fa7b000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007fea9ea0e000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fea9e7f2000)

ltrace

功能:库调用跟踪器。

我们现在知道了如何使用 ldd 命令查找可执行程序依赖的库。然而,一个库可能会包含数百个函数。在这数百个函数中,我们的二进制代码使用的实际函数是哪个呢?

ltrace 命令显示在运行时从库中调用的所有函数。在下面的示例中,我们可以看到正在调用的函数名,以及传递给该函数的参数,还可以在输出的最右侧看到这些函数返回的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

$ ltrace ls
__libc_start_main(0x4028c0, 1, 0x7ffd94023b88, 0x412950 <unfinished ...>
strrchr("ls", '/') = nil
setlocale(LC_ALL, "") = "en_US.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
textdomain("coreutils") = "coreutils"
__cxa_atexit(0x40a930, 0, 0, 0x736c6974756572) = 0
isatty(1) = 1
getenv("QUOTING_STYLE") = nil
getenv("COLUMNS") = nil
ioctl(1, 21523, 0x7ffd94023a50) = 0
<< snip >>
fflush(0x7ff7baae61c0) = 0
fclose(0x7ff7baae61c0) = 0
+++ exited (status 0) +++

Hexdump

功能:以ASCII、十进制、十六进制或八进制显示文件内容。

通常,当你打开一个未知文件的时候,系统可能不知道如何处理该文件。如果尝试使用vim打开可执行文件或视频文件,我们通常会看到一堆乱码。

使用 hexdump 命令打开未知文件可以帮助我们查看文件到底包含什么。此外还可以选择使用一些命令行选项查看文件中数据的ASCII表示,这会帮助我们了解它是什么类型的文件。

1
2
3
4
5
6
7
8
9
10
11
$ hexdump -C /bin/ls | head
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 d4 42 40 00 00 00 00 00 |..>......B@.....|
00000020 40 00 00 00 00 00 00 00 f0 c3 01 00 00 00 00 00 |@...............|
00000030 00 00 00 00 40 00 38 00 09 00 40 00 1f 00 1e 00 |....@.8...@.....|
00000040 06 00 00 00 05 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 f8 01 00 00 00 00 00 00 f8 01 00 00 00 00 00 00 |................|
00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................|
00000080 38 02 00 00 00 00 00 00 38 02 40 00 00 00 00 00 |8.......8.@.....|
00000090 38 02 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |8.@.............|

strings

功能:输出文件中可打印的字符串。

如果你想在二进制文件中查找一些可打印的字符,那么可以使用 strings 命令。

在开发软件时,会向其中添加一些文本信息,比如打印消息,调试信息,帮助信息,错误信息等等。如果这些信息都以二进制形式存在,那么使用 strings 命令可以将其输出到屏幕中。

1
$ strings /bin/ls

readelf

功能:显示ELF文件的信息。

ELF(可执行和可链接文件格式)是可执行文件或二进制文件的主要文件格式,不仅在Linux上,在各种UNIX系统上也是如此。如果您使用了诸如file命令之类的工具,该命令告诉你文件是ELF格式的,那么下一个逻辑步骤将是使用readelf 命令及其各种选项来进一步分析文件。

使用 readelf 命令的时候,可参考 ELF 规范,点击这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4042d4
Start of program headers: 64 (bytes into file)
Start of section headers: 115696 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30

objdump

功能:显示对象文件中的信息。

二进制文件是你在编写完源代码后,由编译器将源代码编译后形成的。该编译器会将源代码编译为机器代码,然后由CPU执行给定任务。这种机器代码可以通过汇编语言来解释。汇编语言是一组指令,可以帮助我们理解程序是如何执行的。

objdump 命令可以读取二进制或可执行文件,并将汇编语言指令输出到屏幕上。汇编知识对于理解 objdump 命令的输出很重要。

1
2
3
4
5
6
7
8
9
10
11
$ objdump -d /bin/ls | head

/bin/ls: file format elf64-x86-64

Disassembly of section .init:

0000000000402150 <_init@@Base>:
402150: 48 83 ec 08 sub $0x8,%rsp
402154: 48 8b 05 6d 8e 21 00 mov 0x218e6d(%rip),%rax # 61afc8 <__gmon_start__>
40215b: 48 85 c0 test %rax,%rax
$

strace

功能:跟踪系统调用和信号。

strace 跟前面介绍的 ltrace 类似,区别是,strace 跟踪系统调用,而不是库调用。系统调用是你与内核交互以完成工作的方式。

举个例子,如果你想在屏幕上打印一些东西,你将使用标准库 libc 中的 printfput 函数;然而,在后台,系统最终会调用一个名为 write 的函数将这些内容打印到屏幕上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

$ strace -f /bin/ls
execve("/bin/ls", ["/bin/ls"], [/* 17 vars */]) = 0
brk(NULL) = 0x686000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f967956a000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=40661, ...}) = 0
mmap(NULL, 40661, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9679560000
close(3) = 0
<< snip >>
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9679569000
write(1, "R2 RH\n", 7R2 RH
) = 7
close(1) = 0
munmap(0x7f9679569000, 4096) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++

nm

功能:列出对象文件中的符号。

如果你使用的是未剥离的二进制文件,nm命令将为你提供编译期间嵌入二进制文件中的有价值信息。nm 可以帮助我们从二进制文件中识别变量和函数,如果我们无法访问待分析的二进制文件的源代码,使用 nm 命令将会非常有用。

为了展示 nm 的功能,我们将编写一个小程序并使用 -g 选项编译它,我们还将看到,使用 file 命令不会剥离二进制文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

$ cat hello.c
#include <stdio.h>
int main() {
printf("Hello world!");
return 0;
}

$ gcc -g hello.c -o hello

$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3de46c8efb98bce4ad525d3328121568ba3d8a5d, not stripped

$ ./hello
Hello world!$

$ nm hello | tail
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
00000000004005b0 T __libc_csu_fini
0000000000400540 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
000000000040051d T main
U printf@@GLIBC_2.2.5
0000000000400490 t register_tm_clones
0000000000400430 T _start
0000000000601030 D __TMC_END__

gdb

功能:GNU调试器。

并不是所有的二进制文件都可以静态分析。我们确实执行了一些解析二进制文件的命令,比如 ltracestrace,但是,软件的体系结构是多种多样的,我们不可能通过一种方法解析所有文件。

对于不好解析的文件,唯一的方法就是在运行时,能够在某些位置暂停程序,并能够分析信息,然后进行下一步执行。

这就是调试器该起作用的地方。在Linux中,gdb 实际上是调试器。它可以帮助我们加载程序,在特定位置设置断点,分析内存和CPU寄存器,或者做更多的事情。它补充了上述其他工具,并允许我们进行更多的运行时分析。

需要注意的一点是,一旦你使用 gdb 加载程序,你将会看到 gdb 输出的信息提示,所有后续命令都会在 gdb 命令提示符下运行,直到你退出。

我们将使用之前编译的“hello”程序,并使用 gdb 查看其工作原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

$ gdb -q ./hello
Reading symbols from /home/flash/hello...done.
(gdb) break main
Breakpoint 1 at 0x400521: file hello.c, line 4.
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400521 in main at hello.c:4
(gdb) run
Starting program: /home/flash/./hello

Breakpoint 1, main () at hello.c:4
4 printf("Hello world!");
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64
(gdb) bt
#0 main () at hello.c:4
(gdb) c
Continuing.
Hello world![Inferior 1 (process 29620) exited normally]
(gdb) q

mapfile

将文件的内容赋值给变量

使用 mapfile,可以读取文件的内容,将其输出分配被 bash 变量(数组)。文件中的每一行,都会是数组中的一个数组项(元素)。

比如,我们有一个文件 file.txt,其中的内容如下:

1
2
3
4
5
line 1
line 2
line 3
line 4
line 5

然后,我们运行以下命令,将该文件中的内容转为数组中的元素。数组为变量 file_var:

1
2
3
4
5
6
#!/usr/bin/env bash
mapfile file_var < file.txt

for i in "${file_var[@]}"; do
echo "${i}"
done

在上图的结果中,我们会看到输出中多了很多空白行。可以在 mapfile 中使用 -t 选项来解决这个问题:

1
2
3
4
5
6
#!/usr/bin/env bash
mapfile -t file_var < file.txt

for i in "${file_var[@]}"; do
echo "${i}"
done

需要注意的是,不能通过管道重定向,将文件的内容给到 mapfile 命令,就像下面这样:

1
2
3
#!/usr/bin/env bash

cat file.txt | mapfile -t file_var

上述代码是不正确的,这是因为管道右侧的 mapfile 命令是在子 shell 中运行的,也就是一个新的 bash 实例,它在当前的 shell 下是不起作用的。

source

source 命令可以在当前 shell 中执行文件中的命令,也可以用于刷新环境变量。不过老实说,它的主要用途就是用于刷新环境变量。其语法格式如下所示:

1
source filename [options]

也可以用一个点 . 来代替 source 命令:

1
. filename [options]

source 命令是如何工作的

这个命令的语法很简单,但是要理解它,需要稍微了解一些 Linux 概念。如果你是 Linux 新手,可能对于变量是什么只有一个比较模糊的概念。不过没关系,我们都是从这个阶段过来的。

为了方便理解 source 命令,下面我们是对变量做一个简短的解释。

关于变量

你可以打开一个 bash 终端来创建一个新的变量,变量可以被认为是一个占位符,可以用来指向一条信息(字母,数字或者符号等)。

比如,我们创建一个名为 name 的变量,将其赋值为 Christopher。

在 bash 中,命名变量的格式为:

1
variable_name=your_variable

注意,不要在等号 = 和文本之间添加空格。

1
2
3
$ name=Christopher
$ echo $name
Christopher

那么,如果我只键入变量名会怎样呢?看下面的例子:

1
2
$ echo name
name

如果忘记带美元符号 $,比如上面的例子,bash 就会直接返回你输入的文本。

变量的调用可以在任何调用的位置,所以我们也可以将其放在如下代码中:

1
2
$ echo "Hello, $name. $name is a great name. It's good to meet you."
Hello, Christopher. Christopher is a great name. It's good to meet you.

变量可以帮助我们做很多事情,上述对变量的简单介绍应该可以让大家了解它是如何工作的了。

环境变量与 shell 变量

理解 source 命令的另外一个关键问题,就是关于变量的持久性,这是用来思考 shell 变量和环境变量之间区别的一种简单方法。

简单地说,如果在终端 shell 中创建一个变量,那么一旦退出该 shell,它就会丢失。

相比之下,环境变量会持久性的保存在操作系统中,而且通常都是使用大写字母。

其中一个例子,就是用户名,操作系统中其变量名称为 $USER。如下所示:

1
2
$ echo $USER
christopher

上面我们是介绍了一些关于环境变量和 shell 变量的区别,那么这跟 source 命令有啥关系呢?

source 和 bash

如果你使用过 Linux,可能会遇到过这两个命令,并且认为他们的功能是一样的。毕竟 source 和 bash 都可以用来执行脚本。

source 命令会在当前 shell 中执行,而 bash 会创建一个新的 shell 会话。这并不明显,因为没有显示新窗口。

下面我们创建一个名为 echo.sh 的脚本,内容如下:

1
2
3
4
#! bin/bash

echo $USER
echo $name

然后在终端中给 name 赋值:

1
$ name=chris

接下来,我们分别使用 bash 和 source 命令来执行一下这个脚本,看如下结果:

1
2
3
4
5
6
7
8
9
10
$ bash echo.sh 
christopher


$ source echo.sh
christopher
chris
$ . echo.sh
christopher
chris

正如上述结果,当使用 bash 命令执行脚本时,shell 变量 $name 并没有被识别出来。

使用 source 命令刷新环境变量

source 还可以用于更新当前 shell 中的环境变量,一个常见的例子是在当前 shell 中更新 bash 配置文件。

用户可能想要修改他们的 bash 配置文件,比如创建一个别名。通常,保存配置后,需要打开一个新的终端窗口才能看到更改生效。

1
$ source .bashrc 

运行此操作将刷新当前 shell 中的设置,而不会强制您打开新的终端。

#!

我们在 shell 脚本中,经常看到以下内容作为开头:

1
#!/bin/bash

这里 #! 被称为 shebang 或者 hasbang。shebang 在 shell 脚本中起着重要的作用,特别是在处理不同类型的 shell 时。

本文将介绍:

  1. 什么是 shebang;
  2. 它如何在shell脚本中扮演重要角色。

shell脚本中的shebang是什么

shebang 是指的符号 #! ,这种字符的组合在脚本第一行中使用时具有特殊意义,它用于指定默认情况下运行给定脚本的解释器。

所以,如果脚本的第一行是:

1
#!/bin/bash

这意味着解释器应该是 bash shell。如果第一行是:

1
#!/bin/zsh

这表示要使用的解释器是 Z shell

在这里 #! 的用法当然是特殊的。因为 # 在 shell 脚本中是用于注释,在这里它有特殊的含义。

为什么 shebang 在 shell 脚本中很重要?

是这样的,shebang 后跟的 shell 可执行文件对于脚本来说不是强制性的。

如果我们写了这样一个简单的脚本:

1
echo "Hello,TIAP!"

赋予执行权限,使用 . 运行它,它将由当前登录的 shell 运行:

1
2
3
4
5
$ cat sample 
echo "Hello,TIAP!"
$ chmod u+x sample
$ ./sample
Hello,TIAP!

那么,为什么还要在 shell 脚本中添加 #!/bin/bash 这一行作为开头呢?

因为在 Linux 或者 Unix 系统中,有多个 shell 可用。虽然这些 shell 大多具有相同的语法,但它们还是有一些差别,在某些语法中有不同的处理方式

所以,我们需要在脚本中指定正确的 shell 解释器。否则的话,某些脚本在不同的 shell 中运行,可能就会产生不同的结果。

下面我们来举个例子。

使用 shebang 指定 shell 解释器的重要性

我写了一个示例脚本,内容是将几个 Linux 发行版本的名称放到一个数组中,然后打印出数组中第2个元素。

1
2
distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"

我没有添加 shebang 行来指定任何 shell 解释器。这意味着当我执行这个脚本时,它将由默认shell(在我们的例子中是bash)运行。

以下为输出:

1
2
3
4
5
6
7
$ cat arrays.sh
distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"
$ echo $0
bash
$ ./arrays.sh
Distro at index 2 is: SUSE

在上面例子中,数组的第2个元素输出显示为 SUSE,因为在 bash 和许多其他编程和脚本语言中,数组索引是从 0 开始的。但是在 Z shell 中不是,Z shell 中数组的索引是从1开始

我在系统中安装了 Z shell,然后更改脚本,在第一行添加 shebang,并指定脚本由 Z shell 运行:

1
2
3
4
#!/bin/zsh

distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"

以下为输出:

1
2
3
4
5
6
7
$ cat arrays.sh
#!/bin/zsh

distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"
$ ./arrays.sh
Distro at index 2 is: Fedora

看到区别了吧,相同的脚本却有不同的输出,这就是为什么要添加 shebang 来指定解释器的原因。作为系统管理员,在编写脚本的时候知道使用的是哪个 shell,但是不能确定脚本的运行环境是不是与编写环境使用相同的默认shell,所以,需要指定一个 shell。

如果运行时指定了 shell,那么 shebang 将会被忽略

前文中为什么要强调“默认”shell 呢?因为 shebang 指定了运行脚本的解释器。即如果没有使用 shebang 指定解释器, 那么就会使用 “默认shell” 执行脚本

但是,在运行的时候可以显式指定 shell,这种情况下,shebang 将会被忽略:

1
2
3
4
$ ./arrays.sh
Distro at index 2 is: Fedora
$ bash arrays.sh
Distro at index 2 is: SUSE

shebang 是怎样工作的?

当你在脚本的第一行使用 shebang 时,就是在告诉 shell 使用指定的命令运行脚本。

基本上,#/bin/zsh 相当于:

1
/bin/zsh script_name

我们前文中说过,如果脚本中第一行写了 shebang,这就意味着已经指定了 shell 解释器。

这其实是部分正确。事实上,这就是 shebang 存在的目的。但是 shebang 这一行不一定非要有可执行的 shell,它可以是任何东西。

比如,我们使用 #!/bin/cat 来代替 #!/bin/zsh/bin/catcat 命令的可执行文件。

1
2
3
#!/bin/cat
distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"

那现在这个脚本将使用 cat 命令运行,并显示脚本的内容:

1
2
3
4
5
6
7
8
9
$ cat arrays.sh
#!/bin/cat

distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"
$ ./arrays.sh
#!/bin/cat
distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"

只要它指定的是可执行命令,那么它就会正常运行。如果你放的是其他一些随机的东西,那么就会报错。

比如,我在 shebang 一行这样写:

1
#!/home/tiap

很显然,它指向的并不是一个可执行文件,因此会抛出一个错误的解释器错误:

1
2
3
4
5
6
$ cat arrays.sh 
#!/home/tiap
distros=("Ubuntu" "Fedora" "SUSE" "Debian")
echo "Distro at index 2 is: ${distros[2]}"
$ ./arrays.sh
bash: ./arrays.sh: /home/tiap: bad interpreter: Permission denied

最后,我们再来明确几个注意事项:

1)在 #! 之间没有空格,不能这样写# !/bin/bash

2)大多数系统允许 #!/bin/bash 之间有空格,但是,作为一个好的习惯,还是不要#!/bin/bash 之间添加空格;

3)#! 必须放在第一行,否则,shebang 将会被认为是一个注释,最好在它之前也不要有空行。

环境变量

在《Shell变量类型》中介绍了HOME$$ 等常用的预定义变量,他们在系统启动后就可以被使用,那么它们在哪里存储呢?在[《Shell变量作用范围》]中介绍了通过在/etc/bashrc配置变量实现跨进程,跨终端,跨用户。这个文件是什么? 为什么这个配置文件中的变量可以实现跨进程、终端、用户?他们是本章将要介绍的重点:环境变量配置文件。

环境变量配置文件主要定义的是对系统操作环境生效的系统默认环境变量,每次登录都会被加载。比如:PATHHOME等。通常也用来设置一些常用的别名、函数和环境变量。如果要添加或修改环境变量的值,则必须再修改完配置文件后重新注销登录读取环境变量配置文件才能生效,或使用source命令可以使配置文件立即生效。格式: source 配置文件. 配置文件(注意点号) 。

在Linux中,常见的环境变量配置文件有:

  • /etc/profile
  • /etc/profile.d/\*.sh
  • /etc/bashrc
  • ~/.bash_profile
  • ~/.bashrc

其中**/etc/目录下的配置文件对所有用户生效,家目录(~)下的配置文件只对当前用户**生效,下面将详细说明。

配置文件种类

/etc/profile

配置系统环境,如: PATH、USER等变量,同时还包含一些在用户登录时执行的脚本,这些脚本由/etc/profile启动运行,当用户登录Linux系统时,bash将执行 /etc/profile 文件中的命令,这些命令帮助用户设置工作环境,然后再将控制权交给用户主目录下的 ~/.profile 文件。当系统给出主提示符($PS1)后,用户就可以开始自己的工作。

/etc/profile.d/*.sh

/etc/profile.d/ 目录主要用于存放一些应用程序所需的启动脚本。这些脚本文件通常由应用程序安装,并且在用户登录时由 /etc/profile 文件调用执行,如用户登录Linux系统或使用 su 命令切换到另一个用户时,设置用户环境第一个读取的文件就是 /etc/profile。在 /etc/profile 中,会使用一个for循环语句来调用 /etc/profile.d/*.sh 脚本文件, 使得这些脚本文件所设置的环境变量就和 /etc/profile 启动时一起被设置起来了。

因此,/etc/profile.d 目录中的脚本文件通常用于设置应用程序特定的环境变量和配置,以便在用户登录时正确地初始化和运行这些应用程序

~/.bash_profile

在Shell中 ~/.bash_profile 文件主要用于配置用户环境变量和启动程序。它只对单一用户有效,位于用户的家目录(~)下 ~/.bash_profile ,当用户登录(login)时,~/.bash_profile 文件会被执行,用于设置环境变量和启动程序。这个文件可以用于配置用户的 PATHHISTSIZEHISTFILESIZE等环境变量的值。

此外,~/.bash_profile 文件还可以包含一些用户专用的bash shell信息。它会在用户登录时由 ~/.bashrc 文件执行。

~/.bashrc

~/.bashrc文件是bash shell的配置文件,用于定义用户登录后的Shell环境。它存储了用户特定的配置信息,例如自定义别名、环境变量、命令别名、Shell函数等。每个用户都可以在自己的家目录(~)下的 ~/.bashrc 文件中添加自己的配置,以满足其个性化的需求.

/etc/bashrc

/etc/bashrc 是一个全局的 Bash 配置文件,用于定义系统中所有用户的 Bash shell 的默认行为和环境变量。它在用户登录时被执行,为用户的交互式 Bash shell 提供一些全局的设置。

具体来说,/etc/bashrc 文件可以定义全局的环境变量,这些变量将在用户登录后的 Bash shell 中生效。通过在文件中添加 export 语句,可以设置全局的环境变量,例如 PATH、LANG、PS1 等。/etc/bashrc 文件还可以定义全局的别名和函数,供所有用户的 Bash shell 使用。通过在文件中添加 alias 或 function 语句,可以创建全局的别名和函数,以简化命令的输入和执行。

此外,/etc/bashrc 文件还可以配置全局的 Shell 行为,例如设置默认的提示符、设置历史命令记录和历史命令的数量限制、配置自动补全等。它还可以导入其他的配置文件,例如 /etc/profile.d/*.sh 或 /etc/bashrc.local 等。这些文件包含了更具体和个性化的设置,用于扩展和定制系统的全局 Bash 配置。

总的来说,/etc/bashrc 文件的作用是为系统中所有用户的 Bash shell 提供默认的环境变量、别名、函数和行为设置。它可以在用户登录时执行,确保所有用户共享相同的全局配置,并提供一致的 Shell 使用体验。

看到这里是不是似乎有点明白了为什么在《shell变量作用范围》文章中介绍到 /etc/bashrc 中的变量可以实现共享了。

配置文件加载顺序

前面介绍了配置文件的基本信息,下面介绍配置文件加载顺序,也就是系统启动或登录时加载配置文件的顺序,优先加载哪个配置文件?如果配置文件中有相同的变量会被覆盖?这里需要注意bash登录或新启终端会重新加载配置文件。为了更好理解,在配置文件中添加一些日志。然后通过新启终端来观察日志输出得到配置文件加载的先后顺序

添加日志

注意此时登录账户为root,只有root编辑/etc目录下配置文件的权限。先给每个环境变量配置文件添加日志方便观察;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# /etc/profile 文件添加日志
[root@ ~]# vim /etc/profile
# /etc/profile 文件开头
echo "/etc/profile start $USER"

# /etc/profile 文件结尾
echo "/etc/profile end $USER"

# /etc/profile.d/ 目录下添加 hello.sh文件 ,内容:echo '/etc/profile.d/hello.sh'
[root@ ~]# cd /etc/profile.d/ ; vim hello.sh
echo "/etc/profile.d/hello.sh $USER"

# ~/.bash_profile 文件中添加日志
[root@ ~]# vim ~/.bash_profile
# ~/.bash_profile 文件开头
echo "~/.bash_profile start $USER"
# ~/.bash_profile 文件结尾
echo "~/.bash_profile end $USER"

# ~/.bashrc 文件中添加日志
[root@ ~]# vim ~/.bashrc
# ~/.bashrc 文件开头
echo "~/.bashrc start $USER"
# ~/.bashrc 文件结尾
echo "~/.bashrc end $USER"

# /etc/bashrc 文件中添加日志
[root@ ~]# vim /etc/bashrc
# /etc/bashrc 文件开头
echo "/etc/bashrc start $USER"
# /etc/bashrc 文件结尾
echo "/etc/bashrc end $USER"

开启终端查看

新启终端,可以看到日志打印顺序

1
2
3
4
5
6
7
8
9
10
/etc/profile start root
/etc/profile.d/hello.sh root
/etc/profile end root
~/.bash_profile start root
~/.bashrc start root
/etc/bashrc start root
/etc/bashrc end root
~/.bashrc end root
~/.bash_profile end root
[root@ ~]#

日志加载基本顺序:

1
2
3
4
5
/etc/profile 
/etc/profile.d/*.sh
~/.bash_profile
~/.bashrc
/etc/bashrc

添加环境变量时,注意当前的配置文件以及变量在配置文件所在位置,避免加载不到或被其他配置文件中相同变量给覆盖额情况 ,需要注意几个细节:

  • /etc/profile加载过程中会去加载/etc/profile.d/*.sh文件
  • ~/.bash_profile 加载过程中会先加载 ~/.bashrc, 在加载完 /etc/bashrc之后,接着完成 ~/.bashrc的加载,最后结束 ~/.bash_profile的加载

配置文件特点

上述介绍5种环境变量配置文件,他们有什么区别什么场景使用?如何区分?接下来从配置文件加载的方式、是否登录状态、服务的对象(系统用户和普通用户)进行介绍;

配置文件加载的方式

当我们对环境变量配置文件进行修改后,配置不会立即生效,可通过3种方式重新加载配置文件使其生效。

新启终端

1
2
3
4
5
6
7
8
9
10
11
# 新增环境变量
[root@ ~]$ vim ~/.bashrc
export name=123456

# 当前终端访问
[root@ ~]# echo "$name, hello"
, hello

# 新启终端访问
[root@ ~]# echo "$name, hello"
123456, hello

source命令重新加载

1
2
3
4
5
6
7
8
9
10
11
# 新增环境变量
[root@ ~]$ vim ~/.bashrc
export NAME=123456
# 立即访问
[root@ ~]$ echo "$NAME, hello"
, hello
# 使用source命令加载配置文件
[root@ ~]$ source ~/.bashrc
# 再次访问
[root@ ~]$ echo "$NAME, hello"
123456, hello

登录

这里演示登录普通用户观察配置文件加载情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 切换到joy用户 保持环境
[root@ ~]$ su - joy
# 新增环境变量
[joy@ ~]$ vim ~/.bashrc
export app=123456
# 立即访问
[joy@ ~]$ echo "$app, hello"
, hello
# 重新登录joy用户,加载配置文件
[joy@ ~]$ su - joy
Password:
# 切换用户后可以访问变量
[joy@ ~]$ echo "$app, hello"
123456, hello

好像有点懵, 登录?那不登录配置文件会被加载?接下来请继续看

登录态

登录态表示 login shellno login shell, 他们将加载的环境变量配置文件主要在于是否加载/etc/profile、~/.bash_profile。

  • su 不登录 加载部分配置文件,
    ~/.bashrc、/etc/bashrc、/etc/profile.d/*.sh配置文件会被加载,profile不会被加载
  • su - 登录 加载所有配置文件,以下文件都会被加载
    • /etc/profile
    • ~/.bash_profile
    • ~/.bashrc
    • /etc/bashrc
    • /etc/profile.d/*.sh

以下演示登录态加载配置文件区别

3.2.1、no login

1
2
3
4
5
6
7
# root用户 no login 方式切换用户
[root@ ~]# su root
~/.bashrc start root
/etc/bashrc start root
/etc/profile.d/hello.sh root
/etc/bashrc end root
~/.bashrc end root

no login bash情况下会按照以下顺序加载 3种配置文件:

1
~/.bashrc  >  /etc/bashrc  >  /etc/profile.d/*.sh

注意:/etc/profile 和 bash_profile 配置文件未被加载

3.2.2、login

1
2
3
4
5
6
7
8
9
10
11
# root用户 log方式切换用户
[root@ ~]# su - root
/etc/profile start root
/etc/profile.d/hello.sh root
/etc/profile end root
~/.bash_profile start root
~/.bashrc start root
/etc/bashrc start root
/etc/bashrc end root
~/.bashrc end root
~/.bash_profile end root

此时所有的配置文件均被加载。

总结:login 和 no login 最主要区别在 no login 不会加载 /etc/profile 和 bash_profile 配置文件,为了方便记忆,这里就简称为profile文件。

服务的用户对象

针对环境变量配置服务的用户对象即普通用户和系统用户,将其分为**/etc目录的配置和家目录(~)**的配置文件

为了节约篇幅,我们将《配置文件加载顺序#添加日志》例子中

  • /etc/profile
  • /etc/bashrc
  • /etc/profile.d/*.sh

三个配置文件中的$USER全部改为123

~/.bash_profile和~/.bashrc中的$USER全部改为456

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# /etc/profile 文件添加日志
[root@ ~]# vim /etc/profile
# /etc/profile 文件开头
echo "/etc/profile start 123"

# /etc/profile 文件结尾
echo "/etc/profile end 123"

篇幅有限,省略/etc/bashrc、/etc/profile.d/*.sh配置文件 日志记录


# ~/.bash_profile 文件中添加日志
[root@ ~]# vim ~/.bash_profile
# ~/.bash_profile 文件开头
echo "~/.bash_profile start 456"
# ~/.bash_profile 文件结尾
echo "~/.bash_profile end 456"

# ~/.bashrc 文件中添加日志
[root@ ~]# vim ~/.bashrc
# ~/.bashrc 文件开头
echo "~/.bashrc start 456"
# ~/.bashrc 文件结尾
echo "~/.bashrc end 456"

通过示例演示,使用系统用户root和普通用户joy

3.3.1、系统用户

1
2
3
4
5
6
7
8
9
10
11
# root用户 log方式切换用户
[root@ ~]# su - root
/etc/profile start 123
/etc/profile.d/hello.sh 123
/etc/profile end 123
~/.bash_profile start 456
~/.bashrc start 456
/etc/bashrc start 123
/etc/bashrc end 123
~/.bashrc end 456
~/.bash_profile end 456

可以看到系统用户root下的环境变量配置文件均加载了

3.3.2、普通用户joy

1
2
3
4
5
6
7
8
# log方式 切换非root用户 
[root@ ~]# su - joy
/etc/profile start 123
/etc/profile.d/hello.sh joy
/etc/profile.d/hello.sh 123
/etc/profile end 123
/etc/bashrc start 123
/etc/bashrc end 123

通过对比发现,普通用户joy家目录(~)下的配置文件中没有输出带有456日志。为什么?难道没有加载joy用户家目录(~)下的 ~/.bashrc 和 ~/.bash_profile环境变量配置文件?

切换joy用户,给 ~/.bashrc和 ~/.bash_profile环境变量配置文件添加日志,注意,此时添加的数字为789

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ~/.bash_profile 文件中添加日志
[joy@ ~]# vim ~/.bash_profile
# ~/.bash_profile 文件开头
echo "~/.bash_profile start 789"
# ~/.bash_profile 文件结尾
echo "~/.bash_profile end 789"

# ~/.bashrc 文件中添加日志
[joy@ ~]# vim ~/.bashrc
# ~/.bashrc 文件开头
echo "~/.bashrc start 789"
# ~/.bashrc 文件结尾
echo "~/.bashrc end 789"

再次切换joy用户

1
[joy@ ~]$ su - joy/etc/profile start 123/etc/profile.d/hello.sh joy/etc/profile.d/hello.sh 123/etc/profile end 123~/.bash_profile start 789~/.bashrc start 789/etc/bashrc start 123/etc/bashrc end 123~/.bashrc end 789~/.bash_profile end 789

可以看到所有文件均加载了,我们对此下

系统用户家目录(~)环境变量配置文件输出日志都带456,/etc目录下配置文件打印是123

普通用户家目录(~)环境变量配置文件打印日志都带789,/etc目录下配置文件打印是123

总结:

系统用户加载在**/etc目录*下的变量配置文件
/etc/profile、/etc/bashrc、/etc/profile.d/
.sh对所有用户生效。

普通用户加载**家目录(~)**用户特有的配置文件 ~/.bash_profile、~/.bashrc,只对当前用户生效。

上述对环境变量配置文件的加载方式、登录态和服务用户三个不同的角度深入的介绍配置文件的特点,从而对配置文件有了更好的把控。

扩展

我们知道环境变量配置文件通常也用来设置一些常用的自定义别名、环境变量、命令别名、Shell函数,那么我们是否可以利用这些能力,简化常用命令以提高工作效率?

系统默认自定义命令别名

1
2
3
4
[root@ ~]# cat ~/.bashrc 
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

从这里看到对rm、cp、mv命令重新定义为选择交互方式执行,以保证操作更安全。

自定义命令别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@ ~]# vim ~/.bashrc
# 定义sortn按照数字升序排序
alias sortn='sort -n'
# 定义访问数据库,此处为示例,注明mysql密码,实际工作总不建议明文配置。
alias mydb='mysql -h 127.0.0.1 -ujoy -p123456 user'

# source 使配置立即生效
[root@ ~]# source ~/.bashrc

[root@ ~]# vi data.txt
20
5
3
60

# 使用自定义命令sortn
[root@ ~]# sortn data.txt > result.txt
[root@ ~]# more result.txt
3
5
20
60

以上演示了sort、mysql命令自定义示例,注:定义访问数据库,此处为示例,mysql为明文密码,实际工作总不建议明文配置。读者可结合自己工作常用命令赶紧来试试吧。