jeudi 3 janvier 2019
Ce billet a été vu pour la première fois sur le blog de Synbioz le 03 January 2019 sous licence CC BY-NC-SA.

Un plugin Vim à la mimine

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.

C’est une bonne situation ça, script ?

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

VimL

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.

Commande

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.

Fonction

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 !

Appliquer sur un intervalle

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.

Une commande qui vous ressemble

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.

Mappings

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
Bonus à la ligne

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.

Quelques vérifications

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.

Quoi d’neuf Doc ?

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 :

Vim GPG help

À 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.

Le mot de la fin

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.

GPG QP en action

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 !