shell教程
OutOfMemory.CN技术专栏-> shell-> shell教程-> 12.5. 文件与归档命令

12.5. 文件与归档命令

归档命令tar标准的UNIX归档工具.[1]起初这只是一个磁带归档程序,而现在这个工具已经被开发为通用打包程序,它能够处理所有设备的所有类型的归档文件,包括磁带设备,正常文件,甚至是stdout(参见Example3-4).GNU的tar工具现在可以接受不同种类的压缩过滤器,比如tarczvfarchive_name.tar.gz*,并且可以递归的处理归档文件,还可以用gzip压缩目录下的所有文件

归档命令

tar

标准的 UNIX 归档工具. [1] 起初这只是一个 磁带 归档 程序, 而现在这个工具已经被开发为通用打包程序, 它能够处理所有设备的所有类型的归档文件, 包括磁带设备, 正常文件, 甚至是 stdout (参见Example 3-4). GNU 的tar工具现在可以接受不同种类的压缩过滤器, 比如tar czvf archive_name.tar.gz *, 并且可以递归的处理归档文件, 还可以用 gzip 压缩目录下的所有文件, 除了当前目录下($PWD)的 点文件 . [2]

一些有用的 tar 命令选项:

  1. -c 创建 (一个新的归档文件)

  2. -x 解压文件 (从存在的归档文件中)

  3. --delete 删除文件 (从存在的归档文件中)

    这个选项不能用于磁带类型设备.

  4. -r 将文件添加到现存的归档文件的尾部

  5. -Atar 文件添加到现存的归档文件的尾部

  6. -t 列出现存的归档文件中包含的内容

  7. -u 更新归档文件

  8. -d 使用指定的文件系统 比较归档文件

  9. -zgzip 压缩归档文件

    (压缩还是解压, 依赖于是否组合了 -c 或 -x)选项

  10. -jbzip2 压缩归档文件

如果想从损坏的用 gzip 压缩过的 tar 文件中取得数据, 那将是很困难的. 所有当我们归档重要的文件的时候, 一定要保留多个备份.

shar

Shell 归档工具. 存在于 shell 归档文件中的所有文件都是未经压缩的, 并且本质上是一个shell 脚本,以 #!/bin/sh 开头, 并且包含所有必要的解档命令. Shar 归档文件 至今还在 Internet 新闻组中使用, 否则的话 shar早就被 tar/gzip 所取代了. unshar 命令用来解档 shar 归档文件.

ar

创建和操作归档文件的工具, 主要在对2进制目标文件打包成库时才会用到.

rpm

Red Hat 包管理器, 或者说 rpm 工具提供了一种对源文件或2进制文件进行打包的方法. 除此之外, 它还包括安装命令, 并且还检查包的完整性.

一个简单的 rpm -i package_name.rpm 命令对于安装一个包来说就足够了, 虽然这个命令还有好多其它的选项.

rpm -qf 列出一个文件属于那个包.

 bash$ rpm -qf /bin/ls
 coreutils-5.2.1-31
 	      

rpm -qa 将会列出给定系统上所有安装了的 rpm 包. rpm -qa package_name 命令将会列出于给定名字匹配的包.

 bash$ rpm -qa
 redhat-logos-1.1.3-1
 glibc-2.2.4-13
 cracklib-2.7-12
 dosfstools-2.7-1
 gdbm-1.8.0-10
 ksymoops-2.4.1-1
 mktemp-1.5-11
 perl-5.6.0-17
 reiserfs-utils-3.x.0j-2
 ...
 
 
 bash$ rpm -qa docbook-utils
 docbook-utils-0.6.9-2
 
 
 bash$ rpm -qa docbook | grep docbook
 docbook-dtd31-sgml-1.0-10
 docbook-style-dsssl-1.64-3
 docbook-dtd30-sgml-1.0-10
 docbook-dtd40-sgml-1.0-11
 docbook-utils-pdf-0.6.9-2
 docbook-dtd41-sgml-1.0-10
 docbook-utils-0.6.9-2
 	      

cpio

这个特殊的归档拷贝命令(拷贝输入和输出)现在已经很少能见到了, 因为它已经被 tar/gzip 所替代了.现在这个命令只在一些比较特殊的地方还在使用,比如拷贝一个目录树.


Example 12-27. 使用 cpio 来拷贝一个目录树

#!/bin/bash

# 使用 'cpio' 拷贝目录树.

# 使用 'cpio' 的优点:
#   加速拷贝. 比通过管道使用 'tar' 命令快一些.
#   很适合拷贝一些 'cp' 命令
#+  搞不定的的特殊文件(比如名字叫 pipes 的文件, 等等)

ARGS=2
E_BADARGS=65

if [ $# -ne "$ARGS" ]
then
  echo "Usage: `basename $0` source destination"
  exit $E_BADARGS
fi

source=$1
destination=$2

find "$source" -depth | cpio -admvp "$destination"
#               ^^^^^         ^^^^^
# 阅读 'find' 和 'cpio' 的man 页来了解这些选项的意义.


# 练习:
# -----

#  添加一些代码来检查 'find | cpio' 管道命令的退出码($?)
#+ 并且如果出现错误的时候输出合适的错误码.

exit 0

rpm2cpio

这个命令可以从 rpm 归档文件中解出一个 cpio 归档文件.


Example 12-28. 解包一个 rpm 归档文件

#!/bin/bash
# de-rpm.sh: 解包一个 'rpm' 归档文件

: ${1?"Usage: `basename $0` target-file"}
# 必须指定 'rpm' 归档文件名作为参数.


TEMPFILE=$$.cpio                         # Tempfile 必须是一个"唯一"的名字.
                                         # $$ 是这个脚本的进程 ID.

rpm2cpio < $1 > $TEMPFILE                # 将 rpm 归档文件转换为 cpio 归档文件.
cpio --make-directories -F $TEMPFILE -i  # 解包 cpio 归档文件.
rm -f $TEMPFILE                          # 删除 cpio 归档文件.

exit 0

#  练习:
#  添加一些代码来检查    1) "target-file" 是否存在
#+                       2) 这个文件是否是一个 rpm 归档文件.
#  暗示:                    分析 'file' 命令的输出.

压缩命令

gzip

标准的 GNU/UNIX 压缩工具, 取代了比较差的 compress 命令. 相应的解压命令是gunzip, gzip -d 是等价的.

zcat 过滤器可以将一个 gzip 文件解压到 stdout, 所以尽可能的使用管道和重定向. 这个命令事实上就是一个可以工作于压缩文件(包括一些的使用老的 compress 工具压缩的文件)的 cat 命令. zcat 命令等价于 gzip -dc.

在某些商业的 UNIX 系统上, zcatuncompress -c 等价, 并且不能工作于 gzip 文件.

参见 Example 7-7.

bzip2

用来压缩的一个可选的工具, 通常比 gzip 命令压缩率更高(所以更慢), 适用于比较大的文件. 相应的解压命令是 bunzip2.

新版本的 tar 命令已经直接支持 bzip2 了.

compress, uncompress

这是一个老的, 私有的压缩工具, 一般的商业 UNIX 发行版都会有这个工具. 更有效率的 gzip 工具早就把这个工具替换掉了. Linux 发行版一般也会包含一个兼容的 compress 命令, 虽然 gunzip 也可以加压用 compress 工具压缩的文件.

znew 命令可以将 compress 压缩的文件转换为 gzip 压缩的文件.

sq

另一种压缩工具, 一个只能工作于排过序的 ASCII 单词列表的过滤器.这个命令使用过滤器标准的调用语法, sq < input-file > output-file. 速度很快, 但是效率远不及 gzip. 相应的解压命令为 unsq, 调用方法与 sq 相同.

sq 的输出可以通过管道传递给 gzip 以便于进一步的压缩.

zip, unzip

跨平台的文件归档和压缩工具, 与 DOS 下的 pkzip.exe 兼容. zip 归档文件看起来在互联网上比 tar 包更流行.

unarc, unarj, unrar

这些 Linux 工具可以用来解档那些用 DOS 下的 arc.exe, arj.exe, 和 rar.exe 程序进行归档的文件.

文件信息

file

确定文件类型的工具. 命令 file file-name 将会用 ascii 文本或数据的形式返回 file-name 文件的详细描述. 这个命令会使用 /usr/share/magic, /etc/magic, 或 /usr/lib/magic 中定义的 魔法数字 来标识包含某种魔法数字的文件, 上边所举出的这3个文件需要依赖于具体的 Linux/UNIX 发行版.

-f 选项将会让 file 命令运行于批处理模式, 也就是说它会分析 -f 后边所指定的文件, 从中读取需要处理的文件列表, 然后依次执行 file 命令. -z 选项, 当对压缩过的目标文件使用时, 将会强制分析压缩的文件类型.

 bash$ file test.tar.gz
 test.tar.gz: gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix
 
 bash file -z test.tar.gz
 test.tar.gz: GNU tar archive (gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix)
 	      

# 在给定的目录中找出sh和Bash脚本文件:

DIRECTORY=/usr/local/bin
KEYWORD=Bourne
# Bourne 和 Bourne-Again shell 脚本

file $DIRECTORY/* | fgrep $KEYWORD

# 输出:

# /usr/local/bin/burn-cd:          Bourne-Again shell script text executable
# /usr/local/bin/burnit:           Bourne-Again shell script text executable
# /usr/local/bin/cassette.sh:      Bourne shell script text executable
# /usr/local/bin/copy-cd:          Bourne-Again shell script text executable
# . . .


Example 12-29. 从 C 文件中去掉注释

#!/bin/bash
# strip-comment.sh: 去掉C 程序中的注释 (/* 注释 */)

E_NOARGS=0
E_ARGERROR=66
E_WRONG_FILE_TYPE=67

if [ $# -eq "$E_NOARGS" ]
then
  echo "Usage: `basename $0` C-program-file" >&2 # 将错误消息发到 stderr.
  exit $E_ARGERROR
fi

# 检查文件类型是否正确.
type=`file $1 | awk '{ print $2, $3, $4, $5 }'`
# "file $1" echoe 出文件类型 . . .
# 然后 awk 会删掉第一个域,  就是文件名 . . .
# 然后结果将会传递到变量 "type" 中.
correct_type="ASCII C program text"

if [ "$type" != "$correct_type" ]
then
  echo
  echo "This script works on C program files only."
  echo
  exit $E_WRONG_FILE_TYPE
fi


# 相当隐秘的 sed 脚本:
#--------
sed '
/^\/\*/d
/.*\*\//d
' $1
#--------
# 如果你花上几个小时来学习 sed 语法的话, 上边这个命令还是很好理解的.


#  如果注释和代码在同一行上, 上边的脚本就不行了.
#+ 所以需要添加一些代码来处理这种情况.
#  这是一个很重要的练习.

#  当然, 上边的代码也会删除带有 "*/" 的非注释行 --
#+ 这也不是一个令人满意的结果.

exit 0


# ----------------------------------------------------------------
# 下边的代码不会执行, 因为上边已经 'exit 0' 了.

# Stephane Chazelas 建议使用下边的方法:

usage() {
  echo "Usage: `basename $0` C-program-file" >&2
  exit 1
}

WEIRD=`echo -n -e '\377'`   # or WEIRD=$'\377'
[[ $# -eq 1 ]] || usage
case `file "$1"` in
  *"C program text"*) sed -e "s%/\*%${WEIRD}%g;s%\*/%${WEIRD}%g" "$1" \
     | tr '\377\n' '\n\377' \
     | sed -ne 'p;n' \
     | tr -d '\n' | tr '\377' '\n';;
  *) usage;;
esac

#  如果是下列的这些情况, 还是很糟糕:
#  printf("/*");
#  or
#  /*  /* buggy embedded comment */
#
#  为了处理上边所有这些特殊情况(字符串中的注释, 含有 \", \\" ...
#+ 的字符串中的注释) 唯一的方法还是写一个 C 分析器
#+ (或许可以使用lex 或者 yacc ?).

exit 0

which

which command-xxx 将会给出 "command-xxx" 的完整路径. 当你想在系统中准确定位一个特定的命令或工具的时候, 这个命令就非常有用了.

$bash which rm

 /usr/bin/rm

whereis

与上边的 which 很相似, whereis command-xxx 不只会给出 "command-xxx" 的完整路径, 而且还会给出这个命令的 man页 的完整路径.

$bash whereis rm

 rm: /bin/rm /usr/share/man/man1/rm.1.bz2

whatis

whatis filexxx 将会在 whatis 数据库中查询 "filexxx". 当你想确认系统命令和重要的配置文件的时候, 这个命令就非常重要了. 可以把这个命令认为是一个简单的 man 命令.

$bash whatis whatis

 whatis               (1)  - search the whatis database for complete words


Example 12-30. Exploring /usr/X11R6/bin

#!/bin/bash

# 在 /usr/X11R6/bin 中的所有神秘的2进制文件都是什么东西?

DIRECTORY="/usr/X11R6/bin"
# 也试试 "/bin", "/usr/bin", "/usr/local/bin", 等等.

for file in $DIRECTORY/*
do
  whatis `basename $file`   # 将会 echo 出这个2进制文件的信息.
done

exit 0

# 你可能希望将这个脚本的输出重定向, 像这样:
# ./what.sh >>whatis.db
# 或者一页一页的在 stdout 上查看,
# ./what.sh | less

参见 Example 10-3.

vdir

显示详细的目录列表. 与 ls -l 的效果类似.

这是一个 GNU fileutils.

 bash$ vdir
 total 10
 -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.xrolo
 -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.xrolo.bak
 -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.xrolo
 
 bash ls -l
 total 10
 -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.xrolo
 -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.xrolo.bak
 -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.xrolo
 	      

locate, slocate

locate 命令将会在预先建立好的档案数据库中查询文件. slocate 命令是 locate 的安全版本( locate 命令可能已经被关联到 slocate 命令上了).

$bash locate hickson

 /usr/lib/xephem/catalogs/hickson.edb

readlink

显示符号连接所指向的文件.

 bash$ readlink /usr/bin/awk
 ../../bin/gawk
 	      

strings

使用 strings 命令在二进制或数据文件中找出可打印字符. 它将在目标文件中列出所有找到的可打印字符的序列. 这个命令对于想进行快速查找一个 n 个字符的打印检查来说是很方便的,也可以用来检查一个未知格式的图片文件 (strings image-file | more 可能会搜索出像 JFIF 这样的字符串, 那么这就意味着这个文件是一个 jpeg 格式的图片文件). 在脚本中, 你可能会使用 grepsed 命令来分析 strings 命令的输出. 参见 Example 10-7Example 10-9.


Example 12-31. 一个"改进过"的 strings 命令

#!/bin/bash
# wstrings.sh: "word-strings" (增强的 "strings" 命令)
#
#  这个脚本将会过滤 "strings" 命令的输出.
#+ 通过排除标准单词列表的形式检查来过滤输出.
#  这将有效的过滤掉无意义的字符,
#+ 并且指挥输出可以识别的字符.

# ===========================================================
#                 脚本参数的标准检查
ARGS=1
E_BADARGS=65
E_NOFILE=66

if [ $# -ne $ARGS ]
then
  echo "Usage: `basename $0` filename"
  exit $E_BADARGS
fi

if [ ! -f "$1" ]                      # 检查文件是否存在.
then
    echo "File \"$1\" does not exist."
    exit $E_NOFILE
fi
# ===========================================================


MINSTRLEN=3                           #  最小的字符串长度.
WORDFILE=/usr/share/dict/linux.words  #  字典文件.
                                      #  也可以指定一个不同的
                                      #+ 单词列表文件,
                                      #+ 但这种文件必须是以每个单词一行的方式进行保存.


wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`

# 将'strings' 命令的输出通过管道传递到多个 'tr' 命令中.
#  "tr A-Z a-z"  全部转换为小写字符.
#  "tr '[:space:]'"  转换空白字符为多个 Z.
#  "tr -cs '[:alpha:]' Z"  将非字母表字符转换为多个 Z,
#+ 然后去除多个连续的 Z.
#  "tr -s '\173-\377' Z"  把所有z后边的字符都转换为 Z.
#+ 并且去除多余重复的Z.(注意173(123 ascii "{")和377(255 ascii 最后一个字符)都是8进制)
#+ 这样处理之后, 我们所有之前需要处理的令我们头痛的字符
#+ 就全都转换为字符 Z 了.
#  最后"tr Z ' '" 将把所有的 Z 都转换为空格,
#+ 这样我们在下边循环中用到的变量 wlist 中的内容就全部以空格分隔了.

#  ****************************************************************
#  注意, 我们使用管道来将多个 'tr' 的输出传递到下一个 'tr' 时
#+ 每次都使用了不同的参数.
#  ****************************************************************


for word in $wlist                    # 重要:
                                      # $wlist 这里不能使用双引号.
                                      # "$wlist" 不能正常工作.
                                      # 为什么不行?
do

  strlen=${#word}                     # 字符串长度.
  if [ "$strlen" -lt "$MINSTRLEN" ]   # 跳过短的字符串.
  then
    continue
  fi

  grep -Fw $word "$WORDFILE"          #  只匹配整个单词.
#      ^^^                            #  "固定字符串" 和
                                      #+ "整个单词" 选项.

done


exit $?

比较命令

diff, patch

diff: 一个非常灵活的文件比较工具. 这个工具将会以一行接一行的形式来比较目标文件. 在某些应用中, 比如说比较单词词典, 在通过管道将结果传递给 diff 命令之前, 使用诸如 sortuniq 命令来对文件进行过滤将是非常有用的.diff file-1 file-2 将会输出2个文件不同的行,并会通过符号标识出每个不同行所属的文件.

diff 命令的 --side-by-side 选项将会把2个比较中的文件全部输出, 按照左右分隔的形式, 并会把不同的行标记出来. -c-u 选项也会使得 diff 命令的输出变得容易解释一些.

还有一些 diff 命令的变种, 比如 sdiff, wdiff, xdiff, 和 mgdiff.

如果比较的两个文件是完全一样的话, 那么 diff 命令会返回 0 作为退出码, 如果不同的话就返回 1 作为退出码. 这样 diff 命令就可以用在 shell 脚本的测试结构中了. (见下边)

diff 命令的一个重要用法就是产生区别文件, 这个文件将用作 patch 命令的 -e 选项的参数, -e 选项接受 edex 脚本.

patch: 灵活的版本工具.给出一个用 diff 命令产生的区别文件, patch 命令可以将一个老版本的包更新为一个新版本的包. 因为你发布一个小的区别文件远比重新发布一个大的软件包来的容易得多.对于频繁更新的 Linux 内核来说, 使用补丁包的形式来发布将是一种很好的方法.

patch -p1 <patch-file
# 在'patch-file'中取得所有的修改列表
# 然后把它们应用于其中索引到的文件上.
# 那么这个包就被更新为新版本了.

更新 kernel:

cd /usr/src
gzip -cd patchXX.gz | patch -p0
#  使用'patch'来更新内核源文件.
# 来自于匿名作者(Alan Cox?)的
# Linux 内核文档 "README".

diff 命令也可以递归的比较目录下的所有文件(包含子目录).

 bash$ diff -r ~/notes1 ~/notes2
 Only in /home/bozo/notes1: file02
 Only in /home/bozo/notes1: file03
 Only in /home/bozo/notes2: file04
 	      

使用 zdiff 来比较 gzip 文件.

diff3

一个 diff 命令的扩展版本, 可以同时比较3个文件. 如果成功执行那么这个命令就返回0, 但是不幸的是这个命令不给出比较结果的信息.

 bash$ diff3 file-1 file-2 file-3
 ====
 1:1c
   This is line 1 of "file-1".
 2:1c
   This is line 1 of "file-2".
 3:1c
   This is line 1 of "file-3"
 	      

sdiff

比较 和/或 编辑2个文件, 将它们合并到一个输出文件中. 因为这个命令的交互特性, 所以在脚本中很少使用这个命令.

cmp

cmp 命令是上边 diff 命令的一个简单版本. diff 命令会报告两个文件的不同之处, 而 cmp 命令仅仅指出那些位置有不同, 而不会显示不同的具体细节.

diff 一样,如果两个文件相同 cmp 返回0作为退出码, 如果不同返回1. 这样就可以用在 shell 脚本的测试结构中了.


Example 12-32. 在一个脚本中使用 cmp 来比较2个文件.

#!/bin/bash

ARGS=2  # 脚本需要2个参数.
E_BADARGS=65
E_UNREADABLE=66

if [ $# -ne "$ARGS" ]
then
  echo "Usage: `basename $0` file1 file2"
  exit $E_BADARGS
fi

if [[ ! -r "$1" || ! -r "$2" ]]
then
  echo "Both files to be compared must exist and be readable."
  exit $E_UNREADABLE
fi

cmp $1 $2 &> /dev/null  # /dev/null 将会禁止 "cmp" 命令的输出.
#   cmp -s $1 $2  与上边这句结果相同 ("-s" 选项是安静标志)
#   Thank you  Anders Gustavsson for pointing this out.
#
# 用 'diff' 命令也可以, 比如,   diff $1 $2 &> /dev/null

if [ $? -eq 0 ]         # 测试 "cmp" 命令的退出码.
then
  echo "File \"$1\" is identical to file \"$2\"."
else
  echo "File \"$1\" differs from file \"$2\"."
fi

exit 0

zcmp 处理 gzip 文件.

comm

多功能的文件比较工具. 使用这个命令之前必须先排序.

comm -options first-file second-file

comm file-1 file-2 将会输出3列:

  • 第 1 列 = 只在 file-1 中存在的行

  • 第 2 列 = 只在 file-2 中存在的行

  • 第 2 列 = 两边相同的行.

下列选项可以禁止1列或多列的输出.

  • -1 禁止显示第一栏 (译者: 在 File1 中的行)

  • -2 禁止显示第二栏 (译者: 在 File2 中的行)

  • -3 禁止显示第三栏 (译者: File1 和 File2 公共的行)

  • -12 禁止第一列和第二列, (就是说选项可以组合).

一般工具

basename

从文件名中去掉路径信息, 只打印出文件名. 结构 basename $0 可以让脚本知道它自己的名字, 也就是, 它被调用的名字. 可以用来显示用法信息, 比如如果你调用脚本的时候缺少参数, 可以使用如下语句:

echo "Usage: `basename $0` arg1 arg2 ... argn"

dirname

从带路径的文件名中去掉文件名, 只打印出路径信息.

basenamedirname 可以操作任意字符串. 参数可以不是一个真正存在的文件, 甚至可以不是一个文件名.(参见 Example A-7).


Example 12-33. basenamedirname

#!/bin/bash

a=/home/bozo/daily-journal.txt

echo "Basename of /home/bozo/daily-journal.txt = `basename $a`"
echo "Dirname of /home/bozo/daily-journal.txt = `dirname $a`"
echo
echo "My own home is `basename ~/`."         # `basename ~` also works.
echo "The home of my home is `dirname ~/`."  # `dirname ~`  also works.

exit 0

split, csplit

将一个文件分割为几个小段的工具. 这些命令通常用来将大的文件分割, 并备份到软盘上, 或者是为了切成合适的尺寸用 email 上传.

csplit 根据 上下文 来切割文件, 切割的位置将会发生在模式匹配的地方.

sum, cksum, md5sum, sha1sum

这些都是用来产生 checksum 的工具. checksum 的目的是用来检验文件的完整性, 是对文件的内容进行数学计算而得到的. 出于安全目的一个脚本可能会有一个 checksum 列表, 这样可以确保关键系统文件的内容不会被修改或损坏. 对于需要安全性的应用来说, 应该使用 md5sum (message digest 5 checksum) 命令, 或者更好的更新的 sha1sum (安全 Hash 算法).

 bash$ cksum /boot/vmlinuz
 1670054224 804083 /boot/vmlinuz
 
 bash$ echo -n "Top Secret" | cksum
 3391003827 10
 
 
 
 bash$ md5sum /boot/vmlinuz
 0f43eccea8f09e0a0b2b5cf1dcf333ba  /boot/vmlinuz
 
 bash$ echo -n "Top Secret" | md5sum
 8babc97a6f62a4649716f4df8d61728f  -
 	      

cksum 命令将会显示目标的尺寸(字节), 目标可以使文件或 stdout.

md5sumsha1sum 命令在它们收到 stdout 的输入时候, 显示一个 dash .


Example 12-34. 检查文件完整性

#!/bin/bash
# file-integrity.sh: 检查一个给定目录下的文件
#                    是否被改动了.

E_DIR_NOMATCH=70
E_BAD_DBFILE=71

dbfile=File_record.md5
# 存储记录的文件名 (数据库文件).


set_up_database ()
{
  echo ""$directory"" > "$dbfile"
  # 把目录名写到文件的第一行.
  md5sum "$directory"/* >> "$dbfile"
  # 在文件中附上  md5 checksums 和 filenames.
}

check_database ()
{
  local n=0
  local filename
  local checksum

  # ------------------------------------------- #
  #  这个文件检查其实是不必要的,
  #+ 但是能安全一些.

  if [ ! -r "$dbfile" ]
  then
    echo "Unable to read checksum database file!"
    exit $E_BAD_DBFILE
  fi
  # ------------------------------------------- #

  while read record[n]
  do

    directory_checked="${record[0]}"
    if [ "$directory_checked" != "$directory" ]
    then
      echo "Directories do not match up!"
      # 换个目录试一下.
      exit $E_DIR_NOMATCH
    fi

    if [ "$n" -gt 0 ]   # 不是目录名.
    then
      filename[n]=$( echo ${record[$n]} | awk '{ print $2 }' )
      #  md5sum 向后写记录,
      #+ 先写 checksum, 然后写 filename.
      checksum[n]=$( md5sum "${filename[n]}" )


      if [ "${record[n]}" = "${checksum[n]}" ]
      then
        echo "${filename[n]} unchanged."

      elif [ "`basename ${filename[n]}`" != "$dbfile" ]
             #  跳过checksum 数据库文件,
             #+ 因为在每次调用脚本它都会被修改.
	     #  ---
	     #  这不幸的意味着当我们在 $PWD中运行这个脚本
	     #+ 时, 修改这个 checksum 数
	     #+ 据库文件将不会被检测出来.
	     #  练习: 修复这个问题.
	then
          echo "${filename[n]} : CHECKSUM ERROR!"
        # 因为最后的检查, 文件已经被修改.
      fi

      fi



    let "n+=1"
  done <"$dbfile"       # 从 checksum 数据库文件中读.

}

# =================================================== #
# main ()

if [ -z  "$1" ]
then
  directory="$PWD"      #  如果没制定参数,
else                    #+ 那么就使用当前的工作目录.
  directory="$1"
fi

clear                   # 清屏.
echo " Running file integrity check on $directory"
echo

# ------------------------------------------------------------------ #
  if [ ! -r "$dbfile" ] # 是否需要建立数据库文件?
  then
    echo "Setting up database file, \""$directory"/"$dbfile"\"."; echo
    set_up_database
  fi
# ------------------------------------------------------------------ #

check_database          # 调用主要处理函数.

echo

#  你可能想把这个脚本的输出重定向到文件中,
#+ 尤其在这个目录中有很多文件的时候.

exit 0

#  如果要对数量非常多的文件做完整性检查,
#+ 可以考虑一下 "Tripwire" 包,
#+ http://sourceforge.net/projects/tripwire/.

参见 Example A-19Example 33-14 , 这两个例子展示了 md5sum 命令的用法.

已经有 128-bit md5sum 被破解的报告了,所以现在更安全的 160-bit sha1sum 是非常受欢迎的, 并且已经被加入到 checksum 工具包中.

一些安全顾问认为即使是 sha1sum 也是会被泄漏的. 所以, 下一个工具是什么呢? -- 512-bit 的 checksum 工具?

 bash$ md5sum testfile
 e181e2c8720c60522c4c4c981108e367  testfile
 
 
 bash$ sha1sum testfile
 5d7425a9c08a66c3177f1e31286fa40986ffc996  testfile
 	      
shred

用随机字符填充文件, 使得文件无法恢复, 这样就可以保证文件安全的被删除. 这个命令的效果与 Example 12-55 一样, 但是使用这个命令是一种更优雅更彻底的方法.

这是一个 GNU fileutils.

即使使用了 shred 命令, 高级的(forensic)辩论技术还是能够恢复文件的内容.

编码和解码

uuencode

这个工具用来把二进制文件编码成 ASCII 字符串,这个工具适用于编码e-mail消息体,或者新闻组消息.

uudecode

这个工具用来把 uuencode 后的 ASCII 字符串恢复为二进制文件.


Example 12-35. Uudecod 编码后的文件

#!/bin/bash
# 在当前目录下 uudecode 所有用 uuencode 编码的文件.

lines=35        # 允许读头部的 35 行(范围很宽).

for File in *   # Test 所有 $PWD 下的文件.
do
  search1=`head -$lines $File | grep begin | wc -w`
  search2=`tail -$lines $File | grep end | wc -w`
  #  Uuencode 过的文件在文件开始的地方有个 "begin",
  #+ 在文件结尾的地方有个 "end".
  if [ "$search1" -gt 0 ]
  then
    if [ "$search2" -gt 0 ]
    then
      echo "uudecoding - $File -"
      uudecode $File
    fi
  fi
done

#  小心不要让这个脚本运行自己,
#+ 因为它也会把自身也认为是一个 uuencoded 文件,
#+ 这都是因为这个脚本自身也包含 "begin" 和 "end".

#  练习:
#  -----
#  修改这个脚本, 让它可以检查一个新闻组的每个文件,
#+ 并且如果下一个没找的话就跳过.

exit 0

fold -s 命令在处理从 Usenet 新闻组下载下来的长的uudecode 文本消息的时候可能会有用(可能在管道中).

mimencode, mmencode

mimencodemmencode 命令处理多媒体编码的 email 附件. 虽然 mail 用户代理 (比如 pinekmail) 通常情况下都会自动处理, 但是这些特定的工具允许从命令行或shell脚本中来手动操作这些附件.

crypt

这个工具曾经是标准的 UNIX 文件加密工具. [3] 政府由于政策上的动机规定禁止加密软件的输出, 这样导致了 crypt 命令从 UNIX 世界消失, 并且在大多数的 Linux 发行版中也没有这个命令. 幸运的是, 程序员们想出了一些替代它的方法, 在这些方法中有作者自己的 cruft (参见 Example A-4).

一些杂项工具

mktemp

使用一个"唯一"的文件名来创建一个 临时文件 [4] . 如果不带参数的在命令行下调用这个命令时, 将会在 /tmp 目录下产生一个零长度的文件.

 bash$ mktemp
 /tmp/tmp.zzsvql3154
 	      

PREFIX=filename
tempfile=`mktemp $PREFIX.XXXXXX`
#                        ^^^^^^ 在这个临时的文件名中
#+                              至少需要6个占位符.
#  如果没有指定临时文件的文件名,
#+ 那么默认就是 "tmp.XXXXXXXXXX".

echo "tempfile name = $tempfile"
# tempfile name = filename.QA2ZpY
#                 或者一些其他的相似的名字...

#  使用 600 为文件权限
#+ 来在当前工作目录下创建一个这样的文件.
#  这样就不需要 "umask 177" 了.
#  但不管怎么说, 这也是一个好的编程风格.

make

build 和 compile 二进制包的工具. 当源文件被增加或修改时就会触发一些操作, 这个工具用来控制这些操作.

make 命令将会检查 Makefile, makefile 是文件的依赖和操作列表.

install

特殊目的的文件拷贝命令, 与 cp 命令相似, 但是具有设置拷贝文件的权限和属性的能力. 这个命令看起来是为了安装软件包所定制的, 而且就其本身而言, 这个命令经常出现在 Makefile 中(在 make install : 区中). 在安装脚本中也会看到这个命令的使用.

dos2unix

这个工具是由 Benjamin Lin 和其同事编写的, 目的是将 DOS 格式的文本文件 (以 CR-LF 为行结束符) 转换为 UNIX 格式 (以 LF 为行结束符), 反过来也一样.

ptx

ptx [targetfile] 命令将会输出目标文件的序列改变的索引(交叉引用列表). 如果必要的话, 这个命令可以在管道中进行更深层次的过滤和格式化.

more, less

分页显示文本文件或 stdout, 一次一屏.可以用来过滤 stdout 的输出 . . . 或一个脚本的输出.

more 命令的一个有趣的应用就是测试一个命令序列的执行, 来避免可能发生的糟糕的结果.

ls /home/bozo | awk '{print "rm -rf " $1}' | more
#                                            ^^^^

# 检测下边(灾难性的)命令行的效果:
#      ls /home/bozo | awk '{print "rm -rf " $1}' | sh
#      推入 shell 中执行 . . .

注意事项:

[1]

在这里所讨论的一个归档文件, 只不过是存储在一个单一位置上的一些相关文件的集合.

[2]

tar czvf archive_name.tar.gz * 可以 包含当前工作目录下的点文件. 这是一个未文档化的 GNU tar 的"特征".

[3]

这是一个对称的块密码, 过去曾在单系统或本地网络中用来加密文件, 用来对抗 "public key" 密码类, pgp 就是一个众所周知的例子.

[4]

使用 -d 选项可以创建一个临时的目录.

© 内存溢出 OutOfMemory.CN