Cet article est publié sous licence CC BY-NC-SA
Dans l’article précédent, intitulé une assez bonne intimité, je vous présentais GPG et le chiffrement de courriels. Nous avons alors remarqué que le contenu d’un courriel était encodé de sorte que le texte, bien que parfaitement lisible pour un anglophone, devienne totalement illisible pour un francophone du fait de l’utilisation de diacritiques dans la langue de Molière. Cette semaine, je vous propose ainsi de nous essayer à la création d’un petit plugin Vim pour décoder un bloc de texte Quoted-Printable !
L’idée étant de passer aisément de ceci :
D=C3=A8s No=C3=ABl o=C3=B9 un z=C3=A9phyr ha=C3=AF me v=C3=AAt de gla=C3=A7=
ons w=C3=BCrmiens je d=C3=AEne d=E2=80=99exquis r=C3=B4tis de b=C5=93uf au =
kir =C3=A0 l=E2=80=99a=C3=BF d=E2=80=99=C3=A2ge m=C3=BBr & c=C3=A6tera=C2=
=A0!
À cela :
Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne d’exquis rôtis de
bœuf au kir à l’aÿ d’âge mûr & cætera !
Écrire un plugin pour Vim, de prime abord, ça peut faire peur. Mais finalement, il n’y a rien de sorcier ! Il suffit de connaître un peu la structure de fichiers de Vim, d’avoir quelques notions quant au fonctionnement de notre éditeur favori, et de ne pas se laisser impressionner par un langage de programmation quelque peu exotique : j’ai nommé VimL.
Dans sa forme la plus basique, un plugin Vim n’est rien d’autre qu’un script
.vim
déposé dans un dossier plugin/
.
Celui-ci sera chargé automatiquement au démarrage de l’éditeur.
Et des plugins pour Vim, il en existe une tripotée !
En creusant un peu, on distingue deux sortes de plugins :
Notre objectif étant de nous mettre à disposition une nouvelle commande
permettant de décoder un bloc de texte formaté en Quoted-Printable au sein
d’un fichier .asc
, nous allons donc nous orienter vers ce second type de
plugin.
Pour ce faire, rien de plus simple, nous allons tout simplement créer un fichier
nommé asc_qp.vim
dans le dossier ~/.vim/ftplugin/
.
Notez le nom du dossier, ftplugin
.
Vous l’aurez compris, s’il s’était agi d’un plugin global, le dossier de
destination aurait été ~/.vim/plugin/
, tout simplement.
Si vous êtes utilisateur de NeoVim, la documentation préconise de déposer
votre script dans le dossier ~/.local/share/nvim/site/ftplugin/
.
Dans tous les cas, si le dossier n’existe pas, créez-le.
Le nom de notre fichier a son importance ici ! Il doit être nommé d’après le
type de fichier ciblé (dans notre cas, les fichiers .asc
) et peut être suffixé
au besoin pour éviter un conflit avec un autre plugin.
On aurait donc pu l’appeler asc.vim
, mais par sécurité j’ai opté pour le
suffixer d’un _qp
pour Quoted-Printable.
Au moindre doute, n’oubliez pas : l’aide de Vim est votre meilleure amie !
:help plugin
Un plugin Vim a pour but de nous mettre à disposition de nouvelles commandes. Pour cela, on va avoir la possibilité de déclarer des fonctions et des variables auxquelles ces commandes feront appel. Rien d’extraordinaire somme toute.
Plutôt que de faire un tour exhaustif du langage et de ses rudiments, je vous propose d’en découvrir quelques aspects, en l’occurrence, ceux qui nous seront utiles pour atteindre notre objectif.
Procédons par étapes. Voyons tout d’abord comment déclarer une nouvelle commande qui sera accessible dans notre éditeur.
command DecodeQP call s:qp()
Ici nous déclarons une nouvelle commande DecodeQP
qui, quand on en fera usage,
fera appel à la fonction s:qp()
.
À l’instar d’une commande builtin, nous pourrons l’appeler comme ceci :
:DecodeQP
Il est à noter que les commandes que nous créons se doivent de commencer par une majuscule de façon à ce qu’elles n’entrent pas en conflit avec une commande prédéfinie.
Attardons-nous un peu sur cette intrigante fonction s:qp()
.
Vous l’aurez deviné, il va nous falloir l’implémenter.
Mais avant cela, demandons-nous quelle est la signification du préfixe s:
?
Il s’agit là d’une singularité du langage.
Les fonctions et les variables déclarées sont préfixées par leur portée.
Ici s:
signifie que la fonction ne sera accessible qu’au sein du script où
elle est déclarée.
Les autres préfixes sont les suivants :
(aucun) Dans une fonction: local à la fonction; sinon: global
buffer-variable b: Local au buffer courant.
window-variable w: Local à la fenêtre courante.
tabpage-variable t: Local à l'onglet courant.
global-variable g: Global.
local-variable l: Local à la fonction.
script-variable s: Local au script.
function-argument a: Argument de fonction (à l'intérieur d'une fonction).
vim-variable v: Global, prédéfini par Vim.
Cela étant dit, à quoi ressemble notre fameuse fonction ?
function s:qp() abort
let l:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
execute "%!" . l:decodeqp_command
endfunction
Autre particularité du langage, nous nous apercevons de l’usage du mot-clé
abort
à la déclaration de la fonction.
Celui-ci permet de préciser un comportement attendu ; ici, en l’occurrence, nous
demandons à ce que la fonction prenne fin aussitôt une erreur survenue.
D’autres mots-clés tels que dict
, closure
ou encore range
peuvent être
utilisés en signature de fonction.
Voyons justement comment tirer parti de ce dernier !
Vim nous permet d’appeler une commande sur un intervalle (range
en anglais),
qu’il s’agisse d’un intervalle arbitraire ou d’une sélection.
Si, par exemple, nous souhaitons appliquer notre commande sur les lignes 4 à 7, il nous suffit de l’appeler comme suit :
:4,7DecodeQP
Lors de l’appel d’une commande en mode visuel, les bornes de la sélection sont
représentées par les marques '<
et '>
.
Ainsi, l’appel de notre commande sur une sélection prendra cette forme :
:'<,'>DecodeQP
Ainsi, en ajoutant le mot-clé range
à la signature de notre fonction, deux
arguments sont automatiquement mis à disposition de celle-ci : a:firstline
et
a:lastline
.
Voyons comment adapter notre fonction pour en tirer avantage :
function s:qp() range abort
let l:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
execute a:firstline . "," . a:lastline . "!" . l:decodeqp_command
endfunction
Il nous faudra aussi adapter notre commande pour préciser qu’elle accepte un intervalle.
command -range=% DecodeQP <line1>,<line2>call s:qp()
On définit par la même occasion une valeur par défaut dans le cas où un
intervalle n’est pas précisé ; cette valeur (%
) signifie que notre commande
s’appliquera sur l’ensemble du fichier, de la première à la dernière ligne.
Voyons à présent comment assouplir notre plugin. Peut-être aurez vous envie d’utiliser une autre approche pour décoder un texte Quoted-Printable ; un programme dédié comme qprint plutôt qu’une regexp Perl, par exemple. Qu’à cela ne tienne ! Donnons à l’utilisateur la possibilité de définir une commande personnalisée à exécuter.
if !exists("g:decodeqp_command")
let g:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
endif
function s:qp() range abort
execute a:firstline . "," . a:lastline . "!" . g:decodeqp_command
endfunction
Voyez la portée de notre variable decodeqp_command
s’étendre pour devenir
globale.
Ainsi, si la variable g:decodeqp_command
n’est pas définie par l’utilisateur
dans son fichier de configuration .vimrc
, on la déclare en début de script.
Peut-on faire mieux ? Ne trouvez-vous pas ça usant de devoir saisir le nom de la commande au complet, 8 caractères, dont 3 majuscules… Ne pourrait-on pas disposer d’un petit raccourci clavier ?
C’est précisément là qu’entrent en jeu les mappings. Ils permettent de faire correspondre une commande avec une combinaison de touches. Vim étant un éditeur modal, comme on a déjà eu l’occasion de le découvrir dans un précédent billet, les mappings peuvent être déclinés pour chacun des modes.
Disons que nous souhaitons utiliser la séquence gcp
en mode normal pour faire
appel à notre méthode.
Ainsi nous déclarerions notre mapping comme ceci :
nmap gcp :DecodeQP<CR>
De même, pour utiliser cette même séquence sur une sélection visuelle, nous
ferions usage de xmap
en lieu et place de nmap
.
Notez qu’il faut ajouter un retour chariot (<CR>
) pour s’assurer de
l’exécution de notre commande, faute de quoi elle ne ferait que s’afficher sur
la ligne de commande.
Mais s’agissant d’un plugin, il serait bien avisé de se prémunir d’éventuels conflits avec d’autres mappings existant.
Pour commencer, nous allons donc déclarer un mapping spécifique à notre plugin
en faisant usage du préfixe <Plug>
.
On s’assurera par la même occasion que la partie droite de notre mapping ne
pourra faire l’objet d’un mapping récursif en utilisant noremap
au lieu de
map
.
xnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <Plug>DecodeQP :DecodeQP<CR>
Petite sécurité supplémentaire, définissons nos mappings uniquement si
<Plug>DecodeQP
n’a pas déjà été employé par ailleurs.
Ainsi, si l’utilisateur souhaite définir ses propres séquences, nous ne créerons
pas de nouveaux mappings inutilement.
if !hasmapto('<Plug>DecodeQP')
xmap gcp <Plug>DecodeQP
nmap gcp <Plug>DecodeQP
endif
Soyons zélés et poussons le vice un peu plus loin !
Je vous propose d’ajouter un dernier mapping sur la séquence gcpp
pour
appliquer notre commande sur la ligne courante uniquement.
nnoremap <silent> <Plug>DecodeQPLine :call <SID>qp()<CR>
if !hasmapto('<Plug>DecodeQP')
nmap gcpp <Plug>DecodeQPLine
endif
Petite particularité ici, nous faisons fi de l’intervalle et appelons directement
la méthode qp()
à laquelle on substitue le préfixe <SID>
à s:
, un
identifiant de script unique qui évitera toute collision avec une fonction
homonyme qui pourrait être déclarée au sein d’un autre script.
Détail supplémentaire, l’usage du mot-clé <silent>
taira tout message inutile,
sans quoi :call <SNR>139_qp()
apparaitrait dans la ligne de statut en bas de
l’écran.
Afin de s’assurer que le plugin ne sera pas chargé par une version trop ancienne de Vim, et pour s’éviter un double chargement inutile, ajoutons quelques lignes en tête de notre script :
if exists("g:loaded_decodeqp") || v:version < 700
finish
endif
let g:loaded_decodeqp = 1
Voyez comme il est aisé de s’assurer de la compatibilité de notre script à
l’aide de la variable prédéfinie par Vim v:version
; ici 700
signifie que
l’on accepte le chargement dudit plugin par Vim 7.0 et supérieurs.
Si cette condition n’est pas respectée, l’interprétation de notre script prendra
fin immédiatement grâce à l’instruction finish
.
De même, si la variable globale g:loaded_decodeqp
a déjà été déclarée, c’est
que notre script a déjà été interprété et qu’il n’est nul besoin de répéter
l’opération.
Pour parfaire notre plugin, tâchons de rédiger un peu de documentation pour
informer nos utilisateurs des nouvelles fonctionnalités de leur éditeur de texte
préféré ! Pour cela, rien de plus simple : dans un dossier doc/
, nous allons
créer un bête fichier texte nommé gpg_qp.txt
.
Quelques règles typographiques sont à respecter pour tirer parti des possibilités offertes par la documentation intégrée de Vim, car oui, la documentation de notre plugin sera accessible depuis Vim via la commande suivante :
:help gpg
Il s’agit là d’une fraction du nom de notre fichier ne portant pas à confusion,
dès lors nul besoin de rechercher gpg_qp.txt
, Vim peut se contenter de gpg
pour satisfaire à votre demande.
Notre documentation se présente sous cette forme :
*gpg_qp.txt* Decode Quoted-Printable text
Author: François Vantomme <akarzim@pm.me>
License: Same terms as Vim itself (see |license|)
Decode Quoted-Printable text in ASC files. Relies on g:decodeqp_command which
can be customized, otherwise a Perl regexp will be used.
*gcp*
gcp Decode the whole file.
*v_gcp*
{Visual}gcp Decode the highlighted lines.
*:DecodeQP*
:[range]DecodeQP Decode [range] lines. Defaults to the whole file.
vim:tw=78:et:ft=help:norl:
Notez les mots-clés encadrés d’étoiles, les liens encadrés de barres verticales et la dernière ligne qui donne quelques informations à Vim quant à la largeur de page et au type de fichier notamment. Le rendu dans l’éditeur sera le suivant :
À l’installation du plugin, la documentation sera automatiquement rendue disponible par votre gestionnaire de plugins et des tags seront générés de manière à pouvoir chercher de l’aide sur les mots-clés. Pour les curieux, voici les tags générés :
:DecodeQP gpg_qp.txt /*:DecodeQP*
gcp gpg_qp.txt /*gcp*
gpg_qp.txt gpg_qp.txt /*gpg_qp.txt*
v_gcp gpg_qp.txt /*v_gcp*
Grâce à ces tags, il nous est alors possible de demander de l’aide sur gcp
et Vim nous dirigera directement sur la section de la documentation
correspondante.
Au final, nous nous retrouvons avec un petit plugin d’une vingtaine de lignes qui nous met à disposition une nouvelle commande qui lance une belle regexp sur une sélection de texte ou l’ensemble de notre fichier, avec quelques raccourcis clavier en prime, et au travers duquel nous avons pu découvrir les rudiments du langage VimL.
Voici notre plugin dans son ensemble :
" gpg_qp.vim - Decode Quoted-Printable text
" Maintainer: François Vantomme <akarzim@pm.me>
" Version: 0.5
if exists("g:loaded_decodeqp") || v:version < 700
finish
endif
let g:loaded_decodeqp = 1
if !exists("g:decodeqp_command")
let g:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
endif
function s:qp() range abort
execute a:firstline . "," . a:lastline . "!" . g:decodeqp_command
endfunction
command -range=% DecodeQP <line1>,<line2>call <SID>qp()
xnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <silent> <Plug>DecodeQPLine :call <SID>qp()<CR>
if !hasmapto('<Plug>DecodeQP')
xmap gcp <Plug>DecodeQP
nmap gcp <Plug>DecodeQP
nmap gcpp <Plug>DecodeQPLine
endif
Et si vous souhaitez tout simplement installer ce plugin, vous le retrouverez sur VimAwesome et sur GitHub.
J’espère que ce petit tour vous a plu et vous a donné envie de pousser plus loin l’exploration de Vim et de son langage de script !
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.