<!--
  canonical-url: https://www.avonture.be/fr/blog/ollama-ai-explain
  published:     2026-11-12
  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.
-->

# ai-explain : l'ELI5 des scripts et des erreurs, directement dans votre terminal

> Encore une fonction de la série Ollama au quotidien : envoyez n'importe quel script, message d'erreur ou stack trace à ai-explain et obtenez une explication en langage clair de ce qu'il fait ou de ce qui a planté — 100 % local, sans cloud.

---
![ai-explain : l'ELI5 des scripts et des erreurs, directement dans votre terminal](/img/v2/playing_with_ollama.webp)

**TL;DR**

`ai-explain` est une nouvelle fonction de la série Ollama au quotidien. Envoyez-lui un script, une sortie de log ou une stack trace — ou donnez-lui un chemin de fichier, ou collez un message d'erreur en argument — et vous obtenez une explication en langage clair : ce que fait le script, ce que signifie l'erreur et quoi corriger. Une fonction, trois modes d'entrée, zéro cloud.

Vous avez cloné un repository et vous y trouvez un script Bash exécuté par le pipeline CI. Trois cents lignes, aucun commentaire. Vous devez comprendre ce qu'il fait avant de le modifier. Vous pouvez le lire attentivement, chercher la signification des flags, suivre la logique. Ou vous pouvez l'envoyer à `ai-explain`.

Même histoire pour les stack traces. Une erreur React en production avec six frames de sortie minifiée. Un `Fatal error` PHP à la ligne 847 d'un fichier que vous n'avez jamais ouvert. Un build Docker qui échoue sur une étape RUN que vous n'avez pas écrite.

`ai-explain` est la version terminal du [composant de tooltip ELI5](/blog/docusaurus-eli5-snippet-tooltips) de ce blog — la même idée « explique-moi comme si j'avais cinq ans », mais dans votre shell plutôt que dans le navigateur.

## Expliquer un script que vous n'avez pas écrit {#explain-a-script-you-didnt-write}

**deploy.sh**

```bash
#!/usr/bin/env bash
set -euo pipefail

for tool in docker git curl; do
  if ! command -v "$tool" >/dev/null 2>&1; then
    echo "Missing prerequisite: $tool" >&2
    exit 1
  fi
done

git pull --rebase origin main

tag="myapp:$(git rev-parse --short HEAD)"
docker build -t "$tag" .

APP_IMAGE="$tag" docker compose up -d --no-deps app

for _ in $(seq 1 12); do
  if [[ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health)" == "200" ]]; then
    echo "Deploy successful: $tag"
    exit 0
  fi
  sleep 5
done

echo "Health check failed for $tag" >&2
exit 1
```

```bash
$ ai-explain deploy.sh

This script automates deploying an application using Docker and Docker Compose. It first checks that `docker`, `git`, and `curl` are installed, exiting if any are missing. It pulls the latest code from the `main` branch and builds a new Docker image tagged with the current git commit hash. Then it starts the app service in detached mode without restarting dependent services. After starting the service, it waits up to 60 seconds (12 attempts of 5 seconds each) checking if the app responds with HTTP 200 at `/health`. If successful, it prints a success message and exits. If the health check fails after all attempts, it reports the failure and exits with error code 1. The script modifies the local Docker environment by building a new image and starting containers, but doesn't change any files on disk except through Git operations. Risks include failing to start the app or timing out if the service takes longer than 60 seconds to become healthy. To fix potential issues, ensure all prerequisites are installed and the app's health endpoint is correctly configured.
```

Voilà toute l'interaction : un chemin de fichier en entrée, une explication en langage clair en sortie — les prérequis, le tag d'image basé sur le hash du commit, le redémarrage avec `--no-deps`, le health check de 60 secondes et ce qui se passe en cas de timeout.

## Pourquoi ça fonctionne {#why-it-works}

`ai-explain` gère trois modes d'entrée, vérifiés dans cet ordre : l'entrée via pipe (donc `cat file | ai-explain` l'emporte toujours, même si un nom de fichier est aussi passé), un fichier existant, ou du texte inline pour une question rapide — une seule fonction couvre « explique-moi ce fichier » et « explique-moi cette erreur » sans commandes séparées à retenir. Le prompt demande quinze lignes de texte maximum, jamais une copie de l'entrée, et transmet `uname -s` pour que les commandes suggérées correspondent à la plateforme sur laquelle vous êtes réellement.

## Installation {#installing-it}

Tout tourne sur votre machine : zsh, `curl` et `jq` (qui construit la requête envoyée à Ollama), et un serveur Ollama accessible — voir [installer Ollama](/blog/ollama-installation) si vous n'en avez pas encore.

**1. Créez les fichiers.** Chaque fonction de cette série vit dans son propre fichier dans `~/.zsh/fns/`. La commande ci-dessous crée ce dossier et les deux fichiers d'un coup — ou téléchargez le ZIP, ou créez chaque fichier à la main et collez-y son contenu. `_ollama.zsh` est la fondation partagée par toute la série, présentée dans [`ai-test`](/blog/ollama-test-generator) : si un article précédent vous l'a déjà donné, la copie ici est le même fichier.

**Project: \~/.zsh/fns**

**\_ollama.zsh**

````bash
# Foundation file for every "ai-*" function in this series. Named with a
# leading underscore: the ~/.zsh/fns/*.zsh loader sources files in
# alphabetical order, and "_" sorts before any letter — so this file is
# always loaded first, no matter how many ai-* functions get added later.
#
# Provides:
#   - _ollama_check       reachability guard for Ollama
#   - _ollama_query       shared HTTP client (curl + jq)
#   - _ai_strip_fences    removes the Markdown fences models add anyway
#   - _ai_bat             resolves "bat" vs Debian's "batcat"
#   - _ai_confirm         yes/no prompt
#   - _git_staged_diff    validated staged-diff fetcher (shared by pre-commit functions)
#   - AI_COMMANDS         registry each ai-* function adds itself to
#   - AI_PARAMS           registry of interactive parameter types (file/language/number/text/none)
#   - _ai_prompt_*        FZF/read helpers for interactive argument collection
#   - ai                  entry-point dispatcher / interactive menu

typeset -gA AI_COMMANDS
typeset -gA AI_PARAMS

# ---------------------------------------------------------------------------
# Ollama helpers
# ---------------------------------------------------------------------------

# _ollama_check — verify that the Ollama daemon is reachable.
# Fails loudly so callers can bail early rather than watching curl hang.
_ollama_check() {
  local host="${OLLAMA_HOST:-http://localhost:11434}"
  if ! curl --silent --max-time 2 "${host}/api/tags" >/dev/null 2>&1; then
    echo "ollama: cannot reach ${host} — is the Ollama container running?" >&2
    return 1
  fi
}

# _ollama_query <prompt> — send a prompt to the local model and print the response.
# Assumes Ollama is reachable; call _ollama_check first if you need an early guard.
_ollama_query() {
  local prompt="$1"
  local model="${OLLAMA_MODEL:-qwen3-coder:30b}"
  local host="${OLLAMA_HOST:-http://localhost:11434}"

  _ollama_check || return 1

  # The prompt is piped into jq rather than passed with "--arg": Linux caps a
  # single argv entry at 128 KB (MAX_ARG_STRLEN), and a large staged diff or a
  # long source file sails straight through that ceiling — jq then dies with
  # "argument list too long". "-R -s" slurps stdin as one raw string, which jq
  # escapes exactly like --arg would.
  print -r -- "$prompt" \
    | jq -Rs --arg model "$model" '{model: $model, prompt: ., stream: false}' \
    | curl --silent "${host}/api/generate" --data-binary @- \
    | jq -r '.response // empty'
}

# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------

# _ai_strip_fences <text> — return only the code, without the Markdown fences.
#
# Every prompt in this series ends with "no markdown fences" and models still
# wrap their answer in ```language … ``` often enough that the output can't be
# piped or pasted as-is. When at least one fence is present, only the fenced
# regions are kept (which also drops the "Here is your test suite:" preamble);
# when there is none, the text is returned untouched.
_ai_strip_fences() {
  local text="$1"

  if [[ "$text" != *'```'* ]]; then
    print -r -- "$text"
    return 0
  fi

  print -r -- "$text" | awk '
    /^[[:space:]]*```/ { inside = !inside; next }
    inside             { print }
  '
}

# _ai_bat — print the name of the "bat" binary installed on this machine, if any.
# Debian and Ubuntu ship the package as "batcat" because the "bat" name was
# already taken by bacula-console-qt, so both spellings have to be probed.
_ai_bat() {
  local candidate
  for candidate in bat batcat; do
    if command -v "$candidate" >/dev/null 2>&1; then
      print -- "$candidate"
      return 0
    fi
  done
  return 1
}

# _ai_relpath <path> — print <path> relative to the current folder when it sits
# below it. Purely cosmetic: it keeps prompts readable instead of asking
# "Save as /home/christophe/projects/…/tests/backup.bats?".
_ai_relpath() {
  local path="${1:A}"
  print -- "${path#${PWD}/}"
}

# _ai_confirm <question> — ask a yes/no question; return 0 only on an explicit yes.
_ai_confirm() {
  local answer
  print -n "${1} [y/N] "
  read -r answer
  [[ "$answer" == [yY]* ]]
}

# ---------------------------------------------------------------------------
# Git helpers
# ---------------------------------------------------------------------------

# _git_staged_diff <caller> — validate the git context and return the staged diff.
# Handles three early-exit cases (not a repo, nothing staged, oversized diff)
# so every pre-commit function gets them for free without repeating the guards.
#
# Past AI_DIFF_MAX_CHARS the full diff is *not* returned: a diff of several
# hundred kilobytes overflows the model's context window, and the answer that
# comes back is worse than the one you get from a summary. The fallback keeps
# the shape of the change — per-file stat, plus the file and hunk headers,
# which carry the enclosing function names git puts after each "@@" — and
# drops the line-by-line content.
_git_staged_diff() {
  local caller="${1:-ai}"
  local max="${AI_DIFF_MAX_CHARS:-12000}"

  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo "${caller}: not inside a git repository" >&2
    return 1
  fi

  local diff
  diff=$(git diff --staged)

  if [[ -z "$diff" ]]; then
    echo "${caller}: nothing staged — run 'git add' first" >&2
    return 1
  fi

  # "print -r" everywhere below: without it, print expands escape sequences and
  # a literal \n or \t inside the diff would come back mangled.
  if (( ${#diff} <= max )); then
    print -r -- "$diff"
    return 0
  fi

  echo "${caller}: staged diff is large (${#diff} chars) — sending a structural summary instead of the full diff. Commit in smaller chunks for a more precise answer." >&2

  local summary
  summary="$(git diff --staged --stat)

--- FILE AND HUNK HEADERS ONLY (full diff omitted, ${#diff} chars) ---
$(print -r -- "$diff" | grep -E '^(diff --git|new file|deleted file|rename (from|to)|@@)')"

  # Even the summary can exceed the ceiling on a very wide change.
  if (( ${#summary} > max )); then
    summary="${summary[1,$max]}
[…summary truncated…]"
  fi

  print -r -- "$summary"
}

# ---------------------------------------------------------------------------
# Interactive argument helpers (used by the ai() dispatcher)
# ---------------------------------------------------------------------------

# _ai_prompt_file [label] — open an fzf file picker; print the chosen path.
_ai_prompt_file() {
  local label="${1:-Select a file:}"
  local bat_bin preview='cat {}'
  bat_bin=$(_ai_bat) && preview="${bat_bin} --color=always {} 2>/dev/null || cat {}"

  if command -v fd >/dev/null 2>&1; then
    fd --type f | fzf --prompt="${label} " --height=50% --reverse --preview="$preview"
  else
    find . -type f | fzf --prompt="${label} " --height=50% --reverse --preview="$preview"
  fi
}

# _ai_prompt_language — open an fzf language picker; print the chosen language.
_ai_prompt_language() {
  printf '%s\n' English French Dutch German Spanish Italian Portuguese Japanese Chinese Korean \
    | fzf --prompt='Target language: ' --height=40% --reverse
}

# _ai_prompt_number [label] — read a positive integer; print it (or nothing on Enter, which
# lets the caller fall back to its own default). Returns 1 only on invalid input.
_ai_prompt_number() {
  local label="${1:-Number (Enter for default):}"
  print -n "${label} "
  local n
  read n
  if [[ -z "$n" ]]; then
    return 0
  fi
  if [[ ! "$n" =~ ^[1-9][0-9]*$ ]]; then
    echo "Expected a positive integer." >&2
    return 1
  fi
  print -- "$n"
}

# _ai_prompt_text [label] — read a free-form string; print it. Returns 1 if empty.
_ai_prompt_text() {
  local label="${1:-Input:}"
  print -n "${label} "
  local t
  read t
  if [[ -z "$t" ]]; then
    echo "No input given." >&2
    return 1
  fi
  print -- "$t"
}

# ---------------------------------------------------------------------------
# Entry-point dispatcher
# ---------------------------------------------------------------------------

# ai              → interactive fzf menu of every registered ai-* command
# ai <cmd> [args] → dispatch straight to ai-<cmd> [args]
#
# When opened via the fzf menu, each command's declared AI_PARAMS type triggers
# a secondary picker before the command runs:
#   file     → fzf file picker (fd + preview if available)
#   language → fzf language list
#   number   → read prompt, empty input uses the command's own default
#   text     → read prompt, required
#   none     → run the command directly (it collects its own input)
# Space-separated values declare multiple parameters in order, e.g. "file language".
ai() {
  local cmd="$1"

  # Direct dispatch: ai <cmd> [args]
  if [[ -n "$cmd" && "$cmd" != "help" && "$cmd" != "list" ]]; then
    if (( ${+AI_COMMANDS[$cmd]} )); then
      shift
      "ai-${cmd}" "$@"
      return
    fi
    echo "ai: unknown command '${cmd}'" >&2
    echo >&2
  fi

  # Interactive menu
  if ! command -v fzf >/dev/null 2>&1; then
    echo "Available ai commands:"
    for key in ${(ok)AI_COMMANDS}; do
      printf "  ai %-10s %s\n" "$key" "$AI_COMMANDS[$key]"
    done
    return
  fi

  local picked
  picked=$(
    for key in ${(ok)AI_COMMANDS}; do
      printf '%s\t%s\n' "$key" "$AI_COMMANDS[$key]"
    done \
      | fzf --delimiter='\t' --with-nth=2 --prompt='ai > ' --height=40% --reverse \
      | cut -f1
  )
  [[ -z "$picked" ]] && return

  # Collect arguments interactively based on AI_PARAMS declaration
  local -a args=()
  local param_type
  for param_type in ${(s: :)${AI_PARAMS[$picked]:-none}}; do
    case "$param_type" in
      file)
        local f
        f=$(_ai_prompt_file "File for ai-${picked}:") || return 1
        args+=("$f")
        ;;
      language)
        local l
        l=$(_ai_prompt_language) || return 1
        args+=("$l")
        ;;
      number)
        local n
        n=$(_ai_prompt_number) || return 1
        [[ -n "$n" ]] && args+=("$n")
        ;;
      text)
        local t
        t=$(_ai_prompt_text) || return 1
        args+=("$t")
        ;;
      none) ;;
    esac
  done

  "ai-${picked}" "${args[@]}"
}
````

**ai-explain.zsh**

```bash
# ai-explain [file|text] — explain a script, an error message or any piped
# output in plain English. Three input modes, checked in this order:
#   cat error.log | ai-explain            piped input always wins
#   ai-explain deploy.sh                  an existing file is read
#   ai-explain 'error: undefined symbol'  anything else is the text itself

AI_COMMANDS[explain]="ai-explain [file|text]  — explain a script, an error or piped output in plain English"
AI_PARAMS[explain]="file"

ai-explain() {
  local input

  if [[ ! -t 0 ]]; then
    input=$(cat)
  elif [[ -f "$1" ]]; then
    input=$(cat "$1")
  elif (( $# > 0 )); then
    input="$*"
  else
    echo 'Usage: ai-explain <file>   or   command 2>&1 | ai-explain   or   ai-explain "error message"' >&2
    return 1
  fi

  if [[ -z "$input" ]]; then
    echo "ai-explain: empty input — nothing to explain" >&2
    return 1
  fi

  local prompt="You are a helpful assistant that explains technical content in plain English.

Look at the content below and:
- If it is a script (bash, zsh, python, php, etc.): explain what it does step by step. Mention what it produces, what it modifies, and any side effects or risks.
- If it is an error message, stack trace, or log output: explain what went wrong, why it happened, and what to do to fix it.
- If you are not sure: describe what you see and give your best interpretation.

Keep the explanation concrete and practical. No hype, no filler. If there is a fix, give the exact command or change.
Never copy the input back: name the command or the line you are talking about instead. Plain text, no headings, no emoji, at most 15 lines. The user is on $(uname -s), so only suggest commands for that platform.

---
${input}"

  _ollama_query "$prompt"
}
```

**2. Chargez le dossier depuis `~/.zshrc`.** Déjà fait si vous avez suivi un article précédent de cette série. Sinon, ajoutez ces lignes à la fin de `~/.zshrc` — elles sourcent chaque fichier `.zsh` du dossier, donc la prochaine fonction que vous ajouterez ne demandera aucune modification ici :

```zsh title="~/.zshrc"
for fn_file in ~/.zsh/fns/*.zsh; do
  source "$fn_file"
done
```

**3. Ouvrez un nouveau terminal** (ou lancez `exec zsh` dans celui en cours), et vérifiez que le shell connaît la nouvelle fonction :

```zsh
type ai-explain
```

`ai-explain is a shell function from …/ai-explain.zsh` signifie que vous êtes prêt. `ai-explain not found` signifie qu'il manque l'étape 2.

## Autres démos {#more-demos}

### Expliquer une stack trace {#explain-a-stack-trace}

**example-error.txt**

```txt
Fatal error: Uncaught TypeError: Cannot read properties of undefined (reading 'map')
    at ProductList (/app/src/components/ProductList.jsx:42:32)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom.development.js:14985:18)
    at mountIndeterminateComponent (/app/node_modules/react-dom/cjs/react-dom.development.js:17811:13)
    at beginWork (/app/node_modules/react-dom/cjs/react-dom.development.js:19049:16)
    at HTMLUnknownElement.callCallback (/app/node_modules/react-dom/cjs/react-dom.development.js:3945:14)
    at Object.invokeGuardedCallbackDev (/app/node_modules/react-dom/cjs/react-dom.development.js:3994:16)
The above error occurred in the <ProductList> component.
Consider adding an error boundary to your tree to customize error handling behavior.
```

````bash
$ ai-explain example-error.txt

The error happens because the code tries to call `.map()` on a variable that is `undefined` in the ProductList component. This occurs at line 42 of ProductList.jsx where it expects an array but gets nothing.

To fix this, add a check to ensure the data exists before mapping:
```jsx
{products && products.map(product => (
  <div key={product.id}>{product.name}</div>
))}
```

If `products` is undefined, the component won't crash and will render nothing instead of throwing an error.
````

### Envoyer la sortie de n'importe quelle commande {#pipe-any-command-output}

Le pattern le plus utile : lancez une commande qui échoue et envoyez sa sortie directement :

```bash
docker build . 2>&1 | ai-explain
```

```bash
npm install 2>&1 | ai-explain
```

```bash
./run-tests.sh 2>&1 | tail -50 | ai-explain
```

Le `2>&1` redirige stderr vers stdout pour que les deux streams arrivent dans le pipe. Sans lui, les messages d'erreur de la plupart des outils CLI n'atteignent jamais `ai-explain`.

Pour les commandes qui produisent beaucoup de sortie, passez d'abord par `tail` pour ne garder que la fin pertinente (là où les erreurs atterrissent généralement).

### Utilisation depuis le dispatcher `ai` {#use-from-the-ai-dispatcher}

Comme toutes les fonctions de cette série, `ai-explain` s'enregistre dans le registre `AI_COMMANDS`. Le dispatcher `ai` (issu de `_ollama.zsh`) liste chaque fonction enregistrée — ici avec les fonctions publiées de cette série installées :

```bash
$ ai

ai >
  ai-blog-search — ask your blog a question (RAG over every published article)
  ai-commit  — draft a Conventional Commits message from staged changes
  ai-data <file.json|file.csv>  — suggest jq/awk commands tailored to this file
  ai-diagram <description>|<file>  — turn a description or a config file into a Mermaid diagram
  ai-explain [file|text]  — explain a script, an error or piped output in plain English
  ai-review  — code review of staged changes (SOLID, magic constants, naming)
  ai-secrets  — scan staged changes for hardcoded credentials
  ai-summarize <file> [points]  — summarize a document locally (default: 5 points)
  ai-test <file.sh|.php|.py>  — generate or complete a unit test suite
  ai-translate <file> [language]  — translate a document locally (default: French)
```

Sélectionnez `ai-explain` et le dispatcher ouvre d'abord son sélecteur de fichiers fzf : `AI_PARAMS[explain]="file"` lui indique de demander un fichier plutôt qu'une ligne de texte.

## Sous le capot (passez votre chemin si vous voulez juste l'utiliser) {#under-the-hood-skip-this-if-you-just-want-to-use-it}

### Quand ai-explain est le plus utile {#when-ai-explain-is-most-useful}

* **Scripts hérités** : comprendre ce que fait un script CI/CD avant de le modifier
* **Erreurs inconnues** : une stack trace d'un langage ou d'un framework que vous maîtrisez mal
* **Sorties de log cryptiques** : avertissements du daemon Docker, entrées du journal systemd, messages du kernel
* **Archéologie de one-liners** : cette commande `awk '{ split($0,a,":"); ... }'` trouvée dans un Makefile
* **Vérification rapide** : est-ce que ce script fait bien ce que son nom suggère ?

C'est aussi utile comme outil d'apprentissage : envoyez-lui un script que vous avez écrit vous-même et voyez si l'explication correspond à votre intention. Si ce n'est pas le cas, soit l'explication est fausse (ça arrive), soit le script contient un bug subtil.

## Conclusion {#conclusion}

`ai-explain` rejoint la série « Ollama daily use ».

La série a commencé avec [`ai-test`](/blog/ollama-test-generator) (la fondation : `_ollama.zsh`, le dispatcher, le registre `AI_COMMANDS`) et s'est enrichie de fonctions une par une — chacune résolvant un problème précis de workflow dans le terminal avec un LLM local :

* **Workflow du développeur** : [`ai-review`, `ai-secrets`, `ai-commit`](/blog/ollama-git-precommit), `ai-standup`
* **Opérations** : `ai-ci`, `ai-fix`
* **Assistance shell** : `ai-ask`, [`ai-data`](/blog/ollama-ai-data), [`ai-diagram`](/blog/ollama-ai-diagram)
* **Travail documentaire** : [`ai-translate`, `ai-summarize`](/blog/ollama-ai-docs), `ai-diff`
* **Votre propre documentation** : [`ai-blog-search`](/blog/anythingllm-chat-with-your-docs)
* **Explication** : `ai-explain`

Toutes partagent la même fondation `_ollama_query`. Ensemble, elles couvrent les moments d'une journée de développement où vous iriez normalement chercher dans un navigateur ou une IA dans le cloud — mais où un modèle local est plus rapide d'accès et garde tout sur votre machine.

Les fonctions sans lien sont encore à venir. Lancez `ai` pour voir ce que votre terminal sait déjà faire.
