viteshell

viteshell

A minimalistic shell implementation written in TypeScript.

GitHub Workflow Status npm GitHub release (latest SemVer) npm bundle size GitHub

Table of Contents

Overview

According to the GNU Bash Reference Manual, a shell is simply a macro processor that executes commands.

viteshell is made to be used in interactive mode only, that is, accept input typed from keyboard. It is intended to work synchronously; waiting for command execution to complete before accepting more input. Just like real shell programs like dash, bash, fish or zsh, viteshell provides a set of built-in commands as well as environment variables. However, it does not contain or implement it’s own file system.

Everything you need to know about how viteshell works is discussed in the subsquent sections.

Live Demo

The demo allows you to explore the features of viteshell and interact with the key functionalities. Click the link below to access the live demo:

🚀 Live Demo

Getting Started

To get started with viteshell, you will need an interface through which you can enter commands and also view output of the executed commands. The interface can be the browser console or a remote server or an in-browser terminal emulator(like xterminal or jquery.terminal or xterm.js).

Installation

The latest version of viteshell, packaged and published on npm, can be installed into a new or existing project in a number of ways:

Initialization

To use viteshell, first create a new instance of it

<script>
    const vsh = new ViteShell();
</script>

State Management

viteshell maintains it’s own state used during command execution. It contains a list of aliases, environment variables and command history.

Aliases

These are basically placeholders of other commands. They allow a string to be substituted for a word when it is used as the first word of a simple command. The shell’s alias object can be accessed using vsh.alias.

Example: set print as another name of the echo command

vsh.alias.print = "echo";
// or
vsh.alias["print"] = "echo";

Example: unset print alias

vsh.alias.print = undefined;
// or
delete vsh.alias["print"];

Using viteshell’s alias and unalias built-in commands, aliases can be set and unset.

By default, viteshell provides a number of aliases that may be commonly known for example: cls for clear, print for echo and so on. View all of them in your console using: console.log(vsh.alias);

Environment Variables

The shell uses variables to store pieces of information for its internal operations such as parameter and variable expansion. These variables are stored in the vsh.env object.

Example: changing the default prompt PS1 to #

vsh.env.PS1 = "# ";

Example: a compound variable (contains other variables)

vsh.env.PS1 = "$USERNAME@$HOSTNAME # ";

There exists export built-in command for setting and displaying these variables.

History

Whenever the shell recieves input, it is stored in the history object. viteshell does not store similar and consecutive inputs.

To access the entries in the history: vsh.history

console.log(vsh.history);

Backup and Restore

You can backup and restore the state of the shell using vsh.exportState() and vsh.loadState().

Example: backup state

const backup = vsh.exportState(); // JSON string

localStorage.setItem("shell", backup);

Example: Restore state automatically

window.onload = () => {
    const backup = localStorage.getItem("shell");
    vsh.loadState(backup);
};

Commands

A sequence of words provided as input to the shell denotes a command. For example: echo 1 2 3 4 denotes a simple command echo followed by space separated arguments 1, 2, 3 and 4. When executed, the shell captures it’s exit status and saves it in the environment variable: ?

A combination of several simple commands separated by the shell’s special character (delimiters) results into a compound command. In viteshell, delimiters include chaining characters and pipes.

Builtin Commands

viteshell comes with buitlin commands most of which can be re-implemented by you.

To programmatically view all registered commands, use

const list = vsh.listCommands();
// ['alias', 'date', ...]

List of Builtin Commands

Custom Commands

You can add custom commands like hello:

vsh.addCommand("hello", {
    desc: "A command that greets the user",
    usage: "hello [...name]",
    action(process) {
        const { argv, stdout } = process;
        if (argv.length) {
            stdout.writeln(
                `Hello ${argv.join(" ")}.\nIt is your time to shine.`
            );
        } else {
            stdout.writeln(`Opps!! I forgot your name.`);
        }
    }
});

Simply remove a command using

vsh.removeCommand(/* name */);

Callbacks

You need a terminal interface for inputting textual commands and outputting data.

Below is a generic setup to register callback functions for output handling and shell termination.

// triggered when outputting data
vsh.onoutput = (data) => {
    /* print data */
};

// triggered when writing errors
vsh.onerror = (error) => {
    /* print error */
};

// triggered when the `clear` command is issued
vsh.onclear = () => {
    /* clear output display */
};

// triggered when the `exit` command is issued
vsh.onexit = () => {
    /* cleanup */
};

Output and input streams can be implemented to make use of the in-browser console or a remote server or web-based terminal emulator(like xterminal, xterm.js, jquery.terminal).

In case you want to use a terminal emulator, XTerminal provides a simple interface (recommended). Checkout vix, a starter template for viteshell and XTerminal. Learn how to use xterminal here.

Otherwise, for simplicity, add two elements in your markup: #input for capturing input and #output for displaying output.

<div id="output"></div>
<input id="input" type="text" />

Now connect your shell to utilize those elements as follows:

const vsh = new ViteShell();

const output = document.querySelector("#output");
const input = document.querySelector("#input");

vsh.onoutput = (data) => { output.innerHTML += data };
vsh.onerror = (error) => { output.innerHTML += error };
vsh.onclear = () => { output.innerHTML = "" };

input.onkeydown = (ev) => {
    if (ev.key == "Enter") {
        ev.preventDefault();
        vsh.execute(input.value);
        input.value = "";
    }
};

Note: The vsh.execute() is discussed in command execution section.

Activation

Now activate the shell to prepare it for command execution with an optional greeting or intro message.

vsh.reset("\nHello World!\n");

The above line not only resets or activates the shell but also prints the greeting message followed by the default prompt.

At this point, it should be working just well.

Command Execution

Executing commands

Since you have connected your shell to an input/output stream using callbacks and it is activated, you can now execute commands:

const exec = async (input) => {
    // ...
    await vsh.execute(input);
    // ...
};

exec('echo "Hello World!"');

Timeout

Set an execution time limit beyond which the execution of a command is aborted.

Example: All commands must execute to completion in 5 seconds otherwise timed out(aborted).

vsh.setExecutionTimeout(5);

Chaining and Pipes

Chaining

Sometimes you need to run commands basing on the success or failure of the previously executed command or just normally.

Example:

Pipes

Use | to pipe the output of one command as input to another command.

Example: To search for occurences of the word (clear) in the output of help: help | grep clear

Abort Signal

To abort an executing command, invoke the abort method with an optional reason.

Example: Abort on recieving CTRL+C

document.addEventListener("keydown", (ev) => {
    if (ev.ctrlKey && ev.key.toLowerCase() == "c") {
        ev.preventDefault();

        // abort the execution
        vsh.abort(/* reason */);
    }
});

API Reference

The full public API for viteshell is contained within the TypeScript declaration file. It helps you understand the different interfaces required to setup your shell.

License

Copyright (c) 2023-Present Henry Hale.

Released under the MIT License.