Run a YAML File with Bash!
Wait, what?!!?
Run a YAML file?
And with Bash?
How is that possible?
Step 0🔗
Let's run this simple YAML file.yaml
:
name: Fido
age: 3
dog years: age * 7
When we run this file, we'd like to see this JSON output:
{
"name": "Fido",
"age": 3,
"dog years": 21
}
$ bash file.yaml
file.yaml: line 1: name:: command not found
file.yaml: line 2: age:: command not found
file.yaml: line 3: dog: command not found
Well that didn't work...
Let's not give up yet.
Step 1🔗
Let's forget about Bash for a moment and just load the YAML file with YS
:
$ ys -J file.yaml
{"name":"Fido", "age":3, "dog years":"age * 7"}
Close, but look at the dog years
value. It's just a string.
We need to tell YS to evaluate the expression age * 7
as a math expression.
YS won't do any evaluation unless we tell it to with the !YS-v0:
tag.
Then we need to tell YS that just the dog years
value should be evaluated.
We do that with the ::
syntax:
!YS-v0:
name: Fido
age: 3
dog years:: age * 7
Let's run it:
$ ys -J file.yaml
Error: Could not resolve symbol: age
Oh, right. The age
symbol is not defined here.
Luckily we can define it inline:
!YS-v0:
name: Fido
age =: 3
age:: age
dog years:: age * 7
Again:
$ ys -J file.yaml
{"name":"Fido", "age":3, "dog years":21}
Sweet! We have the JSON output we wanted.
Note: We could do the same thing with an anchor and alias:
!YS-v0:
name: Fido
age =: &age 3
dog years:: 7 * *age
Now Back to Bash🔗
OK. But this YAML file is definitely not a Bash script.
But maybe we can make it one?
Let's try this:
#!/usr/bin/env ys-0
source <(echo 'export YS_FORMAT=json' && curl '-s' 'https://getys.org/run') "$@":
---
!YS-v0:
name: Fido
age: &age 3
dog years:: 7 * *age
Now:
$ bash file.yaml
{"name":"Fido", "age":3, "dog years":21}
Woah! It worked! We just ran a YAML file with Bash!
But how?
Well I cheated a bit and showed you the second time I ran it. The first time I ran it, I got this:
$ bash file.yaml
Installing YS CLI '/tmp/yamlscript-run-ys/bin/ys-0.1.95' now...
Ctl-C to abort
See https://yamlscript.org/doc/run-ys for more information.
Installed /tmp/yamlscript-run-ys/bin/ys - version 0.1.95
--------------------------------------------------------------------------------
{"name":"Fido", "age":3, "dog years":21}
The first time you run the file, it installs YS under /tmp
and then runs the
file with YS.
The second time you run it, it just runs the file with YS.
The Polyglot Magic🔗
It turns out that the first two lines of the file are both valid Bash and valid YS!
The first line is a Bash shebang that invokes YS, but we ran the file with Bash.
Let's change the shebang to call Bash instead:
#!/usr/bin/env bash
source <(echo 'export YS_FORMAT=json' && curl '-s' 'https://getys.org/run') "$@":
---
!YS-v0:
name: Fido
age: &age 3
dog years:: 7 * *age
Then let's make the file executable, and run it:
$ chmod +x file.yaml
$ ./file.yaml
{"name":"Fido", "age":3, "dog years":21}
Works like a charm!
Ok so what about the second line?
For YS, the source
command is a no-op.
A function that ignores its arguments and returns nothing.
For Bash, we are sourcing a "process substitution" that curls a tiny script that
will install a YS binary under /tmp
and use it to run that file.
We also set the YS_FORMAT
environment variable to json
, which tells YS to
--load
the file starting in data mode.
Update
It seems that you can't source a process substitution in Bash 3.2 (the
default /bin/bash
on MacOS).
Fortunately, this variation works on all the versions of Bash used on Linux
and MacOS:
source /dev/stdin <<<"$(echo 'export YS_FORMAT=json' && curl '-s' 'https://getys.org/run')" "$@":
How can that be valid YS?
Again, this is both valid Bash and valid YS!
Here's how YS compiles the source
line to Clojure code:
$ ys -c -
!YS-v0
source /dev/stdin <<<"$(echo 'export YS_FORMAT=json' && curl '-s' 'https://getys.org/run')" "$@":
(source
#"dev"
stdin
<<<
(echo "export YS_FORMAT=json" && curl "-s" "https://getys.org/run")
"$@")
The YS source
function ignores its arguments, but those arguments need to
be valid YS code.
Ironically here /dev/stdin
compiles as the regex form /dev/
followed by
the symbol stdin
!
Also <<<
is valid syntax for a YS operator, but currently there is no
operator called <<<
in YS.
But Why?🔗
Ok, neat trick, but how is this useful in the real world?
First off it's a great way to write a YS program for someone who might not have YS installed. You don't even need to tell them about YS at all.
But for data files there's a more interesting use case.
Say you have a config.yaml
config file for some service that you'd like to be
able to use YS for instead of plain YAML.
If that service already used YS to load its YAML configs then you'd be all set.
But most services don't. Yet!
What if you could put a config.ys
file in the same directory as the
config.yaml
file and made it executable?
And what if the config.ys
file wrote its output to the config.yaml
file when
it was run?
As long as you could set things up to make sure the config.ys
file was run
whenever it (or one of its dependencies) changed, you'd have a generated
config.yaml
file that would do everything you wanted it to do.
Let's try it out. We'll make a config.ys
file that loads an other.yaml
file
and uses environment variables to set some values:
!YS-v0:
name: My Thing
stuff:: load('other.yaml').stuff
:when ENV.DEBUG.?::
environment:: ENV
And an other.yaml
file:
stuff:
some: data
If we load it with YS we get:
$ ys -Y config.ys
name: My thing
stuff:
some: data
This pulled in a part of the other.yaml
file.
It also would add a map of all your environment variables if DEBUG
was set.
Let's add 2 lines to make it write to config.yaml
:
!YS-v0:
name: My thing
stuff:: load('other.yaml').stuff
:when ENV.DEBUG.?::
environment:: ENV
--- !YS-v0
write FILE.replace(/\.ys$/ '.yaml'): _:yaml/dump
Now we can run it:
$ ys config.ys && cat config.yaml
name: My thing
stuff:
some: data
Perfect! Since YS evaluates to the last document in a multi-document file, we get what we want by running it.
The only remaining problem is that we need to have ys
installed to run this...
Or do we?
Let's try it with Bash:
#!/usr/bin/env bash
source <(curl '-s' 'https://getys.org/run') "$@":
--- !YS-v0:
name: My thing
stuff:: load('other.yaml').stuff
:when ENV.DEBUG.?::
environment:: ENV
--- !YS-v0
write FILE.replace(/\.ys$/ '.yaml'): _:yaml/dump
Now:
$ chmod +x config.ys
$ ./config.ys && cat config.yaml
name: My thing
stuff:
some: data
Voila! We just replaced a config.yaml
file with a config.ys
file for any
YAML configured service.
A More Solid Approach
The strategy for using YS files to generate YAML files is solid. But you probably don't want to actually run them (with Bash or YS).
A better way is to simply configure your pipeline to install YS and then run
ys -Y config.ys > config.yaml
at the appropriate time.
Conclusion🔗
I hope you've enjoyed this little journey into the world YS, YAML, and Bash.
Hopefully it gives you some interesting ideas for how to use YS in your own projects.
Please let us know if you come up with any super cool uses for this technique.
Happy YAMLing!
PS Ingy (that's me!) will be at KubeCon in London
next week.
Let's talk about YS, HelmYS, or whatever you'd like!