Как преобразовать ответ JSON в YAML в Bash

Я читаю данные из файла json с помощью jq. Я хочу добавить результаты в файл yaml, но у меня не получается. Я довольно новичок в программировании оболочки. Моя цель - добавить эти "пользователи" к существующему массиву "пользователи" в файле yaml.

Это мой файл JSON:

#$DEFAULTS_FILE

{"users":
  [
    {"name":"pi",
      "gecos": "Hypriot Pirate",
      "sudo":"ALL=(ALL) NOPASSWD:ALL",
      "shell": "/bin/bash",
      "groups":"users,docker,video",
      "plain_text_passwd":"pi",
      "lock_passwd":"false",
      "ssh_pwauth":"true",
      "chpasswd": {"expire": false}
    },
    {"name":"admin",
      "gecos": "Hypriot Pirate",
      "sudo":"ALL=(ALL) NOPASSWD:ALL",
      "shell": "/bin/bash",
      "primary-group": "users",
      "groups":"users,docker,adm,dialout,audio,plugdev,netdev,video",
      "ssh-import-id":"None",
      "plain_text_passwd":"pi",
      "lock_passwd":"true",
      "ssh_pwauth":"true",
      "chpasswd": "{expire: false}",
      "ssh-authorized-keys": ["ssh-rsa abcdefg1234567890 YOUR_KEY@YOURHOST.local"]
    }
  ]
  }

Я фильтрую это с этим:

cat $DEFAULTS_FILE | jq .users

Я понятия не имею, как преобразовать этот JSON в YAML.

Мой ожидаемый результат должен быть:

users:
  - name:                pi
    gecos:               "Hypriot Pirate"
    sudo:                ALL=(ALL) NOPASSWD:ALL
    shell:               /bin/bash
    groups:              users,docker,video
    plain_text_passwd:   pi
    lock_passwd:         false
    ssh_pwauth:          true
    chpasswd: { expire:  false }
  - name:                admin
    primary-group:       users
    shell:               /bin/bash
    sudo:                ALL=(ALL) NOPASSWD:ALL
    groups:              users,docker,adm,dialout,audio,plugdev,netdev,video
    ssh-import-id:       None

Я пытался использовать второй инструмент под названием yq который похож на jq и может писать файлы yaml. Но у меня нет положительного прогресса.

РЕДАКТИРОВАТЬ

Я знаю, что могу добавить контент в yaml с этим:

yq w -i "my.yml" "users[+]" "some content"

Но я не знаю, как слить мой JSON в это.

Любая помощь или подсказка были бы хороши, спасибо заранее...

13 ответов

С участием yq version 3.3.2:

cat $DEFAULTS_FILE | yq r -P -

yq обертка yaml для jq

r читать

-P --prettyPrint

- из STDIN

function yaml_validate {
  python -c 'import sys, yaml, json; yaml.safe_load(sys.stdin.read())'
}

function yaml2json {
  python -c 'import sys, yaml, json; print(json.dumps(yaml.safe_load(sys.stdin.read())))'
}

function yaml2json_pretty {
  python -c 'import sys, yaml, json; print(json.dumps(yaml.safe_load(sys.stdin.read()), indent=2, sort_keys=False))'
}

function json_validate {
  python -c 'import sys, yaml, json; json.loads(sys.stdin.read())'
}

function json2yaml {
  python -c 'import sys, yaml, json; print(yaml.dump(json.loads(sys.stdin.read())))'
}

Дополнительные трюки Bash на http://github.com/frgomes/bash-scripts

Я не уверен, какие правила вы используете, чтобы получить ожидаемый результат. Кажется, что вы случайным образом применяете разные правила для преобразования значений.

Насколько я понимаю, скалярные значения просто выводятся как есть (с потенциальным кодированием), объекты выводятся в виде пар ключ / значение, а объекты массива выводятся с - за каждый предмет. Отступы связывают то, что является частью чего.

На основании этих правил, если вы собираетесь использовать jq:

def yamlify:
    (objects | to_entries[] | (.value | type) as $type |
        if $type == "array" then
            "\(.key):", (.value | yamlify)
        elif $type == "object" then
            "\(.key):", "    \(.value | yamlify)"
        else
            "\(.key):\t\(.value)"
        end
    )
    // (arrays | select(length > 0)[] | [yamlify] |
        "  - \(.[0])", "    \(.[1:][])"
    )
    // .
    ;

Затем, чтобы использовать его, добавьте его в свой .jq файл и использовать его:

$ jq -r yamlify input.json
users:
  - name:       pi
    gecos:      Hypriot Pirate
    sudo:       ALL=(ALL) NOPASSWD:ALL
    shell:      /bin/bash
    groups:     users,docker,video
    plain_text_passwd:  pi
    lock_passwd:        false
    ssh_pwauth: true
    chpasswd:
        expire: false
  - name:       admin
    gecos:      Hypriot Pirate
    sudo:       ALL=(ALL) NOPASSWD:ALL
    shell:      /bin/bash
    primary-group:      users
    groups:     users,docker,adm,dialout,audio,plugdev,netdev,video
    ssh-import-id:      None
    plain_text_passwd:  pi
    lock_passwd:        true
    ssh_pwauth: true
    chpasswd:   {expire: false}
    ssh-authorized-keys:
      - ssh-rsa abcdefg1234567890 YOUR_KEY@YOURHOST.local

Вот еще один вариант, который выравнивает значения

def yamlify2:
    (objects | to_entries | (map(.key | length) | max + 2) as $w |
        .[] | (.value | type) as $type |
        if $type == "array" then
            "\(.key):", (.value | yamlify2)
        elif $type == "object" then
            "\(.key):", "    \(.value | yamlify2)"
        else
            "\(.key):\(" " * (.key | $w - length))\(.value)"
        end
    )
    // (arrays | select(length > 0)[] | [yamlify2] |
        "  - \(.[0])", "    \(.[1:][])"
    )
    // .
    ;
$ jq -r yamlify2 input.json
users:
  - name:               pi
    gecos:              Hypriot Pirate
    sudo:               ALL=(ALL) NOPASSWD:ALL
    shell:              /bin/bash
    groups:             users,docker,video
    plain_text_passwd:  pi
    lock_passwd:        false
    ssh_pwauth:         true
    chpasswd:
        expire:  false
  - name:                 admin
    gecos:                Hypriot Pirate
    sudo:                 ALL=(ALL) NOPASSWD:ALL
    shell:                /bin/bash
    primary-group:        users
    groups:               users,docker,adm,dialout,audio,plugdev,netdev,video
    ssh-import-id:        None
    plain_text_passwd:    pi
    lock_passwd:          true
    ssh_pwauth:           true
    chpasswd:             {expire: false}
    ssh-authorized-keys:
      - ssh-rsa abcdefg1234567890 YOUR_KEY@YOURHOST.local

yq eval -P

с участием mikefarah/yq версия 4.0 (выпущена в декабре 2020 г.), устанавливается через большинство менеджеров пакетов Unix-подобных ОС: через Homebrew для macOS ( brew install yq), Debian с apt ( apt install yq), Альпийский с apk ( apk add yq), так далее.

См. Работа с JSON.

Чтобы читать в json, просто передайте файл json вместо yaml, он просто будет работать - поскольку json является подмножеством yaml. Однако вы, вероятно, захотите использовать Оператор стиля или --prettyPrint/-P flag, чтобы он больше походил на идиоматический документ yaml.

Я предлагаю использовать yq с участием -y вариант

$ pip3 install yq # requires jq

$ cat in.json | yq -y
users:
  - name: pi
    gecos: Hypriot Pirate
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    groups: users,docker,video
    plain_text_passwd: pi
    lock_passwd: 'false'
    ssh_pwauth: 'true'
    chpasswd:
      expire: false
  - name: admin
    gecos: Hypriot Pirate
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    primary-group: users
    groups: users,docker,adm,dialout,audio,plugdev,netdev,video
    ssh-import-id: None
    plain_text_passwd: pi
    lock_passwd: 'true'
    ssh_pwauth: 'true'
    chpasswd: '{expire: false}'
    ssh-authorized-keys:
      - ssh-rsa abcdefg1234567890 YOUR_KEY@YOURHOST.local

Я использовал Ruby, чтобы записать свой json-контент в yaml.

Что касается вашего примера, это можно сделать так:

cat $DEFAULTS_FILE | jq .users | ruby -ryaml -rjson -e 'puts YAML.dump(JSON.parse(STDIN.read))' > my.yml

Еще один лайнер:

python -c 'import yaml, sys; print(yaml.dump(yaml.load(open(sys.argv[1])), default_flow_style=False))' input.json

(используя тот факт, что действительный json также действителен yaml)

И yaml для json:

python -c 'import yaml, json, sys; print(json.dumps(yaml.load(open(sys.argv[1])), indent=2))' input.yaml

Решение в jq (без других инструментов)

На основе кода @Jeff Mercado в этом посте я добавил поддержку многострочных строк и экранирование одинарных кавычек.

      # purpose: converts Json to Yaml
# remarks:
#   You can use 'yq -y' to convert json to yaml, but ...
#     * this function can be used several times within a single jq program
#     * this function may be faster than using yq
#     * maybe yq is not available in your environment
#
# input: any Json
# output: json converted to yaml
def toYaml:
   def handleMultilineString($level):
      reduce ([match("\n+"; "g")]                       # find groups of '\n'
              | sort_by(-.offset))[] as $match
             (.; .[0:$match.offset + $match.length] +
                 "\n\("    " * $level)" +               # add one extra '\n' for every group of '\n's. Add indention for each new line
                 .[$match.offset + $match.length:]);

   def toYamlString($level):
      if type == "string"
      then handleMultilineString($level)
           | sub("'"; "''"; "g")           # escape single quotes
           | "'\(.)'"                      # wrap in single quotes
      else .
      end;

   def _toYaml($level):
      (objects | to_entries[] |
          if (.value | type) == "array" then
              "\(.key):", (.value | _toYaml($level))
          elif (.value | type) == "object" then
              "\(.key):", "\("    ")\(.value | _toYaml($level))"
          else
              "\(.key): \(.value | toYamlString($level))"
          end
      )
      // (arrays | select(length > 0)[] | [_toYaml($level)] |
          "  - \(.[0])", "\("    ")\(.[1:][])"
      )
      // .;

   _toYaml(1);

Пример использования

File 'containsMultilineStrings.json'


jq -r 'toYaml' < containsMultilineStrings.json

      response:
    code: 200
    message: 'greeting

    that''s all folks



    '

jq -r 'toYaml' containsMultilineStrings.json | yq (поездка в оба конца)

      {
  "response": {
    "code": 200,
    "message": "greeting\nthat's all folks\n\n\n"
  }
}

Контрольная работа

Вы можете проверить правильность функции, преобразовав json в yaml, а затем обратно в json с помощью yq.

FILE='containsMultilineStrings.json'; diff <(cat "$FILE") <(jq -r 'toYaml' $FILE | yq)

Представление

Быстрый тест показывает сокращение времени выполнения функции toYamlпо сравнению с использованием yq. На своем компьютере я измерил:

time for i in {1..100}; do yq -y > /dev/null < containsMultilineStrings.json; done

8.4 с.

time for i in {1..100}; do jq -r 'toYaml' > /dev/null containsMultilineStrings.json; done

3,4 с

Другой вариант — использовать gojq. Это порт jq с поддержкой чтения и записи yaml. Его можно установить с помощью выпусков GitHub, доморощенного и нулевой установки. Команда для вашего вопроса будет:

      cat test.json | gojq --yaml-output > test.yaml

Вы можете использовать потрясающую библиотекуyq

      # install yq on macOS
brew install yq

Затем выполните следующую команду

      cat file.json | yq -P > file.yml

Вы также можете преобразовать и сохранить ответ JSON в файл YAML, например:

      curl https://raw.githubusercontent.com/typicode/demo/master/db.json | yq -P > example.yml

кот sample.json | yq -P

По состоянию на 2023 год — ответ @ianmunoz верен

Исходный файл

      cat sample.json
[
  {"name": "Abc", "id": "10"},
  {"name": "Def", "id": "11"},
  {"name": "Xyz", "id": "12"}
]

Как Ямл

      cat sample.json | yq -P
- name: Abc
  id: "10"
- name: Def
  id: "11"
- name: Xyz
  id: "12"

Как CSV

      cat sample.json | yq e '.[0, 1] | [key+1, .id, .name] | @csv'
1,10,Abc
2,11,Def

Примечание. Вы можете извлечь диапазон записей с помощью[0, 1]и добавьтеkeyна выходе.

В итоге я установил гем и использовал ruby:

gem install deep_merge ( https://github.com/danielsdeleo/deep_merge)

Вот мой подход:

#!/usr/bin/env ruby
#
require 'json'
require 'yaml'
require 'deep_merge/rails_compat'

json_input_file = ARGF.argv[0]
yaml_output_file = ARGF.argv[1]
scope = ARGF.argv[2]

json = File.read(json_input_file)
yaml = File.read(yaml_output_file)

json_data = JSON.parse(json)
scoped_result = json_data[scope]

old_values_hash = YAML.load(File.read(yaml_output_file))
result = YAML.parse(yaml)
merged = old_values_hash.deeper_merge scoped_result

File.write(yaml_output_file, merged.to_yaml)

Затем он может быть использован для чтения конкретного scoped_value из файла JSON.

jsonyaml.sh $JSON_FILE $YAML_TARGET "my_scope_to_add"

Мне удалось получить результат с помощью следующей команды:

jq -r .users defaultsfile.txt >> users.yaml

Вывод в ясном формате yaml.

Другие вопросы по тегам