nutools/ufile

372 lines
13 KiB
Bash
Executable File

#!/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] <files...>
$scriptname [options] -r <files|dirs...>
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