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