arthur.bebou

Le site arthur.bebou.netlib.re - retour accueil

git clone git://bebou.netlib.re/arthur.bebou
Log | Files | Refs |

index.sh (12887B)


      1 #! page
      2 title: tsv2anything
      3 author: Arthur Pons
      4 description: Ou comment créer des systèmes de templating rudimentaires dans des systèmes POSIX
      5 publication: 2024-09-11
      6 
      7 sectionmd: main
      8 
      9 **article non relu**
     10 
     11 ## Le Besoin
     12 
     13 Il m'arrive relativement fréquemment de devoir prendre un [TSV](/amourtsv/) et
     14 de l'afficher dans un format arbitraire. Par exemple nous pouvons avoir le TSV
     15 suivant :
     16 
     17     Index  First Name  Last Name  Email                        Date of birth  Job Title
     18     1      Shelby      Terrell    elijah57@example.net         1945-10-26     Games developer
     19     2      Phillip     Summers    bethany14@example.com        1910-03-24     Phytotherapist
     20     3      Kristine    Travis     bthompson@example.com        1992-07-02     Homeopath
     21     4      Yesenia     Martinez   kaitlinkaiser@example.com    2017-08-03     Market researcher
     22     5      Lori        Todd       buchananmanuel@example.net   1938-12-01     Veterinary surgeon
     23     6      Erin        Day        tconner@example.org          2015-10-28     Waste management officer
     24     7      Katherine   Buck       conniecowan@example.com      1989-01-22     Intelligence analyst
     25     8      Ricardo     Hinton     wyattbishop@example.com      1924-03-26     Hydrogeologist
     26     9      Dave        Farrell    nmccann@example.net          2018-10-06     Lawyer
     27     10     Isaiah      Downs      virginiaterrell@example.org  1964-09-20     Engineer, site
     28 
     29 et nous voulons afficher chaque personne sous le format :
     30 
     31     [id] | First Name Last Name - Job Title
     32     Date of birth
     33     (Email)
     34     ------------------
     35 
     36 ## Implémentation
     37 
     38 J'imagine qu'il y a mille façons de faire cela. Je privilégie les
     39 implémentations peu verbeuses, POSIX et faisant appel aux abstractions du shell
     40 quand cela est possible. Cela écarte de nombreuses solutions possiblement plus
     41 élégantes et/ou (beaucoup) plus performantes au profit d'une rapidité
     42 d'implémentation et d'une certaine homogénéité d'environnement pour peu que l'on
     43 ait l'habitude d'évoluer dans du shell.
     44 
     45 ### Avec printf
     46 
     47 `printf` est en soit une commande qui permet de mettre des données dans un
     48 certain format. En lui donnant à manger notre tableau et en inscrivant notre
     49 template en argument de printf on peut recréer très rapidement un système de
     50 template.
     51 
     52 L'argument correspondant pour printf serait :
     53 
     54     %s | %s %s - %s
     55     %s
     56     (%s)
     57     ------------
     58 
     59 Il y a quelques temps j'aurais naïvement écrit quelque chose sous la forme
     60 
     61     < in.tsv tail -n+2 | #retirer le header
     62         tr '	' '\n' | #mettre un élément par ligne
     63         xargs -d'\n' -n6 printf '%s | %s %s - %s\n%s\n(%s)\n-----------' #donner exactement 6 arguments à printf à chaque fois
     64 
     65 ce qui aurait exécuté :
     66 
     67     printf %s | %s %s - %s\n%s\n(%s) 1 Shelby Terrell elijah57@example.net 1945-10-26 Games developer
     68     printf %s | %s %s - %s\n%s\n(%s) 2 Phillip Summers bethany14@example.com 1910-03-24 Phytotherapist
     69     printf %s | %s %s - %s\n%s\n(%s) 3 Kristine Travis bthompson@example.com 1992-07-02 Homeopath
     70     printf %s | %s %s - %s\n%s\n(%s) 4 Yesenia Martinez kaitlinkaiser@example.com 2017-08-03 Market researcher
     71     [...]
     72 
     73 Sauf que `printf` est malin. S'il reçoit plus de paramètres que d'endroits où
     74 les placer dans le template il boucle sur les paramètres suivant. On peut donc
     75 faire un unique appel à printf avec tous les arguments et en plaçant un
     76 judicieux retour à la ligne à la fin du template :
     77 
     78     < in.tsv tail -n+2 | #retirer le header
     79         tr '	' '\n' | #mettre un élément par ligne
     80         xargs -d'\n' printf '%s | %s %s - %s\n%s\n(%s)\n-----------\n' #donner tous les arguments d'un coup
     81 
     82 
     83 
     84 ce qui exécutera cette commande de la mort :
     85 
     86     printf '%s | %s %s - %s\n%s\n(%s)\n-----------\n' 1 Shelby Terrell elijah57@example.net 1945-10-26 Games developer 2 Phillip Summers bethany14@example.com 1910-03-24 Phytotherapist 3 Kristine Travis bthompson@example.com 1992-07-02 Homeopath 4 Yesenia Martinez kaitlinkaiser@example.com 2017-08-03 Market researcher 5 Lori Todd buchananmanuel@example.net 1938-12-01 Veterinary surgeon 6 Erin Day tconner@example.org 2015-10-28 Waste management officer 7 Katherine Buck conniecowan@example.com 1989-01-22 Intelligence analyst 8 Ricardo Hinton wyattbishop@example.com 1924-03-26 Hydrogeologist 9 Dave Farrell nmccann@example.net 2018-10-06 Lawyer 10 Isaiah Downs virginiaterrell@example.org 1964-09-20 Engineer, site
     87 
     88 qui donnera :
     89 
     90     1 | Shelby Terrell - elijah57@example.net
     91     1945-10-26
     92     (Games developer)
     93     -----------
     94     2 | Phillip Summers - bethany14@example.com
     95     1910-03-24
     96     (Phytotherapist)
     97     -----------
     98     3 | Kristine Travis - bthompson@example.com
     99     1992-07-02
    100     (Homeopath)
    101     -----------
    102     [...]
    103 
    104 Ah mais l'adresse et le métier on été interchangés ! Normal, l'adresse vient
    105 avant le métier dans le tableau et printf ne nous donne pas moyen de "nommer"
    106 spécifiquement une colonne dans template. Si dans le template la seconde colonne
    107 doit être imprimée avant la première il faut qu'elles apparaissent dans cet
    108 ordre là dans la source. Autrement dit il faut réarranger la source des données
    109 dans l'ordre d'apparition voulue dans le template. Pour notre cas cela pourrait
    110 être :
    111 
    112     $ < in.tsv awk -F'\t' -vOFS='\t' '{print $1,$2,$3,$6,$5,$4}' |
    113         head -n2
    114     Index  First Name  Last Name  Job Title        Date of birth  Email
    115     1      Shelby      Terrell    Games developer  1945-10-26     elijah57@example.net
    116 
    117 Donc si l'on met tout bout à bout :
    118 
    119     $ < in.tsv awk -F'\t' -vOFS='\t' '{print $1,$2,$3,$6,$5,$4}' |
    120         tail -n+2 | #retirer le header
    121         tr '	' '\n' | #mettre un élément par ligne
    122         xargs -d'\n' printf '%s | %s %s - %s\n%s\n(%s)\n-----------\n' #donner tous les arguments d'un coup
    123 
    124     1 | Shelby Terrell - Games developer
    125     1945-10-26
    126     (elijah57@example.net)
    127     -----------
    128     2 | Phillip Summers - Phytotherapist
    129     1910-03-24
    130     (bethany14@example.com)
    131     -----------
    132     3 | Kristine Travis - Homeopath
    133     1992-07-02
    134     (bthompson@example.com)
    135     -----------
    136     4 | Yesenia Martinez - Market researcher
    137     2017-08-03
    138     (kaitlinkaiser@example.com)
    139     -----------
    140     [...]
    141 
    142 On peut déplier le format printf pour plus de clarté :
    143 
    144     xargs -d'\n' printf '
    145     %s | %s %s - %s
    146     %s
    147     (%s)
    148     -----------'
    149 
    150 #### Avantages
    151 
    152   * Relativement rapide
    153 
    154 Prenons notre fichier de test de 10 lignes et dupliquons le de façon à avoir un
    155 fichier de 1 100 000 lignes. L'impression des environs 4 millions de ligne
    156 prend autour de 6/7 secondes sur ma machine, à vous de décider si c'est
    157 raisonnable ou pas. On doit évidemment pouvoir faire beauuuuucoup plus rapide
    158 avec un programme compilé et un peu optimisé.
    159 
    160   * Permet de profiter des fonctionnalités de formatage de printf
    161 
    162 `printf` inclu de nombreuses possibilités de formatage des données. Par
    163 exemple, si l'on ne veut pas que le métier dépasse 10 caractères de long :
    164 
    165 	%s | %s %s - %.5s
    166 	%s
    167 	(%s)
    168 	-----------
    169 
    170   * Peu de ligne de code, possibilité de le réécrire de zéro très rapidement en
    171   l'adaptant au besoin
    172 
    173 Une remarque ici est que passé une certaine quantité d'arguments il faut
    174 préciser à xargs un nombre max par appel à printf. Si on ne le fait pas il se
    175 pourrait que l'on rencontre la limite du nombre d'argument de votre système et
    176 que la totalité du tableau ne soit pas imprimées. [Cet excellent
    177 article](https://www.in-ulm.de/~mascheck/various/argmax/) évoque le sujet en
    178 détail. En l'occurence `xargs --show-limits` permet de savoir ce que l'on
    179 peut mettre derrière `-n` mais pour être safe côté portabilité on peut utiliser
    180 le plus grand multiple du nombre d'éléments à afficher dans le template
    181 inférieur à 4096.
    182 
    183 #### Inconvénients
    184 
    185   * Pas de séparation template/code
    186 
    187 En l'état le template vit dans le code. Il faut donc dupliquer le script en autant de temaplte que l'on a. On devrait pouvoir faire un peu de méta-programmation pour contourner cette contrainte mais je ne l'ai pas fait.
    188 
    189   * Nécessite un prétraitement
    190 
    191 Tout se passe parfaitement bien si toutes les données sont dans l'ordre mais il
    192 est obligatoire d'avoir un léger prétraitement pour réordonner le tableau dans
    193 l'ordre du template.
    194 
    195   * Le template ne comporte pas de variables nommées
    196 
    197 Il faut jeter un oeil aux données (et possiblement au prétraitement) pour se
    198 souvenir de quoi va où. Peut mieux faire en terme de maintenabilité.
    199 
    200 ### Variables shell + here-doc "à la catium"
    201 
    202 Si on instancie les variables shell
    203 
    204 	name="Jean"
    205 	dob="1994-06-09"
    206 	job="mailman"
    207 
    208 On peut ensuite les appeler dans un here-doc
    209 
    210 	<<delim cat
    211 	Mister $name, born on $dob, is a $job
    212 	delim
    213 
    214 Le here-doc peut vivre dans un fichier séparé, nommé `layout` par exemple.
    215 L'astuce est donc d'avoir un peu de code qui pour chaque ligne de notre TSV
    216 instancie les variables et appel le here-doc. Le nom de la variable contenant
    217 les valeurs de la première colonne sera la valeur se trouvant dans l'en-tête du
    218 tableau de la première colonne. Le code suivant en est un exemple, il prendra
    219 dans stdin le tableau et en argument le fichier contenant le here-doc :
    220 
    221 	tmpd=$(mktemp -d)
    222 	tee $tmpd/all | head -n1 |
    223 		tr '	' '\n' > $tmpd/vars
    224 	tail -n+2 $tmpd/all |
    225 	while read line;do
    226 		eval $(echo "$line" | tr '	' '\n' |
    227 			   	   paste -d '=' $tmpd/vars - |
    228 				   sed -E 's/"/\\\"/g' |
    229 				   sed -E 's/=/&"/;s/$/"/')
    230 		. "$1"
    231 	done
    232 	rm -rf $tmpd
    233 
    234 En reprenant notre exmple de printf le layout ressemblera à :
    235 
    236 	<<delim cat
    237     $id | $firstname $lastname - $job
    238     $dob
    239     ($email)
    240     -----------'
    241 	delim
    242 
    243 Les variables ne pouvant pas contenir d'espace il faudra renommer les entêtes :
    244 
    245 	id	firstname	lastname	email	dob	job
    246 	1		Terrell	elijah57@example.net	1945-10-26	Games developer
    247 	2	Phillip	Summers	bethany14@example.com	1910-03-24	Phytotherapist
    248 	[...]
    249 
    250 Il reste a appeler notre script (nommé `printlayout` pour l'occasion) avec le fichier de notre here-doc en argument :
    251 
    252     $ < in.tsv ./printlayout ./layout
    253 
    254 	1 |  Terrell - Games developer
    255 	1945-10-26
    256 	(elijah57@example.net)
    257 	-----------
    258 	2 | Phillip Summers - Phytotherapist
    259 	1910-03-24
    260 	(bethany14@example.com)
    261 	-----------
    262 	3 | Kristine Travis - Homeopath
    263 	1992-07-02
    264 	(bthompson@example.com)
    265 	-----------
    266 	4 | Yesenia Martinez - Market researcher
    267 	2017-08-03
    268 	(kaitlinkaiser@example.com)
    269 	-----------
    270 	[...]
    271 
    272 #### Avantages
    273 
    274   * Le template est explicite avec des noms de variables aussi claires que vous
    275   ne pouvez les nommer
    276 
    277 Cela implique également que le prétraitement n'est pas nécessaire pour
    278 réordonner le tableau. L'ordre des des données et du template peut être
    279 décorrélé. Le template est également plus facile à maintenir.
    280 
    281   * N'importe quel traitement shell peut être inclus dans le here-doc
    282 
    283 Il est possible de mettre du shell plus avancé dans le template. Par exemple,
    284 en utilisant l'interpolation des variables shell, mettre une valeur par défaut
    285 si elle n'existe pas dans les données ou terminer l'affichage avec un erreur
    286 s'il manque une donnée cruciale :
    287 
    288 	<<delim cat
    289     ${id:?missing id, something is wrong} | $firstname $lastname - ${job:-no known job}
    290     ${dob:-date of birth unknown}
    291     ($email)
    292     -----------
    293 	delim
    294 
    295 Il est également possible de mettre des conditions pour que le template diffère
    296 légèrement basé sur la valeur d'une variable par exemple :
    297 
    298 	<<delim cat
    299     $([ "$firstname" = "Alice" ] \
    300         && $id | $firstname $lastname - $job \
    301         || $id | $firstname (wow what a pretty name) $lastename - $job
    302     )
    303     ${dob:-date of birth unknown}
    304     ($email)
    305     -----------
    306 	delim
    307 
    308 Qui donnera sur les lignes avec Alice comme prénom :
    309 
    310     1 | Alice (wow what a pretty name) Terrell - Games developer
    311     1945-10-26
    312     (elijah57@example.net)
    313     -----------
    314     2 | Phillip Summers - Phytotherapist
    315     1910-03-24
    316     (bethany14@example.com)
    317     -----------
    318 
    319 Si cela est vraiment un avantage pourrait être sujet à débat. Il pourrait être
    320 considéré comme de mauvais goût d'ajouter de l'intelligence dans le template
    321 directement. A vous de vous faire votre avis.
    322 
    323 #### Inconvénients
    324 
    325   * C'est lent
    326 
    327 Cette technique prend entre trois et quatre secondes pour formater 1000 lignes
    328 du tableau. Elle n'est pas appropriée pour de grands jeux de données. Pour générer
    329 des petites listes pour un site web c'est tout à fait correct et déjà utilisé
    330 en production sur plusieurs sites.
    331 
    332 ### D'autres
    333 
    334 On peut imaginer pleins d'autres implémentations et je vais tenter de les
    335 renseigner ici. Une version assez simpliste contenu dans un binaire C avec un
    336 format de templating raisonnable pourrait être le meilleur des deux mondes bien
    337 que moins simple à recoder/modifier à la volée.
    338 
    339 Je suis également persuadé que l'on pourrait faire quelque chose de bien plus
    340 malin et efficace entre perl/awk mais je n'ai pas l'énergie d'y réfléchir à
    341 l'instant.
    342