web-dev-qa-db-ja.com

LinuxシェルスクリプトからYAMLファイルを解析するにはどうすればよいですか?

技術に詳しくないユーザーでもできるだけ簡単に編集できる構造化された構成ファイルを提供したいので(残念ながらファイルでなければなりません)、YAMLを使用したいと考えました。ただし、これをUnixシェルスクリプトから解析する方法は見つかりません。

152
Zubair

私のユースケースは、この元の投稿が求めていたものとまったく同じ場合もそうでない場合もありますが、間違いなく似ています。

いくつかのYAMLをbash変数として取り込む必要があります。 YAMLの深さが1レベルを超えることはありません。

YAMLは次のようになります。

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

次のような出力:

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は、=の前のすべてのスペースを削除します
40

以下は、sedとawkを利用して単純なyamlファイルを解析するbash専用のパーサーです。

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"

また、Rubyシンボルを含むRubyによって生成されるyamlファイルも理解します。

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

もう1つの素晴らしい使用法は、最初にデフォルトファイルを解析し、次にユーザー設定を解析することです。後者の設定は最初の設定を上書きするため、動作します。

eval $(parse_yaml defaults.yml)
eval $(parse_yaml project.yml)
229
Stefan Farestam

シェルコマンドラインからのYAMLクエリニーズのために、pythonにshyamlを記述しました。

概要:

$ 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.-1subvalue.thingsシーケンスの最後の要素です)。
  • bashループで使用するために一度にすべてのシーケンス/構造体要素にアクセスします。
  • yAMLファイルのサブパート全体を... YAMLとして出力できます。これはshyamlとのさらなる操作のためにうまく調和します。

より多くのサンプルとドキュメントが shyaml githubページ または shyaml PyPIページ で利用可能です。

85
vaab

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

、wheredataは、yamlの値を持つハッシュ(または配列)です。

ボーナスとして、解析されます Jekyllの前件 まったく問題ありません。

Ruby -ryaml -e "puts YAML::load(open(ARGV.first).read)['tags']" example.md
31
Rafael

Python3とPyYAMLが今日満たすのに非常に簡単な依存関係であることを考えると、以下が役に立つかもしれません:

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

VALUE=$(yaml ~/my_yaml_file.yaml "['a_key']")
15
Torsten Bronger

yq は軽量でポータブルなコマンドラインYAMLプロセッサです

プロジェクトの目的は、yamlファイルの jq またはsedにすることです。

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

例として( documentation から直接盗まれた)、次のsample.yamlファイルが与えられた場合:

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

それから

yq r sample.yaml bob.*.cats

出力します

- bananas
- apples
13
bmaupin

パーサーがYAMLドキュメントから何を抽出するかによって異なるため、言うのは難しいです。単純な場合、grepcutawkなどを使用できる場合があります。より複雑な解析を行うには、Pythonの PyYAML または YAML: :Perl

11
dogbane

Yay!YamlはYamlesqueではありません!)と呼ばれるパーサーを作成し、Yamlesque、YAMLの小さなサブセット。したがって、Bashの100%準拠のYAMLパーサーを探している場合、これはそうではありません。しかし、OPを引用するために、もしあなたがYAMLのような非技術的なユーザーが編集するのが可能な限り簡単な構造化された設定ファイルを望むなら、これは興味。

以前の回答では説明されていません ですが、連想配列を書き込みます(はい、基本変数の代わりにBash 4.xが必要です)。これは、キーを事前に知らなくてもデータを解析できるようにして、データ駆動型コードを作成できるようにします。

キー/値配列要素に加えて、各配列には、キー名のリストを含むkeys配列、子配列の名前を含むchildren配列、およびその親を参照するparentキーがあります。

これ はヤムレスクの例です:

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

配列宣言は、Bashバージョン4の機能である連想(-A)であることに注意してください。宣言もグローバル(-g)であるため、関数で実行できますが、yayヘルパーのようなグローバルスコープで使用できます。

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

入力データは、最初にsedで処理されます。有効なヤムレスクフィールドを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" |

2つの式は似ています。違いは、最初の値が引用された値を選択し、2番目の値が引用されていない値を選択するためです。

File Separator (28/hex 12/octal 034)が使用されるのは、印刷できない文字として、入力データに含まれている可能性が低いためです。

結果はawkにパイプされ、入力は一度に1行ずつ処理されます。 FS 文字を使用して、各フィールドを変数に割り当てます。

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

すべての行にはインデント(場合によってはゼロ)とキーがありますが、すべての行に値はありません。先頭の空白を含む最初のフィールドの長さを2で割る行のインデントレベルを計算します。インデントのない最上位のアイテムは、インデントレベル0です。

次に、現在のアイテムに使用するprefixを決定します。これがキー名に追加されて配列名になります。データセット名とアンダースコアとして定義されるトップレベルの配列にはroot_prefixがあります:

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

parent_keyは、現在の行のインデントレベルより上のインデントレベルのキーであり、現在の行が属するコレクションを表します。コレクションのキー/値のペアは、prefixparent_keyの連結として定義された名前を持つ配列に格納されます。

最上位(インデントレベル0)では、データセットプレフィックスが親キーとして使用されるため、プレフィックスはありません(""に設定されます)。他のすべての配列には、ルートプレフィックスがプレフィックスとして付けられます。

次に、現在のキーがキーを含む(awk-internal)配列に挿入されます。この配列はawkセッション全体を通じて持続するため、前の行で挿入されたキーが含まれます。キーは、インデントを配列インデックスとして使用して配列に挿入されます。

keys[indent] = key;

この配列には前の行のキーが含まれているため、現在の行のインデントレベルよりも大きいインデントレベルを持つキーは削除されます。

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

これにより、インデントレベル0のルートから現在の行までのキーチェーンを含むキー配列が残ります。前の行が現在の行よりも深くインデントされたときに残っている古いキーを削除します。

最後のセクションはbashコマンドを出力します:値のない入力行は新しいインデントレベル(YAML用語ではcollection)を開始し、値のある入力行はキーを追加します現在のコレクション。

コレクションの名前は、現在の行のprefixparent_keyを連結したものです。

キーに値がある場合、その値を持つキーは次のように現在のコレクションに割り当てられます。

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

最初のステートメントは、キーにちなんだ名前の連想配列要素に値を割り当てるコマンドを出力し、2番目のステートメントは、コレクションのスペース区切りの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リストに新しいコレクションを追加するコマンドを出力し、2番目のステートメントは、新しいコレクションの新しい連想配列を宣言するコマンドを出力します。

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

yay_parseからのすべての出力は、bash evalまたはsource組み込みコマンドによってbashコマンドとして解析できます。

10
starfry

ここにステファン・ファレスタムの答えの拡張版があります:

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には多次元配列がないため、これを回避する方法の1つです。複数のレベルがサポートされています。 @briceburgで言及されている末尾の空白の問題を回避するには、値を一重引用符または二重引用符で囲む必要があります。ただし、まだいくつかの制限があります。辞書とリストを展開すると、値にコンマが含まれている場合に誤った結果が生じる可能性があります。また、複数行にわたる値(sshキーなど)などのより複雑な構造は(まだ)サポートされていません。

コードに関するいくつかの言葉:最初のsedコマンドは、辞書の短い形式{ key: value, ...}を通常の形式に拡張し、それらをより単純なyamlスタイルに変換します。 2番目のsed呼び出しは、リストの短い表記に対して同じことを行い、[ entry, ... ]-表記で項目化されたリストに変換します。 3番目のsed呼び出しは、通常の辞書を処理した元の呼び出しであり、現在は-とインデントを含むリストを処理するための追加があります。 awk部分は各インデントレベルにインデックスを導入し、変数名が空の場合(つまり、リストを処理する場合)にインデックスを増やします。空のvnameの代わりに、カウンターの現在の値が使用されます。 1レベル上がると、カウンターはゼロになります。

編集:私は githubリポジトリ を作成しました。

7
Martin Hecht

別のオプションは、YAMLをJSONに変換し、jqを使用してJSON表現と対話し、JSON表現から情報を抽出するか編集することです。

この接着剤を含む簡単なbashスクリプトを作成しました- GitHubのY2Jプロジェクト を参照してください

5
jonseymour
Perl -ne 'chomp; printf qq/%s="%s"\n/, split(/\s*:\s*/,$_,2)' file.yml > file.sh
4
mushuweasel

python 2とPyYAMLがある場合、 parse_yaml.py と呼ばれるこのパーサーを使用できます。洗練された機能のいくつかは、接頭辞を選択して(同様の変数を持つ複数のファイルがある場合)、yamlファイルから単一の値を選択できるようにすることです。

たとえば、これらのyamlファイルがある場合:

staging.yaml:

db:
    type: sqllite
    Host: 127.0.0.1
    user: dev
    password: password123

prod.yaml:

db:
    type: postgres
    Host: 10.0.50.100
    user: postgres
    password: password123

競合することなく両方をロードできます。

$ eval $(python parse_yaml.py prod.yaml --prefix prod --cap)
$ eval $(python parse_yaml.py staging.yaml --prefix stg --cap)
$ echo $PROD_DB_Host
10.0.50.100
$ echo $STG_DB_Host
127.0.0.1

そして、チェリーでさえあなたが望む値を選びます。

$ prod_user=$(python parse_yaml.py prod.yaml --get db_user)
$ prod_port=$(python parse_yaml.py prod.yaml --get db_port --default 5432)
$ echo prod_user
postgres
$ echo prod_port
5432
1
cangers

Golangで書かれた yqequivalent を使用できます。

./go-yg -yamlFile /home/user/dev/ansible-firefox/defaults/main.yml -key
firefox_version

戻り値:

62.0.3
1
030

これは非常に具体的ですが、私の答えは特定のユーザーに役立つと思います。
nodenpmがマシンにインストールされている場合、js-yamlを使用できます。
最初のインストール:

npm i -g js-yaml
# or locally
npm i js-yaml

その後、あなたのbashスクリプトで

#!/bin/bash
js-yaml your-yaml-file.yml

また、jqを使用している場合は、そのようなことを行うことができます

#!/bin/bash
json="$(js-yaml your-yaml-file.yml)"
aproperty="$(jq '.apropery' <<< "$json")"
echo "$aproperty"

js-yamlはyamlファイルをjson文字列リテラルに変換するためです。その後、Unixシステム内の任意のjsonパーサーで文字列を使用できます。

1
vdegenne

単一の値が必要な場合、YAMLドキュメントをJSONに変換して jq にフィードするツールを使用できます。たとえば、 yq です。

Sample.yamlのコンテンツ:

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

例:

$ yq -r '.bob["thing"]["cats"]' sample.yaml 
oranges
1

Grunt (JavaScript Task Runner)の使用も検討できます。シェルと簡単に統合できます。 YAML(grunt.file.readYAML)およびJSON(grunt.file.readJSON)ファイルの読み取りをサポートしています。

これは、Gruntfile.js(またはGruntfile.coffee)でタスクを作成することで実現できます。例:

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

};

次に、Shellからgrunt fooを実行するだけです(使用可能なタスクについてはgrunt --helpを確認してください)。

さらに、タスク(exec:foo)から渡された入力変数を使用してgrunt-execタスク(foo: { cmd: 'echo bar <%= foo %>' })を実装し、任意の形式で出力を印刷し、別のコマンドにパイプすることができます。


Gruntに似たツールもあります。これは gulp と呼ばれ、追加のプラグイン gulp-yaml です。

経由でインストール:npm install --save-dev gulp-yaml

サンプル使用法:

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/'))

YAML形式 を処理するその他のオプションについては、利用可能なプロジェクト、ライブラリ、およびその形式の解析に役立つその他のリソースについて YAMLサイト を確認してください。


その他のツール:

  • Jshon

    jSONの解析、読み取り、作成

0
kenorb