arthur.bebou

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

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

3.sh (6530B)


      1 #! page
      2 title: Advent of code 2024^\(mais c\'est l\'advent of *unix* code\) - jour 3
      3 author: Arthur Pons
      4 description: L\'advent of code 2024 version Unix
      5 publication: 2024-12-04
      6 
      7 sectionmd: main
      8 
      9 ## Jour 3
     10 
     11 [Introduction et jour 1](/aoc2024)\
     12 [Jour 2](2.html) - [Jour 4](4.html)
     13 
     14 ### Intitulé résumé
     15 
     16 #### Première partie
     17 
     18 It seems like the goal of the program is just to multiply some numbers. It does
     19 that with instructions like mul(X,Y), where X and Y are each 1-3 digit numbers.
     20 For instance, mul(44,46) multiplies 44 by 46 to get a result of 2024.
     21 Similarly, mul(123,4) would multiply 123 by 4.
     22 
     23 However, because the program's memory has been corrupted, there are also many
     24 invalid characters that should be ignored, even if they look like part of a mul
     25 instruction. Sequences like mul(4*, mul(6,9!, ?(12,34), or mul ( 2 , 4 ) do
     26 nothing.
     27 
     28 #### Deuxième partie
     29 
     30 There are two new instructions you'll need to handle:
     31 
     32  * The do() instruction enables future mul instructions.
     33  * The don't() instruction disables future mul instructions.
     34 
     35 Only the most recent do() or don't() instruction applies. At the beginning of the program, mul instructions are enabled.
     36 
     37 ### Commentaire de ma solution
     38 
     39 #### Première partie
     40 
     41 Là on est *typiquement* sur un problème chouette pour du shell. Déjà c'est de
     42 la manipulation linéaire de texte. Ensuite c'est des regex. Miam miam on se
     43 régale.
     44 
     45 Pour la première partie on va, comme pour le [premier jour](/aoc2024), se
     46 contenter de faire travailler `grep` :
     47 
     48 	< 3.input grep -Eo 'mul\([0-9]{1,3},[0-9]{1,3}\)' |
     49 	grep -Eo '[0-9]+,[0-9]+' |
     50 	tr ',' '*' | paste -s -d'+' |
     51 	bc
     52 
     53 L'astuce ici est de connaître l'option `-o` de `grep`. Celle-ci permet de
     54 demander à `grep` qu'il n'affiche que les match (un par ligne) et non les
     55 lignes dans lesquelles il a y un ou plusieurs match. Cela permet de se retrouve
     56 avec une sortie toute propre
     57 
     58     mul(1,2)
     59     mul(764,2)
     60 
     61 Qu'on peut ensuite modifier à notre guise pour piper dans bc. La transformation
     62 suite au premier `grep` peut être faite de mille façons différentes, celle ici
     63 est très "logique" pour notre cerveau mais ce n'est certainement pas la plus
     64 efficace.
     65 
     66 #### Deuxième partie
     67 
     68 Cette nouvelle version du problème revient à poser la question plus générale
     69 de comment filtrer entre deux patterns sur plusieurs lignes, un problème
     70 que je trouve étonnamment récurent. Si l'on veut, comme ici, supprimer entre
     71 deux patterns, on peut utiliser sed :
     72 
     73     sed "/don't()/,/do()/ d"
     74 
     75 Sauf que cela exclu le pattern de fin de notre intervalle. On aura encore des
     76 lignes avec `do()`. On pourrait suivre le `sed` avec `grep -v "do()"` et le tour
     77 serait jouer mais il est possible de le faire directement dans `sed` :
     78 
     79     sed "/don't()/,/do()/ d; /do()/ d"
     80 
     81 Il suffit de revérifier si la ligne contient `do()` après la première
     82 instruction et, si c'est le cas, la supprimer. Cette commande n'imprime donc
     83 que ce qui se trouve *en dehors* de l'intervalle `dont/do`, les patterns exclus.
     84 
     85 J'avais aussi une solution avec `awk` :
     86 
     87     awk 'BEGIN{p=1};/do()/{p=1};/don\047t()/{p=0};p'
     88 
     89 ou plus joliment :
     90 
     91     awk '
     92         BEGIN        {p=1}
     93         /do()/       {p=1}
     94         /don\047t()/ {p=0}
     95         p
     96         '
     97 
     98 Malheureusement pour utiliser une apostrophe dans script awk appelé depuis le
     99 shell le plus fiable est d'inscrire son code octal `\047`. Bienvenue dans le
    100 quoting hell. A part ça j'aime bien ce bout d'`awk` parce qu'il permet
    101 d'évoquer plusieurs mécanismes fondamentaux.
    102 
    103   1. Le bloc `BEGIN`
    104 
    105 `awk` fonctionne, comme la plupart des outils tradi Unix, ligne par ligne. Si
    106 l'on veut exécuter quoi que ce soit avant de commencer à manger les lignes on
    107 peut utiliser le bloc `BEGIN{}`. Ici on veut que notre état de départ soit
    108 d'imprimé, on met donc la variable `p` à 1. On verra ensuite pourquoi.
    109 
    110   2. Le schéma `/pattern/ {action}`
    111 
    112 En son coeur `awk` fonctionne toujours sous forme de `/pattern/ {action}`.
    113 `pattern` sera une regex qui tentera d'être matchée sur chaque ligne ou un
    114 expression ayant un valeur de vérité. `action` est la séquence d'instructions
    115 awk qui doivent être exec si on trouve un match pour `pattern` ou si
    116 l'expression est vraie. Ici ce mécanisme se prête très bien à notre problème.
    117 On a trois types de lignes, `don't`, `do` et les autres.  On veut faire quelque
    118 chose de différent sur chacune de ces lignes. Si on est sur une ligne `do`
    119 `/do()/` matche et on met `p` à 1, comme pour dire qu'à partir de maintenant
    120 on veut imprimer. Quand on rencontre une ligne `don't` on fait l'inverse. Le
    121 reste du temps on imprime.
    122 
    123   3. p ?
    124 
    125 On se doute maintenant que la ligne `p` permet d'imprimer. Oui mais pourquoi ?
    126 Bienvenue dans le monde des comportements implicites et paramètres par défaut,
    127 j'ai nommé les outils Unix. Il n'y a pas d'entourloupe sur ce qu'est `p`. C'est
    128 bien simplement une variable qui contient `1` ou `0`. Pas une fonction interne
    129 à `awk` qui serait un raccourci pour `print` ou que sais-je encore. La réalité
    130 qui se cache derrière cette ligne est la suivante[^1] :
    131 
    132     p==1 {print $0}
    133 
    134 En `awk` il est possible d'omettre l'action auquel cas elle sera par défaut
    135 celle d'imprimer la ligne courante. Ici c'est ce que l'on veut, on peut donc
    136 retirer l'action :
    137 
    138     p==1
    139 
    140 Il nous reste l'expression qui test si `p` est égale à 1. On peut la raccourcir :
    141 
    142     p
    143 
    144 Et voilà comment on passe de `p==1 {print $0}` à `p`. Ce sont généralement ce
    145 genre de choix de design de langage qui expliquent que de nombreuses personnes
    146 trouvent le shell, awk et perl difficile à lire. Il est utile de gardez en tête
    147 que lorsque quelque chose paraît magique dans l'un de ces langages il y a de
    148 très bonnes chance que cela s'explique par un comportement par défaut. L'usage
    149 systématique, et possiblement abusif, de ce genre de mécanismes peuvent freiner
    150 la lisibilité et maintenabilité du code. Cependant c'est aussi ce qui explique
    151 que leurs adeptes les apprécient et se sentent capable d'exprimer rapidement
    152 et facilement leurs pensées avec. Maintenant vous devriez avoir toutes les cartes
    153 en main pour comprendre la ligne du départ :
    154 
    155     awk 'BEGIN{p=1};/do()/{p=1};/don\047t()/{p=0};p'
    156 
    157 Pas si terrifiant que ça non ? 🙂
    158 
    159 [^1]: une version encore plus longue et moins idiomatique que j'avais
    160     initialement écrite était `{if(p==1}{print $0}}`. Ce format est celui-ci où
    161     l'on omet `pattern` ce qui permet d'exec `action` quoi qu'il arrive. C'est
    162     à l'intérieur de l'action qu'on test la valeur de `p` avec un `if`.