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
244 changes: 232 additions & 12 deletions crates/bashkit/src/builtins/printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,148 @@ impl Builtin for Printf {
}
}

/// Parsed format specification
struct FormatSpec {
left_align: bool,
zero_pad: bool,
sign_plus: bool,
width: Option<usize>,
precision: Option<usize>,
}

impl FormatSpec {
fn parse(spec: &str) -> Self {
let mut left_align = false;
let mut zero_pad = false;
let mut sign_plus = false;
let mut chars = spec.chars().peekable();

// Parse flags
while let Some(&c) = chars.peek() {
match c {
'-' => {
left_align = true;
chars.next();
}
'0' if !zero_pad && chars.clone().nth(1).is_some() => {
// Only treat as flag if followed by more chars (width)
zero_pad = true;
chars.next();
}
'+' => {
sign_plus = true;
chars.next();
}
' ' | '#' => {
chars.next();
}
_ => break,
}
}

// Parse width
let mut width_str = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
width_str.push(chars.next().unwrap());
} else {
break;
}
}
let width = if width_str.is_empty() {
None
} else {
width_str.parse().ok()
};

// Parse precision
let precision = if chars.peek() == Some(&'.') {
chars.next();
let mut prec_str = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
prec_str.push(chars.next().unwrap());
} else {
break;
}
}
if prec_str.is_empty() {
Some(0)
} else {
prec_str.parse().ok()
}
} else {
None
};

Self {
left_align,
zero_pad,
sign_plus,
width,
precision,
}
}

/// Format an integer with the parsed spec
fn format_int(&self, n: i64) -> String {
let formatted = if self.sign_plus && n >= 0 {
format!("+{}", n)
} else {
n.to_string()
};

self.apply_width(&formatted, true)
}

/// Format an unsigned integer with the parsed spec
fn format_uint(&self, n: u64) -> String {
let formatted = n.to_string();
self.apply_width(&formatted, true)
}

/// Format a string with the parsed spec
fn format_str(&self, s: &str) -> String {
let s = if let Some(prec) = self.precision {
&s[..s.len().min(prec)]
} else {
s
};
self.apply_width(s, false)
}

/// Apply width padding
fn apply_width(&self, s: &str, is_numeric: bool) -> String {
let width = match self.width {
Some(w) => w,
None => return s.to_string(),
};

if s.len() >= width {
return s.to_string();
}

let pad_char = if self.zero_pad && is_numeric && !self.left_align {
'0'
} else {
' '
};
let padding = width - s.len();

if self.left_align {
format!("{}{}", s, " ".repeat(padding))
} else if self.zero_pad && is_numeric && s.starts_with('-') {
// Handle negative numbers: put minus before zeros
format!("-{}{}", pad_char.to_string().repeat(padding), &s[1..])
} else if self.zero_pad && is_numeric && s.starts_with('+') {
// Handle explicit plus sign
format!("+{}{}", pad_char.to_string().repeat(padding), &s[1..])
} else {
format!("{}{}", pad_char.to_string().repeat(padding), s)
}
}
}

/// Format a string using printf-style format specifiers
fn format_string(format: &str, args: &[String], arg_index: &mut usize) -> String {
let mut output = String::new();
Expand Down Expand Up @@ -91,6 +233,8 @@ fn format_string(format: &str, args: &[String], arg_index: &mut usize) -> String
}
}

let fmt_spec = FormatSpec::parse(&spec);

// Get the format type
if let Some(fmt_type) = chars.next() {
let arg = args.get(*arg_index).map(|s| s.as_str()).unwrap_or("");
Expand All @@ -99,52 +243,60 @@ fn format_string(format: &str, args: &[String], arg_index: &mut usize) -> String
match fmt_type {
's' => {
// String
output.push_str(arg);
output.push_str(&fmt_spec.format_str(arg));
}
'd' | 'i' => {
// Integer
if let Ok(n) = arg.parse::<i64>() {
output.push_str(&n.to_string());
output.push_str(&fmt_spec.format_int(n));
} else {
output.push('0');
output.push_str(&fmt_spec.format_int(0));
}
}
'u' => {
// Unsigned integer
if let Ok(n) = arg.parse::<u64>() {
output.push_str(&n.to_string());
output.push_str(&fmt_spec.format_uint(n));
} else {
output.push('0');
output.push_str(&fmt_spec.format_uint(0));
}
}
'o' => {
// Octal
if let Ok(n) = arg.parse::<u64>() {
output.push_str(&format!("{:o}", n));
let formatted = format!("{:o}", n);
output.push_str(&fmt_spec.apply_width(&formatted, true));
} else {
output.push('0');
output.push_str(&fmt_spec.apply_width("0", true));
}
}
'x' => {
// Lowercase hex
if let Ok(n) = arg.parse::<u64>() {
output.push_str(&format!("{:x}", n));
let formatted = format!("{:x}", n);
output.push_str(&fmt_spec.apply_width(&formatted, true));
} else {
output.push('0');
output.push_str(&fmt_spec.apply_width("0", true));
}
}
'X' => {
// Uppercase hex
if let Ok(n) = arg.parse::<u64>() {
output.push_str(&format!("{:X}", n));
let formatted = format!("{:X}", n);
output.push_str(&fmt_spec.apply_width(&formatted, true));
} else {
output.push('0');
output.push_str(&fmt_spec.apply_width("0", true));
}
}
'f' | 'e' | 'E' | 'g' | 'G' => {
// Float
if let Ok(n) = arg.parse::<f64>() {
output.push_str(&format!("{}", n));
let formatted = if let Some(prec) = fmt_spec.precision {
format!("{:.prec$}", n, prec = prec)
} else {
format!("{}", n)
};
output.push_str(&fmt_spec.apply_width(&formatted, true));
} else {
output.push_str("0.0");
}
Expand Down Expand Up @@ -207,3 +359,71 @@ fn expand_escapes(s: &str) -> String {

output
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_zero_padding() {
let args = vec!["42".to_string()];
let mut idx = 0;
assert_eq!(format_string("%05d", &args, &mut idx), "00042");
}

#[test]
fn test_zero_padding_negative() {
let args = vec!["-42".to_string()];
let mut idx = 0;
assert_eq!(format_string("%06d", &args, &mut idx), "-00042");
}

#[test]
fn test_width_without_zero() {
let args = vec!["42".to_string()];
let mut idx = 0;
assert_eq!(format_string("%5d", &args, &mut idx), " 42");
}

#[test]
fn test_left_align() {
let args = vec!["42".to_string()];
let mut idx = 0;
assert_eq!(format_string("%-5d", &args, &mut idx), "42 ");
}

#[test]
fn test_string_width() {
let args = vec!["hi".to_string()];
let mut idx = 0;
assert_eq!(format_string("%5s", &args, &mut idx), " hi");
}

#[test]
fn test_string_left_align() {
let args = vec!["hi".to_string()];
let mut idx = 0;
assert_eq!(format_string("%-5s", &args, &mut idx), "hi ");
}

#[test]
fn test_precision_float() {
let args = vec!["3.14159".to_string()];
let mut idx = 0;
assert_eq!(format_string("%.2f", &args, &mut idx), "3.14");
}

#[test]
fn test_width_and_precision() {
let args = vec!["3.14".to_string()];
let mut idx = 0;
assert_eq!(format_string("%8.2f", &args, &mut idx), " 3.14");
}

#[test]
fn test_hex_zero_padding() {
let args = vec!["255".to_string()];
let mut idx = 0;
assert_eq!(format_string("%04x", &args, &mut idx), "00ff");
}
}
13 changes: 11 additions & 2 deletions crates/bashkit/src/builtins/sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ fn parse_sed_command(s: &str, extended_regex: bool) -> Result<(Option<Address>,
.replace("\\+", "+")
.replace("\\?", "?")
};

// Build regex with optional case-insensitive flag
let case_insensitive = flags.contains('i');
let regex = RegexBuilder::new(&pattern)
Expand All @@ -321,9 +320,10 @@ fn parse_sed_command(s: &str, extended_regex: bool) -> Result<(Option<Address>,
.replace('&', "$0")
.replace("\x00", "&");

// Use ${N} format instead of $N to avoid ambiguity with following chars
let replacement = Regex::new(r"\\(\d+)")
.unwrap()
.replace_all(&replacement, "$$$1")
.replace_all(&replacement, r"$${$1}")
.to_string();

// Parse nth occurrence from flags (e.g., "2" in s/a/b/2)
Expand Down Expand Up @@ -632,6 +632,15 @@ mod tests {
assert_eq!(result.stdout, "world hello\n");
}

#[tokio::test]
async fn test_sed_backref_single() {
// Test single backreference: capture "hel", replace entire match with captured + "p"
let result = run_sed(&["s/\\(hel\\)lo/\\1p/"], Some("hello"))
.await
.unwrap();
assert_eq!(result.stdout, "help\n");
}

#[tokio::test]
async fn test_sed_ampersand() {
let result = run_sed(&["s/world/[&]/"], Some("hello world"))
Expand Down
Loading
Loading