Как я могу разобрать файл YAML из сценария оболочки Linux? - PullRequest
155 голосов
/ 16 февраля 2011

Я хочу предоставить структурированный файл конфигурации, который как можно проще для нетехнического пользователя редактировать (к сожалению, это должен быть файл), и поэтому я хотел использовать YAML.Однако я не могу найти какой-либо способ синтаксического анализа этого из сценария оболочки Unix.

Ответы [ 16 ]

235 голосов
/ 17 января 2014

Вот синтаксический анализатор только для bash, который использует sed и awk для разбора простых файлов yaml:

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);
      }
   }'
}

Он понимает такие файлы, как:

## global definitions
global:
  debug: yes
  verbose: no
  debugging:
    detailed: no
    header: "debugging started"

## output
output:
   file: "yes"

Который, когда анализируется с использованием:

parse_yaml sample.yml

выведет:

global_debug="yes"
global_verbose="no"
global_debugging_detailed="no"
global_debugging_header="debugging started"
output_file="yes"

, он также понимает файлы yaml, сгенерированные ruby, которые могут содержать символы ruby, например:

---
:global:
  :debug: 'yes'
  :verbose: 'no'
  :debugging:
    :detailed: 'no'
    :header: debugging started
  :output: 'yes'

, и будетвыведите то же, что и в предыдущем примере.

типичное использование в скрипте:

eval $(parse_yaml sample.yml)

parse_yaml принимает аргумент префикса, так что все импортированные настройки имеют общий префикс (который уменьшаетриск столкновений в пространстве имен).

parse_yaml sample.yml "CONF_"

приводит к:

CONF_global_debug="yes"
CONF_global_verbose="no"
CONF_global_debugging_detailed="no"
CONF_global_debugging_header="debugging started"
CONF_output_file="yes"

Обратите внимание, что к предыдущим настройкам в файле могут обращаться более поздние настройки:

## global definitions
global:
  debug: yes
  verbose: no
  debugging:
    detailed: no
    header: "debugging started"

## output
output:
   debug: $global_debug

Еще одно полезное использование - сначала проанализировать файл по умолчанию, а затем пользовательские настройки, которые работают, поскольку последние переопределяют первые:

eval $(parse_yaml defaults.yml)
eval $(parse_yaml project.yml)
87 голосов
/ 27 февраля 2013

Я написал shyaml в python для запросов YAML из командной строки оболочки.

Обзор:

$ pip install shyaml      ## installation

Пример файла YAML (со сложными функциями):

$ 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

Базовый запрос:

$ cat test.yaml | shyaml get-value subvalue.maintainer
Valentin Lab

Более сложный циклический запрос для комплексных значений:

$ 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'

Несколько ключевых моментов:

  • все типы YAML и странности синтаксиса правильно обрабатываются, как многострочные, строки в кавычках, строковые последовательности ...
  • \0 дополняемый вывод доступен для манипулирования сплошными многострочными записями.
  • простая пунктирная запись для выбора подзначений (т. Е. subvalue.maintainer является действительным ключом).
  • доступ по индексу предоставляется последовательностям (т. Е.: subvalue.things.-1 является последним элементомsubvalue.things sequence.)
  • доступ ко всем элементам последовательности / структур за один раз для использования в циклах bash.
  • вы можете вывести всю часть файла YAML как ... YAML,которые хорошо сочетаются для дальнейших манипуляций с shyaml.

Дополнительные образцы и документацию доступны на странице shyaml github или на странице shyaml PyPI .

41 голосов
/ 01 ноября 2012

Мой вариант использования может совпадать или не совпадать с тем, о чем просил этот оригинальный пост, но он определенно похож.

Мне нужно добавить некоторые YAML в качестве переменных bash. YAML никогда не будет глубиной более одного уровня.

YAML выглядит так:

KEY:                value
ANOTHER_KEY:        another_value
OH_MY_SO_MANY_KEYS: yet_another_value
LAST_KEY:           last_value

Вывод как a-dis:

KEY="value"
ANOTHER_KEY="another_value"
OH_MY_SO_MANY_KEYS="yet_another_value"
LAST_KEY="last_value"

Я добился результата с помощью этой строки:

sed -e 's/:[^:\/\/]/="/g;s/$/"/g;s/ *=/=/g' file.yaml > file.sh
  • s/:[^:\/\/]/="/g находит : и заменяет его на =", игнорируя :// (для URL-адресов)
  • s/$/"/g добавляет " в конец каждой строки
  • s/ *=/=/g удаляет все пробелы до =
31 голосов
/ 12 февраля 2012

Возможно передать небольшой скрипт некоторым интерпретаторам, например, Python. Простой способ сделать это с помощью Ruby и его библиотеки YAML заключается в следующем:

$ 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

, где data - хеш (или массив) со значениями из yaml.

В качестве бонуса, он проанализирует Фронтальный вопрос Джекилла очень хорошо.

ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)['tags']" example.md
16 голосов
/ 13 декабря 2017

Учитывая, что Python3 и PyYAML в настоящее время представляют собой довольно простые зависимости, может помочь следующее:

yaml() {
    python3 -c "import yaml;print(yaml.load(open('$1'))$2)"
}

VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")
15 голосов
/ 19 февраля 2019

yq - это легкий и портативный YAML-процессор командной строки

Цель проекта - создать jq или sed файлов yaml.

(http://mikefarah.github.io/yq/)

В качестве примера (украдено прямо из документации ), для файла sample.yaml:

---
bob:
  item1:
    cats: bananas
  item2:
    cats: apples

затем

yq r sample.yaml bob.*.cats

выдаст

- bananas
- apples
11 голосов
/ 16 февраля 2011

Трудно сказать, потому что это зависит от того, что вы хотите, чтобы синтаксический анализатор извлек из вашего документа YAML. В простых случаях вы можете использовать grep, cut, awk и т. Д. Для более сложного анализа вам потребуется использовать полнофункциональную библиотеку синтаксического анализа, такую ​​как Python PyYAML или YAML :: Perl .

10 голосов
/ 29 июля 2015

Я только что написал парсер, который назвал Yay! ( Yaml - не Yamlesque! ), который анализирует Yamlesque , небольшое подмножество YAML. Так что, если вы ищете 100% совместимый YAML-парсер для Bash, то это не так. Однако, если процитировать OP, если вы хотите структурированный файл конфигурации, который как можно проще для нетехнического пользователя редактировать , который похож на YAML, это может представлять интерес.

Это , вдохновленное более ранним ответом , но записывает ассоциативные массивы ( да, для этого требуется Bash 4.x ) вместо базовых переменных. Это делается таким образом, что позволяет анализировать данные без предварительного знания ключей, чтобы можно было писать код, управляемый данными.

Как и элементы массива ключ / значение, каждый массив имеет массив keys, содержащий список имен ключей, массив children, содержащий имена дочерних массивов, и ключ parent, который ссылается на его родителя.

Этот является примером 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

Здесь - пример, показывающий, как его использовать:

#!/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

который выводит:

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
  }
}

И здесь - это синтаксический анализатор:

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 "$@"); }

В связанном исходном файле есть некоторая документация, и ниже приведено краткое объяснение того, что делает код.

Функция yay_parse сначала находит файл input или завершает работу со статусом выхода 1. Затем она определяет набор данных prefix, явно указанный или полученный из имени файла.

Он записывает действительные команды bash в свой стандартный вывод, который, если выполняется, определяет массивы, представляющие содержимое файла входных данных. Первый из них определяет массив верхнего уровня:

echo "declare -g -A $prefix;"

Обратите внимание, что объявления массивов являются ассоциативными (-A), что является особенностью Bash версии 4. Объявления также являются глобальными (-g), поэтому они могут выполняться в функции, но быть доступными для глобальной области видимости, например yay помощник:

yay() { eval $(yay_parse "$@"); }

Входные данные первоначально обрабатываются с sed. Он удаляет строки, которые не соответствуют спецификации формата Yamlesque, перед тем, как разграничить допустимые поля Yamlesque символом ASCII File Separator и удалить все двойные кавычки, окружающие поле значения.

 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" |

Два выражения похожи; они отличаются только тем, что первый выбирает указанные значения, тогда как второй выбирает не заключенные в кавычки.

Разделитель файлов (28 / шестнадцатеричный 12 / восьмеричный 034) используется потому, что, будучи непечатным символом, он вряд ли будет во входных данных.

Результат передается в awk, который обрабатывает входные данные по одной строке за раз. Он использует символ FS для назначения каждого поля переменной:

indent       = length($1)/2;
key          = $2;
value        = $3;

У всех строк есть отступ (возможно, ноль) и ключ, но не у всех есть значение. Он вычисляет уровень отступа для строки, разделяющей длину первого поля, которое содержит начальный пробел, на два. Элементы верхнего уровня без отступа находятся на нулевом уровне отступа.

Далее выясняется, что prefix использовать для текущего элемента. Это то, что добавляется к имени ключа для создания имени массива. Для массива верхнего уровня есть root_prefix, который определяется как имя набора данных и подчеркивание:

root_prefix  = "'$prefix'_";
if (indent ==0 ) {
  prefix = "";          parent_key = "'$prefix'";
} else {
  prefix = root_prefix; parent_key = keys[indent-1];
}

parent_key - это ключ на уровне отступа над уровнем отступа текущей строки и представляет коллекцию, частью которой является текущая строка. Пары ключ / значение коллекции будут храниться в массиве, имя которого определено как объединение prefix и parent_key.

Для верхнего уровня (нулевого уровня отступа) префикс набора данных используется в качестве родительского ключа, поэтому у него нет префикса (он установлен на ""). Все остальные массивы имеют префикс root.

Затем текущий ключ вставляется в (awk-internal) массив, содержащий ключи.Этот массив сохраняется в течение всего сеанса awk и поэтому содержит ключи, вставленные предыдущими строками.Ключ вставляется в массив, используя его отступ в качестве индекса массива.

keys[indent] = key;

Поскольку этот массив содержит ключи из предыдущих строк, удаляются все ключи с уровнем отступа, меньшим, чем уровень отступа текущей строки:

 for (i in keys) {if (i > indent) {delete keys[i]}}

В результате получается массив ключей, содержащийцепочка ключей от корня на уровне отступа 0 до текущей строки.Он удаляет устаревшие ключи, которые остаются, когда предыдущая строка была смещена глубже текущей строки.

В последнем разделе выводятся команды bash: строка ввода без значения начинает новый уровень отступа ( collection на языке YAML), а строка ввода со значением добавляет ключк текущей коллекции.

Имя коллекции - это объединение prefix и parent_key текущей строки в текущей строке. Когда ключ имеет значение, ключ с этим значением назначается текущей коллекции следующим образом.:

printf("%s%s[%s]=\"%s\";\n", prefix, parent_key , key, value);
printf("%s%s[keys]+=\" %s\";\n", prefix, parent_key , key);

Первый оператор выводит команду для присвоения значения элементу ассоциативного массива, названного в честь ключа, а второй выводит команду для добавления ключа в разделенный пробелами список keys:

<current_collection>[<key>]="<value>";
<current_collection>[keys]+=" <key>";

Когда ключ не имеет значения, новая коллекция запускается следующим образом:

printf("%s%s[children]+=\" %s%s\";\n", prefix, parent_key , root_prefix, key);
printf("declare -g -A %s%s;\n", root_prefix, key);

Первый оператор выводит команду для добавления новой коллекции всписок children разделенной пробелами текущей коллекции, а второй выводит команду для объявления нового ассоциативного массива для новой коллекции:

<current_collection>[children]+=" <new_collection>"
declare -g -A <new_collection>;

Все выходные данные из yay_parse могут быть проанализированы как команды bashвстроенными командами bash eval или source.

7 голосов
/ 10 августа 2018

вот расширенная версия ответа Стефана Фарестама:

function parse_yaml {
   local prefix=$2
   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
   sed -ne "s|,$s\]$s\$|]|" \
        -e ":1;s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: [\3]\n\1  - \4|;t1" \
        -e "s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s\]|\1\2:\n\1  - \3|;p" $1 | \
   sed -ne "s|,$s}$s\$|}|" \
        -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1  \3: \4|;t1" \
        -e    "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1  \2|;p" | \
   sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p" \
        -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|p" \
        -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" | \
   awk -F$fs '{
      indent = length($1)/2;
      vname[indent] = $2;
      for (i in vname) {if (i > indent) {delete vname[i]; idx[i]=0}}
      if(length($2)== 0){  vname[indent]= ++idx[indent] };
      if (length($3) > 0) {
         vn=""; for (i=0; i<indent; i++) { vn=(vn)(vname[i])("_")}
         printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, vname[indent], $3);
      }
   }'
}

Эта версия поддерживает нотацию - и краткую нотацию для словарей и списков. Следующий вход:

global:
  input:
    - "main.c"
    - "main.h"
  flags: [ "-O3", "-fpic" ]
  sample_input:
    -  { property1: value, property2: "value2" }
    -  { property1: "value3", property2: 'value 4' }

производит этот вывод:

global_input_1="main.c"
global_input_2="main.h"
global_flags_1="-O3"
global_flags_2="-fpic"
global_sample_input_1_property1="value"
global_sample_input_1_property2="value2"
global_sample_input_2_property1="value3"
global_sample_input_2_property2="value 4"

Как видите, элементы - автоматически нумеруются, чтобы получить разные имена переменных для каждого элемента. В bash нет многомерных массивов, так что это один из способов обхода. Несколько уровней поддерживаются. Чтобы обойти проблему с конечными пробелами, упомянутую @briceburg, нужно заключить значения в одинарные или двойные кавычки. Тем не менее, есть некоторые ограничения: Расширение словарей и списков может давать неправильные результаты, если значения содержат запятые. Кроме того, более сложные структуры, такие как значения, охватывающие несколько строк (например, ssh-ключи), (пока) не поддерживаются.

Несколько слов о коде: первая команда sed расширяет краткую форму словарей { key: value, ...} до обычных и преобразует их в более простой стиль yaml. Второй вызов sed делает то же самое для краткой записи списков и преобразует [ entry, ... ] в подробный список с записью -. Третий вызов sed - это исходный вызов, который обрабатывает обычные словари, теперь с добавлением обработки списков с - и отступами. Часть awk вводит индекс для каждого уровня отступа и увеличивает его, когда имя переменной пустое (т.е. при обработке списка). Текущее значение счетчиков используется вместо пустого vname. При подъеме на один уровень счетчики обнуляются.

Редактировать: для этого я создал репозиторий github .

5 голосов
/ 02 августа 2015

Другой вариант - преобразовать YAML в JSON, а затем использовать jq для взаимодействия с представлением JSON либо для извлечения из него информации, либо для ее редактирования.

Я написал простой bash-скрипт, который содержит этот клей - см. Проект Y2J на GitHub

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...