pawscript
v0.1.3
Published
PawScript: A command language with token-based suspension for text editors and command-driven applications
Downloads
29
Maintainers
Readme
PawScript: A command language with token-based suspension for text editors and command-driven applications.
## Features
- **Complex Command Syntax**: Support for sequences (`;`), conditionals (`&`), and alternatives (`|`)
- **Token-Based Suspension**: Pause and resume command execution for long-running operations
- **Macro System**: Define and execute reusable command sequences
- **Syntactic Sugar**: Automatic transformation of convenient syntax patterns
- **Type Safety**: Full TypeScript support with comprehensive type definitions
- **Host Agnostic**: Clean interface for integration with any application
- **Command Line Tool**: Execute PawScript files directly from the command line
## Installation
```bash
npm install pawscriptCommand Line Usage
PawScript includes a paw command-line tool for executing scripts:
# Execute a script file
paw hello.paw
# Execute with arguments
paw script.paw -- arg1 arg2 arg3
# Execute from stdin
echo "echo 'Hello World'" | paw
# Execute redirected input with arguments
paw -- arg1 arg2 < script.paw
# Auto-adds .paw extension
paw hello # Executes hello.pawStandard Library Commands
The CLI provides these built-in commands:
argc- Returns the number of script argumentsargv [index]- Returns all arguments or a specific argument by indexecho/write/print <text>- Output text to stdoutread- Read a line from stdin (interactive or redirected)true- Sets success state (exit code 0)false- Sets error state (exit code 1)
Example Scripts
hello.paw:
echo "Hello from PawScript!";
echo "You provided {argc} arguments";interactive.paw:
echo "What's your name?";
read;
echo "Hello, {get_result}!";Library Usage
Quick Start
import { PawScript } from 'pawscript';
// Create PawScript interpreter
const pawscript = new PawScript({
debug: true,
allowMacros: true
});
// Set up host interface
pawscript.setHost({
getCurrentContext: () => ({ cursor: { x: 0, y: 0 } }),
updateStatus: (msg) => console.log(msg),
requestInput: (prompt) => Promise.resolve('user input'),
render: () => console.log('render called')
});
// Register commands
pawscript.registerCommands({
'hello': (ctx) => {
console.log('Hello from PawScript!');
return true;
},
'echo': (ctx) => {
console.log('Echo:', ctx.args[0]);
return true;
}
});
// Execute commands
pawscript.execute('hello'); // Simple command
pawscript.execute("echo 'Hello World'"); // Command with arguments
pawscript.execute('hello; echo "chained"'); // Command sequenceCommand Syntax
Basic Commands
pawscript.execute('save_file');
pawscript.execute("open_file '/path/to/file'");
pawscript.execute('move_cursor 10, 5');Command Sequences
// Sequence: Execute all commands
pawscript.execute('save_file; close_buffer; open_file "new.txt"');
// Conditional: Stop on failure
pawscript.execute('save_file & close_buffer & exit');
// Alternative: Stop on success
pawscript.execute('auto_save | prompt_save | cancel');Syntactic Sugar
// Automatic quote insertion for identifiers
pawscript.execute('macro hello(save_file; exit)');
// Becomes: pawscript.execute("macro 'hello', (save_file; exit)");Built-in Commands
When allowMacros is enabled (default), PawScript automatically registers these built-in commands:
Macro Commands
macro <name>, <commands>- Define a new macrocall <name>- Execute a macro by namemacro_list- List all defined macrosmacro_delete <name>- Delete a specific macromacro_clear- Clear all macros
Macros
// Define macro using built-in command with syntactic sugar
pawscript.execute("macro quick_save(save_file; update_status 'Saved')");
// Define macro with arguments
pawscript.execute("macro greet(echo 'Hello $1!')");
// Execute macro using built-in command
pawscript.execute('call quick_save');
// Execute macro with arguments
pawscript.execute("call greet 'World'");
// Or execute macro directly (if it's defined)
pawscript.execute('quick_save');
pawscript.execute("greet 'Alice'");
// Macros in sequences
pawscript.execute('quick_save; close_buffer');
// Programmatic macro management (bypasses syntactic sugar)
pawscript.defineMacro('quick_save', 'save_file; update_status "Saved"');
pawscript.executeMacro('quick_save');
### Usage Examples
```typescript
// Define a macro (using syntactic sugar)
pawscript.execute("macro quick_save(save_file; update_status 'Saved')");
// Execute a macro
pawscript.execute('call quick_save');
// List all macros
pawscript.execute('macro_list');
// Delete a macro
pawscript.execute('macro_delete quick_save');
// Clear all macros
pawscript.execute('macro_clear');Disabling Built-in Commands
const pawscript = new PawScript({
allowMacros: false // Disables macro commands
});
// Or dynamically
pawscript.configure({ allowMacros: false });Token-Based Suspension (The "Paws" Feature)
For long-running operations, commands can return tokens that pause execution. This is the correct pattern:
pawscript.registerCommand('async_operation', (ctx) => {
// Request a token to pause execution
const token = ctx.requestToken((tokenId) => {
console.log('Operation was interrupted:', tokenId);
});
// Start async operation using setImmediate
setImmediate(() => {
// Simulate async work
setTimeout(() => {
console.log('Async operation completed');
ctx.resumeToken(token, true); // Resume with success
}, 5000);
});
return token; // Return token immediately to pause sequence
});
// This will pause at async_operation and resume when it completes
pawscript.execute('async_operation; echo "This runs after async completes"');Key Points About Tokens:
- Immediate Return: Commands must return the token immediately, not wait for async completion
- Use setImmediate: Start async work with
setImmediate()to avoid blocking - Resume Later: Call
ctx.resumeToken()when the async operation completes - Cleanup Support: Provide cleanup callbacks for interruption handling
Result Management
PawScript commands can set formal results that flow through command sequences:
pawscript.registerCommand('calculate', (ctx) => {
const result = Number(ctx.args[0]) + Number(ctx.args[1]);
ctx.setResult(result); // Set formal result
return true; // Indicate success
});
// Result flows through sequences
pawscript.execute('calculate 5, 3; print_result'); // Prints 8Brace Expressions
Use {...} for command evaluation and ${...} for prefixed evaluation:
// Execute command and substitute result
pawscript.execute('echo {calculate 10, 5}'); // Outputs: 15
// Execute command and prefix result with $
pawscript.execute('echo ${get_arg_number}'); // If returns "2", outputs: $2Host Interface
PawScript integrates with your application through a host interface:
interface IPawScriptHost {
getCurrentContext(): any;
updateStatus(message: string): void;
requestInput(prompt: string, defaultValue?: string): Promise<string>;
render(): void;
// Optional methods for advanced features
createWindow?(options: any): string;
removeWindow?(id: string): void;
saveState?(): any;
restoreState?(snapshot: any): void;
emit?(event: string, ...args: any[]): void;
on?(event: string, handler: Function): void;
}Configuration
const pawscript = new PawScript({
debug: false, // Enable debug logging
defaultTokenTimeout: 300000, // Token timeout in ms (5 minutes)
enableSyntacticSugar: true, // Enable syntax transformations
allowMacros: true, // Enable macro system
commandSeparators: {
sequence: ';', // Command sequence separator
conditional: '&', // Conditional separator
alternative: '|' // Alternative separator
}
});Integration Example
Here's how to integrate PawScript with an existing application:
// Your existing application
class MyEditor {
constructor() {
this.pawscript = new PawScript({ debug: true });
this.setupPawScript();
}
setupPawScript() {
// Set up host interface
this.pawscript.setHost({
getCurrentContext: () => ({
cursor: this.getCursorPosition(),
selection: this.getSelection(),
filename: this.getCurrentFilename()
}),
updateStatus: (msg) => this.statusBar.show(msg),
requestInput: (prompt, def) => this.showPrompt(prompt, def),
render: () => this.redraw()
});
// Register application-specific commands
this.pawscript.registerCommands({
'save_file': (ctx) => this.saveCurrentFile(),
'open_file': (ctx) => this.openFile(ctx.args[0]),
'move_cursor': (ctx) => this.moveCursor(ctx.args[0], ctx.args[1]),
'find_text': (ctx) => this.findText(ctx.args[0])
});
}
// Handle user input (key presses, menu clicks, etc.)
handleCommand(commandString) {
this.pawscript.execute(commandString);
}
}API Reference
PawScript
Constructor
new PawScript(config?: PawScriptConfig)Methods
setHost(host: IPawScriptHost): Set the host application interfaceregisterCommand(name: string, handler: PawScriptHandler): Register a single commandregisterCommands(commands: Record<string, PawScriptHandler>): Register multiple commandsexecute(commandString: string, ...args: any[]): Execute a command stringrequestToken(cleanup?, parent?, timeout?): Request an async tokenresumeToken(tokenId: string, result: boolean): Resume a suspended commanddefineMacro(name: string, commands: string): Define a macroexecuteMacro(name: string): Execute a macrolistMacros(): Get list of defined macrosdeleteMacro(name: string): Delete a macroclearMacros(): Clear all macrosgetTokenStatus(): Get information about active tokensconfigure(config: Partial<PawScriptConfig>): Update configuration
PawScriptHandler
Command handlers receive a PawScriptContext object:
interface PawScriptContext {
host: IPawScriptHost; // Reference to host application
args: any[]; // Parsed command arguments
state: any; // Current application state
requestToken(cleanup?: Function): string; // Request async token
resumeToken(tokenId: string, result: boolean): void; // Resume token
// Result management
setResult(value: any): void; // Set formal result
getResult(): any; // Get current result
hasResult(): boolean; // Check if result exists
clearResult(): void; // Clear current result
}Return Values
boolean: Synchronous success/failurestring(starting with "token_"): Async token for suspension
Command Parsing
PawScript automatically parses command arguments:
// String arguments (quoted)
pawscript.execute("echo 'hello world'");
// → ctx.args = ['hello world']
// Multiple arguments
pawscript.execute("move_cursor 10, 20");
// → ctx.args = [10, 20]
// Mixed types
pawscript.execute("create_window 'MyWindow', 100, 50, true");
// → ctx.args = ['MyWindow', 100, 50, true]
// Parenthetical content (passed as-is)
pawscript.execute("macro hello(save_file; exit)");
// → After syntactic sugar: ctx.args = ['hello', 'save_file; exit']Error Handling
PawScript provides robust error handling:
pawscript.registerCommand('risky_operation', (ctx) => {
try {
// Risky operation
return performRiskyOperation();
} catch (error) {
ctx.host.updateStatus(`Operation failed: ${error.message}`);
return false;
}
});Testing
The library works well with Jest and other testing frameworks:
import { PawScript } from 'pawscript';
describe('My Application Commands', () => {
let pawscript: PawScript;
let mockHost: any;
beforeEach(() => {
mockHost = {
getCurrentContext: jest.fn().mockReturnValue({}),
updateStatus: jest.fn(),
requestInput: jest.fn(),
render: jest.fn()
};
pawscript = new PawScript({ debug: false });
pawscript.setHost(mockHost);
});
test('should execute my command', () => {
const myCommand = jest.fn().mockReturnValue(true);
pawscript.registerCommand('my_command', myCommand);
const result = pawscript.execute('my_command');
expect(result).toBe(true);
expect(myCommand).toHaveBeenCalled();
});
test('should handle async commands with tokens', () => {
const asyncCommand = jest.fn().mockImplementation((ctx) => {
const token = ctx.requestToken();
setImmediate(() => {
ctx.resumeToken(token, true);
});
return token;
});
pawscript.registerCommand('async_cmd', asyncCommand);
const result = pawscript.execute('async_cmd');
expect(typeof result).toBe('string');
expect(result).toMatch(/^token_/);
});
});Advanced Features
Token Chaining
PawScript automatically chains tokens in command sequences:
// If 'async_save' returns a token, 'async_backup' will wait for it
pawscript.execute('async_save; async_backup; notify_complete');Fallback Handlers
You can register fallback handlers for unknown commands:
pawscript.setFallbackHandler((cmdName, args) => {
if (cmdName.startsWith('custom_')) {
return handleCustomCommand(cmdName, args);
}
return null; // Let PawScript handle as unknown command
});Token Status Monitoring
Monitor active tokens for debugging:
const status = pawscript.getTokenStatus();
console.log(`Active tokens: ${status.activeCount}`);
status.tokens.forEach(token => {
console.log(`${token.id}: age ${token.age}ms, children: ${token.childCount}`);
});Best Practices
1. Proper Async Pattern
// ✅ CORRECT
pawscript.registerCommand('async_save', (ctx) => {
const token = ctx.requestToken();
setImmediate(() => {
fs.writeFile('file.txt', data, (err) => {
ctx.resumeToken(token, !err);
});
});
return token;
});
// ❌ WRONG - Don't use Promises directly
pawscript.registerCommand('wrong_async', async (ctx) => {
await fs.promises.writeFile('file.txt', data);
return true;
});2. Error Handling
pawscript.registerCommand('safe_command', (ctx) => {
try {
const result = riskyOperation(ctx.args[0]);
ctx.host.updateStatus('Operation completed');
return true;
} catch (error) {
ctx.host.updateStatus(`Error: ${error.message}`);
return false;
}
});3. State Management
pawscript.registerCommand('context_aware', (ctx) => {
const { cursor, selection } = ctx.state;
if (!selection) {
ctx.host.updateStatus('No selection available');
return false;
}
// Process selection...
return true;
});The Name
PawScript gets its name from the token-based suspension system - when a command needs to wait for an async operation to complete, execution "paws" (pauses) until the operation finishes. The name also nods to the language's origins in the mew text editor (which has a cat mascot), while being professional enough for standalone use.
Migration from Other Command Systems
If you're migrating from a different command system:
- Wrap existing handlers: Your existing command handlers can be wrapped to match the PawScriptHandler interface
- Update async commands: Convert Promise-based async commands to use the token pattern
- Configure syntax: Disable syntactic sugar if you need exact command parsing compatibility
- Update macros: Migrate existing macros to PawScript's macro system
License
MIT
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass with
npm test - Submit a pull request
Changelog
0.1.3
- Implemented braces for command evaluation (function-like behavior)
- Implemented substitution for macro arguments $* $# $1 $2
- Added result management system with formal results, in addition to the success/fail states
- Added command-line tool (
paw) for executing PawScript files - Added standard library commands (argc, argv, echo, read, true, false)
- Fixed syntactic sugar parsing for multi-line content
- Fixed token suspension and resumption for async operations
- Improved macro execution with proper state management
- Enhanced test coverage and documentation
0.1.2
- Minor fixes
0.1.1
- Initial release
- Basic command execution with sequences, conditionals, and alternatives
- Token-based suspension system ("paws" feature)
- Macro system with define/execute/list capabilities
- Syntactic sugar for convenient command syntax
- Full TypeScript support with comprehensive type definitions
- Host-agnostic design for easy integration
- Comprehensive test suite and documentation
