BKL

bkl is a templating configuration language without the templates. It's designed to be simple to read and write with obvious behavior.

Write your configuration in your favorite format: JSON, YAML, or TOML. Layer configurations on top of each other, even from different file formats. Use filenames to define the inheritance. Have as many layers as you like. bkl merges your layers together with sane default behavior that you can override. Export your results in any supported format for human or machine consumption. Use the CLI directly or in scripts or automate with the library.

Go Reference GitHub: bkl Discord: bkl

Example

addr: 127.0.0.1 name: myService port: 8080
port = 8081
$ bkl service.test.toml addr = '127.0.0.1' name = 'myService' port = 8081

bkl automatically inherits service.test.toml from service.yaml by filename pattern and handles format conversion.

Install

# Install go from go.dev $ go install github.com/gopatchy/bkl/...@latest
# Install brew from brew.sh $ brew install gopatchy/bkl/bkl

Download binaries from releases.

Formats

Output defaults to input format. Use -f to override.

$ bkl -f yaml service.test.toml addr: 127.0.0.1 name: myService port: 8081
$ bkl -f toml service.test.toml addr = '127.0.0.1' name = 'myService' port = 8081
$ bkl -f json service.test.toml {"addr":"127.0.0.1","name":"myService","port":8081}
$ bkl -f json-pretty service.test.toml { "addr": "127.0.0.1", "name": "myService", "port": 8081 }

jsonl is an alias for json (see JSON Lines). Format is auto-detected from file extensions.

$ bkl service.test.yaml # real file is service.test.toml addr: 127.0.0.1 name: myService port: 8081

Output

Output goes to stdout by default. Use -o to write to a file.

$ bkl -o out.yaml service.test.toml

The output format is automatically detected from the output filename.

Inputs

$ bkl a.b.yaml c.d.yaml # a.yaml + a.b.yaml + c.yaml + c.d.yaml

Specifying multiple input files merges all layers in order.

$ bkl -- -.yaml <<'EOF' a: 1 EOF

Use -.yaml, -.json, or -.toml to read from stdin. Prefix with -- to prevent flag interpretation.

Inheritance

Inheritance is determined using filenames by default. After stripping the extension, the remaining filename is split on . and treated as an inheritance hierarchy (e.g. a.b.c.yaml inherits from a.b.<ext> inherits from a.<ext>). Parent layers may have any supported file extension.

$parent: a.b # inherits from a.b., from a.

bkl will check for all supported endings of a manually-specified parent file and will still evaluate layers under the parent in the normal order.

$parent: - a - b
$parent: a.*

$parent supports lists and wildcards for multiple inheritance. Wildcard * matches one segment only: a.* matches a.b.yaml but not a.b.c.yaml.

$ bkl a.b.yaml c.d.yaml # a.yaml + a.b.yaml + c.yaml + c.d.yaml

Layer order can be specified with commandline argument order.

$parent: false # no further inheritance

Setting $parent to false or null stops any further inheritance regardless of filename structure.

Streams

Streams contain multiple documents in one file. YAML uses --- delimiters. JSON formats use newlines (see JSON Lines).

By default, child documents apply to all parent documents. Use $match to target specific documents.

a: 1 --- b: 2
+
c: 3
=
a: 1 c: 3 --- b: 2 c: 3
a: 1 --- b: 2
+
$match: b: 2 c: 3
=
a: 1 --- b: 2 c: 3

The supplied pattern can match multiple documents and the updates will be applied to all of them. $match: {} matches any documents with a map as their root element, regardless of parent.

a: 1 --- b: 2 --- a: 1
+
$match: a: 1 c: 3
=
a: 1 c: 3 --- b: 2 --- a: 1 c: 3

$invert: true inside a $match block inverts the match, causing it to apply to any documents that do not contain the match criteria.

a: 1 --- b: 2
+
$match: a: 1 $invert: true c: 3
=
a: 1 --- b: 2 c: 3

$match: null forces the updates to apply to a new document.

a: 1
+
$match: null b: 2
=
a: 1 --- b: 2

Maps

Maps are merged by default. Use $replace: true to replace the entire map or $delete to remove specific keys.

a: 1
+
b: 2
=
a: 1 b: 2
a: 1
+
b: 2 $replace: true
=
b: 2
a: 1 b: 2
+
c: 3 b: $delete
=
a: 1 c: 3

bkl returns an error if you use $delete, $replace: true, or a key: value pair when they don't override a value from a lower layer. This helps keep upper layers minimal.

Lists

Lists are merged by default. Use $replace: true to replace the entire list or $delete to remove specific entries.

- 1
+
- 2
=
- 1 - 2
- 1
+
- 2 - $replace: true
=
- 2
- x: 1 - x: 2
+
- x: 3 - $delete: x: 2
=
- x: 1 - x: 3

bkl returns an error if you use $replace: true or $delete when they don't override a value from a lower layer. This helps keep upper layers minimal.

To update a specific list item, use $match. For scalar values in lists, use $value to replace the entire item.

- a: 1 - b: 2
+
- $match: b: 2 b: 10
=
- a: 1 - b: 10
- 1 - 2
+
- $match: 2 $value: 10
=
- 1 - 10

$""

Use $"" to interpolate format strings with $merge-style references.

a: 1 b: c: foo d: $"{b.c} bar {a} 2"
=
a: 1 b: c: foo d: foo bar 1 2

$decode

$decode transforms a string into a tree by parsing standard formats.

$value: | {"a":1} $decode: json
=
a: 1

$env

Use $env: to substitute environment variables. This is supported in keys and values.

# export FOO=test "$env:FOO": 1
=
test: 1
# export FOO=test a: $env:FOO
=
a: test

Note that all $env: substitutions result in string values even if the substituted value is true, false, null, or all digits.

$encode

$encode transforms the subtree into the specified format.

$value: a $encode: base64
=
YQ==
a: 1 b: 2 $encode: flags # [tolist:=, prefix:--]
=
- --a=1 - --b=2
- - a - b - - c - 4 - e - $encode: flatten
=
- a - b - c - 4 - e
$value: [a, b] $encode: join:/
=
a/b
a: 1 b: 2 $encode: json
=
| {"a":1,"b":2}
- a: 1 - b: 2 - $encode: json
=
| [{"a":1},{"b":2}]
- a - 2 - $encode: prefix:X
=
- Xa - X2
$value: hello world $encode: sha256
=
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
a: 1 b: 2 $encode: tolist:=
=
- a=1 - b=2
a: 1 b: 2 c: 3 $encode: values
=
- 1 - 2 - 3
a: b: 1 c: d: 2 $encode: values:name
=
- b: 1 name: a - d: 2 name: c
a: 1 b: 2 $encode: values:name:value
=
- name: a value: 1 - name: b value: 2
a: 1 b: 2 $encode: [tolist:=, "join:,"]
=
a=1,b=2

$required

Use $required in lower layers to force upper layers to replace the value.

a: 1 b: $required
+
c: 3
=
Error
a: 1 b: $required
+
b: 2 c: 3
=
a: 1 b: 2 c: 3
a: 1 b: - $required
+
c: 3
=
Error
a: 1 b: - $required
+
b: - 2 c: 3
=
a: 1 b: - 2 c: 3

$merge

Use $merge to merge the contents one subtree or scalar value into another.

foo: bar: a: 1 zig: b: 2 $merge: foo.bar
=
foo: bar: a: 1 zig: a: 1 b: 2
foo: bar: - a: 1 zig: - b: 2 - $merge: foo.bar
=
foo: bar: - a: 1 zig: - b: 2 - a: 1
foo: bar: a: 1 zig: b: 2 c: $merge:foo.bar.a
=
foo: bar: a: 1 zig: b: 2 c: 1

You can also merge across documents using $match and optionally $path:

a: 1 b: 2 --- c: 3 $merge: $match: a: 1
=
a: 1 b: 2 --- a: 1 b: 2 c: 3
a: 1 b: c: 3 --- d: 4 $merge: $match: a: 1 $path: b
=
a: 1 b: c: 3 --- c: 3 d: 4

$merge supports a shorthand syntax: $merge: [{pattern}, path] is equivalent to using $match and $path.

a: 1 b: 2 --- c: 3 $merge: [{a: 1}]
=
a: 1 b: 2 --- a: 1 b: 2 c: 3
a: 1 b: c: 3 --- d: 4 $merge: [{a: 1}, b]
=
a: 1 b: c: 3 --- c: 3 d: 4

$replace

Use $replace to merge the contents of one subtree or scalar value with another.

foo: bar: a: 1 zig: b: 2 $replace: foo.bar
=
foo: bar: a: 1 zig: a: 1
foo: bar: - a: 1 zig: - b: 2 - $replace: foo.bar
=
foo: bar: - a: 1 zig: - a: 1
foo: bar: a: 1 zig: b: 2 c: $replace:foo.bar.a
=
foo: bar: a: 1 zig: b: 2 c: 1

Note that $merge and $replace are equivalent for scalar values.

You can also replace across documents using $match and optionally $path:

a: 1 b: 2 --- c: 3 $replace: $match: a: 1
=
a: 1 b: 2 --- a: 1 b: 2
a: 1 b: c: 3 --- d: 4 $replace: $match: a: 1 $path: b
=
a: 1 b: c: 3 --- c: 3

$replace supports a shorthand syntax: $replace: [{pattern}, path] is equivalent to using $match and $path.

a: 1 b: 2 --- c: 3 $replace: [{a: 1}]
=
a: 1 b: 2 --- a: 1 b: 2
a: 1 b: c: 3 --- d: 4 $replace: [{a: 1}, b]
=
a: 1 b: c: 3 --- c: 3

$output

Use $output: true to select a subtree for output instead of the document root.

foo: bar: $output: true a: 1 b: 2
=
a: 1 b: 2
foo: bar: - $output: true - a: 1 - b: 2
=
- a: 1 - b: 2

Multiple instances of $output: true in a document will generate multiple output documents (delimited with ---).

Use $output: false to omit a subtree or entire document from the output.

a: b: 1 $output: false c: d: 2
=
c: d: 2
a: - b: 1 - $output: false c: - d: 2
=
c: - d: 2
a: 1 $output: false --- b: 2
=
b: 2

Combine $output with $merge to create reusable template sections.

$repeat

$repeat: at the top level of a document causes the document to be duplicated the given number of times. Within the document, $repeat can be used to reference the zero-based repeat index.

a: foo b: $repeat $repeat: 2
=
a: foo b: 0 --- a: foo b: 1

$repeat: can also contain a map of named repeat iterators to repeat count. The result is the cartesian product (all combinations) of repeat values.

$repeat: a: 2 b: 3 $value: $"{$repeat:a},{$repeat:b}"
=
0,0 --- 0,1 --- 0,2 --- 1,0 --- 1,1 --- 1,2

$repeat: can contain a list of values to iterate over:

$repeat: [foo, bar, baz] a: $repeat
=
a: foo --- a: bar --- a: baz

$repeat: supports range parameters for numeric sequences:

$repeat: $first: 10 $count: 3 a: $repeat
=
a: 10 --- a: 11 --- a: 12

Range parameters require exactly 2 of $first, $last, and $count. Optional $step defaults to 1.

$repeat: also works at the object level within maps and lists:

a: $"b{$repeat}": c: $repeat $repeat: 3
=
a: b0: c: 0 b1: c: 1 b2: c: 2

Object-level repeat in lists:

a: - b: $repeat $repeat: [foo, bar, baz]
=
a: - b: foo - b: bar - b: baz

$defer

Use $defer: true to process a document after all non-deferred documents have been fully evaluated.

This allows operations on the final output after transformations like $repeat have been applied.

$repeat: 2 a: $repeat
+
$repeat: 3 --- $defer: true $match: a: 2 b: special
=
a: 0 --- a: 1 --- a: 2 b: special

$$

$$ translates to a literal $ in cases where it would otherwise trigger special handling.

a: $$env:foo
=
a: $env:foo

TOML

TOML doesn't allow unquoted $ in keys. Quote keys containing bkl directives:

"$parent" = false

bklb

bklb is a wrapper for CLI programs that take configuration files as commandline arguments but do not support bkl format. It transparently merges layers, translates formats, writes to temporary files, alters the commandline arguments, then execs the wrapped program.

$ ln -s ~/go/bin/bklb ~/go/bin/catb # catb could be, e.g. kubectlb $ cat service.yaml addr: 127.0.0.1 name: myService port: 8080 $ cat service.test.toml port = 8081 $ catb service.test.toml addr = "127.0.0.1" name = "myService" port = 8081 $ cat service.test.yaml service.test.json cat: service.test.yaml: No such file or directory cat: service.test.json: No such file or directory $ catb service.test.yaml addr: 127.0.0.1 name: myService port: 8081 $ catb service.test.json {"addr":"127.0.0.1","name":"myService","port":8081}

Note that the files mentioned don't have to exist; bklb will search for files with the same root name but different extensions, then merge layers and translate into the specified format.

bklb takes the name of the program it wraps from its own filename, hence the ln -s symlink creation in the example above. It trims at most one b from the end of its name before searching for the wrapped program so they can coexist in your PATH.

bkld

$ bkld [--selector path] <base_layer_path> <target_output_path>

bkld (d for "diff") generates the minimal intermediate layer needed to create the target output from the base layer. Along with bkli, it automates splitting existing configurations into layers.

Use --selector to limit processing to documents matching a specific path.

a: 1 b: $required c: 3
?
a: 1 b: 2 d: 4
=
$match: {} b: 2 c: $delete d: 4
- a: 1 - b: 2
?
- a: 1 - c: 3
=
- $match: {} - c: 3 - $delete: b: 2
- 1 - 2
?
- 1 - 3
=
- $match: {} - 1 - 3 - $replace: true

bkli

$ bkli [--selector path] <target_output_path> <target_output_path> ...

bkli (i for "intersect") generates the maximal base layer that the specified targets have in common. Along with bkld, it automates splitting existing configurations into layers.

Fields that exist in all inputs with the same value are included in the result. Fields with different values are omitted.

Use --selector to limit processing to documents matching a specific path.

a: 1 b: 2 c: 3
a: 1 b: 10 d: 4
=
a: 1
- a: 1 - b: 2 - c: 3
- a: 1 - b: 10 - d: 4
=
- a: 1

bklr

$ bklr <lower_layer_path>

bklr extracts only $required fields and their parent paths. Use this output as a template for creating minimal upper layers.

bklc

$ bklc [--color] [--sort path] <file1> <file2>

bklc (c for "compare") compares two bkl files and shows colorized text differences between their evaluated outputs. Use --color to enable colored output. Use --sort to order documents by a specific path before comparison.

a: 1 b: 2 c: 3
a: 1 b: 4 c: 3 d: 5
=
--- file1 +++ file2 @@ -1,3 +1,4 @@ a: 1 -b: 2 +b: 4 c: 3 +d: 5

kubectl bkl

$ kubectl bkl <kubectl_commands>

kubectl bkl is a kubectl plugin that wraps all the normal kubectl commands by evaluating any input files as bkl layers and passing the output to kubectl.

# deploy.dev.yaml and deploy.yaml are bkl layer files $ kubectl bkl apply -f deploy.dev.yaml deployment.apps/deploy-dev unchanged

Migrate

For Kubernetes migration examples and best practices, see Kubernetes with bkl.