<!--
  canonical-url: https://www.avonture.be/fr/blog/ssh-with-fuzzy-finder
  published:     2026-04-27
  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.
-->

# 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' \
    '#' \
    '# <yellow>Backend:</yellow> PHP 8.5' \
    '# <green>Note:</green>    Part of TremendousApp.' \
    '#' \
    '# <bg-red>  CRITICAL  </bg-red> - This is a production server!' \
    '# <bg-yellow>    INFO    </bg-yellow> - Database is <green>PostgreSQL</green>' \
    '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' \
    '#' \
    '# <yellow>Backend:</yellow> PHP 8.3' \
    '# <green>Note:</green> This is the TESTS environment' \
    '#' \
    '# <bg-yellow>    INFO    </bg-yellow> - Database is <green>PostgreSQL</green>' \
    '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 <red>, <bg-red>, <bold>, 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):' \
    '#   <red>text</red>, <green>text</green>, <yellow>text</yellow>, <blue>text</blue>' \
    '#   <bg-red>highlight</bg-red>, <bg-yellow>warning</bg-yellow>, <bold>important</bold>' \
    '# ------------------------------------------------------------------------------' \
    '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/<red>/\e[31m/g;          s/<\/red>/\e[0m/g;' \
    '            s/<green>/\e[32m/g;        s/<\/green>/\e[0m/g;' \
    '            s/<yellow>/\e[33m/g;       s/<\/yellow>/\e[0m/g;' \
    '            s/<bg-red>/\e[41;37m/g;    s/<\/bg-red>/\e[0m/g;' \
    '            s/<bg-yellow>/\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g;' \
    '            s/<bold>/\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
#
# <yellow>Backend:</yellow> PHP 8.5
# <green>Note:</green>    Part of TremendousApp.
#
# <bg-red>  CRITICAL  </bg-red> - This is a production server!
# <bg-yellow>    INFO    </bg-yellow> - Database is <green>PostgreSQL</green>
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
#
# <yellow>Backend:</yellow> PHP 8.3
# <green>Note:</green> This is the TESTS environment
#
# <bg-yellow>    INFO    </bg-yellow> - Database is <green>PostgreSQL</green>
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 <red>, <bg-red>, <bold>, 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):
#   <red>text</red>, <green>text</green>, <yellow>text</yellow>, <blue>text</blue>
#   <bg-red>highlight</bg-red>, <bg-yellow>warning</bg-yellow>, <bold>important</bold>
# ------------------------------------------------------------------------------
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/<red>/\e[31m/g;          s/<\/red>/\e[0m/g;
            s/<green>/\e[32m/g;        s/<\/green>/\e[0m/g;
            s/<yellow>/\e[33m/g;       s/<\/yellow>/\e[0m/g;
            s/<bg-red>/\e[41;37m/g;    s/<\/bg-red>/\e[0m/g;
            s/<bg-yellow>/\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g;
            s/<bold>/\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 <red>, <bg-red>, <bold>, 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):
#   <red>text</red>, <green>text</green>, <yellow>text</yellow>, <blue>text</blue>
#   <bg-red>highlight</bg-red>, <bg-yellow>warning</bg-yellow>, <bold>important</bold>
# ------------------------------------------------------------------------------
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/<red>/\e[31m/g;          s/<\/red>/\e[0m/g;
            s/<green>/\e[32m/g;        s/<\/green>/\e[0m/g;
            s/<yellow>/\e[33m/g;       s/<\/yellow>/\e[0m/g;
            s/<bg-red>/\e[41;37m/g;    s/<\/bg-red>/\e[0m/g;
            s/<bg-yellow>/\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g;
            s/<bold>/\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 <red>, <bg-red>, <bold>, 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):
#   <red>text</red>, <green>text</green>, <yellow>text</yellow>, <blue>text</blue>
#   <bg-red>highlight</bg-red>, <bg-yellow>warning</bg-yellow>, <bold>important</bold>
# ------------------------------------------------------------------------------
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/<red>/\e[31m/g;          s/<\/red>/\e[0m/g;
            s/<green>/\e[32m/g;        s/<\/green>/\e[0m/g;
            s/<yellow>/\e[33m/g;       s/<\/yellow>/\e[0m/g;
            s/<bg-red>/\e[41;37m/g;    s/<\/bg-red>/\e[0m/g;
            s/<bg-yellow>/\e[43;30m/g; s/<\/bg-yellow>/\e[0m/g;
            s/<bold>/\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 <red>, <bg-red>, <bold>, 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):
#   <red>text</red>, <green>text</green>, <yellow>text</yellow>, <blue>text</blue>
#   <bg-red>highlight</bg-red>, <bg-yellow>warning</bg-yellow>, <bold>important</bold>
# ------------------------------------------------------------------------------
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/<red>/\e[31m/g;           s/<\/red>/\e[0m/g;
            s/<green>/\e[32m/g;         s/<\/green>/\e[0m/g;
            s/<yellow>/\e[33m/g;        s/<\/yellow>/\e[0m/g;
            s/<bg-red>/\e[41;37m/g;     s/<\/bg-red>/\e[0m/g;
            s/<bg-yellow>/\e[43;30m/g;  s/<\/bg-yellow>/\e[0m/g;
            s/<bold>/\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 !
