added interactive inputs, better error messages, bugfixes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
target
|
target
|
||||||
node_modules
|
node_modules
|
||||||
|
*.vsix
|
||||||
@@ -11,6 +11,7 @@ Rush is an experimental shell scripting language interpreter that combines simpl
|
|||||||
- **Control flow** with `if`/`else` and `not`
|
- **Control flow** with `if`/`else` and `not`
|
||||||
- **For loops** for iteration
|
- **For loops** for iteration
|
||||||
- **Parallel execution** blocks for concurrent tasks
|
- **Parallel execution** blocks for concurrent tasks
|
||||||
|
- **Interactive input** with `input()`, `confirm()`, `select()`, and `multiselect()`
|
||||||
- **Strict parsing** that catches errors before execution
|
- **Strict parsing** that catches errors before execution
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
- [Loops](./language/loops.md)
|
- [Loops](./language/loops.md)
|
||||||
- [Parallel Execution](./language/parallel.md)
|
- [Parallel Execution](./language/parallel.md)
|
||||||
- [Built-in Variables](./language/builtins.md)
|
- [Built-in Variables](./language/builtins.md)
|
||||||
|
- [User Input](./language/input.md)
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
|
|||||||
347
docs/src/language/input.md
Normal file
347
docs/src/language/input.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# User Input
|
||||||
|
|
||||||
|
> ⚠️ This documentation is AI-generated and may contain errors.
|
||||||
|
|
||||||
|
Rush provides several built-in functions for interactive user input, making it easy to create interactive scripts and tools.
|
||||||
|
|
||||||
|
## Input Functions
|
||||||
|
|
||||||
|
### `input()`
|
||||||
|
|
||||||
|
Read a line of text from the user.
|
||||||
|
|
||||||
|
**Syntax:**
|
||||||
|
```rush
|
||||||
|
VARIABLE = input()
|
||||||
|
VARIABLE = input("prompt")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```rush
|
||||||
|
# With prompt
|
||||||
|
NAME = input("What is your name? ")
|
||||||
|
echo "Hello, $NAME!"
|
||||||
|
|
||||||
|
# Without prompt
|
||||||
|
echo "Enter a value:"
|
||||||
|
VALUE = input()
|
||||||
|
echo "You entered: $VALUE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Collecting user information
|
||||||
|
- Reading configuration values
|
||||||
|
- Interactive script parameters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `confirm()`
|
||||||
|
|
||||||
|
Ask a yes/no question and get a boolean response.
|
||||||
|
|
||||||
|
**Syntax:**
|
||||||
|
```rush
|
||||||
|
VARIABLE = confirm("question")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `1` for yes (y/yes/Y/YES)
|
||||||
|
- `0` for no (n/no/N/NO)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```rush
|
||||||
|
PROCEED = confirm("Do you want to continue?")
|
||||||
|
|
||||||
|
if $PROCEED {
|
||||||
|
echo "Continuing..."
|
||||||
|
} else {
|
||||||
|
echo "Stopped."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rush
|
||||||
|
# Check before dangerous operation
|
||||||
|
DELETE_CONFIRM = confirm("Are you sure you want to delete all files?")
|
||||||
|
|
||||||
|
if not $DELETE_CONFIRM {
|
||||||
|
echo "Operation cancelled."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Deleting files..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Confirmations before destructive actions
|
||||||
|
- Binary choices
|
||||||
|
- Permission checks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `select()`
|
||||||
|
|
||||||
|
Display a menu and let the user select one option.
|
||||||
|
|
||||||
|
**Syntax:**
|
||||||
|
```rush
|
||||||
|
CHOICE = select("prompt", "option1", "option2", "option3", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- A number representing the selected option (1-indexed)
|
||||||
|
- `0` if cancelled or error
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```rush
|
||||||
|
ENV = select("Select environment:", "Development", "Staging", "Production")
|
||||||
|
|
||||||
|
if $ENV {
|
||||||
|
echo "Selected environment: $ENV"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rush
|
||||||
|
# Using the result
|
||||||
|
LANG = select("Choose language:", "English", "Spanish", "French", "German")
|
||||||
|
|
||||||
|
# Process based on selection
|
||||||
|
if $LANG {
|
||||||
|
echo "You selected option $LANG"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Display:**
|
||||||
|
```
|
||||||
|
Choose language:
|
||||||
|
|
||||||
|
1. English
|
||||||
|
2. Spanish
|
||||||
|
3. French
|
||||||
|
4. German
|
||||||
|
|
||||||
|
Select (1-4):
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Environment selection
|
||||||
|
- Configuration options
|
||||||
|
- Mode selection
|
||||||
|
- File/directory choice
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `multiselect()`
|
||||||
|
|
||||||
|
Display a menu and let the user select multiple options.
|
||||||
|
|
||||||
|
**Syntax:**
|
||||||
|
```rush
|
||||||
|
CHOICES = multiselect("prompt", "option1", "option2", "option3", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- A comma-separated string of selected indices (e.g., "1,3,5")
|
||||||
|
- Empty string if none selected or error
|
||||||
|
|
||||||
|
**Input format:**
|
||||||
|
- Comma-separated: `1,2,3`
|
||||||
|
- Space-separated: `1 2 3`
|
||||||
|
- Mixed: `1, 2, 3`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```rush
|
||||||
|
COMPONENTS = multiselect("Select components to install:", "Core", "Database", "Web Server", "Cache", "Monitoring")
|
||||||
|
|
||||||
|
echo "Selected components: $COMPONENTS"
|
||||||
|
```
|
||||||
|
|
||||||
|
```rush
|
||||||
|
# Feature flags
|
||||||
|
FEATURES = multiselect("Enable features:", "Logging", "Analytics", "Notifications", "Dark Mode", "Auto-save")
|
||||||
|
|
||||||
|
echo "Enabled features: $FEATURES"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Display:**
|
||||||
|
```
|
||||||
|
Select components to install:
|
||||||
|
|
||||||
|
1. Core
|
||||||
|
2. Database
|
||||||
|
3. Web Server
|
||||||
|
4. Cache
|
||||||
|
5. Monitoring
|
||||||
|
|
||||||
|
Select multiple (e.g., 1,2,3 or 1 2 3):
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Component selection
|
||||||
|
- Feature flags
|
||||||
|
- Multiple file selection
|
||||||
|
- Task selection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
Here's a complete interactive script demonstrating all input methods:
|
||||||
|
|
||||||
|
```rush
|
||||||
|
#!/usr/bin/env rush
|
||||||
|
|
||||||
|
echo "=== Project Setup Wizard ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get project details
|
||||||
|
PROJECT_NAME = input("Project name: ")
|
||||||
|
DESCRIPTION = input("Description: ")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Select project type
|
||||||
|
PROJECT_TYPE = select("Project type:", "Web Application", "CLI Tool", "Library", "API Service")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Select features
|
||||||
|
FEATURES = multiselect("Select features:", "Authentication", "Database", "API", "Testing", "Documentation")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Confirm setup
|
||||||
|
PROCEED = confirm("Create project with these settings?")
|
||||||
|
|
||||||
|
if not $PROCEED {
|
||||||
|
echo "Setup cancelled."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Creating Project ==="
|
||||||
|
echo "Name: $PROJECT_NAME"
|
||||||
|
echo "Description: $DESCRIPTION"
|
||||||
|
echo "Type: $PROJECT_TYPE"
|
||||||
|
echo "Features: $FEATURES"
|
||||||
|
echo ""
|
||||||
|
echo "Project created successfully!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patterns and Best Practices
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
Always validate user input:
|
||||||
|
|
||||||
|
```rush
|
||||||
|
AGE = input("Enter your age: ")
|
||||||
|
|
||||||
|
# Basic validation (you can add more sophisticated checks)
|
||||||
|
if not $AGE {
|
||||||
|
echo "Age is required"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Age: $AGE"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Confirmations
|
||||||
|
|
||||||
|
For destructive operations, always confirm:
|
||||||
|
|
||||||
|
```rush
|
||||||
|
echo "WARNING: This will delete all data!"
|
||||||
|
CONFIRMED = confirm("Are you absolutely sure?")
|
||||||
|
|
||||||
|
if not $CONFIRMED {
|
||||||
|
echo "Operation cancelled."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proceed with destructive operation
|
||||||
|
echo "Deleting data..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menu-driven Scripts
|
||||||
|
|
||||||
|
Create interactive menus for complex workflows:
|
||||||
|
|
||||||
|
```rush
|
||||||
|
echo "=== Main Menu ==="
|
||||||
|
ACTION = select("What would you like to do?", "Install", "Update", "Remove", "Configure", "Exit")
|
||||||
|
|
||||||
|
# Handle selection
|
||||||
|
# ... process based on $ACTION value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progressive Input
|
||||||
|
|
||||||
|
Collect information step by step:
|
||||||
|
|
||||||
|
```rush
|
||||||
|
echo "Step 1: User Information"
|
||||||
|
USERNAME = input("Username: ")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 2: Preferences"
|
||||||
|
THEME = select("Theme:", "Light", "Dark", "Auto")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 3: Features"
|
||||||
|
FEATURES = multiselect("Features:", "Feature A", "Feature B", "Feature C")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
CONFIRM = confirm("Save these settings?")
|
||||||
|
|
||||||
|
if $CONFIRM {
|
||||||
|
echo "Settings saved!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Input functions handle errors gracefully:
|
||||||
|
|
||||||
|
- `input()` - Returns empty string on EOF
|
||||||
|
- `confirm()` - Keeps prompting until valid input (y/n)
|
||||||
|
- `select()` - Returns 0 on error, keeps prompting for valid range
|
||||||
|
- `multiselect()` - Returns empty string on error, validates range
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```rush
|
||||||
|
# Check for empty input
|
||||||
|
NAME = input("Name: ")
|
||||||
|
|
||||||
|
if not $NAME {
|
||||||
|
echo "Error: Name cannot be empty"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Hello, $NAME!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Clear prompts** - Make prompts descriptive and include examples
|
||||||
|
2. **Show options** - For select/multiselect, use clear, concise option names
|
||||||
|
3. **Confirm destructive actions** - Always use `confirm()` before dangerous operations
|
||||||
|
4. **Validate input** - Check that required inputs are provided
|
||||||
|
5. **Provide feedback** - Echo back what the user selected for confirmation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Input is line-based (no character-by-character input)
|
||||||
|
- No built-in password masking (input is visible)
|
||||||
|
- No default values (yet)
|
||||||
|
- No input history or autocomplete
|
||||||
|
|
||||||
|
These features may be added in future versions.
|
||||||
37
src/error/colors.rs
Normal file
37
src/error/colors.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
pub struct Colors;
|
||||||
|
|
||||||
|
impl Colors {
|
||||||
|
pub const RESET: &'static str = "\x1b[0m";
|
||||||
|
pub const BOLD: &'static str = "\x1b[1m";
|
||||||
|
|
||||||
|
pub const RED: &'static str = "\x1b[31m";
|
||||||
|
pub const GREEN: &'static str = "\x1b[32m";
|
||||||
|
pub const YELLOW: &'static str = "\x1b[33m";
|
||||||
|
pub const BLUE: &'static str = "\x1b[34m";
|
||||||
|
pub const MAGENTA: &'static str = "\x1b[35m";
|
||||||
|
pub const CYAN: &'static str = "\x1b[36m";
|
||||||
|
|
||||||
|
pub const BOLD_RED: &'static str = "\x1b[1;31m";
|
||||||
|
pub const BOLD_YELLOW: &'static str = "\x1b[1;33m";
|
||||||
|
pub const BOLD_CYAN: &'static str = "\x1b[1;36m";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(msg: &str) -> String {
|
||||||
|
format!("{}{}ERROR:{} {}", Colors::BOLD_RED, Colors::BOLD, Colors::RESET, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(msg: &str) -> String {
|
||||||
|
format!("{}{}WARNING:{} {}", Colors::BOLD_YELLOW, Colors::BOLD, Colors::RESET, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(msg: &str) -> String {
|
||||||
|
format!("{}{}INFO:{} {}", Colors::BOLD_CYAN, Colors::BOLD, Colors::RESET, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn location(file: &str, line: usize) -> String {
|
||||||
|
format!("{}{}:{}{}", Colors::CYAN, file, line, Colors::RESET)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight(text: &str) -> String {
|
||||||
|
format!("{}{}{}", Colors::YELLOW, text, Colors::RESET)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod colors;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RushError {
|
pub enum RushError {
|
||||||
SyntaxError(String),
|
SyntaxError(String),
|
||||||
@@ -7,7 +9,11 @@ pub enum RushError {
|
|||||||
|
|
||||||
impl std::fmt::Display for RushError {
|
impl std::fmt::Display for RushError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
match self {
|
||||||
|
RushError::SyntaxError(msg) => write!(f, "{}", msg),
|
||||||
|
RushError::RuntimeError(msg) => write!(f, "{}", msg),
|
||||||
|
RushError::VariableError(msg) => write!(f, "{}", msg),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,53 @@
|
|||||||
use crate::parser::ast::{AstNode, Command, Statement};
|
use crate::parser::ast::{AstNode, Command, Statement};
|
||||||
use crate::runtime::builtins;
|
use crate::runtime::builtins;
|
||||||
use crate::runtime::environment::Environment;
|
use crate::runtime::environment::Environment;
|
||||||
|
use crate::error::colors;
|
||||||
use std::process::Command as StdCommand;
|
use std::process::Command as StdCommand;
|
||||||
|
|
||||||
pub struct Executor<'a> {
|
pub struct Executor<'a> {
|
||||||
env: &'a mut Environment,
|
env: &'a mut Environment,
|
||||||
|
current_file: Option<String>,
|
||||||
|
current_line: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Executor<'a> {
|
impl<'a> Executor<'a> {
|
||||||
pub fn new(env: &'a mut Environment) -> Self {
|
pub fn new(env: &'a mut Environment) -> Self {
|
||||||
Executor { env }
|
Executor {
|
||||||
|
env,
|
||||||
|
current_file: None,
|
||||||
|
current_line: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_file(&mut self, filename: String) {
|
||||||
|
self.current_file = Some(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_context(&self) -> String {
|
||||||
|
if let Some(ref file) = self.current_file {
|
||||||
|
if self.current_line > 0 {
|
||||||
|
format!(" at {}", colors::location(file, self.current_line))
|
||||||
|
} else {
|
||||||
|
format!(" in {}", colors::highlight(file))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_node(&mut self, node: AstNode) {
|
pub fn execute_node(&mut self, node: AstNode) {
|
||||||
match node {
|
match node {
|
||||||
AstNode::VariableAssignment { name, value } => {
|
AstNode::VariableAssignment { name, value } => {
|
||||||
if let AstNode::Literal(val) = *value {
|
match *value {
|
||||||
let substituted = self.substitute_variables(&val);
|
AstNode::Literal(val) => {
|
||||||
self.env.set_variable(name, substituted);
|
let substituted = self.substitute_variables(&val);
|
||||||
|
self.env.set_variable(name, substituted);
|
||||||
|
}
|
||||||
|
AstNode::FunctionCall { name: fn_name, args } => {
|
||||||
|
let result = self.execute_function_call(&fn_name, args);
|
||||||
|
self.env.set_variable(name, result);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AstNode::Command { name, args } => {
|
AstNode::Command { name, args } => {
|
||||||
@@ -43,6 +73,16 @@ impl<'a> Executor<'a> {
|
|||||||
builtins::exit(code);
|
builtins::exit(code);
|
||||||
} else if name == "require_root" {
|
} else if name == "require_root" {
|
||||||
builtins::require_root();
|
builtins::require_root();
|
||||||
|
} else if name == "cd" {
|
||||||
|
let path = arg_strings.first().map(|s| s.as_str()).unwrap_or(".");
|
||||||
|
if let Err(e) = builtins::cd(path) {
|
||||||
|
eprintln!("{}{}", colors::error(&format!("Failed to change directory: {}", e)), self.error_context());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
} else if name == "test" {
|
||||||
|
|
||||||
|
|
||||||
|
builtins::test(&arg_strings);
|
||||||
} else {
|
} else {
|
||||||
let cmd = Command {
|
let cmd = Command {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
@@ -51,6 +91,10 @@ impl<'a> Executor<'a> {
|
|||||||
self.execute_command(cmd);
|
self.execute_command(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AstNode::FunctionCall { name, args } => {
|
||||||
|
|
||||||
|
self.execute_function_call(&name, args);
|
||||||
|
}
|
||||||
AstNode::ControlFlow {
|
AstNode::ControlFlow {
|
||||||
condition,
|
condition,
|
||||||
then_branch,
|
then_branch,
|
||||||
@@ -384,27 +428,186 @@ impl<'a> Executor<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute_function_call(&mut self, name: &str, args: Vec<AstNode>) -> String {
|
||||||
|
let arg_strings: Vec<String> = args
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|arg| {
|
||||||
|
if let AstNode::Literal(s) = arg {
|
||||||
|
Some(self.substitute_variables(&s))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match name {
|
||||||
|
"input" => {
|
||||||
|
|
||||||
|
let prompt = arg_strings.first().map(|s| s.as_str());
|
||||||
|
builtins::input(prompt)
|
||||||
|
}
|
||||||
|
"confirm" => {
|
||||||
|
|
||||||
|
let prompt = arg_strings.first().map(|s| s.as_str()).unwrap_or("Confirm?");
|
||||||
|
if builtins::confirm(prompt) {
|
||||||
|
"1".to_string()
|
||||||
|
} else {
|
||||||
|
"0".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"test" => {
|
||||||
|
|
||||||
|
if builtins::test(&arg_strings) {
|
||||||
|
"1".to_string()
|
||||||
|
} else {
|
||||||
|
"0".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"select" => {
|
||||||
|
|
||||||
|
if arg_strings.is_empty() {
|
||||||
|
eprintln!("{}{}", colors::error("select() requires at least a prompt"), self.error_context());
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
let prompt = &arg_strings[0];
|
||||||
|
let options = &arg_strings[1..];
|
||||||
|
if options.is_empty() {
|
||||||
|
eprintln!("{}{}", colors::error("select() requires at least one option"), self.error_context());
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
let selection = builtins::select(prompt, &options.to_vec());
|
||||||
|
selection.to_string()
|
||||||
|
}
|
||||||
|
"multiselect" => {
|
||||||
|
|
||||||
|
if arg_strings.is_empty() {
|
||||||
|
eprintln!("{}{}", colors::error("multiselect() requires at least a prompt"), self.error_context());
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let prompt = &arg_strings[0];
|
||||||
|
let options = &arg_strings[1..];
|
||||||
|
if options.is_empty() {
|
||||||
|
eprintln!("{}{}", colors::error("multiselect() requires at least one option"), self.error_context());
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
builtins::multiselect(prompt, &options.to_vec())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("{}{}", colors::error(&format!("Unknown function: {}", colors::highlight(name))), self.error_context());
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_command(&mut self, command: Command) {
|
fn execute_command(&mut self, command: Command) {
|
||||||
let result = StdCommand::new(&command.name).args(&command.args).output();
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write as IoWrite;
|
||||||
|
|
||||||
|
|
||||||
|
let mut redirect_stdout: Option<(String, bool)> = None;
|
||||||
|
let mut suppress_stderr = false;
|
||||||
|
|
||||||
|
let filtered_args: Vec<String> = command
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, arg)| {
|
||||||
|
if arg == ">" || arg == ">>" {
|
||||||
|
|
||||||
|
if i + 1 < command.args.len() {
|
||||||
|
redirect_stdout = Some((command.args[i + 1].clone(), arg == ">>"));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else if arg.starts_with('>') {
|
||||||
|
|
||||||
|
let (file, append) = if arg.starts_with(">>") {
|
||||||
|
(&arg[2..], true)
|
||||||
|
} else {
|
||||||
|
(&arg[1..], false)
|
||||||
|
};
|
||||||
|
redirect_stdout = Some((file.to_string(), append));
|
||||||
|
None
|
||||||
|
} else if arg == "2>/dev/null" || arg.starts_with("2>") {
|
||||||
|
suppress_stderr = true;
|
||||||
|
None
|
||||||
|
} else if redirect_stdout.is_some() && i > 0 && (command.args[i-1] == ">" || command.args[i-1] == ">>") {
|
||||||
|
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(arg.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let result = StdCommand::new(&command.name).args(&filtered_args).output();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
|
||||||
if !output.stdout.is_empty() {
|
if !output.stdout.is_empty() {
|
||||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
let stdout_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
if let Some((filename, append)) = redirect_stdout {
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(!append)
|
||||||
|
.append(append)
|
||||||
|
.open(&filename);
|
||||||
|
|
||||||
|
match file {
|
||||||
|
Ok(ref mut f) => {
|
||||||
|
let _ = f.write_all(stdout_str.as_bytes());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"{}{}",
|
||||||
|
colors::error(&format!(
|
||||||
|
"Failed to write to '{}': {}",
|
||||||
|
colors::highlight(&filename),
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
self.error_context()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
print!("{}", stdout_str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !output.stderr.is_empty() {
|
|
||||||
|
|
||||||
|
if !output.stderr.is_empty() && !suppress_stderr {
|
||||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Command '{}' failed with exit code: {:?}",
|
"{}{}",
|
||||||
command.name,
|
colors::error(&format!(
|
||||||
output.status.code()
|
"Command '{}' failed with exit code {}",
|
||||||
|
colors::highlight(&command.name),
|
||||||
|
output.status.code().unwrap_or(-1)
|
||||||
|
)),
|
||||||
|
self.error_context()
|
||||||
);
|
);
|
||||||
|
std::process::exit(output.status.code().unwrap_or(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to execute command '{}': {}", command.name, e);
|
eprintln!(
|
||||||
|
"{}{}",
|
||||||
|
colors::error(&format!(
|
||||||
|
"Failed to execute command '{}': {}",
|
||||||
|
colors::highlight(&command.name),
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
self.error_context()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,10 +645,19 @@ impl<'a> Executor<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_truthy(&self, value: &str) -> bool {
|
fn is_truthy(&self, value: &str) -> bool {
|
||||||
// Empty string, "0", "false", "no" are falsy
|
|
||||||
// Everything else is truthy
|
|
||||||
|
|
||||||
|
if value == "0" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "1" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
!value.is_empty()
|
!value.is_empty()
|
||||||
&& value != "0"
|
|
||||||
&& value.to_lowercase() != "false"
|
&& value.to_lowercase() != "false"
|
||||||
&& value.to_lowercase() != "no"
|
&& value.to_lowercase() != "no"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ use crate::runtime::environment::Environment;
|
|||||||
use executor::Executor;
|
use executor::Executor;
|
||||||
|
|
||||||
pub fn execute(content: &str) {
|
pub fn execute(content: &str) {
|
||||||
|
execute_with_filename(content, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_with_filename(content: &str, filename: Option<String>) {
|
||||||
let parser = Parser::new();
|
let parser = Parser::new();
|
||||||
|
|
||||||
let program = match parser.parse(content) {
|
let program = match parser.parse(content) {
|
||||||
Ok(prog) => prog,
|
Ok(prog) => prog,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Parse error: {}", e);
|
eprintln!("{}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -19,6 +23,10 @@ pub fn execute(content: &str) {
|
|||||||
let mut env = Environment::new();
|
let mut env = Environment::new();
|
||||||
|
|
||||||
let mut executor = Executor::new(&mut env);
|
let mut executor = Executor::new(&mut env);
|
||||||
|
|
||||||
|
if let Some(file) = filename {
|
||||||
|
executor.set_file(file);
|
||||||
|
}
|
||||||
|
|
||||||
for statement in program.statements {
|
for statement in program.statements {
|
||||||
executor.execute_node(statement);
|
executor.execute_node(statement);
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -1,3 +1,5 @@
|
|||||||
|
use rush::error::colors;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
@@ -11,9 +13,12 @@ fn main() {
|
|||||||
let content = std::fs::read_to_string(script_path);
|
let content = std::fs::read_to_string(script_path);
|
||||||
if content.is_err() {
|
if content.is_err() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Error reading file {}: {}",
|
"{}",
|
||||||
script_path,
|
colors::error(&format!(
|
||||||
content.err().unwrap()
|
"Failed to read file '{}': {}",
|
||||||
|
colors::highlight(script_path),
|
||||||
|
content.err().unwrap()
|
||||||
|
))
|
||||||
);
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
@@ -25,5 +30,5 @@ fn main() {
|
|||||||
content
|
content
|
||||||
};
|
};
|
||||||
|
|
||||||
rush::interpreter::execute(&stripped_content);
|
rush::interpreter::execute_with_filename(&stripped_content, Some(script_path.clone()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub enum AstNode {
|
|||||||
name: String,
|
name: String,
|
||||||
args: Vec<AstNode>,
|
args: Vec<AstNode>,
|
||||||
},
|
},
|
||||||
|
FunctionCall {
|
||||||
|
name: String,
|
||||||
|
args: Vec<AstNode>,
|
||||||
|
},
|
||||||
For {
|
For {
|
||||||
var: String,
|
var: String,
|
||||||
items: Vec<String>,
|
items: Vec<String>,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
|
||||||
use crate::error::RushError;
|
use crate::error::RushError;
|
||||||
|
use crate::error::colors;
|
||||||
pub use ast::*;
|
pub use ast::*;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@@ -72,6 +73,11 @@ impl Parser {
|
|||||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AstNode::FunctionCall { name: _, args } => {
|
||||||
|
for arg in args {
|
||||||
|
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
AstNode::For {
|
AstNode::For {
|
||||||
var,
|
var,
|
||||||
items: _,
|
items: _,
|
||||||
@@ -129,10 +135,12 @@ impl Parser {
|
|||||||
AstNode::Literal(s) => {
|
AstNode::Literal(s) => {
|
||||||
for var_name in self.extract_variables(s) {
|
for var_name in self.extract_variables(s) {
|
||||||
if !defined_vars.contains(&var_name) {
|
if !defined_vars.contains(&var_name) {
|
||||||
return Err(RushError::VariableError(format!(
|
return Err(RushError::VariableError(
|
||||||
"Variable '{}' is used before being defined",
|
colors::error(&format!(
|
||||||
var_name
|
"Variable '{}' is used before being defined",
|
||||||
)));
|
colors::highlight(&format!("${}", var_name))
|
||||||
|
))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +152,11 @@ impl Parser {
|
|||||||
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AstNode::FunctionCall { name: _, args } => {
|
||||||
|
for arg in args {
|
||||||
|
self.check_node_for_undefined_vars(arg, defined_vars)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// other node types dont directly contain variable references
|
// other node types dont directly contain variable references
|
||||||
}
|
}
|
||||||
@@ -180,7 +193,7 @@ impl Parser {
|
|||||||
fn parse_statement(
|
fn parse_statement(
|
||||||
&self,
|
&self,
|
||||||
lines: &[&str],
|
lines: &[&str],
|
||||||
mut i: usize,
|
i: usize,
|
||||||
) -> Result<(Option<AstNode>, usize), RushError> {
|
) -> Result<(Option<AstNode>, usize), RushError> {
|
||||||
self.parse_statement_with_context(lines, i, false)
|
self.parse_statement_with_context(lines, i, false)
|
||||||
}
|
}
|
||||||
@@ -278,7 +291,42 @@ impl Parser {
|
|||||||
let var_name = parts[0].trim();
|
let var_name = parts[0].trim();
|
||||||
if !var_name.is_empty() && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
if !var_name.is_empty() && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||||
{
|
{
|
||||||
let value = parts[1].trim().trim_matches('"').to_string();
|
let value_str = parts[1].trim();
|
||||||
|
|
||||||
|
// Check if the value is a function call (e.g., input("prompt") or confirm("question"))
|
||||||
|
// But NOT command substitution $(...)
|
||||||
|
let is_command_sub = value_str.trim_start_matches('"').starts_with("$(");
|
||||||
|
|
||||||
|
if value_str.contains('(') && value_str.contains(')') && !is_command_sub {
|
||||||
|
let fn_start = value_str.find('(').unwrap();
|
||||||
|
let fn_name = value_str[..fn_start].trim();
|
||||||
|
let fn_end = value_str.rfind(')').unwrap();
|
||||||
|
let args_str = &value_str[fn_start + 1..fn_end];
|
||||||
|
|
||||||
|
let args: Vec<AstNode> = if args_str.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
args_str
|
||||||
|
.split(',')
|
||||||
|
.map(|arg| {
|
||||||
|
AstNode::Literal(arg.trim().trim_matches('"').to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok((
|
||||||
|
Some(AstNode::VariableAssignment {
|
||||||
|
name: var_name.to_string(),
|
||||||
|
value: Box::new(AstNode::FunctionCall {
|
||||||
|
name: fn_name.to_string(),
|
||||||
|
args,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
i + 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = value_str.trim_matches('"').to_string();
|
||||||
return Ok((
|
return Ok((
|
||||||
Some(AstNode::VariableAssignment {
|
Some(AstNode::VariableAssignment {
|
||||||
name: var_name.to_string(),
|
name: var_name.to_string(),
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use std::io::{self, Write};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
pub fn echo(args: Vec<String>) {
|
pub fn echo(args: Vec<String>) {
|
||||||
println!("{}", args.join(" "));
|
println!("{}", args.join(" "));
|
||||||
}
|
}
|
||||||
@@ -13,6 +16,169 @@ pub fn exit(code: i32) {
|
|||||||
std::process::exit(code);
|
std::process::exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cd(path: &str) -> Result<(), String> {
|
||||||
|
env::set_current_dir(path).map_err(|e| format!("cd: {}: {}", path, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test file/directory conditions
|
||||||
|
/// Returns true (1) if condition is met, false (0) otherwise
|
||||||
|
/// Supported flags:
|
||||||
|
/// -d path: true if path exists and is a directory
|
||||||
|
/// -f path: true if path exists and is a regular file
|
||||||
|
/// -e path: true if path exists
|
||||||
|
pub fn test(args: &[String]) -> bool {
|
||||||
|
if args.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match args[0].as_str() {
|
||||||
|
"-d" => {
|
||||||
|
if args.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::path::Path::new(&args[1]).is_dir()
|
||||||
|
}
|
||||||
|
"-f" => {
|
||||||
|
if args.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::path::Path::new(&args[1]).is_file()
|
||||||
|
}
|
||||||
|
"-e" => {
|
||||||
|
if args.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::path::Path::new(&args[1]).exists()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("test: unknown flag '{}'", args[0]);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a line of input from stdin with an optional prompt
|
||||||
|
/// Returns the input string (trimmed)
|
||||||
|
pub fn input(prompt: Option<&str>) -> String {
|
||||||
|
if let Some(p) = prompt {
|
||||||
|
print!("{}", p);
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut buffer)
|
||||||
|
.expect("Failed to read input");
|
||||||
|
|
||||||
|
buffer.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask a yes/no question and return true for yes, false for no
|
||||||
|
/// Accepts: y/yes/Y/YES or n/no/N/NO
|
||||||
|
/// Keeps prompting until valid input is received
|
||||||
|
pub fn confirm(prompt: &str) -> bool {
|
||||||
|
loop {
|
||||||
|
print!("{} (y/n): ", prompt);
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut buffer)
|
||||||
|
.expect("Failed to read input");
|
||||||
|
|
||||||
|
let response = buffer.trim().to_lowercase();
|
||||||
|
match response.as_str() {
|
||||||
|
"y" | "yes" => return true,
|
||||||
|
"n" | "no" => return false,
|
||||||
|
_ => {
|
||||||
|
println!("Please enter 'y' or 'n'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display a menu and let the user select one option
|
||||||
|
/// Returns the selected option (1-indexed) or 0 if cancelled
|
||||||
|
pub fn select(prompt: &str, options: &[String]) -> usize {
|
||||||
|
if options.is_empty() {
|
||||||
|
eprintln!("Error: No options provided to select");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", prompt);
|
||||||
|
println!();
|
||||||
|
for (i, option) in options.iter().enumerate() {
|
||||||
|
println!(" {}. {}", i + 1, option);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
print!("Select (1-{}): ", options.len());
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut buffer)
|
||||||
|
.expect("Failed to read input");
|
||||||
|
|
||||||
|
let choice = buffer.trim();
|
||||||
|
if let Ok(num) = choice.parse::<usize>() {
|
||||||
|
if num >= 1 && num <= options.len() {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Please enter a number between 1 and {}", options.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display a menu and let the user select multiple options
|
||||||
|
/// Returns a comma-separated string of selected indices (1-indexed)
|
||||||
|
/// User can enter multiple numbers separated by commas or spaces
|
||||||
|
pub fn multiselect(prompt: &str, options: &[String]) -> String {
|
||||||
|
if options.is_empty() {
|
||||||
|
eprintln!("Error: No options provided to multiselect");
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", prompt);
|
||||||
|
println!();
|
||||||
|
for (i, option) in options.iter().enumerate() {
|
||||||
|
println!(" {}. {}", i + 1, option);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
print!("Select multiple (e.g., 1,2,3 or 1 2 3): ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut buffer)
|
||||||
|
.expect("Failed to read input");
|
||||||
|
|
||||||
|
let input = buffer.trim();
|
||||||
|
|
||||||
|
let selections: Vec<usize> = input
|
||||||
|
.split(|c: char| c == ',' || c.is_whitespace())
|
||||||
|
.filter_map(|s| s.trim().parse::<usize>().ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if selections.iter().all(|&n| n >= 1 && n <= options.len()) && !selections.is_empty() {
|
||||||
|
return selections
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if selections.is_empty() {
|
||||||
|
println!("Please enter at least one number");
|
||||||
|
} else {
|
||||||
|
println!("Please enter numbers between 1 and {}", options.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_root() -> bool {
|
fn is_root() -> bool {
|
||||||
// check if the current user is root by checking UID
|
// check if the current user is root by checking UID
|
||||||
// on Unix systems, root has UID 0
|
// on Unix systems, root has UID 0
|
||||||
|
|||||||
@@ -87,26 +87,57 @@ impl Environment {
|
|||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
if ch == '$' {
|
if ch == '$' {
|
||||||
let mut var_name = String::new();
|
if chars.peek() == Some(&'(') {
|
||||||
|
chars.next();
|
||||||
while let Some(&next_ch) = chars.peek() {
|
let mut cmd = String::new();
|
||||||
if next_ch.is_alphanumeric() || next_ch == '_' {
|
let mut depth = 1;
|
||||||
var_name.push(next_ch);
|
|
||||||
|
while let Some(&next_ch) = chars.peek() {
|
||||||
chars.next();
|
chars.next();
|
||||||
} else {
|
if next_ch == '(' {
|
||||||
break;
|
depth += 1;
|
||||||
|
cmd.push(next_ch);
|
||||||
|
} else if next_ch == ')' {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cmd.push(next_ch);
|
||||||
|
} else {
|
||||||
|
cmd.push(next_ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if let Ok(output) = std::process::Command::new("sh")
|
||||||
if !var_name.is_empty() {
|
.arg("-c")
|
||||||
if let Some(value) = self.get_variable(&var_name) {
|
.arg(&cmd)
|
||||||
result.push_str(value);
|
.output()
|
||||||
} else {
|
{
|
||||||
result.push('$');
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
result.push_str(&var_name);
|
result.push_str(stdout.trim());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.push('$');
|
let mut var_name = String::new();
|
||||||
|
|
||||||
|
while let Some(&next_ch) = chars.peek() {
|
||||||
|
if next_ch.is_alphanumeric() || next_ch == '_' {
|
||||||
|
var_name.push(next_ch);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !var_name.is_empty() {
|
||||||
|
if let Some(value) = self.get_variable(&var_name) {
|
||||||
|
result.push_str(value);
|
||||||
|
} else {
|
||||||
|
result.push('$');
|
||||||
|
result.push_str(&var_name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push('$');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.push(ch);
|
result.push(ch);
|
||||||
|
|||||||
@@ -2,3 +2,15 @@
|
|||||||
.vscode-test/**
|
.vscode-test/**
|
||||||
.gitignore
|
.gitignore
|
||||||
vsc-extension-quickstart.md
|
vsc-extension-quickstart.md
|
||||||
|
node_modules/**
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
*.vsix
|
||||||
|
.editorconfig
|
||||||
|
.eslintrc.json
|
||||||
|
tsconfig.json
|
||||||
|
**/*.ts
|
||||||
|
**/*.map
|
||||||
|
**/tsconfig.json
|
||||||
|
README-old.md
|
||||||
|
INSTALL.md
|
||||||
@@ -2,8 +2,29 @@
|
|||||||
|
|
||||||
All notable changes to the "rush" extension will be documented in this file.
|
All notable changes to the "rush" extension will be documented in this file.
|
||||||
|
|
||||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
## [0.0.2] - 2025-11-03
|
||||||
|
|
||||||
## [Unreleased]
|
### Added
|
||||||
|
- Added `test` builtin command syntax highlighting
|
||||||
|
- Added `input`, `confirm`, `select`, `multiselect` builtin functions
|
||||||
|
- Shebang lines (`#!/...`) now have distinct purple highlighting
|
||||||
|
|
||||||
- Initial release
|
### Changed
|
||||||
|
- Improved shebang pattern matching to work on any line
|
||||||
|
- Removed shell command highlighting (git, cargo, npm, etc.) - only Rush builtins are highlighted now
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Comments no longer match shebang lines
|
||||||
|
- Shebang `#!` punctuation now has the same color as the rest of the line
|
||||||
|
|
||||||
|
## [0.0.1] - 2025-11-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
|
- Basic syntax highlighting for Rush shell scripts
|
||||||
|
- Support for `.rush` and `.rsh` file extensions
|
||||||
|
- Keyword highlighting (if, else, for, parallel, workers)
|
||||||
|
- Variable highlighting ($VAR syntax)
|
||||||
|
- Builtin command highlighting (echo, exit, cd, require_root)
|
||||||
|
- Comment support
|
||||||
|
- String interpolation support
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "rush",
|
"name": "rush",
|
||||||
"displayName": "Rush Shell",
|
"displayName": "Rush Shell",
|
||||||
"description": "Syntax highlighting and validation for Rush shell scripts",
|
"description": "Syntax highlighting and validation for Rush shell scripts",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"publisher": "rush-lang",
|
"publisher": "rush-lang",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
"include": "#shebang"
|
"include": "#shebang"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#invalid-syntax"
|
"include": "#comments"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#comments"
|
"include": "#invalid-syntax"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#keywords"
|
"include": "#keywords"
|
||||||
@@ -40,11 +40,11 @@
|
|||||||
"shebang": {
|
"shebang": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "meta.shebang.rush",
|
"name": "meta.directive.shebang.rush",
|
||||||
"match": "\\A#!.*$",
|
"match": "^(#!.*$)",
|
||||||
"captures": {
|
"captures": {
|
||||||
"0": {
|
"1": {
|
||||||
"name": "comment.line.shebang.rush"
|
"name": "keyword.other.directive.shebang.rush"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "comment.line.number-sign.rush",
|
"name": "comment.line.number-sign.rush",
|
||||||
"match": "#(?!!).*$"
|
"match": "^[\\t ]*#(?!\\!).*$"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "support.function.builtin.rush",
|
"name": "support.function.builtin.rush",
|
||||||
"match": "\\b(echo|exit|cd|ls|mkdir|rm|cp|mv|cat|grep|find|chmod|chown)\\b"
|
"match": "\\b(echo|exit|cd|test|require_root|input|confirm|select|multiselect)\\b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user