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:
- 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).
- 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 onLoad MemFS from solutions
to get a shell where the default filesystem has no restriction on filesize. - 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.
PROGNAME
represents the name of the program to be executedARG
represents an argumentSTREAMNAME
represents the name of an output stream
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 ARG
s).
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.
- 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.
- It’s cooler and easier to show off to people.
- It’ll give you the experience required to use these skills to work on bigger and better projects.
- 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 inc
(although I would reccomend doing it inc
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());
})