Compresión de las páginas de manual e info

Los programas para leer páginas de manual e info pueden procesar transparentemente páginas comprimidas con gzip o bzip2. Sin embargo, las cosas no son tan simples: los directorios man tienden a contener enlaces (duros o simbólicos) que arruinan ideas simples como la de ejecutar recursivamente gzip sobre ellos. Un método mejor es usar el siguiente guión.

cat > /usr/bin/compressdoc << "EOF"

#!/bin/bash
# VERSION: 20031029.0025
#
# Comprime (con bzip2 o gzip) todas las páginas de manual de una
# jerarquía y actualiza los enlaces simbólicos
# - Por Marc Heerdink <marc @ koelkast.net>
# Modificado por Mark Hymers <markh @ linuxfromscratch.org>
# para poder escojer entre ficheros gzip o bzip2 y tratar correctamente
# todos los enlaces simbólicos
#
# Modificado el 30/09/2003 por Yann E. Morin <yann.morin.1998 @ anciens.enib.fr>
# para aceptar compresión/descompresión, manejar correctamente enlaces
# duros, permitir el cambio de enlaces duros por blandos, especificar el nivel
# de compresión, procesar man.conf para todas las apariciones de MANPATH,
# permitir hacer copias de respaldo y permitir que se gu7arde la versión mas
# reciente de una página.
#
# TODO:
#        - elegir el método de compresión por defecto basandose en la
#          herramienta disponible : gzip o bzip2;
#        - ofrecer una opción para elegir automáticamente el mejor método de
#          compresión según la página (por ejemplo, comprobar página a página
#          qué gzip/bzip2/lo que sea es mas efectivo);
#        - cuando exista una variable de entorno MANPATH, utilizarla en vez de
#          /etc/man.conf (útil para que los usuarios (des)compriman sus
#          páginas de manual);
#        - ofrecer una opción para recuperar una copia de respaldo anterior;
#        - añadir otras herramientas de compresión (compress, zip, etc?). 
#           ¿Es necesario?

# Bastante divertido, esta función muestra algo de ayuda.
function help ()
{
  if [ -n "$1" ]; then
    echo "Opción desconocida : $1"
  fi
  ( echo "Uso: $0 <metodo_de_compresion> [opciones] [dirs]" && \
  cat << EOT
Donde metodo_de_compresion es uno de :
  --gzip, --gz, -g
  --bzip2, --bz2, -b
                Comprime utilizando gzip o bzip2.

  --decompress, -d
                Descomprime las páginas de manual.

  --backup      Especifica que ha de hacerse un respaldo .tar para cada directorio.
                En caso de que un respalde exista, es salvado como .tar.old antes
                de hacer el nuevo respaldo. Si existe un respaldo .tar.old, este es
                eliminado antes de salvar el respaldo.
                En modo de respaldo no puede realizarse ninguna otra acción.

Y donde las opciones són :
  -1 to -9, --fast, --best
                El nivel de copmpresión, tal y como lo aceptan gzip y bzip2. Cuando
                no se especifica, utiliza el nivel por defecto del método indicado
                (-6 para gzip y -9 para bzip2). No se utiliza en los modos de
				respaldo o descompresión.

  --force, -F   Fuerza la (re-)compresión, incluso si el anterior tenía el mismo
                método. ütil cuando se cambia el nivel de compresión. Por defecto,
                una página no es recomprimida si termina en el mismo sufijo que
                que añade el método (.bz2 para bzip2, .gz para gzip).

  --soft, -S    Cambia enlaces duros por enlaces blandos. Usar con cuidado,
                pues el primer fichero encontrado se usará como referencia. No se
                utiliza en modo de respaldo.

  --hard, -H    Cambia enlaces blandos por enlaces duros. No se utiliza en
                modo de respaldo..

  --conf=dir, --conf dir
                Especifica la localización de man.conf. Por defecto /etc.

  --verbose, -v Modo detallado, muestra el nombre del directorio que se está 
                procesando. Dobla la opción para hecerle mas detallado y que muestre
				el nombre del fichero que se está procesando.

  --fake, -f    Falsa ejecución. Muestra los parámetros actuales que se usarán.

  dirs          Una lista de rutas absolutas a los directorios man separadas por
                espacios.
                Cuando está vacio, y solo entonces, procesa todas las apariciones
				de MANPATH en ${MAN_CONF}/man.

Notas sobre la compresión
  Hubo una discusión en blfs-support sobre los niveles de compresión de
  gzip y bzip2 en las páginas de manual, teniendo en cuenta el sistema de
  ficheros anfitrión, la arquitectura, etc... Al final la conclusión fué que gzip
  es mucho mas eficiente sobre ficheros "pequeños" y bzip2 sobre ficheros
  "pequeños", siendo pequeño y grande muy dependiente del contenido de
  los ficheros.

  Mira el mensaje original de Mickael A. Peters, titulado "Bootable Utility CD",
  y fechado 20030409.1816(+0200), y los mensajes siguientes:
  http://linuxfromscratch.org/pipermail/blfs-support/2003-April/038817.html

  En mi sistema (x86, ext3), las páginas de manual tenían 35564kiB antes
  de comprimirlas. Comprimidas con gzip -9 bajaron a 20372kiB (57.28%), 
  con bzip2 -9 bajaron a 19812kiB   (55.71%). Esto es un ahorro de espacio
  del 1.57%. YMMV.

  Lo que no se tuvo en consideración fué la velocidad de descompresión.
  Pero, ¿esto tiene sentido?. U obtienes acceso rápido con página de manual
  sin comprimir, o ganas espacio a expensas de un ligero retardo de tiempo.
  Bien, mi P4-2.5GHz no llegó a percatarse de esto... :-)
EOT
) | less
}

# Esta función comprueba que la página de manual es idéntica entre las
# versiones bzip2, gzip y sin comprimir.
#  $1 es el directorio en el que reside el fichero
#  $2 es el nombre del fichero de la página de manual
# Devuelve 0 (verdadera) si el fichero es mas reciente y debe tenerse en cuenta,
# y 1 (falso) si el fichero no lo es (y por tanto debe borrarse)
function check_unique ()
{
  # NB. Cuando hay enlaces duros a este fichero, estos no
  # son borrados. De hecho, si hay enlaces duros, todos ellos
  # tienen la misma fecha/hora, lo que los deja preparados para
  # borrarlos mas adelante.

  # Construye la lista con todas las páginas de manual que tienen
  # el mismo nombre
  DIR=$1
  BASENAME=`basename "${2}" .bz2`
  BASENAME=`basename "${BASENAME}" .gz`
  GZ_FILE="$BASENAME".gz
  BZ_FILE="$BASENAME".bz2

  # Busca y guarda la mas reciente
  LATEST=`(cd "$DIR"; ls -1rt "${BASENAME}" "${GZ_FILE}" "${BZ_FILE}" 2>/dev/null | tail -n 1)`
  for i in "${BASENAME}" "${GZ_FILE}" "${BZ_FILE}"; do
    [ "$LATEST" != "$i" ] && rm -f "$DIR"/"$i"
  done

  # En caso de que el fichero especificado sea el mas nuevo, devuelve 0
  [ "$LATEST" = "$2" ] && return 0
  # Si el fichero no es el mas nuevo, devuelve 1
  return 1
}

# OK, procesa los argumentos de la línea de comandos e inicializa un estado
# algo sensible, esto es : no cambia el estado de los enlaces, procesa
#  /etc/man.conf, es mas silencioso, busca man.conf en /etc, y no fuerza la
# (re-)compresión.
COMP_METHOD=
COMP_SUF=
COMP_LVL=
FORCE_OPT=
LN_OPT=
MAN_DIR=
VERBOSE_LVL=0
BACKUP=no
FAKE=no
MAN_CONF=/etc
while [ -n "$1" ]; do
  case $1 in
    --gzip|--gz|-g)
      COMP_SUF=.gz
      COMP_METHOD=$1
      shift
      ;;
    --bzip2|--bz2|-b)
      COMP_SUF=.bz2
      COMP_METHOD=$1
      shift
      ;;
    --decompress|-d)
      COMP_SUF=
      COMP_LVL=
      COMP_METHOD=$1
      shift
      ;;
    -[1-9]|--fast|--best)
      COMP_LVL=$1
      shift
      ;;
    --force|-F)
      FORCE_OPT=-F
      shift
      ;;
    --soft|-S)
      LN_OPT=-S
      shift
      ;;
    --hard|-H)
      LN_OPT=-H
      shift
      ;;
    --conf=*)
      MAN_CONF=`echo $1 | cut -d '=' -f2-`
      shift
      ;;
    --conf)
      MAN_CONF="$2"
      shift 2
      ;;
    --verbose|-v)
      let VERBOSE_LVL++
      shift
      ;;
    --backup)
      BACKUP=yes
      shift
      ;;
    --fake|-f)
      FAKE=yes
      shift
      ;;
    --help|-h)
      help
      exit 0
      ;;
    /*)
      MAN_DIR="${MAN_DIR} ${1}"
      shift
      ;;
    -*)
      help $1
      exit 1
      ;;
    *)
      echo "\"$1\" no es el nombre absoluto de una ruta"
      exit 1
      ;;
  esac
done

# Redirecciones
case $VERBOSE_LVL in
  0)
     # O, silencioso
     DEST_FD0=/dev/null
     DEST_FD1=/dev/null
     VERBOSE_OPT=
     ;;
  1)
     # 1, algo detallado
     DEST_FD0=/dev/stdout
     DEST_FD1=/dev/null
     VERBOSE_OPT=-v
     ;;
  *)
     # 2 y superiores, es mas detallado
     DEST_FD0=/dev/stdout
     DEST_FD1=/dev/stdout
     VERBOSE_OPT="-v -v"
     ;;
esac

# Nota: en mi máquina, 'man --path' muestra /usr/share/man duplicado, 
# una vez con '/' al final, y otra si él.
if [ -z "$MAN_DIR" ]; then
  MAN_DIR=`man --path -C "$MAN_CONF"/man.conf \
            | sed 's/:/\\n/g' \
            | while read foo; do dirname "$foo"/.; done \
            | sort -u \
            | while read bar; do echo -n "$bar "; done`
fi

# Si no hay MANPATH en ${MAN_CONF}/man.conf, aborta el proceso
if [ -z "$MAN_DIR" ]; then
  echo "Directorio no especificado y no encontrado con \`man --path'"
  exit 1
fi

# ¿Falsa ejecución?
if [ "$FAKE" != "no" ]; then
  echo "Parámetros actuales usados:"
  echo -n "Compresión.......: "
  case $COMP_METHOD in
    --bzip2|--bz2|-b) echo -n "bzip2";;
    --gzip|__gz|-g) echo -n "gzip";;
    --decompress|-d) echo -n "descompresión";;
    *) echo -n "desconocido";;
  esac
  echo " ($COMP_METHOD)"
  echo "Nivel de compresión.: $COMP_LVL"
  echo "Sufijo de compresión: $COMP_SUF"
  echo -n "Compresión forzada.: "
  [ "foo$FORCE_OPT" = "foo-F" ] && echo "si" || echo "no"
  echo "man.conf is.......: ${MAN_CONF}/man.conf"
  echo -n "Enlaces duros........: "
  [ "foo$LN_OPT" = "foo-S" ] && echo "convertir en blandos" || echo "dejarlos así"
  echo -n "Enlaces blandos........: "
  [ "foo$LN_OPT" = "foo-H" ] && echo "convertir en duros" || echo "dejarlos así"
  echo "Respaldo............: $BACKUP"
  echo "Falsa ejecución (!si!).....: $FAKE"
  echo "Directorios.......: $MAN_DIR"
  echo "Nivel de detalles...: $VERBOSE_LVL"
  exit 0
fi

# Si no se especifica un método, mostrar la ayuda
if [ -z "${COMP_METHOD}" -a "${BACKUP}" = "no" ]; then
  help
  exit 1
fi

# En modo respaldo, hace solo el respaldo
if [ "$BACKUP" = "yes" ]; then
  for DIR in $MAN_DIR; do
    cd "${DIR}/.."
    DIR_NAME=`basename "${DIR}"`
    echo "Backing up $DIR..." > $DEST_FD0
    [ -f "${DIR_NAME}.tar.old" ] && rm -f "${DIR_NAME}.tar.old"
    [ -f "${DIR_NAME}.tar" ] && mv "${DIR_NAME}.tar" "${DIR_NAME}.tar.old"
    tar cfv "${DIR_NAME}.tar" "${DIR_NAME}" > $DEST_FD1
  done
  exit 0
fi

# Sé que MAN_DIR solo contiene rutas absolutas.
# Necesito tener en cuenta las páginas de manual localizadas, por lo
# que lo hago recursivamente.
for DIR in $MAN_DIR; do
  MEM_DIR=`pwd`
  cd "$DIR"
  for FILE in *; do
    # Corrige el caso en el que los directorios están vacios.
    if [ "foo$FILE" = "foo*" ]; then continue; fi

    # Corrige el caso en el que los enlaces duros ven el cambio en su esquema
	# de compresión (de sin comprimir a comprimidas, o de bzip2 a gzip o de
	# gzip a bzip2). También corrige el caso en el que hay presentes mútilples
	# versiones de la página, que pueden estar comprimidas o no.
    if [ ! -L "$FILE" -a ! -e "$FILE" ]; then continue; fi

    # No comprime los ficheros whatis
    if [ "$FILE" = "whatis" ]; then continue; fi

    if [ -d "$FILE" ]; then
      cd "${MEM_DIR}"  # Regresa a donde ejecutamos "$0", en caso de que "$0"=="./compressdoc" ...
      # Vamos recursivamente a este directorio
      echo "-> Entrando a ${DIR}/${FILE}..." > $DEST_FD0
      # No necesito usar --conf, pues especifico el directorio de trabajo.
      # Pero necesito salir en caso de error.
      "$0" ${COMP_METHOD} ${COMP_LVL} ${LN_OPT} ${VERBOSE_OPT} ${FORCE_OPT} "${DIR}/${FILE}" || exit 1
      echo "<- Saliendo de ${DIR}/${FILE}." > $DEST_FD1
      cd "$DIR"  # Necesario para la siguiente iteración del bucle.

    else # !dir
      if ! check_unique "$DIR" "$FILE"; then continue; fi

      # Comprueba si el fichero ya está comprimido con el método especificado
      BASE_FILE=`basename "$FILE" .gz`
      BASE_FILE=`basename "$BASE_FILE" .bz2`
      if [ "${FILE}" = "${BASE_FILE}${COMP_SUF}" -a "foo${FORCE_OPT}" = "foo" ]; then continue; fi

      # Si tenemos un enlace simbólico.
      if [ -h "$FILE" ]; then
        case "$FILE" in
          *.bz2)
            EXT=bz2 ;;
          *.gz)
            EXT=gz ;;
          *)
            EXT=none ;;
        esac

        if [ ! "$EXT" = "none" ]; then
          LINK=`ls -l "$FILE" | cut -d ">" -f2 | tr -d " " | sed s/\.$EXT$//`
          NEWNAME=`echo "$FILE" | sed s/\.$EXT$//`
          mv "$FILE" "$NEWNAME"
          FILE="$NEWNAME"
        else
          LINK=`ls -l "$FILE" | cut -d ">" -f2 | tr -d " "`
        fi

        if [ "$LN_OPT" = "-H" ]; then
          # Cambia este enlace blando por uno duro
          rm -f "$FILE" && ln "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
          chmod --reference "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
        else
          # Mantiene este enlace como blando.
          rm -f "$FILE" && ln -s "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
        fi
        echo "Reenlazando $FILE" > $DEST_FD1

      # En cambio, si tenemos un fichero plano.
      elif [ -f "$FILE" ]; then
        # Tiene en cuenta los enlaces duros: contruye la liste de ficheros enlazados
        # al que vamos a (des)comprimir.
        # NB. Esto no es óptimo pues el fichero será comprimido eventualmente
        # tantas vecese como enlaces duros tenga. Pero por ahora es la forma
		# mas segura.
        inode=`ls -li "$FILE" | awk '{print $1}'`
        HLINKS=`find . \! -name "$FILE" -inum $inode`

        if [ -n "$HLINKS" ]; then
          # ¡Tenemos enlaces duros! Los elimina ahora.
          for i in $HLINKS; do rm -f "$i"; done
        fi

        # Ahora tiene en cuanta los ficheros sin enlaces duros.
        # Los descomprimimos primero para comprimirlos mas tarde
        # con el nivel de compresión seleccionado...
        case "$FILE" in
          *.bz2)
            bunzip2 $FILE
            FILE=`basename "$FILE" .bz2`
          ;;
          *.gz)
            gunzip $FILE
            FILE=`basename "$FILE" .gz`
          ;;
        esac

        # Comprime el fichero con el nivel de compresión indicado, si es necesario.
        case $COMP_SUF in
          *bz2)
            bzip2 ${COMP_LVL} "$FILE" && chmod 644 "${FILE}${COMP_SUF}"
            echo "Comprimiendo $FILE" > $DEST_FD1
            ;;
          *gz)
            gzip ${COMP_LVL} "$FILE" && chmod 644 "${FILE}${COMP_SUF}"
            echo "Comprimiendo $FILE" > $DEST_FD1
            ;;
          *)
            echo "Sin comprimir: $FILE" > $DEST_FD1
            ;;
        esac

        # Si el fichero tiene enlaces, los regenera (tanto duros como blandos)
        if [ -n "$HLINKS" ]; then
          for i in $HLINKS; do
            NEWFILE=`echo "$i" | sed s/\.gz$// | sed s/\.bz2$//`
            if [ "$LN_OPT" = "-S" ]; then
              # Hace este enlace duro uno blando.
              ln -s "${FILE}$COMP_SUF" "${NEWFILE}$COMP_SUF"
            else
              # Mantiene el enlace duro como tal.
              ln "${FILE}$COMP_SUF" "${NEWFILE}$COMP_SUF"
            fi
            chmod 644 "${NEWFILE}$COMP_SUF" # En realidad solo funciona con 
			                                                                  # enlaces duros. Inofensivo con
																			  # enlaces blandos.
          done
        fi

      else
        # Hay un problema cuando no obtenemos un enlace o un fichero plano.
        # Obviamente, nunca deberíamos llegar aquí... :-(
        echo "Whaooo... \"${DIR}/${FILE}\" no es un enlace o un fichero plano. Compruebalo:"
        ls -l "${DIR}/${FILE}"
        exit 1
      fi
    fi
  done # para FILE
done # para DIR
EOF
chmod 755 /usr/bin/compressdoc

Ahora, como root, puedes ejecutar /usr/bin/compressdoc --bz2 para comprimir todas las páginas de manual de tu sistema. También puedes ejecutar /usr/bin/compressdoc --help para obtener una ayuda clara sobre lo que el guión puede hacer.

No olvide que algunos programas, como el sistema X Window o XEmacs, también instalan su documentación en lugares no estándares (como /usr/X11R6/man, etc...). No olvides añadir estas localizaciones al fichero /etc/man.conf con una sección MANPATH=/ruta.

Ejemplo:


    ...
    MANPATH=/usr/share/man
    MANPATH=/usr/local/man
    MANPATH=/usr/X11R6/man
    MANPATH=/opt/qt/doc/man
    ...

Generalmente, los sistemas de instalación de paquetes no comprimen las páginas man/info, lo que significa que necesitarás ejecutar este guión otra vez si quieres mantener el tamaño de tu documentación tan pequeño como sea posible. Igualmente, ten en cuenta que es seguro ejecutar el guión tras actualizar un paquete: cuando tienes varias versiones de una página (por ejemplo, una comprimida y otra sin comprimir) la mas reciente es la que se guarda y las otras borradas..