Additional syntax definitions can be taken from a simple text configuration file. This allows the syntax to be easily extended without recompiling the main program. It also means that mere humans can create a functional CLI.
The file format for the syntax definitions is straightforward. At the simplest level, it is just a copy of commands that the user is allowed to type:
foo bar baz
Will allow the user to type "foo bar baz". Anything else is a syntax error.
Multiple lines can be used:
foo bar baz
bar bad
hello there
Again, any of these commands are allowed.
Comments can be used, and blank lines are ignored:
# this command does "foo" stuff, with "bar" and "baz
# chasers.
foo bar baz
# A "bar" command
bar bad
# Cheery command.
hello there
By itself, this syntax isn't very useful. To make it more useful, we allow optional parameters. These are signified by surrounding them with square brackets, as with regular expressions:
# allow "foo", or "foo bar baz"
foo [bar baz]
The above example is equivalent to the following:
foo
foo bar baz
but it can be easier to understand.
Alternate choices can be specified by using round brackets and pipe characters, as with regular expressions:
# allow "foo a", or "foo b"
foo (a|b)
Again, the above example is equivalent to the following:
foo a
foo b
Alternate choices become more useful when you have multiple choices:
foo (a|b) (b|c) (e|f) (g|h)
It would be expensive to specify the above example using the "one line for each allowed syntax" paradigm.
The standard "+" and "*" can be used to repeat any subset of a syntax.
The above examples can be combined, of course:
# Allow "foo a b", "foo a c", "foo b", or "foo c"
foo [a] (b|c)
And nested:
# allow lots of things
foo (b (d|e) | c)
bar (b [c] | hack)
It is often useful to allow for a variable number of arguments to a command. This can be done via the following syntax:
# allow *any* number of *any* arguments after "foo"
foo ...
The "..." syntax is a special-case for variable argument support. When it is seen, it will accept absolutely any number of arguments, in any format, after the initial command. There must be at least one command before the variable agument block. i.e. "..." by itself is forbidden.
It is also forbidden to use alternation or optional parameters with variable arguments.
# Disallowed: variable arguments in optional test
foo [...]
# Disallowed: there can't be alternation
foo (... | bar )
# Disallowed: this is the same as above
foo ...
foo bar
The input syntax is checked for being properly formatted. Invalid formats are rejected. Each input syntax is then merged with the previous ones, as a logical alternation. i.e. the following two syntaxes are identical, and will result in the identical syntax:
# two lines
foo bar
bar baz
and
# one line
(foo bar|bar baz)
Specifying the "same" syntax multiple times will not cause a problem. The duplicate syntaxes will be discovered, and ignored.
It is possibly to specify ambiguous syntaxes, as with the following:
[a] cat
a cat sat
In which case the second line cannot be used, as it will cause a syntax error.
The parser is "greedy", in that it takes the first match, and does minimal backtracking. The solution to poorly defined syntaxes is to use a well-defined syntax.
The syntax format allows you to specify allowed data types in a command.
foo STRING
Allows the entry of a double-quoted or single quoted string. This means that the user can put spaces in a string, along with the question mark character. Backslashes are used within the string to escape the quote character.
Otherwise, the string is left as-is. Entering "foo bar" on the CLI when a STRING is allowed means that exact string is used, including the quotation marks.
foo IPADDR
Allows an IPv4 address, dotted quad notation.
foo INTEGER
Allows positive or negative integers, base 10.
foo MACADDR
Allows MAC (ethernet) addresses, 6 sets of 2 hex digits, separated by colons. i.e. 00:01:02:03:04:05. Other formats cause syntax errors.
See DATATYPES.md for more details.
There are cases where the same syntax is needed in many places. Recli
therefore allows macros, via NAME=syntax
format:
OPTIONS=(int INTEGER|ip IPADDR)
add foo OPTIONS
del foo OPTIONS
Macros make the syntax clearer and easier to write.