A minimalistic shell implementation written in TypeScript.
Table of Contents
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.
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:
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).
The latest version of viteshell, packaged and published on npm, can be installed into a new or existing project in a number of ways:
NPM : Install the module via npm. Run the following command to add as a dependency.
npm install viteshell
Then import the package:
import ViteShell from "viteshell";
CDN: Install the module using any CDN that delivers packages from npm registry, for example: unpkg, jsdelivr
Using unpkg:
<script
type="text/javascript"
src="https://unpkg.com/viteshell/dist/viteshell.umd.js"
></script>
Or jsDelivr:
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/viteshell/dist/viteshell.umd.js"
></script>
To use viteshell, first create a new instance of it
<script>
const vsh = new ViteShell();
</script>
viteshell maintains it’s own state used during command execution. It contains a list of aliases, environment variables and command history.
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)
;
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.
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);
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);
};
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.
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
exit
exit - Terminates the current process
clear
clear - Clears the standard output stream
pwd
pwd - Prints the current working directory
echo
echo [args] - Write arguments to the standard output followed by a new line character.
Example: echo "Hello $USERNAME!"
alias
alias [-p] [name=[value] …] - Defines aliases for commands
Example: alias -p
to display all aliases, alias println=echo
to use println
as echo
unalias
unalias [name …] - Removes aliases for commands
Example: unalias println
to remove the println
alias
export
export [-p] [name=[value] … ] - Set shell variables by name and value
Example: export -p
to display all variables, export ANSWER=123
to assign 123
to ANSWER
variable
history
history [-c] [-n] - Retrieve previous input entries
Example: history -c
to clear the history list, history -n
to display the number of items in the history list
help
help [command] - Displays information on available commands.
Example: help
to display all commands, help clear
to show a manual of the clear
command
read
read [prompt] [variable] - Capture input and save it in the env object
Example: read "Do you want to continue? (y/n) " ANSWER
to prompt user Do you want to continue? (y/n)
and save the user response in the ANSWER
variable
sleep
sleep [seconds] - Delay for a specified amount of time (in seconds).
Example: sleep 3
to pause the shell for 3
seconds
grep
grep [keyword] [context …] - Searches for matching phrases in the text
Example: help | grep clear
to capture all occurences of clear
in the output of the help
command
date
date - Displays the current time and date
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 */);
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.
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.
Since you have connected your shell to an input/output stream using callbacks and it is activated, you can now execute commands:
using the input element: type help
and hit Enter
key on your keyboard
programmatically
const exec = async (input) => {
// ...
await vsh.execute(input);
// ...
};
exec('echo "Hello World!"');
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);
Sometimes you need to run commands basing on the success or failure of the previously executed command or just normally.
Example:
echo "1" && echo "2"
: If the first command (echo 1
) is succesfully executed, then echo 2
will be executed.echo "1" || echo "2"
: The second command (echo 2
) will not be executed if the first was succesfull.echo "1" ; echo "2"
: Both commands are executed irrespective of the success or failure of the previously executed command.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
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 */);
}
});
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.
Copyright (c) 2023-Present Henry Hale.
Released under the MIT License.