Stril is currently proprietary software, written and owned by Jon-Egil Korsvold. It is available for beta testing at your own risk. There is no warranty. Bug reports and feedback should be sent to jonegilkorsvold@gmail.com. The license for beta testing expires on January the first, 2020. By that time, Stril will be made available under another, reasonably permissive license. I will probably reserve the right to make money from it.
The Stril binary should be ready to run on any Linux system. It is not necessary to reassemble it if your shell is /bin/sh and expects a command string after the -c switch. If you need to change this setting, you have to edit "arg1" and "arg2" near the end of stril.asm and reassemble the binary. The current version of Stril will only run on a Linux system. Copy stril to anywhere in your path: cp stril /usr/local/bin/;chmod +x /usr/local/bin/stril. If you want to use the scripts in the examples directory, you should follow the same procedure. In addition, you need to edit the first line in each script to reflect the new location of stril. Two of the scripts (text2text.str and text2html.str) need to find resource files (txt.stl and html.stl). The full path to these resource files should be added to line 3 in text2text.str and text2html.str. It is not really necessary to install Stril. It is possible to run it where it is once you have extracted the zip archive, but you need to make sure that the binary and the scripts are executable. It would also be sensible to add the location of stril and the scripts to your path whether you install it or not.
To start Stril, you have to feed it a program file: stril prog.str (enter]. If the program file is executable, you can also do it like this: prog.str (enter). It will only work on a *nix system, and only if the first line in prog.str calls Stril properly: #!/path/to/stril. Stril has to be started from the command line. Some programming languages require a special startup procedure. An Awk script has to start with BEGIN {. That is not the case with Stril. You can start feeding it commands right away. When Stril starts, it stores the script name in the variable |0. The rest of the command line is stored in |1. These variables will be overwritten by the split commands. If you are going to use the contents of |0 and |1 later, you need to save them to variables in the range |5-|9 or |a-|z.
In addition to the first line (#!/...), Stril programs consist of statements. Each statement is a command followed by arguments. Statements are separated by comma or newline. Comma can also be used as a dummy command with commands that require an action. Less than (<) signifies a text option, and "|'" is used as a marker for current. All commands are single letter ones, written in lowercase. The same applies to variables. Variables in the range a-z and 0-9 are allowed, and each variable can have a length of up to 127 bytes (0-126). All variables have the same prefix, "|". No space is allowed between the prefix and a variable. Some variables are more or less reserved. |0 holds the script name at startup, and |1 holds the rest of the command line. |0, |1 and |2 are used by character, match, less and greater. |3 is used to store the number of read bytes after each read-operation. |4 is the variable that current initially points to.
Current is an internal variable in Stril. It points to another variable, a feature that is very useful in loops. The u-command decides which variable current points to. It understands all the three argument types that Stril supports. If you use the text option, current is defined explicitly: u<a. Current now points to |a. The two other forms of the u-command behave differently: d|a<test,u<a,u. First we define |a as test. Then we point current to |a. Finally, we use the u-command with nothing as a parameter. It reads the variable that current points to, gets the first byte and gets the name of the variable from that byte. After the final u in the example, current points to |t (the first t in test). "d|a<test,u|a" would produce the same result.
Commands can be distinguished from variables since they do not have any prefix. The syntax is fairly uniform. Stril commands accept three types of arguments, notably nothing, a variable or a text option. No command may have more than two arguments, and all commands require at least one argument. There are only two exceptions, the commands that store jump addresses (0-4) and the commands that store bookmarks (5-9). They do not accept any arguments. These are the commands that require two arguments: a, c, d, f, g, l, m, n q, s, + and -. The first argument has to be a variable.
Nothing usually means that current is to be used. The +-command adds numbers and is a good example. Consider the following line: "u<a,d|'<\1,d|b<\2,+|b,,". In plain language, we can say: Use |a as current, define the contents of current as one, define |b as two, add current to |b. In the expression "+|b", the absence of a second argument means that current is used. Instead of "+|b", we could write "+|b|a". We just made sure that current points to |a. Adding |a to |b is obviously the same as adding current to |b. This is the third form of the +-command: "+|b<\1". We just defined current as one, so the result should be still be 3.
The commands +, -, c, h, n and y (add, subtract, character, hurdle, num2string and yawn) need true numbers as input. \1 means one while 1 means 49! The rest of the commands interpret input as letters (1 means one) except when backslash codes are explicitly used, for example in a definition.
The mathematical operators (+, -, n, s) and the split commands (m, l, g, c) double as control statements. The same applies to attach (a), read (r) and quantify (q). They all work the same way. On success, they skip the next command. On failure, they execute it.
The variables a-z can be used as a very limited array. It is possible to loop through several variables: d|9<a,u|9,+|9<\1,h<\0,u|9. First you define |9 as "a". Then you use it as current. Current now points to the variable |a. Then you increment |9. It now contains "b". If you issue the command "u|9", current will point to the variable |b. It is important to make sure that the contents of a variable starts with a-z or 0-9 before you try to use it as current.
In most programming languages, it is possible to define a function or a procedure and call it repeatedly from different locations in the program code. A return command or a break command will transport you back to a location immediately after the call to the function or procedure. This is not the case in Stril.
0, 1, 2, 3 or 4 defines the start point of a jump. The commands j<0-j<4 will jump to it. The command j<0 will jump to the point defined by 0 (or to the start of the program file if it is undefined). The command j<1 will jump to the point defined by 1 and so on. The return address is different, so jumps may be nested. You can put a 1-jump inside a 0-jump, but you cannot put a 0-jump inside a 0-jump.
All jumps create an eternal loop, so you need to combine jumps with control statements: "d|a<\2,u<s,1,r,h<\0,w,+|a<\1,h<\0,l|a<\3,h<\0,j<1". Here |a is defined as 2, and |s is used as current, then we define a jump point (1), reads a line into |s (r) and prints the line with "w". We increment |a and jump to 1 if |a is still less than 3. It isn't, so we never start looping. Read, add and less double as control statements. On failure, they execute the next command, in this case h<\0: Skip the rest of the line if nothing was read, if the add-command failed and if |a isn't less than 3.
In Stril, it is not possible to define a function in a program file and call it repeatedly from different places in the same file, but Stril can have several open files and switch code source. It is possible to define a function in a separate file and run it repeatedly from a different code source. Jump points can be redefined, so it is also possible to define the same functions several times in a program file. That is a simple copy and paste operation in any modern text editor.
Most programming languages can do a fair amount of math. Stril can count from 0 to 126, and that is it. No more is required to reference the ASCII range of characters and the positions in a 127 byte buffer. The add-command takes a non-empty variable as its first argument and any of the three argument types in Stril as the second one. If the result of the addition is greater than 126, the operation is interrupted and the next command is executed. On success, it is skipped. The subtract-command (-) works the same way, but the operation is interrupted if the result of the subtraction is less than 0. These commands double as control statements. It is possible to use them to check if a byte is inside the ASCII range: +|a<\0,h<\0,w|a. Skip the rest of the line if the first byte of |a is greater than 126, print |a if its value is 126 or less.
In addition to add and subtract, the s-command (string to number) and the n-command (number to string), can be counted as mathematical operators. The s-command allows you to define any number between 0 and 126 explicitly. The n-command allows you to show the numerical value of a letter in the ASCII range. The syntax is similar to the add command and the subtract command. The s-command and the n-command double as control statements and execute the next command only on failure.
You may, of course, comment your code. Stril expects the first letter after a comma or a newline to be a command. If it isn't, Stril ignores anything up to the next comma or newline. Any uppercase letter can be used to start an inline comment. That is also true for leading variables, for example |a. If you open the program file as an input file, you can read it and print it. The variables will then be replaced with their contents. This feature makes it possible to preprocess your code. A comment can also span an entire line. If you want it to do so, you should start the line with the h<\0 command. Stril doesn't preparse your code. It is therefore impossible to jump towards (but not to) the end of a file. Consequently, comments are parsed just like any other command. Excessive comments will slow down your scripts unless you place your comments after the exit command at the end of the script. If you do so, you do not have to use any comment initiators. Comments inside loops carry the greatest speed penalty. Such comments should always be avoided!
Stril can act as a filter. In other words, it can read from standard input and write to standard output, and that is the default action, but Stril can also work with files. We have already mentioned input files and program files. These terms may be misleading. Stril knows about streams, files and slots. The standard streams are always open and occupy the first three slots. Standard input occupies slot 0, standard output occupies slot 1 and standard error occupies slot 2. To open a file, you need a filename and a slot: d|a<1.txt,f|a<3. This command will open 1.txt in slot 3. Slot 9 is the last valid slot, but it is reserved for the main program file. If you want to close a file, you will still use the file-command, but the first argument should be empty: d|a<,f|a<3. This should close the file in slot 3, but it is not strictly required. Open files are closed when Stril exits. If you open a new file in an occupied slot, the old file is automatically closed before the new file is opened. It is not possible to close the standard streams or open files in slots 0 through 2. It is impossible to open the standard streams in any of the slots in the range 3-9. All files are opened in read-write and append mode. If they do not exist already, they are created on the fly. All write operations occur at the end of the files.
When a file is opened, it isn't clear whether it is an input file, a program file or an output file. As a matter of fact, it can be any of the above or even all of the above. If you issue the command d|a<3,u<a,p (Store 3 in |a, use |a as current, p), you will use the file in slot 3 as a program source. "d|a<3,p|a" will do the same. The third valid form of the same command is "p<3". The i-command switches input, and the o-command switches output. They have the same syntax as the p-command. It is quite possible to use the same file for input and output. The technique is demonstrated in "add-pim.str" in the examples directory. It is also possible to use a program file as an output file since all write operations occur at the end of the file. You'll probably crash if you try to use the same file as program source and input simultaneously. That would indeed be rather tricky.
Stril provides some ways to navigate files. Streams are unseekable, so they can't be navigated. The t-command goes to the top of the file in the chosen slot. The e-command goes to the end, and the b-command goes backwards a single element. The definition of an element depends on the value of the zone separator, but an element is at most a single line. These commands have the same syntax as the p-command, the i-command and the o-command. "e<3" jumps to the end of the file in slot 3.
It is possible to store a jump address in the current program file. That is done with the commands 0-4. They do not take any arguments. The commands 5-9 work the same way, but they operate on the current input file. These addresses are used by the jump command. It takes a single argument: d|a<3,u<a,j. In this example, the argument is nothing, so we know that current is used. Current points to |a, |a is defined as 3, jump to the address that was stored when 3 was used as a command. "d|a<3,j|a" does the same as the example above. The third form of the command is "j<3". When the argument of the jump command contains a value between 0 and 4, we jump to a location in the current program file. If we have switched program file since we issued the 3-command in the examples above, we are likely to crash. If the argument of the j-command contains a value between 5 and 9, we jump to a bookmark in the current input file. A program file or an input file may be used for output as well, but jumps have no effect on output. All write operations occur at the end of the files.
In Stril, variables can be populated in several ways. You can read into a variable with the r-command or the k-command, you can define it explicitly and you can attach a variable or some text to it. Many commands, for example quantify, also write directly to variables.
The read-command has only two valid forms, "r" and "r|var", for example "r|a". The first version (r) reads into current, while the second one reads into a named variable. The input command decides where read gets its data from: i<3,r. In this case, the file in slot 3 is being read. Read stops when the zone separator is encountered, at the first newline and when the buffer length is reached. It doesn't store trailing newlines. In Stril, it is far easier to add a trailing newline than it is to remove one. Read stores the number of bytes it has read in |3, and that includes trailing newlines. In some cases, it is important to know if a newline has been read: r|a,h<\0,q|b|a,,g|3|b,h<\0,w<\a. This piece of code prints a newline if |3 is greater than |b. |b contains the length of the string that was stored by read. Quantify is used to get the length of that string (q|b|a) and store it in |b. Read doubles as a control statement. When the end of the file is reached, it executes the next command. On success, the next command is skipped. Read merely stores what it reads. It doesn't interpret or convert backslash codes.
The k-command stores a single keypress. It has the same valid forms as the read command (k, k|var). Like the r-command, it reads from current input, cfr. the i-command. The k-command doesn't echo the character it reads. A subsequent write operation is necessary if it is to be shown: k|a,w|a. The k-command doesn't store the number of bytes in |3 since it always reads a single byte. If it reads a newline, it is stored like any other byte. The k-command is supposed to succeed, so it doesn't double as a control statement either. It is meant to be used for user input.
The contents of a variable can also be defined explicitly with the d-command. It takes two arguments. The first one has to be a variable. The second argument can be nothing, a variable or some text. "d|a" defines |a equal to the variable current points to. "d|'|a" copies the contents of |a and overwrites the variable current points to. "d|a<Test" stores "Test" in |a and overwrites any previous contents. If the second argument is empty, the contents of the first variable are deleted. The text option of all commands interprets backslash codes, but the define command also interprets and converts backslash sequences in variables. If you want to avoid it, you should use the define command to delete a variable, and then use the attach command on the empty variable. Backslash codes are explained in the command list. Another way to avoid interpretation of backslash codes is to escape them with double backslashes.
The a-command attaches content to a variable. It has the same syntax as the define command. The a-command interprets backslash sequences, but only when the text option is used: a|b<\a. In this example, a trailing newline is attached. Since the a-command adds to the length of an existing variable, there is a risk of buffer overflow. That is why the a-command doubles as a control statement. On buffer overflow, the operation is aborted and the next command is executed. On success, it is skipped.
No programming language would be complete without tools to manipulate a variable once it has been populated. Stril doesn't contain a single substitution command, but it contains several split commands. If you want to delete a part of a string, you split it first. Then you print the parts you don't want to delete. If you want to change a part, you use the d-command to change it.
Match requires two arguments, first the string and then the substring. The first argument has to be a variable. The second argument can be any of the three types supported by Stril. First we define the string: d|a<abcdefgh. Then we define the substring: d|b<bc. Now we issue a command: u<b,m|a. In this case, current is equivalent to |b and holds the substring. We could also write "m|a|b" or "m|a<bc". All of the three examples should produce the same result. |0 should contain "a", |1 should contain "bc" and |2 should contain "defgh". The match always ends up in |1. In this example, a match was found, and the split operation succeeded. On success, the next command is skipped. On failure it is executed. Most errors are treated as an unsuccessful comparison: d|a<b,d|b<cd,m|a|b,h<\0,w<|0|1|2\a. The substring is longer than the string, so we know that the comparison is unsuccessful. The command "h<\0" is executed, and the rest of the line is discarded. The print statement is never reached. When match is used to compare two bytes (when the first argument is a single byte), it doesn't try to split the string, and the variables |0, |1 and |2 are left unchanged.
Greater and less are siblings of match. Greater checks if the first argument contains a string that has the same length as the second argument, but still is greater than the second argument. "b" has a higher numerical value than "a", so "bc" should be greater than "ab". Less checks if the first argument contains a string that is less than the substring, but of the same length. "ab" should be less than "bc". In all other respects, less and greater work the same way as match. The match always ends up in |1, and the match always has the same length as the second argument. Let us try the same example as above: d|a<abcdefgh,l|a<bc. In this case |0 should be empty. "ab" is less than "bc", and "ab" is at the start of the string. |2 should contain the part after the match (cdefgh). Let us try greater: d|a<abcdefgh,g|a<bc. "cd" should be greater than "bc". |0 should contain "ab", |1 should contain "cd" and |2 should contain "efgh". Note that these commands always yield the first match. If |0 is empty, the match is at the start of the string. If |2 is empty, the match is at the end of the string.
The verity-command has the ability to change the behaviour of match, less and greater. They behave as described above if the verity or truth level is 1. If it is 2, all comparisons are true: v<2,d|a<abcdefgh,d|b<def,m|a|b,h<