#!/bin/sh
# 检查/设定临时目录
TMP=$ROOT/var/log/setup/tmp # removepkg 使用环境变量 ROOT 的值为根目录
# If the $TMP directory doesn't exist, create it:
if [ ! -d $TMP ]; then
rm -rf $TMP # make sure it's not a symlink or something stupid
mkdir -p $TMP
chmod 700 $TMP # no need to leave it open
fi
ADM_DIR=$ROOT/var/log
PRES_DIR=$TMP/preserved_packages # 保留包内容所用到的目录
# 函数: 显示 $1 目录下除了 $2 文件之外所有文件的内容
cat_except() {
( cd "$1" && cat `ls * | sed "/^$2\$/d"` )
}
# 函数: 通过 doinst.sh 中的命令来得到符号链接的路径
# 这个函数不接受参数, 只是从标准输入读取输入, 处理后再输出
#
# makepkg 制作软件包的时候, 可以选择把其中的符号链接删除, 并生成 doinst.sh,
# 在 doinst.sh 中包含重新建立的命令, 下面是从 aaa_base-12.1.0-noarch-2.tgz.gz 中
# 抄出来的:
# ( cd usr/man ; rm -rf cat1 )
# ( cd usr/man ; ln -sf /var/man/cat1 cat1 )
# 本函数通过分析第一行, 得到 usr/man/cat1 这样的符号链接
extract_links() {
sed -n 's,^( *cd \([^ ;][^ ;]*\) *; *rm -rf \([^ )][^ )]*\) *) *$,\1/\2,p'
}
# 函数: 保留文件
# 当 PRESERVE 设置为 true 的时候, 把传进来的参数所代表的文件保存到 $PRES_DIR
# 目录下面的 $PKGNAME 目录下, 并保持原先的目录结构
# $PKGNAME 是由 remove_packages 函数中得到的, 表示正在处理的包名称
preserve_file() {
if [ "$PRESERVE" = "true" ]; then
F="`basename "$1"`"
D="`dirname "$1"`"
if [ ! -d "$PRES_DIR/$PKGNAME/$D" ]; then
mkdir -p "$PRES_DIR/$PKGNAME/$D" || return 1
fi
cp -p "$ROOT/$D/$F" "$PRES_DIR/$PKGNAME/$D" || return 1
fi
return 0
}
# 函数: 保留目录
# 在 $PRES_DIR/$PKGNAME 下建立传入参数所代表的路径
# 使用的是 mkdir -p, 所以不一定要是一层目录
preserve_dir() {
if [ "$PRESERVE" = "true" ]; then
if [ ! -d "$PRES_DIR/$PKGNAME/$1" ]; then
mkdir -p "$PRES_DIR/$PKGNAME/$1" || return 1
fi
fi
return 0
}
# 函数: 从标准输入读入文件列表, 检查是否是文件后通过调用 preserve_file 来保存文件
# 如果列表中的某个文件实际是目录, 那么则使用 preserve_dir 来保留目录
#
# 另, 此函数认为传进来的所有文件名称都是某个包中包含的, 所以如果有文件名从参数
# 传进来, 不是目录又不是文件, 则认为是其它软件包中包含的, 但不存在的文件,
# 会给出相应的警告信息
keep_files() {
while read FILE ; do
if [ ! -d "$ROOT/$FILE" ]; then
if [ -r "$ROOT/$FILE" ]; then
echo " --> $ROOT/$FILE was found in another package. Skipping."
preserve_file "$FILE"
else
if [ "`echo $FILE | cut -b1-8`" != "install/" ]; then
echo "WARNING: Nonexistent $ROOT/$FILE was found in another package. Skipping."
fi
fi
else
preserve_dir "$FILE"
fi
done
}
# 函数: 类似 keep_files, 不过它认为标准输入得到的文件名要么是链接, 要么不存在
keep_links() {
while read LINK ; do
if [ -L "$ROOT/$LINK" ]; then
echo " --> $ROOT/$LINK (symlink) was found in another package. Skipping."
else
echo "WARNING: Nonexistent $ROOT/$LINK (symlink) was found in another package. Skipping."
fi
done
}
# 函数: 删除文件
# 从标准输入中读取文件列表, 并依次删除其中文件
delete_files() {
while read FILE ; do
if [ ! -d "$ROOT/$FILE" ]; then
if [ -r "$ROOT/$FILE" ]; then
# 如果文件比安装记录要新, 则给出警告
if [ "$ROOT/$FILE" -nt "$ADM_DIR/packages/$PKGNAME" ]; then
echo "WARNING: $ROOT/$FILE changed after package installation."
fi
if [ ! "$WARN" = "true" ]; then
echo " --> Deleting $ROOT/$FILE"
preserve_file "$FILE" && rm -f "$ROOT/$FILE"
else # warn 模式, 只提示, 不实际删除
echo " --> $ROOT/$FILE would be deleted"
preserve_file "$FILE" # preserve_file 会根据 PRESERVE 的设定来决定是否保留
fi
else # 要删除的文件已经不存在了, 给出提示
echo " --> $ROOT/$FILE no longer exists. Skipping."
fi
else # 如果文件名实际是一个目录, 则调用 preserve_dir 来处理
preserve_dir "$FILE"
fi
done
}
# 函数: 删除链接, 类似 delete_files
delete_links() {
while read LINK ; do
if [ -L "$ROOT/$LINK" ]; then
if [ ! "$WARN" = "true" ]; then
echo " --> Deleting symlink $ROOT/$LINK"
rm -f $ROOT/$LINK
else
echo " --> $ROOT/$LINK (symlink) would be deleted"
fi
else
echo " --> $ROOT/$LINK (symlink) no longer exists. Skipping."
fi
done
}
# 函数: 删除目录, 用来删除卸载软件之后的目录
# 在 remove_packages 函数中, 传递给它的是 uniq_list$$ 文件, 是被卸载软件独有的部分
delete_dirs() {
# 逆序排序, 这样内层的目录就会排在前面
sort -r | \
while read DIR ; do
if [ -d "$ROOT/$DIR" ]; then
if [ ! "$WARN" = "true" ]; then
# 目录为空
if [ `ls -a "$ROOT/$DIR" | wc -l` -eq 2 ]; then
echo " --> Deleting empty directory $ROOT/$DIR"
rmdir "$ROOT/$DIR"
else
echo "WARNING: Unique directory $ROOT/$DIR contains new files"
fi
else # warn 模式, 只提示, 不删除
echo " --> $ROOT/$DIR (dir) would be deleted if empty"
fi
fi
done
}
# 这个函数从代码看, 是将 man* 下面的文件对应的 cat* 下的文件删除
# 但是我的机器上没看到 cat* 下有文件
delete_cats() {
sed -n 's,/man\(./[^/]*$\),/cat\1,p' | \
while read FILE ; do
if [ -f "$ROOT/$FILE" ]; then
if [ ! "$WARN" = "true" ]; then
echo " --> Deleting $ROOT/$FILE (fmt man page)"
rm -f $ROOT/$FILE
else
echo " --> $ROOT/$FILE (fmt man page) would be deleted"
fi
fi
done
}
# 获取软件名, 与 installpkg 中的相同
package_name() {
STRING=`basename $1 .tgz`
# Check for old style package name with one segment:
if [ "`echo $STRING | cut -f 1 -d -`" = "`echo $STRING | cut -f 2 -d -`" ]; then
echo $STRING
else # has more than one dash delimited segment
# Count number of segments:
INDEX=1
while [ ! "`echo $STRING | cut -f $INDEX -d -`" = "" ]; do
INDEX=`expr $INDEX + 1`
done
INDEX=`expr $INDEX - 1` # don't include the null value
# If we don't have four segments, return the old-style (or out of spec) package name:
if [ "$INDEX" = "2" -o "$INDEX" = "3" ]; then
echo $STRING
else # we have four or more segments, so we'll consider this a new-style name:
NAME=`expr $INDEX - 3`
NAME="`echo $STRING | cut -f 1-$NAME -d -`"
echo $NAME
# cruft for later ;)
#VER=`expr $INDEX - 2`
#VER="`echo $STRING | cut -f $VER -d -`"
#ARCH=`expr $INDEX - 1`
#ARCH="`echo $STRING | cut -f $ARCH -d -`"
#BUILD="`echo $STRING | cut -f $INDEX -d -`"
fi
fi
}
# 函数: 把参数作为软件列表, 一个一个删除
remove_packages() {
# 一个一个处理
for PKGLIST in $*
do
# 如果软件名包括了 .tgz, 则把 .tgz 去掉
PKGNAME=`basename $PKGLIST .tgz`
echo
# 如果没找到这个软件, 那么这里试着把这个软件作为短名称(像
# bash-3.1.017-i486-2 中的 bash 那样)来寻找长的包名称(name-version-arch-build)
# 正常情况下, 一个短名称只会找到一个长名称, 但是这个不是强制性的, 如果有多个
# 对应的长名称, 那么只删出其中的一个, 如果要全部删除, 则需要再使用几次 removepkg
if [ ! -e $ADM_DIR/packages/$PKGNAME ]; then
SHORT="`package_name $PKGNAME`" # 这句没什么用
for long_package in $ADM_DIR/packages/${PKGNAME}* ; do
if [ "`package_name $PKGNAME`" = "`package_name $long_package`" ]; then
PKGNAME="`basename $long_package`"
fi
done
fi
if [ -r $ADM_DIR/packages/$PKGNAME ]; then
if [ ! "$WARN" = true ]; then
echo "Removing package $ADM_DIR/packages/$PKGNAME..."
fi
# 在包记录文件($ROOT/var/log/packages/$PKGNAME)中从 ./ 开始, 之后的行是这个包
# 所安装的文件及目录, 如果没有找到 ./, 那么就是从 FILE LIST: 开始
if fgrep "./" $ADM_DIR/packages/$PKGNAME 1> /dev/null 2>&1; then
TRIGGER="^\.\/"
else
TRIGGER="FILE LIST:"
fi
if [ ! "$WARN" = true ]; then
echo "Removing files:"
fi
# 将包记录文件中的文件/目录列表提取出来, 如果是以 FILE LIST: 开始的,
# 则去掉这一行, 将结果排序后放入 $TMP/delete_list$$
# 之所以要排序, 是因为这个函数使用了 comm 命令来查找两个文件之间的相同和不同部分
# comm 要求两个文件都是排序的
# $$ 在脚本中表示本脚本的进程号, 在这个地方用来建立和其它程序不冲突的临时文件
#
# comm 比较两个文件, 并输出三列,
# 第一列显示的是只属于第一个文件的内容
# 第二列显示的是只属于第二个文件的内容
# 第三列显示的是两个文件共有的内容
# comm 有三个参数 -1, -2, -3 分别代表不显示第一列, 不显示第二列, 不显示第三列
# 所以 comm -12 就表示只显示第三列, 也就是显示出两个文件相同的部分
# comm -23 表示只显示第一列, 就是显示出只属于第一个文件的部分
sed -n "/$TRIGGER/,/^$/p" < $ADM_DIR/packages/$PKGNAME | \
fgrep -v "FILE LIST:" | sort -u > $TMP/delete_list$$
# 将除了被卸载的包之外其它包的记录排序后存放到一个文件($TMP/required_list$$)中
# 用以找出被卸载的包中包含在其它包中的文件
cat_except $ADM_DIR/packages $PKGNAME | sort -u > $TMP/required_list$$
# $ADM_DIR/scripts 目录下存放是软件包安装时的 doinst.sh 脚本, 如果存在,
# 则从其中提取出符号链接列表, 排序并去除重复后存入 $TMP/del_link_list$$ 中
if [ -r $ADM_DIR/scripts/$PKGNAME ]; then
extract_links < $ADM_DIR/scripts/$PKGNAME | sort -u > $TMP/del_link_list$$
# 同样将要除了被卸载的包的脚本之外其它脚本中的语句通过 extract_links 提取出
# 符号链接列表, 排序并去除重复后存放入 $TMP/required_links$$
cat_except $ADM_DIR/scripts $PKGNAME | extract_links | \
sort -u > $TMP/required_links$$
# 将 required_list$$ required_links$$ 的内容合并, 排序, 并去除重复项,
# 存入 $TMP/required_list$$
mv $TMP/required_list$$ $TMP/required_files$$
sort -u $TMP/required_links$$ $TMP/required_files$$ > $TMP/required_list$$
# 将被卸载的包中存在于其它包内的文件/链接交给 keep_links 处理
# 因为它寻找的是被卸载的包中的链接, 而在其它包中, 同名的文件也许就不再是链接
# 而是一个常规文件了, 所以上面要把 required_list$$ 和 required_links$$ 合并
# 而这个地方要交给 keep_links, delete_links 而不是 keep_files,delete_files 来处理
comm -12 $TMP/del_link_list$$ $TMP/required_list$$ | keep_links
# 将被卸载的删除的包中不存在于其它包内的文件/链接交给 delete_links 处理
comm -23 $TMP/del_link_list$$ $TMP/required_list$$ | delete_links
else # 没有 $ADM_DIR/scripts/$PKGNAME 文件
# 与上面类似, 不过是在没有 $ADM_DIR/scripts/$PKGNAME 文件的情况下,
# 所以不需要用 cat_except, 而是直接用 cat
# 而且没有 $ADM_DIR/scripts/$PKGNAME 就表示不会从 doinst.sh 中处理出符号链接来
# 所以不需要处理符号链接
cat $ADM_DIR/scripts/* | extract_links | \
sort -u > $TMP/required_links$$
mv $TMP/required_list$$ $TMP/required_files$$
sort -u $TMP/required_links$$ $TMP/required_files$$ >$TMP/required_list$$
fi
# 对普通文件/目录, 用 keep_files 来处理
comm -12 $TMP/delete_list$$ $TMP/required_list$$ | keep_files
# 对于只存在于被卸载的包中的文件/目录, 调用 delete_file, delete_dir, delete_cats
# 来进行处理, 不明白为什么要调用 delete_cats, 难道 delete_files 和 delete_dirs
# 不能把所有的文件都删除
comm -23 $TMP/delete_list$$ $TMP/required_list$$ > $TMP/uniq_list$$
delete_files < $TMP/uniq_list$$
delete_dirs < $TMP/uniq_list$$
delete_cats < $TMP/uniq_list$$
# KEEP 不为 true 的时候, 把 $TMP 下的那些文件列表删除
# 这些列表分别是:
# 被卸载软件的文件列表, 符号链接列表
# 其它软件的文件列表, 符号链接列表
# 被卸载软件独有的文件/符号链接列表
# 其它软件的文件/符号链接列表
if [ ! "$KEEP" = "true" ]; then
rm -f $TMP/delete_list$$ $TMP/required_files$$ $TMP/uniq_list$$
rm -f $TMP/del_link_list$$ $TMP/required_links$$ $TMP/required_list$$
fi
# 如果设置了 PRESERVE 为 true, 那么把 $ADM_DIR/scripts/$PKGNAME 脚本重新以
# install/doinst.sh 的名字存放到 $PRES_DIR/$PKGNAME 下
if [ "$PRESERVE" = "true" ]; then
if [ -r $ADM_DIR/scripts/$PKGNAME ]; then
if [ ! -d "$PRES_DIR/$PKGNAME/install" ]; then
mkdir -p "$PRES_DIR/$PKGNAME/install"
fi
cp -p $ADM_DIR/scripts/$PKGNAME $PRES_DIR/$PKGNAME/install/doinst.sh
fi
fi
# 将记录文件存放到 $ADM_DIR/removed_packages 目录下
# 将脚本文件存放到 $ADM_DIR/removed_scripts 目录下
if [ ! "$WARN" = "true" ]; then
for DIR in $ADM_DIR/removed_packages $ADM_DIR/removed_scripts ; do
if [ ! -d $DIR ] ; then mkdir -p $DIR ; chmod 755 $DIR ; fi
done
mv $ADM_DIR/packages/$PKGNAME $ADM_DIR/removed_packages
if [ -r $ADM_DIR/scripts/$PKGNAME ]; then
mv $ADM_DIR/scripts/$PKGNAME $ADM_DIR/removed_scripts
fi
fi
else
echo "No such package: $ADM_DIR/packages/$PKGNAME. Can't remove."
fi
done
}
# 无参数, 则显示用法并退出程序
if [ "$#" = "0" ]; then
echo "Usage: `basename $0` [-copy] [-keep] [-preserve] [-warn] packagename ..."; exit 1
fi
# 死循环, 用来处理参数
# while : ; do 中的 : 是一个 sh 内置命令: 空命令, 它什么都不干, 只是返回 0
while : ; do
case "$1" in
-copy) WARN=true; PRESERVE=true; shift;; # 只是把文件保存到保留目录下, 不删除
-keep) KEEP=true; shift;; # KEEP 为 true 时, 会保留 $TMP 下的各种文件列表文件
-preserve) PRESERVE=true; shift;; # 保存包内容并删除软件
-warn) WARN=true; shift;; # 只显示相关信息, 不真正删除
# 非上面四种以之外任何以 - 开头的参数都是非法的, 显示用法并退出
-*) echo "Usage: `basename $0` [-copy] [-keep] [-preserve] [-warn] packagename ..."; exit 1;;
# 遇到第一个非 - 开头的参数, 或者是不再有参数时则退出参数处理, 剩下的部分都认为是软件
*) break
esac
done
# 根据 WARN 和 PRESERVE 的设定情况来显示相关的信息
if [ "$WARN" = "true" ]; then
echo "Only warning... not actually removing any files."
if [ "$PRESERVE" = "true" ]; then
echo "Package contents is copied to $PRES_DIR."
fi
echo "Here's what would be removed (and left behind) if you"
echo "removed the package(s):"
echo
else
if [ "$PRESERVE" = "true" ]; then
echo "Package contents is copied to $PRES_DIR."
fi
fi
# 调用 removed_packages 来处理剩下的参数
# 剩下的参数是由参数处理部分处理后的部分, 它们被认为是软件列表
remove_packages $*