归档和备份

gzip

gzip 程序用来压缩文件,原文件的压缩版(添加gz后缀名)会替代原文件。gunzip 程序用来还原压缩版本。

1
2
$ gzip foo.txt
$ gunzip foo.txt.gz

gzip的参数如下。

  • -c 把输出写入到标准输出,并且保留原始文件。也有可能用–stdout 和–to-stdout 选项来指定。
  • -d 解压缩。正如 gunzip 命令一样。也可以用–decompress 或者–uncompress 选项来指定.
  • -f 强制压缩,即使原始文件的压缩文件已经存在了,也要执行。也可以用–force 选项来指定。
  • -h 显示用法信息。也可用–help 选项来指定。
  • -l 列出每个被压缩文件的压缩数据。也可用–list 选项。
  • -r 若命令的一个或多个参数是目录,则递归地压缩目录中的文件。也可用–recursive 选项来指定。
  • -t 测试压缩文件的完整性。也可用–test 选项来指定。
  • -v 显示压缩过程中的信息。也可用–verbose 选项来指定。
  • -number 设置压缩指数。number 是一个在1(最快,最小压缩)到9(最慢,最大压缩)之间的整数。 数值1和9也可以各自用–fast 和–best 选项来表示。默认值是整数6。

下面是一些例子。

1
2
# 查看解压缩后的内容
$ gunzip -c foo.txt | less

zcat程序等同于带有-c 选项的 gunzip 命令。它可以像cat命令那样,用来查看gzip压缩文件。

1
$ zcat foo.txt.gz | less

bzip2

bzip2程序与gzip程序相似,但是使用了不同的压缩算法,舍弃了压缩速度,实现了更高的压缩级别。在大多数情况下,它的工作模式等同于gzip。 由bzip2压缩的文件,用扩展名.bz2表示。

1
2
$ bzip2 foo.txt
$ bunzip2 foo.txt.bz2

gzip程序的所有选项(除了-r),bzip2 程序同样也支持。同样有 bunzip2 和 bzcat 程序来解压缩文件。bzip2 文件也带有 bzip2recover 程序,其会 试图恢复受损的 .bz2 文件。

zip

zip程序既是压缩工具,也是一个打包工具,读取和写入.zip文件。

1
$ zip options zipfile file...

它的用法如下。

1
2
# 将指定目录压缩成zip文件
$ zip -r playground.zip playground

ziptar命令有一个相反之处。如果压缩文件已存在,其将被更新而不是被替代。这意味着会保留此文件包,但是会添加新文件,同时替换匹配的文件。

解压使用unzip命令。

1
$ unzip ../playground.zip

unzip命令的参数如下。

  • -l 列出文件包中的内容而不解压
  • -v 显示冗余信息
  • -p 输出发送到标准输出
1
$ unzip -p ls-etc.zip | less

tar

tar是tape archive的简称,原来是一款制作磁带备份的工具,现在主要用于打包。一个 tar 包可以由一组独立的文件,一个或者多个目录,或者两者混合体组成。

tar程序的语法如下。

1
$ tar mode[options] pathname...

tar支持以下模式。

  • c 表示create,为文件和/或目录列表创建归档文件。
  • x 抽取归档文件。
  • r 追加具体的路径到归档文件的末尾。
  • t 列出归档文件的内容。

支持的参数如下。

  • f 表示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
# 创建子目录的tar包
$ tar cf playground.tar playground

# 查看tar包内容
$ tar tf playground.tar

# 查看更详细的列表信息
$ tar tvf playground.tar

# 还原归档文件
$ tar xf playground.tar

# 还原单个文件
$ tar xf archive.tar pathname

# 还原文件到指定目录
$ tar xvf archive.tar -C /home/me/

# 追加文件
$ tar rf archive.tar file.txt

# 验证归档文件内容是否正确
$ tar tvfW archive.tar

# 支持通配符
$ tar xf ../playground2.tar --wildcards 'home/me/playground/\*.txt'

注意,tar命令还原的时候,总是还原为相对路径。如果归档的时候,保存的是绝对路径,那么还原的时候,这个绝对路径会整个变成相对路径。

find命令可以与tar命令配合使用。

1
$ find playground -name 'file.txt' -exec tar rf playground.tar '{}' '+'

上面的命令先用find程序找到所有名为file.txt的文件,然后使用追加模式(r)的tar命令,把匹配的文件添加到归档文件playground.tar里面。

这种tarfind的配合使用,可以创建逐渐增加的目录树或者整个系统的备份。通过find命令匹配新于某个时间戳的文件,我们就能够创建一个归档文件,其只包含新于上一个 tar 包的文件。

tar支持压缩功能。

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
# 打成gzip压缩包
$ tar czvf assets.tar.gz dist

# 打成bz2压缩包
$ tar cvfj assets.tar.bz2 dist

# 解压 tar.gz 文件
$ tar xzv archive.tar.gz
$ tar xvf archive.tar.gz

# 解压bz2压缩包
$ tar xvf archive.tar.bz2

# 显示gzip压缩包内容
$ tar tvf archive.tar.gz

# 显示bz2压缩包内容
$ tar tvf archive.tar.bz2

# 从gzip压缩包取出单个文件
$ tar zxvf archive.tar.gz file.txt

# 从bz2压缩包取出单个文件
$ tar jxvf archive.tar.bz2 file.txt

# 按通配符取出文件
$ tar zxvf archive.tar.gz --wildcards '*.php'
$ tar jxvf archive.tar.bz2 --wildcards '*.php'

# 追加文件到压缩包
$ tar rvf archive.tar.gz xyz.txt
$ tar rvf archive.tar.bz2 xyz.txt

rsync

rsync命令用于在多个目录之间、或者本地与远程目录之间同步。字母r表示remote

1
$ rsync options source destination

source 和 destination 是下列选项之一:

  • 一个本地文件或目录
  • 一个远端文件或目录,以[user@]host:path的形式存在
  • 一个远端 rsync 服务器,由rsync://[user@]host[:port]/path指定

注意 source 和 destination 两者之一必须是本地文件。rsync 不支持远端到远端的复制。

rsync命令的参数如下。

  • -a 递归和保护文件属性
  • -v 冗余输出
  • --delete 删除可能在备份设备中已经存在但却不再存在于源设备中的文件
  • --rsh=ssh 使用 ssh 程序作为远程 shell,目的地必须标注主机名。
1
2
3
4
5
6
7
8
9
10
11
# 同步两个本地目录
$ rsync -av playground foo

# 删除源设备不存在的文件
$ sudo rsync -av --delete /etc /home /usr/local /media/BigDisk/backup

# 远程同步
$ sudo rsync -av --delete --rsh=ssh /etc /home /usr/local remote-sys:/backup

# 与远程rsync主机同步
$ rsync -av -delete rsync://rsync.gtlib.gatech.edu/path/to/oss fedora-devel

异步任务

Bash脚本有时候需要同时执行多个任务。通常这涉及到启动一个脚本,依次,启动一个或多个子脚本来执行额外的任务,而父脚本继续运行。然而,当一系列脚本 以这种方式运行时,要保持父子脚本之间协调工作,会有一些问题。也就是说,若父脚本或子脚本依赖于另一方,并且 一个脚本必须等待另一个脚本结束任务之后,才能完成它自己的任务,这应该怎么办?

bash 有一个内置命令,能帮助管理诸如此类的异步执行的任务。wait 命令导致一个父脚本暂停运行,直到一个 特定的进程(例如,子脚本)运行结束。

首先我们将演示一下 wait 命令的用法。为此,我们需要两个脚本,一个父脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# async-parent : Asynchronous execution demo (parent)
echo "Parent: starting..."
echo "Parent: launching child script..."
async-child &
pid=$!
echo "Parent: child (PID= $pid) launched."
echo "Parent: continuing..."
sleep 2
echo "Parent: pausing to wait for child to finish..."
wait $pid
echo "Parent: child is finished. Continuing..."
echo "Parent: parent is done. Exiting."

和一个子脚本:

1
2
3
4
5
#!/bin/bash
# async-child : Asynchronous execution demo (child)
echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."

在这个例子中,我们看到该子脚本是非常简单的。真正的操作通过父脚本完成。在父脚本中,子脚本被启动, 并被放置到后台运行。子脚本的进程 ID 记录在 pid 变量中,这个变量的值是 $! shell 参数的值,它总是 包含放到后台执行的最后一个任务的进程 ID 号。

父脚本继续,然后执行一个以子进程 PID 为参数的 wait 命令。这就导致父脚本暂停运行,直到子脚本退出, 意味着父脚本结束。

当执行后,父子脚本产生如下输出:

1
2
3
4
5
6
7
8
9
10
$ async-parent
Parent: starting...
Parent: launching child script...
Parent: child (PID= 6741) launched.
Parent: continuing...
Child: child is running...
Parent: pausing to wait for child to finish...
Child: child is done. Exiting.
Parent: child is finished. Continuing...
Parent: parent is done. Exiting.

Shell 的命令

命令的类别

Bash可以使用的命令分成四类。

  • 可执行程序
  • Shell 提供的命令
  • Shell 函数
  • 前三类命令的别名

type, whatis

type命令可以显示命令类型。

1
$ type command

下面是几个例子。

1
2
3
4
5
6
7
8
$ type type
type is a shell builtin

$ type ls
ls is aliased to `ls --color=tty'

$ type cp
cp is /bin/cp

whatis命令显示指定命令的描述。

1
2
$ whatis ls
ls (1) - list directory contents

apropos

apropos命令返回符合搜索条件的命令列表。

1
2
3
4
5
6
7
$ apropos floppy
create_floppy_devices (8) - udev callout to create all possible
fdformat (8) - Low-level formats a floppy disk
floppy (8) - format floppy disks
gfloppy (1) - a simple floppy formatter for the GNOME
mbadblocks (1) - tests a floppy disk, and marks the bad
mformat (1) - add an MSDOS filesystem to a low-level

alias, unalias

alias命令用来为命令起别名。

1
2
3
4
$ alias foo='cd /usr; ls; cd -'

$ type foo
foo is aliased to `cd /usr; ls ; cd -'

上面命令指定foo为三个命令的别名。以后,执行foo就相当于一起执行这三条命令。

注意,默认情况下,别名只在当前Session有效。当前Session结束时,这些别名就会消失。

alias命令不加参数时,显示所有有效的别名。

1
2
3
4
$ alias
alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'

unalias命令用来取消别名。

1
2
3
$ unalias foo
$ type foo
bash: type: foo: not found

which

which命令显示可执行程序的路径。

1
2
$ which ls
/bin/ls

which命令用于Shell内置命令时(比如cd),将没有任何输出。

help,man

help命令用于查看Shell内置命令的帮助信息,man命令用于查看可执行命令的帮助信息。

1
2
$ help cd
$ man ls

man里面的文档一共有8类,如果同一个命令,匹配多个文档,man命令总是返回第一个匹配。如果想看指定类型的文档,命令可以采用下面的形式。

1
$ man 5 passwd

script

script命令会将输入的命令和它的输出,都保存进一个文件。

1
$ script [file]

如果没有指定文件名,则所有结果会保存进当前目录下typescript文件。结束录制的时候,可以按下Ctrl + d

export

export命令用于将当前进程的变量,输出到所有子进程。

命令的连续执行

多个命令可以写在一起。

Bash 提供三种方式,定义它们如何执行。

1
2
3
4
5
6
7
8
# 第一个命令执行完,执行第二个命令
command1; command2

# 只有第一个命令成功执行完(退出码0),才会执行第二个命令
command1 && command2

# 只有第一个命令执行失败(退出码非0),才会执行第二个命令
command1 || command2

上面三种执行方法的退出码,都是最后一条执行的命令的退出码。

bash 允许把命令组合在一起。可以通过两种方式完成;要么用一个 group 命令,要么用一个子 shell。 这里是每种方式的语法示例:

组命令:

1
{ command1; command2; [command3; ...] }

子 shell

1
(command1; command2; [command3;...])

这两种形式的不同之处在于,组命令用花括号把它的命令包裹起来,而子 shell 用括号。值得注意的是,鉴于 bash 实现组命令的方式, 花括号与命令之间必须有一个空格,并且最后一个命令必须用一个分号或者一个换行符终止。

那么组命令和子 shell 命令对什么有好处呢? 它们都是用来管理重定向的。

1
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt

使用一个子 shell 是相似的。

1
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt

组命令和子 shell 真正闪光的地方是与管道线相结合。 当构建一个管道线命令的时候,通常把几个命令的输出结果合并成一个流是很有用的。 组命令和子 shell 使这种操作变得很简单。

1
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

这里我们已经把我们的三个命令的输出结果合并在一起,并把它们用管道输送给命令 lpr 的输入,以便产生一个打印报告。

虽然组命令和子 shell 看起来相似,并且它们都能用来在重定向中合并流,但是两者之间有一个很重要的不同。 然而,一个组命令在当前 shell 中执行它的所有命令,而一个子 shell(顾名思义)在当前 shell 的一个 子副本中执行它的命令。这意味着运行环境被复制给了一个新的 shell 实例。当这个子 shell 退出时,环境副本会消失, 所以在子 shell 环境(包括变量赋值)中的任何更改也会消失。因此,在大多数情况下,除非脚本要求一个子 shell, 组命令比子 shell 更受欢迎。组命令运行很快并且占用的内存也少。

当我们发现管道线中的一个 read 命令 不按我们所期望的那样工作的时候。为了重现问题,我们构建一个像这样的管道线:

1
2
echo "foo" | read
echo $REPLY

该 REPLY 变量的内容总是为空,是因为这个 read 命令在一个子 shell 中执行,所以它的 REPLY 副本会被毁掉, 当该子 shell 终止的时候。因为管道线中的命令总是在子 shell 中执行,任何给变量赋值的命令都会遭遇这样的问题。 幸运地是,shell 提供了一种奇异的展开方式,叫做进程替换,它可以用来解决这种麻烦。进程替换有两种表达方式:

一种适用于产生标准输出的进程:

1
<(list)

另一种适用于接受标准输入的进程:

1
>(list)

这里的 list 是一串命令列表:

为了解决我们的 read 命令问题,我们可以雇佣进程替换,像这样。

1
2
read < <(echo "foo")
echo $REPLY

进程替换允许我们把一个子 shell 的输出结果当作一个用于重定向的普通文件。事实上,因为它是一种展开形式,我们可以检验它的真实值:

1
2
[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63

通过使用 echo 命令,查看展开结果,我们看到子 shell 的输出结果,由一个名为 /dev/fd/63 的文件提供。