¿Cómo puedo analizar un archivo YAML desde un script de shell de Linux?
(12)
¡Acabo de escribir un analizador que llamé a Yay! (¡ Yaml no es Yamlesque! ) Que analiza Yamlesque , un pequeño subconjunto de YAML. Entonces, si estás buscando un analizador YAML 100% compatible para Bash, entonces esto no es todo. Sin embargo, para citar el OP, si desea un archivo de configuración estructurada que sea lo más fácil posible para un usuario no técnico para editar que sea similar a YAML, esto puede ser de interés.
Está share pero escribe matrices asociativas ( sí, requiere Bash 4.x ) en lugar de variables básicas. Lo hace de una manera que permite que los datos sean analizados sin conocimiento previo de las claves para que se pueda escribir un código basado en datos.
Además de los elementos de matriz de clave / valor, cada matriz tiene una matriz de keys
contiene una lista de nombres de clave, una matriz de elementos secundarios que contiene nombres de matrices secundarias y una clave principal que hace referencia a su elemento primario.
This es un ejemplo de 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
Here hay un ejemplo que muestra cómo 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
qué salidas:
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
}
}
Y here está el analizador:
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 "$@"); }
Hay alguna documentación en el archivo fuente vinculado y a continuación hay una breve explicación de lo que hace el código.
La función yay_parse
primero localiza el archivo de input
o sale con un estado de salida de 1. A continuación, determina el prefix
del conjunto de datos, explícitamente especificado o derivado del nombre del archivo.
Escribe comandos bash
válidos en su salida estándar que, si se ejecuta, define matrices que representan el contenido del archivo de datos de entrada. El primero de estos define la matriz de nivel superior:
echo "declare -g -A $prefix;"
Tenga en cuenta que las declaraciones de matriz son asociativas ( -A
), que es una característica de Bash versión 4. Las declaraciones también son globales ( -g
) por lo que se pueden ejecutar en una función pero estar disponibles para el alcance global como el yay
helper:
yay() { eval $(yay_parse "$@"); }
Los datos de entrada se procesan inicialmente con sed
. Suelta líneas que no coinciden con la especificación de formato Yamlesque antes de delimitar los campos Yamlesque válidos con un carácter separador de archivos ASCII y eliminar las comillas dobles que rodean el campo de valor.
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" |
Las dos expresiones son similares; difieren solo porque el primero selecciona los valores cotizados, ya que el segundo selecciona los que no están citados.
El Separador de archivos (28 / hex 12 / octal 034) se usa porque, como un carácter no imprimible, es poco probable que esté en los datos de entrada.
El resultado se canaliza en awk
que procesa su entrada una línea a la vez. Utiliza el carácter FS para asignar cada campo a una variable:
indent = length($1)/2;
key = $2;
value = $3;
Todas las líneas tienen una sangría (posiblemente cero) y una clave, pero no todas tienen un valor. Calcula un nivel de sangría para la línea que divide la longitud del primer campo, que contiene el espacio en blanco inicial, por dos. Los elementos de nivel superior sin ninguna sangría están en el nivel de sangría cero.
A continuación, determina qué prefix
usar para el elemento actual. Esto es lo que se agrega a un nombre de clave para crear un nombre de matriz. Hay un root_prefix
para la matriz de nivel superior que se define como el nombre del conjunto de datos y un guión bajo:
root_prefix = "''$prefix''_";
if (indent ==0 ) {
prefix = ""; parent_key = "''$prefix''";
} else {
prefix = root_prefix; parent_key = keys[indent-1];
}
La parent_key
es la clave en el nivel de sangrado por encima del nivel de sangría de la línea actual y representa la colección de la que forma parte la línea actual. Los pares clave / valor de la colección se almacenarán en una matriz con su nombre definido como la concatenación del prefix
y parent_key
.
Para el nivel superior (nivel de sangrado cero), el prefijo del conjunto de datos se usa como clave principal, por lo que no tiene prefijo (se establece en ""
). Todas las demás matrices están prefijadas con el prefijo raíz.
A continuación, la clave actual se inserta en una matriz (awk-internal) que contiene las claves. Esta matriz persiste durante toda la sesión de awk y, por lo tanto, contiene claves insertadas por líneas anteriores. La clave se inserta en la matriz usando su sangría como el índice de la matriz.
keys[indent] = key;
Debido a que este conjunto contiene claves de líneas anteriores, se eliminan todas las teclas con un nivel de sangría superior al nivel de sangría de la línea actual:
for (i in keys) {if (i > indent) {delete keys[i]}}
Esto deja la matriz de claves que contiene el llavero desde la raíz en el nivel de sangría 0 a la línea actual. Elimina las claves obsoletas que permanecen cuando la línea anterior se sangraba más profundamente que la línea actual.
La sección final emite los comandos bash
: una línea de entrada sin un valor inicia un nuevo nivel de sangría (una colección en el lenguaje YAML) y una línea de entrada con un valor agrega una clave a la colección actual.
El nombre de la colección es la concatenación del prefix
la línea actual y parent_key
.
Cuando una clave tiene un valor, una clave con ese valor se asigna a la colección actual de esta manera:
printf("%s%s[%s]=/"%s/";/n", prefix, parent_key , key, value);
printf("%s%s[keys]+=/" %s/";/n", prefix, parent_key , key);
La primera instrucción emite el comando para asignar el valor a un elemento asociativo de la matriz nombrado después de la clave y el segundo emite el comando para agregar la clave a la lista de keys
delimitadas por espacios de la colección:
<current_collection>[<key>]="<value>";
<current_collection>[keys]+=" <key>";
Cuando una clave no tiene un valor, se inicia una nueva colección como esta:
printf("%s%s[children]+=/" %s%s/";/n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;/n", root_prefix, key);
La primera instrucción genera el comando para agregar la nueva colección a la lista de elementos delimitados por espacios de la colección actual y la segunda genera el comando para declarar una nueva matriz asociativa para la nueva colección:
<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;
Todos los resultados de yay_parse
se pueden analizar como comandos bash mediante los comandos bash eval
o source
built-in.
Deseo proporcionar un archivo de configuración estructurada que sea lo más fácil posible para que un usuario no técnico lo edite (desafortunadamente tiene que ser un archivo) y por eso quería usar YAML. Sin embargo, no puedo encontrar ninguna forma de analizar esto desde un script de shell de Unix.
Aquí hay un analizador bash-only que aprovecha sed y awk para analizar archivos yaml simples:
function parse_yaml {
local prefix=$2
local s=''[[:space:]]*'' w=''[a-zA-Z0-9_]*'' fs=$(echo @|tr @ ''/034'')
sed -ne "s|^/($s/):|/1|" /
-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" $1 |
awk -F$fs ''{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
printf("%s%s%s=/"%s/"/n", "''$prefix''",vn, $2, $3);
}
}''
}
Comprende archivos tales como:
## global definitions
global:
debug: yes
verbose: no
debugging:
detailed: no
header: "debugging started"
## output
output:
file: "yes"
Que, cuando se analiza usando:
parse_yaml sample.yml
dará salida:
global_debug="yes"
global_verbose="no"
global_debugging_detailed="no"
global_debugging_header="debugging started"
output_file="yes"
también comprende archivos yaml, generados por ruby, que pueden incluir símbolos ruby, como:
---
:global:
:debug: ''yes''
:verbose: ''no''
:debugging:
:detailed: ''no''
:header: debugging started
:output: ''yes''
y producirá lo mismo que en el ejemplo anterior.
el uso típico dentro de un script es:
eval $(parse_yaml sample.yml)
parse_yaml acepta un argumento de prefijo para que todas las configuraciones importadas tengan un prefijo común (lo que reducirá el riesgo de colisiones de espacios de nombres).
parse_yaml sample.yml "CONF_"
rendimientos:
CONF_global_debug="yes"
CONF_global_verbose="no"
CONF_global_debugging_detailed="no"
CONF_global_debugging_header="debugging started"
CONF_output_file="yes"
Tenga en cuenta que las configuraciones anteriores en un archivo pueden ser referidas por configuraciones posteriores:
## global definitions
global:
debug: yes
verbose: no
debugging:
detailed: no
header: "debugging started"
## output
output:
debug: $global_debug
Otro buen uso es primero analizar un archivo predeterminado y luego la configuración del usuario, que funciona ya que la última configuración anula las primeras:
eval $(parse_yaml defaults.yml)
eval $(parse_yaml project.yml)
Dado que Python3 y PyYAML son dependencias bastante fáciles de cumplir hoy en día, lo siguiente puede ayudar:
yaml() {
python3 -c "import yaml;print(yaml.load(open(''$1''))$2)"
}
VALUE=$(yaml ~/my_yaml_file.yaml "[''a_key'']")
Difícil de decir porque depende de lo que quiere que el analizador extraiga de su documento YAML. Para casos simples, es posible que pueda usar grep
, cut
, awk
, etc. Para un análisis más complejo, necesitaría utilizar una biblioteca de análisis en toda regla, como PyYAML o YAML::Perl Python.
Es posible pasar un pequeño script a algunos intérpretes, como Python. Una forma sencilla de hacerlo con Ruby y su biblioteca YAML es la siguiente:
$ RUBY_SCRIPT="data = YAML::load(STDIN.read); puts data[''a'']; puts data[''b'']"
$ echo -e ''---/na: 1234/nb: 4321'' | ruby -ryaml -e "$RUBY_SCRIPT"
1234
4321
, donde los data
son un hash (o matriz) con los valores de yaml.
Como beneficio adicional, analizará el asunto frontal de Jekyll sin problemas .
ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)[''tags'']" example.md
He encontrado que esta herramienta Jshon es la mejor para este propósito, pero en el mundo JSON.
Pero no he encontrado rastros en Internet de tal herramienta para YAML. Deberá (al menos por ahora) usar el script Perl / Python / Ruby para hacer eso (como en las respuestas anteriores).
He escrito shyaml
en python para consultas de YAML desde la línea de comandos del shell.
Visión de conjunto:
$ pip install shyaml ## installation
Archivo YAML del ejemplo (con características complejas):
$ cat <<EOF > test.yaml
name: "MyName !!"
subvalue:
how-much: 1.1
things:
- first
- second
- third
other-things: [a, b, c]
maintainer: "Valentin Lab"
description: |
Multiline description:
Line 1
Line 2
EOF
Consulta básica:
$ cat test.yaml | shyaml get-value subvalue.maintainer
Valentin Lab
Consulta de bucle más compleja en valores complejos:
$ cat test.yaml | shyaml values-0 | /
while read -r -d $''/0'' value; do
echo "RECEIVED: ''$value''"
done
RECEIVED: ''1.1''
RECEIVED: ''- first
- second
- third''
RECEIVED: ''2''
RECEIVED: ''Valentin Lab''
RECEIVED: ''Multiline description:
Line 1
Line 2''
Algunos puntos clave:
- todos los tipos YAML y las rarezas de sintaxis se manejan correctamente, como multilínea, cadenas entre comillas, secuencias en línea ...
-
/0
salida acolchada está disponible para la manipulación de entrada sólida de líneas múltiples. - notación punteada simple para seleccionar
subvalue.maintainer
(es decir:subvalue.maintainer
es una clave válida). - el acceso por índice se proporciona a las secuencias (es decir:
subvalue.things.-1
es el último elemento de la secuenciasubvalue.things
). - acceda a todos los elementos de secuencia / estructuras de una vez para usarlos en bash loops.
- puede generar una subparte entera de un archivo YAML como ... YAML, que se combina bien para manipulaciones adicionales con shyaml.
Hay más ejemplos y documentación disponibles en la página de shyaml github o en la página de PyPI de shyaml .
Mi caso de uso puede o no ser el mismo que el que esta publicación original estaba pidiendo, pero definitivamente es similar.
Necesito incluir algunos YAML como variables de bash. El YAML nunca tendrá más de un nivel de profundidad.
YAML se ve así:
KEY: value
ANOTHER_KEY: another_value
OH_MY_SO_MANY_KEYS: yet_another_value
LAST_KEY: last_value
Salida como una dis:
KEY="value"
ANOTHER_KEY="another_value"
OH_MY_SO_MANY_KEYS="yet_another_value"
LAST_KEY="last_value"
Logré la salida con esta línea:
sed -e ''s/:[^:////]/="/g;s/$/"/g;s/ *=/=/g'' file.yaml > file.sh
-
s/:[^:////]/="/g
encuentra:
y lo reemplaza con="
, mientras ignora://
(para URL) -
s/$/"/g
agrega"
al final de cada línea -
s/ *=/=/g
elimina todos los espacios antes=
Otra opción es convertir el YAML en JSON, luego use jq para interactuar con la representación JSON, ya sea para extraer información de ella o editarla.
Escribí un script bash simple que contiene este pegamento - ver el proyecto Y2J en GitHub
Sé que esto es muy específico, pero creo que mi respuesta podría ser útil para ciertos usuarios.
Si tiene node
y npm
instalados en su máquina, puede usar js-yaml
.
Primera instalación:
npm i -g js-yaml
# or locally
npm i js-yaml
luego en tu script bash
#!/bin/bash
js-yaml your-yaml-file.yml
Además, si usas jq
puedes hacer algo como eso
#!/bin/bash
json="$(js-yaml your-yaml-file.yml)"
aproperty="$(jq ''.apropery'' <<< "$json")"
echo "$aproperty"
Porque js-yaml
convierte un archivo yaml en un literal de cadena json. Luego puede usar la cadena con cualquier analizador json en su sistema Unix.
También puede considerar el uso de Grunt (The JavaScript Task Runner). Se puede integrar fácilmente con shell. Admite la lectura de archivos YAML ( grunt.file.readYAML
) y JSON ( grunt.file.readJSON
).
Esto se puede lograr creando una tarea en Gruntfile.js
(o Gruntfile.coffee
), por ejemplo:
module.exports = function (grunt) {
grunt.registerTask(''foo'', [''load_yml'']);
grunt.registerTask(''load_yml'', function () {
var data = grunt.file.readYAML(''foo.yml'');
Object.keys(data).forEach(function (g) {
// ... switch (g) { case ''my_key'':
});
});
};
a continuación, desde el shell simplemente ejecute grunt foo
(revise grunt --help
para las tareas disponibles).
Además, puede implementar las tareas exec:foo
( grunt-exec
) con las variables de entrada pasadas desde su tarea ( foo: { cmd: ''echo bar <%= foo %>'' }
) para imprimir la salida en el formato que desee , luego canalícela en otro comando.
También hay una herramienta similar a Grunt, se llama gulp con plugin adicional gulp-yaml .
Instalar a través de: npm install --save-dev gulp-yaml
Uso de muestra:
var yaml = require(''gulp-yaml'');
gulp.src(''./src/*.yml'')
.pipe(yaml())
.pipe(gulp.dest(''./dist/''))
gulp.src(''./src/*.yml'')
.pipe(yaml({ space: 2 }))
.pipe(gulp.dest(''./dist/''))
gulp.src(''./src/*.yml'')
.pipe(yaml({ safe: true }))
.pipe(gulp.dest(''./dist/''))
Para obtener más opciones para lidiar con el formato YAML , consulte el sitio YAML para ver los proyectos, bibliotecas y otros recursos disponibles que pueden ayudarlo a analizar ese formato.
Otras herramientas:
analiza, lee y crea JSON
perl -ne ''chomp; printf qq/%s="%s"/n/, split(//s*:/s*/,$_,2)'' file.yml > file.sh