Structured data
When a command prints JSON, hobnob keeps it as data, not text. You can reach straight into it, in any later step, without jq or a parsing step:
tasks:
fetch-question:
steps:
- run: curl -s "https://the-trivia-api.com/v2/questions?limit=1"
into:
- QUESTIONS: stdout
- run: echo "{{ .QUESTIONS[0].question.text }} {{ .QUESTIONS[0].correctAnswer }}"$ hobnob fetch-question
…
[fetch-question] In which country did the 'Bay of Pigs' invasion take place? Cuba
✓ [fetch-question] doneNumbers and true/false keep their type too. With the API's /metadata captured as META, {{ gt .META.byCategory.science 1000 }} compares two numbers, however many steps later you write it.
Where data comes from
A variable holds a list or a map in only three cases:
- You wrote one in YAML, in
set:,vars:,const:,with:orinto:, such as[history, science]or{ easy: 1, hard: 3 }. - A command printed a JSON list or object, and you kept it with
into:. - You used the
jsonfilter to turn text into data.
Everything else is text, even when it looks like JSON (see Text that looks like JSON).
Reaching into data
Reach into data the way it's written: .key for a field of a map, [0] for an item of a list, and [1:3] for a slice of a list. Combine them to any depth:
tasks:
fetch-question:
steps:
- run: curl -s "https://the-trivia-api.com/v2/questions?limit=1"
into:
- QUESTIONS: stdout
- set:
- QUESTION: .QUESTIONS[0].question.text[-1] counts from the end, and [.KEY] takes the key from another variable. The accessor table lists every form.
Every item at once
[*] reaches into every item of a list. The rest of the path then applies to each one, and you get a list back:
tasks:
fetch-questions:
steps:
- run: curl -s "https://the-trivia-api.com/v2/questions?limit=5"
into:
- QUESTIONS: stdout
- set:
- TEXTS: .QUESTIONS[*].question.text # ["How many limbs does an octopus have?", …]
- FIRST_TWO: .QUESTIONS[0:2].difficulty # ["easy", "hard"]A slice like [0:2] works the same way, for just those items.
Missing values
Reaching for something that isn't there stops the task with an error, so a typo can't quietly turn into an empty value. The questions have no author field, so {{ .QUESTIONS[0].author }} fails with "author" not found. Add default where a value really might be missing:
tasks:
show-author:
steps:
- run: echo "{{ .QUESTIONS[0].author | default "anonymous" }}" # anonymousKeeping a type
A field that holds a single variable and nothing else keeps it as it is, so a list stays a list. Put any other text around it and the field becomes text:
tasks:
show-categories:
steps:
- set:
- A: .CATEGORIES # still a list
- B: "{{ .CATEGORIES }}" # still a list
- C: "count: {{ .CATEGORIES | len }}" # textThis is why options: .CATEGORIES gives a prompt a real list of choices.
Combining lists
A list inside a list stays a separate item, as it would in JSON. Add ... after it to pour its items in instead:
tasks:
build-curl-opts:
steps:
- set:
- CURL_OPTS: [-s, --max-time, "10"]
- EXTRA: [--retry, "3"]
- NESTED: [.CURL_OPTS, --verbose] # [["-s", "--max-time", "10"], "--verbose"]
- FLAT: [.CURL_OPTS..., .EXTRA..., --verbose] # ["-s", "--max-time", "10", "--retry", "3", "--verbose"]That makes it easy to add to a list as you go:
tasks:
build-curl-opts:
steps:
- set:
- CURL_OPTS: [.CURL_OPTS..., --verbose]
if: '[ "{{.VERBOSE}}" = true ]'Lists as command arguments
In the list form of run:, a list variable becomes several arguments, one per item. You don't need ...:
tasks:
fetch-question:
steps:
- set:
- CURL_OPTS: [-s, --max-time, "10"]
- run: [curl, .CURL_OPTS, "https://the-trivia-api.com/v2/questions?limit=1"]
# runs: curl -s --max-time 10 https://the-trivia-api.com/v2/questions?limit=1Text that looks like JSON
Command-line arguments, env files and your shell's environment only ever give text, however much it looks like JSON. Turn it into data with | json:
hobnob report CATEGORIES='["history","science"]'tasks:
report:
steps:
- loop: .CATEGORIES | json
steps:
- run: echo "{{.ITEM}}"Without | json, .CATEGORIES is 21 characters of text, not a list of two.
Rules
Reaching into data
- A key from a variable is matched exactly. With
Kset toscience.v2,.LABELS[.K]finds the keyscience.v2; the dot isn't read as a path. [*]and slices drop items the rest of the path doesn't match. If one question has noquestion.text,TEXTSsimply has one fewer item. If none match, you get an empty list.[*]on a map gives its values, sorted by key, in the same order askeys.- Treating text as data is always an error, even with
default. Indexing text, or spreading it with..., means it needs| jsonfirst. - Data is only detected once, when it's captured or written. A later filter never reads text as a list, which is why
keys,joinand[0]fail on text.
Combining lists
- Spread as many lists as you like, anywhere in a list. An empty list adds nothing.
...works in any list you write: inset:,vars:,const:,with:andinto:. Ininto:you can spread an output too:[stdout..., exit]....applies to the whole item, including its path and filters:.QUESTIONS[*].category...,(.TAGS | json)....- Only a variable spreads. Plain text such as
Loading...stays text. To put dots after a variable's value, write"{{ .A }}...". - Spreading anything but a list is an error.
Lists as command arguments
- An empty list adds no arguments, but an empty string
""stays as one empty argument, so the arguments after it don't shift. ...is allowed and changes nothing, so[curl, .CURL_OPTS..., .URL]runs the same as[curl, .CURL_OPTS, .URL].- A map is an error. Pick the field you meant, such as
.CONFIG.url.