<!--
  canonical-url: https://www.avonture.be/fr/blog/fzf-ripgrep
  published:     2026-06-08
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# 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] <pattern> [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] <pattern> [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
```
