Ho appena scritto un parser che ho chiamato Yay! ( Yaml non è Yamlesque! ) Che analizza Yamlesque , un piccolo sottoinsieme di YAML. Quindi, se stai cercando un parser YAML conforme al 100% per Bash, non è questo. Tuttavia, per citare l'OP, se si desidera modificare un file di configurazione strutturato il più semplice possibile per un utente non tecnico simile a YAML, ciò può essere interessante.
È ispirato dalla risposta precedente ma scrive array associativi ( sì, richiede Bash 4.x ) invece di variabili di base. Lo fa in un modo che consente di analizzare i dati senza una conoscenza preliminare delle chiavi in modo da poter scrivere il codice basato sui dati.
Oltre agli elementi array chiave / valore, ogni array ha un keys
array contenente un elenco di nomi chiave, un children
array contenente nomi di array figlio e una parent
chiave che fa riferimento al suo genitore.
Questo è un esempio di Yamlesque:
root_key1: this is value one
root_key2: "this is value two"
drink:
state: liquid
coffee:
best_served: hot
colour: brown
orange_juice:
best_served: cold
colour: orange
food:
state: solid
apple_pie:
best_served: warm
root_key_3: this is value three
Ecco un esempio che mostra come usarlo:
#!/bin/bash
# An example showing how to use Yay
. /usr/lib/yay
# helper to get array value at key
value() { eval echo \${$1[$2]}; }
# print a data collection
print_collection() {
for k in $(value $1 keys)
do
echo "$2$k = $(value $1 $k)"
done
for c in $(value $1 children)
do
echo -e "$2$c\n$2{"
print_collection $c " $2"
echo "$2}"
done
}
yay example
print_collection example
che produce:
root_key1 = this is value one
root_key2 = this is value two
root_key_3 = this is value three
example_drink
{
state = liquid
example_coffee
{
best_served = hot
colour = brown
}
example_orange_juice
{
best_served = cold
colour = orange
}
}
example_food
{
state = solid
example_apple_pie
{
best_served = warm
}
}
Ed ecco il parser:
yay_parse() {
# find input file
for f in "$1" "$1.yay" "$1.yml"
do
[[ -f "$f" ]] && input="$f" && break
done
[[ -z "$input" ]] && exit 1
# use given dataset prefix or imply from file name
[[ -n "$2" ]] && local prefix="$2" || {
local prefix=$(basename "$input"); prefix=${prefix%.*}
}
echo "declare -g -A $prefix;"
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -n -e "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$input" |
awk -F$fs '{
indent = length($1)/2;
key = $2;
value = $3;
# No prefix or parent for the top level (indent zero)
root_prefix = "'$prefix'_";
if (indent ==0 ) {
prefix = ""; parent_key = "'$prefix'";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
keys[indent] = key;
# remove keys left behind if prior row was indented more than this row
for (i in keys) {if (i > indent) {delete keys[i]}}
if (length(value) > 0) {
# value
printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
} else {
# collection
printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);
printf("%s%s[parent]=\"%s%s\";\n", root_prefix, key, prefix, parent_key);
}
}'
}
# helper to load yay data file
yay() { eval $(yay_parse "$@"); }
C'è della documentazione nel file sorgente collegato e di seguito è una breve spiegazione di ciò che fa il codice.
La yay_parse
funzione individua innanzitutto il input
file o esce con uno stato di uscita di 1. Successivamente, determina il set di dati prefix
, esplicitamente specificato o derivato dal nome del file.
Scrive bash
comandi validi nel suo output standard che, se eseguiti, definiscono array che rappresentano il contenuto del file di dati di input. Il primo di questi definisce l'array di primo livello:
echo "declare -g -A $prefix;"
Si noti che le dichiarazioni di array sono associative ( -A
) che è una caratteristica di Bash versione 4. Le dichiarazioni sono anche globali ( -g
), quindi possono essere eseguite in una funzione ma essere disponibili nell'ambito globale come l' yay
helper:
yay() { eval $(yay_parse "$@"); }
I dati di input vengono inizialmente elaborati con sed
. Elimina le righe che non corrispondono alle specifiche del formato Yamlesque prima di delimitare i campi Yamlesque validi con un carattere di separatore di file ASCII e rimuovere le virgolette doppie che circondano il campo valore.
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -n -e "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$input" |
Le due espressioni sono simili; differiscono solo perché il primo seleziona i valori tra virgolette dove il secondo seleziona quelli non quotati.
Il separatore di file (28 / hex 12 / ottale 034) viene utilizzato perché, come carattere non stampabile, è improbabile che si trovi nei dati di input.
Il risultato viene convogliato in awk
cui elabora il suo input una riga alla volta. Utilizza il carattere FS per assegnare ciascun campo a una variabile:
indent = length($1)/2;
key = $2;
value = $3;
Tutte le righe hanno un rientro (possibilmente zero) e una chiave ma non tutte hanno un valore. Calcola un livello di rientro per la linea che divide la lunghezza del primo campo, che contiene lo spazio bianco iniziale, per due. Gli elementi di livello superiore senza alcun rientro sono al livello di rientro zero.
Successivamente, determina cosa prefix
utilizzare per l'elemento corrente. Questo è ciò che viene aggiunto al nome di una chiave per creare un nome di array. Esiste un root_prefix
array di livello superiore che è definito come il nome del set di dati e un trattino basso:
root_prefix = "'$prefix'_";
if (indent ==0 ) {
prefix = ""; parent_key = "'$prefix'";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
La parent_key
è la chiave al livello di rientro sopra livello di rientro della riga corrente e rappresenta la raccolta che la linea corrente appartiene. Le coppie chiave / valore della raccolta verranno archiviate in un array con il nome definito come concatenazione di prefix
e parent_key
.
Per il livello superiore (rientro livello zero) il prefisso del set di dati viene utilizzato come chiave principale, quindi non ha prefisso (è impostato su ""
). Tutti gli altri array hanno il prefisso root.
Successivamente, la chiave corrente viene inserita in un array (interno-awk) contenente le chiavi. Questo array persiste per tutta la sessione di awk e quindi contiene chiavi inserite da righe precedenti. La chiave viene inserita nell'array usando il suo rientro come indice dell'array.
keys[indent] = key;
Poiché questo array contiene chiavi delle righe precedenti, tutte le chiavi con una grattugia di livello di rientro rispetto al livello di rientro della riga corrente vengono rimosse:
for (i in keys) {if (i > indent) {delete keys[i]}}
Ciò lascia l'array di chiavi contenente la catena chiave dalla radice al livello di rientro 0 alla riga corrente. Rimuove le chiavi obsolete che rimangono quando la riga precedente è stata rientrata più in profondità rispetto alla riga corrente.
La sezione finale genera i bash
comandi: una riga di input senza un valore avvia un nuovo livello di rientro (una raccolta in linguaggio YAML) e una riga di input con un valore aggiunge una chiave alla raccolta corrente.
Il nome della collezione è la concatenazione della linea corrente prefix
e parent_key
.
Quando una chiave ha un valore, una chiave con quel valore viene assegnata alla raccolta corrente in questo modo:
printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);
La prima istruzione genera il comando per assegnare il valore a un elemento di array associativo che prende il nome dalla chiave e la seconda genera il comando per aggiungere la chiave keys
all'elenco delimitato da spazi della raccolta :
<current_collection>[<key>]="<value>";
<current_collection>[keys]+=" <key>";
Quando una chiave non ha un valore, viene avviata una nuova raccolta in questo modo:
printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);
La prima istruzione genera il comando per aggiungere la nuova raccolta children
all'elenco delimitato da spazi della raccolta corrente e la seconda emette il comando per dichiarare una nuova matrice associativa per la nuova raccolta:
<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;
Tutto l'output di yay_parse
può essere analizzato come comandi bash dai comandi bash eval
o source
integrati.