Exercism and jq
I wanted to see how a jq track might work in Exercism. Here's what I tried out this morning.
Exercism is a great resource for learning and practising languages. I've dabbled in a couple of tracks and it's a fun and compelling way to iterate and meditate on constructs in the languages you're interested in. One of the very appealing things to me is that as well as a very capable online editor environment, there's a command line interface (CLI) for working locally.
Digging into jq
I've recently been digging into jq and wanting to build my knowledge out beyond the classic one-liners one might normally express in a JSON processing pipeline situation. jq
is a complete language, with a functional flavour and there's support for modules, function definitions and more. The manual felt pretty terse at first, but after a while my brain got used to it.
I thought it might be an interesting exercise to see how a jq
track might work with Exercism; initially I just want to perhaps use some of the existing tests to code against, where I provide jq
scripts to compute the right answers.
Using the bash track for jq
As jq
is "just another Unix tool" that works well on the command line, it seemed logical to try and start with something similar, which I did - the bash
track. Here's what I did to feel my way into this journey. It's early days, and this blog post is more of a reminder to my future self what I did.
Downloading a bash track exercise as a base
Having set myself up for working locally I downloaded a simple exercise from the Bash track - Reverse String, and moved it to a new, local jq
track directory:
# /home/user
; cd work/Exercism/
# /home/user/work/Exercism
; ls
./ ../ bash/
# /home/user/work/Exercism
; mkdir jq
# /home/user/work/Exercism
; exercism download --exercise=reverse-string --track=bash
Downloaded to
/home/user/work/Exercism/bash/reverse-string
# /home/user/work/Exercism
; mv bash/reverse-string jq/
# /home/user/work/Exercism
; cd jq/reverse-string/
# /home/user/work/Exercism/jq/reverse-string/
; ls
./ ../ .exercism/ HELP.md README.md bats-extra.bash reverse_string.bats reverse_string.sh
# /home/user/work/Exercism/jq/reverse-string
;
Modifying the bats file
The bash
track uses the Bash Automated Testing System, known as bats
, for unit testing. The tests are in the reverse_string.bats
file and look like this (just the first two tests are shown here):
#!/usr/bin/env bats
load bats-extra
# local version: 1.2.0.1
@test "an empty string" {
#[[ $BATS_RUN_SKIPPED == "true" ]] || skip
run bash reverse_string.sh ""
assert_success
assert_output ""
}
@test "a word" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
run bash reverse_string.sh "robot"
assert_success
assert_output "tobor"
}
I modified each test line (run bash <sometest>.sh <test input>
) to reflect a more jq
oriented invocation, which looks like this:
run jq -rR -f <sometest>.jq <<< <test input>
This:
- invokes
jq
instead ofbash
- uses the
-r
flag to telljq
to output raw strings, rather than JSON texts (this means that the valuebanana
would be output as is, rather than"banana"
with double quotes; a double-quoted string is valid JSON andjq
strives to output valid JSON by default) - uses the
-R
flag to telljq
to expect raw strings, rather than JSON input - uses the
-f
flag to point to a file containing the actualjq
script (called a "filter") - provides the input via a here string as
jq
expects the input via STDIN (so far, the<test input>
values have been scalar values)
This is what the above excerpted unit test file now looks like:
#!/usr/bin/env bats
load bats-extra
# local version: 1.2.0.1
@test "an empty string" {
#[[ $BATS_RUN_SKIPPED == "true" ]] || skip
run jq -rR -f reverse_string.jq <<< ""
assert_success
assert_output ""
}
@test "a word" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
run jq -rR -f reverse_string.jq <<< "robot"
assert_success
assert_output "tobor"
}
Writing the solution file
The solution file supplied by default here is reverse_string.sh
and contains some hints as to how to structure the contents. Basically, the file has to be written in such a way that when it's invoked, with the input supplied, it outputs the expected answer.
So here, I created reverse_string.jq
to be used instead of the default reverse_string.sh
. Having deliberately chosen a simple exercise, here's what my solution looks like in this file:
#!/usr/bin/env jq
split("") | reverse | join("")
Running the unit tests
I'm a big fan of entr and used it here to rerun the unit tests every time I changed either them or my solution file reverse_string.jq
, like this:
# /home/user/work/Exercism/jq/reverse-string
; ls *.bats *.jq | entr -c bats reverse_string.bats
This provided me with a lovely unit test result that would automatically update if I modified the solution or even the unit test file itself:
✓ an empty string
- a word (skipped)
- a capitalised word (skipped)
- a sentence with punctuation (skipped)
- a palindrome (skipped)
- an even-sized word (skipped)
- avoid globbing (skipped)
7 tests, 0 failures, 6 skipped
Activating the further tests
As you can see from the unit test results, only one test ("an empty string") was executed. The others are skipped. This is by design - see the Skipped tests section of the test documentation.
Activating the further tests is just a matter of commenting out the [[ $BATS_RUN_SKIPPED == "true" ]] || skip
line - note that the first test in the file has this line commented out by default so just that first test is run initially.
Alternatively, as you can see from that line, the BATS_RUN_SKIPPED
environment variable can be set to true
instead, and all of the tests will be run, like this:
# /home/user/work/Exercism/jq/reverse-string
; BATS_RUN_SKIPPED=true bats reverse_string.bats
✓ an empty string
✓ a word
✓ a capitalised word
✓ a sentence with punctuation
✓ a palindrome
✓ an even-sized word
✓ avoid globbing
7 tests, 0 failures
Looks like that jq
filter passes all the tests 🎉
Anyway, that's as far as I got - I think there could be some mileage in pursuing this approach further. Now it's time for me to use this technique to help me dig into writing a jq
filter to solve the Scrabble Score exercise!