slackware 包管理(pkgtools)分析 upgradepkg

2011-02-03 16:19

#!/bin/sh

# 函数: 显示帮助
usage() {
 cat << EOF

Usage: upgradepkg newpackage [newpackage2 ... ]
       upgradepkg oldpackage%newpackage [oldpackage2%newpackage2 ... ]

Upgradepkg upgrades a Slackware .tgz package from an older version to a
newer one.  It does this by INSTALLING the new package onto the system, and
then REMOVING any files from the old package that aren't in the new package.
If the old and new packages have the same name, a single argument is all that
is required.  If the packages have different names, supply the name of the
old package followed by a percent symbol (%), then the name of the new package.
Do not add any extra whitespace between pairs of old/new package names.

Before upgrading a package, save any configuration files (such as in /etc)
that you wish to keep.  Sometimes these will be preserved, but it depends on
the package structure.  If you want to force new versions of the config files
to be installed, remove the old ones manually prior to running upgradepkg.

To upgrade in a directory other than / (such as /mnt):

   ROOT=/mnt upgradepkg package.tgz

EOF
}

# 检查/设置临时目录
TMP=$ROOT/var/log/setup/tmp
# 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 $TMP
  chmod 700 $TMP # no need to leave it open
fi

# 此脚本需要将 umask 设置为 022
umask 022

# 如果定义了 ROOT 环境变量, 则将它 export
# 如果不 export 的话, 那么这个环境变量在此脚本的子进程中无效,
# 所以将其 export, 以便对 installpkg 和 removepkg 起作用
if [ -d "$ROOT" ]; then
  export ROOT
fi

# 无参数运行或者是使用了 --help 参数, 显示帮助信息
if [ "$1" = "" -o "$1" = "--help" -o "$1" = "-?" ]; then
  usage;
  exit 1;
fi

# 参数处理, 所有支持的参数必须在包名称前, 遇到第一个不支持的参数, 则认为后面的
# 都是包名称
while [ 0 ]; do
  if [ "$1" = "--no-paranoia" ]; then
    # no paranoia 模式, 默认模式下, upgradepkg 在升级完软件后还会再次用 installpkg
    # 来安装一次刚升级的软件, no paranoia 模式下不进行这一步处理
    NOT_PARANOID="true"
    shift 1
  elif [ "$1" = "--install-new" ]; then
    # 如果要升级的软件包并非是已安装的软件, 那么使用 installpkg 对其进行安装
    # 默认情况下是忽略这种软件的
    INSTALL_NEW="yes"
    shift 1
  elif [ "$1" = "--reinstall" ]; then
    # 升级版和原先安装的版本相同时重新安装
    # 默认情况下是忽略这种软件的
    REINSTALL="true"
    shift 1
  elif [ "$1" = "--verbose" -o "$1" = "-v" ]; then
    # 详细模式, 显示更多的信息
    VERBOSE="verbose"
    shift 1
  elif [ "$1" = "--dry-run" ]; then
    # 只显示哪些软件会被安装, 或者是升级, 而不实际执行这样的操作
    DRY_RUN="true"
    shift 1
  else # 没有以上几种参数, 退出参数处理循环
    break;
  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
}

ERRCODE=0

# 主循环
# 将参数处理循环处理后的参数作为包名称对(packagename 或者是 new%old)
# 来进行处理
while [ ! "$1" = "" ]; do

# 将 new%old 形式的参数分割开, 分别存入 NEW, OLD 变量
# 如果不是 new%old 的形式, 而只是 new 的形式, cut -f1 和 cut -f2 会输出相同的内容
# 后面会用 OLD 查找旧包, 从而把 OLD 变为真正的已安装的版本号,
# 所以不会造成新旧软件相同的误判
OLD=`echo $1 | cut -f 1 -d '%'`
NEW=`echo $1 | cut -f 2 -d '%'`
INCOMINGDIR=`dirname $NEW` # 新软件包的存放路径
OLD=`basename $OLD .tgz`
NEW=`basename $NEW .tgz`

# 取得旧的软件包的基础包名称
SHORT="`package_name $OLD`"
# 如果没有此基础包名称的记录文件, 则查找其它有此相同的基础包名称的包记录文件
# 这意味着哪怕你指定了错的旧包文件名称, 也无所谓, 实例:
#
# 假定你安装了 bash-3.1.017-i486-2 你想升级到 bash-3.2.0-i486-1, 那么你用
# upgradpkg bash-3.1.000-i486-1%bash-3.2.0-i486-1 来升级也无所谓,
# 这段代码会查找 $ROOT/var/log/packages/bash, 如果没有, 就查找
# $ROOT/var/log/packages/bash*, 然后在里面找到一个基础名称为 bash 的记录项, 把它当作旧软件名
# 虽然这个地方只是找到了一个, 但是这个只是为了开始输出信息使用的,
# 后面实际操作的部分是删除所有符合条件的旧包的
if [ ! -r $ROOT/var/log/packages/$OLD ]; then
  if ls $ROOT/var/log/packages/$SHORT* 1> /dev/null 2> /dev/null ; then
    for installed_package in $ROOT/var/log/packages/$SHORT* ; do
      if [ "`package_name $installed_package`" = "$SHORT" ]; then
        OLD="`basename $installed_package`"
        break
      fi
    done
  fi
fi

# 如果还找不到旧包的安装记录, 那么根据 INSTALL_NEW 的设定来决定是否安装
# 输出信息会根据 DRY_RUN 的设定而有所不同
if [ ! -r $ROOT/var/log/packages/$OLD ]; then
  if [ ! "$INSTALL_NEW" = "yes" ]; then
    if [ "$DRY_RUN" = "true" ]; then
      echo "$OLD would not be upgraded (no installed package named $SHORT)."
    else
      echo
      echo "Error:  there is no installed package named $OLD."
      echo "        (looking for $ROOT/var/log/packages/$OLD)"
      echo
    fi
    ERRCODE=1
  else # 使用了 --install-new 参数, 使用 installpkg 来安装新软件
    if [ "$DRY_RUN" = "true" ]; then
      echo "$NEW would be installed (new package)."
    else
      cat << EOF

+==============================================================================
| Installing new package $INCOMINGDIR/$NEW.tgz
+==============================================================================

EOF
      installpkg $INCOMINGDIR/$NEW.tgz
    fi
  fi
  # 忽略了这一个, 处理下一个
  shift 1
  continue;
elif [ ! -r "$INCOMINGDIR/$NEW.tgz" ]; then # 如果新软件包文件不存在, 给出提示
  if [ "$DRY_RUN" = "true" ]; then
    echo "$NEW incoming package not found (command line)."
  else
    echo
    echo "Error:  incoming package $INCOMINGDIR/$NEW.tgz not found."
    echo
  fi
  # 忽略了这一个, 处理下一个
  shift 1
  ERRCODE=1
  continue;
fi

# 没有 --reinstall 参数, 则比较新旧软件包的名称(长名称 name-version-arch-build)
# 如果两者相同, 则输出提示, 处理下一个包, 根据 DRY_RUN 设定情况输出有所不同
if [ ! "$REINSTALL" = "true" ]; then
  if [ "$OLD" = "$NEW" ]; then
    if [ "$DRY_RUN" = "true" ]; then
      echo "$NEW would be skipped (already installed)."
    else
      cat << EOF

+==============================================================================
| Skipping package $NEW (already installed)
+==============================================================================

EOF
    fi
    shift 1
    continue;
  fi
fi

TIMESTAMP=`date +%Y-%m-%d,%T` # 时间戳
SHORT="`package_name $OLD`"
if [ "$DRY_RUN" = "true" ]; then # DRY_RUN 模式, 只提示, 不操作
  echo -n "$NEW would upgrade: "
  for installed_package in $ROOT/var/log/packages/$SHORT* ; do
  if [ "`package_name $installed_package`" = "$SHORT" ]; then # 显示所有符合旧软件的基础名称的包
    echo -n "`basename $installed_package .tgz` "
  fi
  done
  echo
  shift 1
  continue
fi
# 在 $ROOT/var/log/packages 下面查找与要卸载的软件的基础软件名相同的包记录,
# 在其名称后加上 -upgraded-$TIMESTAMP, 以便后面统一通过此特征来进行删除
for installed_package in $ROOT/var/log/packages/$SHORT* ; do
  if [ "`package_name $installed_package`" = "$SHORT" ]; then
    mv $installed_package ${installed_package}-upgraded-$TIMESTAMP
  fi
done
# 与上面的部分相似, 这个地方处理 $ROOT/var/log/scripts 下面的脚本
for installed_script in $ROOT/var/log/scripts/$SHORT* ; do
  if [ "`package_name $installed_script`" = "$SHORT" ]; then
    if [ -r $installed_script ]; then
      mv $installed_script ${installed_script}-upgraded-$TIMESTAMP
    fi
  fi
done

cat << EOF

+==============================================================================
| Upgrading $OLD package using $INCOMINGDIR/$NEW.tgz
+==============================================================================

EOF

# 先安装新软件
# 这个地方的 if 语句写错了
# 这样写的话就表示 VERBOSE 模式下不显示 Pre-installing package $NEW... 了
# 而 VERBOSE 模式应该更详细
# 所以应该写成 if [ ! "VERBOSE" = "verbose" ]; then
if [ "$VERBOSE" = "verbose" ]; then
  installpkg $INCOMINGDIR/$NEW.tgz
  RETCODE=$?
else
  echo "Pre-installing package $NEW..."
  installpkg $INCOMINGDIR/$NEW.tgz 1> /dev/null
  RETCODE=$?
fi
# installpkg 返回错误码, 表示新软件无法安装, 显示提示信息, 处理下一个包
if [ ! $RETCODE = 0 ]; then
  echo "ERROR:  Package $INCOMINGDIR/$NEW.tgz did not install"
  echo "correctly.  You may need to reinstall your old package"
  echo "to avoid problems.  Make sure the new package is not"
  echo "corrupted."
  sleep 30
  # 忽略此软件包, 处理下一个
  shift 1
  continue;
fi

# 将找到的旧软件包全部删除, 如果是非 VERBOSE 模式, 则过滤一些输出
# 这个地方对 ROOT 是否目录做了判断, 其实上面一直接都没判断,
# 如果 ROOT 真的不是目录的话早就出问题了
if [ -d "$ROOT" ]; then
  ( cd $ROOT/var/log/packages
    for rempkg in *-$TIMESTAMP ; do
      if [ "$VERBOSE" = "verbose" ]; then
        ROOT=$ROOT removepkg $rempkg
      else
        ROOT=$ROOT removepkg $rempkg | grep -v "Skipping\." | grep -v "Removing files:"
      fi
    done
  )
else
  ( cd /var/log/packages
    for rempkg in *-$TIMESTAMP ; do
      if [ "$VERBOSE" = "verbose" ]; then
        removepkg $rempkg
      else
        removepkg $rempkg | grep -v "Skipping\." | grep -v "Removing files:"
      fi
    done
  )
fi
echo

# 非 no paranoia 模式, 再次使用 installpkg 来安装一次新的软件包
# 这段代码首次出现于 slackware 8.0 的 upgradepkg 中, 在 slackware 8.0 的 Changelog.txt
# 中有这样一段相关的说明:
#    Patch upgradepkg to "double install" packages by default.  Note that you
#    might get an error if you use upgradepkg to update itself, but the
#    installed package will be ok.  I wouldn't give upgradegpkg a long list
#    of things to upgrade that includes hdsetup.tgz, though, or it will stop
#    when it reaches that package.
if [ ! "$NOT_PARANOID" = "true" ]; then
  installpkg $INCOMINGDIR/$NEW.tgz
fi

echo "Package $OLD upgraded with new package $INCOMINGDIR/$NEW.tgz."
ERRCODE=0

# 处理下一个
shift 1

done

if [ ! "$DRY_RUN" = "true" ]; then
  echo
fi
exit $ERRCODE
Lane East

,

评论 [2]

slackware 包管理(pkgtools)分析 removepkg

2011-02-03 02: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 $*
Lane East

,

评论

slackware 包管理(pkgtools)分析 pkgtool

2011-02-02 20:37

pkgtool 是 pkgtools 包中较为(最为?)复杂的一个, 不光是功能复杂, 实现也复杂, 它有以下两种运行模式:

  • 命令行模式, 就是提供参数给 pkgtools, 不显示功能选择界面, 这种模式下, 只有软件安装的功能, 而没有“浏览”, “卸载”, “配置“功能
  • 交互模式, 直接运行 pkgtool, 这个时候使用 dialog 画出选择界面, 选择其中的功能来运行

交互模式下 pkgtool 除了安装软件之外, 还有“浏览”, “卸载”, “配置(也就是选择执行 $ADM_DIR/setup/setup.* 下面的一个配置脚本)“的功能, 此模式下的安装功能有三种位置可供选择:

  • 安装当前目录下的软件包
  • 从指定位置安装软件包
  • 从软盘安装软件包

在这三种位置中, 选择从软盘安装软件包时, SOURCE_MOUNTED 不设定, 所以会要求放入软盘

不管是命令行模式还是交互模式, pkgtools 的软件安装都为两种软件安装模式:

  • 普通模式, 它是直接安装一个目录下的软件包
  • 软件集模式, 它是安装某一个目录下的软件包, 并且在此目录下查找 install.end 文件, 如果此文件存在, 则表示一个软件集安装完成, 如果不存在, 则认为软件集存放在了不同的介质/目录, 将软件集名称后面加上编号, 继续安装, 比如安装软件集 a, 如果没找到 install.end, 则继续安装 a2, a3, 它通常有两种情况, 一种是 SOURCE_MOUNTED 未设定的情况, 这样的话会要求插入存储介质, 进行安装, 直到在某张盘中发现了 install.end, 就表示此软件集安装结束, 如果是安装多个软件集, 则还需要再次进行这样的操作, 另一个是 SOURCE_MOUNTED 已经设定成 always, 这时候认为软件包存放在一个目录($SOURCE_DIR)的子目录下, 同样, 如果这个子目录下没有 install.end, 则查找加了编号的子目录, 如安装 a 软件集, 则会先安装 $SOURCE_DIR/a 下的软件, 如果此目录下没有 install.end, 则会接着安装 $SOURCE_DIR/a2, $PACKAGE_DIR/a3 …如果是多个软件集, 则还要进行类似的操作

普通模式下, DISK_SETS 设置为 “disk”, 软件集模式下, DISK_SETS 设置为一个用 # 分割的列表, (理论上)不能有大写字母, 命令行模式的时候, 脚本会自动转换成小写字母, 而交互模式下的软盘安装时, 只是会提出要求, 并未有什么针对用户输入了大写字母的行动, 还有就是在 install_disk_set 函数的注释部分提到了这么一个要求

交互模式下的功能是搭配好了的, 选择了软盘安装, 则会要求选择一种软盘设备, 并且会提示放入软盘, 由本脚本来进行挂载, 但是在命令行模式下, 情况则自由了很多, 也复杂了很多, 因为可以用 -source_device 来指定设备, 所以就不一定是软盘设备了(但是挂载用的函数中使用了 msdos 文件系统来挂载), 甚至会出现指定了设备, 但是又没用到的情况(指定了设备, 同时又使用了 -source_mounted 参数)

另外, 在命令行模式下的软件集安装方式, 还可以用 $TMP/SeT* 进行额外设定:

  • SeTtagext: 此文件内容为 tagfile 的后缀, 如它的内容为 .txt 那么则用 tagfile.txt 为 tagfile
  • SeTtagpath: 此文件内容为 tagfile 的所在路径, 可以用来指定非标准的 tagfile 路径(标准的为 $SOURCE_DIR, 也就是所安装软件的软件包存放目录), 这个文件只有在没有 SeTtagext 的情况下起作用
  • SeTQUICK: 此文件内容不重要, 只要此文件存在, 则会尝试执行软件包存放目录中的 maketag 脚本, 再将此脚本产生的 SeTnewtag 存为 $TMP/tagfile, 以作为 tagfile 使用

中文注释的代码如下:

#!/bin/sh

SOURCE_DIR=/var/log/mount # 默认的软件包存放目录, 也是用来挂载设备的默认目录
ASK="tagfiles" # 默认用 tagfile 来判断是否安装
# 根据 /bin/chmod 和 /bin/chown 是否为符号链接来判断是否是在 rootdisk 下使用 busybox 的情况 {{{
if [ -L /bin/chmod -a -L /bin/chown ]; then
 # 用 rootdisk 的时候目标目录为 /mnt, 临时目录为 /mnt/var/log/setup/tmp
 TARGET_DIR=/mnt
 TMP=/mnt/var/log/setup/tmp
 if mount | grep "on /mnt" 1> /dev/null 2>&1 ; then
  true
 else
  # 如果 /mnt 上没挂载东西, 则给出提示, 并退出
  # 这是为了避免使用 rootdisk 维护系统时忘记挂载而导致的问题
  echo
  echo
  echo "You can't run pkgtool from the rootdisk until you've mounted your Linux"
  echo "partitions beneath /mnt. Here are some examples of this:"
  echo
  echo "If your root partition is /dev/hda1, and is using ext2fs, you would type:"
  echo "mount /dev/hda1 /mnt -t ext2"
  echo
  echo "Then, supposing your /usr partition is /dev/hda2, you must do this:"
  echo "mount /dev/hda2 /mnt/usr -t ext2"
  echo
  echo "Please mount your Linux partitions and then run pkgtool again."
  echo
  exit
 fi
else
 # 一般情况下运行 pkgtool, 目标目录为 /, 临时目录为 /var/log/setup/tmp
 TARGET_DIR=/
 TMP=/var/log/setup/tmp
fi
if [ ! -d $TMP ]; then
 mkdir -p $TMP
 chmod 700 $TMP
 fi
ADM_DIR=$TARGET_DIR/var/log
LOG=$TMP/PKGTOOL.REMOVED
# }}}

# 函数: 除去多余的空白 {{{
# 在 installpkg 中出现过
crunch() {
  while read FOO ; do
    echo $FOO
  done
}
# }}}

# 函数: 获取软件包的基础名称 {{{
# 将 foo-1.0.0-i386-1.tgz, foo-1.0.0-i386-1, foo.tgz, foo 等形式的名称统一转化为 foo
package_name() {
  # 此处用到了不同于 installpkg 中的获取包名称的方法, 更加简洁
  # 当然, 它仍然不能准确取得包含多于三个 "-" 的老式包名
  # 先将结尾的 .tgz 删除(其实这个地方应该用 s/\.tgz$//; 而不是 s/.tgz$//;)
  # 然后用替换的方式将尾部的的 -*-*-* 去除掉, 如果是旧式包名称, 那么没有匹配,
  # 也不会有问题.
  echo $1 | sed 's/.tgz$//;s/-[^-]*-[^-]*-[^-]*$//'
  return
  # 下面的部分是旧的获取软件名的方式, 因为上面的 return 不会被执行到
  STRING=`basename $1 .tgz`
  # Work out number of segments
  INDEX="`echo $STRING | tr -d -c -`"
  INDEX="`expr length $INDEX + 1`"
  # Check we have at least 4 segments
  if [ $INDEX -lt 4 ]; then
    # If we don't have four segments, return the old-style (or out of spec) package name:
    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
}
# }}}

# 函数: 删除多个软件包 {{{
# 将传递进来的参数作为软件包列表, 逐个删除
remove_packages() {
 for pkg_name in $*
 do
  if [ -r $ADM_DIR/packages/$pkg_name ]; then
   # dialog 的 --cr-wrap 参数能使 dialog 命令中的文本中的换行显示成换行,
   # 所以下面的 \n\ 其实是可以不要的
   # 如果 installpkg 中的 dialog 使用 --cr-wrap 参数,
   # 那么也同样不需要用 paste 来再每一行尾部加 \n 的
   dialog --title "PACKAGE REMOVAL IN PROGRESS" --cr-wrap --infobox \
"\nRemoving package $pkg_name.\n\
\n\
Since each file must be checked \
against the contents of every other installed package to avoid wiping out \
areas of overlap, this process can take quite some time. If you'd like to \
watch the progress, flip over to another virtual console and type:\n\
\n\
tail -f $TMP/PKGTOOL.REMOVED\n" 13 60
   # 调用 removepkg 来删除软件包
   # removepkg 会根据环境变量 ROOT 来判断要操作的"根"目录
   # 用在诸如使用维护盘启动, 真正的根目录并非 / 的情况
   # 另, pkgtools 中的"根"目录判断方式不统一, removepkg 和 upgradepkg 中使用
   # ROOT 环境变量, installpkg 中使用 -root 参数, 如果没有这个参数, 再使用 ROOT 环境变量
   export ROOT=$TARGET_DIR
   removepkg $pkg_name >> $LOG 2> /dev/null
  else
   echo "No such package: $pkg_name. Can't remove." >> $LOG
  fi
 done
}
# }}}

# 参数处理
if [ $# -gt 0 ]; then
# 命令行模式运行 {{{
 while [ $# -gt 0 ]; do
  case "$1" in
  "-sets")            # 指定要安装的软件集名称
   DISK_SETS=`echo $2 | tr "[A-Z]" "[a-z]"` ; shift 2 ;; # 软件集名称要求小写
  "-source_mounted")  # 不需要挂载
   SOURCE_MOUNTED="always" ; shift 1 ;;
  "-ignore_tagfiles") # 安装时不询问是否安装, 不管 tagfiles 里的设定是什么
   ASK="never" ; shift 1 ;;
  "-tagfile")         # 指定 tagfile
   USETAG=$2 ; shift 2 ;;
  "-source_dir")      # 指定软件包所存放的位置
   SOURCE_DIR=$2 ; shift 2 ;;
  "-target_dir")      # 指定安装的目标位置
   TARGET_DIR=$2
   ADM_DIR=$TARGET_DIR/var/log
   shift 2 ;;
  "-source_device")   # 指定软件包存放的设备, 本脚本认为设备的文件系统是 msdos
   SOURCE_DEVICE=$2 ; shift 2 ;;
  esac
 done
# }}}
else
# 无参数, 交互式运行 {{{
 # 用 dialog 显示界面, 让用户进行选择
 # CMD_START 表示无参数运行 pkgtool, 这种情况下, pkgtool 会用 dialog 来画出界面,
 # 本脚本最后会根据 CMD_START 的情况, 来决定退出时是不是调用
 # dialog --clear 来清除屏幕上的对话框
 CMD_START="true"
 # 无参数, 以交互方式运行, 用不到 SeT*, 删除
 rm -f $TMP/SeT*
 while [ 0 ]; do
  # 功能选择界面 {{{
  # 产生 17x75 大小的窗口, 选项框高度为 7, 里面有如下选项:
  # Current  安装当前目录下的软件包
  # Other    从其它位置安装软件包(从一个已经存在的目录安装)
  # Floppy   从软盘安装软件包
  # Remove   卸载软件
  # View     浏览已安装的软件所安装的文件
  # Setup    设置(再次选择执行 slackware 提供的设置脚本)
  # Exit     退出
  # 并将所选的项存入 $TMP/reply 文件中
  dialog --title "Slackware Package Tool (pkgtool version 12.0)" \
--menu "\nWelcome to the Slackware package tool.\n\
\nWhich option would you like?\n" 17 75 7 \
"Current" "Install packages from the current directory" \
"Other" "Install packages from some other directory" \
"Floppy" "Install packages from floppy disks" \
"Remove" "Remove packages that are currently installed" \
"View" "View the list of files contained in a package" \
"Setup" "Choose Slackware installation scripts to run again" \
"Exit" "Exit Pkgtool" 2> $TMP/reply
  if [ ! $? = 0 ]; then # 选择了 Cancel(取消) 按钮
   rm -f $TMP/reply
   dialog --clear
   exit
  fi
  REPLY="`cat $TMP/reply`"
  rm -f $TMP/reply
  if [ "$REPLY" = "Exit" ]; then # 选择了 Exit(退出) 按钮
   dialog --clear
   exit
  fi # }}}
  if [ "$REPLY" = "Setup" ]; then # 功能: 设置 {{{
    # 选择了 Setup, 根据 slackware 提供的设置脚本而产生选择框
    # 将命令放入 $TMP/setupscr, 再加入后面根据 slackware 提供的脚本的名称来生成的内容,
    # 形成完整的命令, 再用 . $TMP/setupscr 执行

    # --checklist 建立一个复选列表, 后面的文本为说明
    # 17 70 9 表示 对话框尺寸为 17x70, 选择框高 9, 超过的可以通过上下键选择
    # 后面的部分为选择项, 如果没有 --item-help 参数, 则 3 个为一组, 分别为:
    # 选项名, 选项说明, 选项状态, 选项状态为 "on" 时项默认为选中, 其它为不选中
    # 有 --item-help 参数时 4 个为一组, 第 4 个参数在左下角显示作为提示
    echo 'dialog --title "SELECT SYSTEM SETUP SCRIPTS" --item-help --checklist \
    "Please use the spacebar to select the setup scripts to run.  Hit enter when you \
are done selecting to run the scripts." 17 70 9 \' > $TMP/setupscr
    for script in $ADM_DIR/setup/setup.* ; do
      # slackware 提供的脚本中有一行含有 #BLURB 的行
      # #BLURB= 后的内容显示了脚本的作用, 可以用作说明
      # 这里取出这个说明
      BLURB=`grep '#BLURB' $script | cut -b8-`
      # 对空说明进行处理, 便于后面统一处理
      if [ "$BLURB" = "" ]; then
        BLURB="\"\""
      fi
      # slackware 提供的设置脚本名结构为: setup.编号.脚本名称
      # 去掉脚本名称中的 setup 的部分, 并加入说明, 状态(不选中),
      # 以及左下角的说明(同说明)
      echo " \"`basename $script | cut -f2- -d .`\" $BLURB \"no\" $BLURB \\" >> $TMP/setupscr
    done
    # 将选择放入 $TMP/return
    echo "2> $TMP/return" >> $TMP/setupscr
    . $TMP/setupscr
    if [ ! "`cat $TMP/return`" = "" ]; then
      # 执行每个选中的脚本:
      for script in `cat $TMP/rturn` ; do
        # 将 $TMP/return 中的每行的内容重新变成包含对应的脚本名称
        scrpath=$ADM_DIR/setup/setup.`echo $script | tr -d \"` # tr 删出脚本名称外的引号
        # 获得根文件系统的设备名, 用以传递给要执行的设定脚本
        rootdevice="`mount | head -n 1 | cut -f 1 -d ' '`"
        # 执行设定脚本
        ( COLOR=on ; cd $TARGET_DIR ; . $scrpath / $rootdevice )
      done
    fi
    rm -f $TMP/return $TMP/setupscr
    continue
  fi # 功能: 设置 }}}
  if [ "$REPLY" = "View" ]; then # 功能: 查看 {{{
   # 选择了 View
   # 显示出已安装软件包的列表, 选择其中一项则显示所安装的文件

   # 默认项目, 查看一个特定软件所安装的文件返回后所选中的项, 目前为空
   DEFITEM=""
   export DEFITEM
   # 此处同样是将命令输出重定向到一个文件($TMP/viewscr)中, 然后用 . 来执行它
   (
     echo 'dialog $DEFITEM --item-help --menu "Please select the package you wish to view." 17 68 10 \'
     FILES=`ls $ADM_DIR/packages`
     if [ -n "$FILES" ]; then
       cd $ADM_DIR/packages
       # 从软件包安装记录中读取出软件包描述, 并用 sed 处理成需要的结果
       # 所用到的 grep 参数解释:
       #     -Z:  用二进制 00 来分隔查找到的文件名与查找的内容, 而不是默认的 :
       #     -H:  在查找到的结果前面加上对应的文件名
       #     -m1: 找到一个结果就不再查找
       #     -A1: 显示出查找出的内容之后的一行内容
       #
       # 所以下面的 grep 表示从所有包的记录文件中找到 PACKAGE DESCRIPTION 行和它
       # 的下一行(软件的简单说明), 并且在前面符加上记录文件的文件名, 且使用二进
       # 制 00 来分隔文件名和内容.
       # grep 会使用 -- 来分隔不同的文件的结果, 所以结果就是类似下面这样:
       #     aaa_base-12.0.0-noarch-1\x00PACKAGE DESCRIPTION:
       #     aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       #     --
       #     aaa_elflibs-12.0.0-i486-3\x00PACKAGE DESCRIPTION:
       #     aaa_elflibs-12.0.0-i486-3\x00aaa_elflibs: aaa_elflibs (shared libraries needed by many programs)
       # \x00 表示二进制的 00
       # 由于最后一个结果的后面没有 --, 为了统一处理, 所以在后面加了个 echo; 形
       # 成三行一组的格式, 用于后面的 sed 的处理
       #
       # 后面的 sed 命令中 -n 参数表示不使用自动打印, 除非使用了 p 之类的命令
       # 除了常见的 s 命令之外, 下面的 sed 命令中还使用了 n, h, x 命令
       # 它们牵涉到 sed 的两个空间, 一个叫模式空间(pattern space),
       # 一个叫存储空间(hold space), 模式空间是用来存储当前行的内容的, 而存储空
       # 间是用来临时保存内容的:
       #     n   把下一行的内容读取到模式空间
       #     h   把模式空间的内容放入存储空间
       #     x   交换模式空间和存储空间的内容
       # sed 的命令使用换行或者是 ; 来分隔, 下面的 sed 命令用来处理 grep 产生的
       # 三行一组的内容, 用 aaa_base 的那一组为例:
       # h  把首行 aaa_base-12.0.0-noarch-1\x00PACKAGE DESCRIPTION: 放入存储空间
       # n  将下一行放入模式空间. 其实由于首行的部分对于我们来说是没有必要的, 所
       #    以第一个 h 命令是可以省略的.
       # /\x00/{h;n;};x;s/  */ /g  将含有 \x00 的行(现在是第二行)进行处理, {h;n;}
       #    是将两个命令合成一组, 表示模式空间的内容中如果有 \x00 的话, 就执行这
       #    两个命令, 如果没有, 就不执行, 其实由于前面要求 grep 使用 \x00 来分隔
       #    文件名和匹配的内容, 所以这里可以直接写 h;n, 这两个命令执行完之后, 
       #    模式空间的内容就是 --, 也就是三行一组的最后一行, 而存储空间中的内容
       #    是真正需要的内容:
       #    aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       # x  将存储空间中需要的内容放入模式空间, 把模式空间的内容扔到存储空间中,
       #    这样就能开始真正的处理了
       # s/  */ /g  将连续的多个空间处理为一个空格
       # s/ $//  将行尾空格去掉(现在不存在连续的多个空格了)
       # s/[\"`$]/\\&/g  将有特殊含义的字符转义, 使其能正确输出, 这里的 & 表示
       #    前面的匹配内容
       # s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" "View information about package \1" \\/
       #    此命令中的 \(.*\)\x00 把二进制 00 前面的部分当作 \1, 后面的部分一直
       #    到 : 的部分是可选的, 所以使用 \?, 表示可以有也可以没有, 由于使用了括
       #    号, 所以 \2 是这部分内容, 后面的空格和*用来忽略空格, 剩下的部分其实
       #    是真正的软件包说明, 放入 \3. 所以在我们的例子中, 这一行是把
       #    aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       #    变化为(下面三行为一行, 太长, 才折成三行写):
       #    "aaa_base-12.0.0-noarch-1" 
       #    "aaa_base (Basic Linux filesystem package)" 
       #    "View information about aaa_base-12.0.0-noarch-1" \
       # 这其实就是 dialog 的 --item-help --menu 参数所需要的一组内容:
       #   选项, 说明, 左下角的提示
       { grep '^PACKAGE DESCRIPTION:$' -Z -H -m1 -A1 $FILES; echo; } \
       | sed -n 'h;n;/\x00/{h;n;};x;s/  */ /g;s/ $//;s/[\"`$]/\\&/g
           s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" "View information about package \1" \\/;p'
     fi
     # 所选项放入 $TMP/return 中
     echo "2> $TMP/return"
   ) > $TMP/viewscr
   while [ 0 ]; do
    . $TMP/viewscr
    if [ ! "`cat $TMP/return`" = "" ]; then # 选择的项目不为空
     DEFITEM="--default-item `cat $TMP/return`"  # 设置默认项, 用于显示查看列表
                                                 # 由于前面形成的 $TMP/viewscr 中
                                                 # 使用了 $DEFITEM, 并且是在单引号
                                                 # 中的, 所以每次执行 $TMP/viewscr
                                                 # 的时候都会根据当前的 DEFITEM
                                                 # 设定来决定不同的默认项
     # 显示所安装的文件
     dialog --title "CONTENTS OF PACKAGE: `cat $TMP/return`" --no-shadow --textbox "$ADM_DIR/packages/`cat $TMP/return`" \
     0 0 2> /dev/null
    else # 没选择, 退出
     break
    fi
   done
   # 清除临时文件
   rm -f $TMP/return $TMP/viewscr $TMP/tmpmsg
   # 防止有问题的包改变 / 和 /tmp 的权限
   chmod 755 /
   chmod 1777 /tmp
   continue
  fi # 功能: 查看 }}}
  if [ "$REPLY" = "Remove" ]; then # 功能: 删除 {{{
  # 选择了 Remove
   (
     cat << EOF
dialog --title "SELECT PACKAGES TO REMOVE" --item-help --checklist \
"Please select the \
packages you wish to Remove. Use the \
spacebar to select packages to delete, and the UP/DOWN arrow keys to \
scroll up and down through the entire list." 20 75 11 \\
EOF
     # 下面的部分和 View 的部分类似, 只是改变了左下角提示
     # 并且因为一次可以删除多个软件, 所以使用了 --checklist 所以就多出个 off
     # 用来设定默认为不选择
     FILES=`ls $ADM_DIR/packages`
     if [ -n "$FILES" ]; then
       cd $ADM_DIR/packages
       { grep '^PACKAGE DESCRIPTION:$' -Z -H -m1 -A1 $FILES; echo; } \
       | sed -n 'h;n;/\x00/{h;n;};x;s/  */ /g;s/ $//;s/[\"`$]/\\&/g
           s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" off "Select\/Unselect removing package \1" \\/;p'
     fi
     echo "2> $TMP/return"
   ) > $TMP/rmscript
   # 删除上次的记录文件
   if [ -L $LOG -o -r $LOG ]; then
     rm -f $LOG
   fi
   # 生成一个空的记录文件
   cat /dev/null > $LOG
   chmod 600 $LOG
   chmod 700 $TMP/rmscript
   export ADM_DIR;
   $TMP/rmscript
   # 调用 remove_packages 来删除所有选中了的软件包
   # 其中的 tr -d "\042" 用来去掉选择的包名称外的引号, 在 $REPLY = "Setup" 中也有
   # 个类似的功能, 用的是 tr -d \"
   remove_packages `cat $TMP/return | tr -d "\042"`
   if [ "`cat $TMP/PKGTOOL.REMOVED`" = "" ]; then
    rm -f $TMP/PKGTOOL.REMOVED
    dialog --title "NO PACKAGES REMOVED" --msgbox "Hit OK to return \
to the main menu." 5 40
   else
    dialog --title "PACKAGE REMOVAL COMPLETE" --msgbox "The packages have \
been removed. A complete log of the files that were removed has been created \
in $TMP: PKGTOOL.REMOVED." 0 0
   fi
   rm -f $TMP/rmscript $TMP/return $TMP/tmpmsg $TMP/SeT*
   chmod 755 /
   chmod 1777 /tmp # 功能: 删除 }}}
  elif [ "$REPLY" = "Floppy" ]; then # 功能: 从软盘安装 {{{
   # 选择软盘设备
   dialog --title "SELECT FLOPPY DRIVE" --menu "Which floppy drive would \
you like to install from?" \
11 70 4 \
"/dev/fd0u1440" "1.44 MB first floppy drive" \
"/dev/fd1u1440" "1.44 MB second floppy drive" \
"/dev/fd0h1200" "1.2 MB first floppy drive" \
"/dev/fd1h1200" "1.2 MB second floppy drive" 2> $TMP/wdrive
   if [ $? = 1 ]; then
    dialog --clear
    exit # 用户选择了取消, 直接退出整个 pkgtool 命令
         # 这个地方我觉得用 continue 来返回功能选择界面更合适, 毕竟可以用户可能
         # 只是误选了功能而已
   fi
   SOURCE_DEVICE="`cat $TMP/wdrive`"
   rm -f $TMP/wdrive
   # 输入软件集的名称, 使用空格来分隔, 后面会把这个地方的空格处理成 #
   cat << EOF > $TMP/tmpmsg

Enter the names of any disk sets you would like to install.
Separate the sets with a space, like this: a b oi x

To install packages from one disk, hit [enter] without typing
anything.

EOF
   dialog --title "SOFTWARE SELECTION" --inputbox "`cat $TMP/tmpmsg`" 13 70 2> $TMP/sets 
   DISK_SETS="`cat $TMP/sets`"
   rm -f $TMP/sets
   if [ "$DISK_SETS" = "" ]; then # 未输入软件集, 将 DISK_SETS 设置为 disk
    DISK_SETS="disk"
   else
    # 将原先输入的用空格分割的内容用 # 来分割
    DISK_SETS=`echo $DISK_SETS | sed 's/ /#/g'`
    DISK_SETS="#$DISK_SETS" # 在开始添加上 #
   fi
   break; # 功能: 从软盘安装 }}}
  elif [ "$REPLY" = "Other" ]; then # 功能: 从指定位置安装 {{{
   dialog --title "SELECT SOURCE DIRECTORY" --inputbox "Please enter the name of the directory that you wish to \
install packages from:" 10 50 2> $TMP/pkgdir
   if [ $? = 1 ]; then
    rm -f $TMP/pkgdir $TMP/SeT*
    dialog --clear
    exit # 同软盘安装模式中一样, 用户选择了取消, 我觉得用 continue 更合理
   fi
   SOURCE_DIR="`cat $TMP/pkgdir`"
   SOURCE_MOUNTED="always" # 将 SOURCE_MOUNTED 设置为 always, mount_the_source
                           # 函数就不会再挂载安装介质了
   DISK_SETS="disk"
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   rm -f $TMP/pkgdir
   if [ ! -d $SOURCE_DIR ]; then
    dialog --title "DIRECTORY NOT FOUND" --msgbox "The directory you want to \
install from ($SOURCE_DIR) \
does not seem to exist. Please check the directory and then try again." \
10 50
    dialog --clear
    exit # 输入的路径不是一个目录, 则退出整个 pkgtool 命令
         # 同样, 我觉得用 continue 更合理, 因为用户可能是输入错误, 回到功能选择
         # 界面会更方便一些
   fi
   break; # 功能: 从指定位置安装 }}}
  else # 功能: 从当前目录安装 {{{
   SOURCE_MOUNTED="always" # 无需挂载
   SOURCE_DIR="$PWD"
   DISK_SETS="disk"
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   break;
  fi # 功能: 从当前目录安装 }}}
 done
# }}}
fi
if [ "$DISK_SETS" = "disk" ]; then # DISK_SETS 为 disk 的时候将 ASK 设置为 always
                                   # 意味着普通安装模式下会不停地问是否安装的, 真恐怖
 ASK="always"
fi

# 函数: 挂载软件存储介质 {{{
# 将 $SOURCE_DEVICE 挂载到 $SOURCE_DIR
mount_the_source() {
 if [ "$SOURCE_MOUNTED" = "always" ]; then # 不需要挂载
  # 检查目录是否存在
  if [ ! -d $SOURCE_DIR ]; then
   cat << EOF > $TMP/tmpmsg

Your source device cannot be accessed properly.

Please be sure that it is mounted on $SOURCE_DIR,
and that the Slackware disks are found in subdirectories 
of $SOURCE_DIR like specified.

EOF
   dialog --title "MOUNT ERROR" --msgbox "`cat $TMP/tmpmsg`" 11 67
   rm -f $TMP/tmpmsg
   exit 1;
  fi
  return 0;
 fi # End if [ "$SOURCE_MOUNTED" = "always" ]
 # 如果 SOURCE_MOUNTED 不为 always, 提示放入软盘
 dialog --title "INSERT DISK" --menu "Please insert disk $1 and \
press ENTER to continue." \
11 50 3 \
"Continue" "Continue with the installation" \
"Skip" "Skip the current disk series" \
"Quit" "Abort the installation process" 2> $TMP/reply
 if [ ! $? = 0 ]; then
  REPLY="Quit"
 else
  REPLY="`cat $TMP/reply`"
 fi
 rm -f $TMP/reply
 if [ "$REPLY" = "Skip" ]; then
  return 1;
 fi
 if [ "$REPLY" = "Quit" ]; then
   dialog --title "ABORTING" --msgbox "Aborting software installation." 5 50
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   exit 1;
 fi;
 # 挂载设备, 如果不成功则重试, 直到成功或者用户选择放弃
 go_on=y
 not_successfull_mounted=1
 while [ "$go_on" = y -a "$not_successfull_mounted" = 1 ]; do
  mount -r -t msdos $SOURCE_DEVICE $SOURCE_DIR
  not_successfull_mounted=$?
  if [ "$not_successfull_mounted" = 1 ]; then
   mount_answer=x
   while [ "$mount_answer" != "y" -a "$mount_answer" != "q" ] ; do
    dialog --title "MOUNT PROBLEM" --menu "Media was not successfully \
mounted! Do you want to \
retry, or quit?" 10 60 2 \
"Yes" "Try to mount the disk again" \
"No" "No, abort." 2> $TMP/mntans
    mount_answer="`cat $TMP/mntans`"
    rm -f $TMP/mntans
    if [ "$mount_answer" = "Yes" ]; then
     mount_answer="y"
    else
     mount_answer="q"
    fi
   done
   go_on=$mount_answer
  fi
 done
 test $not_successfull_mounted = 0
}
# }}}

# 函数: 卸载(umount)设备 {{{
# SOURCE_MOUNTED 为 always 的时候不卸载
umount_the_source() {
 if [ ! "$SOURCE_MOUNTED" = "always" ]; then
  umount $SOURCE_DEVICE 1> /dev/null 2>&1
 fi;
}
# }}}

# 函数: 安装 {{{
install_disk() {
 # 挂载安装介质, 如果无法挂载则退出(无需挂载, 并且软件包存放的目录正常时不退出)
 mount_the_source $1
 if [ $? = 1 ]; then
  umount_the_source;
  return 1;
 fi
 CURRENT_DISK_NAME="$1"
 PACKAGE_DIR=$SOURCE_DIR
 # 无需挂载, 并且是软件集安装方式的时候 PACKAGE_DIR 设定为 $PACKAGE_DIR/$1
 if [ "$SOURCE_MOUNTED" = "always" -a ! "$DISK_SETS" = "disk" ]; then
   PACKAGE_DIR=$PACKAGE_DIR/$1
 fi

 # 目录不存在或者是目录中没有 .tgz 文件, 退出 {{{
 # 这个返回值会影响到软件集的安装, 如果用户只是插错了软盘, 由于这里的 return 1,
 # install_disk_set 会认为是软件集已经安装结束
 if [ ! -d $PACKAGE_DIR ]; then
  return 1
 fi
 if ls $PACKAGE_DIR/*.tgz 1> /dev/null 2> /dev/null ; then
  true
 else
  return 1
 fi
 # }}}

 # tagfile 的处理
 touch $TMP/tagfile
 if [ ! "$DISK_SETS" = "disk" ]; then # 软件集安装模式
  if [ -r $TMP/SeTtagext ]; then
   # 有 SeTtagext, 使用其内容为 tagfile 文件名的后缀 {{{
   if [ -r $PACKAGE_DIR/tagfile`cat $TMP/SeTtagext` ]; then
    cat $PACKAGE_DIR/tagfile`cat $TMP/SeTtagext` >> $TMP/tagfile
   else # 加了后缀的 tagfile 不存在, 使用原始的名称
    if [ -r $PACKAGE_DIR/tagfile ]; then
     cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
    fi
   fi
   # }}}
  elif [ -r $TMP/SeTtagpath ]; then
   # 使用 SeTtagpath 中设定的目录作为 tagfile 的存放目录 {{{
   custom_path=`cat $TMP/SeTtagpath`
   short_path=`basename $PACKAGE_DIR`

   # 设定的目录中有 tagfile, 使用它
   if [ -r $custom_path/$short_path/tagfile ]; then
    cat $custom_path/$short_path/tagfile >> $TMP/tagfile

   else # 指定目录中没有 tagfile, 使用原始位置的
    if [ -r $PACKAGE_DIR/tagfile ]; then
     cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
    fi
   fi
   # }}}
  elif [ -r $PACKAGE_DIR/tagfile ]; then
   # 没相关设定, 直接使用原始位置原始文件的内容
   cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
  fi

  # 命令行模式, 设定了 SeTQUICK, 进入 QUICK 模式 {{{
  # QUICK 模式: 使用脚本来生成 tagfile
  if [ -r $TMP/SeTQUICK -a -r $PACKAGE_DIR/maketag ]; then
   # MAKETAG 环境变量用来设定 maketag 脚本的名称, 如果此变量已设定
   # 并且 $PACKAGE_DIR/$MAKETAG 存在, 则执行该脚本
   # 否则才执行默认的 $PACKAGE_DIR/maketag
   if [ ! "$MAKETAG" = "" -a -r $PACKAGE_DIR/$MAKETAG ]; then # use alternate maketag
    sh $PACKAGE_DIR/$MAKETAG
   else
    sh $PACKAGE_DIR/maketag
   fi
   if [ -r $TMP/SeTnewtag ]; then
    mv $TMP/SeTnewtag $TMP/tagfile
   fi
  fi
  # }}}

  # 重设 tagfile 的权限, 防止恶意修改
  if [ -r $TMP/tagfile ]; then
   chmod 600 $TMP/tagfile
  fi

 fi #  ! "$DISK_SETS" = "disk"

 # 如果用户在命令行指定过 tagfile, 则把这个文件内容给覆盖到 $TMP/tagfile 里面
 # 真狠, 上面忙了半天就全部作废- -U
 if [ ! "$USETAG" = "" ]; then
   cat $USETAG > $TMP/tagfile
 fi

 # 如果存在目录(不是文件夹那种目录, 而是像书目那种目录)文件, 则根据这个目录文件来
 # 检查是否缺少包或者有多出来的包
 # 这个地方的检查其实很麻烦的, 因为它是对每一个包进行处理的
 # 也就是说, 少一个或者多一个就会弹出一次对话框,
 # 如果多或者少的包数量很大的话, 会很烦人的
 #
 # 这里用到的目录文件的格式不太清楚, 也没找到相关的东西, 所以不太好分析
 if [ "$1" = "single_disk" -o -r $PACKAGE_DIR/disk$1 -o -r $PACKAGE_DIR/package-list.txt ]; then
  if [ -r $PACKAGE_DIR/package-list.txt ]; then
   CATALOG_FILE=$PACKAGE_DIR/package-list.txt
  else
   CATALOG_FILE=`basename $PACKAGE_DIR/disk*`;
  fi
  if [ -r $PACKAGE_DIR/$CATALOG_FILE -a ! -d $PACKAGE_DIR/$CATALOG_FILE ]; then
   if grep CONTENTS: $PACKAGE_DIR/$CATALOG_FILE 1> /dev/null 2>&1 ; then
    # 检查是否缺少软件包 {{{
    for PKGTEST in `grep "^CONTENTS:" $PACKAGE_DIR/$CATALOG_FILE | cut -f2- -d : 2> /dev/null` ; do
     # 不是严格的检查, 比如缺少了 emacs, 但是却有 emacs-nox, 就会出现误判
     if ls $PACKAGE_DIR/$PKGTEST*.tgz 1> /dev/null 2> /dev/null ; then
      true
     else
      cat << EOF > $TMP/tmpmsg

WARNING!!!

While looking through your index file ($CATALOG_FILE),
I noticed that you might be missing a package:

$PKGTEST-\*-\*-\*.tgz

that is supposed to be on this disk (disk $1). You may go
on with the installation if you wish, but if this is a 
crucial file I'm making no promises that your machine will
boot.

EOF
      dialog --title "FILE MISSING FROM YOUR DISK" --msgbox \
"`cat $TMP/tmpmsg`" 17 67
     fi
    done # }}}
    # 检查是否有多出来的包 {{{
    ALLOWED="`grep CONTENTS: $PACKAGE_DIR/$CATALOG_FILE | cut -b10- 2> /dev/null`" 
    for PACKAGE_FILENAME in $PACKAGE_DIR/*.tgz; do
     BASE="`basename $PACKAGE_FILENAME .tgz`"
     BASE="`package_name $BASE`"
     if echo $ALLOWED | grep $BASE 1> /dev/null 2>&1 ; then
      true
     else
      cat << EOF > $TMP/tmpmsg

WARNING!!!

While looking through your index file ($CATALOG_FILE),
I noticed that you have this extra package:

($BASE.tgz) 

that I don't recognize. Please be sure this package is
really supposed to be here, and is not left over from an
old version of Slackware. Sometimes this can happen at the 
archive sites.

EOF
      dialog --title "EXTRA FILE FOUND ON YOUR DISK" \
--msgbox "`cat $TMP/tmpmsg`" 17 67 
      rm -f $TMP/tmpmsg
     fi
    done # }}}
   fi
  fi
 fi

 # 安装文件
 for PACKAGE_FILENAME in $PACKAGE_DIR/*.tgz; do
  # 如果 $PACKAGE_FILENAME 下没有 .tgz 文件, 则跳过
  # 既是跳过这一次, 也是跳过所有, 因为如果没有 .tgz 文件, 上面的 for .. in ..; do
  # 中只会有一项
  # 其实上面已经检查过目录下是否存在 .tgz 文件了
  if [ "$PACKAGE_FILENAME" = "$PACKAGE_DIR/*.tgz" ]; then
   continue;
  fi
  if [ "$ASK" = "never" ]; then # 不询问, 直接安装, 用 -infobox 作参数调用 installpkg
   installpkg -root $TARGET_DIR -infobox -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  elif [ "$ASK" = "tagfiles" ]; then # 直接根据 tagfile 的设定来决定是否安装
   installpkg -root $TARGET_DIR -menu -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  else # ASK 为 always, 除了使用 tagfile 之外, 还会询问是否安装
   installpkg -root $TARGET_DIR -menu -ask -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  fi
  # installpkg 返回 99 表示用户在安装软件对话框上选择退出(Quit), 而不是取消(Cancel)
  # 退出安装过程, 剩余的软件包也不再安装, 对软件集的安装有同样的效果
  if [ "$ERROR" = "99" ]; then
   umount_the_source;
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   exit 1;
   fi
 done
 # 检查是否存在 install.end, 用来判断一个软件集是否安装完毕
 # 安装完毕则返回 1, install_disk_set 中会使用这个返回值
 OUTTAHERE="false"
 if [ -r $PACKAGE_DIR/install.end ]; then
  OUTTAHERE="true"
 fi
 umount_the_source; # 根据需要卸载设备
 if [ "$OUTTAHERE" = "true" ]; then
  return 1;
 fi
}
# }}}

# 函数: 安装软件集合 {{{
# 只接收一个参数, 要求是小写字母
install_disk_set() {
 SERIES_NAME=$1
 CURRENT_DISK_NUMBER="1";
 while [ 0 ]; do
  # 一个包集合不一定在一个盘上, 开始的一个盘不加数字后缀, 之后数字后缀递增
  # 用在软盘作为安装介质的时代的, 比如包集合 a, 第一张软盘上的目录是 a,
  # 之后分别是 a2, a3 之类的
  if [ $CURRENT_DISK_NUMBER = 1 ]; then
    DISKTOINSTALL=$SERIES_NAME
  else
    DISKTOINSTALL=$SERIES_NAME$CURRENT_DISK_NUMBER
  fi
  install_disk $DISKTOINSTALL
  # 当 install_disk 发现软件包存储目录下有 install.end 的时候, 表示这个软件集
  # 已经是最后一个盘了, 于是返回非 0, 表示软件集合安装完成
  if [ ! $? = 0 ]; then
   return 0;
  fi
  # 磁盘号递增
  CURRENT_DISK_NUMBER=`expr $CURRENT_DISK_NUMBER + 1`
 done;
}
# }}}

# /* main() */ ;)
if [ "$DISK_SETS" = "disk" ]; then # 非软件集安装模式
 install_disk single_disk;
 ASK="always"
else # 软件集安装模式 {{{
 touch $TMP/tagfile
 chmod 600 $TMP/tagfile
 # 如果 DISK_SETS 包含有 a 软件集, 则把 a 单独处理, 不明白为什么
 # 而且如果 DISK_SETS 里面 a 是最后一个的话, 是不会产生 #a# 这样的情况的,
 # 因为前面的部分是把用空格分割的软件集中的空格换成 #, 然后在开始加上 # 形成用
 # # 分割的 DISK_SETS, 但是并没有在最后加上 #, 这样的话, DISK_SETS 的最后是 #a,
 # 而不会出现 #a#
 if echo $DISK_SETS | grep "#a#" 1> /dev/null 2>&1; then
  A_IS_NEEDED="true"
 else
  A_IS_NEEDED="false"
 fi
 while [ 0 ];
 do
  while [ 0 ]; # 将 DISK_SETS 中开头部分的 # 去掉, 此处考虑了 多个 # 的情况
  do
   if [ "`echo $DISK_SETS | cut -b1`" = "#" ]; then
    DISK_SETS="`echo $DISK_SETS | cut -b2-`"
   else
    break;
   fi
  done
  # 如果前面的 DISK_SETS 中包含 a 软件集, 在此单独处理
  if [ "$A_IS_NEEDED" = "true" ]; then
   cat << EOF > $TMP/tmpmsg

--- Installing package series ==>a<==

EOF
   dialog --infobox "`cat $TMP/tmpmsg`" 5 45
   sleep 1
   rm -f $TMP/tmpmsg
   # a 后面的 ; 不需要, 难道本脚本的编写者是个 c 语言的开发者? :-)
   # 下面还有些类似的
   install_disk_set a;
   A_IS_NEEDED="false"
  fi
  count="1"
  if [ "`echo $DISK_SETS | cut -b$count`" = "" ]; then
   break; # DISK_SETS 的第一个字节为空了, 说明 DISK_SETS 已经被处理完了, 结束
  else
   # 这个地方是用 count 来计算从开始到下一个 # 或者是末尾有多少字节数,
   # 然后就可以用 cut -b1-$count 来得到 DISK_SETS 中下一个软件集的名称了
   # 这种处理方式并不算好, 完全可以用 cut -d# -f<N> 来获取 diskset(<N>为数字)
   # 只要用:
   #   sed 's/^#\{1,\}//;s/#\{2,\}/#/g'
   # 把开头的 # 去掉, 避免第一次 cut 获取到空字串, 再把多个连续的 # 改为单独的 #
   # 避免 cut 获取到空字串, 就能以 cut 获取到空字串为结束的标志了.
   # 这样代码应该会简单很多.
   count="2"
   while [ 0 ]; do
    if [ "`echo $DISK_SETS | cut -b$count`" = "" -o "`echo $DISK_SETS | cut -b$count`" = "#" ]; then
     count="`expr $count - 1`"
     break;
    else
     count="`expr $count + 1`"
    fi
   done
  fi
  # 将下一个要安装的软件集名称存放到 diskset 中
  diskset="`echo $DISK_SETS | cut -b1-$count`"
  # 把已经获取的软件集名称从 DISK_SETS 中去除,
  # 因为 cut -b$count- 是包含第 $count 的字符的, 所以要把 count 加上 1
  # 把上一个软件集的最后一个字母去掉
  count="`expr $count + 1`"
  DISK_SETS="`echo $DISK_SETS | cut -b$count-`"
  if [ "$diskset" = "a" ]; then
   continue; # 前面已经单独处理过 a 软件集了
  fi
  # 安装其它的软件集
  cat << EOF > $TMP/tmpmsg

Installing package series ==>$diskset<==

EOF
  dialog --infobox "`cat $TMP/tmpmsg`" 5 45
  sleep 1
  rm -f $TMP/tmpmsg
  install_disk_set $diskset;
 done
fi # }}}

# 清理
if [ "$DISK_SETS" = "disk" -o "$CMD_START" = "true" ]; then
 # 如果不是软件集的安装方式, 或者是交互方式安装的
 # 则清理 $TMP/tagfile
 # 并使用 dialog --clear 来清除由 dialog 而产生的对话框在屏幕上的残留
 # 这样的话, 不是意味着软件集的安装方式下 tagfile 不会被删除了?
 if [ -r $TMP/tagfile ]; then
  rm $TMP/tagfile
 fi
 dialog --clear
fi
chmod 755 $TARGET_DIR $TARGET_DIR/var $TARGET_DIR/usr
chmod 1777 $TARGET_DIR/tmp

# vim:fdm=marker:ft=sh:

注 pkgtool 的代码比较复杂, 所以在里面加上了 {{{ 和 }}} 这样的标记用来让 vim 进行代码折叠

Lane East

,

评论

slackware 包管理(pkgtools)分析 makepkg

2010-04-24 09:28

makepkg 是 slackware 中用来制作软件包的工具, 中文注释的代码如下:

 #!/bin/sh

# 检查 tar 的版本
TAR=tar-1.13
umask 022
$TAR --help 1> /dev/null 2> /dev/null
if [ ! $? = 0 ]; then
  TAR=tar
fi
if [ ! "`LC_MESSAGES=C $TAR --version`" = "tar (GNU tar) 1.13

Copyright (C) 1988, 92,93,94,95,96,97,98, 1999 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Written by John Gilmore and Jay Fenlason." ]; then
  echo "WARNING: pkgtools are unstable with tar > 1.13."
  echo "         You should provide a \"tar-1.13\" in your \$PATH."
  sleep 5
fi

# 这个函数读取一个文件, 将文件中记录的符号链接及对应的目标逐行读取出来,
# 以产生建立符号链接的命令
# 所读取的文件的内容格式为: 链接名 -> 链接目标名
# 详细情况可以参考后面使用此函数的部分.
make_install_script() {
  # 逐行处理
  COUNT=1
  LINE="`sed -n "$COUNT p" $1`"
  while [ ! "$LINE" = "" ]; do
   # 取出链接名, 得到其所在目录(LINKGOESIN)
   # 这个地方并没有考虑到一个比较少见的情况:
   # 如果一个链接名中含有空格,
   # 那么这里的 LINKGOESIN 和下面的 LINKNAMEIS, LINKPOINTSTO 都会不正确
   LINKGOESIN="`echo "$LINE" | cut -f 1 -d " "`" 
   LINKGOESIN="`dirname $LINKGOESIN`" 
   # 取出链接名, 得到其文件名(LINKNAMEIS)
   LINKNAMEIS="`echo "$LINE" | cut -f 1 -d ' '`"
   LINKNAMEIS="`basename "$LINKNAMEIS"`"
   # 获得链接的目标
   LINKPOINTSTO="`echo "$LINE" | cut -f 3 -d ' '`"
   # 生成用于产生符号链接的 shell 命令, 以便存入 install/doinst.sh
   echo "( cd $LINKGOESIN ; rm -rf $LINKNAMEIS )"
   echo "( cd $LINKGOESIN ; ln -sf $LINKPOINTSTO $LINKNAMEIS )"
   # 处理下一行
   COUNT=`expr $COUNT + 1`
   LINE="`sed -n "$COUNT p" $1`"
  done
}

# 使用帮助
# -l, --linkadd    是否把符号链接的处理放到 install/doinst.sh 中
# -p, --prepend    如果已存在 install/doinst.sh, 把处理符号链接的命令放到
#                  已存在内容的前面
# -c, --chown      改变文件属主和访问权限
usage() {
  cat << EOF

Usage: makepkg package_name.tgz

Makes a Slackware compatible "*.tgz" package containing the contents of the 
current and all subdirectories. If symbolic links exist, they will be removed
and an installation script will be made to recreate them later. This script
will be called "install/doinst.sh". You may add any of your own ash-compatible
shell scripts to this file and rebuild the package if you wish.

options:  -l, --linkadd y|n (moves symlinks into doinst.sh: recommended)
          -p, --prepend (prepend rather than append symlinks to an existing
                         doinst.sh.  Useful to link libraries needed by
                         programs in the doinst.sh script)
          -c, --chown y|n (resets all permissions to root:root 755
                           - not generally recommended)

If these options are not set, makepkg will prompt as appropriate.
EOF
}

TMP=/tmp

# 参数处理
while [ 0 ]; do
  # 检查是否用参数指定了 linkadd
  if [ "$1" = "--linkadd" -o "$1" = "-l" ]; then
    if [ "$2" = "y" ]; then
      LINKADD=y
    elif [ "$2" = "n" ]; then
      LINKADD=n
    else
      usage
      exit 2
    fi
    shift 2
  # 检查是否用参数指定了 chown
  elif [ "$1" = "--chown" -o "$1" = "-c" ]; then
    if [ "$2" = "y" ]; then
      CHOWN=y
    elif [ "$2" = "n" ]; then
      CHOWN=n
    else
      usage
      exit 2
    fi
    shift 2
  # 检查是否用参数指定了 prepend
  elif [ "$1" = "-p" -o "$1" = "--prepend" ]; then
    PREPEND=y
    shift 1
  # 检查是否有 -h, -H, --help, 如果有, 则显示帮助信息, 然后退出.
  elif [ "$1" = "-h" -o "$1" = "-H" -o "$1" = "--help" -o $# = 0 ]; then
    usage
    exit 0
  else
    break
  fi
done

echo
echo "Slackware package maker, version 2.1."
# 根据给定的文件名来提取存放位置及包文件名
PACKAGE_NAME=$1
TARGET_NAME="`dirname $PACKAGE_NAME`"
PACKAGE_NAME="`basename $PACKAGE_NAME`"
TAR_NAME="`basename $PACKAGE_NAME .tgz`"
echo
echo "Searching for symbolic links:"
# 创建临时文件, 使用 mktemp 以避免已存在文件的问题
INST=`mktemp $TMP/makepkg.XXXXXX`
# 查指符号链接, 以便产生 "install/doinst.sh"
# 先是用 find . -type l 来查找当前目录下的符号链接
# 然后用 ls 显示出来, 限定 time style 利于后面的 cut 来进行控制
# 这里的 ls 命令需要 coreutils-5.0 或者以上的版本
# 后面的 while read ... done 其实是和 installpkg 中的 crunch 函数是一样的
# 不过此处没有把它放到函数里面去.
# cut -f 8- -d ' ' 是以一个空格为界, 取第 8 段及之后的部分:
# ./链接名 -> 链接目标
# 然后使用 cut -b3- 来去掉前 2 个字节(./)
find . -type l -exec ls -l --time-style=long-iso {} \; | while read foo ; do echo $foo ; done | cut -f 8- -d ' ' | cut -b3- | tee $INST
if [ ! "`cat $INST`" = "" ]; then
  echo
  echo "Making symbolic link creation script:"
  # 产生生成符号链接的命令, 并存入 doinst.sh(不是 install/doinst.sh)
  make_install_script $INST | tee doinst.sh
fi
echo
# 存在符号链接时询问是否建立安装脚本(处理符号链接的命令)
if [ ! "`cat $INST`" = "" ]; then
  # 如果存在 install/doinst.sh, 则询问是否将产生的安装脚本添加到已经存在的 install/doinst.sh 中
  if [ -r install/doinst.sh ]; then
    echo "Unless your existing installation script already contains the code"
    echo "to create these links, you should append these lines to your existing"
    echo "install script. Now's your chance. :^)"
    echo
    echo "Would you like to add this stuff to the existing install script and"
    echo -n "remove the symbolic links ([y]es, [n]o)? "
  # 不存在 install/doinst.sh, 则是询问是否产生安装脚本
  else
    echo "It is recommended that you make these lines your new installation script."
    echo
    echo "Would you like to make this stuff the install script for this package"
    echo -n "and remove the symbolic links ([y]es, [n]o)? "
  fi
  # 如果没有用参数指定 linkadd, 那么则读取用户输入, 否则直接输出之前的选择, 并继续
  if [ ! "$LINKADD" ]; then
    read LINKADD;
    echo
  else
    echo $LINKADD
    echo
  fi
  # linkadd 的情况下, 处理所产生的命令(产生符号链接的命令)
  if [ "$LINKADD" = "y" ]; then
    # 存在 install/doinst.sh
    if [ -r install/doinst.sh ]; then
      UPDATE="t"
      # 如果用户选择了 --prepend 选项,
      # 则将新产生的安装脚本放至已存在的 install/doinst.sh 的开头
      # 否则直接扔到已经 install/doinst.sh 的末尾
      if [ "$PREPEND" = "y" ]; then
        touch install/doinst.sh
        mv install/doinst.sh install/doinst.sh.shipped
        cat doinst.sh > install/doinst.sh
        echo "" >> install/doinst.sh
        cat install/doinst.sh.shipped >> install/doinst.sh
        rm -f install/doinst.sh.shipped
      else
        cat doinst.sh >> install/doinst.sh
      fi
    # 不存在 install/doinst.sh 则直接将 doinst.sh 填入 install/doinst.sh
    else
      mkdir -p install
      cat doinst.sh > install/doinst.sh
    fi
    echo
    # 删除符号链接
    echo "Removing symbolic links:"
    find . -type l -exec rm -v {} \;
    echo
    # 输出提示信息
    if [ "$UPDATE" = "t" ]; then
      if [ "$PREPEND" = "y" ]; then
        echo "Updating your ./install/doinst.sh (prepending symlinks)..."
      else
        echo "Updating your ./install/doinst.sh..."
      fi
    else
      echo "Creating your new ./install/doinst.sh..."
    fi
  fi
# 没有在当前目录找到符号链接, 直接给出提示
else
  echo "No symbolic links were found, so we won't make an installation script."
  echo "You can make your own later in ./install/doinst.sh and rebuild the"
  echo "package if you like."
fi
# 删除临时文件
rm -f doinst.sh $INST

# chown 的相关处理, 非必要的
# 有些软件需要特定的用户组或权限设置, 那样的话, 这一步就不能做
echo
echo "This next step is optional - you can set the directories in your package"
echo "to some sane permissions. If any of the directories in your package have"
echo "special permissions, then DO NOT reset them here!"
echo 
echo "Would you like to reset all directory permissions to 755 (drwxr-xr-x) and"
echo -n "directory ownerships to root.root ([y]es, [n]o)? "
# 根据是否已经选择了 chown 来决定是要求用户输入还是直接输出选择结果
if [ ! "$CHOWN" ]; then
  read CHOWN;
  echo
else
  echo $CHOWN
  echo
fi
# 根据用户选择, 进行权限设置
# 可以看出此处只修改了目录的权限和属主信息
if [ "$CHOWN" = "y" ]; then
  find . -type d -exec chmod -v 755 {} \; 
  find . -type d -exec chown -v root.root {} \;
fi
# 打包
echo
echo "Creating tar file $TAR_NAME.tar..."
echo
# 从上面可以知道,  $TAR_NAME.tar 只是个文件名, 并没有包含目录信息,
# 所以这里是把当前目录打包, 并存入当前目录下的 $TAR_NAME.tar
# 于是就会有这样的情况, tar 会不会把 $TAR_NAME.tar 本身再打包呢?
# 实际上 tar 会给出个提示, 说 $TAR_NAME.tar 就是打包的文件本身, 所以不打包它.
# 这算是个错误提示, 只不过在 slackware 打包的时候用了 v 参数, 所以会显示打包了哪些文件,
# 于是通常用户是注意不到这个信息的
$TAR cvf $TAR_NAME.tar .
# 对空文件进行提示
find . -type f -size 0c | while read file ; do
  echo "WARNING: zero length file $file"
done
find . -type f -name '*.gz' -size 20c | while read file ; do
  echo "WARNING: possible empty gzipped file $file"
done
# 压缩
echo
echo "Gzipping $TAR_NAME.tar..."
gzip -9 $TAR_NAME.tar
echo
echo "Renaming $TAR_NAME.tar.gz to $PACKAGE_NAME..."
# 把 $TAR_NAME.tar.gz 改名为指定的包文件名
mv $TAR_NAME.tar.gz $PACKAGE_NAME
# 如果指定存放包的位置不是当前目录, 则把它移动指定目录
if [ ! "$TARGET_NAME" = "." ]; then
  echo
  echo "Moving $PACKAGE_NAME to $TARGET_NAME..."
  mv $PACKAGE_NAME $TARGET_NAME
fi
# 创建完成, 给个提示
echo
echo "Package creation complete."
echo
Lane East

,

评论

slackware 包管理(pkgtools)分析 installpkg

2010-03-15 18:30

installpkg 是用来安装 slackware 软件包的工具, 根据它的帮助, 可以知道它的各个参数:

-warn       只用来警告是否有文件被覆盖, 并不进行实际的安装
-root /mnt  用其它的目录来作根目录, 可以用在系统维护或是系统安装的时候, 如果没使用这个参数, 那么则使用环境变量中的 ROOT 设置
-infobox    安装的时候显示一个文本对话框, 以显信息
-menu       使用文本对话框来询问是否安装, 对指定了[required](ADD)优先级的包无效
-ask        和上一个参数(-menu)联合使用, 用以忽略优先级的设置, 即指定了
            [required](ADD)优先级的包也要询问是否安装, 这是帮助中说的, 实际上,
            从代码中看, 即使是 infobox 模式下, 或者是 skip 优先级, -ask 也是
            有用的
-priority ADD|REC|OPT|SKP
            手工指定包的优先级, 而不是从 tagfile 中读取. 可以使用的优先级有:
            # ADD: required, 系统必需的软件
            # REC: recommended, 推荐安装的软件
            # OPT: optional, 可以选的软件
            # SKP: skip, 忽略的软件
-tagfile /somedir/tagfile
            手工指定 tagfile, 而不是使用默认的位置存放的 tagfile.
            默认位置在软件包所在的目录.

installpkg 中还使用了 dialog 来显示文本界面的对话框, 主要用到了两种形式, 在此简单说明一下:

dialog --title "对话框标题" --infobox "显示的信息" 0 0 其中的 0 0 表示对话框的大小是自适应的

dialog --title "对话框标题" --menu "菜单的提示信息" 0 0 3 \
  "第一个选项" "第一个选项的说明"
  "第二个选项" "第二个选项的说明"
  "第三个选项" "第三个选项的说明" 2>result.txt

其中的 0 0 表示对话框大小是自适应的, 3 表示有 3 个选项, “2>result.txt“表示把选择的结果放进 result.txt 中

下面是省去了版权信息及英文注释的中文注释了的代码(为了节约篇幅), 代码的相关版权请参照原软件包:

#!/bin/sh
# 默认返回 0, 如果后面遇到错误, 则重新设置 EXITSTATUS 的值
EXITSTATUS=0

# 检查 tar 的版本是否符合要求
umask 022
TAR=tar-1.13
$TAR --help 1> /dev/null 2> /dev/null
if [ ! $? = 0 ]; then
  TAR=tar
fi
if [ ! "`LC_MESSAGES=C $TAR --version`" = "tar (GNU tar) 1.13

Copyright (C) 1988, 92,93,94,95,96,97,98, 1999 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Written by John Gilmore and Jay Fenlason." ]; then
  echo "WARNING: pkgtools are unstable with tar > 1.13."
  echo "         You should provide a \"tar-1.13\" in your \$PATH."
  sleep 5
fi

# 帮助信息
usage() {
 cat << EOF
Usage: installpkg [options] package_name

Installpkg is used to install a .tgz package like this:
   installpkg xf_bin.tgz

options:      -warn (warn if files will be overwritten, but do not install)
              -root /mnt (install someplace else, like /mnt)
              -infobox (use dialog to draw an info box)
              -menu (confirm package installation with a menu, unless
                    the priority is [required] or ADD)
              -ask (used with menu mode: always ask if a package should be
                   installed regardless of what the package's priority is)
              -priority ADD|REC|OPT|SKP  (provide a priority for the entire
                    package list to use instead of the priority in the
                    tagfile)
              -tagfile /somedir/tagfile (specify a different file to use
                    for package priorities.  The default is "tagfile" in
                    the package's directory)

EOF
}

# 消除多余的空白
# 后面用它来处理 gzip -l 的输出, 并将处理完的结果送给 cut,
# 避免了多个空白对 cut 的处理
crunch() {
  while read FOO ; do
    echo $FOO
  done
}

# 由软件包的文件名获取相应的软件名
# 支持老式包名(软件名.tgz)和新式包名(软件名-版本号-平台-打包次数.tgz)
package_name() {
  # 去掉结尾的 .tgz
  STRING=`basename $1 .tgz`
  # 检查是否是老式包名
  # 当包名中不含 "-" 的时候,"cut -f 1 -d -" 和 "cut -f 2 -d -" 的执行结果相同,
  # 此处通过这个特性来确定包名中是否含有 "-", 这是不严密的, 因为可能会存在第一
  # 段和第二段内容相同的情况(应该很少见). 为什么不直接比较 cut -f 1 -d - 和
  # $STRING 本身呢?
  if [ "`echo $STRING | cut -f 1 -d -`" = "`echo $STRING | cut -f 2 -d -`" ]; then
    echo $STRING
  else
  # 包名中包含 "-", 不过有可能是因为软件名中含有 "-"(例如: util-linux), 
  # 所以仍有可能是老式包名
    # 计算文件名被 "-" 分割为了几段, 少于4段的则仍旧为老式的文件名
    INDEX=1
    while [ ! "`echo $STRING | cut -f $INDEX -d -`" = "" ]; do
      INDEX=`expr $INDEX + 1`
    done
    INDEX=`expr $INDEX - 1`
    # 少于4段的, 就仍然是旧式的软件包
    if [ "$INDEX" = "2" -o "$INDEX" = "3" ]; then
      echo $STRING
    else
    # 多于 4 段, 为新式文件名(此做法有一定的限制: 在软件名中包含大于等于 3 个
    # "-"的时候会误判,不过目前没见到过有这样的软件名)
      # 由于版本号,平台和打包次数均不含 "-",所以从后面去掉三段(显示从第 1 段到第
      # $INDEX - 3 段),剩下的部分作为软件名,这样能正确处理软件名中包含"-"的情况
      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
}

# 参数处理, installpkg 程序的真正入口
# 默认为 install 模式(命令行安装模式)
MODE=install
while [ 0 ]; do
  # warn 模式(只提示要安装, 删除哪些东西, 但是并不真正执行)
  if [ "$1" = "-warn" ]; then
    MODE=warn
    shift 1
  # infobox 模式(使用文本对话框来显安装信息, 但是无需选择是否安装)
  elif [ "$1" = "-infobox" ]; then
    MODE=infobox
    shift 1
  # menu 模式(使用文本对话框来询问是否要安装, 不询问优先级为[required]的软件包)
  elif [ "$1" = "-menu" ]; then
    MODE=menu
    shift 1
  # menu 模式下, 即使优先级为[required]的软件包, 也询问是否安装
  elif [ "$1" = "-ask" ]; then
    ALWAYSASK="yes"
    shift 1
  # 使用用户指定的 tagfile
  elif [ "$1" = "-tagfile" ]; then
    if [ -r "$2" ]; then
      USERTAGFILE="$2"
    elif [ -r "`pwd`/$2" ]; then
      USERTAGFILE="`pwd`/$2"
    else
      usage
      exit
    fi
    shift 2
  # 使用用户指定的优先级
  elif [ "$1" = "-priority" ]; then
    if [ "$2" = "" ]; then
      usage
      exit
    fi
    USERPRIORITY="$2"
    shift 2
  # 指定其它的安装位置, 可以用在系统安装或者是维护的时候
  elif [ "$1" = "-root" ]; then
    if [ "$2" = "" ]; then
      usage
      exit
    fi
    ROOT="$2"
    shift 2
  else
  # 遇到第一个非以上众多参数之一的, 则退出参数处理
  # 后面的参数都会被当作是软件名, 这是 installpkg 的一个限制:
  # 比如 installpkg packagename.tgz -warn 中的 -warn 也会被认为是个软件包名
    break
  fi
done

# 检查所需目录结构是否存在, 并且是否是目录, 如果不是, 则建立, 并设置好相应权限
# 相应的目录有 /var/log/packages, /var/log/removed_packages,
# /var/log/removed_scripts, /var/log/scripts, /var/log/setup
ADM_DIR="$ROOT/var/log"
for PKGDBDIR in packages removed_packages removed_scripts scripts setup ; do
  if [ ! -d $ADM_DIR/$PKGDBDIR ]; then
    rm -rf $ADM_DIR/$PKGDBDIR # make sure it's not a symlink or something stupid
    mkdir -p $ADM_DIR/$PKGDBDIR
    chmod 755 $ADM_DIR/$PKGDBDIR 
  fi
done

# 检查是否有临时目录
TMP=$ADM_DIR/setup/tmp
# 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
fi

# 无参数(或者是处理完 -warn 之类的参数后没有软件包名称)时显示帮助
if [ $# = 0 ]; then
  usage;
  exit
fi

# warn 模式
if [ "$MODE" = "warn" ]; then
  # 循环处理所有的包
  while [ -f "$1" ]; do
    echo "#### Scanning the contents of $1..."
    mkdir -p $TMP/scan$$
    # 在括号中的命令会使用一个子 shell, 在这个子 shell 中的 cd 命令不会影响到
    # installpkg 自身的工作目录
    ( cd $TMP/scan$$ ; $TAR xzf - install ) < $1 2> /dev/null 
    if [ -r $TMP/scan$$/install/doinst.sh ]; then
      # 包中包含 install/doinst.sh, 且其中包含 ' rm -rf' 的内容
      if cat $TMP/scan$$/install/doinst.sh | grep ' rm -rf ' 1>/dev/null 2>/dev/null ; then
        # 收集含有 ' rm -rf' 的行,并处理后输出
        cat $TMP/scan$$/install/doinst.sh | grep ' rm -rf ' > $TMP/scan$$/install/delete
        echo "The following locations will be completely WIPED OUT to allow symbolic"
        echo "links to be made. (We're talking 'rm -rf') These locations may be files,"
        echo "or entire directories.  Be sure you've backed up anything at these"
        echo "locations that you want to save before you install this package:"
        cat $TMP/scan$$/install/delete | cut -f 3,7 -d ' ' | tr ' ' '/'
      fi
      if [ -d $TMP/scan$$ ]; then
        # 直接使用
        # rm -rf $TMP/scan$$/install 2>/dev/null
        # rmdir scan$$ 2>/dev/null
        # 不是更好?
        # 另: 不直接使用 rm -rf $TMP/scan$$ 2>/dev/null 是因为 rmdir 只删除
        # 空目录, 所以不会影响 $TMP/scan$$ 下的的其它文件(如果存在的话)
        ( cd $TMP/scan$$ ; rm -rf install ) 2> /dev/null
        ( cd $TMP ; rmdir scan$$ ) 2> /dev/null
      fi
    fi
    echo "The following files will be overwritten when installing this package."
    echo "Be sure they aren't important before you install this package:"
    # tar 的 vv 参数会让 tar 显示更详细的文件信息(列出文件类型权限等),
    # 这样就可以用来过滤掉目录
    # 下面的这个地方没必要用这个括号, 直接 $TAR tzvvf $1 | grep -v 'drwx' 就可以
    # 在这可以看出, warn 模式给出的提示只是 *可能* 会被覆盖的
    ( $TAR tzvvf - ) < $1 | grep -v 'drwx'
    echo
    shift 1
  done
  # 全部的包都 warn 完之后退出, 不进行后面的安装
  exit
fi

# 各种安装模式(install, infobox, menu 模式)
for package in $* ; do

  # 运行命令时包名称未带 .tgz 则加上,以便统一处理
  if [ ! -r "$package" -a -r "$package.tgz" ]; then
    package=$package.tgz
  fi

  # 去掉包名中的 .tgz 部分
  shortname="`basename $package .tgz`"
  # 包所在的目录
  packagedir="`dirname $package`"
  # 调用 package_name 函数来获取软件名
  packagebase="`package_name $shortname`"

  # 拒绝非 .tgz 结尾的包(设置返回值为 3)
  # 前面已经有 packagedir='`dirname $package`", 为什么这里不直接使用?
  if [ ! -r "`dirname $package`/$shortname.tgz" ]; then
    EXITSTATUS=3
    if [ "$MODE" = "install" ]; then
      echo "Cannot install $package: package does not end in .tgz"
    fi
    continue;
  fi

  # 处理包的优先级
  unset PRIORITY
  if [ "$USERTAGFILE" = "" ]; then
    # 同样, 前面已经有 packagedir='`dirname $package`", 为什么这里不直接使用?
    TAGFILE="`dirname $package`/tagfile"   
  else
    TAGFILE="$USERTAGFILE"
  fi
  if [ ! -r "$TAGFILE" ]; then
    TAGFILE=/dev/null
  fi
  # 读取优先级信息:
  if grep "^$packagebase:" "$TAGFILE" | grep ADD > /dev/null 2> /dev/null ; then
    PRIORITY="ADD"
  elif grep "^$packagebase:" "$TAGFILE" | grep REC > /dev/null 2> /dev/null ; then
    PRIORITY="REC"
  elif grep "^$packagebase:" "$TAGFILE" | grep OPT > /dev/null 2> /dev/null ; then
    PRIORITY="OPT"
  elif grep "^$packagebase:" "$TAGFILE" | grep SKP > /dev/null 2> /dev/null ; then
    PRIORITY="SKP"
  fi
  # 根据不同的优先级, 设定 PMSG, 用于后面的显示
  if [ "$PRIORITY" = "ADD" ]; then
    PMSG="[required]"
  elif [ "$PRIORITY" = "REC" ]; then
    PMSG="[recommended]"
  elif [ "$PRIORITY" = "OPT" ]; then
    PMSG="[optional]"
  elif [ "$PRIORITY" = "SKP" ]; then
    PMSG="[skip]"
  else
    PMSG=""
  fi

  # 获取软件包相应的描述文件
  DESCRIPTION="/dev/null"
  # 在软件包相同的目录下查找 disk* package_descriptions $shortname.txt
  # $packagebase.txt, 看是否含有描述信息
  for file in $packagedir/disk* $packagedir/package_descriptions $packagedir/$shortname.txt $packagedir/$packagebase.txt ; do
    if grep "^$packagebase:" "$file" 1> /dev/null 2> /dev/null ; then
      DESCRIPTION="$file"
    elif grep "^$shortname:" "$file" 1> /dev/null 2> /dev/null ; then
      DESCRIPTION="$file"
    fi
  done
  # 未在软件包外找到描述文件, 则在软件包内查找. 需要解压缩
  if [ "$DESCRIPTION" = "/dev/null" ]; then
    mkdir -p $TMP/scan$$
    # 常用的方法, 在子 shell 中改变工作目录, 从而不影响整个脚本的工作目录
    ( cd $TMP/scan$$ ; $TAR xzf - install ) < $package 2> /dev/null
    if grep "^$packagebase:" "$TMP/scan$$/install/slack-desc" 1> /dev/null 2> /dev/null ; then
      DESCRIPTION="$TMP/scan$$/install/slack-desc"
    elif grep "^$shortname:" "$TMP/scan$$/install/slack-desc" 1> /dev/null 2> /dev/null ; then
      DESCRIPTION="$TMP/scan$$/install/slack-desc"
    fi
  fi

  # 包合法性检查
  if [ ! -f $package ]; then # 非普通文件
    EXITSTATUS=4
    if [ "$MODE" = "install" ]; then
      echo "Cannot install $package: package is not a regular file"
    fi
    continue;
  fi
  gzip -l $package 1> /dev/null 2> /dev/null
  if [ ! "$?" = "0" ]; then # 无法用 gzip -l 来操作软件包
    EXITSTATUS=2 # failed gzip -l
    if [ "$MODE" = "install" ]; then
      echo "Cannot install $package: package is corrupt (failed 'gzip -l $package')"
    fi
    continue;
  fi

  # 获取压缩前后的文件大小信息, 存入临时文件
  COMPRESSED=`gzip -l $package | grep -v uncompressed_name | crunch | cut -f 1 -d ' '`
  UNCOMPRESSED=`gzip -l $package | grep -v uncompressed_name | crunch | cut -f 2 -d ' '`
  COMPRESSED="`expr $COMPRESSED / 1024` K"
  UNCOMPRESSED="`expr $UNCOMPRESSED / 1024` K"
#  MD5SUM=`md5sum $package | cut -f 1 -d ' '`
  cat $DESCRIPTION | grep "^$packagebase:" | cut -f 2- -d : | cut -b2- 1> $TMP/tmpmsg$$ 2> /dev/null
  if [ "$shortname" != "$packagebase" ]; then
    cat $DESCRIPTION | grep "^$shortname:" | cut -f 2- -d : | cut -b2- 1>> $TMP/tmpmsg$$ 2> /dev/null
  fi
  # slackware 包的描述文件最高为 13 行, 少于 12 行时用空行补到 12 行(最后一行为文件尺寸信息)
  LENGTH=`cat $TMP/tmpmsg$$ | wc -l`
  while [ $LENGTH -lt 12 ]; do
    echo >> $TMP/tmpmsg$$
    LENGTH=`expr $LENGTH + 1`
  done
  # 第 13 行, 文件尺寸信息
  echo "Size: Compressed: $COMPRESSED, uncompressed: $UNCOMPRESSED." >> $TMP/tmpmsg$$
  # 根据英文的注释, 新版本的 dialog 需要在每一行的末尾多加一个 '\n',
  # 否则输出的内容会比较混乱, 但是似乎不是所有版本的 dialog 都这样,
  # 不过就算加上也不会有什么影响
  cat << EOF > $TMP/controlns$$
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
EOF
  # 将提取出来的软件描述和 dialog 需要的行尾 '\n' 连接起来
  # -d "" 表示两个文件的对应行连接的时候用 "" 来分隔, 其实就是中间不加入分隔
  paste -d "" $TMP/tmpmsg$$ $TMP/controlns$$ > $TMP/pasted$$
  rm -f $TMP/controlns$$
  mv $TMP/pasted$$ $TMP/tmpmsg$$
  # install 模式,不理会优先级的设定, 即使是 SKP 的软件也安装
  if [ "$MODE" = "install" ]; then
    if [ "$PMSG" = "" ]; then
      echo "Installing package $shortname... "
    else
      echo "Installing package $shortname ($PMSG)... "
    fi
    echo "PACKAGE DESCRIPTION:"
    cat $DESCRIPTION | grep "^$packagebase:" | uniq
    if [ "$shortname" != "$packagebase" ]; then
      cat $DESCRIPTION | grep "^$shortname:" | uniq
    fi
  # infobox 模式, 不安装 SKP 优先级的软件
  elif [ "$MODE" = "infobox" -a ! "$PRIORITY" = "SKP" ]; then
    dialog --title "Installing package ==>$shortname<== $PMSG" --infobox "`cat $TMP/tmpmsg$$`" 0 0
  # menu 模式下, 优先级为 ADD(required) 的包, 并且未使用 -ask 参数要求询问,
  # 则无需询问, 直接用 dialog 显示相关信息
  elif [ "$MODE" = "menu" -a "$PRIORITY" = "ADD" -a ! "$ALWAYSASK" = "yes" ]; then
    dialog --title "Installing package ==>$shortname<== $PMSG" --infobox "`cat $TMP/tmpmsg$$`" 0 0
  # menu 模式下, 用户指定优先级为 ADD(required) 的包, 不管包原先的优先级是什么
  # 都无需询问, 直接用 dialog 显相关信息
  elif [ "$MODE" = "menu" -a "$USERPRIORITY" = "ADD" ]; then
    dialog --title "Installing package ==>$shortname<== $PMSG" --infobox "`cat $TMP/tmpmsg$$`" 0 0
  # menu/infobox 模式下, 优先级为 SKP 的包, 且未使用 -ask 参数要求询问, 
  # 则直接删除临时文件, 不作其它处理
  elif [ "$MODE" = "menu" -a "$PRIORITY" = "SKP" -a ! "$ALWAYSASK" = "yes" ]; then
    rm -f $TMP/tmpmsg$$
    continue # 处理下一个软件包
  # infobox 模式, skip 优先级, 无 -ask, 则直接删除临时文件, 不作其它处理
  # 从下面这一条和上面这一条可以看出, -ask 并不像帮助中说的那样, 只对 warn 模式或
  # ADD 优先级起作用
  elif [ "$MODE" = "infobox" -a "$PRIORITY" = "SKP" -a ! "$ALWAYSASK" = "yes" ]; then
    rm -f $TMP/tmpmsg$$
    continue
  # 剩下的情况都是需要使用菜单来询问是否真的要安装
  else
    dialog --title "Package Name: ==>$shortname<== $PMSG" --menu "`cat $TMP/tmpmsg$$`" 0 0 3 \
    "Yes" "Install package $shortname" \
    "No" "Do not install package $shortname" \
    "Quit" "Abort software installation completely" 2> $TMP/reply$$
    # 当选择的是上面三个选项的时候, dialog 返回 0,
    # 而当直接选择"取消"按钮, 则返回非 0, 与选择 No 选项一样处理
    if [ ! $? = 0 ]; then
      echo "No" > $TMP/reply$$
    fi
    REPLY="`cat $TMP/reply$$`"
    rm -f $TMP/reply$$ $TMP/tmpmsg$$
    if [ "$REPLY" = "Quit" ]; then
      # 返回值 99: 用户选择 Quit, 退出 *所有的* 软件包的安装, 不光是当前这一个
      exit 99
    elif [ "$REPLY" = "No" ]; then
      continue # 用户选择不安装本软件包, 跳过
    fi
  fi

  # 得到将要安装的文件列表
  $TAR tzf $package 1> $TMP/tmplist$$ 2> /dev/null
  TARERROR=$?
  # 无法用 tar tzf 列出包的内容, tar 包不正确
  if [ ! "$TARERROR" = "0" ]; then
    EXITSTATUS=1
    if [ "$MODE" = "install" ]; then
      echo "Unable to install $package: tar archive is corrupt (tar returned error code $TARERROR)"
    fi
    rm -f $TMP/tmplist$$
    continue
  fi
  # 删除文件列表中的符号链接
  cat $TMP/tmplist$$ | grep -v "/$" | while read file ; do
    if [ -L "$ROOT/$file" ]; then
      rm -f "$ROOT/$file"
    fi
  done
  rm -f $TMP/tmplist$$

  # 记录将要安装的包的信息
  echo "PACKAGE NAME:     $shortname" > $ADM_DIR/packages/$shortname
  echo "COMPRESSED PACKAGE SIZE:     $COMPRESSED" >> $ADM_DIR/packages/$shortname
  echo "UNCOMPRESSED PACKAGE SIZE:     $UNCOMPRESSED" >> $ADM_DIR/packages/$shortname
  echo "PACKAGE LOCATION: $package" >> $ADM_DIR/packages/$shortname
#  echo "PACKAGE MD5SUM: $MD5SUM" >> $ADM_DIR/packages/$shortname
  echo "PACKAGE DESCRIPTION:" >> $ADM_DIR/packages/$shortname
  cat $DESCRIPTION | grep "^$packagebase:" >> $ADM_DIR/packages/$shortname 2> /dev/null
  if [ "$shortname" != "$packagebase" ]; then
    cat $DESCRIPTION | grep "^$shortname:" >> $ADM_DIR/packages/$shortname 2> /dev/null
  fi
  echo "FILE LIST:" >> $ADM_DIR/packages/$shortname
  # 此处使用了很多的 tar 参数, 其中关键的有:
  #   -U 在解压要重写的文件前先删除它们,避免了覆盖已存在文件时的问题
  #   -p 解包时保留文件的权限信息, 软件包中的文件的权限在安装的时候是应该保留的
  #   -v 显示出解包出来的文件列表, 用它来记录所安装的文件/目录
  # 此外还有一个 -l, 它在 gnu tar 1.13 版的时候表示 --one-file-system,
  # 而 gnu tar 1.14 之后就不再赞成使用 -l 表示 --one-file-system 了, 到了 gnu tar 1.15.91
  # 之后 -l 就表示 --check-links 了, 这大概也是 pkgtools 中非要用 tar 1.13 的原因吧.
  # 但是 gnu 的文档中说: "Used when creating an archive.", 是不是表示这里可以不要这个 -l?
  ( cd $ROOT/ ; $TAR -xzlUpvf - ) < $package >> $TMP/$shortname 2> /dev/null
  # 这个地方的 grep 是有问题的, slackware 期望要安装的文件列表里面有且只有一个
  # 行首为 "./" 的行, 而这里的 grep '^./' 查找的是行首为一个字符后面跟着 "/" 的行,
  # 所以当包内的文件中含有单个字母名称的目录时就会出现问题
  if [ "`cat $TMP/$shortname | grep '^./' | wc -l | tr -d ' '`" = "1" ]; then
    # Good.  We have a package that meets the Slackware spec.
    cat $TMP/$shortname >> $ADM_DIR/packages/$shortname
  else
    # 非 makepkg 制作的包的内容可能不符合 pkgtools 所需要的格式,则在记录中作出相应调整
    echo './' >> $ADM_DIR/packages/$shortname
    cat $TMP/$shortname | grep -v '^./$' | cut -b3- >> $ADM_DIR/packages/$shortname
  fi
  rm -f $TMP/$shortname

  # 如果 ldconfig 可以执行,则运行它,这个是 glibc 的某种机制,相应可参考 LFS 的相关手册
  if [ -x /sbin/ldconfig ]; then
    /sbin/ldconfig
  fi
  # 执行安装脚本
  # 此处执行安装脚本的时候传递了 -install 参数, 但是实际上脚本中未必会理会这个参数,
  # 而 removepkg/upgradepkg 中也没有用到 doinst.sh, 大概是为了以后的扩展吧.
  if [ -f $ROOT/install/doinst.sh ]; then
    if [ "$MODE" = "install" ]; then
      echo "Executing install script for $shortname..."
    fi
    ( cd $ROOT/ ; sh install/doinst.sh -install; )
  fi
  # 将 $ROOT/install 下的相关内容移动到包记录的目录下
  if [ -d $ROOT/install ]; then
    if [ -r $ROOT/install/doinst.sh ]; then
      cp $ROOT/install/doinst.sh $ADM_DIR/scripts/$shortname
      chmod 755 $ADM_DIR/scripts/$shortname
    fi
    # 删除 /install/doinst.sh /install/slack-*,而如果 /install 下存在其它文件,不删除,
    # 这说明 slackware 的包管理系统只占用了这样的几个文件名
    ( cd $ROOT/install ; rm -f doinst.sh slack-* 1> /dev/null 2>&1 )
    rmdir $ROOT/install 1> /dev/null 2>&1
  fi
  # 删除临时文件
  if [ -d "$TMP/scan$$" ]; then
    rm -rf "$TMP/scan$$"
  fi
  rm -f $TMP/tmpmsg$$ $TMP/reply$$
  # install 模式下安装完成后再输出一个空行,作为提示和分隔
  if [ "$MODE" = "install" ]; then
    echo
  fi
done

exit $EXITSTATUS
Lane East

,

评论

« 较早的