ralf : Rename A Lot of Files

2025-01-27T21:03:40Z

Entre tous les gestionnaires de fichiers, il y a une fonctionnalité que je trouve tout particulièrement réussie dans Thunar (le gestionnaire par défaut dans XFCE) : le renommage en masse de fichiers.

Il offre plusieurs options très pratiques pour renommer des fichiers avec des fonctionnalités comme chercher/remplacer, support des regex, à partir d'une position donnée...

Bref, même si je n'utilise plus XFCE sur mon ordinateur de travail (mais c'est l'environnement que j'installe à mes proches), j'ouvre encore Thunar pour... renommer des fichiers ^^.

Il était temps pour moi de trouver un outil qui me permet de faire la même chose dans un terminal, puisque je passe le plus clair de mon temps dedans, et si je dois gérer des fichiers, c'est avec rover.

=> https://github.com/lecram/rover

C'est ainsi qu'est née l'envie d'écrire ralf, comme "Rename A Lot of Files" (le nom a beaucoup changé...).

Vous pouvez le récupérer et le tester à partir de cette URL:

=> https://git.sr.ht/~prx/ralf

J'ignore si c'est bien portable en dehors d'OpenBSD, puisque j'utilise strlcat et sltrcpy pour assurer le bon fonctionnement du traîtement des chaînes de caractères. Je pourrais m'en passer avec memcpy() et memmove(), mais autant m'appuyer sur le travail de gens sérieux.

Sur debian, il faut installer libbsd-dev je crois.

ralf est encore tout nouveau, il y a sans doutes des améliorations à y apporter.

Avant cela, et aussi pour me permettre de réfléchir à ce que j'ai écrit, voici quelques explications sur le code.

Options? Flags?

Au départ, j'ai envisager de recopier les diverses façons de renommer que propose Thunar:

Après avoir écrit un menu pour sélectionner la méthode, j'ai finalement réalisé que les regex permettaient de faire le tout.

Liste des fichiers ?

Initialement, ralf listait les fichiers présents dans le dossier courant avec scandir(3).

C'est une fonction que j'ai déjà utilisé, qui permet d'avoir rapidement une liste des fichiers/dossiers présents.

Puis, je me suis dit que restreindre au dossier courant n'était pas très pratique, et que devoir réallouer la taille de la liste en mémoire avec des mallocs n'était pas très efficace.

De plus, on peut profiter des méthodes de sélection du shell, comme le fameux glob "*".

Alors, tant qu'à faire, je laisse le shell et l'utilisateur décrire les fichiers à renommer en les passant en argument.

En passant, j'ai réalisé que le premier argument est toujours le programme en cours d'exécution, j'ai donc retiré le premier paramètre dans argc.

Enregistrement des fichiers à traîter

Pour commencer, j'ai enregistré la liste des fichiers à éventuellement renommer dans une liste, tout simple après avoir vérifié que le fichier existe bel et bien:

int
file_exist(const char *fn)
{
        struct stat sb;
        if (stat(fn, &sb) == 0) {
                return 1;
        } else {
                return 0;
        }
}

Ça fonctionnait bien, mais cela devenait plus compliqué lorsque je voulais associer le nouveau nom de fichier et parcourir la liste en question.

Puis je me suis souvenu qu'il existe déjà tout ce qu'il faut pour ce genre de taches : les queues!

Avec sys/queue.h, il y a des méthodes toutes prêtes pour insérer/traiter dans l'ordre une liste chaînée d'objets.

Me voilà donc avec une structure qui me permet d'avoir pour chaque fichier le nom courant et à côté le nom à donner:

struct Filename {
        int modified;
        char name[FILENAME_MAX];
        char new[FILENAME_MAX];
        SIMPLEQ_ENTRY(Filename) filename;
};
SIMPLEQ_HEAD(Filelist_head, Filename);

On remarque la variable "modified", qui me permet de savoir ensuite si le fichier doit effectivement être renommé, et mettre un peu de couleur pour identifier le changement prévu.

Interface utilisateur

Je ne connais pas ncurses.h.

Alors j'en suis resté aux bons vieux fgets() et getchar().

Oups, je devrais dire getline() et getchar(), puisque le manpage de fgets() conseille d'utiliser getline dans le cas d'entrée hasardeuse.

Là encore, tout est prêt et bien pensé, c'est du gâteau.

J'ai choisi de tout simplement demander à l'utilisateur la regex à rechercher puis par quoi remplacer.

La gestion des regex.

Là, j'au un peu plus réfléchi.

Au départ, j'avais une boucle qui cherchait dans le nom du fichier les patterns correspondant à la regex entrée et qui remplaçait dans ce nom du fichier par la chaîne choisie par l'utilisateur.

Cependant, ça posait un problème de boucles infinie.

Imaginez que l'on a le fichier "babar.txt".

L'utilisateur demande de remplacer "[a-c]" par "b".

On comprend qu'il voudrait que "babar.txt" devienne "bbbbr.txt".

Cependant, puisque "b" fait partie de "[a-c]", alors ça boucle sans cesse.

J'ai donc modifié mon algorithme pour ne traîter chaque pattern qu'à une seule reprise. Au final, la fonction est nettement plus simple:

        regex_t regex;
        regmatch_t match[1];
        int regret = 0;
        int n = 0;
        int lastpos = 0;
        char buf[BUFSIZ] = {'\0'};
        char tmp[FILENAME_MAX] = {'\0'};

        /* copy the string to tmp, then modify it */
        estrlcpy(tmp, str, sizeof(tmp));

        if ((regret = regcomp(®ex, pattern, REG_EXTENDED)) != 0) {
                regerror(regret, ®ex, buf, sizeof(buf));
                regfree(®ex);
                err(1, "%s", buf);
        }

        while (regexec(®ex, tmp, 1, match, 0) == 0) {

                /* append beginning of tmp in new until match start */
                tmp[match[0].rm_so] = '\0';
                estrlcat(new, tmp, newsiz);

                /* replace this part */
                estrlcat(new, rep, newsiz);

                /* move the end of the string at the beginning of tmp */
                memmove(tmp, tmp + match[0].rm_so + 1, sizeof(tmp));

                n++;
        }
        /* at the end, copy the unmatching ending of tmp in new */
        estrlcat(new, tmp, newsiz);

        regfree(®ex);
        return n;

stack

J'ai choisi d'éviter les malloc().

Ce n'est pas par fainéantise (un peu), mais puisqu'il existe déjà FILENAME_MAX définit dans POSIX.

Par ailleurs, ça simplifie le code plutôt que d'allouer de la mémoire et lancer free() ci et là.

Ce qu'il manque

Ça, c'est à vous de me le dire ^^.

Pour l'instant, j'ai identifié le souci qui se pose quand un nom de fichier existe déjà : il est tout simplement écrasé. Mettre un petit warning serait pas mal ^^.

J'hésite aussi à virer strlcpy et strlcat pour rendre le code plus portable, et aussi pour l'exercice. Qu'en pensez-vous?

Ah, il faudrait aussi une manpage.


Une réaction?

Envoyez votre commentaire par mail:

=> mailto:prx@si3t.ch?subject=ralf

Ou rejoignez le salon XMPP:

=> fremen@chat.si3t.ch

Proxy Information
Original URL
gemini://si3t.ch/log/2025-01-27-ralf.txt
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
486.210627 milliseconds
Gemini-to-HTML Time
1.183322 milliseconds

This content has been proxied by September (3851b).