I'm just ~braindumping something to (hopefully) get it out of my head.
Part of what I like about shell is how squishy/pliable the language is. For a while I've had the nagging sense that it is really close to being an interesting format for a lot of different kinds of plain-text ~DSLs...
go to the store later
is a perfectly-good shell invocation. If you wanted some little task/TODO DSL, you could dive right in by writing shell functions that handle a few starting verbs and you could already have wind in your sails (even if you rewrite the DSL in something else later).- But, shell still parses a few bits syntax/furniture in ways that make
full-throated versions unworkable:
- We get broad control over how parsing works at the line level, but
it isn't total. We can't repurpose shell metacharacters and control
operators such as
|
and&&
. - Nested/multiline structures require leaning on shell syntax/idioms.
- We get broad control over how parsing works at the line level, but
it isn't total. We can't repurpose shell metacharacters and control
operators such as
For a more concrete example, here's an example of a bats test from the bats repo:
@test 'files in \$src_dir are added to tar archive' {
run main
[ "${status}" -eq 0 ]
run tar tf "$dst_tarball"
[ "${status}" -eq 0 ]
[[ "${output}" =~ a ]]
[[ "${output}" =~ b ]]
[[ "${output}" =~ c ]]
}
I wasn't really happy with how obtuse this was, because I want resholve's test suite to be clearer than this allows:
- invocations are hidden behind the run command
- magic internal variables
- shell/bash syntax that isn't broadly understood and adds friction to understanding the test's ~semantics/intent
I ended up hacking my way to something I was happy with, but it was more work than it needed to be, and it entailed some tradeoffs:
@test "invoking resholve with missing interpreter prints an error" {
require <({
status 2
line -1 equals "resholve: error: argument --interpreter: Interpreter must exist or be the string 'none'"
})
} <<CASES
resholve --interpreter /blah < file_simple.sh
resholve --interpreter /blah file_simple.sh
CASES
I'm using the same basic framework, but I've added support for:
- a ~list of cases that should all produce the same result
- simpler assertions with clearer structure and semantics
Compromises:
- Grouping test cases entailed shell furniture (shoehorning them into a heredoc)
- the assertions require shell furniture to group and pass them to their parsing function
- the assertions are backed by a function that parses them and generates code that has to be evaled again later
A more minimal version could've been:
test "invoking resholve with missing interpreter prints an error"
status 2
line -1 equals "resholve: error: argument --interpreter: Interpreter must exist or be the string 'none'"
cases
resholve --interpreter /blah < file_simple.sh
resholve --interpreter /blah file_simple.sh
Key features of the supporting language seem to be simple ways to:
- modify global and local scoping rules
- Something like the shell/bash builtin
test
might still be available here, but it is easy to declare an overriding idiomatictest
command in the global scope of the test file. - But then within the scope of that test command, it's simple to say whether a nested test is valid or not, and which test command it'll be.
- This mechanism supports hard grammar constraints when you want them. In the example
above, for example, imagine the interpreter can emit an error explaining that the
only valid first-words inside of test are
status
,line
, andcases
.
- Something like the shell/bash builtin
- declare what configuration of positional and/or block args a command accepts
- declare what kind of ownership vs. delegation (and whether they are eager/on-demand)
applies in a given context. i.e.,
test
declares whether it wants:- the interpreter to evaluate the cases block (i.e., pass the contents to the cases command) and give it the result
- the ability to modify the cases block's argument, choose whether to eval it, etc.
- strong control over what syntax affordances work at a given site/time
- It isn't that the language wouldn't support the input redirect in
resholve --interpreter /blah < file_simple.sh
, for example (though it might just outsource this to a primary shell ala make?), but more like the cases command declares whether it takes a block of completely unparsed lines, or if they should undergo whitespace stripping, and/or different kinds of word/parameter/etc. expansion.
- It isn't that the language wouldn't support the input redirect in
- since the rules will be squishy, it likely needs a good way to introspect what rules apply in a given context and perhaps to step through their application
Writing these out helped me stumble into more ways to see/frame this:
- an unbundled Shell-toolkit
- an extremely broad/flexible superset of Shell
(plus, in both cases, declarative syntax for granularly annotating what rules apply in what contexts)