Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions crates/bashkit/src/builtins/printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,44 @@ impl Builtin for Printf {
return Ok(ExecResult::ok(String::new()));
}

let format = &ctx.args[0];
let args = &ctx.args[1..];
let mut args_iter = ctx.args.iter();
let mut var_name: Option<String> = None;

// Check for -v varname flag
let format = loop {
match args_iter.next() {
Some(arg) if arg == "-v" => {
if let Some(vname) = args_iter.next() {
var_name = Some(vname.clone());
}
}
Some(arg) => break arg.clone(),
None => return Ok(ExecResult::ok(String::new())),
}
};

let args: Vec<String> = args_iter.cloned().collect();
let mut arg_index = 0;
let mut output = String::new();

// Bash printf repeats the format string until all args are consumed
loop {
let start_index = arg_index;
output.push_str(&format_string(format, args, &mut arg_index));
output.push_str(&format_string(&format, &args, &mut arg_index));

// If no args were consumed or we've used all args, stop
if arg_index == start_index || arg_index >= args.len() {
break;
}
}

Ok(ExecResult::ok(output))
if let Some(name) = var_name {
// -v: assign to variable instead of printing
ctx.variables.insert(name, output);
Ok(ExecResult::ok(String::new()))
} else {
Ok(ExecResult::ok(output))
}
}
}

Expand Down
84 changes: 70 additions & 14 deletions crates/bashkit/src/builtins/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,60 @@ impl Builtin for Read {

// Parse flags
let mut raw_mode = false; // -r: don't interpret backslashes
let mut array_mode = false; // -a: read into array
let mut delimiter = None::<char>; // -d: custom delimiter
let mut nchars = None::<usize>; // -n: read N chars
let mut prompt = None::<String>; // -p prompt
let mut var_args = Vec::new();
let mut args_iter = ctx.args.iter();
while let Some(arg) = args_iter.next() {
if arg.starts_with('-') && arg.len() > 1 {
for flag in arg[1..].chars() {
let mut chars = arg[1..].chars();
while let Some(flag) = chars.next() {
match flag {
'r' => raw_mode = true,
'a' => array_mode = true,
'd' => {
// -d delim: use first char of next arg as delimiter
let rest: String = chars.collect();
let delim_str = if rest.is_empty() {
args_iter.next().map(|s| s.as_str()).unwrap_or("")
} else {
&rest
};
delimiter = delim_str.chars().next();
break;
}
'n' => {
let rest: String = chars.collect();
let n_str = if rest.is_empty() {
args_iter.next().map(|s| s.as_str()).unwrap_or("0")
} else {
&rest
};
nchars = n_str.parse().ok();
break;
}
'p' => {
// -p takes next arg as prompt
if let Some(p) = args_iter.next() {
prompt = Some(p.clone());
let rest: String = chars.collect();
prompt = Some(if rest.is_empty() {
args_iter.next().cloned().unwrap_or_default()
} else {
rest
});
break;
}
't' | 's' | 'u' | 'e' | 'i' => {
// -t timeout, -s silent, -u fd: accept and ignore
if matches!(flag, 't' | 'u') {
let rest: String = chars.collect();
if rest.is_empty() {
args_iter.next();
}
break;
}
}
_ => {} // ignore unknown flags
_ => {}
}
}
} else {
Expand All @@ -43,8 +82,14 @@ impl Builtin for Read {
}
let _ = prompt; // prompt is for interactive use, ignored in non-interactive

// Get first line
let line = if raw_mode {
// Extract input based on delimiter or nchars
let line = if let Some(n) = nchars {
// -n N: read at most N chars
input.chars().take(n).collect::<String>()
} else if let Some(delim) = delimiter {
// -d delim: read until delimiter
input.split(delim).next().unwrap_or("").to_string()
} else if raw_mode {
// -r: treat backslashes literally
input.lines().next().unwrap_or("").to_string()
} else {
Expand All @@ -61,13 +106,6 @@ impl Builtin for Read {
result
};

// If no variable names given, use REPLY
let var_names: Vec<&str> = if var_args.is_empty() {
vec!["REPLY"]
} else {
var_args
};

// Split line by IFS (default: space, tab, newline)
let ifs = ctx.env.get("IFS").map(|s| s.as_str()).unwrap_or(" \t\n");
let words: Vec<&str> = if ifs.is_empty() {
Expand All @@ -79,6 +117,24 @@ impl Builtin for Read {
.collect()
};

if array_mode {
// -a: read all words into array variable
let arr_name = var_args.first().copied().unwrap_or("REPLY");
// Store as _ARRAY_<name>_<idx> for the interpreter to pick up
ctx.variables.insert(
format!("_ARRAY_READ_{}", arr_name),
words.join("\x1F"), // unit separator as delimiter
);
return Ok(ExecResult::ok(String::new()));
}

// If no variable names given, use REPLY
let var_names: Vec<&str> = if var_args.is_empty() {
vec!["REPLY"]
} else {
var_args
};

// Assign words to variables
for (i, var_name) in var_names.iter().enumerate() {
if i == var_names.len() - 1 {
Expand Down
44 changes: 37 additions & 7 deletions crates/bashkit/src/builtins/vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,29 @@ impl Builtin for Unset {

/// set builtin - set/display shell options and positional parameters
///
/// Currently supports:
/// - `set -e` - exit on error (stored but not enforced yet)
/// - `set -x` - trace mode (stored but not enforced yet)
/// Supports:
/// - `set -e` / `set +e` - errexit
/// - `set -u` / `set +u` - nounset
/// - `set -x` / `set +x` - xtrace
/// - `set -o option` / `set +o option` - long option names
/// - `set --` - set positional parameters
pub struct Set;

/// Map long option names to their SHOPT_* variable names
fn option_name_to_var(name: &str) -> Option<&'static str> {
match name {
"errexit" => Some("SHOPT_e"),
"nounset" => Some("SHOPT_u"),
"xtrace" => Some("SHOPT_x"),
"verbose" => Some("SHOPT_v"),
"pipefail" => Some("SHOPT_pipefail"),
"noclobber" => Some("SHOPT_C"),
"noglob" => Some("SHOPT_f"),
"noexec" => Some("SHOPT_n"),
_ => None,
}
}

#[async_trait]
impl Builtin for Set {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
Expand All @@ -43,20 +60,33 @@ impl Builtin for Set {
return Ok(ExecResult::ok(output));
}

for arg in ctx.args.iter() {
let mut i = 0;
while i < ctx.args.len() {
let arg = &ctx.args[i];
if arg == "--" {
// Set positional parameters (would need call stack access)
// For now, just consume remaining args
break;
} else if (arg.starts_with('-') || arg.starts_with('+'))
&& arg.len() > 1
&& (arg.as_bytes()[1] == b'o' && arg.len() == 2)
{
// -o option_name / +o option_name
let enable = arg.starts_with('-');
i += 1;
if i < ctx.args.len() {
if let Some(var) = option_name_to_var(&ctx.args[i]) {
ctx.variables
.insert(var.to_string(), if enable { "1" } else { "0" }.to_string());
}
}
} else if arg.starts_with('-') || arg.starts_with('+') {
// Shell options - store in variables for now
let enable = arg.starts_with('-');
for opt in arg.chars().skip(1) {
let opt_name = format!("SHOPT_{}", opt);
ctx.variables
.insert(opt_name, if enable { "1" } else { "0" }.to_string());
}
}
i += 1;
}

Ok(ExecResult::ok(String::new()))
Expand Down
Loading
Loading