Skip to content

Instantly share code, notes, and snippets.

@abathur
Last active June 14, 2022 13:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abathur/d1da98819824656b0993a3716951ea63 to your computer and use it in GitHub Desktop.
Save abathur/d1da98819824656b0993a3716951ea63 to your computer and use it in GitHub Desktop.
some sort of hierarchical shell-esque language?

I'm just ~braindumping something to (hopefully) get it out of my head.

status quo

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.

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

aspiration

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 idiomatic test 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, and cases.
  • 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.
  • 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment