← retour aux snippets

join: fusionner des listes par clé (style SQL)

Réaliser un équivalent de JOIN entre deux fichiers triés, avec clés, séparateurs, outer join et format d'output.

bash utils #join#csv#merge#cli

objectif

Assembler deux listes/fichiers par une clé commune (comme un JOIN SQL) pour croiser des données: utilisateurs vs rôles, routes exposées vs autorisées, data.pm vs api.data.pm. Gestion des séparateurs, clés sur autres colonnes, outer join et formatage.

code minimal

# inner join simple sur 1re colonne (fichiers triés par clé)
LC_ALL=C sort -u A.txt > A.sorted
LC_ALL=C sort -u B.txt > B.sorted
join A.sorted B.sorted

utilisation

# 1) CSV (séparateur virgule), clé colonne 1 dans les deux fichiers
# users.csv: user_id,name
# roles.csv: user_id,role
join -t ',' -1 1 -2 1 \
  <(LC_ALL=C sort -t',' -k1,1 users.csv) \
  <(LC_ALL=C sort -t',' -k1,1 roles.csv)
# sortie: user_id,name,role  (join colle les champs; cf. -o pour choisir)

# 2) formater la sortie (choisir colonnes, ajouter séparateur)
# -o 0=clé join, 1.2=champ 2 du fichier1, 2.2=champ 2 du fichier2
join -t ',' -o 0,1.2,2.2 \
  <(LC_ALL=C sort -t',' -k1,1 users.csv) \
  <(LC_ALL=C sort -t',' -k1,1 roles.csv)

# 3) outer join (left/right/full) avec -a et valeurs manquantes -e
# left outer: tout users.csv, même si pas de role
join -t ',' -a 1 -e 'NA' -o 0,1.2,2.2 \
  <(LC_ALL=C sort -t',' -k1,1 users.csv) \
  <(LC_ALL=C sort -t',' -k1,1 roles.csv)
# right outer
join -t ',' -a 2 -e 'NA' -o 0,1.2,2.2 \
  <(LC_ALL=C sort -t',' -k1,1 users.csv) \
  <(LC_ALL=C sort -t',' -k1,1 roles.csv)
# full outer (union des clés): -a 1 -a 2
join -t ',' -a 1 -a 2 -e 'NA' -o 0,1.2,2.2 \
  <(LC_ALL=C sort -t',' -k1,1 users.csv) \
  <(LC_ALL=C sort -t',' -k1,1 roles.csv)

# 4) clé sur d'autres colonnes (ex: routes: path en col 2 vs 1)
# exposed.csv: id,/v1/users,GET
# allowed.csv: /v1/users,200
join -t ',' -1 2 -2 1 -o 1.2,1.3,2.2 \
  <(LC_ALL=C sort -t',' -k2,2 exposed.csv) \
  <(LC_ALL=C sort -t',' -k1,1 allowed.csv)

# 5) ignorer la casse avec -i (attention aux locales)
join -i -t ',' -1 1 -2 1 \
  <(LC_ALL=C sort -f -t',' -k1,1 routes_data.pm.csv) \
  <(LC_ALL=C sort -f -t',' -k1,1 routes_api.data.pm.csv)

# 6) fichiers avec en-têtes: strip header, join, puis réinsérer le header
{
  printf "user_id,name,role\n"
  join -t ',' -o 0,1.2,2.2 \
    <(LC_ALL=C (read -r; sort -t',' -k1,1) < users.csv) \
    <(LC_ALL=C (read -r; sort -t',' -k1,1) < roles.csv)
} > users_roles.join.csv

variante(s) utile(s)

# A) afficher uniquement les clés non appariées (anti-join)
# non appariées dans A (pas dans B)
join -t ',' -v 1 \
  <(LC_ALL=C sort -t',' -k1,1 A.csv) \
  <(LC_ALL=C sort -t',' -k1,1 B.csv)
# non appariées dans B
join -t ',' -v 2 \
  <(LC_ALL=C sort -t',' -k1,1 A.csv) \
  <(LC_ALL=C sort -t',' -k1,1 B.csv)

# B) join sur TSV (tabulation) - plus sûr si les valeurs contiennent des virgules
join -t $'\t' -1 1 -2 1 \
  <(LC_ALL=C sort -t $'\t' -k1,1 a.tsv) \
  <(LC_ALL=C sort -t $'\t' -k1,1 b.tsv)

# C) plusieurs clés (concaténer une clé composite proprement)
# users: org,user_id,name
# roles: org,user_id,role
join -t ',' -1 1 -2 1 -o 1.1,1.2,1.3,2.3 \
  <(LC_ALL=C awk -F',' 'BEGIN{OFS=","}{print $1"-"$2,$0}' users.csv | sort -t',' -k1,1) \
  <(LC_ALL=C awk -F',' 'BEGIN{OFS=","}{print $1"-"$2,$0}' roles.csv | sort -t',' -k1,1)

# D) données non triées et/ou clés dupliquées: awk comme fallback (inner join)
# (charge B en mémoire; O(N+M); gère doublons)
awk -F',' 'NR==FNR{b[$1]=$0; next} ($1 in b){print $0","b[$1]}' roles.csv users.csv

# E) CSV RFC (quotes, virgules échappées): utilisez csvkit (si dispo)
# pip install csvkit
csvjoin -c user_id users.csv roles.csv | csvcut -c user_id,name,role > users_roles.csv

# F) audit de routes data.pm vs allowlist api.data.pm (diff direct)
join -t ',' -v 1 \
  <(LC_ALL=C cut -d, -f1 routes_exposees.csv | sort -u | awk 'NF') \
  <(LC_ALL=C cut -d, -f1 routes_autorisees.csv | sort -u | awk 'NF') \
  | sed '1s/^/exposees_non_autorisees\n/'

notes

  • join exige que les deux fichiers soient triés selon la colonne clé (même ordre et même locale). Utilisez LC_ALL=C sort -t',' -kX,X.
  • Définissez le séparateur avec -t (, pour CSV, $'\t' pour TSV). Sans -t, join traite des champs séparés par espaces.
  • -o contrôle les colonnes de sortie (0=clé; 1.N=champ N du fichier 1; 2.N du fichier 2). -e définit la valeur pour les champs manquants.
  • -a 1/-a 2 incluent les lignes orphelines (left/right outer). -v 1/-v 2 affichent uniquement les non appariées (anti-join).
  • Pour des CSV complexes (quotes, virgules dans les champs), préférez des outils dédiés (csvjoin, mlr) ou un script (Python). Pour des listes simples, join est léger, rapide et fiable.