Compresser des pages de man et d'info

Les lecteurs de man et d'info peuvent gérer de façon transparente des fichiers compressés avec gzip ou bzip2, une fonctionnalité que vous pouvez utiliser pour libérer de l'espace disque en laissant disponible la documentation. Les choses ne sont cependant pas si simples ; les répertoires de man ont tendance à contenir des liens—durs et symboliques—qui remettent en cause les idées simples telles que l'appel de gzip sur eux de manière récursive. Une meilleure manière de faire est d'utiliser le script ci-dessous. Si vous préférez télécharger le fichier au lieu de le créer en tapant ou en faisant un copier-coller, vous pouvez le trouver sur http://anduin.linuxfromscratch.org/files/BLFS/svn/compressdoc (vous devriez installer le fichier dans le répertoire /usr/sbin).

cat > /usr/sbin/compressdoc << "EOF"
#!/bin/bash
# VERSION: 20080421.1623
#
# Compresse (avec bzip2 ou gzip) toutes les pages de  man d'une hiérarchie et
# met à jour les liens symboliques  - Par Marc Heerdink <marc @ koelkast.net>
#
# Modifié pour pouvoir éventuellement gzipper ou bzip2er des fichiers et pour
# gérer correctement tous les liens symboliques par Mark Hymers <markh @ linuxfromscratch.org>
#
# Modifié le 30092003 par Yann E. Morin <yann.morin.1998 @ anciens.enib.fr>
# pour accepter la compression/décompression, pour gérer correctement les liens
# en dur, pour permettre de transformer les liens durs en liens mous, de spécifier
# le niveau de compression, de parser le man.conf pour toutes les occurrences de
# MANPATH, pour permettre une sauvegarde, pour permettre de prendre la version 
# la plus récente d'une page.
#
# Modifié le 30032004 par Tushar Teredesai pour remplacer par $0 par le nom du
# script.
#   (Remarque : On suppose que le script se trouve dans le PATH de
# l'utilisateur)
#
# Modifié le 12012005 par Randy McMurchy pour raccourcir la longueur des lignes
# et corriger des fautes de grammaire.
#
# Modifié le 28012006 par Alexander E. Patrakov pour une compatibilité avec
# Man-DB.
#
# Modifié le 11032006 par Archaic pour utiliser l'outil manpath de Man-DB qui
# remplace man --path de Man.
#
# Modifié le 21042008 par Dan Nicholson pour exécuter correctement le bon
# compressdoc lors d'un fonctionnement de façon récursive. Cela signifie que
# le même compressdoc sera utilisé si on donne un chemin complet ou s'il a été 
# résolu à partir de PATH.
#
# Modifié le 21042008 par Dan Nicholson pour être plus robuste face aux
# répertoires qui n'existent pas ou qui n'ont pas assez de droits.
#
# Modifié le 21042008 par Lars Bamberger pour choisir (en quelque sorte)
# automatiquement une méthode de compression basée sur la taille de la page de 
# man. Deux corrections de bogues ont été ajoutées par Dan Nicholson.
#
# Modifié le 21042008 par Dan Nicholson pour supprimer des avertissements de 
# manpath vu qu'ils apparaissent quand $MANPATH est paramétré. Suppression du
# TODO l'utilisation de la variable $MANPATH puisque manpath(1) gère déjà
# cela. 
#
# TODO : (à faire)
#     - choisir une méthode de compression par défaut basée sur l'outil
#       disponible : gzip or bzip2;
#     - offrir une option pour restaurer une sauvegarde précédente ;
#     - ajout d'autres moteurs de compression (compress, zip, etc?). Nécessaire ?

# Assez marrant, cette fonction affiche de l'aide.
function help ()
{
  if [ -n "$1" ]; then
    echo "Option inconnue : $1"
  fi
  ( echo "Utilisation : $MY_NAME <comp_method> [options] [reps]" && \
  cat << EOT
Où comp_method est un de ceux-ci :
  --gzip, --gz, -g
  --bzip2, --bz2, -b
                Compresse en utilisant gzip ou bzip2.
  --automatic
                Compresse en utilisant soit gzip ou bzip2, selon la taille du
                fichier à compresser. Les fichiers plus grands que 5
                Kio sont bzippées, les fichiers plus grands que 1 Kio asont
                gzippés et les fichiers plus petits qu'1 Kio ne sont pas.
                compressés.

  --decompress, -d
                Décompresse les pages de man.

  --backup      Spécifier qu'une sauvegarde .tar sera créé pour tous les
  répertoires.
                Si une sauvegarde existe déjà, elle est sauvegardée comme
                un .tar.old avant de faire les nouvelles sauvegardes. Si une
                sauvegarde .tar.old existe, elle est supprimée avant de 
                créer une sauvegarde. En mode sauvegarde, aucune autre action 
                n'est effectuée.

Et où options sont :
  -1 to -9, --fast, --best
                Le niveau de compression, comme accepté par gzip et bzip2.
                Lorsqu'elle n'est pas sécifiée, utilise le niveau de compression
                de la méthode donnée (-6 pour gzip, et -9 pour bzip2).
                Pas utilisée quand en modes sauvegarde ou décompression.

  --force, -F   Oblige la (re-)compression, même si celle précédente était
                la même méthode. Utile quand on change le ratio d compression.
                Par défaut, une page ne sera pas recompressée si
                elle se termine par le même suffixe que celui ajouté par la
                méthode (.bz2 pour bzip2, .gz pour gzip).

  --soft, -S    Change les liens en dur en liens mous. Utilié avec _caution_ car
                le premier fichier rencontré sera utilisé en tant que
                référence. Non utilisé en mode sauvegarde.

  --hard, -H    Transforme des liens mous en liens durs. Pas utilisé en
                mode sauvegarde.

  --conf=dir, --conf dir
                Spécifie l'emplacement de man_db.conf. Par défaut, sur /etc.

  --verbose, -v Mode verbeux, affiche le nom du répertoire traité. Doublez le
                drapeau le rend encore plus verbeux, et affichera le nom du
                fichier traité.

  --fake, -f    Trompe le script. Affiche les paramètres actuels que compressdoc 
                utilisera.

  dirs          Une liste de chemins _absolus_ vers les pages de man séparés par
                des espaces. Si et seulement si vous laissez vide, utilise
                manpath pour parser ${MAN_CONF}/man_db.conf pour toutes les 
                occurrences valides de MANDATORY_MANPATH.

Remarque sur la compression :
  Il y a eu une discussion sur blfs-support concernant les ratios de compression
  des pages de man par gzip et bzip2, prenant en compte le système de fichiers 
  hôte, l'architecture, etc... En particulier, on en a conclu que gzip était
  beaucoup plus efficace pour les 'petits' fichiers, et bzip2 sur les 'gros'
  fichiers, la notion de petit et de gros dépendant beaucoup du contenu des 
  fichiers.

  Voir le message d'origine de Mickael A. Peters, intitulé
  "Bootable Utility CD" (outil CD amorçable), datant du 09042003.1816(+0200), et
  les messages consécutifs :
  http://linuxfromscratch.org/pipermail/blfs-support/2003-April/038817.html

  Sur mon système (x86, ext3), les pages de man faisaient 35564Kio avant compression.
  gzip -9 les a compressé jusqu'à 20372Kio (57.28%), bzip2 -9 les a passés à
  19812Kio (55.71%). Soit un gain d'espace de 1.57%. YMMV.

  On n'a pas tenu compte de de la vitesse de décompression. Mais cela a-t-il un
  sens ? Vous gagnez en rapidité d'accès avec des pages de man cooppessées,
  ou vous gagnez de la place et un peu de temps.
  Bon, mon P4-2.5GHz ne me notifie même pas de ça... :-)

EOT
) | less
}

# Cette fonction vérifie que la page de man est unique entre bzip2'd,
# gzip'd et des versions décompressées.
#  $1 le répertoire dans lequel le fichier réside
#  $2 le nom du fichier de la page de man
# Retourne 0 (true, vrai)) si le fichier 
# est la dernière version et s'il doit être traitéet 1 (false, faux) si le
# fichier n'est pas la dernière version (et a donc été effacé).
function check_unique ()
{
  # NB. Quand il y a des liens en dur vers ce fichier, ils _ne_ sont _pas_
  # effacés. En fait, s'il y a des liens en dur, ils sont tous de la même
  # date/h1ore, étant ainsi prêt pour un effacement futur.

  # Construire la liste de toutes les pages de man ayant le même nom
  DIR=$1
  BASENAME=`basename "${2}" .bz2`
  BASENAME=`basename "${BASENAME}" .gz`
  GZ_FILE="$BASENAME".gz
  BZ_FILE="$BASENAME".bz2

  # Cherche et garde la plus récente
  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

  # Si le fichier spécifié était la dernière version, retourner 0
  [ "$LATEST" = "$2" ] && return 0
  # Si le fichier spécifié n'était pas la dernière version, retourner 1
  return 1
}

# Nom du script
MY_NAME=`basename $0`

# D'accord, parser la ligne de commande pour y trouver les arguments et
# initialiser dans un état intelligent, c'est-à-dire : ne pas changer l'état des
# liens, parser /etc/man_db.conf, être le plus discret, chercher man_db.conf 
# dans /etc et ne pas forcer une (re)-compression.
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
      ;;
    --automatic)
      COMP_SUF=TBD
      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\" n'est pas le nom d'un chemin absolu"
      exit 1
      ;;
  esac
done

# Redirections
case $VERBOSE_LVL in
  0)
     # O, être discret
     DEST_FD0=/dev/null
     DEST_FD1=/dev/null
     VERBOSE_OPT=
     ;;
  1)
     # 1, être un peu verbeux
     DEST_FD0=/dev/stdout
     DEST_FD1=/dev/null
     VERBOSE_OPT=-v
     ;;
  *)
     # 2 et supérieur, être très verbeux
     DEST_FD0=/dev/stdout
     DEST_FD1=/dev/stdout
     VERBOSE_OPT="-v -v"
     ;;
esac

# Remarque : sur ma machine, 'man --path' donne /usr/share/man deux fois, une
# fois avec un '/', rampant, une fois sans.
if [ -z "$MAN_DIR" ]; then
  MAN_DIR=`manpath -q -C "$MAN_CONF"/man_db.conf \
            | sed 's/:/\\n/g' \
            | while read foo; do dirname "$foo"/.; done \
            | sort -u \
            | while read bar; do echo -n "$bar "; done`
fi

# Si pas de MANDATORY_MANPATH dans ${MAN_CONF}/man_db.conf, s'arrêter
if [ -z "$MAN_DIR" ]; then
  echo "Aucun répertoire spécifié et aucun répertoire trouvé avec \`manpath'"
  exit 1
fi

# Vérifier que les réperqoires spécifiés existent effectivement et sont lisibles

for DIR in $MAN_DIR; do
  if [ ! -d "$DIR" -o ! -r "$DIR" ]; then
    echo "Le répertoire '$DIR' n'existe pas ou n'est pas lisible"
    exit 1
  fi
done

# Tromperie ?
if [ "$FAKE" != "no" ]; then
  echo "Paramètres actuels utilisés :"
  echo -n "Compression.......: "
  case $COMP_METHOD in
    --bzip2|--bz2|-b) echo -n "bzip2";;
    --gzip|--gz|-g) echo -n "gzip";;
    --automatic) echo -n "compressing";;
    --decompress|-d) echo -n "decompressing";;
    *) echo -n "unknown";;
  esac
  echo " ($COMP_METHOD)"
  echo "Niveau de compression. : $COMP_LVL"
  echo "Suffixe de compression : $COMP_SUF"
  echo -n "Forcer la compression.: "
  [ "foo$FORCE_OPT" = "foo-F" ] && echo "yes" || echo "no"
  echo "man_db.conf est....: ${MAN_CONF}/man_db.conf"
  echo -n "Liens en dur........: "
  [ "foo$LN_OPT" = "foo-S" ] &&
  echo "convertir en liens mous" || echo "leave as is"
  echo -n "Liens mous........: "
  [ "foo$LN_OPT" = "foo-H" ] &&
  echo "convertir en liens durs" || echo "leave as is"
  echo "Sauvegarde............: $BACKUP"
  echo "Tromperie (oui !).....: $FAKE"
  echo "Répertoires.......: $MAN_DIR"
  echo "Niveau de verbosité...: $VERBOSE_LVL"
  exit 0
fi

# Si aucune méthode n'a été spécifiée, afficher l'aide
if [ -z "${COMP_METHOD}" -a "${BACKUP}" = "no" ]; then
  help
  exit 1
fi

# En mode sauvegarde, faire seulement la sauvegarde
if [ "$BACKUP" = "yes" ]; then
  for DIR in $MAN_DIR; do
    cd "${DIR}/.."
    if [ ! -w "`pwd`" ]; then
      echo "On ne peut pas écrire dans le répertoire '`pwd`'"
      exit 1
    fi
    DIR_NAME=`basename "${DIR}"`
    echo "Sauvegarde de $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 -cvf "${DIR_NAME}.tar" "${DIR_NAME}" > $DEST_FD1
  done
  exit 0
fi

# Je sais que MAN_DIR n'a que des yoms de chemins absolus
# J'ai besoin de tenir compte des man localisés, donc je procède récursivement
for DIR in $MAN_DIR; do
  MEM_DIR=`pwd`
  if [ ! -w "$DIR" ]; then
    echo "On ne peut écrire dans le répertoire '$DIR'"
    exit 1
  fi
  cd "$DIR"
  for FILE in *; do
    # Corrige le cas où le répertoire est vide
    if [ "foo$FILE" = "foo*" ]; then continue; fi

    # Corrige le cas où les liens en dur voient leur plan de compression changer
    # (de non compressé en compressé, ou de bz2 en gz, ou de gz en bz2)
    # Corrige aussi le cas où plusieurs versions de la page sont présentes,
    # qu'elles soient ou non compressées
    if [ ! -L "$FILE" -a ! -e "$FILE" ]; then continue; fi

    # Ne compresse pas ces fichiers
    if [ "$FILE" = "whatis" ]; then continue; fi

    if [ -d "$FILE" ]; then
      # Nous procédons de façon récursive dans ce répertoire
      echo "-> Entrant dans ${DIR}/${FILE}..." > $DEST_FD0
      # J'ai besoin de passer --conf, car je précise le répertoire sur lequel
      # travailler. Mais je dois quitter en cas d'erreur. 
      # Nous devons faire en sens inverse avec le répertoire d'origine pour que
      # $0 soit résolu correctement.
      (cd "$MEM_DIR" && eval "$0" ${COMP_METHOD} ${COMP_LVL} ${LN_OPT} \
        ${VERBOSE_OPT} ${FORCE_OPT} "${DIR}/${FILE}") || exit $?
      echo "<- Quittant ${DIR}/${FILE}." > $DEST_FD1

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

      # Avec la compression automatique, afficher la taille du fichier 
      # décompressé (déférencement des liens symboliques), et choisir une
      # méthode de compression adaptée.
      if [ "$COMP_METHOD" = "--automatic" ]; then
        declare -i SIZE
        case "$FILE" in
          *.bz2)
            SIZE=$(bzcat "$FILE" | wc -c) ;;
          *.gz)
            SIZE=$(zcat "$FILE" | wc -c) ;;
          *)
            SIZE=$(wc -c < "$FILE") ;;
        esac
        if (( $SIZE >= (5 * 2**10) )); then
          COMP_SUF=.bz2
        elif (( $SIZE >= (1 * 2**10) )); then
          COMP_SUF=.gz
        else
          COMP_SUF=
        fi
      fi

      # Vérifier si le fichier est déjà compressé avec la méthode specifiée
      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 on a un lien symbolique
      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
          # Change this soft-link into a hard- one
          rm -f "$FILE" && ln "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
          chmod --reference "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
        else
          # Keep this soft-link a soft- one.
          rm -f "$FILE" && ln -s "${LINK}$COMP_SUF" "${FILE}$COMP_SUF"
        fi
        echo "Réédition du lien $FILE" > $DEST_FD1

      # sinon, si on a un fichier "plain"
      elif [ -f "$FILE" ]; then
        # Gérer les liens en dur : construire la liste des fichiers liés en
        # dur vers un autre qu'on {dé,}compresse.
        # NB. Ce n'est pas optimum car le fichier sera éventuellement compressé
        # autant de fois qu'il est lié en dur. Mais pour l'instant,
        # c'est une méthode sécurisée.
        inode=`ls -li "$FILE" | awk '{print $1}'`
        HLINKS=`find . \! -name "$FILE" -inum $inode`

        if [ -n "$HLINKS" ]; then
          # On a des liens en dur ! Les supprimer maintenant
          for i in $HLINKS; do rm -f "$i"; done
        fi

        # Maintenant, traiter le fichier qui n'a pas de lien en dur
        # On décompresse d'abord pour re-compresser plus tard avec le ratio de
        # compression sélectionné...
        case "$FILE" in
          *.bz2)
            bunzip2 $FILE
            FILE=`basename "$FILE" .bz2`
          ;;
          *.gz)
            gunzip $FILE
            FILE=`basename "$FILE" .gz`
          ;;
        esac

        # Compresse le fichier avec le ratio de compression donné si nécessaire
        case $COMP_SUF in
          *bz2)
            bzip2 ${COMP_LVL} "$FILE" && chmod 644 "${FILE}${COMP_SUF}"
            echo "$FILE compressé" > $DEST_FD1
            ;;
          *gz)
            gzip ${COMP_LVL} "$FILE" && chmod 644 "${FILE}${COMP_SUF}"
            echo "$FILE compressé" > $DEST_FD1
            ;;
          *)
            echo "$FILE décompressé" > $DEST_FD1
            ;;
        esac

        # Si le fichier avait des liens en dur, les recréer (en dur ou mou)
        if [ -n "$HLINKS" ]; then
          for i in $HLINKS; do
            NEWFILE=`echo "$i" | sed s/\.gz$// | sed s/\.bz2$//`
            if [ "$LN_OPT" = "-S" ]; then
              # Rendre mou ce lien dur 
              ln -s "${FILE}$COMP_SUF" "${NEWFILE}$COMP_SUF"
            else
              # Garder le lien dur en l'état
              ln "${FILE}$COMP_SUF" "${NEWFILE}$COMP_SUF"
            fi
            # Ne fonctionne vraiment que pour les liens durs. Dangereux pour 
            # les liens mous
            chmod 644 "${NEWFILE}$COMP_SUF"
          done
        fi

      else
        # Il y a un problème quand on n'a ni lien symbolique ni fichier réel.
        # On n'en arrivera bien sûr jamais là... :-(
        echo -n "Whaooo... \"${DIR}/${FILE}\" n'est ni un lien symbolique "
        echo "ni un vrai fichier. Merci de vérifier :"
        ls -l "${DIR}/${FILE}"
        exit 1
      fi
    fi
  done # for FILE
done # for DIR

EOF

En tant qu'utilisateur root, rendez exécutable compressdoc pour tous les utilisateurs :

chmod -v 755 /usr/sbin/compressdoc

Maintenant, en tant qu'utilisateur root, vous pouvez exécuter la commande compressdoc --bz2 pour compresser toutes les pages de man de votre système. Vous pouvez aussi lancer compressdoc --help pour obtenir une aide complète les possibilités du script.

N'oubliez pas que quelques paquets tels que le Système X Window et XEmacs installent aussi leur documentation à des endroits non standards (tels que /usr/X11R6/man, etc.). Assurez-vous d'ajouter ces emplacements au fichier /etc/man_db.conf, sous forme de lignes MANDATORY_MANPATH </chemin>.

Exemple :

    ...
    MANDATORY_MANPATH                       /usr/share/man
    MANDATORY_MANPATH                       /usr/X11R6/man
    MANDATORY_MANPATH                       /usr/local/man
    MANDATORY_MANPATH                       /opt/qt/doc/man
    ...

En général, les systèmes d'installation des paquets ne compressent pas les pages de man/info, ce qui veut dire que vous devrez de nouveau lancer le script si vous voulez maintenir la taille de documentation la plus petite possible. Remarquez aussi que l'exécution du script après la mise à jour d'un paquet est sécurisée ; quand vous avez plusieurs versions d'une page (par exemple, une compressée et une décompressée), celle la plus récente est conservée et les autres sont effacées.

Last updated on 2008-04-22 01:27:43 +0200