##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## fonctions pour awk
##@cooked nocomments
##@require base
uprovide awk
urequire base

__AWKDEF_HELP="\
Les variables données en arguments sont définies dans une section BEGIN{}. Si
une valeur ne ressemble pas à une définition de variable, l'analyse des
variables s'arrête et le reste des arguments est inséré tel quel.

Normalement, les variables définies sont scalaires, avec une syntaxe de la forme
name:str=value pour une chaine, name:int=value pour une valeur entière, ou
name=value pour déterminer automatiquement le type: entier si la valeur ne
contient que des chiffres, chaine sinon.

Il est aussi possible d'utiliser la syntaxe awk_array[@]=bash_array pour
initialiser le tableau awk_array, qui contiendra toute les valeurs du tableau
nommé bash_array, avec les indices de 1 à N, N étant le nombre d'éléments du
tableau bash_array. La variable awk_array_count est aussi initialisée, et
contient le nombre d'éléments du tableau.
La syntaxe simplifiée array[@] est équivalente à array[@]=array
Il existe une autre syntaxe 'awk_array[@]=<' qui permet de spécifier les valeurs
du tableau, une par ligne, e.g:
    $'values[@]=<\nvalue1\nvalue2'
pour un tableau values qui contiendra deux valeurs: value1 et value2

Les fonctions suivantes sont définies:

num(s)
    si s ne contient que des chiffres, retourner la valeur numérique associée,
    sinon retournée la valeur inchangée

ord(s)
    retourner le code ASCII du premier caractère de la chaine s. seuls les codes
    de 32 à 127 sont supportés

hex(i)
    retourner la représentation hexadécimale du nombre i

qhtml(s)
    remplacer respectivement dans la chaine s les valeurs &, \", > et < par
    &amp;, &quot;, &gt; et &lt;
    L'alias quote_html(s) existe pour compatibilité

unquote_html(s)
    faire le contraire de qhtml(s)

qval(s)
    quoter une valeur pour le shell. la valeur est entourée de quotes, e.g:
        qval(\"here, \\\"there\\\" and 'everywhere'.\")
        --> 'here, \"there\" and '\\''everywhere'\\''.'
    L'alias quote_value(s) existe pour compatibilité

sqval(s)
    comme qval() mais ajouter un espace avant la valeur quotée. ceci permet de
    construire facilement une ligne de commande, e.g.:
        print \"mycmd\" sqval(arg1) sqval(arg2)
    L'alias qsval(s) existe pour compatibilité

qvals()
    quoter les valeurs \$1..\$NF pour les passer comme argument sur la ligne de
    commande avec eval. e.g.:
        print \"mycmd \" qvals()
    La ligne qui est affichée pourra être évaluée avec eval dans le shell.
    L'alias quoted_values(s) existe pour compatibilité

sqvals(s)
    comme qvals() mais ajouter un espace avant la valeur quotée. ceci permet de
    construire facilement une ligne de commande, e.g.:
        print \"mycmd\" sqvals()
    L'alias qsvals(s) existe pour compatibilité

qarr(vs[, prefix])
    quoter les valeurs du tableau vs pour le shell, e.g:
        print \"values=(\" qarr(values) \")\"
    si prefix est spécifié, il est affiché suivi d'un espace, suivi des valeurs
    du tableau, ce qui permet de construire une ligne de commande, e.g.:
        print qarr(values, \"mycmd\")

qsubrepl(s)
    quoter une valeur pour l'argument r des fonctions sub() et gsub(). Les
    caractères suivants sont mis en échappement: \\ &
    L'alias quote_subrepl(s) existe pour compatibilité

qgrep(s)
    quoter une valeur pour un pattern *simple* de grep. Les caractères suivants
    sont mis en échappement: \\ . [ ^ \$ *
    L'alias quote_grep(s) existe pour compatibilité

qegrep(s)
    quoter une valeur pour un pattern *étendu* de grep. Les caractères suivants
    sont mis en échappement: \\ . [ ^ \$ ? + * ( ) | {
    L'alias quote_egrep(s) existe pour compatibilité

qsql(s, [suffix])
    quoter une valeur pour un script sql. la valeur est entourée de quotes, e.g:
        qsql(\"hello'there\")
        --> 'hello''there'
    L'alias quote_sql(s) existe pour compatibilité
    Si suffix est spécifié, le rajouter après la valeur précédé d'un espace, e.g:
        qsql(\"value\", \"field\")
        --> 'value' field

cqsql(s, [suffix])
    comme qsql() mais ajouter une virgule avant la valeur quotée. ceci permet de
    construire facilement une requête SQL, e.g:
        print \"insert into t(a, b, c) values (\" qsql(a) cqsql(b) cqsql(c) \");\"
        --> insert into t(a, b, c) values ('a','b','c');
    Si suffix est spécifié, le rajouter après la valeur précédé d'un espace, e.g:
        cqsql(\"value\", \"field\")
        --> ,'value' field

unquote_mysqlcsv(s)
    Analyser une valeur exportée de MySQL avec mysqlcsv. Les transformations
    suivantes sont effectuées:
        \\n   --> <newline>
        \\t   --> <tab>
        \\0   --> <caractère NUL>
        \\\\  --> \\

sval(s)
    retourner la valeur s précédée d'un espace si elle est non vide, e.g:
        sval(\"\") --> \"\"
        sval(\"any\") --> \" any\"

cval(s, [suffix])
    retourner la valeur s précédée d'une virgule si elle est non vide, e.g:
        sval(\"\") --> \"\"
        sval(\"any\") --> \",any\"
    Si suffix est spécifié, le rajouter après la valeur précédé d'un espace, e.g:
        cval(\"\", \"field\") --> \"\"
        cval(\"value\", \"field\") --> \",value field\"

mkindices(values, indices)
    créer le tableau indices qui contient les indices du tableau values, de 1 à N,
    et retourner la valeur N. Il faudra utiliser les valeurs de cette manière:
        count = mkindices(values, indices)
        for (i = 1; i <= count; i++) {
            value = values[indices[i]]
            ...
        }
    cette fonction nécessite gnuawk

array_new(dest)
    créer un nouveau tableau vide dest

array_newsize(dest, size)
    créer un nouveau tableau de taille size, rempli de chaines vides

array_len(src)
    calculer la taille d'un tableau. length(array) a un bug sur GNUawk 3.1.5.
    cette fonction est plus lente, mais fonctionne sur toutes les versions de
    awk et GNUawk

array_copy(dest, src)
    faire une copie d'un tableau. Cette fonction nécessite gnuawk, puisqu'elle
    utilise mkindices().

array_getlastindex(src)
    Retourner l'index du dernier élément du tableau src

array_add(dest, value)
    Ajouter un élément dans dest, avec l'index array_getlastindex(dest)+1

array_deli(dest, i)
    Supprimer l'élément d'index i dans le tableau dest.
    Les index des éléments après i sont corrigés en conséquence.
    Cette fonction assume que les indices du tableau commencent à 1 et n'ont pas
    de \"trous\". Si i==0, cette fonction est un NOP.

array_del(dest, value[, ignoreCase])
    Supprimer *tous* les éléments du tableau dest dont la valeur est value. Les
    indexes des valeurs sont trouvées avec key_index(), puis les valeurs sont
    supprimées avec array_deli()
    ignoreCase permet de spécifier que la recherche de la valeur se fait en
    ignorant la casse.

array_extend(dest, src)
    Ajouter les éléments de src dans dest, en commençant avec l'index
    array_getlastindex(dest)+1

array_fill(dest)
    remplir le tableau avec \$1..\$NF

array_getline(src)
    avec le tableau array contenant N éléments, initialise \$1..\$N avec les valeurs
    du tableau

array_appendline(src)
    avec le tableau array contenant N éléments, initialise \$(NF+1)..\$(NF+N) avec
    les valeurs du tableau

in_array(value, values[, ignoreCase])
    tester si le tableau values contient la valeur value, en ne tenant
    éventuellement pas compte de la casse.

key_index(value, values[, ignoreCase])
    trouver l'index de value dans le tableau values, en ne tenant éventuellement
    pas compte de la casse. Retourner 0 si la valeur n'a pas été trouvée

array2s(values, prefix, sep, suffix, noindices)
    convertir un tableau en chaine pour affichage. attention! les valeurs sont
    affichés dans un ordre arbitraire. noindices, s'il vaut 1, supprime
    l'affichage des indices du tableau. prefix (qui vaut par défaut \"[\") est
    ajouté avant la chaine, suffix (qui vaut par défaut \"]\") après, et sep (qui
    vaut par défaut \",\") est utilisé pour séparer chaque valeur.

array2so(values, prefix, sep, suffix, noindices)
    convertir un tableau en chaine pour affichage. Les valeurs sont affichés dans
    l'ordre du tableau. Cette fonction nécessite gnuawk, puisqu'elle utilise
    mkindices(). noindices, s'il vaut 1, supprime l'affichage des indices du
    tableau. prefix est ajouté avant la chaine, suffix après, et sep (qui vaut par
    défaut \",\") est utilisé pour séparer chaque valeur.

array_join(values, sep, prefix, suffix)
    convertir un tableau en chaine pour affichage. Les valeurs sont affichés dans
    l'ordre du tableau. Cette fonction nécessite gnuawk, puisqu'elle utilise
    mkindices().
    Il n'y a pas de valeur par défaut pour sep, prefix et suffix.

printto(s, output)
    en fonction de la valeur de output, afficher la chaine s sur la destination
    spécifiée.
    output est de la forme...            faire....
        \"\"                               print s
        \"dest\"                           print s >\"dest\"
        \">dest\"                          print s >\"dest\"
        \">>dest\"                         print s >>\"dest\"
    XXX les formes suivantes sont désactivées pour le moment:
        \"|dest\"                          print s |\"dest\"
        \"|&dest\"                         print s |&\"dest\"

find_line(input, field, value)
    retourner la première ligne du fichier input dont le champ \$field vaut
    value. Retourner une chaine vide si la ligne n'a pas été trouvée.

merge_line(input, field, key)
    équivaut à:
        \$0 = \$0 FS find_line(input, field, \$key)
    La ligne courante n'est pas modifiée si la ligne correspondante dans input
    n'est pas trouvée.

array_parsecsv2(fields, line, nbfields, colsep, qchar, echar)
array_parsecsv(fields, line, nbfields[, colsep, qchar, echar])
    analyser une ligne au format csv, et initialiser le tableau fields aux valeurs
    des champs trouvés. Si nbfields est spécifié, c'est le nombre de champs
    minimum que doit avoir le tableau résultat. Le tableau est complété avec des
    chaines vides au besoin. Le tableau commence à l'index 1.
    Consulter array_formatcsv2 pour la signification de colsep, qchar et echar.
    array_parsecsv(fields, line, nbfields) est équivalent à
        array_parsecsv2(fields, line, nbfields, ",", "\"", "")

parsecsv(line)
    équivaut à:
        array_parsecsv(fields, line)
        array_getline(fields)

getlinecsv(file)
    obtient une ligne par getline, l'analyse, puis initialise \$1..\$N avec les
    valeurs trouvées. equivaut à:
        getline
        parsecsv(\$0)
    Si file est spécifié, utiliser 'getline <file' au lieu de 'getline'

array_formatcsv2(fields, colsep, mvsep, qchar, echar)
    construit une ligne au format csv avec les colonnes du tableau fields.
    - colsep est le séparateur des colonnes, e.g \",\"
    - mvsep est le séparateur des valeurs des colonnes (si une colonne contient
      plusieurs valeurs, par exemple si elle transporte la valeur d'un attribut
      multivalué). Cette information ne sert que pour décider s'il faut encadrer
      la colonne avec qchar.
    - qchar est le caractère à employer pour encadrer la valeur d'une colonne
      si elle contient l'un des caractères colsep ou mvsep
    - echar est le caractère à employer pour mettre en echappement le caractère
      qchar dans la valeur d'une colonne. Si echar est vide, le caractère qchar
      est doublé.

array_formatcsv(fields, output)
    équivaut à
        array_formatcsv2(fields, \",\", \";\", \"\\\"\", \"\")

array_printcsv(fields, output)
    équivaut à
        printto(array_formatcsv(fields), output)

get_formatcsv()
    équivaut à
        array_fill(fields)
        return array_formatcsv(fields)

formatcsv()
    équivaut à
        \$0 = get_formatcsv()

printcsv(output)
    équivaut à
        array_fill(fields)
        array_printcsv(fields, output)

array_findcsv(fields, input, field, value, nbfields)
    initialiser le tableau fields avec la première ligne au format csv du fichier
    input dont le champ \$field vaut value. Le tableau est toujours initialisé,
    même si la ligne correspondante n'a pas été trouvée.
    Retourner 1 si la ligne correspondante a été trouvée
    Si nbfields est spécifié, c'est le nombre de champs minimum que doit avoir le
    tableau résultat. Le tableau est complété avec des chaines vides au besoin. Le
    tableau commence à l'index 1."

################################################################################

__AWKCSV_HELP="\
Lancer un script awk pour traiter un flux csv
Typiquement, l'option -e sera utilisée pour faire un traitement sur les
données, e.g:
    awkcsv -e '{ \$2 = toupper(\$2) }'
pour mettre en majuscule le deuxième champ.

Analyse du flux en entrée:
-s, -S, --skip-lines nblines
    Sauter nblines au début du flux
-h, -H, --parse-headers
    Lire la liste des champs à partir de la première ligne non ignorée du flux.
    Si la liste des champs est vide, cette option est implicitement activée.
    Par contre, si une liste de champs est spécifiée et que le flux en entrée
    contient une ligne d'en-têtes, il *faut* spécifier explicitement cette
    option.
--sepconf 'CQE'
    Spécifier en une seule option les caractères de séparation des colonnes (C),
    le caractère pour encadrer les chaines (Q) et le caractère d'échappement
    (E). Pour chacun des caractères, s'il n'est pas spécifié, la valeur par
    défaut est prise. CQE vaut par défaut ',\"'
--colsep C
--qchar Q
--echar E
    Spécifier les caractères pour l'analyse du flux csv. Seul le premier
    caractère de respectivement C, Q et E est considéré.
    - C est le séparateur des colonnes, e.g \",\"
    - Q est le caractère employé pour encadrer la valeur d'une colonne si elle
      contient le caractère C
    - E est le caractère employé pour mettre en echappement le caractère Q dans
      la valeur d'une colonne. Si E est vide, le caractère Q doit être doublé.

Flux en sortie:
-n, --no-headers
    Ne pas afficher les en-têtes. Par défaut, les en-têtes sont affichés.
-z, --reset-fields
    Commencer le script -e avec les champs vides. Il faut les copier
    individuellement avec copyfield().

Scripts:
-v var=value
    Définir une variable pour le script awk
-b before
    Instructions à exécuter avant le début de l'analyse. Utiliser la variante
    --rb pour remplacer les instructions par défaut, qui sont exécutées avant
    les instructions de -b
    Par défaut, il n'y a pas d'instructions -b et les instructions de --rb sont
    d'analyser les en-têtes si -h est spécifié. Le tableau ORIGHEADERS contient
    les en-têtes analysées en entrée. Le tableau HEADERS contient les en-têtes
    spécifiées par l'utilisateur (copie de ORIGHEADERS si l'utilisateur de
    spécifie pas d'en-têtes). Il est recommandé de ne pas modifier --rb

-e script
    Instructions à exécuter pour chaque ligne en entrée. Utiliser la variante
    --re pour remplacer les instructions par défaut qui sont exécutées avant les
    instructions de -e
    Par défaut, il n'y a pas d'instructions -e et les instructions de --re sont
    d'analyser la ligne courante. Les champs sont disponibles dans \$1..\$NF et
    dans le tableau ORIGFIELDS. Il est recommandé de ne pas modifier --re
-m, --map FIELDMAP
    Renommer les champs selon la liste FIELDMAP, qui contient des éléments de la
    forme dest:src et séparés par des virgules. Le champ src doit exister dans
    HEADERS, et dest ne doit pas exister dans HEADERS, sinon l'élément est
    ignoré. Un script équivalent au suivant est ajouté aux instructions par
    défaut --re:
        '{ if (do_once(\"mapfields\")) mapfields(FIELDMAP) }'
    Cette option annule l'option -z
-c, --checkfields FIELDS
    S'assurer que toutes les en-têtes spécifiées dans la liste FIELDS sont
    présentes. Sinon, sortir du script avec le code de retour 1. Un script
    équivalent au suivant est rajouté aux instructions par défaut --re:
        '{ if (do_once(\"checkfields\") && !checkfields(FIELDS)) exit 1 }'
    Ce traitement est effectué le cas échéant après le traitement --map
    Cette option annule l'option -z
--checkvalues FIELDS
    S'assurer que toutes les valeurs des en-têtes spécifiées dans la liste
    FIELDS sont non vides. Sinon, sortir du script avec le code de retour 1. Un
    script équivalent au suivant est rajouté aux instructions par défaut --re:
        '{ if (!checkvalues(FIELDS)) exit 1 }'
    Ce traitement est effectué le cas échéant après le traitement --checkfields
    Cette option annule l'option -z
-k, --keep-fields KEEPFIELDS
    Garder les champs spécifiés. Les autres sont supprimés de la sortie.
    KEEPFIELDS est une liste de champs séparés par des virgules, ou '*' pour
    spécifier de garder tous les champs, ou '' pour ne garder aucun champ.
    Si un champ de KEEPFIELDS n'existe pas, il est créé.
    Cette option annule l'option -z
--skip-fields SKIPFIELDS
    Exclure les champs spécifiés. SKIPFIELDS est une liste de champs séparés par
    des virgules.
    Pour les options --keep et --skip, un script équivalent au suivant est
    rajouté aux instructions par défaut --re:
        if (do_once(\"filterfields\")) {
          build_skipfs(KEEPFIELDS, SKIPFIELDS, SKIPFS, ADDFS)
          filterheaders(SKIPFS, ADDFS)
        }
        filterfields(SKIPFS, ADDFS)
    Ce traitement est effectué le cas échéant après le traitement --checkvalues
-a after
    Instructions à exécuter après avoir traité la ligne en entrée. Utiliser la
    variante --ra pour remplacer les instructions par défaut qui sont exécutées
    avant les instructions de -a
    Par défaut, il n'y a pas d'instructions --ra et les instructions de -a sont
    d'afficher les en-têtes si -n n'est pas spécifié. Puis afficher les champs
    \$1..\$NF

En fonctions des arguments, les variables skip_lines, parse_headers et
show_headers sont définis. user_headers est un tableau contenant les champs
spécifiés par l'utilisateur.

Les fonctions suivantes sont disponibles:
do_once(key)
    Retourner vrai s'il faut faire l'opération identifiées par key (qui vaut
    par défaut \"default\"), qui ne devrait être faite qu'une seule fois.
    Utiliser de cette manière:
        if (do_once(key)) { ... }
ogeth(field)
ogeti(num)
oget(field)
    Gestion des valeurs originales.
geth(field)
sethi(num, value)
seth(field, newfield)
addh(field)
delhi(num)
delh(field)
geti(num)
get(field)
seti(num, value)
set(field, value)
add(field, value)
deli(num)
del(field)
    Gestion des valeurs courantes.
comparevic(field1, value1, field2, value2, icfields)
ocompareic(field1, field2, icfields)
compareic(field1, field2, icfields)
infields(field, fields)
parseheaders()
printheaders()
resetfields()
copyfield(field)
copyfields(fields)
copyall()
mapfields(fieldmap)
checkfields(fields[, missings])
checkvalues(fields[, missings])
build_skipfs(keepfields, skipfields, skipfs, addfs)
filterheaders(skipfs, addfs)
filterfields(skipfs, addfs)
    Gestion avancée des valeurs. Pour les fonctions comparevic(), ocompareic(),
    compareic(), infields(), copyfields(), checkfields(), checkvalues(),
    mapfields(), build_skipfs(), les arguments sont une suite d'éléments séparés
    par des virgules."

__AWKCSV_FUNCTIONS='
BEGIN {
  # Forcer ici le type tableau pour les variables ci-dessous. Sinon, leur
  # utilisation dans une section BEGIN{} provoque une erreur fatale, parce
  # qu"ils ne sont déclarés que dans des fonctions
  array_new(HEADERS)
  array_new(ORIGHEADERS)
  array_new(ORIGFIELDS)
  array_new(__DONE_ONCE)
}
function do_once(key) {
  if (!key) key = "default"
  if (__DONE_ONCE[key] != "") return 0
  __DONE_ONCE[key] = 1
  return 1
}

function __geth(field, HEADERS,               nbfields, i) {
  nbfields = array_len(HEADERS)
  if (int(field) == field) {
    field = int(field)
    if (field >= 1 && field <= nbfields) return field
    else return 0
  }
  field = tolower(field)
  for (i = 1; i <= nbfields; i++) {
    if (field == tolower(HEADERS[i])) {
      return i
    }
  }
  return 0
}
function __geti(num, HEADERS) { if (num != 0) return HEADERS[num] }
function __get(field, HEADERS) { return __geti(__geth(field, HEADERS), HEADERS) }


function ogeth(field,                nbfields, i) {
  nbfields = array_len(ORIGHEADERS)
  if (int(field) == field) {
    field = int(field)
    if (field >= 1 && field <= nbfields) return field
    else return 0
  }
  field = tolower(field)
  for (i = 1; i <= nbfields; i++) {
    if (field == tolower(ORIGHEADERS[i])) {
      return i
    }
  }
  return 0
}
function ogeti(num) { if (num != 0) return ORIGFIELDS[num] }
function oget(field) { return ogeti(ogeth(field)) }

function geth(field,                nbfields, i) {
  nbfields = array_len(HEADERS)
  if (int(field) == field) {
    field = int(field)
    if (field >= 1 && field <= nbfields) return field
    else return 0
  }
  field = tolower(field)
  for (i = 1; i <= nbfields; i++) {
    if (field == tolower(HEADERS[i])) {
      return i
    }
  }
  return 0
}
function sethi(num, value) { if (num != 0) HEADERS[num] = value }
function seth(field, value) { sethi(geth(field), value) }
function addh(field,                 num) {
  num = geth(field)
  if (num == 0) {
    num = array_len(HEADERS) + 1
    HEADERS[num] = field
  }
  return num
}
function delhi(num,         i, l) {
  if (num == 0) return
  array_deli(HEADERS, num)
}
function delh(field) { delhi(geth(field)) }
function geti(num) { if (num != 0) return $num }
function get(field) { return geti(geth(field)) }
function seti(num, value) { if (num != 0) $num = value }
function set(field, value) { seti(geth(field), value) }
function add(field, value,           num, i, max) {
  num = addh(field)
  i = NF
  max = array_len(HEADERS)
  if (i < max) {
    i = i + 1
    while(i <= max) {
      $i = ""
      i = i + 1
    }
  }
  if ($num == "") {
    $num = value
  } else {
    $num = $num ";" value
  }
}
function deli(num,         i, j) {
  if (num == 0) return
  i = num
  while (i < NF) {
    j = i + 1
    $i = $j
    i = i + 1
  }
  NF = NF - 1
}
function del(field) { deli(geth(field)) }

function comparevic(field1, value1, field2, value2, icfields,           array) {
  split(icfields, array, /,/)
  if (in_array(field1, array, 1) || in_array(field2, array, 1)) {
    return tolower(value1) == tolower(value2)
  } else {
    return value1 == value2
  }
}
function ocompareic(field1, field2, icfields,           v1, v2, array) {
  return comparevic(field1, oget(field1), field2, oget(field2), icfields)
}
function compareic(field1, field2, icfields,           v1, v2, array) {
  return comparevic(field1, get(field1), field2, get(field2), icfields)
}
function infields(field, fields,           array) {
  split(fields, array, /,/)
  return in_array(field, array, 1)
}
function parseheaders() {
  if (do_once("parse-headers")) {
    array_parsecsv(ORIGHEADERS, $0)
    if (!user_headers_count) array_copy(HEADERS, ORIGHEADERS)
    next
  }
}
function printheaders() {
  if (do_once("show-headers")) {
    array_printcsv(HEADERS)
  }
}
function resetheaders() {
  array_new(HEADERS)
}
function resetfields(                 nf) {
  $0 = ""
  nf = array_len(HEADERS)
  $nf = ""
}
function copyfield(field) {
  set(field, oget(field))
}
function copyfields(fields,           array) {
  split(fields, array, /,/)
  for (i = 1; i <= array_len(array); i++) {
    copyfield(array[i])
  }
}
function copyall() {
  array_getline(ORIGFIELDS)
}
function checkfields(fields, missings,          array, r, field) {
  array_new(missings)
  split(fields, array, /,/)
  r = 1
  for (i = 1; i <= array_len(array); i++) {
    field = array[i]
    if (geth(field) == 0) {
      array_add(missings, field)
      r = 0
    }
  }
  return r
}
function checkvalues(fields, missings,          array, r, field) {
  array_new(missings)
  split(fields, array, /,/)
  r = 1
  for (i = 1; i <= array_len(array); i++) {
    field = array[i]
    if (geth(field) == 0 || !get(field)) {
      array_add(missings, field)
      r = 0
    }
  }
  return r
}
function mapfields(fieldmap,           mapitems, parts) {
  split(fieldmap, mapitems, /,/)
  for (i = 1; i <= array_len(mapitems); i++) {
    split(mapitems[i], parts, /:/)
    if (array_len(parts) != 2) continue
    desti = geth(parts[1])
    srci = geth(parts[2])
    if (desti == 0 && srci != 0) {
      HEADERS[srci] = parts[1]
    }
  }
}
function build_skipfs(keepfields, skipfields, skipfs, addfs,         keepfs, keepfs_count, tmpfields, headers_count, i, field, fieldi) {
  array_new(skipfs)
  array_new(addfs)
  if (skipfields == "*") { skipfields = ""; keepfields = "" }
  if (keepfields == "*" && skipfields == "") return 0
  # construire la liste des champs à garder dans keepfs
  split(keepfields, keepfs, /,/)
  if (in_array("*", keepfs)) {
    array_del(keepfs, "*")
    array_extend(keepfs, HEADERS)
  }
  split(skipfields, tmpfields, /,/)
  for (i in tmpfields) {
    array_del(keepfs, tmpfields[i], 1)
  }
  keepfs_count = array_len(keepfs)
  # puis construire la liste des champs à supprimer dans skipfs
  headers_count = array_len(HEADERS)
  for (i = 1; i <= headers_count; i++) {
    field = HEADERS[i]
    if (!in_array(field, keepfs, 1)) {
      fieldi = geth(field)
      if (i != 0) array_add(skipfs, fieldi)
    }
  }
  asort(skipfs)
  # puis construire la liste des champs à ajouter dans addfs
  for (i = 1; i <= keepfs_count; i++) {
    field = keepfs[i]
    if (!in_array(field, HEADERS, 1)) {
      array_add(addfs, field)
    }
  }
  return array_len(skipfs)
}
function filterheaders(skipfs, addfs,        skipfs_count, addfs_count) {
  skipfs_count = array_len(skipfs)
  for (i = skipfs_count; i >= 1; i--) {
    array_deli(HEADERS, skipfs[i])
  }
  addfs_count = array_len(addfs)
  for (i = 1; i <= addfs_count; i++) {
    addh(addfs[i])
  }
}
function filterfields(skipfs, addfs,         skipfs_count) {
  skipfs_count = array_len(skipfs)
  for (i = skipfs_count; i >= 1; i--) {
    deli(skipfs[i])
  }
  addfs_count = array_len(addfs)
  for (i = 1; i <= addfs_count; i++) {
    add(addfs[i], "")
  }
}
'
function lawkcsv() {
    local beforescript='{
  if (user_headers_count && do_once("user-headers")) {
    if (!parse_headers) array_copy(ORIGHEADERS, user_headers)
    array_copy(HEADERS, user_headers)
  }
  if (parse_headers) parseheaders()
}'
    local append_beforescript=
    local awkscript='{
  array_parsecsv(ORIGFIELDS, $0, array_len(ORIGHEADERS))
  if (reset_fields) { resetfields() } else { copyall() }
}'
    local append_awkscript=
    local afterscript=
    local append_afterscript='{
  if (show_headers) printheaders()
  printcsv()
}'

    local -a args headers vars
    local skip=0 parse_headers=
    local autosep=
    local sepconf=',"' colsep=',' qchar='"' echar=
    local show_headers=1 reset_fields=
    local fieldmap checkfields checkvalues keepf skipf
    if parse_opts \
        -s:,-S:,--skip-lines:,--skiplines: skip= \
        -h,-H,--parse-headers parse_headers=1 \
        --sepconf: '$autosep=sepconf; set@ sepconf' \
        --colsep: '$autosep=indiv; set@ colsep' \
        --qchar: '$autosep=indiv; set@ qchar' \
        --echar: '$autosep=indiv; set@ echar' \
        -n,--no-headers show_headers= \
        --show-headers show_headers=1 \
        -z,--reset-fields reset_fields=1 \
        -v:,--var: vars \
        --rb:,--rbe: '$set@ beforescript; append_beforescript=' \
        -b:,--b:,--be:,--before-script: append_beforescript= \
        --re: '$set@ awkscript; append_awkscript=' \
        -e:,--e:,--awk-script:,--script: append_awkscript= \
        -m:,--map:,--mapfields fieldmap= \
        -c:,--check-fields:,--checkfields: checkfields= \
        --check-values:,--checkvalues: checkvalues= \
        -k:,--keep:,--keep-fields:,--keepfields: keepf= \
        --skip:,--skip-fields:,--skipfields: skipf= \
        --ra:,--rae: '$set@ afterscript; append_afterscript=' \
        -a:,--a:,--ae:,--after-script: append_afterscript= \
        @ args -- "$@"; then
        set -- "${args[@]}"
    else
        eerror "$args"
        return 1
    fi

    if [ "$autosep" == sepconf ]; then
        colsep="${sepconf:0:1}"
        qchar="${sepconf:1:1}"
        echar="${sepconf:2:1}"
        autosep=
        if [ -n "$colsep" -o -n "$qchar" -o -n "$echar" ]; then
            [ -n "$colsep" ] || colsep=','
            [ -n "$qchar" ] || qchar='"'
            # n'activer la configuration que si elle diffère de la configuration
            # par défaut
            [ "$colsep" != ',' -o "$qchar" != '"' -o -n "$echar" ] && autosep=1
        fi
    elif [ "$autosep" == indiv ]; then
        autosep=1
    fi
    colsep="${colsep:0:1}"
    qchar="${qchar:0:1}"
    echar="${echar:0:1}"

    headers=()
    while [ $# -gt 0 -a "$1" != "--" ]; do
        array_add headers "$1"
        shift
    done
    shift

    [ -n "${headers[*]}" ] || parse_headers=1

    if [ -n "$fieldmap" ]; then
        awkscript="$awkscript"'{
  if (do_once("mapfields")) { mapfields(fields2map) }
}'
        reset_fields=
    fi
    if [ -n "$checkfields" ]; then
        awkscript="$awkscript"'{
  if (do_once("checkfields")) { if (!checkfields(fields2check)) exit 1 }
}'
        reset_fields=
    fi
    if [ -n "$checkvalues" ]; then
        awkscript="$awkscript"'{
  if (!checkvalues(values2check)) exit 1
}'
        reset_fields=
    fi
    if [ -n "$skipf" ]; then
        [ -n "$keepf" ] || keepf="*"
    fi
    if [ -n "$keepf" ]; then
        awkscript="$awkscript"'{
  if (do_once("filterfields")) {
    build_skipfs(fields2keep, fields2skip, SKIPFS, ADDFS)
    filterheaders(SKIPFS, ADDFS)
  }
  filterfields(SKIPFS, ADDFS)
}'
        reset_fields=
    fi

    awkrun -f \
        skip_lines:int="$skip" parse_headers:int="$parse_headers" \
        autosep:int="$autosep" colsep="$colsep" qchar="$qchar" echar="$echar" \
        show_headers:int="$show_headers" reset_fields:int="$reset_fields" \
        fields2map="$fieldmap" fields2check="$checkfields" values2check="$checkvalues" \
        fields2keep="$keepf" fields2skip="$skipf" \
        "user_headers[@]=headers" "${vars[@]}" \
"$__AWKCSV_FUNCTIONS"'
BEGIN {
  if (autosep) {
    DEFAULT_COLSEP = colsep
    DEFAULT_QCHAR = qchar
    DEFAULT_ECHAR = echar
  }
}
NR <= skip_lines { next }
'"$beforescript
$append_beforescript
$awkscript
$append_awkscript
$afterscript
$append_afterscript
" -- "$@"
}

function cawkcsv() { LANG=C lawkcsv "$@"; }
function awkcsv() { LANG=C lawkcsv "$@"; }

################################################################################

__GREPCSV_HELP="\
Faire une recherche dans un flux csv, et afficher uniquement les lignes qui
correspondent à l'expression.
EXPR est une expression awk, e.g. 'field == \"value1\" || field == \"value2\"'

Analyse du flux en entrée:
-s, -S, --skip-lines nblines
    Sauter nblines au début du flux
-h, -H, --parse-headers
    Lire la liste des champs à partir de la première ligne non ignorée du flux.
    Si la liste des champs est vide, cette option est implicitement activée.
    Par contre, si une liste de champs est spécifiée et que le flux en entrée
    contient un ligne d'en-têtes, il *faut* spécifier explicitement cette
    option.
--sepconf 'CQE'
    Spécifier en une seule option les caractères de séparation des colonnes (C),
    le caractère pour encadrer les chaines (Q) et le caractère d'échappement
    (E). Pour chacun des caractères, s'il n'est pas spécifié, la valeur par
    défaut est prise. CQE vaut par défaut ',\"'
--colsep C
--qchar Q
--echar E
    Spécifier les caractères pour l'analyse du flux csv. Seul le premier
    caractère de respectivement C, Q et E est considéré.
    - C est le séparateur des colonnes, e.g \",\"
    - Q est le caractère employé pour encadrer la valeur d'une colonne si elle
      contient le caractère C
    - E est le caractère employé pour mettre en echappement le caractère Q dans
      la valeur d'une colonne. Si E est vide, le caractère Q doit être doublé.

Flux en sortie:
-n, --no-headers
    Ne pas afficher les en-têtes. Par défaut, les en-têtes sont affichés.
-q, --quiet
    Ne pas afficher les lignes. Indiquer seulement si des lignes ont été
    trouvées.

Scripts:
-v var=value
    Définir une variable pour le script awk
-e script
    Instructions à exécuter pour chaque ligne en entrée. Utiliser la variante
    --re pour remplacer les instructions par défaut qui sont exécutées avant les
    instructions de -e
    Par défaut, il n'y a pas d'instructions -e et les instructions de --re sont
    de créer des variables nommées d'après les colonnes du flux csv. Il est
    recommandé de ne pas modifier --re
    Note d'implémentation: pour des raisons d'optimisation, le traitement
    effectué sur chaque ligne n'est pas aussi complet que celui de la fonction
    awkcsv(): Les tableaux ORIGFIELDS et ORIGHEADERS sont disponible; par
    contre, la ligne courante est au format csv non analysé. Utiliser la
    fonction copyall() pour être dans les mêmes conditions que le script awkcsv."

function lgrepcsv() {
    local -a args vars
    local skip= parse_headers=
    local sepconf= colsep= qchar= echar=
    local no_headers= quiet=
    local awkscript=--use-default--
    local append_awkscript=
    if parse_opts \
        -s:,-S:,--skip:,--skip-lines:,--skiplines: skip= \
        -h,-H,--parse-headers parse_headers=1 \
        --sepconf: sepconf= \
        --colsep: colsep= \
        --qchar: qchar= \
        --echar: echar= \
        -n,--no-headers no_headers=1 \
        --show-headers no_headers= \
        -q,--quiet quiet=1 \
        -v:,--var: vars \
        --re: '$set@ awkscript; append_awkscript=' \
        -e:,--e:,--awk-script:,--script: append_awkscript= \
        @ args -- "$@"; then
        set -- "${args[@]}"
    else
        eerror "$args"
        return 1
    fi

    local expr="$1"; shift

    local -a inputfiles tmpfiles
    local inputfile
    while [ $# -gt 0 -a "$1" != "--" ]; do
        if [ "$1" == "-" ]; then
            ac_set_tmpfile inputfile
            array_add tmpfiles "$inputfile"
            cat >"$inputfile"
        else
            inputfile="$1"
        fi
        array_add inputfiles "$inputfile"
        shift
    done
    shift
    if [ "${#inputfiles[*]}" -eq 0 ]; then
        ac_set_tmpfile inputfile
        array_add tmpfiles "$inputfile"
        cat >"$inputfile"
        array_add inputfiles "$inputfile"
    fi

    local -a headers
    headers=("$@")
    if [ -z "${headers[*]}" ]; then
        parse_headers=1
        array_from_lines headers "$(awkrun -f lskip:int="$skip" 'NR <= lskip { next }
{
  count = array_parsecsv(__fields, $0)
  for (i = 1; i <= count; i++) {
    print __fields[i]
  }
  exit
}' -- "${inputfiles[@]}")"
    fi
    if [ "$awkscript" == --use-default-- ]; then
        awkscript=
        for header in "${headers[@]}"; do
            awkscript="$awkscript
$header = oget(\"$header\")"
        done
    fi
    local grepscript="\
BEGIN { ec = 1 }
{
  $awkscript
  $append_awkscript
  if ($expr) {
    ec = 0
    if (quiet) exit
    printheaders()
    print
  }
}
END { exit ec }"

    lawkcsv ${skip:+-s "$skip"} ${parse_headers:+-h} \
        ${sepconf:+--sepconf "$sepconf"} ${colsep:+--colsep "$colsep"} ${qchar:+--qchar "$qchar"} ${echar:+--echar "$echar"} \
        ${no_headers:+--no-headers} -v quiet:int=$quiet \
        --re '{array_parsecsv(ORIGFIELDS, $0, array_len(ORIGHEADERS))}' -a "$grepscript" \
        -- "${headers[@]}" -- "${inputfiles[@]}"
    local r=$?

    ac_clean "${tmpfiles[@]}"
    return $r
}

function cgrepcsv() { LANG=C lgrepcsv "$@"; }
function grepcsv() { LANG=C lgrepcsv "$@"; }

################################################################################

__AWKFSV2CSV_HELP="\
Transformer un flux fsv (colonnes à largeurs fixes) en csv

Chaque argument doit être de la forme [-]header:size. La colonne sera incluse
dans le fichier en sortie, sauf si elle est précédée de -

Analyse du flux en entrée:
-s, -S, --skip-lines nblines
    Sauter nblines au début du flux
-r, --no-trim
    Ne pas trimmer les valeurs à droite.

Flux en sortie:
-n, --no-headers
    Ne pas afficher les en-têtes. Par défaut, les en-têtes sont affichés."

function lawkfsv2csv() {
    local -a args headersizes
    local skip=0 rtrim=1 show_headers=1
    if parse_opts \
        -s:,-S:,--skip:,--skip-lines:,--skiplines: skip= \
        -r,--no-trim rtrim= \
        -n,--no-headers show_headers= \
        --show-headers show_headers=1 \
        @ args -- "$@"; then
        set -- "${args[@]}"
    else
        eerror "$args"
        return 1
    fi

    local -a headers starts sizes
    local headersize header i size
    i=1
    while [ $# -gt 0 -a "$1" != "--" ]; do
        headersize="$1"
        shift

        splitpair "$headersize" header size
        [ -n "$header" ] || {
            eerror "header est requis"
            return 1
        }
        [ -n "$size" ] || size=1
        if [ "${header#-}" == "$header" ]; then
            array_add headers "$header"
            array_add starts "$i"
            array_add sizes "$size"
        fi
        i="$(($i + $size))"
    done
    shift

    awkrun -f \
        skip_lines:int="$skip" trim_values:int="$rtrim" show_headers:int="$show_headers" \
        headers[@] starts[@] sizes[@] \
        "$__AWKCSV_FUNCTIONS"'
BEGIN {
  if (show_headers) {
    print array_formatcsv(headers)
  }
}
NR <= skip_lines { next }
{
  line = $0
  $0 = ""
  for (i = 1; i <= headers_count; i++) {
    value = substr(line, starts[i], sizes[i])
    if (trim_values) {
      sub(/^ */, "", value)
      sub(/ *$/, "", value)
    }
    $i = value
  }
  formatcsv()
  print
}
' -- "$@"
}

function cawkfsv2csv() { LANG=C lawkfsv2csv "$@"; }
function awkfsv2csv() { LANG=C lawkfsv2csv "$@"; }

################################################################################

__MERGECSV_HELP="\
Fusionner deux fichiers csv en faisant la correspondance sur la valeur d'un
champ, qui est la clé

-h, -H, --parse-headers
    Lire la liste des champs à partir de la première ligne non ignorée des flux.
    Si cette option est spécifiée (ce qui est le cas par défaut), les champs
    spécifiés avec les options -k, --lk et --rk peuvent être les noms effectifs
    des champs. Sinon, les champs ne peuvent être que numériques.
-n, -N, --numkeys
    Ne pas analyser la première ligne pour les noms des champs. Les champs
    spécifiés ne peuvent être que numériques.
--lskip nblines
--rskip nblines
    Sauter nblines au début du flux, respectivement pour left et pour right
--lheaders FIELDS
--rheaders FIELDS
    Spécifier les noms des champs à utiliser *en sortie*, respectivement pour
    left et pour right. Les champs doivent être séparés par des virgules.
    Si ces valeurs ne sont pas spécifiées, prendre les champs lus avec l'option
    --parse-headers. Sinon, ne pas afficher d'en-têtes en sortie.
--lprefix prefix
--rprefix prefix
    Spécifier un préfixe à rajouter automatiquement aux champs à utiliser *en
    sortie*. Le prefix est rajouté que les champs soient déterminés par
    --parse-headers ou spécifiés par --{l,r}headers. La correspondance des
    champs avec --lk et --rk est faite sur les noms des champs *avant* qu'ils
    soient modifiés par cette option.
--lk, --lkey FIELD
--rk, --rkey FIELD
    Spécifier le champ utilisé pour faire la correspondance entre les deux flux,
    respectivement pour left et pour right.
    Si --parse-headers n'est pas spécifié, ces valeurs doivent être numériques.
    La valeur par défaut est 1.
-k, --key FIELD
    Spécifier le champ utilisé pour faire la correspondance sur les deux
    flux. Equivalent à '--lk FIELD --rk FIELD'
    La correspondance des champs avec --lk et --rk est faite sur les noms des
    champs *avant* qu'ils soient modifiés le cas échéant par --lprefix et/ou
    --rprefix
-i
    Faire la correspondance de la valeur des champs sans tenir compte de la
    casse

Après la fusion des deux fichiers csv, il est possible de faire une sélection
des lignes selon certains critères:

-s SELECT
    Spécifier un type de sélection à faire. SELECT peut valoir:
    none, n
        C'est l'action par défaut; toutes les lignes sont affichées
    inner-join, j
        N'inclure dans le résultat que les lignes pour lesquelles il y a une
        correspondance sur les clés.
        L'option -J est synonyme de '-s inner-join'
    left-join, l
        Inclure dans le résultat les lignes pour lesquelles il y a une
        correspondance sur les clés, et les lignes de left pour lesquelles il
        n'y a pas de correspondance. Les lignes de right sans correspondance ne
        sont pas affichées.
        L'option -L est synonyme de '-s left-join'
    right-join, r
        Inclure dans le résultat les lignes pour lesquelles il y a une
        correspondance sur les clés, et les lignes de right pour lesquelles il
        n'y a pas de correspondance. Les lignes de left sans correspondance ne
        sont pas affichées.
        L'option -R est synonyme de '-s right-join'
    left-only, lo
        N'inclure dans le résultat que les lignes de left pour lesquelles il n'y
        a pas de correspondance dans right.
    right-only, ro
        N'inclure dans le résultat que les lignes de right pour lesquelles il n'y
        a pas de correspondance dans left.

Les options suivantes sont des actions qu'il est possible d'effectuer après la
sélection des lignes. Les noms des champs spécifiés dans ces options sont les
noms en sortie, en tenant compte de l'analyse et du traitement effectués par les
options --parse-headers, --{l,r}headers et --{l,r}prefix.

--copy, --lcopyf FIELDS
    Copier les champs spécifiés de right vers left. Les champs spécifiés sont
    séparés par des virgules et sont de la forme [dest:]src
--rcopyf FIELDS
    Copier les champs spécifiés de left vers right. Les champs spécifiés sont
    séparés par des virgules et sont de la forme [dest:]src
--lkeepf FIELDS
    Garder les champs de left spécifiés. Les autres sont supprimés de la sortie.
    FIELDS est une liste de champs séparés par des virgules, ou '*' pour
    spécifier de garder tous les champs ou '' pour ne garder aucun champ.
    Par défaut, avec '-s right-only', FIELDS vaut '', sinon FIELDS vaut '*'
--keep, --rkeepf FIELDS
    Garder les champs de right spécifiés. Les autres sont supprimés de la
    sortie. FIELDS est une liste de champs séparés par des virgules, ou '*' pour
    spécifier de garder tous les champs, ou '' pour ne garder aucun champ.
    Par défaut, avec '-s left-only', FIELDS vaut '', sinon FIELDS vaut '*'
--lskipf FIELDS
--rskipf FIELDS
    Exclure les champs de left (resp. de right) spécifiés. FIELDS est une liste
    de champs séparés par des virgules."

: "${__MERGECSV_DEBUG:=}"
function lmergecsv() {
    # Fusionner sur la sortie standard les deux fichiers csv $1 et $2. La clé du
    # fichier $1 est spécifiée par l'option --lkey et vaut 1 par défaut. La clé
    # du fichier $2 est spécifiée par l'option --rkey et vaut 1 par défaut. Les
    # valeurs des clés ne doivent pas faire plus de 64 caractères de long.
    eval "$(utools_local)"
    local parse_headers=auto
    local ignore_case=
    local lskip=0 lkey=1 lheaders= lprefix=
    local rskip=0 rkey=1 rheaders= rprefix=
    local select=none

    local postproc=auto
    local lcopyf= rcopyf=
    local lkeepf=--NOT-SET-- rkeepf=--NOT-SET--
    local lskipf= rskipf=
    parse_opts "${PRETTYOPTS[@]}" \
        -h,-H,--parse-headers parse_headers=1 \
        -n,-N,--numkeys parse_headers= \
        --lskip: lskip= \
        --lkey:,--lk: lkey= \
        --lheaders:,--lh: lheaders= \
        --lprefix:,--lp: lprefix= \
        --rskip: rskip= \
        --rkey:,--rk: rkey= \
        --rheaders:,--rh: rheaders= \
        --rprefix:,--rp: rprefix= \
        -i,--ignore-case ignore_case=1 \
        -k:,--key: '$set@ lkey; set@ rkey' \
        -s:,--select: select= \
        -J select=inner-join \
        -L select=left-join \
        -R select=right-join \
        --lcopyf:,--copy: '$set@ lcopyf; postproc=1' \
        --rcopyf: '$set@ rcopyf; postproc=1' \
        --lkeepf: '$set@ lkeepf; postproc=1' \
        --rkeepf:,--keep: '$set@ rkeepf; postproc=1' \
        --lskipf: '$set@ lskipf; postproc=1' \
        --rskipf: '$set@ rskipf; postproc=1' \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    local lfile="$1"
    local rfile="$2"
    [ -f "$lfile" -a -f "$rfile" ] || {
        [ -f "$lfile" ] || eerror "$lfile: fichier introuvable"
        [ -f "$rfile" ] || eerror "$rfile: fichier introuvable"
        return 1
    }

    local padding="----------------------------------------------------------------"
    local padlen=${#padding}

    [ "$parse_headers" == "auto" ] && parse_headers=1
    if [ -n "$parse_headers" ]; then
        [ -n "$lheaders" ] || lheaders="$(<"$lfile" awkrun lskip:int="$lskip" 'NR <= lskip { next } { print; exit }')"
        [ -n "$rheaders" ] || rheaders="$(<"$rfile" awkrun lskip:int="$lskip" 'NR <= lskip { next } { print; exit }')"
    fi

    # faire le fichier de travail pour lfile
    local tmpleft
    ac_set_tmpfile tmpleft "" __mergecsv_left "" __MERGECSV_DEBUG
    <"$lfile" awkrun -f \
        padding="$padding" padlen:int="$padlen" \
        parse_headers:int="$parse_headers" ignore_case:int="$ignore_case" \
        lskip:int="$lskip" lkey="$lkey" \
        "$__AWKCSV_FUNCTIONS"'
NR <= lskip { next }
parse_headers && do_once("parse-headers") {
  array_parsecsv(HEADERS, $0)
  lkey = geth(lkey)
  if (!lkey) lkey = 1
  next
}
{
  oline = $0
  parsecsv($0)
  keyvalue = substr($lkey, 1, padlen)
  if (ignore_case) keyvalue = tolower(keyvalue)
  print keyvalue substr(padding, 1, padlen - length(keyvalue)) "1" oline
}
' | csort -su >"$tmpleft"

    # faire le fichier de travail pour rfile
    local tmpright
    ac_set_tmpfile tmpright "" __mergecsv_right "" __MERGECSV_DEBUG
    <"$rfile" awkrun -f \
        padding="$padding" padlen:int="$padlen" \
        parse_headers:int="$parse_headers" ignore_case:int="$ignore_case" \
        rskip:int="$rskip" rkey="$rkey" \
        "$__AWKCSV_FUNCTIONS"'
NR <= rskip { next }
parse_headers && do_once("parse-headers") {
  array_parsecsv(HEADERS, $0)
  rkey = geth(rkey)
  if (!rkey) rkey = 1
  next
}
{
  oline = $0
  parsecsv($0)
  keyvalue = substr($rkey, 1, padlen)
  if (ignore_case) keyvalue = tolower(keyvalue)
  print keyvalue substr(padding, 1, padlen - length(keyvalue)) "2" oline
}
' | csort -su >"$tmpright"

    # calculer les options de post-traitement
    case "$select" in
    none|n|"") select=none;;
    inner-join|inner|join|j) select=inner-join;;
    left-join|left|l) select=left-join;;
    right-join|right|r) select=right-join;;
    left-only|lo) select=left-only;;
    right-only|ro) select=right-only;;
    *)
        ewarn "$select: valeur de --select invalide. Elle sera ignorée"
        select=none
        ;;
    esac
    if [ "$postproc" == "auto" ]; then
        case "$select" in
        left-only) lkeepf="*"; rkeepf=""; postproc=1;;
        right-only) lkeepf=""; rkeepf="*"; postproc=1;;
        *) lkeepf="*"; rkeepf="*"; postproc=;;
        esac
    fi
    if [ -n "$postproc" ]; then
        if [ "$lkeepf" == "--NOT-SET--" ]; then
            case "$select" in
            right-only) lkeepf=;;
            *) lkeepf="*";;
            esac
        fi
        if [ "$rkeepf" == "--NOT-SET--" ]; then
            case "$select" in
            left-only) rkeepf=;;
            *) rkeepf="*";;
            esac
        fi
        local -a tmpfields fields
        local field src dest
        if [ -n "$lcopyf" ]; then
            array_split tmpfields "$lcopyf" ,
            fields=()
            for field in "${tmpfields[@]}"; do
                splitpair "$field" dest src
                [ -n "$src" ] || src="$dest"
                array_add fields "$dest:$src"
            done
            lcopyf="$(array_join fields ,)"
        fi
        if [ -n "$rcopyf" ]; then
            array_split tmpfields "$rcopyf" ,
            fields=()
            for field in "${tmpfields[@]}"; do
                splitpair "$field" dest src
                [ -n "$src" ] || src="$dest"
                array_add fields "$dest:$src"
            done
            rcopyf="$(array_join fields ,)"
        fi
        if [ "$lskipf" == "*" ]; then
            lskipf=
            lkeepf=
        fi
        if [ -n "$lskipf" ]; then
            [ "$lkeepf" == "*" ] && lkeepf="$lheaders"
            array_split fields "$lkeepf" ,
            array_split tmpfields "$lskipf" ,
            for field in "${tmpfields[@]}"; do
                array_del fields "$field"
            done
            lkeepf="$(array_join fields ,)"
        fi
        if [ "$rskipf" == "*" ]; then
            rskipf=
            rkeepf=
        fi
        if [ -n "$rskipf" ]; then
            [ "$rkeepf" == "*" ] && rkeepf="$rheaders"
            array_split fields "$rkeepf" ,
            array_split tmpfields "$rskipf" ,
            for field in "${tmpfields[@]}"; do
                array_del fields "$field"
            done
            rkeepf="$(array_join fields ,)"
        fi
    fi
    local -a lcopyfs rcopyfs lkeepfs rkeepfs
    array_split lcopyfs "$lcopyf" ,
    array_split rcopyfs "$rcopyf" ,
    array_split lkeepfs "$lkeepf" ,
    array_split rkeepfs "$rkeepf" ,

    # fusionner les deux fichiers
    local tmpmerged
    ac_set_tmpfile tmpmerged "" __mergecsv_merged "" __MERGECSV_DEBUG
    csort -s -k 1,$(($padlen + 1)) "$tmpleft" "$tmpright" >"$tmpmerged"

    <"$tmpmerged" awkrun -f \
        padlen:int="$padlen" \
        parse_headers:int="$parse_headers" ignore_case:int="$ignore_case" \
        lheaderscsv="$lheaders" lkey="$lkey" lprefix="$lprefix" \
        rheaderscsv="$rheaders" rkey="$rkey" rprefix="$rprefix" \
        select="$select" postproc:int="$postproc" \
        lcopyfs[@] rcopyfs[@] \
        lkeepfs[@] rkeepfs[@] \
        "$__AWKCSV_FUNCTIONS"'
function lgeth(field,                nbfields, i) {
  nbfields = array_len(lheaders)
  if (int(field) == field) {
    field = int(field)
    if (field >= 1 && field <= nbfields) return field
    else return 0
  }
  field = tolower(field)
  for (i = 1; i <= nbfields; i++) {
    if (field == tolower(lheaders[i])) {
      return i
    }
  }
  return 0
}
function rgeth(field,                nbfields, i) {
  nbfields = array_len(rheaders)
  if (int(field) == field) {
    field = int(field)
    if (field >= 1 && field <= nbfields) return field
    else return 0
  }
  field = tolower(field)
  for (i = 1; i <= nbfields; i++) {
    if (field == tolower(rheaders[i])) {
      return i
    }
  }
  return 0
}
function copyf(lfields, rfields,        i, fs, vs, l, r) {
  for (i = 1; i <= lcopyfs_count; i++) {
    fs = lcopyfs[i]
    match(fs, /(.*):(.*)/, vs)
    l = vs[1]; l = lgeth(l)
    r = vs[2]; r = rgeth(r)
    if (l && r) {
      lfields[l] = rfields[r]
    }
  }
  for (i = 1; i <= rcopyfs_count; i++) {
    fs = rcopyfs[i]
    match(fs, /(.*):(.*)/, vs)
    l = vs[2]; l = lgeth(l)
    r = vs[1]; r = rgeth(r)
    if (l && r) {
      rfields[r] = lfields[l]
    }
  }
}
function keepf(lfields, rfields,        i) {
  for (i = lskipfs_count; i >= 1; i--) {
    array_deli(lfields, lskipfs[i])
  }
  for (i = rskipfs_count; i >= 1; i--) {
    array_deli(rfields, rskipfs[i])
  }
}
function printmerged(lline, rline, nocopy,        linecsv, tmplinecsv) {
  if (lline != "") array_parsecsv(lfields, lline, array_len(lheaders))
  else array_newsize(lfields, array_len(lheaders))
  if (rline != "") array_parsecsv(rfields, rline, array_len(rheaders))
  else array_newsize(rfields, array_len(rheaders))
  if (postproc) {
    if (!nocopy) copyf(lfields, rfields)
    keepf(lfields, rfields)
  }
  linecsv = array_formatcsv(lfields)
  tmplinecsv = array_formatcsv(rfields)
  if (array_len(rfields) > 0) {
    if (array_len(lfields) > 0) linecsv = linecsv ","
    linecsv = linecsv tmplinecsv
  }
  print linecsv
}
BEGIN {
  if (parse_headers) {
    array_parsecsv(lheaders, lheaderscsv)
    lheaders_count = array_len(lheaders)
    if (lprefix != "") {
      for (i = 1; i <= lheaders_count; i++) {
        lheaders[i] = lprefix lheaders[i]
      }
      lheaderscsv = array_formatcsv(lheaders)
    }
    lkey = lgeth(lkey)
    if (!lkey) lkey = 1

    array_parsecsv(rheaders, rheaderscsv)
    rheaders_count = array_len(rheaders)
    if (rprefix != "") {
      for (i = 1; i <= rheaders_count; i++) {
        rheaders[i] = rprefix rheaders[i]
      }
      rheaderscsv = array_formatcsv(rheaders)
    }
    rkey = rgeth(rkey)
    if (!rkey) rkey = 1
  }
  LEFT = 1
  RIGHT = 2
  hasleft = 0

  # quelle sélection effectuer?
  selectjoin = select ~ /none|inner-join|left-join|right-join/
  selectleft = select ~ /none|left-join|left-only/
  selectright = select ~ /none|right-join|right-only/

  # liste des indexes de champs a supprimer
  array_new(lskipfs)
  if (!in_array("*", lkeepfs)) {
    for (i = 1; i <= lheaders_count; i++) {
      field = lheaders[i]
      if (!in_array(field, lkeepfs)) {
        fieldi = lgeth(field)
        if (i != 0) array_add(lskipfs, fieldi)
      }
    }
    asort(lskipfs)
  }
  lskipfs_count = array_len(lskipfs)

  array_new(rskipfs)
  if (!in_array("*", rkeepfs)) {
    for (i = 1; i <= rheaders_count; i++) {
      field = rheaders[i]
      if (!in_array(field, rkeepfs)) {
        fieldi = rgeth(field)
        if (i != 0) array_add(rskipfs, fieldi)
      }
    }
    asort(rskipfs)
  }
  rskipfs_count = array_len(rskipfs)

  if (parse_headers) {
    # afficher les en-têtes après traitement de lkeepfs et rkeepfs, parce que
    # printmerged() a besoin de lskipfs et rskipfs
    printmerged(lheaderscsv, rheaderscsv, 1)
  }
}
function readleft() {
  lprefix = substr($0, 1, padlen)
  lwhich = substr($0, padlen + 1, 1)
  if (lwhich == "1") lwhich = LEFT; else lwhich = RIGHT
  lline = substr($0, padlen + 2)
  hasleft = 1
}
function readright() {
  rprefix = substr($0, 1, padlen)
  rwhich = substr($0, padlen + 1, 1)
  if (rwhich == "1") rwhich = LEFT; else rwhich = RIGHT
  rline = substr($0, padlen + 2)
}
function right2left() {
  lprefix = rprefix
  lwhich = rwhich
  lline = rline
  hasleft = 1
}
!hasleft {
  readleft()
  next
}
{
  readright()
  if (lprefix == rprefix && lwhich == LEFT && rwhich == RIGHT) {
    if (selectjoin) printmerged(lline, rline)
    hasleft = 0
    next
  } else {
    if (lwhich == LEFT && selectleft) {
      printmerged(lline, "")
    } else if (lwhich == RIGHT && selectright) {
      printmerged("", lline)
    }
    right2left()
    next
  }
}
END {
  if (hasleft) {
    if (lwhich == LEFT && selectleft) {
      printmerged(lline, "")
    } else if (lwhich == RIGHT && selectright) {
      printmerged("", lline)
    }
  }
}
'

    ac_clean "$tmpleft" "$tmpright" "$tmpmerged"
    return 0
}

function cmergecsv() { LANG=C lmergecsv "$@"; }
function mergecsv() { LANG=C lmergecsv "$@"; }

################################################################################

__SORTCSV_HELP="\
Trier un fichier csv sur la valeur d'un champ

-S, --skip-lines nblines
    Sauter nblines au début du flux
-h, -H, --parse-headers
    Lire la liste des champs à partir de la première ligne non ignorée des flux.
    Si cette option est spécifiée (ce qui est le cas par défaut), le champ
    spécifié avec l'option -k peut être le nom effectif du champ. Sinon, le
    champ ne peut être que numérique.
-N, --numkeys
    Ne pas analyser la première ligne pour les noms des champs. Les champs
    spécifiés ne peuvent être que numériques.
-k, --key FIELD
    Spécifier le champ utilisé pour le tri. Si --parse-headers n'est pas
    spécifié, cette valeur doit être numérique. La valeur par défaut est 1.
--no-headers
    Ne pas afficher les en-têtes en sortie.

-n, --numeric-sort
    Comparer selon la valeur numérique du champ
-i, -f, --ignore-case
    Comparer sans tenir compte de la casse
-r, --reverse
    Inverser l'ordre de tri
-s, --stable
    Stabiliser le tri en inhibant la comparaison de dernier recours
-u, --unique
    Ne garder que la première occurence de plusieurs entrées identiques
    rencontrées. Note: la correspondance se fait sur toute l'entrée, pas
    uniquement sur la valeur de la clé.
-o, --output OUTPUT
    Ecrire le résultat dans OUTPUT au lieu de la sortie standard"

: "${__SORTCSV_DEBUG:=}"
function lsortcsv() {
    # Trier le fichier csv $1. La clé du tri est spécifiée par l'option -k et
    # vaut 1 par défaut. Les valeurs des clés ne doivent pas faire plus de 64
    # caractères de long.
    eval "$(utools_local)"
    local skip=0 parse_headers=auto key=1 show_headers=1
    local numeric_sort= ignore_case= reverse_sort= stable_sort= unique_sort= output=
    parse_opts "${PRETTYOPTS[@]}" \
        -S:,--skip:,--skip-lines:,--skiplines: skip= \
        -h,-H,--parse-headers parse_headers=1 \
        -N,--numkeys parse_headers= \
        -k:,--key: key= \
        --no-headers show_headers= \
        --show-headers show_headers=1 \
        -n,--numeric-sort numeric_sort=1 \
        -i,-f,--ignore-case ignore_case=1 \
        -r,--reverse reverse_sort=1 \
        -s,--stable stable_sort=1 \
        -u,--unique unique_sort=1 \
        -o:,--output: output= \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    local input="$1"
    if [ -z "$input" -o "$input" == "-" ]; then
        input=/dev/stdin
    elif [ ! -f "$input" ]; then
        eerror "$input: fichier introuvable"
        return 1
    fi

    local padding="----------------------------------------------------------------"
    local padlen=${#padding}
    local headers

    local -a tmpfiles

    [ "$parse_headers" == "auto" ] && parse_headers=1
    if [ -n "$parse_headers" -a -z "$headers" ]; then
        if [ "$input" == /dev/stdin ]; then
            # Si on lit depuis stdin, il faut faire une copie du flux dans un
            # fichier temporaire pour calculer les en-têtes
            local tmpinput
            ac_set_tmpfile tmpinput "" __sortcsv_input0 "" __SORTCSV_DEBUG
            array_add tmpfiles "$tmpinput"
            cat >"$tmpinput"
            input="$tmpinput"
        fi
        headers="$(<"$input" awkrun skip:int="$skip" 'NR <= skip { next } { print; exit }')"
    fi

    # faire le fichier de travail
    local tmpinput
    ac_set_tmpfile tmpinput "" __sortcsv_input "" __SORTCSV_DEBUG
    array_add tmpfiles "$tmpinput"
    <"$input" >"$tmpinput" awkrun -f \
        padding="$padding" padlen:int="$padlen" \
        skip:int="$skip" parse_headers:int="$parse_headers" \
        key="$key" \
        "$__AWKCSV_FUNCTIONS"'
NR <= skip { next }
parse_headers && do_once("parse-headers") {
  array_parsecsv(HEADERS, $0)
  key = geth(key)
  if (!key) key = 1
  next
}
{
  oline = $0
  parsecsv($0)
  keyvalue = substr($key, 1, padlen)
  print keyvalue substr(padding, 1, padlen - length(keyvalue)) oline
}
'

    # trier le fichier de travail
    local tmpsorted
    ac_set_tmpfile tmpsorted "" __sortcsv_sorted "" __SORTCSV_DEBUG
    array_add tmpfiles "$tmpsorted"
    args=(# arguments de sort
        ${numeric_sort:+-n} ${ignore_case:+-f}
        ${reverse_sort:+-r} ${stable_sort:+-s}
        ${unique_sort:+-u}
    )
    csort -k 1,$(($padlen + 1)) "${args[@]}" <"$tmpinput" >"$tmpsorted"

    # résultat
    [ -n "$output" ] || output=/dev/stdout
    stdredir "$tmpsorted" "$output" "" awkrun -f \
        padlen:int="$padlen" \
        headerscsv="$headers" show_headers:int="$show_headers" \
        '
BEGIN { if (show_headers) print headerscsv }
{ print substr($0, padlen + 1) }
'

    ac_clean "${tmpfiles[@]}"
    return 0
}

function csortcsv() { LANG=C lsortcsv "$@"; }
function sortcsv() { LANG=C lsortcsv "$@"; }

################################################################################

__DUMPCSV_HELP="\
Afficher les champs spécifiés pour traitement par le shell

-S, --skip-lines nblines
    Sauter nblines au début du flux
-H, --parse-headers
    Lire la liste des champs à partir de la première ligne non ignorée du flux.
    Si cette option est spécifiée (ce qui est le cas par défaut), les champs
    spécifiés peuvent être les noms effectifs des champs. Sinon, les champs ne
    peuvent être que numériques.
-N, --numkeys
    Ne pas analyser la première ligne pour les noms des champs. Les champs
    spécifiés ne peuvent être que numériques.

-k, --keep-fields KEEPFIELDS
    Garder les champs spécifiés. Les autres sont supprimés de la sortie.
    KEEPFIELDS est une liste de champs séparés par des virgules.
-s, --skip-fields SKIPFIELDS
    Exclure les champs spécifiés. SKIPFIELDS est une liste de champs séparés par
    des virgules.

-h, --dump-headers
    Inclure les en-têtes dans la sortie. Elles sont traitées exactement comme
    une ligne de données.
--hname HNAME
    Spécifier le nom à utiliser pour afficher les en-têtes avec l'option -h

-n, --name NAME
    Spécifier le nom à utiliser pour les options -f, -a, -b
-f, --function
    Afficher les champs comme l'appel d'une fonction, e.g:
        dump value00 value01...
        dump value10 value11...
    C'est la méthode d'affichage par défaut. L'option -n permet de spécifier le
    nom de la fonction, qui vaut 'dump' par défaut. Avec -h, le nom par défaut
    est 'dumph'
-a, --array
    Afficher les champs comme les valeurs d'un tableau, e.g:
        values=()
        values=(value00 value01...)
        values=(value10 value11...)
    Bien entendu, cela n'a sens que si une seule ligne résultat est attendue.
    L'option -n permet de spécifier le nom du tableau, qui vaut 'values' par
    défaut. Avec -h, le nom par défaut est 'names'
    Cette méthode commence par afficher de quoi initialiser le tableau à une
    valeur vide. Ainsi, si aucune ligne n'est trouvée, le tableau est néanmoins
    initialisé. Si ce comportement n'est pas désiré, il est possible d'utiliser
    l'option --no-reset
-b, --array-function
    Afficher les champs comme l'initialisation des valeurs d'un tableau suivi de
    l'appel d'une fonction, e.g:
        values=(value00 value01...)
        dump
        values=(value10 value11...)
        dump
    Le nom du tableau est fixé à 'values'. L'option -n permet de spécifier le
    nom de la fonction, qui vaut 'dump' par défaut. Avec -h le nom par défaut
    est 'dumph'
-v, --var
    Affiche les champ en initialisant des variables, e.g:
        names=(header0 header1)
        header0=
        header1=
        header0=value00
        header1=value01
        ...
        header0=value10
        header1=value11
        ...
    Bien entendu, cela n'a sens que si une seule ligne résultat est attendue.
    Cette option implique -h. Le nom par défaut du tableau qui liste les
    variables est names.
    Cette méthode commence par réinitialiser les variables à la valeur vide.
    Ainsi, si aucune ligne n'est trouvée, les variables sont vides. Si ce
    comportement n'est pas désiré, il est possible d'utiliser l'option
    --no-reset"

function ldumpcsv() {
    eval "$(utools_local)"
    local skip= parse_headers=1 keepf skipf show_headers
    local dump=function reset=1 name hname
    parse_opts "${PRETTYOPTS[@]}" \
        -S:,--skip:,--skip-lines:,--skiplines: skip= \
        -H,--parse-headers parse_headers=1 \
        -N,--numkeys parse_headers= \
        -k:,--keep:,--keep-fields:,--keepfields: keepf= \
        -s:,--skip:,--skip-fields:,--skipfields: skipf= \
        -h,--dump-headers show_headers=1 \
        -n:,--name: name= \
        --hname: hname= \
        -f,--function dump=function \
        -a,--array dump=array \
        --reset reset=1 \
        --no-reset reset= \
        -b,--array-function dump=array-function \
        -v,--var dump=var \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    args=(
        ${skip:+--skip-lines "$skip"}
        ${keepf:+--keep-fields "$keepf"}
        ${skipf:+--skip-fields "$skipf"}
    )
    if [ -n "$parse_headers" ]; then array_add args -h
    else array_add args -n
    fi

    [ "$dump" == var ] && show_headers=1

    if [ -z "$hname" ]; then
        case "$dump" in
        function|array-function) hname=dumph;;
        array|var) hname=names;;
        esac
    fi
    if [ -z "$name" ]; then
        case "$dump" in
        function|array-function) name=dump;;
        array) name=values;;
        esac
    fi

    local -a fields
    fields=("$@")

    awkcsv "${args[@]}" -v show_headers:int="$show_headers" -v hname="$hname" -v name="$name" -v dump="$dump" -v reset_values:int="$reset" -v fields[@] -e '
function dump_values(name, values, dump) {
  if (dump == "function") {
    print qarr(values, name)
  } else if (dump == "array") {
    print name "=(" qarr(values) ")"
  } else if (dump == "array-function") {
    print "values=(" qarr(values) ")"
    print name
  } else if (dump == "var") {
    i = 1
    while (i <= fields_count) {
      print fields[i] "=" qval(values[i])
      i++
    }
  }
}
function init_fields() {
  if (do_once("init_fields")) {
    if (fields_count == 0) {
      array_copy(fields, HEADERS)
      fields_count = array_len(fields)
    }
    if (show_headers) {
      dump_values(hname, fields, dump == "var"? "array": dump)
    }
    if ((dump == "array" || dump == "var") && reset_values) {
      array_new(reset)
      dump_values(name, reset, dump)
    }
  }
}
{
  init_fields()
  array_new(values)
  i = 1
  while (i <= fields_count) {
    values[i] = get(fields[i])
    i++
  }
  dump_values(name, values, dump)
}
END {
  init_fields()
}' -a ''
}

function cdumpcsv() { LANG=C ldumpcsv "$@"; }
function dumpcsv() { LANG=C ldumpcsv "$@"; }

################################################################################

__PRINTCSV_HELP="\
Afficher les valeurs spécifiées au format CSV

-F, --fields FIELDS
    Spécifier les champs en sortie
-o, --output OUTPUT
    Ajouter la ligne au fichier spécifié. Si le fichier n'existe pas ou est de
    taille vide, et que l'option -F est spécifiée, alors écrire les en-têtes
    dans le fichier avant d'écrire les valeurs.
--show-headers
-n, --no-headers
    Forcer l'affichage (resp. le non-affichage) des en-têtes"

function lprintcsv() {
    eval "$(utools_local)"
    local fields output show_headers=auto
    parse_opts "${PRETTYOPTS[@]}" \
        -F:,--fields: fields= \
        -o:,--output: output= \
        -n,--no-headers show_headers= \
        --show-headers show_headers=1 \
        @ args -- "$@" && set -- "${args[@]}" || die "$args"

    array_split fields "$fields" ,
    [ "$output" == - ] && output=
    [ -n "$fields" ] || show_headers=
    if [ "$show_headers" == auto ]; then
        if [ -n "$output" ]; then
            [ -s "$output" ] || show_headers=1
        else
            show_headers=
        fi
    fi
    [ -n "$output" ] || output=/dev/stdout
    values=("$@")

    stdredir "" ">>$output" "" awkrun -f fields[@] show_headers:int="$show_headers" values[@] '
BEGIN {
  if (show_headers) array_printcsv(fields)
  if (fields_count > 0) count = fields_count
  else count = values_count
  array_new(output)
  for (i = 1; i <= count; i++) {
    if (i <= values_count) output[i] = values[i]
    else output[i] = ""
  }
  array_printcsv(output)
}'
}

function cprintcsv() { LANG=C lprintcsv "$@"; }
function printcsv() { LANG=C lprintcsv "$@"; }
