##@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 &, ", > et < 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 --> \\t --> \\0 --> \\\\ --> \\ 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 size) value = substr(value, 1, size) line = line value } print line } ' -- "$@" } function cawkcsv2fsv() { LANG=C lawkcsv2fsv "$@"; } function awkcsv2fsv() { LANG=C lawkcsv2fsv "$@"; } ################################################################################ __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" lawkrun lskip:int="$lskip" 'NR <= lskip { next } { print; exit }')" [ -n "$rheaders" ] || rheaders="$(<"$rfile" lawkrun 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" lawkrun -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" lawkrun -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" lawkrun -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" lawkrun 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" lawkrun -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" "" lawkrun -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 ou par awk -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, -w -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' -w, --awk-map Afficher les champs comme une fonction awk qui permet de rechercher une valeur et d'afficher la valeur correspondante. L'option -n permet de spécifier le nom de la fonction, qui vaut 'mapval' par défaut. --wtype TYPE Spécifier le type de fonction à générer avec l'option --awk-map. Le type par défaut est 'value' Avec le type 'value', la fonction générée est de la forme dump(inval). Cette fonction prend en argument la valeur de la colonne spécifiée avec --wscol, et si cette valeur est trouvée dans les données, retourner la valeur de la colonne spécifiée avec --wrcol Avec le type 'array', la fonction générée est de la forme dump(inval, outvs) Cette fonction prend en argument la valeur de la colonne spécifiée avec --wscol, et si cette valeur est trouvée dans les données, le tableau outvs est rempli avec les donnée de la ligne correspondante. --wscol SFIELD Nom du champ qui est cherché par la fonction générée avec l'option --awk-map La valeur par défaut est '1', c'est à dire le premier champ. --wrcol RFIELD Nom du champ dont la valeur est retournée par la fonction générée avec l'option '--awk-map --wtype value' en cas de correspondance. La valeur par défaut est '2', c'est à dire le deuxième champ. -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 local wtype wscol wrcol 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 \ -w,--awk-map dump=awk-map \ --wtype: wtype= \ --wscol: wscol= \ --wrcol: wrcol= \ -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;; awk-map) name=mapval;; esac fi case "${wtype:-value}" in array|a) wtype=array;; value|v) wtype=value;; esac [ -n "$wscol" ] || wscol=1 [ -n "$wrcol" ] || wrcol=2 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[@] \ -v wtype="$wtype" -v wscol="$wscol" -v wrcol="$wrcol" \ -e ' function init_fields() { if (do_once("init_fields")) { if (fields_count == 0) { array_copy(fields, HEADERS) fields_count = array_len(fields) } } } function before_awkmap(name, wtype) { if (do_once("before_awkmap")) { if (wtype == "value") { print "function " name "(inval) {" } else if (wtype == "array") { print "function " name "(inval, outvs) {" } } } function awkmap(inval, outval, values, count, wtype, i) { if (wtype == "value") { print "if (inval == " qawk(inval) ") return " qawk(outval) } else if (wtype == "array") { print "if (inval == " qawk(inval) ") {" print "delete outvs" for (i = 1; i <= count; i++) { print "outvs[" i "] = " qawk(values[i]) } print "return 1" print "}" } } function after_awkmap() { if (do_once("after_awkmap")) { if (wtype == "value") { print "return \"\"" } else if (wtype == "array") { print "return 0" } print "}" } } 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 before_dump() { if (do_once("before_dump")) { 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++ } if (dump == "awk-map") { before_awkmap(name, wtype) if (wscol ~ /^[0-9]+$/) inval = geti(wscol) else inval = get(wscol) if (wrcol ~ /^[0-9]+$/) outval = geti(wrcol) else outval = get(wrcol) awkmap(inval, outval, values, fields_count, wtype) } else { before_dump() dump_values(name, values, dump) } } END { if (dump == "awk-map") { before_awkmap(name, wtype) after_awkmap() } else { before_dump() } }' -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" "" lawkrun -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 "$@"; }