Lane East 的 blog

一百年很短,一秒钟很长

slackware 包管理(pkgtools)分析 removepkg

2011-02-03 10:16

#!/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 $*

分类:

评论

  预览后可提交