The core type of my AST, this data-structure represents a single (sub-)command in an excmd
script; and is the product of entry-points like Parser.expressionOfString.
This JavaScript class, like most in my interface, cannot be constructed directly (i.e. new
Expression() is going to throw an error); instead, you must invoke the parser via the functions
in the exported Parser module.
The language accepted by this parser allows for "sub-expressions" — runs of code that may remain
unevaluated, to be executed at a later point.
The most direct method of interacting with these, is manually testing whether a particular method
has produced a Literal or a Sub-expression, and proceeding as appropriate:
However, this can be tedious, if you only wish to evaluate simple expressions. For these
situations, I provide a simplified "recursive evaluation" interface: by inverting control, you
can allow the parser to become responsible for recursively evaluating Sub and Literal
values into simple strings, as long as you can provide the parser with a simplified
handler-callback that will attempt to reduce a single, given sub-expression into a string.
The entry-point to this auto-evaluation interface is either cloneWithEvaluator, or as a
further convenience, the various eval* methods below: evalCommand, evalPositionals, and
so on.
Each of these takes a function — matching the evaluator signature — that takes an expression,
and mechanistically reduces it into a simple string:
(Note, additionally, that within the body of the evaluator above, I've only had to compare
expr.command to a string, directly. As evaluators receive ExpressionEvals instead of plain
expressions, their bodies need no further shenanigans; any requested expression-properties will
recursively evaluate further subexpressions as-needed.)
Finally, if you want to access multiple elements of an expression thusly, it may be easier to
pre-associate an evaluator with the expression using cloneWithEvaluator, producing an
ExpressionEval directly:
Importantly, these Expressions are mutable views onto the underlying AST. They're designed to
ensure you don't access the data-structure in an unsafe way. In particular, a given word in the
original input-string can only be either a flag-payload, or a positional argument — not both.
Does that command have one flag, with the payload world? Or an empty (boolean) flag, and a
single positional-argument, world? My answer, in this interface, is “whichever you observe
first”. To be more specific, any method of either Expression or ExpressionEval that
produces flag-payloads or positional arguments, will mutate the data-structure such that
subsequent accesses don't see an inconsistent state.
Here, getFlag mutated expr1; and so a subsequent getPositionals produced a consistent
result: there were no remaining positional arguments.
const expr2 = Parser.expressionOfString('hello -f world')
expr2.getPositionals() //=> returns one arg, [{ value: 'world' }]
expr2.getFlag('f') //=> returs undefined; because `-f` has no payload
Here, getPositionals mutated expr2; and so a subsequent getFlag, again, produced a
consistent result: there was no value available to become the payload of -f.
This interface is intended to be used in a form wherein the invocations of these methods acts as
the specification of the parser's behaviour. For example, one command might expect a flag -d or
--dest, and demand a payload for that flag; whereas another might use -d as a boolean, and
expect positionals. This way, the behaviour of the parser is flexible enough to handle both of
those situations.
Finally, note that at least for flags, there's also non-mutative accessors: you can always check
whether a flag exists using hasFlag.
The core type of my AST, this data-structure represents a single (sub-)command in an excmd script; and is the product of entry-points like Parser.expressionOfString.
Construction
This JavaScript class, like most in my interface, cannot be constructed directly (i.e.
new Expression()
is going to throw an error); instead, you must invoke the parser via the functions in the exported Parser module.Evaluation
The language accepted by this parser allows for "sub-expressions" — runs of code that may remain unevaluated, to be executed at a later point.
The most direct method of interacting with these, is manually testing whether a particular method has produced a Literal or a Sub-expression, and proceeding as appropriate:
const expr = Parser.expressionOfString('(echo test_cmd) arg'), cmd = expr.command switch (cmd.type) { case 'literal': console.log(cmd.value) // ... use stringish val, case 'subexpression': handleSubexpr(cmd.expr) // or evaluate. }
However, this can be tedious, if you only wish to evaluate simple expressions. For these situations, I provide a simplified "recursive evaluation" interface: by inverting control, you can allow the parser to become responsible for recursively evaluating Sub and Literal values into simple
string
s, as long as you can provide the parser with a simplified handler-callback that will attempt to reduce a single, given sub-expression into astring
.The entry-point to this auto-evaluation interface is either cloneWithEvaluator, or as a further convenience, the various
eval*
methods below: evalCommand, evalPositionals, and so on.Each of these takes a function — matching the evaluator signature — that takes an expression, and mechanistically reduces it into a simple string:
const expr = Parser.expressionOfString('(echo test_cmd) arg') function handleSubexpr(expr) { if (expr.command === 'echo') { return expr.positionals.join(' ') } } console.log("Command:", expr.evalCommand(handleSubexpr)) //=> prints "Command: test_cmd"
(Note, additionally, that within the body of the evaluator above, I've only had to compare
expr.command
to a string, directly. As evaluators receive ExpressionEvals instead of plain expressions, their bodies need no further shenanigans; any requested expression-properties will recursively evaluate further subexpressions as-needed.)Finally, if you want to access multiple elements of an expression thusly, it may be easier to pre-associate an evaluator with the expression using cloneWithEvaluator, producing an ExpressionEval directly:
function handleSubexpr(expr) { if (expr.command === 'echo') { return expr.positionals.join(' ') } } const expr = Parser.expressionOfString('(echo test_cmd) arg'), ee = expr.cloneWithEvaluator(handleSubexpr) console.log(`Command: ${ee.command}, args: ${ee.positionals.join(' ')}`) //=> prints "Command: test_cmd, args: arg"
Mutation
Importantly, these
Expression
s are mutable views onto the underlying AST. They're designed to ensure you don't access the data-structure in an unsafe way. In particular, a givenword
in the original input-string can only be either a flag-payload, or a positional argument — not both.Consider the following:
const expr = Parser.expressionOfString('hello -f world')
Does that command have one flag, with the payload
world
? Or an empty (boolean) flag, and a single positional-argument,world
? My answer, in this interface, is “whichever you observe first”. To be more specific, any method of either Expression or ExpressionEval that produces flag-payloads or positional arguments, will mutate the data-structure such that subsequent accesses don't see an inconsistent state.Let's look at some examples:
const expr1 = Parser.expressionOfString('hello -f world') expr1.getFlag('f').value //=> returs 'world'; expr1.getPositionals() //=> returns []
Here, getFlag mutated
expr1
; and so a subsequent getPositionals produced a consistent result: there were no remaining positional arguments.const expr2 = Parser.expressionOfString('hello -f world') expr2.getPositionals() //=> returns one arg, [{ value: 'world' }] expr2.getFlag('f') //=> returs undefined; because `-f` has no payload
Here, getPositionals mutated
expr2
; and so a subsequent getFlag, again, produced a consistent result: there was no value available to become the payload of-f
.This interface is intended to be used in a form wherein the invocations of these methods acts as the specification of the parser's behaviour. For example, one command might expect a flag
-d
or--dest
, and demand a payload for that flag; whereas another might use-d
as a boolean, and expect positionals. This way, the behaviour of the parser is flexible enough to handle both of those situations.Finally, note that at least for flags, there's also non-mutative accessors: you can always check whether a flag exists using hasFlag.