@acepad/shell
v1.0.0
Published
A Unix-like shell environment for Acepad.
Readme
@acepad/shell
A Unix-like shell environment for browser-based programming with petit-node. This package provides the core shell functionality for acepad, enabling file system operations, command execution, and shell scripting directly in the browser.
Features
- Unix-like commands: cd, pwd, ls, cp, mv, rm, mkdir, cat, grep, etc.
- File system operations: Navigate and manipulate the virtual file system provided by petit-fs
- Custom command creation: Write your own shell commands in JavaScript
- Command execution: Execute commands from both files and built-in functions
- Glob pattern support: Use wildcards (
*,?) in file paths - Environment variables: Access via
this.$variableNamesyntax - Command history: Automatic LRU-based command history per directory
- Proxy-based command resolution: Dynamically resolve commands from PATH
Note: This package is designed to run in acepad/petit-node environment, not in standard Node.js.
Basic Usage
Importing and Initialization
The shell object is automatically initializd, Normally explicit import of "@acepad/shell" is not needed.
(press F2 to create file and F5 to run, in acepad)
#!run
export async function main(){
// The shell is already initialized and assigned to 'this'
this.pwd(); // Shows current directory
}Executing Built-in Commands
#!run
export async function main(){
const sh=this;
// Change directory
sh.cd("/tmp/");
// List files
sh.ls();
// Create a file
sh.touch("myfile.txt");
// Read file content
sh.cat("myfile.txt");
// Create directory
sh.mkdir("mydir/");
// Copy files
sh.cp("myfile.txt", "mydir/");
}Working with Files
#!run
export async function main(){
const sh=this;
// Resolve a file path (returns SFile object)
const file = sh.resolve("./myfile.txt");
// Check if file exists
if (file.exists()) {
sh.echo("File exists!");
}
// File test operators (similar to bash)
sh._e("file.txt"); // exists
sh._f("file.txt"); // is regular file
sh._d("mydir/"); // is directory
sh._s("file.txt"); // file size (0 if empty)
}Shell Variables / Environment variables
- Shell variables are field bounded to the shell object, NOT environment variables.
- Shell variables are referred by
sh.$var_name - If
sh.$var_nameis missing, fallbacks to parent shell object and then environment variables. - Environment variables can be accessed via
process.envwhich is polyfilled in global scope.
#!run
export async function main(){
const sh=this;
// Set variable
sh.set("myvar", "hello");
sh.$myvar = "hello"; // Alternative syntax
// Get variable
const value = sh.get("myvar");
const value2 = sh.$myvar; // Alternative syntax
// Use in commands
sh.echo(sh.$myvar); // Outputs: hello
process.env.envvar="good";
sh.echo(sh.$envvar); // Outputs: good
}Creating Custom Commands
File-based Commands
Commands are JavaScript files placed in directories listed in the $path variable. The file must start with #!run or //!run and export a main function.
Example: Create a greeting command in /idb/run/bin/greet
Tips: You can also create command by typing sh: newcmd greet in 'Directory List' in acepad.
#!run
export async function main(name) {
if (!name) name = "World";
this.echo(`Hello, ${name}!`);
this.echo(`Current directory: ${this.getcwd()}`);
}Usage:
// Assuming /idb/run/bin is in $path
sh.greet("Alice"); // Outputs: Hello, Alice!From 'Directory List' of acepad, line starting with sh: is interpreted shell command.
sh: greet Alice
Hello, Alice!The this Context
Inside main, the command functions, this refers to a cloned shell object with:
- File operations:
this.resolve(),this.exists(),this.directorify() - Output:
this.echo(),this.err() - Directory navigation:
this.getcwd(),this.cd() - Variables:
this.$variableNameorthis.get("variableName") - All built-in commands:
this.ls(),this.cp(), etc. - Command execution: Call other commands via
this.commandName()
Example: A command that copies a file and shows its size
Warning! The code below has not been tested yet and remains as Claude wrote it :-(
#!run
export async function main(src, dst) {
const srcFile = this.resolve(src, true); // true = must exist
const dstFile = this.resolve(dst);
if (!srcFile.exists()) {
throw new Error(`${src}: no such file`);
}
await srcFile.copyTo(dstFile);
this.echo(`Copied ${src} to ${dst}`);
this.echo(`Size: ${dstFile.size()} bytes`);
}Handling Options and Arguments
Use this.collectOptions(args) to separate options from arguments:
#!run
export async function main(...args) {
args = this.collectOptions(args);
const options = args.pop(); // Last element is always the options object
if (options.v || options.verbose) {
this.echo("Verbose mode enabled");
}
// Process remaining arguments
for (let arg of args) {
this.echo(`Processing: ${arg}`);
}
}Usage:
sh: mycommand file1.txt file2.txt -v
sh: mycommand -verbose file.txtAdding Synchronous Commands
For commands that need to be synchronous (non-async), register them directly:
// Method 1: Using newCommand
sh.newCommand("twice", function(n) {
return parseInt(n) * 2;
});
// Method 2: Using addCmd with file argument mapping
sh.addCmd("showsize", function(file) {
this.echo(`Size: ${file.size()}`);
}, "f"); // "f" means first argument is a file pathThe second parameter of addCmd is a spec string where:
"f"at position i means argument i should be resolved as a file
sh.twice(5); // Returns: 10
sh.showsize("myfile.txt"); // Shows file sizeCommand Path and Resolution
PATH Variable
Commands are searched in directories specified by $path:
Note: For convenience of typing in smartphone, PATH variable is referred as $path, not capital letters.
// View current PATH
sh.echo(sh.$path); // e.g., "/bin:/sbin:/home/user/bin"
// Add directory to PATH
sh.addPath("/home/user/scripts/");Command Resolution
When you call sh.commandName(), the shell:
- Checks if it's a built-in command
- Searches for an executable file in PATH directories
- Executes the file's
mainfunction with a cloned shell asthis
Finding Commands
// List all available commands
const cmds = sh.commandList();
// Find where a command is located
const cmdFile = sh.which("ls"); // Returns SFile objectGlob Patterns
Use wildcards to match multiple files:
// Match all .js files in current directory
sh.ls("*.js");
// Match all files starting with "test"
sh.cp("test*", "backup/");
// Single character wildcard
sh.rm("file?.txt"); // Matches file1.txt, file2.txt, etc.Note: Glob patterns do NOT match across directory separators.
Command History
Command history is automatically tracked per directory:
// Add command to history
sh.addHist("ls -la");
// Get history for current directory
const hist = sh.history();
// Get history for specific directory
const hist2 = sh.history("/home/user/");History is stored in .meta/ directory and limited to 16 most recent commands per directory (LRU).
Parsing and Executing Commands
Parse Command String
const args = sh.parseCommand('echo "Hello World" $myvar');
// Returns: ["echo", "Hello World", <value of $myvar>]Features:
- Double quotes preserve spaces
${varname}expands variables- Glob patterns are expanded
- Options (e.g.,
-v,-option=value) are parsed into objects
Execute Commands
// Execute parsed command
sh.evalCommand(["echo", "Hello"]);
// Execute command string
sh.enterCommand("ls /home/");
// Alternative
sh.exec("cp file1.txt file2.txt");Advanced Features
File System Utilities
// Convert path to directory (ensure trailing slash)
const dir = sh.directorify("/home/user");
// Get filesystem root
const root = sh.getRoot();
// Mount/unmount filesystems
await sh.mount({t: "ram"}, "/tmp/");
sh.unmount("/tmp/");
// Show mounted filesystems
sh.fstab();Cloning Shell Environment
// Create isolated shell environment with inherited variables
const newShell = sh.clone();
newShell.$myvar = "isolated value";
sh.$myvar; // Still has original valueZip Operations
// Create zip file
sh.zip("archive.zip", sh.resolve("/home/user/data/"));
// Extract zip file
sh.unzip("archive.zip", sh.resolve("/home/user/restore/"));Examples
Complete Command: Enhanced File Copy
#!run
export async function main(...args) {
args = this.collectOptions(args);
const options = args.pop();
if (args.length < 2) {
this.err("Usage: mycopy [-v] <source> <destination>");
return 1;
}
const dst = args.pop();
const verbose = options.v || options.verbose;
for (let srcPath of args) {
const src = this.resolve(srcPath, true);
const dest = this.resolve(dst);
if (verbose) {
this.echo(`Copying ${src.path()} to ${dest.path()}...`);
}
await src.copyTo(dest);
if (verbose) {
this.echo(`Done. Size: ${dest.size()} bytes`);
}
}
return 0;
}Interactive Shell Script
#!run
export async function main() {
const home = this.$home;
this.echo(`Welcome to ${home}`);
// List all JavaScript files
const files = this.glob("*.js");
this.echo(`Found ${[...files].length} JavaScript files`);
// Find large files
for (let fileName of this.glob("*")) {
const file = this.resolve(fileName);
if (this._f(file) && file.size() > 10000) {
this.echo(`Large file: ${fileName} (${file.size()} bytes)`);
}
}
}Command with Variable Usage
#!run
export async function main() {
// Set a variable
this.$lastBackup = new Date().toISOString();
// Create backup directory
const backupDir = this.resolve(`${this.$home}/backups/`);
if (!backupDir.exists()) {
backupDir.mkdir();
}
// Copy all .txt files to backup
for (let file of this.glob("*.txt")) {
const src = this.resolve(file);
const dst = backupDir.rel(src.name());
await src.copyTo(dst);
this.echo(`Backed up: ${file}`);
}
this.echo(`Backup completed at ${this.$lastBackup}`);
}Common Patterns
Check File/Directory Existence
if (this._d("mydir/")) {
this.echo("Directory exists");
} else {
this.mkdir("mydir/");
}Process All Files in Directory
const dir = this.resolve("./data/");
for (let fileName of dir.ls()) {
const file = dir.rel(fileName);
if (this._f(file)) {
// Process regular file
this.echo(`Processing ${fileName}`);
}
}Error Handling
try {
const file = this.resolve("config.json", true); // Must exist
const config = JSON.parse(file.text());
} catch (e) {
this.err("Error:", e.message);
return 1; // Return non-zero exit code
}API Reference
Core Methods
cd(dir)- Change directorypwd()- Print working directorygetcwd()- Get current working directory (returns SFile)resolve(path, mustExist?)- Resolve path to SFile objectdirectorify(path)- Ensure path ends with/exists(path)- Check if file/directory exists
File Operations
mkdir(path)- Create directorytouch(path)- Create empty file or update timestampcat(...files)- Display file contentscp(src, dst)- Copy file (async)mv(src, dst)- Move filerm(path, options?)- Remove file/directoryln(target, link)- Create symbolic link
Output
echo(...args)- Print to outputerr(...args)- Print error message
Variables
get(name)- Get variable valueset(name, value)- Set variable valuegetenv(name)- Get environment variablesetenv(name, value)- Set environment variable
Commands
parseCommand(string)- Parse command string to argumentsevalCommand(args)- Execute parsed commandenterCommand(string, extraArgs?)- Parse and execute commandexec(string)- Alias for enterCommandcommandList()- List all available commandswhich(command)- Find command file location
Dependencies
petit-node: Browser-based Node.js runtime@hoge1e3/lru: LRU cache for command history@hoge1e3/is-plain-object: Plain object detectionmaybe-monada: Optional value handling (minimal usage)
License
ISC
See Also
- petit-node - The underlying runtime
- acepad - Programming environment using this shell
- @acepad/files - File management utilities
