Logo Gnome Files On trouve toutes sortes de scripts Nautilus sur internet, car ils peuvent rendre des services aussi variés que précieux, mais beaucoup de ceux que l’on croise présentent des défauts de conception majeurs : ils n’itèrent pas, ou mal, sur la liste des fichiers qui leurs sont fournis, et ils ne peuvent pas gérer des fichiers situés sur un système virtuel distant monté par GFVS, typiquement un partage Samba, Webdav ou (S)FTP. Ce billet va tenter de définir une manière fiable et universelle de gérer les fichiers sélectionnés et fournis aux scripts Nautilus.

Les scripts Nautilus, kézako ?

Nautilus est le nom historique du navigateur de fichiers du projet Gnome. Il s’appelle désormais Gnome Files. On en retrouve des dérivés nommées Caja ou Nemo, qui possèdent eux aussi un support pour ces scripts. Si vous ne savez pas ce que sont ces “scripts Nautilus”, il s’agit de scripts que l’on peut appliquer sur un ou plusieurs fichiers et dossiers afin d’effectuer des actions prédéfinies.

Les scripts Nautilus sont des programmes exécutables placés dans le répertoire $HOME/.local/share/nautilus/scripts et ses sous-dossiers. Nautilus détectera tous les fichiers exécutables dans cette arborescence et proposera de les invoquer via le menu contextuel, dans le sous-menu “Scripts”. Pour désactiver un script, il n’est donc pas nécessaire de le supprimer du dossier, il suffit de le rendre non exécutable.

Exemple menu script Nautilus

Ces scripts peuvent être développés dans n’importe quel langage de programmation. Il en existe en Python et en Perl, mais s’agissant de petits scripts dont le but est de manipuler des fichiers ou d’interagir avec un système Linux, le langage Bash est utilisé la plupart du temps.

Lorsqu’on donne des fichiers et dossiers classiques aux scripts Nautilus, tout fonctionne comme prévu. Mais par “classique”, entendez “sur un système de fichier local” et “avec un nom de fichier sans caractère problématique”.

Nous allons voir que des problèmes se posent dans deux situations :

  • Quand, depuis Nautilus, on accède à des systèmes de fichier distants montés par GIO/GVFS (partages Samba, Webdav, SFTP, etc.) ;
  • Quand les fichiers ont un retour à la ligne dans leur nom.

Afin de prévoir d’éventuelles spécificités liées aux versions des logiciels utilisés, je précise que je travaille sur un système GNU/Linux Ubuntu 20.04 LTS avec Nautilus version 3.36.3.

Les variables spécifiques

Nautilus va fournir au script invoqué plusieurs informations relatives aux éléments sélectionnés. Le script pourra ensuite en faire usage si besoin.

Au delà des variables habituelles ($@, $*, $1, $2, etc.), nous avons à disposition les variables spécifiques suivantes :

  • NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
  • NAUTILUS_SCRIPT_SELECTED_URIS
  • NAUTILUS_SCRIPT_CURRENT_URI
  • NAUTILUS_SCRIPT_WINDOW_GEOMETRY

D’autre part la variable $PWD, moins connue mais toujours disponible en Bash, va nous être utile car elle indique le répertoire de travail courant (celui visible dans Nautilus).

Testons un script basique

Pour les besoin de ce tutoriel, on va créer un script tout simple qui affiche le contenu des variables mentionnées ci-dessus. Le script n’étant pas exécuté dans un shell interactif, il est nécessaire de créer un fichier de log dans lequel on pourra suivre son déroulement (par exemple avec tail -f)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
exec &>> "$HOME/nautilus-script-$(basename "$0").log"

echo "\$@=$@"
echo "\$1=$1"
echo "\$2=$2"
echo "\$3=$3"
echo "\$4=$4"
echo
echo "PWD=$PWD"
echo
echo "NAUTILUS_SCRIPT_SELECTED_FILE_PATHS='${NAUTILUS_SCRIPT_SELECTED_FILE_PATHS}'"
echo "NAUTILUS_SCRIPT_SELECTED_URIS='${NAUTILUS_SCRIPT_SELECTED_URIS}'"
echo "NAUTILUS_SCRIPT_CURRENT_URI='${NAUTILUS_SCRIPT_CURRENT_URI}'"
echo "NAUTILUS_SCRIPT_WINDOW_GEOMETRY='${NAUTILUS_SCRIPT_WINDOW_GEOMETRY}'"

Créons une arborescence de fichiers de test en insérant volontairement des espaces dans les noms des éléments, ainsi qu’un retour à la ligne dans un nom de fichier :

vetetix@ordi:~$ mkdir -p "$HOME/demo/sous dossier"
vetetix@ordi:~$ touch "$HOME/demo/fichier 1.txt"
vetetix@ordi:~$ touch "$HOME/demo/fichier 2
> avec newline.txt"
vetetix@ordi:~$ touch "$HOME/demo/sous dossier/fichier 3.txt"

Sélectionnons ces trois fichiers et invoquons le script. Voici ce que ça donne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$@=sous dossier/fichier 3.txt fichier 1.txt fichier 2
avec newline.txt
$1=sous dossier/fichier 3.txt
$2=fichier 1.txt
$3=fichier 2
avec newline.txt
$4=

PWD=/home/vetetix/demo

NAUTILUS_SCRIPT_SELECTED_FILE_PATHS='/home/vetetix/demo/sous dossier/fichier 3.txt
/home/vetetix/demo/fichier 1.txt
/home/vetetix/demo/fichier 2
avec newline.txt
'
NAUTILUS_SCRIPT_SELECTED_URIS='file:///home/vetetix/demo/sous%20dossier/fichier%203.txt
file:///home/vetetix/demo/fichier%201.txt
file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt
'
NAUTILUS_SCRIPT_CURRENT_URI='file:///home/vetetix/demo'
NAUTILUS_SCRIPT_WINDOW_GEOMETRY='1310x694+56+27'

On remarque que :

  • les variables NAUTILUS_SCRIPT_SELECTED_FILE_PATHS (ligne 11) et NAUTILUS_SCRIPT_SELECTED_URIS (ligne 16) contiennent la liste des fichiers séparés par des retours à la ligne ;
  • Ces variables ont un retour à la ligne comme dernier caractère.
    Il pourrait être nécessaire de le retirer manuellement pour pouvoir itérer proprement sur leur contenu. Sans cela, on générerait une itération supplémentaire sur une ligne vide avec un while read.
    Ça peut se faire simplement avec l’expansion de paramètres Bash (en) ${var%$'\n'} qui retire un éventuel retour à la ligne en fin de chaîne ;
  • La variable NAUTILUS_SCRIPT_SELECTED_FILE_PATHS gère mal les retours à la ligne dans les noms de fichiers (cf lignes 13 et 14).
    On ne peut donc pas l’utiliser pour itérer sur les noms de fichier.

On pourrait donc soit itérer sur la variable $@ (for fichier in "$@"; do…), soit sur la variable $NAUTILUS_SCRIPT_SELECTED_URIS (après suppression du dernier retour à la ligne).

Testons ce même script basique sur un répertoire monté par gvfs

Dans l’étape précédente, on a utilisé le script sur le système de fichier local. Mais si on l’utilise sur un système de fichier virtuel monté par gvfs, en l’occurrence le partage webdav d’un compte Nextcloud, regardons ce que ça donne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$@=
$1=
$2=
$3=
$4=

PWD=/run/user/1000/gvfs/dav:host=monserveur.net,ssl=true,user=vetetix,prefix=%2Fremote.php%2Fwebdav

NAUTILUS_SCRIPT_SELECTED_FILE_PATHS=''
NAUTILUS_SCRIPT_SELECTED_URIS='davs://vetetix@monserveur.net/remote.php/webdav/sous%20dossier/Nouveau%20fichier.txt
davs://vetetix@monserveur.net/remote.php/webdav/monfichier.txt
'
NAUTILUS_SCRIPT_CURRENT_URI='davs://vetetix@monserveur.net/remote.php/webdav'
NAUTILUS_SCRIPT_WINDOW_GEOMETRY='1310x694+56+27'

On constate que les variables classiques ($@, $1 et suivantes), ainsi que NAUTILUS_SCRIPT_SELECTED_FILE_PATHS sont vides, et donc non disponibles. Un script nautilus à vocation généraliste, que l’on pourrait partager sur internet, ne doit donc pas se baser sur ces variables puisqu’il ne fonctionnerait pas dans toutes les conditions.

On ne peut donc plus se baser que sur la variable $NAUTILUS_SCRIPT_SELECTED_URIS, qui est la seule qui est disponible dans toutes les conditions, en gérant correctement les retours à la ligne.

Comment itérer sur NAUTILUS_SCRIPT_SELECTED_URIS

On a vu que la variable $NAUTILUS_SCRIPT_SELECTED_URIS contient chaque URI suivie d’un retour à la ligne.

De plus, chaque URI présente ses caractères spéciaux encodés, en particulier les espaces, retours à la ligne et autres “whitespaces” de IFS.

On a donc deux possibilité : une boucle for et une boucle while read.

Avec une boucle for

On peut simplement itérer sur la variable non quotée :

for selected_uri in $NAUTILUS_SCRIPT_SELECTED_URIS; do
    do_something "$selected_uri"
done

Pour éviter d’itérer sur une variable non quotée, on peut préfèrer la transformer en array, puis itérer sur celui-ci :

read -a selected_uris <<< "$NAUTILUS_SCRIPT_SELECTED_URIS"
for selected_uri in "${selected_uris[@]}"; do
    do_something "$selected_uri"
done

Avec une boucle while

Ici aussi, on va itérer sur chaque ligne de la variable, dont supprime le retour à la ligne final :

while read -r selected_uri; do
    do_something "$selected_uri"
done <<< "${NAUTILUS_SCRIPT_SELECTED_URIS%$'\n'}"

Comme les espaces contenues dans les URI sont encodées, il n’est pas nécessaire de modifier l’IFS, contrairement à ce qui se fait habituellement avec ce type de boucle.

La première solution est néanmoins la plus simple, tout en restant sûre.

Transformer une URI en chemin local

On a vu qu’on devait boucler sur les URIs contenues dans $NAUTILUS_SCRIPT_SELECTED_URIS, mais on ne peut pas fournir directement une URI à un programme :

vetetix@ordi:~$ ls file:///home/vetetix/demo/fichier%201.txt
ls: impossible d'accéder à 'file:///home/vetetix/demo/fichier%201.txt': Aucun fichier ou dossier de ce type

Il faut donc transformer ces URI en chemins locaux. On a pour cela deux options : l’utilisation de gio, ou une solution “à la main”.

Utiliser GIO pour transformer une URI en chemin local

Les systèmes de fichiers virtuels étant créés par GVFS, on peut utiliser gio pour traduire les URIs en chemins locaux, car celui-ci sait par définition quel est le point de montage local d’un système de fichier virtuel GVFS :

vetetix@ordi:~$ gio info file:///home/vetetix/demo/fichier%201.txt | grep "local path" | cut -d ' ' -f 3-
/home/vetetix/demo/fichier 1.txt

vetetix@ordi:~$ gio info davs://vetetix@monserveur.net/remote.php/webdav/sous%20dossier/Nouveau%20fichier.txt | grep "local path" | cut -d ' ' -f 3-
/run/user/1000/gvfs/dav:host=monserveur.net,ssl=true,user=vetetix,prefix=%2Fremote.php%2Fwebdav/sous dossier/Nouveau fichier.txt

Ça a l’air de bien fonctionner. Par contre, la solution ci-dessus pose problème avec des retours à la ligne, puisque le nom du fichier se retrouve affichée sur plusieurs lignes, alors qu’on ne récupère que la première :

vetetix@ordi:~$ gio info file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt | grep "local path" | cut -d ' ' -f 3-
/home/vetetix/demo/fichier 2

On doit donc utiliser plus finement grep pour récupérer tout le chemin local, sur plusieurs lignes, avec l’option “-A 1” (s’il y a un seul retour à la ligne) :

vetetix@ordi:~$ gio info file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt | grep -A 1 '^local path: '
local path: /home/vetetix/demo/fichier 2
avec newline.txt

S’il y a plus d’un retour à la ligne dans le chemin complet (dans le nom du fichier ou dans ses répertoires parents), il faut les compter, puis prendre ce nombre en compte :

vetetix@ordi:~$ N=$(echo -n "file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt" | grep -Fo '%0A' | wc -l)
vetetix@ordi:~$ gio info file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt | grep -A "$N" '^local path: '
local path: /home/vetetix/demo/fichier 2
avec newline.txt

Maintenant, il faut juste retirer le début de ligne (“local path: “) pour obtenir le chemin local complet :

vetetix@ordi:~$ N=$(echo -n "file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt" | grep -Fo '%0A' | wc -l)
vetetix@ordi:~$ var=$(gio info file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt | grep -A "$N" '^local path: ')
vetetix@ordi:~$ printf '%s' "${var#local path: }"
/home/vetetix/demo/fichier 2
avec newline.txt

Dans une fonction, cela donne ceci :

1
2
3
4
5
6
get_path_from_uri() {
    local newlines path_lines
    newlines=$(echo -n "$1" | grep -Fo '%0A' | wc -l)
    path_lines=$(gio info "$uri" | grep -m 1 -A "$newlines" '^local path: ')
    echo -n "${path_lines#local path: }"
}

Mais… il y a un “mais” : cette solution utilisant la sortie de gio impose l’utilisation de command substitution dont la sortie est assignée à une variable (var=$(command)). Cela fait que si notre nom de fichier se termine par un ou plusieurs retours à la ligne, ceux-ci seront supprimés et le script plantera.

D’autre part, on utilise aussi la fonction grep, qui a la fâcheuse caractéristique de renvoyer un code de sortie non nul lorsqu’elle ne trouve aucune des chaines recherchées. Les gens qui utilisent le unofficial bash strict mode (set -euo pipefail ou mieux, set -eEuo pipefail) préfèreront s’en passer.

On doit donc abattre notre dernière carte, la solution “a la mano”.

Transformer manuellement une URI en chemin local

Il est possible de transformer une URI en chemin local à la main, avec les élements dont on dispose déjà, et sans utiliser de substitution de commande.

On constatera que la valeur initiale de $PWD représente le chemin local de $NAUTILUS_SCRIPT_CURRENT_URI, et qu’il contient donc le point de montage d’un éventuel système de fichier virtuel :

# Sur un système de fichier local :
PWD=/home/vetetix/demo
NAUTILUS_SCRIPT_CURRENT_URI='file:///home/vetetix/demo'
# Sur un système de fichier virtuel :
PWD=/run/user/1000/gvfs/dav:host=monserveur.net,ssl=true,user=vetetix,prefix=%2Fremote.php%2Fwebdav
NAUTILUS_SCRIPT_CURRENT_URI='davs://vetetix@monserveur.net/remote.php/webdav'

On va donc utiliser la valeur initiale de $PWD associée à l’URI dont on aura retiré le préfixe correspondant à $NAUTILUS_SCRIPT_CURRENT_URI.

Attention, la valeur de $PWD peut changer si une commande (par exemple un cd) change le répertoire actuel. Il faut donc très tôt en récupérer la valeur dans une autre variable, pour la sanctuariser (je l’appellerai $IWD pour Initial Working Directory).

Récupérer la partie finale du chemin

Pour enlever la première partie de l’URI, on va à nouveau utiliser une expansion de paramètre de Bash : ${MON_URI#"$NAUTILUS_SCRIPT_CURRENT_URI"} (ici, MON_URI est la variable à traiter).

vetetix@ordi:~$ MON_URI='file:///home/vetetix/demo/sous%20dossier/fichier%203.txt'
vetetix@ordi:~$ NAUTILUS_SCRIPT_CURRENT_URI='file:///home/vetetix/demo'
vetetix@ordi:~$ echo "${MON_URI#"$NAUTILUS_SCRIPT_CURRENT_URI"}"
/sous%20dossier/fichier%203.txt

Décoder les caractères encodés de l’URI

Il faut aussi décoder les caractères spéciaux de l’URI qui font l’objet d’un encodage-pourcent, ce que l’on va faire en les convertissant en encodage hexadécimal (il suffit de remplacer “%” par “\x”), puis en interprétant ceux-ci avec la commande printf et le format de sortie'%b' en lieu et place de l’habituel '%s' :

vetetix@ordi:~$ percent_var="/sous%20dossier/fichier%203.txt"
vetetix@ordi:~$ hex_var=${percent_var//%/\\x}
vetetix@ordi:~$ echo "$hex_var"
/sous\x20dossier/fichier\x203.txt
vetetix@ordi:~$ printf '%b' "$hex_var"
/sous dossier/fichier 3.txt

La partie de lego pour tout mettre ensemble

On retrouve alors le chemin “local” en concaténant la valeur de $PWD ($IWD) avec la fin de l’URI décodée :

vetetix@ordi:~$ IWD=/home/vetetix/demo
vetetix@ordi:~$ NAUTILUS_SCRIPT_CURRENT_URI="file:///home/vetetix/demo"
vetetix@ordi:~$ MON_URI="file:///home/vetetix/demo/fichier%202%0Aavec%20newline.txt"
vetetix@ordi:~$ FIN_URI=${MON_URI#"$NAUTILUS_SCRIPT_CURRENT_URI"}
vetetix@ordi:~$ printf -v CHEMIN '%s%b' "${IWD}" "${FIN_URI//%/\\x}"
vetetix@ordi:~$ echo "$CHEMIN"
/home/vetetix/demo/fichier 2
avec newline.txt

Dans une fonction, cela pourrait donner ceci :

1
2
3
4
5
readonly IWD=$PWD
set_path_from_uri() {
    uri_end=${1#"$NAUTILUS_SCRIPT_CURRENT_URI"}
    printf -v selected_path '%s%b' "${IWD}" "${uri_end//%/\\x}"
}

Vous aurez remarqué que cette fonction ne renvoit rien, mais définit la valeur de la variable (globale) selected_path. À l’utilisateur de faire ce qui est nécessaire pour ne pas casser le contenu de cette variable par la suite.

Conclusion : un script moins simple, mais plus sain

Maintenant qu’on a trouvé toutes les briques nécessaires pour invoquer un script Nautilus sans se prendre les pieds dans le tapis que sont retours à la ligne et les systèmes de fichier virtuels, mettons le tout dans un script basique qui va simplement faire un ls -ld sur chaque fichier ou dossier sélectionné.

J’ai une préférence pour la conversion directe de l’URI en chemin sans passer par une fonction ni une variable globale :

1
2
3
4
5
6
7
8
#!/bin/bash
readonly IWD=$PWD
for selected_uri in $NAUTILUS_SCRIPT_SELECTED_URIS; do
    uri_end=${selected_uri#"$NAUTILUS_SCRIPT_CURRENT_URI"}
    printf -v selected_path '%s%b' "${IWD}" "${uri_end//%/\\x}"
    # Do something here:
    ls -ld "$selected_path"
done

Ce code devrait servir de base pour toute création de script Nautilus en Bash, car il n’est finalement pas très compliqué et permet d’éviter bien des écueils.

J’ai testé ce code, qui fonction avec tous les fichiers que je leur ai soumis :

  • avec des espaces (heureusement) ;
  • avec des retours à la ligne, y compris en fin de nom ;
  • avec des astérisques ou des points d’interrogation ;
  • avec des backslash (bien que ls affiche mal le résultat dans ce cas, mais ce n’est pas la faute des scripts Nautilus).

Pour rappel, la méthode la plus courante pour itérer sur les fichiers fait trois lignes de moins, mais nous avons vu qu’elle n’est pas universelle :

1
2
3
4
5
#!/bin/bash
for arg; do
    # Do something here:
    ls -ld "$arg"
done

Si vous avez une solution plus claire, plus concise, ou plus générale, n’hésitez pas à me la soumettre afin que je mette à jour mon billet.

Ressources supplémentaires

Quelques ressources si vous voulez explorer un peu plus ces scripts Nautilus :