Voke.js – small functional shell-style scripter
  1. 1. Language
    1. 1.1. Comments and commands
    2. 1.2. Variables
    3. 1.3. Insets
      1. 1.3.1. Multiple lines
    4. 1.4. Scoping
      1. 1.4.1. Example
    5. 1.5. Returning values
    6. 1.6. Block commands
    7. 1.7. User-defined commands
      1. 1.7.1. Backtick pitfall
    8. 1.8. Raw parameter
  2. 2. Standard commands
    1. 2.1. if
    2. 2.2. unless
    3. 2.3. elseif & elsif
    4. 2.4. else
    5. 2.5. cat
    6. 2.6. function & fn

Voke is my yet another interpreter with, again, slightly different syntax and idea flavor. It was the first such interpreter I’ve written in JavaScript. It has all you need and even more: functions including anonymous fn, variables with proper scoping (like in JS itself), conditions that are actually normal commands and of course comments.

It’s also very easy to use and dependency-free:

xml<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
  </head>
  <body>
    <script src="voke.js"></script>

    <script>
      var result = voke.script(
        'command unnamedParam namedEmpty= named=value {json: " mode= here"}\n' +
        ';comment\n' +
        '#{a: true, b: false}\n'
      );

      alert(result.calc());
    </script>
  </body>
</html>

Download voke.js. Released in public domain so you’re free to do whatever you want with it.

Language

Comments and commands

In Voke, there are two types of lines: comments and commands. Comments do nothing (return undefined – this will be important later on) while commands do things.

Comments start with either of the following symbols with possible whitespace in front: ; # \ - *.

Commands are everything else; they accept parameters and return a value. The syntax for a command line is:

[leading whitespace] [unnamed parameters] [named parameters] [{json: 'string'}]

All parts are optional but if none except the first is given the line is considered a blank line and is a subset of comments.

There is no «command name» part – the first unnamed parameter is used for it and it don’t have to be a hardcoded string as normal parameter interpolation rules apply. If no unnamed parameters are given the command becomes set command – it will assign values to variables given in named parameters and/or JSON string.

Unnamed parameters have two forms:

Named parameters consist of two parts: name=value where name is defined as if it’s an unnamed parameter and value simply continues up until the next parameter or JSON string (if present): name=Voke's vocatives! 'name 2'=Second parameter.

When you need to have a name-looking part in the value double the equal sign (=):

name=This is a single==argument 'name 2'=Second parameter

JSON string starts with { and ends with }. Decoded hash (key/value pairs) is merged with named parameters. JSON strings and named parameters are interchangable and internally the former maps to the latter.

Voke supports relaxed JSON format (regular JavaScript object syntax decoded with eval()) unless disabled by voke.unsafeJSON.

Unnamed and named parameters (but not JSON strings) allow for special insets – subcommands that will substitute themselves with their result.

Variables

As mentioned above if a command has no unnamed parameters its named parameters are used to set variables in the current scope:

; now variables have been set yet.
{var1: 'value', var2: true}
; now 2 variables are set.
var1=new value var3=new var
; now 3 variables are defined: var1, var2, var3
; var1's value was changed from 'value' to 'new value'.

As always, JSON string and unnamed parameters forms are interchangable.

Reading variables is done with the normal command syntax:

=[varName [var2 ...]] [set=value ...] [{set: 'value'}]

Notice the leading equal sign (=).

The last feature makes it convenient to exchange values without a temporary variable:

; read the value of x and y and insert into the expression for if.
if `x` > `y`
  ; set y to the value of x while assigning y to x in parallel.
  y=`x x=``y```
; now x is guaranteed to be smaller than y.

Example of reading multiple variables:

path=`prompt 'Enter path to search'`
mask=`prompt 'Enter file mask'`
printf 'Found %d files.' `find ``=path mask```

Insets

Insets are subcommands that will substitute themselves with their result. Any command is technically an inset embedded into the root scope. A script is a set of such insets which in turn may contain insets.

Insets are created with backticks (`); double symbols (``) act as one raw backtick:

printf 'Found %d files.' `find *.dat`
printf 'Prints a backtick: %s.' ``

Insets have exactly the same syntax as a regular command – so exactly, in fact, that they may contain line breaks and nested subcommands. To prevent confusion with regular script line breaks (and thus command lines) circumflex symbol (^) is put at the beginning of each nested line:

printf 'Found %d files.' `opendir .
^ find *.dat
^ closedir`

Since everything in Voke is about parameters, and insets can be interpolated into them it’s possible to call commands or set variables indirectly:

; sets variable named "toBeSetAs" + entered value:
toBeSetAs`prompt 'Enter a name'`=`prompt 'Enter a value'`
`prompt What command to execute` give_it 2=params

Multiple lines

Line breaks inside insets are completely invisible to the parent script so there can be more than one line-breaking inset or arguments among them (although this hurts readability):

printf 'Found %d DATs and %d DOTs in %s.' `opendir .
^ find *.dat
^ closedir` `opendir .
^ find *.dot
^ closedir` 'current folder'

The main line is perceived as:

printf 'Found %d DATs and %d DOTs.' [INSET] [INSET] 'current folder'

Note that whitespace before circumflex (^) is preserved as it’s important for block commands like if.

Insets can be interpolated inside a named or unnamed parameter (but not a JSON string):

print 'Found `find *.dat` files.'

Just like above line breaks don’t matter here:

print 'Found `opendir .
^ find *.dat
^ closedir` DATs and `opendir .
^ find *.dot
^ closedir` DOTs.'

This is perceived as:

print 'Found [INSET] DATs and [INSET] DOTs.'

Insets can be nested – for this each further level has backticks doubled. There’s no limit except your hardcore nature:

print 'Found `findIn ``opendir ````prompt 'Lookin'' where?'``````` files.'

As you see even though backticks are doubled nested strings are not since they belong to each inset separately.

Scoping

Voke uses closure scoping meaning that every inset gets its own scope inherited from the parent one, which in turn may be inherited and so on up until the script scope, which itself is inherited from voke.global scope.

Scope has two properties: list of defined commands and variables. They both can be overriden on per-scope basis just like in JavaScript. Both can have identical names without collisions.

When reading a variable inheritance chain is looked up from the current scope to the root. Accessing parent scopes is done with circumflex (^):

print 'This scope''s parent''s parent variable: `^^var`'

When setting it the same process happens to determine if it’s defined somewhere up and if it is the value is changed there instead of being set in the current scope only. It’s not possible to shade a variable (create new variable in the current scope with the name already existing somewhere down the stack) unless done by the system (such as in user-defined commands).

The set command is a regular command and returns last non-undefined value assigned:

print 'You''ve entered: `input=``prompt 'Enter something'```.'

; is equivalent to:
input=`prompt 'Enter something'`
print 'You''ve entered: `=input`'

A variable can be of any type – see returning values. It’s possible to assign it undefined value but once done so the variable will be considered unset (it can be set later as usual).

Example

path=.
count=`handle=opendir ``=path``
^ findIn handle *.dat`
print 'Found `count` files.'

Above two scopes are created:

  1. Script-wise (where print executes) with path and count variables;
  2. Inset closure (with opendir and findIn) with handle variable.

When the inset is executed it can access path variable but not count because it’s assigned after it has been executed. Outside the inset the handle variable is unseen.

Returning values

Values are returned by any command but it’s only possible to capture them if they’re used as insets. A value can be of any JavaScript type although most common are null and string.

A script or a multiline inset (which are the same) return the last non-undefined value. Undefined values are returned by comments (unless changed by the application) and undefined variables. Also, commands may return this value (think of this as of C’s void function()).

null and undefined are different things and if the last command returns null it gets returned from the entire inset.

This difference is utilized in a functional way:

printf 'Found %d files.' `handle=``opendir .``
^ findIn handle *.dat
^ closedir handle`

Observe that even though closedir is the last command of the inset the value of findIn is returned although closedir is executed after it. This happens because closedir returns undefined. Would it return null the result would be also null.

If all commands return undefined the result is also undefined. This is the only way to return this type.

Block commands

In Voke, blocks are denoted by indentation (2 spaces or 1 tab per level) like in Python:

if `prompt 'Do this?'` = 1
  print 'Did this.'
else
  print 'Not really.'
print 'Finished.'

Block ends as soon as indentation breaks; blank lines are optional. Block commands are regular Voke commands including if and else and internally simply change instruction pointer based on the next command indentation.

User-defined commands

User-defined and regular commands work in the same way. There are two built-in commands for creating new commands:

function name codeDefines a new or overrides an existing named command in current scope; unlike when setting variables commands in parent scope are unaffected. It’s possible to pass multiline code in the last argument as with insets using circumflex (^).
fn codeDefines an anonymous command and returns its reference that can be assigned to a variable or passed to a function like usual. code can be multiline.

This example finds all numbers below 10 using an anonymous filtering function:

str=`prompt Give some space-separated numbers`
numbers=`split ``str`` ' '`
small=`filter ``numbers`` ``fn if it < 10```

The same effect but using a custom command:

str=`prompt Give some space-separated numbers`
numbers=`split ``str`` ' '`
function keep if it < 10
small=`filter ``numbers`` keep`

As you see there’s no difference between passing an anonymous function and a commanb’s name.

When a command is executed it inherits its parent scope. Note that there is no parameter list – Voke will create new variables starting from p1 for any unnamed paarameter given and also define all named parameters. Unlike normal setting rules even if parent scope had a variable by this name it won’t be modified and instead command’s scope will have its own separate variable. It’s possible to access parent scopes using circumflex (^): `fn print ``^parentVar```.

When writing multiline functions you don’t have to put the first command on the same line since blank lines are comments. You also can use indentation for readability (spaces after circumflex (^) are ignored):

function count
  ^ opendir .
  ^ find ``=p1``
  ^ closedir
printf 'Found %d files.' `count *.dat`
; the following demonstrates that named parameters work as well:
printf 'Found %d files.' `count p1=*.dat`
; ...along with JSON:
printf 'Found %d files.' `count {"p1": "*.dat"}`

Backtick pitfall

Don’t forget that insets are defined with a backtick (`) and execute when passing an argument to a command. Since function is a regular command and the code you pass it is a regular argument if you want to execute an inset when the function itself is executed instead of modifying its code you need to use ``.

This will only ask for location once:

function find
  ^ opendir `prompt 'Where to search?'`
  ^ find *.dat
print 'Found %d files.' `find`
print 'Found %d files.' `find`

And this will ask each time find is invoked:

function find
  ^ opendir ``prompt 'Where to search?'``
  ^ find *.dat
print 'Found %d files.' `find`
print 'Found %d files.' `find`

This is because in the first example prompt is part of the function’s code argument and its result changes what function receives: print `=var` (prints the value of variable var).

In the second example ``prompt...`` is part of the code argument – a regular string – and thus is part of its code as is. When the code executes double backticks are replaced with one creating a normal inset: print ``=var`` (prints string `=var`).

Raw parameter

It’s possible to stop regular parsing of parameter string after a certain number of unnamed parameters were read and treat the rest of it as one string parameter. For example, if command has a raw parameter right from the start:

; match variable 'that' against string 'this'.
if 'this' == that

Other commands like function may have it the second:

; takes 2 parameters: string 'name' and 'code goes=here' - even though it has spaces.
function name code goes=here

Note that it doesn’t work with indirect function calls – all parameters are parsed normally:

func=if
`=func` This will=be erroneous!

if will be given 1 unnamed (This) and 1 named parameters (will=be erroneous!).

Standard commands

Voke provides some essential functionality leaving implmeneting the rest to the application.

if

The fundamental conditional block command. See also complementing elseif and else commands.

Takes one argument – the expression that is evaluated as JavaScript code. Whatever it returns is used as the match result and:

The last feature makes it possible to use if to obtain boolean values or calculate expressions without any nested block:

x=`prompt Enter X` y=`prompt Enter Y`
; prints either TRUE or FALSE.
print 'X is greater than Y? `if x > y`'

Example of using if’s return value:

existed=`if file_exists temp.tmp
^ delete temp.tmp`
print 'File existed and was deleted ok? `=existed`'

There are 4 possible execution paths here:

  1. File temp.tmp existed – if block is executed and the match result is true as returned by file_exists:
    1. delete command has succeeded and returned true
    2. It has failed and returned false
  2. File didn’t exist – the match result is false:
    1. There’s no elseif or else blocks – the result of them executing is undefined and as such the match result (false) is used

unless

The opposite of if – works exactly as it except that its block of commands is executed when the condition didn’t hold. Resulting rules are the same (match value isn’t inverted).

Example (like with any block inset can begin with a blank line or comment):

ok=`
  ^ unless dir_exists temp/
    ^ mkdir temp`
  1. If directory temp existed unless will return true
  2. If it didn’t the match value is false and mkdir will be executed. Whatever value it returns is used as unless’ return value (set to variable ok) – except for undefined, if so match value (result of dir_exists) is used, which is false.

elseif & elsif

These two commands are equivalent and work as traditional else-if construct. See also else.

else

Complements if and elseif defining a set of commands for executing when no conditions hold.

cat

Concatenation command – simply returns the string of all arguments joined together without any separator in between. If passed one argument converts it to string.

function printAll print ``cat ````=``````
printAll one two three staple

function & fn

These two create user-defined named and anonymous commands – see that that section for details.