DWAINE Scripting

From Space Station 13 Wiki
Revision as of 05:15, 12 March 2022 by PNET VICTORNICK (talk | contribs) (Created first version of this page from my original page here: https://wiki.ss13.co/User:PNET_VICTORNICK)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

DWAINE scripting is a useful tool that lacks documentation. To remedy this I created this guide to go over some useful topics in DWAINE. This guide will go over:

  • Script vs Executable
  • How to create Scripts
  • Variables
  • If Else Statements
  • Writing and Reading to and from files in different directories
  • Recursive Scripts
  • While Loops
  • And some DWAINE jank

As this is intended as a beginner tutorial, some things mentioned here can appear as obvious or very simple. However, the tutorial assumes that you have read DWAINE for dummies so you have at least some idea of how files are structured, how to change directories and run basic commands.

Disclaimer

The scripts provided here run well but I cannot guarantee that the way they are written are the most efficient. If you know of a better way to do something, please let me know by leaving a comment here https://www.reddit.com/r/SS13/comments/t0zvf9/beginner_dwaine_tutorial/?utm_source=share&utm_medium=web2x&context=3

Scripts and Executables

Scripts are special files in DWAINE containing instructions to perform some task, with each instruction typically starting on a newline and executed sequentially. In DWAINE for a file to be a script it must begin with a line consisting of only a `#!` . If a file does not being with `#!` you will not be capable of running it by calling it the terminal. To run a script, enter enter its name into the terminal. Another key feature of scripts is that they can take a series of arguments as parameters, processing them, printing and saving them as you wish.

Another special file exists in DWAINE called executables. While executables do take in arguments and can also be run to perform a task, executables do not contain a `#!`and consist of some series of bits. Scripts meanwhile are made up of player readable instruction that are interpreted by DWAINE. I bring this up as `eval gptio x` would return 1 while `eval act x` would return 0. So if you are preforming checks on if a certain script exists you are better to use `eval script_name f` instead.

   Example:
   ]Contents of /mnt/artlab
   ]gptio
   ]act
   ]
   >eval /mnt/artlab/gptio x
   ]1
   >eval /mnt/artlab/act x
   ]0
   >cat gptio
   ]�m�g��e�}��r|��l�s�on�t���ed�e of a����|�. �i�e �o��������s{me{wa��
   >cat act
   ]#!
   ]if $argc 1 lt | echo Error: Please specify equipment to activate! | break
   ]gptio activate $arg0
   ]

In this case gptio is an executable and act is a script.

How to create Scripts

Like described in DWAINE for Dummies you can use the echo command together with output redirection to write a line to a specific file. `echo line ^ filename`

However, writing the following script can be done with just 1 step instead of 3 steps with the help of some new line characters.

   #!
   #This is a comment
   echo Hello World

In DWAINE `|n` is the new line character which signifies the end of one line and the start of another. With it we create our first script in the following way

   echo #!|n#This is a comment|necho Hello World ^ HelloWorld

Verify for yourself that the script produced is correct by entering `cat HelloWorld` into your terminal to view the script contents and `HelloWorld` to run it.

Comments

Anything following the `#` symbol will be ignored. You use this to temporarily remove certain instructions from a script of adding text descriptions for your script. Attempting to create an inline comment caused syntax errors for myself. If you wish to create a comment, I suggest this is done on a separate line.

Variables

Variables in DWAINE are much like variables real life . They are temporary places that can store some pieces of information. To distinguish between each such place, each variable is given a name. What makes variables useful is that

  1. You can assign a value to a variable
  2. You can read a value from a variable

Assigning values

Assigning a value to a variable is described in the eval command for DWAINE.

>Variables may be set by pushing the desired value to the stack and then using the TO operator and a variable name. For example, "5 to HAMDAD" would create a variable with the name HAMDAD and value 5

Something notable, you can assign the result of an eval instruction to a variable. Both `eval 5 to i` and `eval 2 3 + to j` will assign 5 to their variables.

For strings, entering `eval 'Hello World' to i` will assign Hello World to i. Leaving the apostrophes out will only assign the last word in the message to i, due to how the eval command works.

In DWAINE variables can take on the following type of values

  • Integer (could not find the exact range but allows for both negative and positive values)
  • Decimal (only 2 significant digits)
  • Floating Point (only 6 significant digits)
  • String

   >eval -5 to i
   ]-5
   >eval 0.54234 to j
   ]0.54
   >eval 1505052.5023 to k
   ]1.50505e+06

Sadly, I could not find a way to use arrays in DWAINE. Attempting to do so, simply assigns the last value of the array to your variable

   >eval (1, 2, 3, 3, 2) to array
   ]2

DWAINE Jank and variable naming

DWAINE is sometimes janky and behaves in inconsistent ways. Having variables containing capital characters is one of these cases. If we wish to read the value from a variable we will need to put a `$` symbol in front of it. However, if the variable contains a capital this method will not work and the only way of outputting the value of that variable will be to use the eval instruction outside of a script.

I am unsure if this behavior is intended, but this means that sadly you should not use camel case when naming your variables.

   >eval 5 to i
   ]5
   >eval i
   ]5
   >echo i
   ]i
   >echo $i
   ]5
   >eval 6 to J
   ]6
   >eval J
   ]6
   >echo J
   ]J
   ]
   >echo $J
   ]$J

In summary avoid camel case and use:

  • `eval 5 to mynum` to assign numbers
  • `eval ' A message I wish to save to mystring '` to assign strings

Reading values

To read a value from a variable, put a `$` symbol immediately in front of it. To have your script print a value you include something similar to `echo $mynum` as `eval mynum` does not print anything to the terminal when outside of a script. When performing any operations using variables you should also use the `$` symbol as otherwise you will get unexpected results. Here is an example script to show you the proper way and improper way to retrieve values from variables.

Example:

   #!
   echo assign 5 to i
   eval 5 to i
   echo eval i
   eval i
   echo eval $i
   eval $i
   echo eval i 5 +
   eval i 5 + to output1
   echo $output1
   echo eval i 6 + to output2
   echo $output2
   eval $i 6 + 
   echo echoing i
   echo i
   echo $i

Use `echo #!|necho assign 5 to i|neval 5 to i|necho eval i|neval i|necho eval $i|neval $i|necho eval i 5 +|neval i 5 + to output1|necho $output1|necho eval i 6 + to output2|necho $output2|neval $i 6 + |necho echoing i|necho i|necho $i ^ test` to type create this script in one step.

Output:

   >test
   ]assign 5 to i
   ]eval i
   ]eval 5
   ]eval i 5 +
   ]10
   ]eval i 6 + to output2
   ]$output2
   ]echoing i
   ]i
   ]5

Summary of Observations:

  1. Both eval i and eval $i, and any other eval instruction do not produce any output to the terminal.
  2. Attempting to perform addition on i only works if the `$` symbol is in front of it.
  3. Trying to echo a variable without the `$` symbol will only output the variable name to the terminal.

Arguments and Special Variables

Arguments are a special type of variables that are assigned values when running a script. They act as the inputs to a script. The first argument passed to the script is arg0, the second is arg1, arg2 as the third etc. argc is a special argument that tells us the number of arguments that is passed to the script. If the script does not receive some argument, it simply has no value. Here is a script to illustrate this idea.

   #!
   echo This script was given $argc arguments
   echo $arg0 $arg1 $arg2
   echo The end

`echo #!|necho This script was given $argc arguments|necho $arg0 $arg1 $arg2|necho The end ^ arguments`

   Output:
   >arguments]
   This script was given 0 arguments
   ]$arg0 $arg1 $arg2
   ]The end
   >arguments 1 2 3
   ]This script was given 3 arguments
   ]1 2 3
   ]The end
   >arguments 1 2 3 4 5 6
   ]This script was given 6 arguments
   ]1 2 3
   ]The end

Variable Scope

Variable scope refers to where a variable name is valid. This may be a bit of a misnomer to use this term when talking about DWAINE, but I find that this term describes this section well. This section discusses what happens if you use the same variable in two scripts. Does changing the value of a variable in one script change its value in the other? What about the terminal?

From my experimentation I found that if you assign a value to a variable in the terminal, you will be able to read the value of the variable only in the terminal, not in a script. Likewise, if you assign a value to a variable in a script you will be able to read it only in that script, not in the terminal and not in other scripts.

In other words, variables assigned values in a script are local to that script, and variables assigned values in the terminal are local to the terminal.

This holds true for the most case, however, very rarely I was able to set the value of a variable in one script, and in another, assign that value to another variable, which then I could read. I could not however, read that variable directly. In addition I've seen scripts that could intentionally do this, but sadly I could not replicate this behavior. Finally, I once saw a mechanic be able to assign values to arguments. They did not do this intentionally and the result was that passing arg0 and arg1 to a script would result in the same values being assigned to arg0 and arg1 instead of those passed.

Variable Scope exploration:

To begin lets create two scripts called a set and get. The set script assigns 34 to mx and prints a message. The get script tries reading the value from mx without setting it, then tries to assign mx to foo and bar two different ways. We then print mx, foo and bar to see if we succeeded.

   #!
   eval 34 to mx
   echo $mx saved to mx

`echo #!|neval 34 to mx|necho $mx saved to mx ^set`

   #!
   echo $mx
   eval mx to foo
   eval $mx to bar
   echo $mx $foo $bar

`echo #!|necho $mx|neval mx to foo|neval $mx to bar|necho $mx $foo $bar ^get`

To test we first use the unset command to free all our variables. We save 23 to mx and print it to make sure it worked. Then we call get. As we can see, we could not read or save mx to a new variable. We then run unset again and try printing it to make sure that the variable is free. We run set and see that the value 34 was saved to mx. We then try reading the value in the terminal. The terminal says no value was assigned to mx. Finally we run get. Get is unable to read mx or save it to a new variable once more.

   >unset

>eval 23 to mx \]23 echo $mx \]23 get \]$mx \]$mx mx $mx unset echo $mx \]$mx set \]34 saved to mx echo $mx \]$mx get \]$mx \]$mx mx $mx echo $mx \]$mx

This topic in DWAINE still eludes me though I found it still necessary to include it in the tutorial as it is an important topic and it might catch the eye of someone who know more about it than me. If you do know how to make global variables, or know why variable scope is inconsistent in DWAINE please comment or message me. This is bugging me a lot.

If else statements

If else statements are conditional statements that allow us to execute different instructions under different conditions. In DWAINE the general structure of an if statement looks like so:

   if condition | instruction1 | instruction2 | else | instruction3 | instruction4

In this case instruction1 and instruction2 will be executed if the condition is true, otherwise instruction3 and instruction4 are executed. You can have as many instructions executed in each case as you like, just make sure to separate them with a `|` symbol. It can also be written on multiple lines like so:

   if condition | instruction1 | instruction2 | 
   else | instruction1 | instruction2

After the if else statement evaluated the condition and executed the instructions. The script continues as normal, executing the next line.

What is considered true and what is considered false?

I found that 0 is considered to be false and everything else is true. Try this on your own using this script.

   #!
   if $arg0 | echo true | else | echo false

`echo #!|nif $arg0 | echo true | else | echo false ^ truefalse`

Checking Arguments

   #! 
   if $argc 1 lt | echo no argument given | break
   echo $arg0

Sometimes if no argument is passed to your script or the argument is in an incorrect format, your script will break. In the above example if no argument is given then the output of the script will be $arg0. This both looks ugly and weird.

A solution is to write a condition to check if the number of arguments given is less that 1, print a warning message to the terminal, and use the break instruction to end the execution of this script. Break will prevent any instructions below it from being executed.

Reading and Writing to files in different directories

At some point you may want to have scripts write or read information from files. Typically all your scripts will be in your working directory, but if you wish to keep your scripts organized there is a way of doing it.

String Concatenation

String concatenation is the operation of joining two string together. In DWAINE it is done like so

   eval Hello to s1
   ]Hello
   eval World to s2 
   ]World 
   eval s1 s2 + to s3 
   ]HelloWorld

Writing to a file

Writing to files is explained well in DWAINE for dummies. You could to use the echo command with output redirection like so:

    >echo Message to Write ^ location

If the file you are writing to, specified by location, doesn't exist, you will create a new file with the name you specified. Otherwise you will write to that existing file. Note, that as a consequence of this you cannot create multiple files in the same directory with the same name using this method. The location you are writing to can either be a relative path or and absolute path. The location needs to contain the path to the file and the filename

    >echo Message to Write ^ filename

The location where you write your message to can either be a relative path or an absolute path. Having location just be the filename you are writing to, means that the path to the file is just the current directory. DWAINE will look for a file named filename in your current directory. If it is a script that is writing to a file, then it will look for a file named filename in the directory that script is located in.

    >echo Message to Write ^ foldername/filename

In the case that your location consists of two parts, a filename and a foldername, DWAINE will look for the folder with the name foldername in your current directory and a file named filename in that folder. If such a folder does not exist, there will be a syntax error, but if the folder does exist, just not the file, the file will be created. Likewise, using this format when you are writing to a file from a script will look for such a folder in the directory the script is located.

\>echo Message to Write \^ /home/usrvnick0/foldername/filename

Your location can contain the complete path from the root to the file you wish to write to. Using such a format will write to that specific file regardless what your current directory is. Likewise when writing to a file from a script, you can be confident that the script is writing to the file you wanted when using this method.

Writing to files using a script Example

Suppose we have several potential files we wish to write to located in the a folder called location in our home directory (/home/usrvnick0/locations in my case). The path to one such file will look something like this /home/usrvnick0/locations/filename. We can build a script takes takes in the filename as argument 0. We also can choose what is written to our selected file by writing argument \`1 to that file. Such script is shown bellow.

   #! 
   if $argc 1 lt | echo no argument given | break
   eval /home/usrvnick0/locations/ $arg0 + to filename
   if $filename f | echo file found! | else | echo no file found | break
   echo $arg1 ^ filename

This script also checks to make sure that we are not creating a new file when writing some message. An if statement is evaluating if /home/usrvnick0/locations/filename is a file using the `f` operator. This operator returns 1 /home/usrvnick0/locations/filename is a file, 0 otherwise. Run `> help eval` for more details.

Reading from a file

To read from files, we make use of the grep command. Run `> help grep` for more details. In short grep returns the matching content from a file specified by a pattern.

   grep [OPTIONS] PATTERN [FILE...] 

The options portion of the command specifies how we wish to process the contents from a file. Ignore this for now, we simply will be using `-o` . The pattern is a regular expression used to specify a sequence of characters we wish to match. Regular expressions are beyond the scope of the tutorial so I would refer you to the regex wiki page and an online regex tool to test your patterns. I would get syntax errors when putting grep commands in my scripts when I used round or square brackets in the regex pattern section. I would avoid using these characters.

[1](https://en.wikipedia.org/wiki/Regular_expression)

[2](https://regex101.com/)

The File is the filepath and filename of the file we are trying to match content from.

Running grep in terminal will simply print the matching content from the specified file. To capture the matching content in a script we do the following.

`eval $(grep -o PATTERN [FILE...]) to variblename`

You could see what value would be assigned to the variable by running:

`echo $(grep -o PATTERN [FILE...])`

Reading from files Example

Suppose we have two files in our directory called medbay and security that contain the coordinates of each location like so:

   ]Screen cleared.
   ls 
   ]Contents of /home/usrvnick0/coords
   ]medbay 
   ]security cat medbay 
   ]medbay 50cx 50cx 4cz
   echo medbay 50cx 50cy 4cz ^coords/medbay
   echo security 40cx 70cy 4cz ^coords/security

We can use grep to retrieve the x coordinate using grep like so: the \\d{1,3}cx says to look for a digit between 1 and 3 characters in length, ending with cx.

   ]>grep -o \d{1,3}cx coords

\]50cx

We encounter interesting behavior if we assign the result of the grep command to a variable. It drops the cx portion of the result and only assigns the integer! We can use this to our benefit.

   >eval $(grep -o \d{1,3}cx coords) to mx

\]50

We change our directory to /home/usrvnick0 and create the following script.

   #!
   eval /home/usrvnick0/coords/ arg0 + to coords
   if $coords f | echo file found | else | break
   eval $(grep -o \d{1,3}cx $coords) to myx
   eval $(grep -o \d{1,3}cy $coords) to myy
   eval $(grep -o \d{1,3}cz $coords) to myz
   echo $myx $myy $myz

Sadly because of the round brackets we will no longer be able to create this file through the terminal. We will have to write this on a piece of paper and scan it with a scanner. You can usually find one in the netcafe or the research lab. Bellow is a command to copy the scanned document to your working directory under the name getcoords, not you will need sc-location to the name of your scanner.

`cp /mnt/sc-location/document getcoords`

Running getcoords gives the following results:

   >getcoords security
   ]file found 
   ]40 70 4 
   >getcoords medbay 
   ]file found 
   ]50 50 4 >getcoords newlocation 
   ]Break at line 2

So the script is capable of ready the coordinates from a different file and if the file doesn't exist the script doesn't do anything just as expected.

Recursive Scripts

Recursive scripts are akin to recursive programming, they are scripts that call themselves and can be used to accomplish interesting tasks. I've seen the most common application of recursive scripts to be implement while loops. While While loops themselves are possible in DWAINE this is a good example problem to demonstrate how recursive scripts work.

Calling a script from another script

Calling another script is as simple as calling a script in a terminal, simply enter its name and any parameter it needs. Calling a script from a different directory is complicated as the call to a different script must call with the script name. A way to overcome this is to call the cd command inside of your script. Change to your desired directory, then change back.

Here is an example with two small scripts. Caller is calling the script call me. Call me takes one parameter and prints it with a message. Notice that caller script has 2 comments. If they are not included they will create syntax errors as a call to a script begins with the script name. We can still call callme in the new folder by changing to that folder, calling call me, and changing back to our home directory.

   #!
   echo I was called #arg0!

`echo #!|necho I was called $arg0 ^callme`

   #!
   callme Neat
   #/home/usrvnick0/callme Neato
   eval /home/usrvnick0/getcoords to foo
   #$foo neatorino
   cd new
   callme fromnew
   cd
   echo Done

`#!|ncallme Neat|n#/home/usrvnick0/callme Neato|n#eval /home/usrvnick0/getcoords to foo|n#$foo neatorino|ncd new|ncallme fromnew|ncd|necho Done`

   >ls
   ]Contents of /home/usrvnick0 
   ]callme 
   ]new 
   ]caller
   >caller 
   ]I was called Neat 
   ]I was called fromnew 
   ]Done

Simple While Loop using recursion

As a word of caution, be wary when writing while loops on any sorts. If you forget to setup an exit criteria or you iterate in the wrong direction you will create an infinite loop. If you script also prints a message, you will make your terminal nearly unusable. Try depowering the APC in your room or depowering the mainframe room to kill the infinite loop.

Below we have a script that calls itself but with an argument less by one than the original. The script will not call itself if the argument is less that 0.

   #!
   if $arg0 0 lt | break
   eval $arg0 1 - to i
   echo $i
   recursive $i

`#!|nif $arg0 0 lt | break|neval $arg0 1 - to i|necho $i|nrecursive $i`

   >recursive 5
   ]4
   ]3
   ]2
   ]1
   ]0
   ]-1
   ]Break at line 1

While loops

While loops like while loops with recursion are very easy to mess up. Be sure that the condition for the while loop will eventually become false. Try avoiding equivalence statements and opt to use greater than or less than statements if possible.

While loops tale the following form:

   #!
   eval value to var
   while condition on var| iterate var| statement1 | statement2
   new instruction

You need to assign the value of a variable before the while loop. The while loop consists of a condition. If true, the statements for following the condition will be executed, otherwise the next line is executed. As with if else you can have as many statements as you wish. Please note that the statements for the while loop are executed at the same time.

While loop examples

   #!
   eval 0 to i
   while i 4 lt | eval i 1 + to i | echo iteration $i
   echo $i end

`echo #!|neval 0 to i|nwhile i 4 lt | eval i 1 + to i | echo iteration $i|necho $i end ^whileloop`

   >whileloop
   ]iteration 1
   ]iteration 2
   ]iteration 3
   ]iteration 4
   ]4 end

Now for a more complicated example.

   #!
   eval 0 to i
   while i 4 lt | eval i 1 + to i | echo iteration $i | eval i 1 + to i | echo iteration $i
   echo $i end

`#!|neval 0 to i|nwhile i 4 lt | eval i 1 + to i | echo iteration $i | eval i 1 + to i | echo iteration $i|necho $i end`

   >whileloop2
   ]iteration 2iteration 2
   ]iteration 4iteration 4
   ]4 end

We might have expected the output to be iteration 1, iteration 2 newline, iteration 3, iteration 4 newline 4 end. However since the statements are executed at the same time, each iteration i increases by 2 and prints two messages.