#!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 source "$(dirname -- "$0")/lib/ulib/auto" || exit 1 urequire multiconf function display_help() { uecho "$scriptname: classer des fichiers selon certains règles Les règles sont spécifiées dans le fichier ~/etc/default/ufile Dans ce fichier, deux tableaux contiennent les règles applicables: * le tableau RULES contient des règles de la forme pattern:destdir[:renamef] et permet de classer des fichiers correspondant à des patterns * le tableau NRULES contient des règles de la forme name:destdir[:renamef] et permet de classer des fichiers quelconques en spécifiant la règle à utiliser Les champs sont: * name est un nom quelconque, utilisé avec l'option --name * pattern est au format glob et identifie les fichiers auxquels s'applique la règle, sauf si la chaine commence par / auquel cas il s'agit d'une expression régulière reconnue par awk. Par exemple, les deux patterns suivants sont équivalents: [ab]cd*.pdf /[ab]cd.*\\\\.pdf\$ * destdir est le répertoire de destination dans lequel classer le fichier * renamef est une fonction qui permet de supporter le renommage d'un fichier lors de son classement. Sa signature est 'renamef filename pf destdir' où filename est le nom du fichier source, pf son chemin complet et destdir la valeur de destdir mentionnée dans la règle. La fonction doit définir la variable dest qui est le nouveau nom. Si la fonction retourne un code d'erreur autre que zéro, la règle est ignorée. Si le nouveau nom contient un chemin, destdir est ignoré et le fichier est déplacé dans le répertoire spécifié avec le nom spécifié. Si dest est un tableau avec plusieurs destinations, alors le fichier est copié en plusieurs fois. Si dest est de la forme [user@]host:path alors le fichier est copié par scp sur l'hôte spécifié vers la destination spécifiée, sauf si l'hôte courant est déjà celui mentionné dans la valeur, auquel cas la copie est faite directement dans le répertoire spécifié. Si le user et l'hôte courant sont déjà à la valeur spécifiée, alors la copie est faite en local sans utiliser scp sauf si l'option --force-scp est utilisée Le chemin spécifié, en local ou distant, est toujours le chemin complet vers le fichier destination. Si on veut copier le fichier sans le renommer vers un répertoire, il faut mettre un slash e.g destdir/ ou user@host:destdir/ variables pouvant être définies mais non documentées: interaction(=-i) USAGE $scriptname [options] $scriptname [options] -r ACTIONS --file Classer les fichiers spécifiés. C'est l'action par défaut -l, --list Lister les règles définies -e, --edit Lancer un éditeur sur le fichier de configuration OPTIONS -c, --config CONFIG Utiliser le fichier de configuration spécifié au lieu de la valeur par défaut ~/etc/default/ufile et ~/etc/ufile.d/*.conf -C, --other-configs Charger les fichiers ~/etc/ufile.d/*.conf en plus du fichier spécifié avec --config. Cette option est ignorée si --config n'est pas utilisé. -j, --nrule NAME Spécifier une règle du tableau NRULES à utiliser pour classer les fichiers spécifiés. Cette option peut être spécifiée autant de fois que nécessaire. Par défaut, seul le tableau RULES est utilisé pour trouver la règle à appliquer. Avec cette option, seul le tableau NRULES est utilisé. -v, --var NAME=VALUE Définir une variable qui sera utilisée par la fonction renamef. Cette option peut être spécifiée autant de fois que nécessaire. Les noms commençant par _ sont réservés et ne peuvent pas être définis. --force-cp Spécifier le mode de classement des fichiers. Par défaut, le fichier est déplacé dans la destination s'il s'agit d'un classement local, ou copié s'il s'agit d'un classement distant. Avec --force-cp, le fichier est systématiquement copié dans la destination. -m, --local-only Ignorer les classements qui auraient pour conséquence de copier le fichier sur un hôte distant. Ne traiter que les classement locaux. Cela s'applique aussi aux classements distants qui désignent l'hôte courant. -S, --ssh SSH S'il faut classer sur un hôte distant avec scp, utiliser le programme spécifié pour la connexion par ssh --force-scp Toujours utiliser scp pour une copie distante. Par défaut s'il est déterminé que l'hôte distant est en réalité l'hôte courant, alors la copie est effectuée directement. -f, --force Si le fichier destination existe, alors l'écraser sans confirmation. Cette option est ignorée pour un classement distant. -r, --recursive Classer récursivement tous les fichiers d'un répertoire. Sans cette option, il n'est pas autorisé de fournir un répertoire comme argument. -n, --fake Afficher les opérations qui seraient faites" } function joinp() { # afficher le chemin $1/$2 local pf="$1" [ -n "$2" -a "${pf%/}" == "$pf" ] && pf="$pf/" pf="$pf${2#/}" echo "$pf" } function __check_destdir() { local destdir="$1" rule="$2" if [ -z "$destdir" ]; then eerror "$rule: règle invalide: destdir est vide" return 1 fi return 0 } function __set_dest() { local dest="$1" destdir="$2" filename="$3" force_scp="$4" local userhost remotedir destname if [[ "$dest" == *:* ]]; then splitpair "$dest" userhost remotedir if [ -z "$force_scp" ] && check_userhostname "$userhost"; then # on est déjà avec le bon user sur le bon hôte if [ -n "$remotedir" ]; then splitpath "$remotedir" destdir destname setx destdir=abspath "$destdir" "$HOME" [ -n "$destname" ] || destname="$filename" setx dest=joinp "$destdir" "$destname" else setx dest=joinp "$HOME" "$filename" fi fi elif [[ "$dest" == */* ]]; then splitpath "$dest" destdir destname [ -n "$destname" ] || destname="$filename" setx dest=joinp "$destdir" "$destname" setx dest=abspath "$dest" else __check_destdir "$destdir" "$rule" || return 1 setx dest=joinp "$destdir" "$dest" fi upvar dest "$dest" return 0 } function define_vars() { local _name _value for _name in "$@"; do splitvar "$_name" _name _value if [[ "$_name" == _* ]]; then ewarn "$_name: cette variable ne peut être définie" else setv "$_name" "$_value" fi done } args=(% --help '$exit_with display_help' -c:,--config: config= -C,--other-configs other_configs=1 --file action=file -j:,--nrule: _nrules -v:,--var: _vars --force-cp force_cp=1 -m,--local-only local_only=1 -S:,--ssh: SSH= --force-scp force_scp=1 -f,--force force=1 -r,--recursive recursive=1 -n,--fake fake=1 -l,--list action=list -e,--edit action=edit ) parse_args "$@"; set -- "${args[@]}" [ -n "$action" ] || action=file ## charger toutes les règles conf_init -a RULES NRULES if [ -n "$config" ]; then if [ "$action" != edit ]; then # le fichier doit exister, sauf en mode édition où il sera créé s'il # n'existe pas déjà [ -f "$config" ] || die "$config: fichier introuvable" fi if [ -f "$config" ]; then source "$config" || die "$config: erreur lors de la lecture du fichier" fi [ -n "$other_configs" ] && conf_load "$HOME/etc/ufile.d/*.conf" else set_defaults ufile conf_load "$HOME/etc/ufile.d/*.conf" fi ## actions particulières if [ "$action" == list ]; then echo "# RULES" array_to_lines RULES echo "# NRULES" array_to_lines NRULES exit 0 elif [ "$action" == edit ]; then [ -n "$config" ] || setx config=get_user_defaults_file ufile if [ ! -f "$config" ]; then einfo "Le fichier $(ppath "$config") n'existe pas. Il sera créé avec un contenu par défaut" mkdirof "$config" || die cp "$scriptdir/lib/default/ufile" "$config" fi "${EDITOR:-vi}" "$config" exit $? elif [ "$action" != file ]; then die "bug: $action: action non implémentée" fi ## classement des fichiers if [ -n "$fake" ]; then function docmd() { qvals "$@"; } else function docmd() { "$@"; } fi if [ ${#_nrules[*]} -gt 0 ]; then array_fix_paths _nrules , array_copy _rules NRULES _nrule=1 else array_copy _rules RULES _nrule= fi [ $# -gt 0 ] || die "Vous devez spécifier des fichiers à classer" [ ${#_rules[*]} -gt 0 ] || die "Il faut définir des règles ${_nrule:+N}RULES dans ~/etc/default/ufile ou ~/etc/ufile.d/*.conf" # vérifier les règles for rule in "${_rules[@]}"; do splitpair "$rule" pattern r2 splitpair "$r2" destdir r3 splitpair "$r3" renamef r4 if [ -z "$destdir" -o "${destdir#"~/"}" != "$destdir" ]; then : elif [ "${destdir#/}" == "$destdir" ]; then ewarn "$rule: règle potentiellement problématique: destdir devrait être absolu" fi done # faire la liste des fichiers _files=() for file in "$@"; do if [ -d "$file" -a -n "$recursive" ]; then setx file=abspath "$file" array_from_lines rfiles "$(find "$file" -type f)" array_extendu _files rfiles elif [ -f "$file" ]; then setx file=abspath "$file" array_addu _files "$file" elif [ -n "$fake" ]; then : # on est en mode fake, pas grave si le fichier n'est pas trouvé elif [ -d "$file" ]; then eerror "$file: est un répertoire. essayez avec -r" else eerror "$file: fichier introuvable. il sera ignoré" fi done # faire le classement effectif r= for file in "${_files[@]}"; do setx pf=abspath "$file" setx dir=dirname -- "$pf" setx filename=basename -- "$pf" found= for rule in "${_rules[@]}"; do splitpair "$rule" pattern r2 splitpair "$r2" odestdir r3 splitpair "$r3" renamef r4 if [ "${odestdir#"~/"}" != "$odestdir" ]; then odestdir="$HOME/${odestdir#"~/"}" elif [ "$odestdir" == "~" ]; then odestdir="$HOME" fi if [ -n "$_nrule" ]; then array_contains _nrules "$pattern" || continue else if [ "${pattern#/}" != "$pattern" ]; then awk -v filename="$filename" -v pattern="${pattern#/}" 'BEGIN { exit(filename ~ pattern? 0: 1) }' || continue else eval "[[ \"\$filename\" == $(qwc "$pattern") ]]" || continue fi fi unset dest interaction=--DEFAULT-- if [ -n "$renamef" ]; then # protéger les variables nécessaires au lancement de renamef _renamef="$renamef" _filename="$filename" _pf="$pf" _odestdir="$odestdir" define_vars "${_vars[@]}" "$_renamef" "$_filename" "$_pf" "$_odestdir" || continue fi if is_array dest; then array_copy tmpdests dest dests=() for dest in "${tmpdests[@]}"; do __set_dest "$dest" "$odestdir" "$filename" "$force_scp" || break array_add dests "$dest" done elif is_defined dest; then __set_dest "$dest" "$odestdir" "$filename" "$force_scp" || break dests=("$dest") else __check_destdir "$odestdir" "$rule" || break setx dest=joinp "$odestdir" "$filename" dests=("$dest") fi i=1 mvi=${#dests[*]} [ -z "$force" ] && mvint=-i || mvint= for dest in "${dests[@]}"; do if [[ "$dest" == *:* ]]; then if [ -n "$local_only" ]; then einfo "$dest: destination ignorée à cause du mode local uniquement" continue else [ "$interaction" == --DEFAULT-- ] && int= || int="$interaction" estep "$filename --> $dest" ask_yesno $int "Voulez-vous continuer?" O || { r=1; found=x; break } docmd scp ${SSH:+-S "$SSH"} "$file" "$dest" || die "problème lors de la copie du fichier" fi else [ "$interaction" == --DEFAULT-- ] && int=-i || int="$interaction" estep "$filename --> $dest" ask_yesno $int "Voulez-vous continuer?" O || { r=1; found=x; break } setx destdir=dirname -- "$dest" docmd mkdir -p "$destdir" || die "$destdir: impossible de créer le répertoire" if [ $i -eq $mvi -a -z "$force_cp" ]; then mvdesc="du déplacement" mvcmd=mv else mvdesc="de la copie" mvcmd=cp fi docmd "$mvcmd" $mvint "$file" "$dest" || die "problème lors $mvdesc du fichier" fi i=$(($i + 1)) done [ -n "$found" ] || found=1 break done if [ -z "$found" ]; then ewarn "$file: aucune correspondance n'a été trouvée" fi done [ -n "$r" ] || r=0 exit $r