If you’ve stumbled upon this page you’re probably trying to write your own programs in the simulator. There’s a couple things you’ll need to know:

  1. If possible, try writing real programs on a linux system. There’s a whole lot of things to learn that our simulator doesn’t support (like shebang lines).
  2. The maximum filesize on the default filesystem is 288B. If you want to play around with bigger files (you’ll almost certainly need to if you want to implement a filesystem), use MemFS in section 10. Click on Load MemFS from solutions to get a shell where the default filesystem has no restriction on filesize.
  3. The easiest way to write content into files from the simulator is using our simulator’s edit utility.

With that out of the way, let’s get started!

The skeleton of a program

Your program should be an async function wrapped in parentheses. A sample program may look like this:

(async function (command){
    await this.filesystem.write(command.output, str_to_bytes("hello world!\n"));
    return 0;
})

By convention, return 0 indicates that everything went well. To return an error, either use the throw keyword in javascript, or call this._return_error(error_msg_str).

Let’s take a look at the components that make this possible.

The Shell object

Your program will be executed under the context of the shell. This means that this will refer to an instance of Shell from /js/shell.mjs.

Below is a description of all availible attributes/functions you may use. Note that names with parentheses next to them are functions.

this.filesystem

This object is a filesystem (instance of DefaultFS). See the section on interacting with the filesystem below.

this.current_dir

This variable tells you what the current working directory is.

this.umask

If you’re creating a new file and don’t know what permissions to give it, just set it’s mode to be the umask.

this.input_path

The absolute path from with you should read for input (i.e. stdin). Note that these reads may block so you should use await on the read call. See the section on interacting with the filesystem below.

this.output_path

Path which points to the shell’s stdout. Note that this may not be the same as your program’s output! Use command.output instead. See the section on the command object instead.

this.error_path

The path to the shell’s stderr. Use this.stderr instead as that is an already open handle to the error path.

this.stderr

An instance of FileDescriptor where you should write all error/debugging output. See the section on interacting with the filesystem below.

this.create_extended_input(content)

Create a <textarea> to allow a user to have a GUI text editor like edit. To actually read that input see this.get_extended_output_and_cleanup.

To pass in some initial text to populate the <textarea> with, pass in a string as content.

this.get_extended_output_and_cleanup()

Wait for the user to press save or cancel. Returns the contents of the <textarea> if the user pressed save and null if the user pressed cancel.

This function may block, so call it with await.

this.run_command(command)

Run a command as if it had been parsed from the command line and return the output of the program that is run.

this.path_join(path1, path2)

Joins two paths and removes/adds any necessary /s.

this.expand_path(path)

Turns a relative path into an absolute path. Expands a path by looking at the current directory and resolve any . and ..s encounted along the way.

Note that this doesn’t check if the path actually corresponds to a file on the filesystem.

this._return_error(error)

Write a string (error) to stderr and return that string.

The Command object

You might have noticed that all our programs so far have been taking a parameter command, and some have even been grabbing a FileDescriptor corresponding to stdout from command.output.

The command object gives us a way to interact with specified command parameters.

A command is a string that (roughly) follows the regex below:

\s*PROGNAME\s*\s(ARG\s*)*(>>?\s*STREAMNAME)?\s*

where \s represents any whitespace.

For each of the “variables” defined above, it must either be any character that is not whitespace, and not >, although it may contain whitespace and > if they are either preceeded by a \ or if a substring containing them is surrounded by ".

The presence of a " mandates a corresponding closing ". To have a variable contain a literal ", escape it with a \.

Note that whitespace at the beginning and the end of a line will be lost (including a newline) when parsed by a shell. (But not if parsed as contiguous input to either parse_command or resume_parse_command).

A single ‘>’ implies that the output should be redirected to STREAMNAME, >> implies that the output should be appended and should not overwrite the destination.

Let’s look at how you can construct/use it.

static parse_command(input)

This static method parses a string input as a command. If the input is incomplete (i.e. has a trailing \ or an unmatched "), it raises a ParsingError.

static resume_parse_command(input, state)

This static method parses a string input as a command and continues parsing from some state, presumably obtained from a ParsingError.

[Constructor] Command(input)

Takes in a string input that will be parsed.

this.output

The object returned by a constructor will store the output path here.

If you invoke a program via run_command this object will be an instance of FileDescriptor, opened with O_WRONLY and if this.append_output is set, with O_APPEND otherwise with O_TRUNC.

this.append_output

Specifies whether output should be appended or overwritten on the output path.

this.arguments

A list of command line arguments passed to the program (i.e. PROGNAME and all ARGs). Note that command.arguments[0] is the name of the running program.

Interacting with the filesystem

The shell’s filesystem is accessible to your function via the this.filesystem variable.

One thing we haven’t mentioned is that in our simulator all filesystem operations are assumed to be asynchronous. This is to allow for things like simulated disk delay in the animated filesystem interactions, or to allow for blocking reads from stdin in the simulator.

Anytime you access the filesystem, use the keyword await right before the function you wanted to call.

For example, the following program tries to read 5 chars from stdin and writes the characters read to both stdout and stderr.

(async function(command) {
    var fd = await this.filesystem.open(this.input_path, O_RDONLY);
    // Let's make sure that we opened stdin correctly
    if (typeof(fd) === 'string')
        return this._return_error(fd);

    var buffer = new Uint8Array(new ArrayBuffer(5));

    // perform the read and check for errrors
    var bytes_read = await this.filesystem.read(fd, buffer);
    if (typeof(bytes_read) === 'string')
        return this._return_error(bytes_read);

    // create a "view" of the buffer that only has the initialized bytes
    var read_view = new Uint8Array(buffer.buffer, 0, bytes_read);

    // write some debugging messages to stderr
    // I've ignored the error checking on the writes.
    await this.filesystem.write(this.stderr, str_to_bytes("Read " + bytes_read + " bytes:\n"));
    await this.filesystem.write(this.stderr, read_view);
    await this.filesystem.write(this.stderr, "\n"));

    // write our output to stdout
    await this.filesystem.write(command.output, read_view));

    // everything was ok!
    return 0;
})

Writing a filesystem as a program

Full disclaimer, it’s probably a better idea to go learn how to write a real POSIX filesystem that can run on something like linux or macOS for a number of reasons.

  1. It’s a better learning experience and you’ll either learn a ton about kernel modules or about FUSE, or generally, about some well documented, relevant, and useful software stack.
  2. It’s cooler and easier to show off to people.
  3. It’ll give you the experience required to use these skills to work on bigger and better projects.
  4. There’s FUSE bindings to just about every language (like fusepy for python) so you don’t have to be locked into a gross language like javascript or forced to write a bunch of boiler plate code in c (although I would reccomend doing it in c anyway).

Assuming you don’t have the necessary resources/motivation to do it with legit libraries or you’ve got an hour or so to kill, continue trying to implement your own filesystem for our simulator. Don’t say I didn’t warn you.

To implement a filesystem for mounting, you need to write a program that returns a filesystem when run.

To see more information on how to write a filesystem, read /pages/filesystem-operations.html and /pages/10-mounting.html.

Then run mount path [path_to_fs_program_filename] to mount your filesystem. It may look something like this:

(async function (command) {
    class SampleFS extends DefaultFS {};

    SampleFS.prototype.readdir = function() {
        console.log("Hello world!");
        return [ new Dirent(0, '.'), new Dirent(0, '..')];
    };

    return (new SampleFS());
})