added interactive inputs, better error messages, bugfixes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
target
|
||||
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`
|
||||
- **For loops** for iteration
|
||||
- **Parallel execution** blocks for concurrent tasks
|
||||
- **Interactive input** with `input()`, `confirm()`, `select()`, and `multiselect()`
|
||||
- **Strict parsing** that catches errors before execution
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- [Loops](./language/loops.md)
|
||||
- [Parallel Execution](./language/parallel.md)
|
||||
- [Built-in Variables](./language/builtins.md)
|
||||
- [User Input](./language/input.md)
|
||||
|
||||
# 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)]
|
||||
pub enum RushError {
|
||||
SyntaxError(String),
|
||||
@@ -7,7 +9,11 @@ pub enum RushError {
|
||||
|
||||
impl std::fmt::Display for RushError {
|
||||
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,24 +1,54 @@
|
||||
use crate::parser::ast::{AstNode, Command, Statement};
|
||||
use crate::runtime::builtins;
|
||||
use crate::runtime::environment::Environment;
|
||||
use crate::error::colors;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub struct Executor<'a> {
|
||||
env: &'a mut Environment,
|
||||
current_file: Option<String>,
|
||||
current_line: usize,
|
||||
}
|
||||
|
||||
impl<'a> Executor<'a> {
|
||||
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) {
|
||||
match node {
|
||||
AstNode::VariableAssignment { name, value } => {
|
||||
if let AstNode::Literal(val) = *value {
|
||||
match *value {
|
||||
AstNode::Literal(val) => {
|
||||
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 } => {
|
||||
let arg_strings: Vec<String> = args
|
||||
@@ -43,6 +73,16 @@ impl<'a> Executor<'a> {
|
||||
builtins::exit(code);
|
||||
} else if name == "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 {
|
||||
let cmd = Command {
|
||||
name: name.clone(),
|
||||
@@ -51,6 +91,10 @@ impl<'a> Executor<'a> {
|
||||
self.execute_command(cmd);
|
||||
}
|
||||
}
|
||||
AstNode::FunctionCall { name, args } => {
|
||||
|
||||
self.execute_function_call(&name, args);
|
||||
}
|
||||
AstNode::ControlFlow {
|
||||
condition,
|
||||
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) {
|
||||
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 {
|
||||
Ok(output) => {
|
||||
|
||||
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());
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
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() && !suppress_stderr {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Command '{}' failed with exit code: {:?}",
|
||||
command.name,
|
||||
output.status.code()
|
||||
"{}{}",
|
||||
colors::error(&format!(
|
||||
"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) => {
|
||||
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 {
|
||||
// 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 != "0"
|
||||
&& value.to_lowercase() != "false"
|
||||
&& value.to_lowercase() != "no"
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ use crate::runtime::environment::Environment;
|
||||
use executor::Executor;
|
||||
|
||||
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 program = match parser.parse(content) {
|
||||
Ok(prog) => prog,
|
||||
Err(e) => {
|
||||
eprintln!("Parse error: {}", e);
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
@@ -20,6 +24,10 @@ pub fn execute(content: &str) {
|
||||
|
||||
let mut executor = Executor::new(&mut env);
|
||||
|
||||
if let Some(file) = filename {
|
||||
executor.set_file(file);
|
||||
}
|
||||
|
||||
for statement in program.statements {
|
||||
executor.execute_node(statement);
|
||||
}
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -1,3 +1,5 @@
|
||||
use rush::error::colors;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
@@ -11,9 +13,12 @@ fn main() {
|
||||
let content = std::fs::read_to_string(script_path);
|
||||
if content.is_err() {
|
||||
eprintln!(
|
||||
"Error reading file {}: {}",
|
||||
script_path,
|
||||
"{}",
|
||||
colors::error(&format!(
|
||||
"Failed to read file '{}': {}",
|
||||
colors::highlight(script_path),
|
||||
content.err().unwrap()
|
||||
))
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -25,5 +30,5 @@ fn main() {
|
||||
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,
|
||||
args: Vec<AstNode>,
|
||||
},
|
||||
FunctionCall {
|
||||
name: String,
|
||||
args: Vec<AstNode>,
|
||||
},
|
||||
For {
|
||||
var: String,
|
||||
items: Vec<String>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod ast;
|
||||
|
||||
use crate::error::RushError;
|
||||
use crate::error::colors;
|
||||
pub use ast::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -72,6 +73,11 @@ impl Parser {
|
||||
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 {
|
||||
var,
|
||||
items: _,
|
||||
@@ -129,10 +135,12 @@ impl Parser {
|
||||
AstNode::Literal(s) => {
|
||||
for var_name in self.extract_variables(s) {
|
||||
if !defined_vars.contains(&var_name) {
|
||||
return Err(RushError::VariableError(format!(
|
||||
return Err(RushError::VariableError(
|
||||
colors::error(&format!(
|
||||
"Variable '{}' is used before being defined",
|
||||
var_name
|
||||
)));
|
||||
colors::highlight(&format!("${}", var_name))
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +152,11 @@ impl Parser {
|
||||
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
|
||||
}
|
||||
@@ -180,7 +193,7 @@ impl Parser {
|
||||
fn parse_statement(
|
||||
&self,
|
||||
lines: &[&str],
|
||||
mut i: usize,
|
||||
i: usize,
|
||||
) -> Result<(Option<AstNode>, usize), RushError> {
|
||||
self.parse_statement_with_context(lines, i, false)
|
||||
}
|
||||
@@ -278,7 +291,42 @@ impl Parser {
|
||||
let var_name = parts[0].trim();
|
||||
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((
|
||||
Some(AstNode::VariableAssignment {
|
||||
name: var_name.to_string(),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::io::{self, Write};
|
||||
use std::env;
|
||||
|
||||
pub fn echo(args: Vec<String>) {
|
||||
println!("{}", args.join(" "));
|
||||
}
|
||||
@@ -13,6 +16,169 @@ pub fn exit(code: i32) {
|
||||
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 {
|
||||
// check if the current user is root by checking UID
|
||||
// on Unix systems, root has UID 0
|
||||
|
||||
@@ -87,6 +87,36 @@ impl Environment {
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '$' {
|
||||
if chars.peek() == Some(&'(') {
|
||||
chars.next();
|
||||
let mut cmd = String::new();
|
||||
let mut depth = 1;
|
||||
|
||||
while let Some(&next_ch) = chars.peek() {
|
||||
chars.next();
|
||||
if next_ch == '(' {
|
||||
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")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
result.push_str(stdout.trim());
|
||||
}
|
||||
} else {
|
||||
let mut var_name = String::new();
|
||||
|
||||
while let Some(&next_ch) = chars.peek() {
|
||||
@@ -108,6 +138,7 @@ impl Environment {
|
||||
} else {
|
||||
result.push('$');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(ch);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,15 @@
|
||||
.vscode-test/**
|
||||
.gitignore
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### 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",
|
||||
"displayName": "Rush Shell",
|
||||
"description": "Syntax highlighting and validation for Rush shell scripts",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"publisher": "rush-lang",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"include": "#shebang"
|
||||
},
|
||||
{
|
||||
"include": "#invalid-syntax"
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
"include": "#invalid-syntax"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
@@ -40,11 +40,11 @@
|
||||
"shebang": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.shebang.rush",
|
||||
"match": "\\A#!.*$",
|
||||
"name": "meta.directive.shebang.rush",
|
||||
"match": "^(#!.*$)",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "comment.line.shebang.rush"
|
||||
"1": {
|
||||
"name": "keyword.other.directive.shebang.rush"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.number-sign.rush",
|
||||
"match": "#(?!!).*$"
|
||||
"match": "^[\\t ]*#(?!\\!).*$"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -163,7 +163,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"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