1655 lines
55 KiB
Bash
1655 lines
55 KiB
Bash
##@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=value pour une chaine, ou name:int=value pour une valeur entière.
|
|
Dans la forme name=value, si la valeur ne contient que des chiffres, alors elle
|
|
est considérée comme entière.
|
|
|
|
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:
|
|
|
|
quote_value(s)
|
|
quoter une valeur pour le shell. la valeur est entourée de quotes, e.g:
|
|
quote_value(\"here, \\\"there\\\" and 'everywhere'.\")
|
|
--> 'here, \"there\" and '\\''everywhere'\\''.'
|
|
|
|
quoted_values()
|
|
quoter les valeurs \$1..\$NF pour les passer comme argument sur la ligne de
|
|
commande avec eval. e.g.:
|
|
print \"mycmd \" quoted_values()
|
|
La ligne qui est affichée pourra être évaluée avec eval dans le shell.
|
|
|
|
quote_subrepl(s)
|
|
quoter une valeur pour l'argument r des fonctions sub() et gsub(). Les
|
|
caractères suivants sont mis en échappement: \\ &
|
|
|
|
quote_grep(s)
|
|
quoter une valeur pour un pattern *simple* de grep. Les caractères suivants
|
|
sont mis en échappement: \\ . [ ^ \$ *
|
|
|
|
quote_egrep(s)
|
|
quoter une valeur pour un pattern *étendu* de grep. Les caractères suivants
|
|
sont mis en échappement: \\ . [ ^ \$ ? + * ( ) | {
|
|
|
|
quote_sql(s)
|
|
quoter une valeur pour un script sql. la valeur est entourée de quotes, e.g:
|
|
quote_sql(\"hello'there\")
|
|
--> 'hello''there'
|
|
|
|
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>
|
|
\\\\ --> \\
|
|
|
|
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_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)
|
|
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()
|
|
|
|
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, --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 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 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 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 {
|
|
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 ogeth(field, nbfields, i) {
|
|
nbfields = length(ORIGHEADERS)
|
|
if (int(field) == field) {
|
|
field = int(field)
|
|
if (field >= 1 && field <= nbfields) return field
|
|
else return 0
|
|
}
|
|
for (i = 1; i <= nbfields; i++) {
|
|
if (tolower(ORIGHEADERS[i]) == tolower(field)) {
|
|
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 = length(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 = length(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 = length(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 = length(HEADERS)
|
|
$nf = ""
|
|
}
|
|
function copyfield(field) {
|
|
set(field, oget(field))
|
|
}
|
|
function copyfields(fields, array) {
|
|
split(fields, array, /,/)
|
|
for (i = 1; i <= length(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 <= length(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 <= length(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 <= length(mapitems); i++) {
|
|
split(mapitems[i], parts, /:/)
|
|
if (length(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])
|
|
}
|
|
keepfs_count = length(keepfs)
|
|
# puis construire la liste des champs à supprimer dans skipfs
|
|
headers_count = length(HEADERS)
|
|
for (i = 1; i <= headers_count; i++) {
|
|
field = HEADERS[i]
|
|
if (!in_array(field, keepfs)) {
|
|
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)) {
|
|
array_add(addfs, field)
|
|
}
|
|
}
|
|
return length(skipfs)
|
|
}
|
|
function filterheaders(skipfs, addfs, skipfs_count, addfs_count) {
|
|
skipfs_count = length(skipfs)
|
|
for (i = skipfs_count; i >= 1; i--) {
|
|
array_deli(HEADERS, skipfs[i])
|
|
}
|
|
addfs_count = length(addfs)
|
|
for (i = 1; i <= addfs_count; i++) {
|
|
addh(addfs[i])
|
|
}
|
|
}
|
|
function filterfields(skipfs, addfs, skipfs_count) {
|
|
skipfs_count = length(skipfs)
|
|
for (i = skipfs_count; i >= 1; i--) {
|
|
deli(skipfs[i])
|
|
}
|
|
addfs_count = length(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, length(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_lines=0 parse_headers=
|
|
local autosep=
|
|
local sepconf=',"' colsep=',' qchar='"' echar=
|
|
local show_headers=1 reset_fields=
|
|
local fieldmap checkfields checkvalues keepfields skipfields
|
|
if parse_opts \
|
|
-s:,--skip-lines: skip_lines= \
|
|
-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:,--checkfields: checkfields= \
|
|
--checkvalues: checkvalues= \
|
|
-k:,--keep:,--keepfields: keepfields= \
|
|
--skip:,--skipfields: skipfields= \
|
|
--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 "$skipfields" ]; then
|
|
[ -n "$keepfields" ] || keepfields="*"
|
|
fi
|
|
if [ -n "$keepfields" ]; 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_lines" 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="$keepfields" fields2skip="$skipfields" \
|
|
"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, --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 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_lines= parse_headers=
|
|
local sepconf= colsep= qchar= echar=
|
|
local no_headers= quiet=
|
|
local awkscript=--use-default--
|
|
local append_awkscript=
|
|
if parse_opts \
|
|
-s:,--skip-lines: skip_lines= \
|
|
-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_lines" '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_lines:+-s "$skip_lines"} ${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, length(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, --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_lines=0 trim_values=1 show_headers=1
|
|
if parse_opts \
|
|
-s:,--skip-lines: skip_lines= \
|
|
-r,--no-trim trim_values= \
|
|
-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_lines" trim_values:int="$trim_values" 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, --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, --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,--parse-headers parse_headers=1 \
|
|
-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 = length(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 = length(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, length(lheaders))
|
|
else array_newsize(lfields, length(lheaders))
|
|
if (rline != "") array_parsecsv(rfields, rline, length(rheaders))
|
|
else array_newsize(rfields, length(rheaders))
|
|
if (postproc) {
|
|
if (!nocopy) copyf(lfields, rfields)
|
|
keepf(lfields, rfields)
|
|
}
|
|
linecsv = array_formatcsv(lfields)
|
|
tmplinecsv = array_formatcsv(rfields)
|
|
if (tmplinecsv != "") {
|
|
if (linecsv != "") linecsv = linecsv ","
|
|
linecsv = linecsv tmplinecsv
|
|
}
|
|
print linecsv
|
|
}
|
|
BEGIN {
|
|
if (parse_headers) {
|
|
array_parsecsv(lheaders, lheaderscsv)
|
|
lheaders_count = length(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 = length(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 = length(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 = length(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
|
|
|
|
--skip 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 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.
|
|
--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[@]}" \
|
|
--skip: skip= \
|
|
-h,--parse-headers parse_headers=1 \
|
|
--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
|
|
<"$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 "$@"; }
|