STRIL TUTORIAL

LICENSE

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.

INSTALLATION

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.

STARTUP

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.

SYNTAX

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.

ARRAYS

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.

PROCEDURES AND FUNCTIONS

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.

NUMBERS

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.

COMMENTS

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!

FILES

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.

VARIABLES

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.

MANIPULATION OF VARIABLES

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<,w|1. This command should print "abc". It works as a simple split command. The string is spilt at the length of the substring. The first part always has the same length as the substring. The first part is stored in |1, and the second part is stored in |2. It doesn't matter if we write "m|a|b", "l|a|b" or "g|a|b" when the truth level is 2. The verity-command accepts all the three argument types that Stril supports. Instead of "v<2", we could write "d|a<2,u<a,v" or "d|a<2,v|a". The result should be the same in all three cases. If the truth level is 0, all matches are inverted: v<0,d|a<abcdefgh,d|b<abc,m|a|b,h<\0,w|1. In this example, "bcd" should be printed. It has the same length as the substring (|b), and it is the first string of three letters that doesn't contain the substring (|b). If we replace "m|a|b" with "l|a|b", "abc" should be printed. After all, "abc" isn't less than "abc". If we substitute "g|a|b" for "m|a|b", we should get the same result since "abc" isn't greater than "abc". The verity-command may not be very helpful as far as strings are concerned since the result can be hard to predict, but it is quite useful when numbers are compared. In assembly language terms, the effects can be described as follows: When the truth level is 1, match jumps if equal ("je" in assembly language).When it is 0, match jumps if not equal ("jne" in assembly language). When the truth level is 2, match simply jumps ("jmp" in assembly language). In the last case, less and greater behave the same way as match. When the truth level is 1, less jumps if less ("jl" in assembly language). When it is 0, less jumps if not less ("jnl" or "jge" in assembly language). When the truth level is 1, greater jumps if greater ("jg" in assembly language). When it is 0, greater jumps if not greater ("jng" or "jle" in assembly language).

The c-command splits strings in much the same way as the commands above, but it chooses a character based on its place in a string. I takes two arguments. The first one has to be a variable containing the position of the character. 0 is the first position in the string. The q-command can be used to get the last position in the string. As its second argument, the c-command accepts any of the three argument types that Stril supports: d|a<\1,d|b<test,c|a|b. In this example, the second character in the variable |b is chosen. |0 should contain "t", |1 should contain "e" and |2 should contain "st". "Character" may be a misnomer in some cases. It is a byte that is chosen, and some characters may span two bytes (the ones that are above the ASCII range). The c-command doubles as a control statement. The next command is executed if the first argument is less than 0 or greater than the length of the second argument. On success, the next argument is skipped. "c|a|b" could also be written as "c|a" if current points to |b or as "c|a<test".

PRINTING

Some programming languages contain several output commands. In Stril, write is the only output command, but there are important differences within it. If you write "w" or "w|s", you hand it a single variable. It opens that variable and expands the variables it finds within it. If you use the text option, write reacts differently. It expands the variables in the text, but it looks no further. If the variables in the text contain variables of their own, those variables are left as they are. Their names are printed, but not their contents. The difference between the text option and the other forms of the write-command can be exploited to good effect. If you study "text2html.str" in the examples directory, you will find out how this can be done. A trailing newline is only printed if it is explicitly requested by the define command or the <text option of the write command. You can also get a trailing newline by adding it to a variable with the a-command.

OTHER COMMANDS

The h-command is central to decision making in Stril. It accepts all the three argument types that Stril understands. The h-command hurdles n lines. N has to be a true number. "h<1" hurdles the rest of the current line and the next 49 lines since "1" has the ASCII value of 49. "h<\1" hurdles the rest of the current line and the next line. It can also be written "d|a<\1,u<a,h" or "d|b<\1,h|b".

The q-command has only been covered in the passing. It quantifies the second argument and stores its length in the first argument. The second argument can be any of the three types that Stril supports, but the first argument has to be a variable: d|a<test,q|b|a. |b should now contain the number 4 (\4). The first byte in a string occupies position 0. The output of quantify should be reduced by one before it is used as the first argument of the c-command to get the last byte in a string: -|b<\1,h<\0,c|b|a. |1 should now contain "t" (the last letter in the string). Quantify doubles as a control statement. On success, the next command is skipped. If the second argument is empty (has zero length), the next command is executed.

The x-command is the system command in Stril. It allows you to execute Linux commands. Their output cannot be read directly into variables, but it is possible to redirect the output to a temporary file and read the file into variables. The x-command takes a single argument, and it understands all the three argument types that Stril supports. If you need variables in the command string, you have to use define and attach to create the argument: d|a<1.txt,d|b<2.txt,d|5<mv ,a|5|a,a|5<\w,a|5|b,x|5. In the example, the command string will look like this for Stril and /bin/sh: mv 1.txt 2.txt. Variables are normally only expanded by the write command. If you want to execute a literal command, it is much simpler: x<ls -lh 1.txt.

The y-command yawns or pauses for a number of seconds. It accepts any of the three argument types supported by Stril: d|a<\9,u<a,y. In this example, Stril should pause for nine seconds. We could also write "d|a<\9,y|a" or "y<\9".

The z-command sets the zone separator, an internal variable that influences the behaviour of the r-command. Reading stops each time the zone separator is encountered. The zone separator is stored unless it is a newline. Initially, it is. The zone separator is a distant relative of the field separator in Awk. The field separator splits a string, but the zone separator terminates a read operation, making it possible to split a line and read the parts into different variables. The zone separator has to be a single byte, but backslash codes can be used to produce that byte: z:<\w. After this command, reading stops each time a space is encountered AND each type a newline is encountered. The command above could also be written "d|a<\w,u<a,z" or "d|a<\w,z|a".

EXIT

Stril exits when the end of the main program file is reached and if the main program file is closed. If the main program file is closed, it will exit with exit code -1, indicating an error: d|a<,f|a<9. If you want to force an exit indicating success, "e<9" should do the trick. Some programming errors will cause a premature exit after an error message, while other programming errors are silently forgiven.

ERRORS

Stril stops at the first error it finds. Line numbers are not given since Stril has limited counting abilities, but the command where it occured is printed, and so is the file slot it is found in. Stril is able to catch most formal errors, but it does not attempt to judge the quality of the program code. It would not be sensible to define a jump point in the main program file and try to jump to it in a different file, but Stril will not catch such an error.

Launch errors may occur at program start-up. A launch error means that the initial program file could not be opened or that the command line argument was too long (more than 127 bytes). A failure by the p-command to switch to a new main program file, will also result in a launch error. All files are created on the fly if they do not exist already. If Stril fails to open a file, the problem is likely to be caused by lack of privileges. If the main program file is created on the fly, Stril will exit immediately without an error message since the main program file is empty.

A parameter error occurs when a parameter is outside the legal bounds of a command. It also occurs when an empty argument is used where content is required.

Syntax error is used for everything else, mainly missing separators and illegal variables.

EXAMPLES

The examples directory contains a lot of examples. Many of them were originally written for previous versions of Stril. They have been changed to work with the current version, but in many cases, they could benefit from a more complete rewriting. The examples in the directory are uncommented. I have therefore enclosed a few commented examples below.

1,r,h<\0,w<|'\a,j<1. In plain language: Store the address. Read a line from standard input and store it in the variable current points to, initially |4. Skip the rest of the line if nothing was read. Write the line and add a newline. Jump back to the stored address and do it all again. This piece of code emulates cat and expects to be fed a file through a pipe or redirection.

Reset is a tiny program. It resets your terminal. On my system, it is 18 kilobytes in size. This is the source code of reset in Stril: w<\rc. Reset is 6 bytes in Stril.

If you write the infamous "Hello, world!" program in assembly, the source code should take some 300 bytes. This is the source code in Stril: w<Hello\x world! . The \x becomes a comma. A comma may not be used directly in a text option. It would end the text option and cause Stril to expect a new command.

LIMITATIONS

Stril is fairly slow. In most cases, the lack of speed won't be a problem on a modern computer, but Stril shouldn't be used for large programs or programs where speed is really important. Stril cannot be used for mathematics. It handles strings and texts well enough, but its size prevents it from performing some text processing tasks. Sorting requires large arrays, and they are not available in Stril.