Синтаксис yaml для js разработчиков
September 08, 2020
Синтаксис yaml
(файлы yml
) — один из самых распространенных синтаксисов
используемых для написания конфигов. Например: конфиги CI чуть ли не для всех
известных CI/CD провайдеров, openapi (swagger), docker-compose, helm, k8s,
немалая часть js инструментов поддерживают конфиги как в формате json/js, так и
в формате yml. Даже если вы работаете с фронтендом и особо не используете
nodejs, вы так или иначе сталкивались с подобными конфигами. Как минимум, чтобы
настроить ci для фронта.
Причина популярности yaml формата, на мой взгляд, в том, что этот формат не требует дополнительных скобочек, кавычек и прочего. Этот формат чем-то похож на markdown в мире форматов данных. Но при этом он очень строг в отношении отступов (количества пробелов), так как отступы используются для определения структуры.
Вторая причина — это наличие дополнительного синтаксиса, который позволяет
уменьшить количество копипасты в конфигах и реюзать части конфига в нескольких
местах, выполнять мердж объектов, писать многострочные строки (да, multiline
strings так себе на русском звучит). А еще в yaml из коробки работают
комментарии (#
), которые отсутствуют в классическом формате JSON.
Одна только проблема, что этот синтаксис довольно не интуитивный для js
разработчиков (на мое субъективное мнение, конечно). Потому в этом посте я хочу
немного рассказать про некоторые возможности yaml
проведя аналогию с языком
программирования JavaScript и привести примеры использования yaml alias
в
конфигах.
Типы данных и примитивы в yaml
Сперва маленький пример того, как выглядят объекты и массивы в yaml:
# это комментарий
array:
- 'one'
- two
- 3
- foo: 'foo'
bar: 'bar'
# мы так же можем использовать что-то похожее на json
- { some: 'json' }
- [foo, bar]
object:
one: 'one'
two: two
three: 3
truth: true
empty: null
another_null:
На выходе получим следующий объект:
const result = {
array: [
'one',
'two',
3,
{
foo: 'foo',
bar: 'bar',
},
{
some: 'json',
},
['foo', 'bar'],
],
object: {
one: 'one',
two: 'two',
three: 3,
truth: true,
empty: null,
another_null: null,
},
};
Из примера выше видно, что yaml умеет распознавать примитивы: строки, числа,
булево и null
. Даже если строку не обвернуть в кавычки, yaml корректно ее
распарсит как строку (при условии, если строка не будет похожа на другой
примитив).
Объекты задаются в виде key: value
, если на месте value
будет список
элементов в формате - item
, мы получим массив. Кроме того, никто не мешает нам
вместо item
начать писать список в формате key: value
, чтобы добавить объект
в массив.
На верхнем уровне мы получили объект (значение переменой result
) по той
причине, что использовали синтаксис key: value
на верхнем уровне yaml файла.
Если мы захотим получить массив в качестве result
, тогда нам нужно начать
объявление файла с использования - item
синтаксиса:
- one
- 2
- three: 3
const result = [
'one',
2,
{
three: 3,
},
];
Предыдущий пример был скорее как бонус, так как обычно конфиги все же описываются как объекты.
Объявление и использование yaml алиасов
Алиасы в yaml дают возможность переиспользовать части конфига в нескольких
местах. Алиасы чем-то похожи на переменные (скорее даже константы) из
традиционных языков программирования. Для того, чтобы объявить yaml алиас, вам
необходимо использовать синтаксис &exampleName
, а для того, чтобы в дальнейшем
считать значение алиаса мы используем *exampleName
. Я так полагаю, что этот
синтаксис выполнен по аналогии с указателями в C.
object: &objectValue
one: &oneValue 'one'
array: &arrayValue
- *oneValue
- 'two'
foo:
- *arrayValue
- *objectValue
Выше приведенный yaml с алиасами примерно соответствует следующему JavaScript коду, где алиасы заменены на переменные:
const oneValue = 'one';
const objectValue = {
one: oneValue,
};
const arrayValue = [oneValue, 'two'];
const result = {
object: objectValue,
array: arrayValue,
foo: [arrayValue, objectValue],
};
То есть с помощью &
мы просто присваиваем значение переменной, а с помощью *
считываем это самое значение.
Выполнение shallow merge объектов
Еще один полезный оператор <<:
необходим для выполнения shallow merge
объектов:
variables: &variables
GITHUB_URL: https://github.com
GITLAB_URL: https://gitlab.com
urls: &defaultUrls
HOMEPAGE_URL: https://udf.su
<<: *variables
GOOGLE_URL: https://google.com
defaultConfig: &defaultConfig
title: 'Yaml'
urls: *defaultUrls
newUrls: &newUrls
urls:
YAML_URL: https://yaml.org
mergedConfig:
# вот здесь мы полностью перезапишем поле `urls`
<<: [*defaultConfig, *newUrls]
description: 'Yaml merge example'
В JavaScript этот оператор полностью соответствует spread оператору (за исключением того, что в yaml нельзя конкатенировать массивы):
const variables = {
GITHUB_URL: 'https://github.com',
GITLAB_URL: 'https://gitlab.com',
};
const defaultUrls = {
HOMEPAGE_URL: 'https://udf.su',
...variables,
GOOGLE_URL: 'https://google.com',
};
const defaultConfig = {
title: 'Yaml',
urls: {
HOMEPAGE_URL: 'https://udf.su',
GITHUB_URL: 'https://github.com',
GITLAB_URL: 'https://gitlab.com',
GOOGLE_URL: 'https://google.com',
},
};
const newUrls = {
urls: { YAML_URL: 'https://yaml.org' },
};
const result = {
variables,
urls: defaultUrls,
defaultConfig,
newUrls,
mergedConfig: {
...defaultConfig,
// вот здесь мы полностью перезапишем поле `urls`
...newUrls,
description: 'Yaml merge example',
},
};
Как я уже говорил, объединять массивы через алиасы в yaml нельзя, однако некоторые приложения/библиотеки могут поддерживать дополнительные спецификаторы
!flatten
или, в случае с Gitlab CI, он автоматически выпрямляет (flatten) массивы вbefore_script
,script
иafter_script
, то есть там можно просто делать вот так:cmds: &cmds - echo one - echo two before_script: - *cmds - echo three
В общем, если вам понадобится мерджить массивы — изучайте документации библиотек/сервисов, которыми вы пользуетесь.
Работа со строками 80 уровня
Отдельным пунктом хочу остановиться на строках в yaml. Вообще этот раздел вполне мог бы быть отдельным постом, но все же хочу затронуть эту тему здесь. Если вы уже устали от чтения, то можно полностью пропустить этот раздел, либо посмотреть интерактивный сайтик, ссылка на который размещена в конце раздела. Итак, в yaml есть два стиля написания строк: “Flow scalars” и “Block scalars”.
Flow scalars (полагаю, что это можно перевести как строчные скаляры) — это по сути то, что мы уже использовали в прошлых примерах. Этот стиль немного более ограниченный по сравнению с блочным. Всего у строчных скаляров есть три немного различающихся синтаксиса:
- Одинарные кавычки (
'hello world'
) — в этом режиме не работает экранирование (escaping), например\n
в результате так и останется\n
. Чтобы экранировать кавычку, нужно ее продублировать''
конвертируется в'
. По аналогии с markdown пустая строка будет конвертирована в перенос строки (\n
) - Двойные кавычки (
"hello world"
) — в этом стиле работает экранирование:\"
→"
,\\n
→\n
,\n
→[перенос строки]
,\
в конце строки отключает перенос строки. Чтобы вставить перенос строки, кроме символа\n
можно точно так же как и в предыдущем, стиле оставлять пустую строку. - Без кавычек — этот стиль действует по тем же правилам, что и использование
кавычек за исключением того, что теперь можно использовать любые кавычки в
тексте без необходимости их экранировать. Кроме этого, стиль без кавычек
немного избирательный к использованию
:
и#
в тексте. Символ:
не может идти перед переносом строки или пробелом, а#
после переноса строки или пробела. Если нарушить эти правила, то парсер yaml воспримет эти символы как попытку объявить объект или написать комментарий, что приведет к синтаксической ошибке.
Block scalars (блочные скаляры) предполагают использование специальных
операторов предназначенных для работы со строками: |
(literal style) и >
(folded style), которые могут использоваться с модификаторами +
и -
. Оба
оператора предназначены для написания текстов с переносами строки.
При использовании >
все переносы строки заменяются на пробелы. Чтобы вставить
перенос строки, необходимо оставить пустую строку. Альтернативно новую строку
можно создать если поставить дополнительный пробел в начале строки. Этот
операто р полезный, если, например, у вас есть длинная однострочная команда и вы
хотите разбить ее на несколько строк для удобства чтения.
При использовании |
все переносы строк остаются как есть.
С помощью +
/-
(chomping indicator) можно регулировать как обрабатывается
окончание строки. По умолчанию режим clip (когда +/- опущены), который
оставляет один перенос строки в конце строки. При использовании +
keep, yaml
оставит все переносы строк в точности так, как вы написали в коде. При
использовании -
strip будут обрезаны все переносы строки в конце строки.
Кроме того, при использовании блочных скаляров можно указать сколько пробелов
отступа вы используете (например |2
, |+2
), чтобы явно задать начало
“печатаемой” части строки. Если это число не указывать, то отступы определяются
по отступу первой строки. Честно говоря, не могу представить в каких случаях
пригодится эта настройка. Разве что, если вам понадобится забросить в строку
кусок кода со строгими правилами индентации, при чем индентация должна
начинаться прямо с первой строки.
В качестве примера предлагаю curl запрос написанный в разных стилях:
flow:
# а сейчас я отключу prettier, иначе он свернет мой пример в строку :D
# prettier-ignore
singleQuote: 'curl ''https://www.google.com/search?q=yaml''
-H "user-agent: Hello world"
-H "accept: text/html"
-H "referer: https://www.google.com/"'
# prettier-ignore
doubleQuote: "curl 'https://www.google.com/search?q=yaml'
-H 'user-agent: Hello world'
-H 'accept: text/html'
-H 'referer: https://www.google.com/'"
# Пришлось убрать пробелы после `:`. Иначе будет синтаксическая ошибка
# prettier-ignore
noQuotes: curl 'https://www.google.com/search?q=yaml'
-H 'user-agent:Hello world'
-H 'accept:text/html'
-H 'referer:https://www.google.com/'
block:
# Использование `-` не критично, bash и так скушает,
# но почему бы не убрать не нужные переносы строки?
#
# Обратите внимание на отступы перед `-H`, если мы не выравняем отступы с `curl`
# то получим перевод строки согласно правил для folded блоков
# prettier-ignore
folded: >-
curl 'https://www.google.com/search?q=yaml'
-H 'user-agent: Hello world'
-H 'accept: text/html'
-H 'referer: https://www.google.com/'
# Здесь нам придется экранировать переносы строки
# Так же как мы бы это делали в командной строке
block: |-
curl 'https://www.google.com/search?q=yaml' \
-H 'user-agent: Hello world' \
-H 'accept: text/html' \
-H 'referer: https://www.google.com/'
И вот такой у нас будет результат:
{
"flow": {
"singleQuote": "curl 'https://www.google.com/search?q=yaml' -H \"user-agent: Hello world\" -H \"accept: text/html\" -H \"referer: https://www.google.com/\"",
"doubleQuote": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent: Hello world' -H 'accept: text/html' -H 'referer: https://www.google.com/'",
"noQuotes": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent:Hello world' -H 'accept:text/html' -H 'referer:https://www.google.com/'"
},
"block": {
"folded": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent: Hello world' -H 'accept: text/html' -H 'referer: https://www.google.com/'",
"block": "curl 'https://www.google.com/search?q=yaml' \\\n -H 'user-agent: Hello world' \\\n -H 'accept: text/html' \\\n -H 'referer: https://www.google.com/'"
}
}
Итог: если вам нужно записать что-то в несколько строк, то лучше
использовать |
и >
, так как они дают более контролируемый результат и не
создают конфликтов с кавычками и прочими служебными символами.
Дополнительную информацию по части строк можно посмотреть на интерактивном сайте посвященном этой теме.
Онлайн конвертер yaml to json
И напоследок, если у вас возникают сложности с пониманием структуры yaml, можно воспользоваться любым онлайн ресурсом для конвертации yaml в более привычный json. Например вот этим