# Modern CLI tools for your terminal A series of 9 article(s), in reading order. # L'utilitaire jq pour Linux > Utilisez le puissant utilitaire Linux jq pour manipuler des données JSON, embellir la sortie et filtrer des nœuds directement depuis votre ligne de commande et vos scripts shell. --- ![L'utilitaire jq pour Linux](/img/v2/json.webp) **TL;DR** Cet article présente `jq`, un processeur JSON en ligne de commande : rediriger la sortie brute d'une API (ici l'API random-user) dans `jq` suffit à la mettre en forme, et ajouter une expression de filtre comme `jq '.results[0].name'` permet d'extraire uniquement un nœud précis de la structure JSON. `jq` est un utilitaire puissant pour Linux qui permet de manipuler des données JSON depuis la ligne de commande et qui s'intègre dans vos scripts shell. ## La même réponse, deux fois {#the-same-answer-twice} En lançant `curl https://randomuser.me/api/`, vous obtiendrez quelque chose comme ceci : ```bash $ curl https://randomuser.me/api/ ``` ```json {"results":[{"gender":"female","name":{"title":"Mademoiselle","first":"Milena","last":"Martin"},"location":{"street":{"number":9831,"name":"Rue de L'Abbé-Migne"},"city":"Lengnau (Ag)","state":"Basel-Landschaft","country":"Switzerland","postcode":3789,"coordinates":{"latitude":"-60.0739","longitude":"135.1462"},"timezone":{"offset":"+7:00","description":"Bangkok, Hanoi, Jakarta"}},"email":"milena.martin@example.com","login":{"uuid":"bafdf972-4183-484a-903a-84f2654f0fec","username":"purpleleopard344","password":"cameron","salt":"gGoFrP1a","md5":"af359ca6697c3ac68f4c190583544619","sha1":"19033bc1630d96bba29726823ef91f53800e67d1","sha256":"cac6a35f5135f14707b8b3ec48617f23b4105b1c5e33a2b64597e7a0c7c891a0"},"dob":{"date":"1998-10-10T02:42:04.525Z","age":25},"registered":{"date":"2005-05-01T01:26:36.354Z","age":18},"phone":"079 098 73 86","cell":"077 411 83 18","id":{"name":"AVS","value":"756.9632.2579.59"},"picture":{"large":"https://randomuser.me/api/portraits/women/37.jpg","medium":"https://randomuser.me/api/portraits/med/women/37.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/37.jpg"},"nat":"CH"}],"info":{"seed":"213f93e3e854d33c","results":1,"page":1,"version":"1.4"}} ``` Le JSON est simplement affiché à l'écran, pas vraiment lisible. Mais dès qu'on redirige vers `jq`, ça devient bien mieux : ```bash $ curl --silent https://randomuser.me/api/ | jq ``` ```json { "results": [ { "gender": "female", "name": { "title": "Mademoiselle", "first": "Salomé", "last": "Roy" }, "location": { "street": { "number": 2361, "name": "Rue Dumenge" }, "city": "Dierikon", [...] }, "email": "salome.roy@example.com", [...] } ], "info": { "seed": "b7ef95d3b252edca", "results": 1, "page": 1, "version": "1.4" } } ``` Mêmes octets, même API, un mot de plus sur la ligne de commande. Ça justifie déjà la place de `jq` dans un pipe, mais la vraie raison pour laquelle je l'utilise est juste en dessous. ## Filtrer la sortie {#filtering-the-output} Imaginons qu'on ait seulement besoin de récupérer le nom. Pour ça, il faut mieux comprendre l'objet JSON. ```json { //highlight-next-line "results": [ { "gender": "female", //highlight-next-line "name": { "title": "Mademoiselle", "first": "Salomé", "last": "Roy" }, "location": { [...] }, [...] ], [...] } ``` La sortie est une représentation JSON avec une propriété racine appelée `results`. Cette propriété est un tableau. Chaque élément de ce tableau contient un nœud `name` avec quelques propriétés comme `title`, `first` et `last`. Si on veut uniquement récupérer le nœud `name`, le filtre à utiliser avec `jq` est `.results[0].name`. ```bash $ curl --silent https://randomuser.me/api/ | jq '.results[0].name' ``` ```json { "title": "Ms", "first": "Brooke", "last": "Morgan" } ``` Pour en savoir plus sur [le filtrage avec jq](https://jqlang.github.io/jq/tutorial/). ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Avant d'installer quoi que ce soit, essayez les deux commandes ci-dessus dans un container jetable. Il embarque `jq` et une copie statique de la réponse de `randomuser.me` utilisée tout au long de cet article, donc la démo fonctionne même sans accès réseau — l'API en ligne reste toutefois accessible si vous voulez l'essayer pour de vrai. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `jq` et dépose un fichier `sample.json` prêt à l'emploi dans le répertoire > de travail. Rien à taper, télécharger ou configurer — il suffit de le passer dans `jq`. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ jq \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # A static snapshot of the randomuser.me response used throughout this # article, so the demo works even without network access. The live API is # still reachable from inside the container if you want to try it for real. RUN mkdir -p /root/demo && \ printf '%s' '{"results":[{"gender":"female","name":{"title":"Mademoiselle","first":"Salomé","last":"Roy"},"location":{"street":{"number":2361,"name":"Rue Dumenge"},"city":"Dierikon","state":"Basel-Stadt","country":"Switzerland","postcode":5075,"coordinates":{"latitude":"-58.3405","longitude":"134.0131"},"timezone":{"offset":"+4:30","description":"Kabul"}},"email":"salome.roy@example.com","dob":{"date":"1960-05-12T07:27:42.104Z","age":63},"registered":{"date":"2021-05-02T04:18:14.996Z","age":2},"phone":"077 228 21 12","cell":"078 051 38 48","picture":{"large":"https://randomuser.me/api/portraits/women/74.jpg"},"nat":"CH"}],"info":{"seed":"b7ef95d3b252edca","results":1,"page":1,"version":"1.4"}}' \ > /root/demo/sample.json WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Buildez et lancez-le : ```bash $ docker build -t jq-demo . [+] Building 12.1s (7/7) FINISHED ✔ exporting to image $ docker run --rm -it jq-demo 🐳 root ~/demo # cat sample.json | jq '.results[0].name' { "title": "Mademoiselle", "first": "Salomé", "last": "Roy" } ``` Même filtre que plus haut, même résultat, et même pas besoin de `curl`. Retirez le filtre et lancez `cat sample.json | jq` seul pour voir tout l'objet mis en forme, ou, puisque le container a un accès réseau, essayez aussi la version en ligne : `curl --silent https://randomuser.me/api/ | jq`. ## Installer jq {#installing-jq} Pour vérifier si `jq` est déjà installé sur votre système, lancez simplement `which jq`. Si vous obtenez `jq not found` comme réponse : Prerequisite: jq ```bash $ sudo apt-get update && sudo apt-get install jq ``` Verify: ```bash $ jq --version jq-1.7.1 ``` ## Jouons un peu {#lets-play} `jq` peut être utilisé sans aucun argument et, dans ce cas, la sortie JSON sera mise en forme. Pour reproduire les exemples ci-dessus, nous allons utiliser une API JSON gratuite et simple comme `https://randomuser.me/api/`. Si vous préférez une autre API, jetez un œil à ; les gratuites sont celles qui ont `No` dans la colonne `Auth`. > **L'API randomuser renvoie chaque fois un nouvel utilisateur:** > > À chaque requête, l'API `randomuser` renvoie un nouvel objet. Ne soyez pas surpris si vos noms diffèrent des miens. **La réponse complète mise en forme (cliquez pour les détails)** Voici l'objet complet renvoyé par l'API, tel que `jq` l'affiche : ```json { "results": [ { "gender": "female", "name": { "title": "Mademoiselle", "first": "Salomé", "last": "Roy" }, "location": { "street": { "number": 2361, "name": "Rue Dumenge" }, "city": "Dierikon", "state": "Basel-Stadt", "country": "Switzerland", "postcode": 5075, "coordinates": { "latitude": "-58.3405", "longitude": "134.0131" }, "timezone": { "offset": "+4:30", "description": "Kabul" } }, "email": "salome.roy@example.com", "login": { "uuid": "dfda3e32-b8b1-44c9-9aa9-194fc17833d3", "username": "organiccat614", "password": "nacked", "salt": "HRepWJ2P", "md5": "643f7a890ba9c9fbe04ea971287f57ba", "sha1": "9407fd8fdf0c344ac1990a89d8d0190f6baabd19", "sha256": "1317478dbfc61c3feaccea4485a367fb6b195009f15eecd9175f2d1a9215d5df" }, "dob": { "date": "1960-05-12T07:27:42.104Z", "age": 63 }, "registered": { "date": "2021-05-02T04:18:14.996Z", "age": 2 }, "phone": "077 228 21 12", "cell": "078 051 38 48", "id": { "name": "AVS", "value": "756.1071.1525.27" }, "picture": { "large": "https://randomuser.me/api/portraits/women/74.jpg", "medium": "https://randomuser.me/api/portraits/med/women/74.jpg", "thumbnail": "https://randomuser.me/api/portraits/thumb/women/74.jpg" }, "nat": "CH" } ], "info": { "seed": "b7ef95d3b252edca", "results": 1, "page": 1, "version": "1.4" } } ``` ## Conclusion {#conclusion} Deux choses à retenir : `jq` seul rend n'importe quel JSON lisible, et `jq '.some.path'` extrait exactement le nœud dont vous avez besoin — c'est ce qui transforme une réponse d'API en quelque chose qu'un script shell peut consommer. `jq` devient vite un réflexe : je l'utilise dans [Troubleshooting for Docker containers](/blog/docker-networking-troubleshooting) pour lire la sortie de `docker inspect`, et dans [la création d'un analyseur d'articles de blog avec Ollama](/blog/docusaurus-ollama-tags) pour formater les réponses JSON du LLM local, par exemple. Et les jours où le filtre dont vous avez besoin — un `group_by`, un `select` sur une date — ne vous revient pas, [ai-data](/blog/ollama-ai-data) demande à un LLM local cinq commandes `jq` adaptées à votre propre fichier, et dépose celle que vous choisissez dans votre prompt, prête à être relue avant de l'exécuter. Pour les fois où la console n'est pas le bon outil pour du JSON, ce blog en propose trois autres : [Rendering a JSON file as a mind map](/blog/json-crack), [JSON - Online linter](/blog/json-lint) et [JSON - Faker & Mockup](/blog/json-faker). --- # L'utilitaire xmlstarlet pour Linux > Maîtrisez la manipulation de données XML en ligne de commande avec xmlstarlet. Ce guide montre comment embellir une sortie XML et filtrer des nœuds avec des expressions XPath --- ![L'utilitaire xmlstarlet pour Linux](/img/v2/bash.webp) **TL;DR** Cet article présente `xmlstarlet`, l'équivalent de `jq` pour le XML : formater un XML minifié avec `xmlstarlet format --indent-spaces 4`, et extraire ou filtrer des nœuds avec des expressions XPath via `xmlstarlet sel -t -v`, y compris des filtres sur attribut comme `//book[@category='children']/title`. `xmlstarlet` est un utilitaire puissant pour Linux qui permet de manipuler des données XML depuis la ligne de commande et qui s'intègre dans vos scripts shell. *Il est au XML ce que [`jq`](/blog/linux-jq) est au JSON.* Avec `xmlstarlet`, vous pouvez embellir une sortie XML mais aussi la filtrer, par exemple pour n'afficher qu'un nœud donné. ## Un mur de XML en entrée, un mot en sortie {#a-wall-of-xml-in-one-word-out} Voici le fichier avec lequel nous allons jouer, `data.xml`, exactement tel qu'une machine l'aurait écrit : tout sur une seule ligne, aucun formatage. **data.xml** ```xml Everyday ItalianGiada De Laurentiis200530.00Harry PotterJ K. Rowling200529.99XQuery Kick StartJames McGovernPer BothnerKurt CagleJames LinnVaidyanathan Nagarajan200349.99Learning XMLErik T. Ray200339.95 ``` Posons-lui maintenant une question : *donne-moi le titre du livre classé dans la catégorie `children`*. ```bash $ cat "data.xml" | xmlstarlet sel -t -v "//book[@category='children']/title" Harry Potter ``` Une commande, une réponse, et pas une seule ligne de code de parsing. ## Embellir le fichier {#beautifying-the-file} La deuxième chose que vous utiliserez tous les jours : rendre ce mur lisible, grâce à l'action `format`. ```bash $ cat "data.xml" | xmlstarlet format --indent-spaces 4 ``` ```xml Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ``` ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Avant d'installer quoi que ce soit, essayez les deux commandes ci-dessus dans un container jetable — `data.xml`, exactement le même fichier que celui montré en haut de cet article, vous attend déjà dans le répertoire de travail. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `xmlstarlet` et écrit `data.xml` pour vous. Rien à créer, coller ou télécharger — il n'y a plus qu'à envoyer le fichier dans `xmlstarlet`. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ xmlstarlet && \ rm -rf /var/lib/apt/lists/* # The exact data.xml used throughout this article, already in place RUN mkdir -p /root/demo && \ printf '%s' 'Everyday ItalianGiada De Laurentiis200530.00Harry PotterJ K. Rowling200529.99XQuery Kick StartJames McGovernPer BothnerKurt CagleJames LinnVaidyanathan Nagarajan200349.99Learning XMLErik T. Ray200339.95' \ > /root/demo/data.xml WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Construisez l'image et lancez-la : ```bash $ docker build -t xmlstarlet-demo . [+] Building 9.4s (6/6) FINISHED ✔ exporting to image $ docker run --rm -it xmlstarlet-demo 🐳 root ~/demo # cat data.xml | xmlstarlet sel -t -v "//book[@category='children']/title" Harry Potter ``` Même fichier, même filtre, même réponse — pas de `mkdir`, pas de copier-coller de XML dans un nouveau fichier. Essayez ensuite `cat data.xml | xmlstarlet format --indent-spaces 4` pour voir la version embellie, ou modifiez l'expression XPath pour récupérer un autre nœud. ## Installer xmlstarlet {#installing-xmlstarlet} Pour vérifier si `xmlstarlet` est déjà installé sur votre système, lancez simplement `which xmlstarlet`. Si vous obtenez `xmlstarlet not found` comme réponse : Prerequisite: xmlstarlet ```bash $ sudo apt-get update && sudo apt-get -y install xmlstarlet ``` Verify: ```bash $ xmlstarlet --version 1.6.1 ``` ## À vous de jouer {#lets-play} Pour reproduire les deux commandes ci-dessus sur votre machine, ouvrez un shell Linux et lancez `mkdir -p /tmp/xmlstarlet && cd $_`, puis créez un fichier `data.xml` avec le contenu affiché en haut de cet article. ## Comprendre l'expression XPath {#understanding-the-xpath-expression} Une expression XPath n'est qu'un chemin dans l'arbre. Notre nœud racine s'appelle `bookstore`, ensuite nous avons un ou plusieurs `book` et chaque livre a un `title` : ```xml Everyday Italian [...] [...] ``` Parcourez ce chemin depuis la racine et vous obtenez tous les titres du fichier : ```bash $ cat "data.xml" | xmlstarlet sel -t -v "/bookstore/book/title" Everyday Italian Harry Potter XQuery Kick Start Learning XML ``` L'expression utilisée au début de cet article, `//book[@category='children']/title`, en est la version filtrée : donne-moi chaque `book`, peu importe où le nœud se trouve (c'est le préfixe `//`), mais uniquement s'il possède un attribut nommé `category` dont la valeur est `children`. Puis, si trouvé, affiche son `title`. ## Conclusion {#conclusion} Deux actions couvrent l'essentiel des besoins quotidiens : `format` pour rendre un fichier XML lisible par un humain, et `sel -t -v` avec une expression XPath pour en extraire exactement le nœud voulu. Les deux s'utilisent dans un pipe, donc les deux s'utilisent dans un script shell. Consultez la [documentation officielle](https://xmlstar.sourceforge.net/docs.php) pour en apprendre plus sur xmlstarlet, et si votre prochain fichier est un `.json`, [l'utilitaire jq pour Linux](/blog/linux-jq) raconte exactement la même histoire avec une syntaxe différente. --- # Introduction à fzf - Fuzzy Finder > Maîtrisez la ligne de commande Linux avec fzf (Fuzzy Finder). Découvrez sa puissante recherche floue dans l'historique (CTRL+R) et sa sélection de fichiers (CTRL+T) pour booster votre productivité. --- ![Introduction à fzf - Fuzzy Finder](/img/v2/linux_tips.webp) **TL;DR** Cet article présente `fzf` (Fuzzy Finder), qui améliore la recherche d'historique `CTRL`+`R` de la console avec une correspondance floue, lettres dans n'importe quel ordre, et ajoute `CTRL`+`T` pour sélectionner interactivement un ou plusieurs fichiers (utilisable avec des commandes comme `rm` ou `head`) ainsi que `ALT`+`C` pour sauter dans un dossier. Comme vous le savez sans doute, `CTRL`+`R` dans la console vous donne accès à votre HISTORIQUE, c'est-à-dire à la liste des commandes que vous avez tapées précédemment. Un peu comme les touches `UP` ou `DOWN`, mais avec un tout petit moteur de recherche. Ça fait le job mais, honnêtement, c'est vraiment basique, non ? L'utilitaire en ligne de commande Fuzzy Finder (alias `fzf`) va faire exploser les possibilités de recherche dans l'historique mais, en réalité, ce n'est qu'une des conséquences de l'installation de fzf, qui est bien plus puissant que ça. *Trois articles de ce blog s'appuient directement sur `fzf` : [FZF + ripgrep: Interactive Code Search with Live Preview](/blog/fzf-ripgrep), [Master your ssh command and select the host from a list](/blog/ssh-with-fuzzy-finder) et [ZSH Functions - Customizing Your Shell for Docker Management](/blog/zsh-docker-functions).* Allons voir ça. En appuyant sur `CTRL`+`R`, vous obtenez nativement une *recherche inversée* dans votre historique. Commencez à taper une commande comme `ls` dans mon exemple ci-dessous et Linux vous montrera la dernière commande utilisée. ![Using CTRL-R](./images/ctrl_r.webp) ## Le voir en action avec Docker {#seeing-it-in-action-with-docker} Avant de cloner quoi que ce soit sur votre machine, lancez un container jetable avec `fzf` déjà configuré et un petit terrain de jeu déjà construit. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `fzf`, active ses raccourcis clavier dans `bash`, pré-remplit un historique de shell (pour que `CTRL`+`R` ait quelque chose à chercher) et crée une poignée de fichiers de logs et de dossiers imbriqués (pour que `CTRL`+`T` et `ALT`+`C` aient quelque chose à parcourir). Rien à préparer vous-même. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ bash \ git \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install fzf and wire its key bindings + fuzzy completion into bash RUN git clone --depth 1 https://github.com/junegunn/fzf.git /root/.fzf && \ /root/.fzf/install --key-bindings --completion --no-update-rc && \ echo '[ -f ~/.fzf.bash ] && source ~/.fzf.bash' >> /root/.bashrc # Build a playground: log files to fuzzy-select with Ctrl+T, nested folders # to jump into with Alt+C RUN mkdir -p /root/demo/logs /root/demo/scripts /root/demo/nested/deep/folder && \ for i in 1 2 3 4; do \ printf 'run%s.sh.log — line 1\nline 2\nline 3\nline 4\nline 5\n' "$i" \ > /root/demo/logs/run"$i".sh.log; \ done && \ touch /root/demo/scripts/deploy.sh /root/demo/scripts/backup.sh # Pre-seed bash history so Ctrl+R has something real to fuzzy-search: type # "github" and the git-clone line below turns up, exactly like the article's # own screenshots. RUN printf '%s\n' \ 'cd /root/demo' \ 'ls -la' \ 'git clone https://github.com/junegunn/fzf-git.sh.git' \ 'docker compose up --detach' \ 'head -n 5 logs/run1.sh.log' \ > /root/.bash_history WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Construisez-le et lancez-le : ```bash $ docker build -t fzf-demo . [+] Building 24.6s (9/9) FINISHED ✔ exporting to image $ docker run --rm -it fzf-demo 🐳 root ~/demo # ``` Vous êtes déjà dans `/root/demo`. Essayez tout de suite les trois raccourcis : * Appuyez sur `CTRL`+`R` et tapez `github` — la ligne pré-enregistrée `git clone https://github.com/junegunn/fzf-git.sh.git` apparaît, exactement comme sur les captures d'écran ci-dessous. * Appuyez sur `CTRL`+`T` et tapez `run` — les quatre fichiers `run*.sh.log` dans `logs/` apparaissent, filtrés en direct au fur et à mesure de la frappe. * Appuyez sur `ALT`+`C` pour sauter dans `nested/deep/folder` sans taper un seul `cd`. * Lancez `fzf -m | xargs head -n 5`, filtrez sur `run`, sélectionnez deux ou trois fichiers avec `TAB`, appuyez sur `ENTER` — le scénario de sélection multiple de la section « Obtenir une liste de fichiers » ci-dessous, prêt à tester sans rien supprimer de réel. ## Installation {#installation} L'installation de `fzf` est simple, il suffit de lancer `git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install` et de répondre `Y`es à deux questions. Une fois terminé, fermez et relancez une nouvelle console (pour que Linux prenne en compte les modifications apportées à vos fichiers de configuration). ## Jouer avec l'historique {#playing-with-the-history} Maintenant, appuyez à nouveau sur `CTRL`+`R` et vous obtiendrez quelque chose de bien meilleur : ![Using CTRL-R once FZF has been installed](./images/ctrl_r_fzf.webp) Comme précédemment, commencez à taper la commande que vous souhaitez retrouver ; dans mon cas, je sais qu'un jour j'ai essayé d'installer un repo depuis github mais je ne me souviens plus de l'instruction, alors je vais commencer à taper `github` : ![Filtering on github](./images/ctrl_r_github.webp) Si vous regardez l'image, vous verrez que `fzf` cherche les lettres `g`, `i`, `t`, `h`, `u` et `b`, pas le mot. La première ligne contient donc le mot `git`, puis il y a un `h` quelque part et le mot `public` donc, `ub` s'y trouvent. C'est pratique puisque vous n'avez pas besoin de vous souvenir de l'ordre exact des mots dans votre ligne de commande précédente, tapez juste des mots dans n'importe quel ordre. Pour moi, rien que pour cet usage, l'installation de `fzf` est déjà pleinement justifiée. ## Obtenir une liste de fichiers {#getting-a-list-of-files} Imaginez que vous souhaitiez afficher le contenu d'un fichier : vous commencez à taper `cat` et, zut, vous ne vous souvenez plus du nom exact du fichier. En appuyant sur `CTRL`+`T`, vous obtiendrez la liste des fichiers du répertoire courant et de ses sous-répertoires : ![Using CTRL-T](./images/ctrl_t.webp) Vous pouvez naviguer avec `UP` et `DOWN` mais aussi taper quelques lettres pour filtrer la liste. Sur ma machine, en commençant à taper `doc`, j'obtiens la liste des fichiers, n'importe où dans l'arborescence, contenant `doc`. Je peux continuer et taper `doc.md` pour forcer la présence des lettres `md` et donc, dans mon cas, chercher des articles à propos de Docker ayant l'extension `.md`. ![Using CTRL-T and filtering on doc](./images/ctrl_t_doc.webp) Cet usage est pratique puisque je n'ai pas besoin de retrouver d'abord le nom exact du fichier que je voulais utiliser. Dans cet exemple c'était pour `cat` mais, bien sûr, ça marche pour n'importe quoi : tapez votre commande et appuyez sur `CTRL`+`T`. > **`**` suivi de TAB est un alias:** > > Pas sûr que ce soit à retenir, mais `cat **` suivi de `TAB` fonctionnera exactement comme si vous appuyiez sur `CTRL`+`T`. Ici, `**` sera développé en `CTRL`+`T`. Ici, avec `CTRL`+`T`, vous ne pourrez sélectionner qu'un seul fichier. Imaginez que vous vouliez en sélectionner plusieurs ? Par exemple, vous voulez lancer `rm` pour supprimer plus d'un fichier. En lançant `fzf -m | xargs rm` suivi de `ENTER`, Fuzzy Finder affichera la liste de tous les fichiers présents dans le répertoire courant. Comme précédemment, je peux commencer à taper pour filtrer la liste. Dans mon exemple ci-dessous, j'ai tapé `run.sh.log` pour retrouver des logs et, dans la liste, j'appuie sur `TAB` pour sélectionner quatre fichiers (voyez le caractère `>` rouge à gauche). Ensuite j'appuie sur `ENTER` pour valider mon choix et... les fichiers ont été supprimés puisque ma commande était `rm`. ![Removing several files](./images/rm_several_files.webp) Si je relance la même commande avec le même filtre, je vois que je ne récupère plus que trois fichiers, ceux que je n'avais pas sélectionnés. Le `rm` a bien fonctionné. ![Removing several files, second run](./images/rm_several_files_bis.webp) Maintenant, au lieu de tuer des fichiers, je vais simplement afficher les cinq premières lignes de chaque fichier sélectionné : pour cela, je vais exécuter `fzf -m | xargs head -n 5`. Je filtre à nouveau sur `run.sh.log`, je sélectionne les trois fichiers avec `TAB`, j'appuie sur `ENTER` pour valider mon choix et tadaaa : ![Selecting several files](./images/head_several_files.webp) ## Raccourcis clavier {#keybindings} | Shortcut | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Ctrl`+`R` | Affiche la liste des commandes que vous avez tapées précédemment (votre historique donc) et vous permet d'en sélectionner une en appuyant sur ENTER. | | `Ctrl`+`T` | Affiche la liste des fichiers du répertoire courant et de ses sous-dossiers, vous permet de sélectionner un ou plusieurs fichiers (appuyez simplement sur TAB pour une sélection multiple) puis appuyez sur ENTER pour renvoyer la liste à la console. | | `Alt`+`C` | Affiche la liste récursive des dossiers du répertoire courant, vous permet d'en sélectionner un et, en appuyant sur ENTER, fzf saute dans ce dossier. | Une fois ces bases devenues naturelles, fzf brille vraiment lorsqu'il est combiné à d'autres outils : voyez [FZF + ripgrep](/blog/fzf-ripgrep) pour une recherche de code interactive avec aperçu en direct, ou [Maîtrisez votre commande ssh et sélectionnez l'host dans une liste](/blog/ssh-with-fuzzy-finder) pour arrêter de taper les noms d'hôtes de mémoire. Si vous voulez spécifiquement une recherche d'historique plus riche — horodatages, codes de sortie et répertoire de travail par commande — Atuin vaut le coup d'œil : il remplace le fichier plat `~/.bash_history` par une base de données SQLite tout en gardant le même raccourci `Ctrl+R`. --- # Revisitons la commande ls grâce à eza > Fatigué de taper ls -alh ? Découvrez eza, le remplaçant moderne et complet de la commande ls de Linux. Apprenez à l'installer et à configurer un alias puissant pour une meilleure expérience en ligne de commande. --- ![Revisitons la commande ls grâce à eza](/img/v2/linux_tips.webp) **TL;DR** Cet article présente `eza`, un remplaçant moderne de la commande `ls` (anciennement connu sous le nom d'`exa`), avec de meilleurs réglages par défaut, des icônes et un meilleur formatage. Il montre comment l'installer avec `apt-get` et configurer un alias avec une combinaison utile de flags (`--all --long --group --group-directories-first --icons --header --time-style long-iso`) pour que `ls` utilise automatiquement `eza` et affiche une liste de fichiers plus riche. Quelle commande CLI utilisez-vous le plus sous Linux ? Très probablement `ls`, pour afficher la liste des fichiers du répertoire courant. > **Je ne sais pas pour vous, mais j'utilise rarement `ls` sans paramètres. Presque sans y penser, j'ajoute `-alh` chaque fois. C'est devenu mécanique.:** Et là vous me direz : il suffit de créer un alias `alias ls="ls -alh"`. Bien sûr, mais allons plus loin : revisitons cette commande de base et ajoutons-lui quelques fonctionnalités. `eza` est un remplaçant moderne de `ls`, comme annoncé sur leur site : . *Il appartient à la même famille de « réécritures modernes des classiques » que [ripgrep](/blog/ripgrep) (pour `grep`) et [delta](/blog/git-delta) (pour `git diff`). Et tant que vous personnalisez votre console, [Customize your Linux prompt with Powerlevel10k](/blog/powerlevel10k_sandbox) vaut aussi dix minutes de votre temps.* > **eza s'appelait d'abord exa:** > > est abandonné ; voir [cette issue](https://github.com/ogham/exa/issues/1243). > > **exa est devenu eza** : ## Résultat {#result} Une fois `ls` aliasé vers `eza` avec quelques flags, voici à quoi ressemble un simple `ls` : ![eza](./images/eza.webp) ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Avant de toucher à votre `~/.bashrc`, essayez l'alias dans un container jetable avec un petit dossier varié déjà prêt — un fichier caché, un ou deux sous-dossiers, des fichiers de tailles différentes — pour que le listing ait vraiment quelque chose à montrer. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `eza`, met en place l'alias `ls` exact de cet article et construit un dossier de démo pour vous. Rien ne change sur votre propre machine. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install eza from a prebuilt release (avoids adding eza's own apt repository # just for a demo). Pin the version so this image keeps building the same # way; bump it during a /freshness pass. RUN curl -fsSL \ https://github.com/eza-community/eza/releases/download/v0.23.5/eza_x86_64-unknown-linux-gnu.tar.gz \ | tar -xz -C /usr/local/bin # Recreate the exact alias from the article RUN echo "alias ls='eza --all --long --group --group-directories-first --icons --header --time-style long-iso'" \ >> /root/.bashrc # Build a small, varied playground so the listing actually looks interesting: # sub-folders, a dotfile, a text file and a binary-ish file of a different size RUN mkdir -p /root/demo/src /root/demo/docs && \ touch /root/demo/.env && \ echo "# Notes" > /root/demo/README.md && \ printf 'console.log("hi")\n' > /root/demo/src/index.js && \ dd if=/dev/zero of=/root/demo/docs/report.pdf bs=1024 count=200 status=none WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Buildez-le et lancez-le : ```bash $ docker build -t eza-demo . [+] Building 14.2s (7/7) FINISHED ✔ exporting to image $ docker run --rm -it eza-demo 🐳 root ~/demo # ls Permissions Size User Date Modified Name drwxr-xr-x - root 2026-08-22 docs drwxr-xr-x - root 2026-08-22 src .rw-r--r-- 0 root 2026-08-22 .env .rw-r--r-- 8 root 2026-08-22 README.md ``` Voilà `ls` — en réalité `eza` sous le capot, grâce à l'alias intégré dans l'image. Icônes, dossiers regroupés en premier, dates lisibles : le même résultat que la capture ci-dessus, sans rien avoir installé sur votre propre machine. ## Installer eza {#install-eza} L'installation est simple, il suffit de lancer `sudo apt-get update && sudo apt-get install eza` ; rien de plus. À partir de là, lancez simplement `eza` en ligne de commande et vous obtiendrez la liste des fichiers avec tous les réglages par défaut. Sur mon ordinateur, j'ai opté pour cette liste de paramètres : `--all --long --group --group-directories-first --icons --header --time-style long-iso` et, bien sûr, j'ai mis à jour mon fichier `~/.bashrc` en y ajoutant un alias (si vous êtes sous ZSH, [donnez à cet alias son propre fichier](/blog/modular-zsh-workflow) plutôt que de faire grossir un `.zshrc` monolithique) : ```bash $ alias ls='eza --all --long --group --group-directories-first --icons --header --time-style long-iso' ``` Et maintenant, en tapant simplement `ls`, vous obtiendrez le listing montré plus haut. --- # Maîtrisez votre commande ssh et choisissez l'host dans une liste > Construisez un sélecteur d'hosts SSH interactif et filtrable avec FZF. Apprenez à modulariser vos configurations SSH, à ajouter une documentation riche et à déclencher des actions personnalisées avancées via une interface terminal rapide. --- ![Maîtrisez votre commande ssh et choisissez l'host dans une liste](/img/v2/sshf.webp) **TL;DR**Gérer des dizaines de connexions serveur dans un fichier `~/.ssh/config` monolithique devient vite ingérable et repose trop sur la mémoire. Cet article montre une approche plus propre : découper votre configuration en fichiers par projet dans un dossier `~/.ssh/conf.d/`, ce qui permet d'attacher une documentation riche et stylée juste au-dessus de vos définitions d'hosts. Pour lier le tout, vous allez écrire une fonction shell personnalisée (sshf) propulsée par FZF. Vous obtenez une interface terminal (TUI) rapide et filtrable pour trouver instantanément un host par nom, projet ou métadonnée. On va même plus loin avec des raccourcis clavier personnalisés, qui déclenchent des actions contextuelles avancées — pinguer des serveurs, modifier des configurations ou générer des inventaires système — directement depuis le menu de recherche. Sur ma machine, j'ai accumulé de plus en plus d'hosts dans mon fichier `~/.ssh/config`. Je peux taper `ssh server4` (voir [zsh-ssh-config-suggestions](/blog/zsh-plugin-ssh-config-suggestions)) pour sauter sur ce serveur, mais je dois encore me souvenir de la liste des serveurs disponibles. C'est gérable avec quelques-uns, mais j'en ai actuellement une cinquantaine, et compter uniquement sur sa mémoire n'est plus réaliste. Il serait bien plus pratique d'afficher la liste complète des hosts dans une TUI (*Terminal User Interface*), de sélectionner celui qu'on veut et d'appuyer simplement sur `Enter`. En plus, ce serait très utile d'avoir des informations contextuelles : l'URL hébergée sur ce serveur, la présence d'une base de données, la version de PHP, etc. Enfin, créer un fichier `.conf` par projet éviterait de maintenir un énorme fichier de configuration monolithique contenant les 50 serveurs. Voyons comment mettre tout cela en place. *Cet article s'appuie sur [Introduction to fzf - Fuzzy Finder](/blog/linux-fzf-introduction) et sur les alias créés dans [How to connect to your hosting server using SSH](/blog/connect-using-ssh-to-your-hosting-server). Il applique à `~/.ssh/config` la même philosophie « un fichier par sujet » que [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow).* Avant d'entrer dans les détails techniques, voici un aperçu du résultat final : ![Notre TUI sshf](./images/tui.webp) À gauche, une zone de recherche (je peux taper `amazing` pour filtrer sur ce motif) au-dessus d'une liste d'hosts. À droite, les informations contextuelles sur l'host sélectionné. Une fois l'host choisi, j'appuie sur `Enter` pour ouvrir une session SSH dessus — sans devoir saisir mes identifiants à la main. C'est parti... ## Le voir en action avec Docker {#seeing-it-in-action-with-docker} Avant de toucher à votre propre `~/.ssh/config`, essayez toute la TUI dans un container jetable — les fichiers `amazing.conf`/`legacy.conf` exacts et la fonction `sshf` décrite plus bas y sont déjà en place. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `fzf`, `perl` et `openssh-client`, écrit les fichiers de configuration SSH exacts montrés plus bas, et ajoute la fonction `sshf` exacte à `~/.bashrc`. Rien ne change sur votre propre machine et — puisque `1.2.3.4` et consorts sont les mêmes IP d'exemple utilisées tout au long de l'article — appuyer sur `Enter` sur un host ne vous connectera nulle part. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # Ubuntu's docker image only enables "main" + "restricted" by default; fzf # lives in "universe". RUN sed -i 's/^Components: main restricted$/Components: main restricted universe multiverse/' \ /etc/apt/sources.list.d/ubuntu.sources && \ apt-get update && \ apt-get install -y --no-install-recommends \ fzf \ perl \ openssh-client && \ rm -rf /var/lib/apt/lists/* # ~/.ssh/conf.d/ — the two exact config files from this article RUN mkdir -p /root/.ssh/conf.d && chmod 700 /root/.ssh RUN printf '%s\n' \ '# MyAmazingApp - Production Environment' \ '#' \ '# https://my.amazing_app.be' \ '# /var/www/amazing' \ '# pgsql: host=1.2.3.4 port=5438 dbname=amazing username=christophe' \ '#' \ '# Backend: PHP 8.5' \ '# Note: Part of TremendousApp.' \ '#' \ '# CRITICAL - This is a production server!' \ '# INFO - Database is PostgreSQL' \ 'Host MyAmazingApp_PROD' \ ' Hostname 1.2.3.4' \ ' User usr_app_prod' \ '' \ '# MyAmazingApp - Tests Environment' \ '#' \ '# https://my.tests_amazing_app.be' \ '# /var/www/tests_amazing' \ '# pgsql: host=1.2.3.4 port=5438 dbname=amazing_newx username=christophe' \ '#' \ '# Backend: PHP 8.3' \ '# Note: This is the TESTS environment' \ '#' \ '# INFO - Database is PostgreSQL' \ 'Host MyAmazingApp_TEST' \ ' Hostname 6.7.8.9' \ ' User usr_app_test' \ '' \ 'Host YourAmazingApp' \ ' Hostname 1.2.3.4' \ ' User you' \ > /root/.ssh/conf.d/amazing.conf RUN printf '%s\n' \ 'Host LegacyApp' \ ' Hostname 1.2.3.4' \ ' User legacy' \ > /root/.ssh/conf.d/legacy.conf # ~/.ssh/config — includes both conf.d files RUN printf '%s\n' \ 'Include conf.d/amazing.conf' \ 'Include conf.d/legacy.conf' \ > /root/.ssh/config && \ chmod 600 /root/.ssh/config /root/.ssh/conf.d/*.conf # ~/.bashrc — the exact sshf() function from this article RUN printf '%s\n' \ '# ------------------------------------------------------------------------------' \ '# FUNCTION: sshf' \ '#' \ '# DESCRIPTION:' \ '# An interactive SSH launcher using fzf and Perl. It parses SSH configuration' \ '# files (~/.ssh/config and ~/.ssh/conf.d/*.conf) to present a searchable' \ '# list of aliases. It supports multi-line comments and rich-text' \ '# formatting (colors, backgrounds) in the preview pane.' \ '#' \ '# FEATURES:' \ '# - Deep search: Filters by Alias, HostName, or any word in the comments.' \ '# - ANSI Support: Supports tags like , , , etc.' \ '# - Non-cluttered UI: Keeps the list clean while showing full docs on the right.' \ '# - Robust Parsing: Handles complex multi-line SSH comment blocks.' \ '#' \ '# INSTALLATION:' \ '# 1. Save this file as ~/.zsh/sshf.zsh' \ '# 2. Add the following line to your ~/.zshrc:' \ '# [[ -f ~/.zsh/sshf.zsh ]] && source ~/.zsh/sshf.zsh' \ '# 3. Restart your terminal or run: source ~/.zshrc' \ '#' \ '# USAGE:' \ '# $ sshf # Opens interactive list' \ '# $ sshf query # Opens interactive list pre-filtered by "query"' \ '#' \ '# DOCUMENTATION TAGS (Use in SSH config comments):' \ '# text, text, text, text' \ '# highlight, warning, important' \ '# ------------------------------------------------------------------------------' \ 'sshf() {' \ ' local selected' \ ' # Step 1: Data extraction with Perl' \ ' # Aggregates comments and attaches them to the subsequent Host definition.' \ ' selected=$(cat ~/.ssh/config ~/.ssh/conf.d/*.conf 2>/dev/null | perl -ne '\''' \ ' BEGIN { $sep = " ;; "; }' \ ' s/\r//g; chomp;' \ '' \ ' # Capture comment lines' \ ' if (/^\s*#/) {' \ ' s/^\s*#\s?//;' \ ' s/\|/-/;' \ ' $buf .= ($buf eq "" ? "" : $sep) . $_;' \ ' next;' \ ' }' \ '' \ ' # Match Host alias (ignoring wildcards)' \ ' if (/^\s*Host\s+([^*\s]+)/i) {' \ ' $new_h = $1;' \ ' if ($h) { print_line($h, $ip, $d); }' \ ' $h = $new_h; $d = $buf; $buf = ""; $ip = "N/A";' \ ' next;' \ ' }' \ '' \ ' # Capture HostName/IP' \ ' if (/^\s*HostName\s+(.+)/i) { $ip = $1; }' \ '' \ ' # Flush the last entry' \ ' END { if ($h) { print_line($h, $ip, $d); } }' \ '' \ ' sub print_line {' \ ' my ($h, $i, $d) = @_;' \ ' $d = "No description" unless $d;' \ ' # 150-space padding prevents description spillover in the main list' \ ' my $padding = " " x 150;' \ ' printf "%-20s | %-15s %s ##### %s\n", $h, $i, $padding, $d;' \ ' }' \ ' '\'' | \' \ ' fzf --query="$1" \' \ ' --reverse \' \ ' --delimiter="#####" \' \ ' --no-hscroll \' \ ' --exact \' \ ' --ansi \' \ ' --header="ALIAS | HOSTNAME" \' \ ' --prompt="Search > " \' \ ' --preview-window="right:70%:wrap" \' \ ' --preview="echo {2} | perl -pe '\''' \ ' s/ ;; /\n/g;' \ ' s//\e[31m/g; s/<\/red>/\e[0m/g;' \ ' s//\e[32m/g; s/<\/green>/\e[0m/g;' \ ' s//\e[33m/g; s/<\/yellow>/\e[0m/g;' \ ' s//\e[41;37m/g; s/<\/bg-red>/\e[0m/g;' \ ' s//\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g;' \ ' s//\e[1m/g; s/<\/bold>/\e[0m/g;' \ ' '\''" | \' \ ' awk '\''{print $1}'\'')' \ '' \ ' # Step 2: Establish SSH Connection' \ ' if [ -n "$selected" ]; then' \ ' # Use ssh -G to get the computed values for this alias' \ ' local ssh_info=$(ssh -G "$selected")' \ ' local real_user=$(echo "$ssh_info" | awk '\''/^user / {print $2}'\'')' \ ' local real_host=$(echo "$ssh_info" | awk '\''/^hostname / {print $2}'\'')' \ ' local real_key=$(echo "$ssh_info" | awk '\''/^identityfile / {print $2}'\'' | head -n 1)' \ '' \ ' # Print the fully expanded command in bold cyan' \ ' printf "\n\e[1;36m>> Executing: ssh -i %s %s@%s\e[0m\n\n" "$real_key" "$real_user" "$real_host"' \ '' \ ' ssh "$selected"' \ ' fi' \ '}' \ >> /root/.bashrc WORKDIR /root # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Buildez et lancez : ```bash $ docker build -t sshf-demo . [+] Building 26.4s (8/8) FINISHED ✔ exporting to image $ docker run --rm -it sshf-demo 🐳 root ~ # sshf ``` La même TUI que sur la capture en haut de l'article s'ouvre, avec les quatre hosts des deux fichiers `conf.d` et leurs doc-blocks. Tapez `prod` ou `php` pour filtrer, exactement comme dans la section « Utiliser les options de filtrage » plus bas — aucun serveur réel n'est jamais contacté. ## Installer FZF {#install-fzf} Consultez mon article [Fuzzy Finder](/blog/linux-fzf-introduction) si vous avez besoin de plus de détails. Sinon, lancez simplement la commande ci-dessous et terminez l'installation en répondant « Yes » à toutes les questions, en suivant les instructions affichées dans votre terminal. ```bash $ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ; ~/.fzf/install ``` Ce n'est pas lié à SSH, mais vous pouvez désormais appuyer sur `CTRL`+`R` pour accéder bien plus facilement à l'historique de votre console. Ce n'est qu'une des très bonnes fonctionnalités de FZF. ## Configuration SSH {#ssh-configuration} L'objectif ici est d'organiser votre configuration plus intelligemment. Créons un dossier `~/.ssh/conf.d` qui contiendra vos fichiers de configuration. ```bash $ mkdir -p ~/.ssh/conf.d ``` Par exemple, créez un fichier `~/.ssh/conf.d/amazing.conf` avec le contenu suivant : **amazing.conf** ```conf # MyAmazingApp - Production Environment # # https://my.amazing_app.be # /var/www/amazing # pgsql: host=1.2.3.4 port=5438 dbname=amazing username=christophe # # Backend: PHP 8.5 # Note: Part of TremendousApp. # # CRITICAL - This is a production server! # INFO - Database is PostgreSQL Host MyAmazingApp_PROD Hostname 1.2.3.4 User usr_app_prod # MyAmazingApp - Tests Environment # # https://my.tests_amazing_app.be # /var/www/tests_amazing # pgsql: host=1.2.3.4 port=5438 dbname=amazing_newx username=christophe # # Backend: PHP 8.3 # Note: This is the TESTS environment # # INFO - Database is PostgreSQL Host MyAmazingApp_TEST Hostname 6.7.8.9 User usr_app_test Host YourAmazingApp Hostname 1.2.3.4 User you ``` Créez ensuite un second fichier nommé `~/.ssh/conf.d/legacy.conf` avec ce contenu : **legacy.conf** ```conf Host LegacyApp Hostname 1.2.3.4 User legacy ``` Mettez maintenant à jour votre fichier principal `~/.ssh/config` pour inclure ces nouveaux fichiers : ```conf Include conf.d/amazing.conf Include conf.d/legacy.conf ``` Cette approche vous permet simplement de regrouper les hosts par projet, ce qui les rend plus faciles à gérer. ### Regardons le fichier amazing.conf {#look-at-the-amazingconf-file} Nous avons défini trois entrées `Host` : `MyAmazingApp_PROD`, `MyAmazingApp_TEST` et `YourAmazingApp`. Remarquez que nous avons ajouté des commentaires juste avant les deux premières lignes `Host` (l'emplacement est crucial). Ce « doc-block » est du texte brut, vous pouvez donc y écrire ce que vous voulez. Pour plus de confort, il prend en charge des marqueurs comme `blue`, `green`, `red`, `yellow`, `bg-red`, `bg-yellow` et `bold`. Quand l'host `MyAmazingApp_PROD` est sélectionné dans la TUI, son doc-block associé s'affiche automatiquement. ## Ajouter la fonction sshf {#add-the-sshf-function} `sshf` a son propre fichier, dans un dossier que votre shell charge au démarrage (l'organisation expliquée dans [Au-delà du monolithe](/blog/modular-zsh-workflow)) : votre fichier rc reste court, et désinstaller l'outil revient à supprimer un fichier. **sshf.sh** ```bash # ------------------------------------------------------------------------------ # FUNCTION: sshf # # DESCRIPTION: # An interactive SSH launcher using fzf and Perl. It parses SSH configuration # files (~/.ssh/config and ~/.ssh/conf.d/*.conf) to present a searchable # list of aliases. It supports multi-line comments and rich-text # formatting (colors, backgrounds) in the preview pane. # # FEATURES: # - Deep search: Filters by Alias, HostName, or any word in the comments. # - ANSI Support: Supports tags like , , , etc. # - Non-cluttered UI: Keeps the list clean while showing full docs on the right. # - Robust Parsing: Handles complex multi-line SSH comment blocks. # # INSTALLATION (zsh and bash, same code): # 1. Save this file as ~/.zsh/fns/sshf.zsh (zsh) or ~/.bash/fns/sshf.sh (bash) # 2. Load that folder from ~/.zshrc or ~/.bashrc with a "for fn_file ..." loop # (see https://www.avonture.be/blog/ssh-with-fuzzy-finder) # 3. Open a new terminal # # USAGE: # $ sshf # Opens interactive list # $ sshf query # Opens interactive list pre-filtered by "query" # # DOCUMENTATION TAGS (Use in SSH config comments): # text, text, text, text # highlight, warning, important # ------------------------------------------------------------------------------ sshf() { local selected # Step 1: Data extraction with Perl # Aggregates comments and attaches them to the subsequent Host definition. selected=$(cat ~/.ssh/config ~/.ssh/conf.d/*.conf 2>/dev/null | perl -ne ' BEGIN { $sep = " ;; "; } s/\r//g; chomp; # Capture comment lines if (/^\s*#/) { s/^\s*#\s?//; s/\|/-/; $buf .= ($buf eq "" ? "" : $sep) . $_; next; } # Match Host alias (ignoring wildcards) if (/^\s*Host\s+([^*\s]+)/i) { $new_h = $1; if ($h) { print_line($h, $ip, $d); } $h = $new_h; $d = $buf; $buf = ""; $ip = "N/A"; next; } # Capture HostName/IP if (/^\s*HostName\s+(.+)/i) { $ip = $1; } # Flush the last entry END { if ($h) { print_line($h, $ip, $d); } } sub print_line { my ($h, $i, $d) = @_; $d = "No description" unless $d; # 150-space padding prevents description spillover in the main list my $padding = " " x 150; printf "%-20s | %-15s %s ##### %s\n", $h, $i, $padding, $d; } ' | \ fzf --query="$1" \ --reverse \ --delimiter="#####" \ --no-hscroll \ --exact \ --ansi \ --header="ALIAS | HOSTNAME" \ --prompt="Search > " \ --preview-window="right:70%:wrap" \ --preview="echo {2} | perl -pe ' s/ ;; /\n/g; s//\e[31m/g; s/<\/red>/\e[0m/g; s//\e[32m/g; s/<\/green>/\e[0m/g; s//\e[33m/g; s/<\/yellow>/\e[0m/g; s//\e[41;37m/g; s/<\/bg-red>/\e[0m/g; s//\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g; s//\e[1m/g; s/<\/bold>/\e[0m/g; '" | \ awk '{print $1}') # Step 2: Establish SSH Connection if [ -n "$selected" ]; then # Use ssh -G to get the computed values for this alias local ssh_info=$(ssh -G "$selected") local real_user=$(echo "$ssh_info" | awk '/^user / {print $2}') local real_host=$(echo "$ssh_info" | awk '/^hostname / {print $2}') local real_key=$(echo "$ssh_info" | awk '/^identityfile / {print $2}' | head -n 1) # Print the fully expanded command in bold cyan printf "\n\e[1;36m>> Executing: ssh -i %s %s@%s\e[0m\n\n" "$real_key" "$real_user" "$real_host" ssh "$selected" fi } ``` Le code est identique pour zsh et bash ; seuls le dossier et le loader changent. **1. Créez le fichier** dans `~/.zsh/fns/` : **Project: \~/.zsh/fns** **sshf.zsh** ```bash # ------------------------------------------------------------------------------ # FUNCTION: sshf # # DESCRIPTION: # An interactive SSH launcher using fzf and Perl. It parses SSH configuration # files (~/.ssh/config and ~/.ssh/conf.d/*.conf) to present a searchable # list of aliases. It supports multi-line comments and rich-text # formatting (colors, backgrounds) in the preview pane. # # FEATURES: # - Deep search: Filters by Alias, HostName, or any word in the comments. # - ANSI Support: Supports tags like , , , etc. # - Non-cluttered UI: Keeps the list clean while showing full docs on the right. # - Robust Parsing: Handles complex multi-line SSH comment blocks. # # INSTALLATION (zsh and bash, same code): # 1. Save this file as ~/.zsh/fns/sshf.zsh (zsh) or ~/.bash/fns/sshf.sh (bash) # 2. Load that folder from ~/.zshrc or ~/.bashrc with a "for fn_file ..." loop # (see https://www.avonture.be/blog/ssh-with-fuzzy-finder) # 3. Open a new terminal # # USAGE: # $ sshf # Opens interactive list # $ sshf query # Opens interactive list pre-filtered by "query" # # DOCUMENTATION TAGS (Use in SSH config comments): # text, text, text, text # highlight, warning, important # ------------------------------------------------------------------------------ sshf() { local selected # Step 1: Data extraction with Perl # Aggregates comments and attaches them to the subsequent Host definition. selected=$(cat ~/.ssh/config ~/.ssh/conf.d/*.conf 2>/dev/null | perl -ne ' BEGIN { $sep = " ;; "; } s/\r//g; chomp; # Capture comment lines if (/^\s*#/) { s/^\s*#\s?//; s/\|/-/; $buf .= ($buf eq "" ? "" : $sep) . $_; next; } # Match Host alias (ignoring wildcards) if (/^\s*Host\s+([^*\s]+)/i) { $new_h = $1; if ($h) { print_line($h, $ip, $d); } $h = $new_h; $d = $buf; $buf = ""; $ip = "N/A"; next; } # Capture HostName/IP if (/^\s*HostName\s+(.+)/i) { $ip = $1; } # Flush the last entry END { if ($h) { print_line($h, $ip, $d); } } sub print_line { my ($h, $i, $d) = @_; $d = "No description" unless $d; # 150-space padding prevents description spillover in the main list my $padding = " " x 150; printf "%-20s | %-15s %s ##### %s\n", $h, $i, $padding, $d; } ' | \ fzf --query="$1" \ --reverse \ --delimiter="#####" \ --no-hscroll \ --exact \ --ansi \ --header="ALIAS | HOSTNAME" \ --prompt="Search > " \ --preview-window="right:70%:wrap" \ --preview="echo {2} | perl -pe ' s/ ;; /\n/g; s//\e[31m/g; s/<\/red>/\e[0m/g; s//\e[32m/g; s/<\/green>/\e[0m/g; s//\e[33m/g; s/<\/yellow>/\e[0m/g; s//\e[41;37m/g; s/<\/bg-red>/\e[0m/g; s//\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g; s//\e[1m/g; s/<\/bold>/\e[0m/g; '" | \ awk '{print $1}') # Step 2: Establish SSH Connection if [ -n "$selected" ]; then # Use ssh -G to get the computed values for this alias local ssh_info=$(ssh -G "$selected") local real_user=$(echo "$ssh_info" | awk '/^user / {print $2}') local real_host=$(echo "$ssh_info" | awk '/^hostname / {print $2}') local real_key=$(echo "$ssh_info" | awk '/^identityfile / {print $2}' | head -n 1) # Print the fully expanded command in bold cyan printf "\n\e[1;36m>> Executing: ssh -i %s %s@%s\e[0m\n\n" "$real_key" "$real_user" "$real_host" ssh "$selected" fi } ``` **2. Chargez le dossier depuis `~/.zshrc`.** Déjà fait si vous avez suivi un de mes articles « Ollama au quotidien ». Sinon, ajoutez à la fin de `~/.zshrc` : ```zsh title="~/.zshrc" for fn_file in ~/.zsh/fns/*.zsh; do source "$fn_file" done ``` **3. Ouvrez un nouveau terminal** (ou lancez `exec zsh`). **1. Créez le fichier** dans `~/.bash/fns/` : **Project: \~/.bash/fns** **sshf.sh** ```bash # ------------------------------------------------------------------------------ # FUNCTION: sshf # # DESCRIPTION: # An interactive SSH launcher using fzf and Perl. It parses SSH configuration # files (~/.ssh/config and ~/.ssh/conf.d/*.conf) to present a searchable # list of aliases. It supports multi-line comments and rich-text # formatting (colors, backgrounds) in the preview pane. # # FEATURES: # - Deep search: Filters by Alias, HostName, or any word in the comments. # - ANSI Support: Supports tags like , , , etc. # - Non-cluttered UI: Keeps the list clean while showing full docs on the right. # - Robust Parsing: Handles complex multi-line SSH comment blocks. # # INSTALLATION (zsh and bash, same code): # 1. Save this file as ~/.zsh/fns/sshf.zsh (zsh) or ~/.bash/fns/sshf.sh (bash) # 2. Load that folder from ~/.zshrc or ~/.bashrc with a "for fn_file ..." loop # (see https://www.avonture.be/blog/ssh-with-fuzzy-finder) # 3. Open a new terminal # # USAGE: # $ sshf # Opens interactive list # $ sshf query # Opens interactive list pre-filtered by "query" # # DOCUMENTATION TAGS (Use in SSH config comments): # text, text, text, text # highlight, warning, important # ------------------------------------------------------------------------------ sshf() { local selected # Step 1: Data extraction with Perl # Aggregates comments and attaches them to the subsequent Host definition. selected=$(cat ~/.ssh/config ~/.ssh/conf.d/*.conf 2>/dev/null | perl -ne ' BEGIN { $sep = " ;; "; } s/\r//g; chomp; # Capture comment lines if (/^\s*#/) { s/^\s*#\s?//; s/\|/-/; $buf .= ($buf eq "" ? "" : $sep) . $_; next; } # Match Host alias (ignoring wildcards) if (/^\s*Host\s+([^*\s]+)/i) { $new_h = $1; if ($h) { print_line($h, $ip, $d); } $h = $new_h; $d = $buf; $buf = ""; $ip = "N/A"; next; } # Capture HostName/IP if (/^\s*HostName\s+(.+)/i) { $ip = $1; } # Flush the last entry END { if ($h) { print_line($h, $ip, $d); } } sub print_line { my ($h, $i, $d) = @_; $d = "No description" unless $d; # 150-space padding prevents description spillover in the main list my $padding = " " x 150; printf "%-20s | %-15s %s ##### %s\n", $h, $i, $padding, $d; } ' | \ fzf --query="$1" \ --reverse \ --delimiter="#####" \ --no-hscroll \ --exact \ --ansi \ --header="ALIAS | HOSTNAME" \ --prompt="Search > " \ --preview-window="right:70%:wrap" \ --preview="echo {2} | perl -pe ' s/ ;; /\n/g; s//\e[31m/g; s/<\/red>/\e[0m/g; s//\e[32m/g; s/<\/green>/\e[0m/g; s//\e[33m/g; s/<\/yellow>/\e[0m/g; s//\e[41;37m/g; s/<\/bg-red>/\e[0m/g; s//\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g; s//\e[1m/g; s/<\/bold>/\e[0m/g; '" | \ awk '{print $1}') # Step 2: Establish SSH Connection if [ -n "$selected" ]; then # Use ssh -G to get the computed values for this alias local ssh_info=$(ssh -G "$selected") local real_user=$(echo "$ssh_info" | awk '/^user / {print $2}') local real_host=$(echo "$ssh_info" | awk '/^hostname / {print $2}') local real_key=$(echo "$ssh_info" | awk '/^identityfile / {print $2}' | head -n 1) # Print the fully expanded command in bold cyan printf "\n\e[1;36m>> Executing: ssh -i %s %s@%s\e[0m\n\n" "$real_key" "$real_user" "$real_host" ssh "$selected" fi } ``` **2. Chargez le dossier depuis `~/.bashrc`.** Ajoutez à la fin de `~/.bashrc` (le test `-r` évite une erreur si le dossier est vide) : ```bash title="~/.bashrc" for fn_file in ~/.bash/fns/*.sh; do [[ -r "$fn_file" ]] && source "$fn_file" done ``` **3. Ouvrez un nouveau terminal** (ou lancez `exec bash`). Vérifiez que le shell connaît la fonction : `type sshf` doit répondre `sshf is a shell function`. Retournez dans votre terminal, tapez `sshf` et appuyez sur `Enter`. La magie opère. Vous verrez la liste complète des hosts définis. En tapant quelques lettres, vous filtrez instantanément. Par exemple, tapez `prod` pour n'afficher que les serveurs de production. Facile, non ? ## Utiliser les options de filtrage {#using-filtering-options} Vous pouvez filtrer sur n'importe quel motif. Par exemple, si vous tapez `php` : ![Filtrage sur les serveurs PHP](./images/php.webp) FZF cherche dans toutes les données disponibles, pas seulement dans les noms d'hosts. ## Mon cas d'usage réel {#my-real-use-case} Au boulot, je suis allé bien plus loin que le simple affichage d'une liste d'hosts. FZF prend en charge les raccourcis clavier personnalisés : vous pouvez associer des actions à d'autres touches que `Enter`. Vous pouvez définir des actions déclenchées par `CTRL`+`A`, `CTRL`+`I`, ou même une simple lettre comme `E`. C'est là que ça devient vraiment puissant ! | Shortcut | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Ctrl`+`A` | J'ai mis en place un second écran qui affiche une liste d'actions à exécuter sur l'host sélectionné. | | `Ctrl`+`I` | Je lance un script de gestion d'inventaire qui scanne tous mes hosts (ou seulement ceux filtrés) et génère une page web avec un inventaire à jour des logiciels installés (versions de PHP, Python, PostgreSQL...). | | `E` | Vous pourriez, par exemple, ouvrir un éditeur pour modifier directement le fichier de configuration de l'host sélectionné. | ```bash # ------------------------------------------------------------------------------ # FUNCTION: sshf_actions # DESCRIPTION: Internal router for specific host actions # ------------------------------------------------------------------------------ sshf_actions() { local action_key=$1 local target=$2 [[ -z "$target" ]] && return case "$action_key" in ctrl-a) local cmd=$(echo -e "Ping\nCopy-ID\nEdit-Config" | fzf --reverse --header="Action for $target") case "$cmd" in Ping) ping $(ssh -G "$target" | awk '/^hostname / {print $2}') ;; Copy-ID) ssh-copy-id "$target" ;; Edit-Config) ${EDITOR:-vim} ~/.ssh/config ;; esac ;; ctrl-h) clear printf "\e[1;34m>> Raw configuration for: %s\e[0m\n" "$target" ssh -G "$target" printf "\n\e[1;30mPress any key to return to list...\e[0m" read -k 1 -s sshf "" ;; esac } # ------------------------------------------------------------------------------ # FUNCTION: sshf # # DESCRIPTION: # An interactive SSH launcher using fzf and Perl. It parses SSH configuration # files (~/.ssh/config and ~/.ssh/conf.d/*.conf) to present a searchable # list of aliases. It supports multi-line comments and rich-text # formatting (colors, backgrounds) in the preview pane. # # FEATURES: # - Deep search: Filters by Alias, HostName, or any word in the comments. # - ANSI Support: Supports tags like , , , etc. # - Non-cluttered UI: Keeps the list clean while showing full docs on the right. # - Robust Parsing: Handles complex multi-line SSH comment blocks. # - Shortcuts: CTRL-A for Actions, CTRL-H for Details. # # INSTALLATION (zsh and bash, same code): # 1. Save this file as ~/.zsh/fns/sshf.zsh (zsh) or ~/.bash/fns/sshf.sh (bash) # 2. Load that folder from ~/.zshrc or ~/.bashrc with a "for fn_file ..." loop # (see https://www.avonture.be/blog/ssh-with-fuzzy-finder) # 3. Open a new terminal # # USAGE: # $ sshf # Opens interactive list # $ sshf query # Opens interactive list pre-filtered by "query" # # DOCUMENTATION TAGS (Use in SSH config comments): # text, text, text, text # highlight, warning, important # ------------------------------------------------------------------------------ sshf() { local out # Step 1: Data extraction with Perl # Aggregates comments and attaches them to the subsequent Host definition. out=$(cat ~/.ssh/config ~/.ssh/conf.d/*.conf 2>/dev/null | perl -ne ' BEGIN { $sep = " ;; "; } s/\r//g; chomp; # Capture comment lines if (/^\s*#/) { s/^\s*#\s?//; s/\|/-/; $buf .= ($buf eq "" ? "" : $sep) . $_; next; } # Match Host alias (ignoring wildcards) if (/^\s*Host\s+([^*\s]+)/i) { $new_h = $1; if ($h) { print_line($h, $ip, $d); } $h = $new_h; $d = $buf; $buf = ""; $ip = "N/A"; next; } # Capture HostName/IP if (/^\s*HostName\s+(.+)/i) { $ip = $1; } # Flush the last entry END { if ($h) { print_line($h, $ip, $d); } } sub print_line { my ($h, $i, $d) = @_; $d = "No description" unless $d; # 150-space padding prevents description spillover in the main list my $padding = " " x 150; printf "%-20s | %-15s %s ##### %s\n", $h, $i, $padding, $d; } ' | \ fzf --query="$1" \ --reverse \ --delimiter="#####" \ --no-hscroll \ --exact \ --ansi \ --header="────────────────────────────────────────────────────────────── ENTER : Connect to host CTRL-A : Open actions menu (Ping, MTR, etc.) CTRL-H : Display raw SSH configuration ────────────────────────────────────────────────────────────── ALIAS | HOSTNAME" \ --prompt="Search > " \ --expect="ctrl-a,ctrl-h" \ --preview-window="right:70%:wrap" \ --preview="echo {2} | perl -pe ' s/ ;; /\n/g; s//\e[31m/g; s/<\/red>/\e[0m/g; s//\e[32m/g; s/<\/green>/\e[0m/g; s//\e[33m/g; s/<\/yellow>/\e[0m/g; s//\e[41;37m/g; s/<\/bg-red>/\e[0m/g; s//\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g; s//\e[1m/g; s/<\/bold>/\e[0m/g; '") # fzf with --expect returns the key on the first line and the selection on the second local key=$(head -n1 <<< "$out") local selected_line=$(sed -n '2p' <<< "$out") local selected=$(echo "$selected_line" | awk '{print $1}') if [ -n "$key" ]; then # A shortcut was pressed sshf_actions "$key" "$selected" elif [ -n "$selected" ]; then # Step 2: Establish SSH Connection (Default Enter) local ssh_info=$(ssh -G "$selected") local real_user=$(echo "$ssh_info" | awk '/^user / {print $2}') local real_host=$(echo "$ssh_info" | awk '/^hostname / {print $2}') local real_key=$(echo "$ssh_info" | awk '/^identityfile / {print $2}' | head -n 1) printf "\n\e[1;36m>> Executing: ssh -i %s %s@%s\e[0m\n\n" "$real_key" "$real_user" "$real_host" ssh "$selected" fi } ``` ![Utilisation des raccourcis clavier](./images/tui_with_actions.webp) Votre imagination est la seule limite ! --- # FZF + ripgrep : recherche de code interactive avec aperçu en direct > Combinez ripgrep et fzf avec un panneau d'aperçu coloré par bat pour construire une recherche de code interactive — puis ouvrez le résultat directement dans VSCode, à la ligne exacte. --- ![FZF + ripgrep : recherche de code interactive avec aperçu en direct](/img/v2/fzf_ripgrep.webp) **TL;DR** `grep -r` renvoie une liste plate — pas de contexte, pas d'aperçu, aucun moyen de naviguer sans ouvrir le fichier. En envoyant `ripgrep` dans `fzf` et en ajoutant un panneau d'aperçu `bat`, vous obtenez une recherche de code interactive et colorée, qui ressemble à la barre de recherche d'un IDE mais vit entièrement dans votre terminal. Cet article construit une fonction ZSH `rgf` qui cherche dans votre code, affiche un aperçu en direct du fichier trouvé et saute directement à la bonne ligne dans VSCode quand vous appuyez sur `Enter`. Vous cherchez où une variable d'environnement est lue, ou quel fichier contient encore cette URL codée en dur que vous vouliez nettoyer. Vous lancez `grep -rn "DB_PASSWORD" .` et vous récupérez quarante lignes de noms de fichiers, de numéros de ligne et d'extraits — tout mélangé. Vous en choisissez une, vous ouvrez le fichier, vous défilez jusqu'à la bonne ligne, vous constatez que ce n'est pas celle-là, vous fermez et vous recommencez. Il y a mieux. **Quick Jump:** [Le panneau d'aperçu en action](#seeing-the-preview-panel) | [Étape 3 — La fonction rgf](#step-3--the-rgf-function) ## Le panneau d'aperçu en action {#seeing-the-preview-panel} Envoyez `ripgrep` dans `fzf` avec un aperçu `bat` attaché, et voici le résultat — un panneau en direct, avec coloration syntaxique, qui se met à jour au fil de vos déplacements : ![Recherche de DB\_PASSWORD avec aperçu](./images/rg_fzf_bat.webp) Quand vous déplacez le curseur dans la liste des résultats, le panneau de droite se met à jour en direct — il affiche le fichier avec coloration syntaxique, la ligne trouvée surlignée en jaune et quelques lignes de contexte autour. ## Pourquoi ripgrep plutôt que grep ? {#why-ripgrep-instead-of-grep} `grep -r` suffit pour un petit projet. `ripgrep` est conçu pour le monde réel : * Il **respecte automatiquement `.gitignore`** — fini les résultats venant de `node_modules`, `vendor` ou des dossiers de build. * Il est **nettement plus rapide** sur les gros projets, grâce au traitement parallèle et à un parcours de fichiers plus intelligent. * Son format de sortie (`file:line:content`) est pensé pour l'intégration avec d'autres outils. Une comparaison rapide — recherche de `TODO` dans un projet Node.js contenant `node_modules` : ```bash $ time grep -rn "TODO" . ... hundreds of results from node_modules ... real 0m4.2s $ time rg "TODO" src/auth/login.ts:47: // TODO: add rate limiting src/api/users.ts:123: // TODO: validate email format src/components/Header.tsx:8: // TODO: make logo clickable real 0m0.08s ``` La vitesse et la réduction du bruit justifient à elles seules le changement. ## Prérequis {#prerequisites} Cet article s'appuie sur [fzf](/blog/linux-fzf-introduction), que vous devriez déjà avoir installé. Deux outils supplémentaires sont nécessaires : **ripgrep** et **bat**. ### ripgrep {#ripgrep} > [ripgrep](https://github.com/BurntSushi/ripgrep) est un outil de recherche en ligne de commande rapide qui parcourt récursivement les fichiers à la recherche de motifs de texte, avec les performances de Rust, tout en respectant automatiquement `.gitignore` et les règles d'exclusion similaires. *Je lui ai consacré un article complet : [ripgrep — The Search Tool That Changed My WSL2 Workflow](/blog/ripgrep).* Prerequisite: ripgrep ```bash $ sudo apt install ripgrep ``` Verify: ```bash $ rg --version ripgrep 14.1.0 ``` ### bat {#bat} [bat](https://github.com/sharkdp/bat) (batcat sur Debian/Ubuntu) est un remplaçant moderne de cat qui affiche les fichiers avec coloration syntaxique, intégration Git, pagination et d'autres fonctionnalités pratiques pour les développeurs. Il sert ici à alimenter le panneau d'aperçu. Prerequisite: bat ```bash $ sudo apt install bat ``` Verify: ```bash $ bat --version bat 0.24.0 ``` > **batcat sur Ubuntu/Debian:** > > Sur certains systèmes Ubuntu/Debian, le binaire s'appelle `batcat` et non `bat`. Si c'est votre cas, remplacez simplement *bat* par *batcat* dans la suite de cet article. ## Essayer avec Docker {#seeing-it-in-action-with-docker} Avant d'installer `ripgrep`, `fzf` et `bat` sur votre machine, essayez `rgf` dans un container jetable qui contient déjà les trois, la fonction elle-même branchée via un véritable autoload ZSH, et un petit projet de démo qui correspond à la section « Scénarios concrets » plus bas : un `DB_PASSWORD` qui fuit, trois marqueurs `TODO`/`FIXME`/`HACK`, un appel à `sendEmail`, un port codé en dur et une interface TypeScript. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous reproduit `rgf` à l'octet près depuis le Snippet plus bas, plus le > câblage `fpath`/`autoload` décrit à l'étape 3. Pas besoin de VSCode pour essayer : `rgf` se replie sur > l'affichage de `file:line` dans le terminal quand `code` n'est pas dans `$PATH`, ce qui est exactement le cas > dans ce container. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # Ubuntu's docker image only enables "main" + "restricted" by default; # fzf and bat live in "universe". RUN sed -i 's/^Components: main restricted$/Components: main restricted universe multiverse/' \ /etc/apt/sources.list.d/ubuntu.sources && \ apt-get update && \ apt-get install -y --no-install-recommends \ zsh \ ripgrep \ fzf \ bat && \ rm -rf /var/lib/apt/lists/* # ~/.zsh/fns/rgf — the exact function from this article, wired via real # zsh autoload (fpath + autoload -Uz), no VSCode required: it falls back to # printing "file:line" when `code` is not in $PATH. RUN mkdir -p /root/.zsh/fns RUN printf '%s\n' \ '#!/bin/zsh' \ '# Description: Interactive code search using ripgrep + fzf + bat preview.' \ '# Opens the selected result in VSCode at the exact matching line.' \ '# Usage: rgf [rg options] [path]' \ '# Place in: ~/.zsh/fns/rgf (no extension)' \ '' \ '_rgf_check_deps() {' \ ' local missing_dep=0' \ ' local os_type' \ ' os_type=$(uname -s)' \ '' \ ' if ! command -v fzf >/dev/null 2>&1; then' \ ' printf "\033[0;31m[Error]\033[0m '\''fzf'\'' is not installed.\n"' \ ' missing_dep=1' \ ' fi' \ '' \ ' if ! command -v rg >/dev/null 2>&1; then' \ ' printf "\033[0;31m[Error]\033[0m '\''rg'\'' (ripgrep) is not installed.\n"' \ ' missing_dep=1' \ ' fi' \ '' \ ' if ! command -v bat >/dev/null 2>&1 && ! command -v batcat >/dev/null 2>&1; then' \ ' printf "\033[0;31m[Error]\033[0m '\''bat'\'' is not installed.\n"' \ ' missing_dep=1' \ ' fi' \ '' \ ' if [[ "$missing_dep" -eq 1 ]]; then' \ ' printf "\033[0;33mHow to install:\033[0m\n"' \ ' if [[ "$os_type" == "Darwin" ]]; then' \ ' printf " brew install fzf ripgrep bat\n"' \ ' elif [[ -f /etc/debian_version ]]; then' \ ' printf " sudo apt update && sudo apt install fzf ripgrep bat\n"' \ ' elif [[ -f /etc/fedora-release ]]; then' \ ' printf " sudo dnf install fzf ripgrep bat\n"' \ ' else' \ ' printf " Please install '\''fzf'\'', '\''rg'\'' and '\''bat'\'' using your package manager.\n"' \ ' fi' \ ' return 1' \ ' fi' \ '' \ ' return 0' \ '}' \ '' \ '# 1. Run the dependency check' \ 'if ! _rgf_check_deps; then' \ ' return 1' \ 'fi' \ '' \ '# 2. Determine the correct bat binary (Ubuntu/Debian ship it as '\''batcat'\'')' \ 'local bat_bin' \ 'if command -v bat >/dev/null 2>&1; then' \ ' bat_bin="bat"' \ 'else' \ ' bat_bin="batcat"' \ 'fi' \ '' \ '# 3. Search and preview — write fzf'\''s output to a temp file so that fzf runs' \ '# in the foreground with full terminal access. Using $(...) would block the' \ '# TUI in WSL subshells.' \ 'local tmpfile' \ 'tmpfile=$(mktemp /tmp/rgf.XXXXXX)' \ '' \ 'rg --color=always --line-number --no-heading --smart-case "${@:-}" \' \ ' | fzf --ansi \' \ ' --layout=reverse \' \ ' --exact \' \ ' --delimiter='\'':'\'' \' \ ' --nth=3.. \' \ ' --preview="${bat_bin} --color=always --pager=never --highlight-line {2} -- {1}" \' \ ' --preview-window='\''right:60%:+{2}+3/3:wrap'\'' > "$tmpfile"' \ '' \ 'local fzf_exit=$?' \ 'local selected' \ 'selected=$(<"$tmpfile")' \ 'rm -f "$tmpfile"' \ '' \ '# fzf exits 130 on Esc/CTRL-C, 1 when no match' \ '[[ "$fzf_exit" -ne 0 || -z "$selected" ]] && return' \ '' \ '# 4. Strip ANSI color codes before parsing file path and line number' \ 'local file line' \ 'file=$(echo "$selected" | sed $'\''s/\033\[[0-9;]*m//g'\'' | cut -d: -f1)' \ 'line=$(echo "$selected" | sed $'\''s/\033\[[0-9;]*m//g'\'' | cut -d: -f2)' \ '' \ '[[ -n "$file" ]] || return' \ '' \ '# 5. Open in VSCode at the exact matching line' \ 'if command -v code >/dev/null 2>&1; then' \ ' code --goto "$file:$line"' \ 'else' \ ' printf "\033[0;33m[Warning]\033[0m '\''code'\'' (VSCode) not found. Result: %s:%s\n" "$file" "$line"' \ 'fi' \ > /root/.zsh/fns/rgf RUN printf '%s\n' \ 'fpath=(~/.zsh/fns $fpath)' \ 'autoload -Uz rgf' \ >> /root/.zshrc # Build a demo project matching the "real-world scenarios" section: a # leaking DB_PASSWORD, TODO/FIXME/HACK markers, a sendEmail call site, a # hardcoded port, and a TypeScript interface RUN mkdir -p /root/demo/src/components /root/demo/config && \ printf 'DB_PASSWORD=demo-secret\n' > /root/demo/config/.env && \ printf 'function sendEmail(user) {\n // TODO: retry on failure\n}\n' \ > /root/demo/src/mailer.js && \ printf 'import { sendEmail } from "./mailer";\n\n// FIXME: this still calls the old name in one place\nsendEmail(currentUser);\n' \ > /root/demo/src/notifications.js && \ printf '// HACK: temporary workaround for #482\nfunction legacyRetry() {}\n' \ > /root/demo/src/legacy.js && \ printf 'services:\n db:\n image: postgres:16\n ports:\n - "5432:5432"\n' \ > /root/demo/compose.yml && \ printf 'export interface UserProps {\n id: number;\n name: string;\n}\n' \ > /root/demo/src/components/UserProfile.tsx WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc CMD ["/bin/zsh", "-i", "-l"] ``` Construisez-le et lancez-le : ```bash $ docker build -t rgf-demo . [+] Building 32.5s (11/11) FINISHED ✔ exporting to image $ docker run --rm -it rgf-demo 🐳 root ~/demo # ``` Essayez maintenant, en direct, les scénarios exacts de la section « Scénarios concrets » ci-dessous : ```bash rgf "DB_PASSWORD" # the leaking secret in config/.env rgf "TODO|FIXME|HACK" # all three markers across the demo project rgf "sendEmail" # the call site in notifications.js rgf -t ts "interface UserProps" ``` Chacun ouvre le même panneau « liste + aperçu » que la capture d'écran en haut de cet article — naviguez avec `↑`/`↓`, filtrez en tapant, appuyez sur `Enter` pour sélectionner. Comme il n'y a pas de VSCode dans le container, `rgf` affiche `file:line` dans le terminal au lieu d'ouvrir un éditeur — tout le reste se comporte exactement comme sur votre propre machine. ## Étape 1 — Brancher ripgrep sur fzf {#step-1--connect-ripgrep-to-fzf} La combinaison la plus simple bat déjà `grep` tout seul : ```bash rg "DB_PASSWORD" | fzf ``` ![Recherche de DB\_PASSWORD sans aperçu](./images/rg_fzf.webp) Vous obtenez une liste interactive et filtrable. Tapez quelques lettres pour affiner les résultats. Appuyez sur `Enter` pour afficher la ligne sélectionnée. Appuyez sur `Esc` pour sortir sans rien sélectionner. Mais cela n'affiche que la ligne trouvée — aucun contexte, aucun aperçu du code autour. ## Étape 2 — Ajouter un panneau d'aperçu bat {#step-2--add-a-bat-preview-panel} C'est là que ça devient intéressant. `fzf` propose une option `--preview` qui exécute une commande arbitraire pour chaque ligne surlignée et affiche sa sortie dans un panneau latéral. ```bash rg --color=always --line-number --no-heading --smart-case "DB_PASSWORD" \ | fzf --ansi \ --delimiter=':' \ --preview='bat --color=always --highlight-line {2} -- {1}' \ --preview-window='right:60%:+{2}+3/3:~3' ``` C'est la commande derrière la capture d'écran en haut de cet article. Voyons ce que fait chaque option : **Référence des options** * **`--color=always`** (rg) — Force une sortie colorée même quand on utilise un pipe. Sans ça, rg détecte le pipe et supprime toutes les couleurs, ne laissant à fzf que du texte brut. * **`--line-number --no-heading`** (rg) — Formate chaque résultat en `filename:linenumber:content` sur une seule ligne. C'est le format que fzf va analyser. * **`--smart-case`** (rg) — Insensible à la casse quand la requête est tout en minuscules, sensible à la casse dès qu'elle contient une majuscule. Le meilleur défaut pour chercher dans du code. * **`--ansi`** (fzf) — Indique à fzf d'interpréter et d'afficher les codes couleur ANSI de la sortie de rg. * **`--delimiter=':'`** (fzf) — Découpe chaque ligne sur `:` de sorte que `{1}` = nom du fichier, `{2}` = numéro de ligne, `{3}` = contenu. Ces champs sont ensuite utilisés dans `--preview`. * **`--highlight-line {2}`** (bat) — Surligne la ligne trouvée dans l'aperçu. `{2}` est résolu par fzf vers le champ du numéro de ligne. * **`-- {1}`** — Le double tiret sépare les options de bat de l'argument nom de fichier. `{1}` est résolu vers le nom du fichier. * **`--preview-window='right:60%:+{2}+3/3:~3'`** — Ouvre l'aperçu à droite, sur 60 % de la largeur du terminal. `+{2}+3/3` fait défiler le panneau pour que la ligne trouvée soit visible, centrée avec du contexte. `~3` garde 3 lignes d'en-tête en sticky. ## Étape 3 — La fonction `rgf` {#step-3--the-rgf-function} Taper tout ce pipeline à chaque fois n'est pas pratique. La solution suit le même principe que l'article [un workflow ZSH modulaire](/blog/modular-zsh-workflow) : un fichier autonome dans `~/.zsh/fns/`, chargé à la demande par ZSH — sans toucher à `.zshrc`. La fonction vérifie toutes ses dépendances au démarrage, détecte automatiquement `bat` ou `batcat`, et ouvre le résultat sélectionné dans VSCode à la ligne exacte quand vous appuyez sur `Enter`. Si `code` n'est pas dans `$PATH`, elle se replie sur l'affichage de `file:line` dans le terminal. **Project: \~/.zsh/fns** > **Note:** > > Le nom du fichier doit être exactement « rgf » — sans extension. L'autoload ZSH utilise le nom du fichier comme nom de commande. **rgf** ``` #!/bin/zsh # Description: Interactive code search using ripgrep + fzf + bat preview. # Opens the selected result in VSCode at the exact matching line. # Usage: rgf [rg options] [path] # Place in: ~/.zsh/fns/rgf (no extension) _rgf_check_deps() { local missing_dep=0 local os_type os_type=$(uname -s) if ! command -v fzf >/dev/null 2>&1; then printf "\033[0;31m[Error]\033[0m 'fzf' is not installed.\n" missing_dep=1 fi if ! command -v rg >/dev/null 2>&1; then printf "\033[0;31m[Error]\033[0m 'rg' (ripgrep) is not installed.\n" missing_dep=1 fi if ! command -v bat >/dev/null 2>&1 && ! command -v batcat >/dev/null 2>&1; then printf "\033[0;31m[Error]\033[0m 'bat' is not installed.\n" missing_dep=1 fi if [[ "$missing_dep" -eq 1 ]]; then printf "\033[0;33mHow to install:\033[0m\n" if [[ "$os_type" == "Darwin" ]]; then printf " brew install fzf ripgrep bat\n" elif [[ -f /etc/debian_version ]]; then printf " sudo apt update && sudo apt install fzf ripgrep bat\n" elif [[ -f /etc/fedora-release ]]; then printf " sudo dnf install fzf ripgrep bat\n" else printf " Please install 'fzf', 'rg' and 'bat' using your package manager.\n" fi return 1 fi return 0 } # 1. Run the dependency check if ! _rgf_check_deps; then return 1 fi # 2. Determine the correct bat binary (Ubuntu/Debian ship it as 'batcat') local bat_bin if command -v bat >/dev/null 2>&1; then bat_bin="bat" else bat_bin="batcat" fi # 3. Search and preview — write fzf's output to a temp file so that fzf runs # in the foreground with full terminal access. Using $(...) would block the # TUI in WSL subshells. local tmpfile tmpfile=$(mktemp /tmp/rgf.XXXXXX) rg --color=always --line-number --no-heading --smart-case "${@:-}" \ | fzf --ansi \ --layout=reverse \ --exact \ --delimiter=':' \ --nth=3.. \ --preview="${bat_bin} --color=always --pager=never --highlight-line {2} -- {1}" \ --preview-window='right:60%:+{2}+3/3:wrap' > "$tmpfile" local fzf_exit=$? local selected selected=$(<"$tmpfile") rm -f "$tmpfile" # fzf exits 130 on Esc/CTRL-C, 1 when no match [[ "$fzf_exit" -ne 0 || -z "$selected" ]] && return # 4. Strip ANSI color codes before parsing file path and line number local file line file=$(echo "$selected" | sed $'s/\033\[[0-9;]*m//g' | cut -d: -f1) line=$(echo "$selected" | sed $'s/\033\[[0-9;]*m//g' | cut -d: -f2) [[ -n "$file" ]] || return # 5. Open in VSCode at the exact matching line if command -v code >/dev/null 2>&1; then code --goto "$file:$line" else printf "\033[0;33m[Warning]\033[0m 'code' (VSCode) not found. Result: %s:%s\n" "$file" "$line" fi ``` > **Pourquoi un fichier temporaire plutôt que $(...):** > > Lancer `selected=$(rg ... | fzf ...)` capture la sortie standard de fzf et l'empêche d'afficher son interface en WSL — la commande reste bloquée. Écrire la sortie de fzf dans un fichier temporaire lui permet de tourner au premier plan avec un accès complet au terminal ; la sélection est lue depuis le fichier après la fermeture de fzf. Si `~/.zsh/fns` est déjà dans votre `fpath` (voir l'article [un workflow ZSH modulaire](/blog/modular-zsh-workflow)), rechargez ZSH et lancez : ```bash rgf "database" ``` Naviguez avec `↑` / `↓`, filtrez en tapant, appuyez sur `Enter` pour sauter à la ligne trouvée dans VSCode, `Esc` pour annuler. ## Scénarios concrets {#real-world-scenarios} Voici quatre situations où `rgf` devient partie intégrante de votre quotidien. ### Trouver tous les TODO du projet {#find-all-todos-in-the-project} ```bash rgf "TODO|FIXME|HACK" ``` rg gère les regex par défaut. Ceci cherche n'importe lequel des trois marqueurs. Parcourez-les tous, choisissez celui que vous voulez traiter et atterrissez directement dans l'éditeur. ### Retrouver où une fonction est appelée {#track-down-where-a-function-is-called} Vous avez renommé `sendEmail` en `sendNotification` mais quelque chose plante encore. Trouvez tous les appels : ```bash rgf "sendEmail" ``` Chaque résultat montre l'appel dans son contexte. Vous voyez immédiatement s'il s'agit d'un import, d'un appel ou d'un test — sans ouvrir les fichiers un par un. ### Localiser une valeur codée en dur {#locate-a-hardcoded-value} Votre fichier Docker Compose contenait un port codé en dur. Traîne-t-il encore quelque part dans le code ? ```bash rgf "5432" ``` L'aperçu montre les lignes autour — vous voyez d'un coup d'œil s'il s'agit d'une valeur par défaut, d'un commentaire ou d'une chaîne de connexion active. ### Chercher dans un dossier précis {#search-within-a-specific-folder} `rgf` passe tous ses arguments directement à `rg`, donc la limitation à un chemin fonctionne comme prévu : ```bash rgf "useEffect" src/components/ ``` Seuls les fichiers sous `src/components/` sont parcourus. Idem pour les filtres par type de fichier : ```bash rgf -t ts "interface UserProps" ``` L'option `-t ts` demande à rg de ne chercher que dans les fichiers TypeScript. ## Pour aller plus loin {#going-further} ### Associer rgf à un raccourci clavier {#bind-rgf-to-a-keyboard-shortcut} Si vous voulez `rgf` à une touche de distance, ajoutez un raccourci ZSH. Celui-ci associe `CTRL`+`F` au lancement de `rgf` avec le contenu actuel de la ligne de commande comme requête initiale : ```zsh # In ~/.zshrc rgf-widget() { rgf "$BUFFER"; zle reset-prompt } zle -N rgf-widget bindkey '^F' rgf-widget ``` ### Combiner avec le navigateur de repos {#combine-with-the-repo-navigator} Si vous avez la fonction `repo` de l'article [un workflow ZSH modulaire](/blog/modular-zsh-workflow), un enchaînement naturel apparaît : `repo` pour sauter au projet, `rgf` pour trouver le code, `Enter` pour l'ouvrir. Trois frappes pour atteindre n'importe quelle ligne de n'importe quel projet. ### Utiliser ripgrep seul {#use-ripgrep-standalone} `rg` vaut aussi la peine d'être appris pour lui-même — même sans `fzf`. Les options que vous utiliserez rapidement : ```bash rg -l "pattern" # filenames only, no content rg -c "pattern" # count of matches per file rg --type-list # list all supported file types rg -t py "def connect" # Python files only rg -g "*.yml" "image:" # glob filter — only YAML files ``` --- # delta : un pager avec coloration syntaxique pour git diff > Arrêtez de plisser les yeux devant les sorties brutes de git diff. Découvrez comment installer et configurer Delta pour ajouter coloration syntaxique et vue côte à côte en moins de 5 minutes. --- ![delta : un pager avec coloration syntaxique pour git diff](/img/v2/git-delta.webp) **TL;DR** La sortie de `git diff` est fonctionnelle mais difficile à lire : pas de coloration syntaxique, pas de vue côte à côte, des numéros de ligne noyés dans le bruit des `@@ -47,6 +47,8 @@`. `delta` est un pager de remplacement immédiat qui transforme chaque `git diff`, `git show` et `git log -p` en une vue côte à côte, colorée, avec des diffs au niveau du mot — sans modifier une seule de vos commandes git. Cela fait trente secondes que vous fixez la sortie d'un `git diff` pour comprendre ce qui a réellement changé. Les lignes en plus et en moins se confondent, chaque fichier affiche la même nuance de rouge et de vert, et les numéros de ligne sont enfouis dans un en-tête qu'il faut déchiffrer consciemment. `delta` règle le problème en cinq minutes. *Il rejoint la même famille de réécritures modernes que [ripgrep](/blog/ripgrep) (pour `grep`) et [eza](/blog/linux-eza) (pour `ls`). En dehors d'un repository git, [`diff`](/blog/linux-diff-file-folder) reste l'outil de référence.* ## Avant et après {#before-and-after} ### Une ligne modifiée sans delta {#a-changed-line-without-delta} ```diff - const expiry = token.exp * 1000; + const expiry = token.exp * 1000 + TOKEN_GRACE_MS; ``` Tout a le même poids visuel. Vous parcourez toute la ligne à la recherche de la différence. ### La même ligne avec delta {#the-same-line-with-delta} Delta met en évidence `+ TOKEN_GRACE_MS` au niveau du caractère — le reste de la ligne reste atténué. Votre œil va directement au changement, pas au contexte inchangé autour. Ce diff au niveau du mot est particulièrement utile quand : * Une variable a été renommée (`getUserById` → `findUserById`) * Une faute de frappe a été corrigée dans une chaîne * Un flag a été ajouté ou retiré d'un long appel de fonction Sans diff au niveau du mot, chacun de ces cas produit une ligne supprimée entièrement rouge et une ligne ajoutée entièrement verte. Avec delta, seul le mot modifié est mis en évidence. ## Qu'est-ce que delta ? {#what-is-delta} [delta](https://github.com/dandavison/delta) est un pager avec coloration syntaxique qui s'intercale entre git et votre terminal. Il intercepte la sortie brute du diff et l'affiche avec : * Une coloration syntaxique par langage — un changement TypeScript ressemble à du TypeScript, un changement YAML ressemble à du YAML * Une vue côte à côte — ancienne version à gauche, nouvelle version à droite, alignées ligne par ligne * Un diff au niveau du mot — met en évidence les caractères exacts qui ont changé dans une ligne, pas la ligne entière * Des numéros de ligne — toujours visibles, toujours alignés * La navigation — sautez d'un hunk à l'autre avec `n` et `N` Le point essentiel : **vous ne changez aucune commande git**. Vous tapez toujours `git diff`, `git show`, `git log -p`. Delta intercepte la sortie automatiquement. ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Vous venez de lire le diff « avant » ci-dessus. Voici un container où delta est déjà installé, configuré et pointé sur un vrai changement non indexé — pour voir l'« après » sans toucher à votre propre `.gitconfig`. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe `git` et `git-delta`, écrit le bloc `.gitconfig` complet de cet > article et prépare un petit repo avec exactement le type de changement d'un seul mot (`+ TOKEN_GRACE_MS`) > qui rend le diff au niveau du mot de delta intéressant à voir. Rien ne change sur votre machine. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # Ubuntu's docker image only enables "main" + "restricted" by default; # git-delta lives in "universe". RUN sed -i 's/^Components: main restricted$/Components: main restricted universe multiverse/' \ /etc/apt/sources.list.d/ubuntu.sources && \ apt-get update && \ apt-get install -y --no-install-recommends \ git \ git-delta && \ rm -rf /var/lib/apt/lists/* # Configure git to use delta — the exact block from this article RUN git config --global core.pager delta && \ git config --global interactive.diffFilter "delta --color-only" && \ git config --global delta.navigate true && \ git config --global delta.side-by-side true && \ git config --global delta.line-numbers true && \ git config --global delta.syntax-theme "Dracula" && \ git config --global merge.conflictstyle diff3 && \ git config --global diff.colorMoved default && \ git config --global user.email "demo@example.com" && \ git config --global user.name "Demo" # Pre-build a repo with a real, word-level-diff-friendly change already # unstaged, so `git diff` has something worth looking at immediately RUN mkdir -p /root/demo && cd /root/demo && \ git init -q && \ printf 'function refreshToken(token) {\n const expiry = token.exp * 1000;\n return expiry;\n}\n' > auth.js && \ git add auth.js && \ git commit -q -m "Add refreshToken helper" && \ sed -i 's/const expiry = token.exp \* 1000;/const expiry = token.exp * 1000 + TOKEN_GRACE_MS;/' auth.js WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Buildez-le et lancez-le : ```bash $ docker build -t delta-demo . [+] Building 26.7s (8/8) FINISHED ✔ exporting to image $ docker run --rm -it delta-demo 🐳 root ~/demo # git diff ``` `git diff` s'ouvre en vue côte à côte, avec coloration syntaxique, et `TOKEN_GRACE_MS` ressort au niveau du caractère au lieu de voir toute la ligne passer au vert. Appuyez sur `q` pour fermer le pager, puis essayez `git log -p`, `git show HEAD`, ou modifiez `auth.js` vous-même et relancez le diff — la même configuration s'applique à toutes les commandes. ## Installation {#install} Prerequisite: git-delta ```bash $ sudo apt install git-delta ``` Verify: ```bash $ delta --version delta 0.18.2 ``` > **Sur les anciennes versions d'Ubuntu/Debian:** > > Le package peut s'appeler `delta` plutôt que `git-delta`. Si `apt install git-delta` échoue, essayez `sudo apt install delta` — ou installez le dernier binaire depuis la [page des releases GitHub](https://github.com/dandavison/delta/releases). ## Configurer git pour utiliser delta {#configure-git-to-use-delta} Delta fonctionne comme un remplacement de `core.pager`. Ouvrez votre `~/.gitconfig` et ajoutez : ```ini [core] pager = delta [interactive] diffFilter = delta --color-only [delta] navigate = true side-by-side = true line-numbers = true syntax-theme = Dracula [merge] conflictstyle = diff3 [diff] colorMoved = default ``` Enregistrez le fichier. Pas besoin de redémarrer le shell — le pager est invoqué à chaque commande. Voyons ce que fait chaque option. **Référence de configuration** * **`core.pager = delta`** — indique à git de faire passer toute sortie paginée par delta au lieu de `less`. Cela couvre `git diff`, `git show`, `git log -p`, `git stash show -p`, et plus encore. * **`interactive.diffFilter = delta --color-only`** — applique le rendu couleur de delta dans les commandes interactives comme `git add -p`, sans casser la gestion de l'entrée. * **`navigate = true`** — appuyez sur `n` pour sauter au fichier modifié suivant, `N` pour revenir en arrière. Indispensable quand vous relisez un diff qui touche beaucoup de fichiers. * **`side-by-side = true`** — affiche l'ancienne et la nouvelle version sur deux colonnes. Le plus gros gain de lisibilité. * **`line-numbers = true`** — affiche les numéros de ligne dans une colonne dédiée de chaque côté. Fini le déchiffrage des en-têtes `@@ -47,6 +47,8 @@`. * **`syntax-theme = Dracula`** — définit le thème de couleurs pour la coloration syntaxique. Changez-le pour l'accorder au thème de votre terminal. Lancez `delta --list-syntax-themes` pour voir toutes les options. * **`merge.conflictstyle = diff3`** — combiné à delta, affiche les trois versions lors d'un conflit de merge (la nôtre, la leur, et l'ancêtre commun). Bien plus simple à résoudre. ## Le mode côte à côte {#side-by-side-mode} Avec `side-by-side = true`, delta divise chaque fichier modifié en deux colonnes : * **Gauche** — l'ancienne version, avec les lignes supprimées en rouge * **Droite** — la nouvelle version, avec les lignes ajoutées en vert * **Les deux** — les lignes de contexte inchangées sont affichées des deux côtés, alignées Pour une fonction renommée appelée à dix endroits, vous ne lisez plus deux streams entremêlés de rouge et de vert. Vous lisez l'avant à gauche et l'après à droite, comme dans un outil de code review. Si votre terminal est étroit et que les deux colonnes deviennent trop serrées, vous pouvez désactiver le côte à côte pour une seule commande : ```bash git -c delta.side-by-side=false diff ``` Ou ajoutez un alias court dans `.gitconfig` : ```ini [alias] dw = -c delta.side-by-side=false diff ``` `git dw` vous donne le mode une colonne ; `git diff` garde le comportement par défaut. ## Les thèmes {#themes} Delta utilise le même moteur de thèmes que `bat` et `syntect`. Pour lister tous les thèmes disponibles : ```bash $ delta --list-syntax-themes ``` Pour prévisualiser un thème sans modifier votre configuration : ```bash $ git diff | delta --syntax-theme=gruvbox-dark ``` Quelques bons points de départ : **Thèmes conseillés selon le fond du terminal** * **Fond sombre** — `Dracula`, `gruvbox-dark`, `TwoDark`, `Nord` * **Fond clair** — `GitHub`, `Monokai Extended Light`, `OneHalfLight` ## La navigation entre les hunks {#navigation-between-hunks} Avec `navigate = true`, delta ajoute une navigation au clavier dans le pager : **Touches de navigation** * **n** — Sauter au fichier modifié suivant * **N** — Sauter au fichier modifié précédent * **q** — Quitter * **Espace** — Page suivante C'est surtout utile avec `git log -p` ou `git diff HEAD~10`, où le diff s'étale sur de nombreux fichiers. ## Delta fonctionne partout où git fonctionne {#delta-works-everywhere-git-does} Une fois `core.pager = delta` défini, delta s'applique à toutes les commandes git qui paginent leur sortie : ```bash git show HEAD # last commit, with highlighting git log -p # full history with diffs git stash show -p # what's in a stash entry git diff HEAD~3 # compare to 3 commits ago git diff main...feature # compare branches ``` Aucun flag supplémentaire. Aucune fonction wrapper. Ça fonctionne, point. ## Pour aller plus loin {#going-further} Si vous utilisez aussi [des alias `git diff` dans votre `.gitconfig`](/blog/git-config), ces alias profitent automatiquement de delta puisque le pager s'applique au niveau de la sortie, pas au niveau de la commande. Pour une expérience git en TUI encore plus riche, delta sert de moteur de rendu des diffs dans [lazygit](https://github.com/jesseduffield/lazygit) et [tig](https://github.com/jonas/tig) — deux clients git TUI qui respectent `core.pager`. ## Conclusion {#conclusion} Trente secondes à plisser les yeux devant un `git diff` brut, c'était tout le problème — pas de coloration syntaxique, pas de côte à côte, des numéros de ligne noyés dans le bruit des `@@`. `delta` règle les trois avec un `apt install` et une poignée de lignes dans `.gitconfig`, et toutes les commandes git que vous connaissez déjà continuent de fonctionner exactement telles quelles. Si vous parcourez aussi l'historique avec des alias, [configurer les alias `git diff` dans `.gitconfig`](/blog/git-config) est l'étape suivante naturelle — ils héritent du rendu de delta gratuitement. --- # ripgrep — l'outil de recherche qui a changé mon workflow WSL2 > Découvrez ripgrep, l'alternative ultra-rapide à grep écrite en Rust. Installation sur Ubuntu/WSL2, configuration ZSH, fonctions shell pratiques et cas d'usage réels qui vont transformer votre façon de chercher dans le code au quotidien. --- ![ripgrep — l'outil de recherche qui a changé mon workflow WSL2](/img/v2/ripgrep.webp) **TL;DR** ripgrep (`rg`) est un outil de recherche ligne par ligne écrit en Rust qui surpasse largement le grep traditionnel — surtout sur les gros codebases. Il respecte `.gitignore` par défaut, ignore les fichiers binaires et gère l'Unicode nativement. Cet article couvre l'installation sur Ubuntu/WSL2, un fichier de configuration `~/.ripgreprc`, des alias et fonctions ZSH pour votre `~/.zshrc` et votre répertoire `~/.zsh/fns/`, ainsi que des cas d'usage réels qui vous feront vous demander comment vous avez fait sans lui jusqu'ici. Zut. J'ai tapé cette commande tellement de fois que je pourrais la taper les yeux fermés : `grep -rn --include="*.php" "getUser" . | grep -v vendor`. Ça marche — grep est un classique, il fait le job. Mais au fil des années, j'ai accumulé tellement de flags, tellement d'alias, tellement de contournements devenus automatiques pour les bizarreries de grep... À un moment, il faut se poser la question : est-ce qu'il existe mieux ? > **Que fait réellement cette commande ?:** > > `grep -rn --include="*.php" "getUser" .` se décompose comme suit : `-r` cherche récursivement dans tous les sous-répertoires, `-n` affiche le numéro de ligne de chaque correspondance, `--include="*.php"` limite la recherche aux seuls fichiers `.php`, `"getUser"` est le motif recherché et `.` indique de partir du répertoire courant. Le `| grep -v vendor` final envoie les résultats dans un second grep qui filtre et EXCLUT (`-v`) toute ligne contenant `vendor` — un contournement courant pour exclure le dossier de dépendances `vendor/`, que grep n'a aucun moyen natif d'ignorer. Oui, il existe mieux. Ça s'appelle [ripgrep](https://github.com/BurntSushi/ripgrep), et une fois qu'on y a goûté, impossible de revenir en arrière. *Il appartient à la même famille de réécritures modernes que [eza](/blog/linux-eza) (pour `ls`) et [delta](/blog/git-delta) (pour `git diff`).* ## ripgrep vs grep — comparaison côte à côte {#ripgrep-vs-grep--a-side-by-side-look} Laissez-moi vous montrer pourquoi ripgrep s'impose aussi vite. Voici les mêmes recherches exprimées dans les deux outils : **Avec grep :** ```bash grep -rn --include="*.php" "getUser" . \ | grep -v vendor ``` **Avec ripgrep :** ```bash rg "getUser" --type php ``` **Avec grep :** ```bash grep -rn -i "TODO" . \ --include="*.py" \ --include="*.pyi" \ --exclude-dir=.venv \ --exclude-dir=dist ``` **Avec ripgrep :** ```bash rg -i "TODO" --type py ``` **Avec grep :** ```bash grep -rn "DB_PASSWORD" . \ --include="*.env" \ --include="*.yml" \ --include="*.yaml" ``` **Avec ripgrep :** ```bash rg "DB_PASSWORD" -t yaml -t sh ``` Bien sûr, les flags de grep sont puissants une fois qu'on les connaît par cœur. Mais la syntaxe de ripgrep est tout simplement plus propre — et par défaut, il ignore déjà `vendor/`, `node_modules/` et tout ce qui se trouve dans votre `.gitignore`, sans le moindre flag supplémentaire. ## Qu'est-ce que ripgrep ? {#what-is-ripgrep} ripgrep (`rg` en ligne de commande) est un outil de recherche ligne par ligne écrit en Rust par Andrew Gallant (BurntSushi). La vitesse est son argument phare — les benchmarks montrent systématiquement qu'il devance grep, ag (le Silver Searcher) et ack de loin sur les gros codebases — et [hyperfine](/blog/hyperfine) vous permet de vérifier cette affirmation sur votre propre machine. Mais la vitesse brute n'est qu'une partie de l'histoire. Ce qui rend ripgrep réellement meilleur au quotidien, ce sont ses **valeurs par défaut intelligentes** : * Il respecte automatiquement les fichiers `.gitignore`, `.ignore` et `.rgignore` — donc `vendor/`, `node_modules/`, `dist/` et `.git/` sont ignorés sans aucune gymnastique à base de `--exclude-dir`. * Il ignore les fichiers binaires par défaut. * Il colorise et regroupe la sortie par nom de fichier d'office. * Il utilise l'Unicode par défaut — fini la sortie illisible sur les noms de fichiers ou les contenus accentués. * Il supporte la syntaxe regex moderne, y compris PCRE2 avec le flag `-P`. * Il ne cherche dans les fichiers cachés que si vous le demandez explicitement (`--hidden`). Ce dernier comportement d'affichage est sous-estimé. Des résultats lisibles, groupés, colorisés — immédiatement, sans la moindre configuration. Plutôt sympa, non ? ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Avant de toucher à votre propre `~/.zshrc`, essayez ripgrep dans un container jetable qui embarque déjà la config `~/.ripgreprc`, les fonctions `rgtodo`/`rgenv` et un petit faux projet conçu pour mettre en valeur les valeurs par défaut intelligentes : un dossier `vendor/` qui doit rester invisible, des commentaires TODO/FIXME, un `DB_PASSWORD` qui fuit et un appel `console.log(token)` qui mérite d'être signalé. > **Pourquoi commencer par Docker ?:** > > Le Dockerfile ci-dessous reproduit exactement le `.ripgreprc`, les fonctions `.zsh/fns/` et la section `.zshrc` > de cet article, puis construit le projet de démonstration. Rien ne change sur votre propre machine. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ zsh \ ripgrep \ git \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # ~/.ripgreprc — the exact config file from this article RUN printf '%s\n' \ '--colors=match:fg:red' \ '--colors=match:style:bold' \ '--colors=path:fg:blue' \ '--follow' \ '--glob=!.cache/*' \ '--glob=!.git/*' \ '--glob=!.output/*' \ '--glob=!.yarn/*' \ '--glob=!build/*' \ '--glob=!dist/*' \ '--glob=!node_modules/*' \ '--glob=!vendor/*' \ '--hidden' \ '--max-filesize=10M' \ '--smart-case' \ > /root/.ripgreprc # ~/.zsh/fns/ — the two custom functions from this article RUN mkdir -p /root/.zsh/fns RUN printf '%s\n' \ '# rgtodo — surface all TODO/FIXME/HACK/NOTE comments in the project' \ 'rgtodo() {' \ ' local dir="${1:-.}"' \ ' rg --color=always --line-number --smart-case "(TODO|FIXME|HACK|NOTE):?\s" "${dir}" | sort' \ '}' \ > /root/.zsh/fns/rgtodo.zsh RUN printf '%s\n' \ '# rgenv — find all references to an environment variable' \ 'rgenv() {' \ ' local var="${1}"' \ ' [[ -z "${var}" ]] && { echo "Usage: rgenv "; return 1; }' \ " rg --type-add 'config:*.{env,cfg,ini,conf}' --type config --type yaml --type sh --type php --type py \"\${var}\"" \ '}' \ > /root/.zsh/fns/rgenv.zsh # ~/.zshrc — the exact ripgrep section from this article RUN printf '%s\n' \ 'export RIPGREP_CONFIG_PATH="$HOME/.ripgreprc"' \ 'for fn_file in ~/.zsh/fns/*.zsh; do source "${fn_file}"; done' \ 'alias rgp="rg --type php"' \ 'alias rgj="rg --type js"' \ 'alias rgy="rg --type yaml"' \ 'alias rgd="rg --glob \"Dockerfile*\" --glob \"*.dockerfile\""' \ >> /root/.zshrc # Build a small demo project: a `vendor/` folder that must stay invisible, a # PHP controller worth finding, TODO/FIXME comments, a leaking JWT in a JS # file, and a compose file with image references RUN mkdir -p /root/demo/vendor/legacy /root/demo/src && \ printf ' /root/demo/vendor/legacy/User.php && \ printf ' /root/demo/src/UserController.php && \ printf ' /root/demo/src/legacy.php && \ printf 'DB_PASSWORD=demo-secret\n' > /root/demo/.env && \ printf 'services:\n app:\n image: php:8.3-fpm\n db:\n image: postgres:16\n' \ > /root/demo/compose.yml && \ printf 'function refreshToken(token) {\n console.log(token);\n}\n\nfunction checkout(cart) {\n for (const item of cart) {\n console.log(item);\n }\n}\n' \ > /root/demo/src/app.js && \ printf 'FROM php:8.3-fpm\nCOPY . /app\n' > /root/demo/Dockerfile.app && \ printf 'vendor/\n' > /root/demo/.gitignore && \ git -C /root/demo init -q && \ git -C /root/demo -c user.email=demo@example.com -c user.name=Demo \ add -A && \ git -C /root/demo -c user.email=demo@example.com -c user.name=Demo \ commit -q -m "Demo project" WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc CMD ["/bin/zsh", "-i", "-l"] ``` Buildez et lancez : ```bash $ docker build -t ripgrep-demo . [+] Building 21.3s (10/10) FINISHED ✔ exporting to image $ docker run --rm -it ripgrep-demo 🐳 root ~/demo # ``` Jouez maintenant avec les scénarios décrits plus haut, en direct : ```bash $ rg "getUser" src/UserController.php 2:function getUser($id) { src/legacy.php 4:function getAllUsers() { $ rgtodo src/legacy.php:2:// TODO: cache this lookup src/legacy.php:3:// FIXME: N+1 query below $ rgenv DB_PASSWORD .env 1:DB_PASSWORD=demo-secret $ rg -C 2 "console.log" src/app.js 2-function refreshToken(token) { 3: console.log(token); 4-} 7- for (const item of cart) { 8: console.log(item); 9- } ``` Remarquez que `vendor/legacy/User.php` — qui définit lui aussi `getUser`, exprès — n'apparaît jamais dans aucune de ces recherches : le `.gitignore` et l'entrée de configuration `--glob=!vendor/*` l'excluent tous les deux, exactement comme décrit plus haut. Essayez `rg --no-ignore "getUser"` pour le voir réapparaître. ## Installation sur Ubuntu / WSL2 {#installation-on-ubuntu--wsl2} ripgrep est présent dans les dépôts de paquets par défaut d'Ubuntu, donc l'installer tient en une ligne : Prerequisite: ripgrep ```bash $ sudo apt update && sudo apt install ripgrep -y Reading package lists... Done Building dependency tree... Done 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. ``` Verify: ```bash $ rg --version ripgrep 14.1.0 -rev 0 (rev 4649aa9700 2024-03-30) features: +SIMD +AVX (compiled), +SIMD +AVX (runtime) ``` > **Vous voulez la dernière version ?:** > > Les dépôts Ubuntu ont parfois quelques releases de retard sur ripgrep en amont. Pour la version la plus récente, récupérez le dernier package `.deb` directement depuis la [page des releases GitHub](https://github.com/BurntSushi/ripgrep/releases) et installez-le avec `sudo dpkg -i ripgrep_*.deb`. À partir de la version 14, vous bénéficiez aussi d'un meilleur support de PCRE2 et d'un parcours de répertoires plus rapide. ## Configuration — \~/.ripgreprc {#configuration--ripgreprc} Comme la plupart des outils Unix, ripgrep se configure via un fichier dédié. Vous le désignez avec la variable d'environnement `RIPGREP_CONFIG_PATH`. Voici un exemple de fichier `~/.ripgreprc` : **\~/.ripgreprc** ``` --colors=match:fg:red --colors=match:style:bold --colors=path:fg:blue --follow --glob=!.cache/* --glob=!.git/* --glob=!.output/* --glob=!.yarn/* --glob=!build/* --glob=!dist/* --glob=!node_modules/* --glob=!vendor/* --hidden --max-filesize=10M --smart-case ``` Chaque ligne est un flag appliqué par défaut à chaque invocation de `rg`. Quelques points méritent une explication : * `--hidden` fait chercher ripgrep à l'intérieur des fichiers et dossiers cachés (tout ce qui commence par un point). Combiné à l'exclusion `--glob=!.git/*`, cela veut dire que vous pouvez chercher dans les fichiers `.env`, `.zshrc`, `.gitignore` et consorts sans ouvrir toute la plomberie interne de `.git/`. * `--max-filesize=10M` ignore silencieusement les très gros fichiers (dumps de logs, assets générés) qui noieraient sinon vos résultats. * `--smart-case` est probablement le réglage le plus impactant : les recherches sont insensibles à la casse par défaut, mais dès que votre motif contient une majuscule, ripgrep bascule en correspondance exacte de casse — exactement le comportement que je veux 95 % du temps. ## Configuration du shell — \~/.zshrc ou \~/.bashrc {#shell-setup--zshrc-or-bashrc} Une fois le fichier de config en place, j'ajoute la variable d'environnement, le chargeur de fonctions et une poignée d'alias par type de fichier dans le fichier rc de mon shell. Le contenu est identique pour zsh et bash, sauf la boucle de chargement : **\~/.zshrc (ripgrep section)** ```bash export RIPGREP_CONFIG_PATH="$HOME/.ripgreprc" # If you don't already have such loop... # Load custom ZSH functions from ~/.zsh/fns/ for fn_file in ~/.zsh/fns/*.zsh; do source "${fn_file}" done # Type-specific shortcuts alias rgp='rg --type php' alias rgj='rg --type js' alias rgt='rg --type ts' alias rgm='rg --type md' alias rgy='rg --type yaml' alias rgb='rg --type sh' alias rgd='rg --glob "Dockerfile*" --glob "*.dockerfile"' ``` **\~/.bashrc (ripgrep section)** ```bash export RIPGREP_CONFIG_PATH="$HOME/.ripgreprc" # If you don't already have such loop... # Load custom bash functions from ~/.bash/fns/ for fn_file in ~/.bash/fns/*.sh; do [[ -r "${fn_file}" ]] && source "${fn_file}" done # Type-specific shortcuts alias rgp='rg --type php' alias rgj='rg --type js' alias rgt='rg --type ts' alias rgm='rg --type md' alias rgy='rg --type yaml' alias rgb='rg --type sh' alias rgd='rg --glob "Dockerfile*" --glob "*.dockerfile"' ``` Le test `-r` évite qu'un dossier `~/.bash/fns/` vide n'affiche une erreur à chaque démarrage du shell. La boucle `for fn_file` source automatiquement chaque fichier de fonction présent dans ce répertoire — ajouter une nouvelle fonction revient donc simplement à y déposer un nouveau fichier, sans avoir à retoucher votre fichier rc. Les alias par type (`rgp` pour PHP, `rgt` pour TypeScript, `rgm` pour Markdown, etc.) font gagner des frappes quand vous savez exactement dans quel type de fichier vous cherchez. J'utilise `rgp "getUser"` ou `rgm "## Installation"` des dizaines de fois par jour. ## Fonctions shell — \~/.zsh/fns/ ou \~/.bash/fns/ {#shell-functions--zshfns-or-bashfns} C'est ici que ça devient amusant. Voici trois fonctions que j'ai installées dans mon répertoire `~/.zsh/fns/`, chacune résolvant un vrai problème. ### rgf — recherche interactive avec fzf {#rgf--interactive-search-with-fzf} J'ai détaillé cette combinaison dans un article dédié : [FZF + ripgrep: Interactive Code Search with Live Preview](/blog/fzf-ripgrep). En résumé : envoyez `rg` dans `fzf` avec un panneau de prévisualisation `bat` et vous obtenez un chercheur flou interactif qui cherche dans le *contenu* des fichiers, prévisualise les correspondances avec coloration syntaxique et ouvre votre éditeur exactement à la ligne trouvée. Si vous n'avez pas encore lu cet article, le détour en vaut la peine. ### rgtodo — faire remonter toute la dette technique d'un coup {#rgtodo--surface-all-technical-debt-at-once} Chaque codebase en est truffé dans ses commentaires : `TODO`, `FIXME`, `HACK`, `NOTE`, `XXX`. Cette fonction les fait tous remonter à la surface : **rgtodo.sh** ```bash # rgtodo — surface all TODO/FIXME/HACK/NOTE comments in the project rgtodo() { local dir="${1:-.}" rg \ --color=always \ --line-number \ --smart-case \ '(TODO|FIXME|HACK|NOTE):?\s' \ "${dir}" \ | sort } ``` Lancez `rgtodo` à la racine de n'importe quel projet et vous obtenez une liste triée et colorisée de chaque marqueur de commentaire. Lancez `rgtodo src/` pour la restreindre à un dossier précis. Révélateur sur des codebases qui ont quelques années au compteur. ```bash $ rgtodo src/ src/Api/UserController.php:47: // TODO: replace with a proper permission check src/Api/UserController.php:112: // FIXME: this bypasses rate limiting on retry src/Services/MailService.php:23: // HACK: temporary workaround for #4521 ``` ### rgenv — traquer l'usage des variables d'environnement {#rgenv--track-down-environment-variable-usage} Dans les projets très orientés Docker, les variables d'environnement finissent éparpillées entre des fichiers `.env`, `compose.yml`, des bootstraps PHP et des configs Python. Cette fonction cherche dans tous ces fichiers d'un seul coup : **rgenv.sh** ```bash # rgenv — find all references to an environment variable across config and source files rgenv() { local var="${1}" if [[ -z "${var}" ]]; then echo "Usage: rgenv " return 1 fi rg \ --type-add 'config:*.{env,cfg,ini,conf}' \ --type config \ --type yaml \ --type sh \ --type php \ --type py \ "${var}" } ``` `rgenv DB_PASSWORD` montre instantanément chaque fichier qui référence cette variable — qu'il la définisse, la consomme ou la passe comme secret Docker. Inestimable pour déboguer les dérives de configuration dans les setups multi-containers complexes. ```bash $ rgenv DB_PASSWORD .env:3:DB_PASSWORD=supersecret compose.yml:18: - DB_PASSWORD=${DB_PASSWORD} src/Config/database.php:14: 'password' => env('DB_PASSWORD'), ``` ### Installer rgtodo et rgenv {#installing-rgtodo-and-rgenv} Les deux fonctions sont du code shell pur : les mêmes fichiers fonctionnent en zsh et en bash, seul le dossier change. La boucle de chargement est déjà dans votre fichier rc depuis la section précédente. **1. Créez les fichiers** dans `~/.zsh/fns/` : **Project: \~/.zsh/fns** **rgtodo.zsh** ```bash # rgtodo — surface all TODO/FIXME/HACK/NOTE comments in the project rgtodo() { local dir="${1:-.}" rg \ --color=always \ --line-number \ --smart-case \ '(TODO|FIXME|HACK|NOTE):?\s' \ "${dir}" \ | sort } ``` **rgenv.zsh** ```bash # rgenv — find all references to an environment variable across config and source files rgenv() { local var="${1}" if [[ -z "${var}" ]]; then echo "Usage: rgenv " return 1 fi rg \ --type-add 'config:*.{env,cfg,ini,conf}' \ --type config \ --type yaml \ --type sh \ --type php \ --type py \ "${var}" } ``` **2. Ouvrez un nouveau terminal** (ou lancez `exec zsh`). **1. Créez les fichiers** dans `~/.bash/fns/` : **Project: \~/.bash/fns** **rgtodo.sh** ```bash # rgtodo — surface all TODO/FIXME/HACK/NOTE comments in the project rgtodo() { local dir="${1:-.}" rg \ --color=always \ --line-number \ --smart-case \ '(TODO|FIXME|HACK|NOTE):?\s' \ "${dir}" \ | sort } ``` **rgenv.sh** ```bash # rgenv — find all references to an environment variable across config and source files rgenv() { local var="${1}" if [[ -z "${var}" ]]; then echo "Usage: rgenv " return 1 fi rg \ --type-add 'config:*.{env,cfg,ini,conf}' \ --type config \ --type yaml \ --type sh \ --type php \ --type py \ "${var}" } ``` **2. Ouvrez un nouveau terminal** (ou lancez `exec bash`). Vérifiez que le shell les connaît : `type rgtodo rgenv` doit répondre deux fois `… is a shell function`. ## Cas d'usage réels {#real-world-use-cases} Passons en revue les recherches que je lance le plus souvent dans mon workflow WSL2 quotidien. ### Trouver la définition d'une fonction dans un projet PHP {#finding-a-function-definition-across-a-php-project} ```bash rg "function getUser" --type php ``` Sans ripgrep : il faut ajouter `--include="*.php"`, `--exclude-dir=vendor`, `-rn`. Avec ripgrep : tapez-le, point. Le répertoire `vendor/` est déjà exclu parce qu'il est dans le `.gitignore`. ### Chercher une référence d'image dans tous les fichiers Docker Compose {#searching-all-docker-compose-files-for-an-image-reference} ```bash rgy "image:" ``` C'est notre alias `rgy` (`rg --type yaml`). Pratique pour auditer un repository à la recherche de références d'images obsolètes, ou pour trouver tous les services qui partagent une image de base. ### Lignes de contexte — comprendre le code autour d'une correspondance {#context-lines--understanding-code-around-a-match} Le flag `-C N` affiche N lignes de contexte autour de chaque correspondance — une fonctionnalité que j'utilise en permanence : ```bash $ rg "DB_HOST" -C 3 --type php src/Config/Database.php 15- 'driver' => 'mysql', 16: 'host' => env('DB_HOST', '127.0.0.1'), 17- 'database' => env('DB_DATABASE', 'forge'), 18- 'username' => env('DB_USERNAME', 'forge'), 19- 'password' => env('DB_PASSWORD', ''), ``` Vous pouvez aussi utiliser `-A N` (après) et `-B N` (avant) séparément, exactement comme avec grep. Un scénario plus courant : un collègue laisse des appels `console.log()` dans du JavaScript avant de pousser en production. Sans contexte, vous ne voyez que la ligne elle-même — vous savez qu'elle existe, mais pas à quel point c'est grave. Avec `-C 2` : ```bash $ rg "console\.log" -C 2 --type js src/auth/login.js 40- const authToken = generateToken(user) 41- const refreshToken = generateRefresh(user) 42: console.log("token:", authToken) 43- return { authToken, refreshToken } src/payment/checkout.js 85- for (const item of cartItems) { 86- const price = calculatePrice(item) 87: console.log("debug") 88- total += price ``` Là, vous pouvez faire la différence. La première correspondance logue un token JWT en clair — c'est un problème de sécurité, pas juste du bruit. La seconde se trouve dans une boucle `for` sur un panier : un seul passage en caisse inonderait la console de centaines de lignes. Le contexte transforme une liste de correspondances en information exploitable. ### Compter les occurrences par fichier {#counting-occurrences-per-file} ```bash rg -c "console.log" --type js ``` Ceci affiche le nombre de correspondances par fichier — parfait pour identifier quels fichiers sources contiennent encore des instructions de debug avant une release. ### Renommage en masse avec rg + sed {#bulk-rename-with-rg--sed} ripgrep est volontairement en lecture seule (pas de mode `-i` d'édition sur place), mais il se marie très bien avec `sed` pour du refactoring en masse : ```bash $ rg -l "oldFunctionName" --type php | xargs sed -i 's/oldFunctionName/newFunctionName/g' ``` `rg -l` ne liste que les noms de fichiers qui contiennent des correspondances. L'envoyer dans `xargs sed -i` réécrit exactement ces fichiers — ni plus, ni moins. Bien plus sûr qu'un `find | xargs sed` brut qui n'a aucune idée des fichiers réellement concernés. ### Chercher dans plusieurs types de fichiers à la fois {#searching-multiple-file-types-at-once} ```bash rg "API_KEY" -t php -t yaml -t sh ``` Plusieurs flags `-t` se combinent — ripgrep cherche dans tous les types correspondants en une seule passe, avec une sortie colorisée unifiée. ### Limiter la profondeur de recherche {#limiting-search-depth} ```bash rg "FROM" --glob "Dockerfile*" --max-depth 3 ``` `--max-depth` est idéal quand vous voulez rester près du répertoire courant sans plonger dans chaque sous-répertoire imbriqué. ## À retenir {#key-takeaways} **ripgrep : aide-mémoire** * **Installer** — `sudo apt install ripgrep` sur Ubuntu/WSL2 * **Configurer** — créez `~/.ripgreprc` et pointez dessus via `RIPGREP_CONFIG_PATH` dans `~/.zshrc` * **Flags de type** — `-t php` au lieu de `--include="*.php"` — plus court et plus malin * **Smart case** — `--smart-case` vous donne des recherches insensibles à la casse qui basculent en exact dès que vous utilisez une majuscule * **Conscient du gitignore** — `vendor/`, `node_modules/`, `.git/` sont exclus automatiquement ; aucun flag nécessaire * **Lignes de contexte** — `-C 3` affiche 3 lignes au-dessus et en dessous de chaque correspondance * **Mode liste de fichiers** — `rg -l` ne renvoie que les noms de fichiers ; envoyez-les dans `xargs` pour des opérations en masse * **Mode interactif** — `rgf "pattern"` combine ripgrep et fzf pour une recherche avec prévisualisation en direct ; configuration complète dans [FZF + ripgrep](/blog/fzf-ripgrep) ## Conclusion {#conclusion} Au début de cet article, je disais avoir tapé cette commande `grep -rn --include="*.php" ... | grep -v vendor` tellement de fois qu'elle était devenue un réflexe. Aujourd'hui, chaque fois que j'ouvre un terminal dans WSL2, je tape `rg` sans même y penser. L'exclusion de `vendor/` est automatique, la sortie est lisible sans aucun flag et la recherche est réellement plus rapide sur les gros codebases. Entre la configuration `~/.ripgreprc`, les alias de type dans `~/.zshrc`, des fonctions comme `rgtodo` et `rgenv` dans `~/.zsh/fns/`, et la recherche interactive `rgf` couverte dans [un article dédié](/blog/fzf-ripgrep), ripgrep a cessé d'être un outil que je choisis consciemment pour devenir la colonne vertébrale invisible de ma façon de naviguer dans le code au quotidien. Si vous en êtes encore à grep nu dans votre environnement WSL2, accordez-vous vingt minutes avec ripgrep — c'est tout ce qu'il faut. --- # yq — Le processeur YAML que vous savez déjà utiliser > yq est à YAML ce que jq est à JSON. Interrogez et modifiez vos fichiers Docker Compose, vos configs CI et n'importe quel document YAML depuis la ligne de commande — aucune installation nécessaire, à part Docker. --- ![yq — Le processeur YAML que vous savez déjà utiliser](/img/v2/linux-yq.webp) **TL;DR** `yq` est un processeur YAML en ligne de commande avec la même syntaxe que `jq`. Si vous utilisez déjà `jq` pour JSON, vous savez déjà utiliser `yq`. Lancez-le sans rien installer grâce à un alias Docker d'une ligne, puis interrogez et modifiez vos `compose.yaml`, vos pipelines CI et n'importe quel fichier YAML directement depuis le terminal. Vous avez un `compose.yaml` avec dix services. Vous voulez savoir quelle image utilise le service `db` sans ouvrir d'éditeur. Vous voulez incrémenter un numéro de version dans un fichier CI sans une one-liner sed que vous ne retiendrez jamais. Vous voulez extraire tous les noms de services dans une liste. Ce sont des problèmes pour `jq`. Mais vos fichiers sont en YAML, pas en JSON. Voici `yq`. ## Ce qu'est yq — et lequel {#what-yq-is--and-which-one} Il existe deux outils nommés `yq`. Cet article traite de [`yq` de Mike Farah](https://github.com/mikefarah/yq) (Go, image Docker `mikefarah/yq`), qui gère nativement YAML et offre une syntaxe compatible `jq`. L'autre (`kislyuk/yq`) encapsule `jq` après avoir converti le YAML en JSON — idée similaire, comportement différent pour les fonctionnalités propres à YAML comme les commentaires et les fichiers multi-documents. Sur Debian et Ubuntu, `apt install yq` installe cet autre outil (`kislyuk/yq` 3.x), pas celui utilisé ici. Si vous avez la version Go installée localement (`yq --version` renvoie quelque chose comme `yq (https://github.com/mikefarah/yq/) version v4.x`), tout est prêt. Sinon, l'approche Docker ci-dessous ne demande rien de plus. ## Le voir à l'œuvre avec Docker {#seeing-it-in-action-with-docker} Avant d'installer quoi que ce soit (ou même de mettre en place l'alias ci-dessous), essayez `yq` dans un container jetable — le `compose.yaml` exact de cet article s'y trouve déjà. > **Pourquoi Docker d'abord ?:** > > Le Dockerfile ci-dessous installe directement le binaire `yq` et dépose le `compose.yaml` exact dans le > répertoire de travail. Rien à créer, à coller ou à télécharger. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install the yq static binary in a plain Ubuntu image, so the demo opens a # bash shell with yq on the PATH. TARGETARCH is set by BuildKit (amd64 or # arm64, Apple Silicon included). Pin the version so this image keeps # building the same way; bump it during a /freshness pass. ARG TARGETARCH=amd64 RUN curl -fsSL -o /usr/local/bin/yq \ "https://github.com/mikefarah/yq/releases/download/v4.54.1/yq_linux_${TARGETARCH}" && \ chmod +x /usr/local/bin/yq # The exact compose.yaml used throughout this article RUN mkdir -p /root/demo RUN printf '%s\n' \ 'services:' \ ' web:' \ ' image: nginx:1.25' \ ' ports:' \ ' - "8080:80"' \ ' environment:' \ ' APP_ENV: production' \ ' LOG_LEVEL: info' \ ' volumes:' \ ' - ./html:/usr/share/nginx/html:ro' \ ' depends_on:' \ ' - db' \ '' \ ' db:' \ ' image: postgres:16' \ ' environment:' \ ' POSTGRES_USER: myapp' \ ' POSTGRES_PASSWORD: s3cr3t' \ ' POSTGRES_DB: myapp_db' \ ' volumes:' \ ' - db_data:/var/lib/postgresql/data' \ '' \ 'volumes:' \ ' db_data:' \ > /root/demo/compose.yaml WORKDIR /root/demo # Signal clearly that this shell runs inside a container RUN echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> /root/.bashrc CMD ["/bin/bash", "-i"] ``` Buildez et lancez : ```bash $ docker build -t yq-demo . [+] Building 8.7s (7/7) FINISHED ✔ exporting to image $ docker run --rm -it yq-demo 🐳 root ~/demo # yq '.services.web.image' compose.yaml nginx:1.25 ``` Même fichier, même query, même résultat — testez directement dessus le reste des sections « Lire des valeurs » et « Modifier sur place » ci-dessous, il n'y a plus rien à préparer. ## Installer — ou juste créer un alias {#install--or-just-alias} Un gestionnaire de packages installe la version Go sur toutes les plateformes : ```bash $ brew install yq # macOS and Linux (Homebrew) $ winget install MikeFarah.yq # Windows ``` Ne rien installer du tout est aussi une option : une petite fonction `yq` lance l'image officielle à la place, dans son propre fichier placé dans un dossier que votre shell charge au démarrage (l'organisation expliquée dans [mon workflow ZSH modulaire](/blog/modular-zsh-workflow)) : **yq.sh** ```bash # yq: run mikefarah/yq through Docker on the files of the current folder, no # local install. Works in zsh and bash: save it as ~/.zsh/fns/yq.zsh or # ~/.bash/fns/yq.sh (see https://www.avonture.be/blog/linux-yq). # # Runs with your own user ID, so files edited in place with -i keep their owner. # The image is pulled automatically the first time. yq() { if ! command -v docker >/dev/null 2>&1; then printf '\e[31mError: docker is not installed or not in your PATH.\e[0m\n' >&2 return 1 fi docker run --rm -i \ -u "$(id -u):$(id -g)" \ -v "${PWD}:/workdir" \ mikefarah/yq:4 "$@" } ``` Elle monte votre répertoire courant dans le container et exécute `yq` de façon transparente, avec votre propre ID utilisateur pour que les fichiers modifiés avec `-i` gardent leur propriétaire. Toutes les commandes `yq` ci-dessous fonctionnent avec elle telles quelles. Le code est identique pour zsh et bash ; seuls le dossier et le chargeur diffèrent. **1. Créez le fichier** dans `~/.zsh/fns/` : **Project: \~/.zsh/fns** **yq.zsh** ```bash # yq: run mikefarah/yq through Docker on the files of the current folder, no # local install. Works in zsh and bash: save it as ~/.zsh/fns/yq.zsh or # ~/.bash/fns/yq.sh (see https://www.avonture.be/blog/linux-yq). # # Runs with your own user ID, so files edited in place with -i keep their owner. # The image is pulled automatically the first time. yq() { if ! command -v docker >/dev/null 2>&1; then printf '\e[31mError: docker is not installed or not in your PATH.\e[0m\n' >&2 return 1 fi docker run --rm -i \ -u "$(id -u):$(id -g)" \ -v "${PWD}:/workdir" \ mikefarah/yq:4 "$@" } ``` **2. Chargez le dossier depuis `~/.zshrc`.** C'est déjà fait si vous avez suivi un de mes articles « Ollama au quotidien ». Sinon, ajoutez ceci à la fin de `~/.zshrc` : ```zsh title="~/.zshrc" for fn_file in ~/.zsh/fns/*.zsh; do source "$fn_file" done ``` **3. Ouvrez un nouveau terminal** (ou lancez `exec zsh`). **1. Créez le fichier** dans `~/.bash/fns/` : **Project: \~/.bash/fns** **yq.sh** ```bash # yq: run mikefarah/yq through Docker on the files of the current folder, no # local install. Works in zsh and bash: save it as ~/.zsh/fns/yq.zsh or # ~/.bash/fns/yq.sh (see https://www.avonture.be/blog/linux-yq). # # Runs with your own user ID, so files edited in place with -i keep their owner. # The image is pulled automatically the first time. yq() { if ! command -v docker >/dev/null 2>&1; then printf '\e[31mError: docker is not installed or not in your PATH.\e[0m\n' >&2 return 1 fi docker run --rm -i \ -u "$(id -u):$(id -g)" \ -v "${PWD}:/workdir" \ mikefarah/yq:4 "$@" } ``` **2. Chargez le dossier depuis `~/.bashrc`.** Ajoutez ceci à la fin de `~/.bashrc` (le test `-r` évite qu'un dossier vide ne génère une erreur) : ```bash title="~/.bashrc" for fn_file in ~/.bash/fns/*.sh; do [[ -r "$fn_file" ]] && source "$fn_file" done ``` **3. Ouvrez un nouveau terminal** (ou lancez `exec bash`). Vérifiez que le shell connaît la fonction : `type yq` doit répondre `yq is a shell function`. La [page des releases](https://github.com/mikefarah/yq/releases) propose aussi des binaires statiques pour Linux, macOS et Windows — sans aucune dépendance à l'exécution. ## Lire des valeurs {#reading-values} Travaillons avec un `compose.yaml` typique : ```yaml services: web: image: nginx:1.25 ports: - "8080:80" environment: APP_ENV: production LOG_LEVEL: info volumes: - ./html:/usr/share/nginx/html:ro depends_on: - db db: image: postgres:16 environment: POSTGRES_USER: myapp POSTGRES_PASSWORD: s3cr3t POSTGRES_DB: myapp_db volumes: - db_data:/var/lib/postgresql/data volumes: db_data: ``` Lire une valeur unique : ```bash yq '.services.web.image' compose.yaml ``` ```bash nginx:1.25 ``` Lire une valeur imbriquée : ```bash yq '.services.db.environment.POSTGRES_DB' compose.yaml ``` ```bash myapp_db ``` Lister tous les noms de services : ```bash yq '.services | keys' compose.yaml ``` ```bash - web - db ``` Récupérer toutes les images de tous les services : ```bash yq '.services.*.image' compose.yaml ``` ```bash nginx:1.25 postgres:16 ``` ## Filtrer et sélectionner {#filtering-and-selecting} Récupérer uniquement les services qui définissent une clé `ports` : ```bash yq '.services | with_entries(select(.value | has("ports")))' compose.yaml ``` ```yaml web: image: nginx:1.25 ports: - "8080:80" ... ``` Lister tous les noms de variables d'environnement du service `web` : ```bash yq '.services.web.environment | keys' compose.yaml ``` ```bash - APP_ENV - LOG_LEVEL ``` ## Modifier sur place {#editing-in-place} Mettre à jour la version de l'image `web`. Le flag `-i` modifie le fichier sur place, exactement comme [`sed -i`](/blog/update-env-files-cli) : ```bash yq -i '.services.web.image = "nginx:1.27"' compose.yaml ``` Ajouter une nouvelle variable d'environnement : ```bash yq -i '.services.web.environment.DEBUG = "false"' compose.yaml ``` Supprimer une clé : ```bash yq -i 'del(.services.db.environment.POSTGRES_PASSWORD)' compose.yaml ``` > **yq -i reformate le fichier:** > > `yq -i` conserve les commentaires et les guillemets, mais supprime les lignes vides — la ligne vide entre `web` et `db` disparaît après la première modification. Vérifiez le diff avant de committer un fichier formaté à la main. ## Lire depuis stdin et chaîner les commandes {#reading-from-stdin-and-piping} `yq` lit depuis stdin quand aucun fichier n'est fourni, ce qui facilite le chaînage avec d'autres commandes : ```bash cat compose.yaml | yq '.services | keys' ``` Combinez-le avec `curl` pour parser des API ou des configs YAML téléchargées à la volée : ```bash curl -s https://example.com/config.yaml | yq '.database.host' ``` ## Cas d'usage pratiques {#practical-use-cases} ### Extraire tous les mappings de ports {#extract-all-port-mappings} ```bash yq '.services.*.ports[]' compose.yaml ``` ```bash 8080:80 ``` ### Vérifier qu'une clé existe avant de l'utiliser {#check-if-a-key-exists-before-using-it} ```bash yq '.services | has("redis")' compose.yaml ``` ```bash false ``` ### Mettre à jour une version dans tout un fichier CI {#update-a-version-across-a-ci-file} Pour un `.gitlab-ci.yml` qui référence une version d'image à plusieurs endroits, `..` parcourt chaque nœud du fichier, quelle que soit sa profondeur : ```bash yq -i '(.. | select(. == "node:20")) = "node:22"' .gitlab-ci.yml ``` ### Merger deux fichiers YAML {#merge-two-yaml-files} ```bash yq '. *= load("overrides.yaml")' base.yaml ``` Cette commande fusionne en profondeur `overrides.yaml` dans `base.yaml` et affiche le résultat — les fichiers source restent inchangés, sauf si vous ajoutez `-i`. ### Convertir du YAML en JSON {#convert-yaml-to-json} Quand vous devez passer des données YAML à un outil qui n'accepte que du JSON : ```bash yq -o json '.services.web' compose.yaml ``` ```json { "image": "nginx:1.25", "ports": [ "8080:80" ], "environment": { "APP_ENV": "production", "LOG_LEVEL": "info" }, "volumes": [ "./html:/usr/share/nginx/html:ro" ], "depends_on": [ "db" ] } ``` Vous pouvez ensuite envoyer cette sortie directement à `jq` pour la traiter davantage. ## Le pont avec jq {#the-jq-crossover} Si vous utilisez déjà [`jq`](/blog/linux-jq) pour JSON, la syntaxe de `yq` vous semblera immédiatement familière. Les expressions `jq` du quotidien fonctionnent telles quelles : | Tâche | jq (JSON) | yq (YAML) | | ------------------- | ----------------------------- | ----------------------------- | | Lire une clé | `jq '.key'` | `yq '.key'` | | Filtrer un tableau | `jq '.[] \| select(.active)'` | `yq '.[] \| select(.active)'` | | Récupérer les clés | `jq 'keys'` | `yq 'keys'` | | Modifier sur place | — | `yq -i '.key = "value"'` | | Convertir le format | — | `yq -o json '.'` | Les principales différences : `yq` ajoute `-i` pour les modifications sur place et `-o` pour le format de sortie, et gère les fonctionnalités natives de YAML comme les ancres et les fichiers multi-documents (séparateurs `---`). ## Conclusion {#conclusion} `yq` comble le manque que tout utilisateur de `jq` finit par ressentir : le même outil, mais pour YAML. Avec un alias Docker, il n'y a rien à installer, et la syntaxe se transpose directement depuis tout ce que vous connaissez déjà. Si votre workflow implique des `compose.yaml`, GitLab CI, GitHub Actions ou des manifests Kubernetes, `yq` a sa place dans votre boîte à outils aux côtés de `jq` et de son équivalent XML, [`xmlstarlet`](/blog/linux-xmlstarlet). La prochaine fois que vous ouvrirez un éditeur de texte pour vérifier une version d'image ou modifier une clé de configuration, utilisez plutôt `yq`. ---