use super::*;
use crate::builtin::{BuiltinHost, BuiltinProperties, BuiltinRegistry, RegisteredBuiltin};
use libc::strtod;
use std::ffi::CString;
use std::ptr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) enum Builtin {
Colon,
True,
False,
Cd,
Pwd,
Export,
Unset,
Exit,
Readonly,
Shift,
Eval,
Dot,
Exec,
Read,
Alias,
Unalias,
Command,
Type,
Set,
Times,
Umask,
Getopts,
Meta,
Jobs,
Bg,
Fg,
Ulimit,
Wait,
Trap,
Break,
Continue,
Return,
Echo,
Printf,
}
#[derive(Clone, Copy)]
struct BuiltinSpec {
name: &'static str,
kind: Builtin,
special: bool,
}
const BUILTINS: &[BuiltinSpec] = &[
BuiltinSpec {
name: ":",
kind: Builtin::Colon,
special: true,
},
BuiltinSpec {
name: "true",
kind: Builtin::True,
special: false,
},
BuiltinSpec {
name: "false",
kind: Builtin::False,
special: false,
},
BuiltinSpec {
name: "cd",
kind: Builtin::Cd,
special: false,
},
BuiltinSpec {
name: "pwd",
kind: Builtin::Pwd,
special: false,
},
BuiltinSpec {
name: "export",
kind: Builtin::Export,
special: true,
},
BuiltinSpec {
name: "unset",
kind: Builtin::Unset,
special: true,
},
BuiltinSpec {
name: "exit",
kind: Builtin::Exit,
special: true,
},
BuiltinSpec {
name: "readonly",
kind: Builtin::Readonly,
special: true,
},
BuiltinSpec {
name: "shift",
kind: Builtin::Shift,
special: true,
},
BuiltinSpec {
name: "eval",
kind: Builtin::Eval,
special: true,
},
BuiltinSpec {
name: ".",
kind: Builtin::Dot,
special: true,
},
BuiltinSpec {
name: "exec",
kind: Builtin::Exec,
special: true,
},
BuiltinSpec {
name: "read",
kind: Builtin::Read,
special: false,
},
BuiltinSpec {
name: "alias",
kind: Builtin::Alias,
special: false,
},
BuiltinSpec {
name: "unalias",
kind: Builtin::Unalias,
special: false,
},
BuiltinSpec {
name: "command",
kind: Builtin::Command,
special: false,
},
BuiltinSpec {
name: "type",
kind: Builtin::Type,
special: false,
},
BuiltinSpec {
name: "set",
kind: Builtin::Set,
special: true,
},
BuiltinSpec {
name: "times",
kind: Builtin::Times,
special: true,
},
BuiltinSpec {
name: "umask",
kind: Builtin::Umask,
special: false,
},
BuiltinSpec {
name: "getopts",
kind: Builtin::Getopts,
special: false,
},
BuiltinSpec {
name: "builtin",
kind: Builtin::Meta,
special: false,
},
BuiltinSpec {
name: "jobs",
kind: Builtin::Jobs,
special: false,
},
BuiltinSpec {
name: "bg",
kind: Builtin::Bg,
special: false,
},
BuiltinSpec {
name: "fg",
kind: Builtin::Fg,
special: false,
},
BuiltinSpec {
name: "ulimit",
kind: Builtin::Ulimit,
special: false,
},
BuiltinSpec {
name: "wait",
kind: Builtin::Wait,
special: false,
},
BuiltinSpec {
name: "trap",
kind: Builtin::Trap,
special: true,
},
BuiltinSpec {
name: "break",
kind: Builtin::Break,
special: true,
},
BuiltinSpec {
name: "continue",
kind: Builtin::Continue,
special: true,
},
BuiltinSpec {
name: "return",
kind: Builtin::Return,
special: true,
},
BuiltinSpec {
name: "echo",
kind: Builtin::Echo,
special: false,
},
BuiltinSpec {
name: "printf",
kind: Builtin::Printf,
special: false,
},
];
pub(crate) fn standard_builtin_registry() -> BuiltinRegistry {
let mut builtin_registry = BuiltinRegistry::empty();
for spec in BUILTINS {
let properties = if spec.special {
BuiltinProperties::special()
} else {
BuiltinProperties::regular()
};
let _ = builtin_registry.insert_standard(spec.name, spec.kind, properties);
}
builtin_registry
}
pub(super) fn lookup_builtin(state: &ShellState, name: &str) -> Option<RegisteredBuiltin> {
state.definition.builtin_registry.lookup(name)
}
pub(super) fn is_special_builtin_in(state: &ShellState, name: &str) -> bool {
lookup_builtin(state, name).is_some_and(|builtin| builtin.properties().is_special())
}
pub(super) fn is_builtin_in(state: &ShellState, name: &str) -> bool {
lookup_builtin(state, name).is_some()
}
pub(super) fn builtin_names_for_state(state: &ShellState) -> Vec<String> {
state.definition.builtin_registry.names()
}
fn deny_builtin_by_security_policy(state: &ShellState, builtin: &str) -> i32 {
shell_errln(
state,
&format!("{builtin}: disabled by shell security policy"),
);
126
}
fn builtin_echo(state: &mut ShellState, args: &[String]) -> i32 {
let (suppress_newline, print_args) = if args.first().map(|s| s.as_str()) == Some("-n") {
(true, &args[1..])
} else {
(false, args)
};
let mut output = print_args.join(" ");
if !suppress_newline {
output.push('\n');
}
if shell_out(state, &output).is_ok() {
0
} else {
1
}
}
enum PrintfPart {
Literal(Vec<u8>),
Conversion(PrintfFormat),
}
#[derive(Clone, Copy)]
enum PrintfConversion {
String,
Decimal,
Unsigned,
Octal,
HexLower,
HexUpper,
Char,
Escaped,
FloatFixedLower,
FloatFixedUpper,
FloatExpLower,
FloatExpUpper,
FloatGeneralLower,
FloatGeneralUpper,
}
#[derive(Clone, Copy)]
struct PrintfFormat {
conversion: PrintfConversion,
left_justify: bool,
force_sign: bool,
space_sign: bool,
alternate: bool,
zero_pad: bool,
width: Option<usize>,
precision: Option<usize>,
}
struct PrintfPlan {
parts: Vec<PrintfPart>,
slots: usize,
stop_after_render: bool,
}
struct PrintfValue {
bytes: Vec<u8>,
display_width: usize,
}
impl PrintfValue {
fn from_string(value: String) -> Self {
let display_width = value.chars().count();
Self {
bytes: value.into_bytes(),
display_width,
}
}
}
enum PrintfEscape {
Continue,
Stop,
}
fn flush_printf_literal(parts: &mut Vec<PrintfPart>, literal: &mut Vec<u8>) {
if !literal.is_empty() {
parts.push(PrintfPart::Literal(std::mem::take(literal)));
}
}
fn push_printf_escape(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
literal: &mut Vec<u8>,
) -> Option<PrintfEscape> {
match chars.next()? {
'a' => literal.push(0x07),
'b' => literal.push(0x08),
'c' => return Some(PrintfEscape::Stop),
'f' => literal.push(0x0c),
'n' => literal.push(b'\n'),
'r' => literal.push(b'\r'),
't' => literal.push(b'\t'),
'v' => literal.push(0x0b),
'\\' => literal.push(b'\\'),
'0' => {
let mut value = 0u16;
let mut saw_digit = false;
for _ in 0..3 {
let Some(next) = chars.peek().copied() else {
break;
};
let Some(digit) = next.to_digit(8) else {
break;
};
saw_digit = true;
value = (value << 3) | digit as u16;
let _ = chars.next();
}
literal.push(if saw_digit { value as u8 } else { 0 });
return Some(PrintfEscape::Continue);
}
_ => return None,
}
Some(PrintfEscape::Continue)
}
fn parse_printf_plan(format: &str) -> Option<PrintfPlan> {
let mut chars = format.chars().peekable();
let mut parts = Vec::new();
let mut literal = Vec::new();
let mut string_slots = 0;
let mut stop_after_render = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => match push_printf_escape(&mut chars, &mut literal)? {
PrintfEscape::Continue => {}
PrintfEscape::Stop => {
stop_after_render = true;
break;
}
},
'%' => match chars.next()? {
'%' => literal.push(b'%'),
first => {
let mut spec = PrintfFormat {
conversion: PrintfConversion::String,
left_justify: false,
force_sign: false,
space_sign: false,
alternate: false,
zero_pad: false,
width: None,
precision: None,
};
let mut conv = first;
loop {
match conv {
'-' => spec.left_justify = true,
'+' => spec.force_sign = true,
' ' => spec.space_sign = true,
'#' => spec.alternate = true,
'0' => spec.zero_pad = true,
_ => break,
}
conv = chars.next()?;
}
let mut width = String::new();
while conv.is_ascii_digit() {
width.push(conv);
conv = chars.next()?;
}
if !width.is_empty() {
spec.width = width.parse::<usize>().ok();
}
if conv == '.' {
let mut precision = String::new();
conv = chars.next()?;
while conv.is_ascii_digit() {
precision.push(conv);
match chars.next() {
Some(next) => conv = next,
None => break,
}
}
spec.precision = if precision.is_empty() {
Some(0)
} else {
precision.parse::<usize>().ok()
};
}
while matches!(conv, 'h' | 'l' | 'L' | 'j' | 'z' | 't') {
conv = chars.next()?;
}
if !matches!(
conv,
's' | 'd'
| 'i'
| 'u'
| 'o'
| 'x'
| 'X'
| 'c'
| 'b'
| 'e'
| 'E'
| 'f'
| 'F'
| 'g'
| 'G'
) {
return None;
}
flush_printf_literal(&mut parts, &mut literal);
spec.conversion = match conv {
's' => PrintfConversion::String,
'd' | 'i' => PrintfConversion::Decimal,
'u' => PrintfConversion::Unsigned,
'o' => PrintfConversion::Octal,
'x' => PrintfConversion::HexLower,
'X' => PrintfConversion::HexUpper,
'c' => PrintfConversion::Char,
'b' => PrintfConversion::Escaped,
'e' => PrintfConversion::FloatExpLower,
'E' => PrintfConversion::FloatExpUpper,
'f' => PrintfConversion::FloatFixedLower,
'F' => PrintfConversion::FloatFixedUpper,
'g' => PrintfConversion::FloatGeneralLower,
'G' => PrintfConversion::FloatGeneralUpper,
_ => unreachable!(),
};
parts.push(PrintfPart::Conversion(spec));
string_slots += 1;
}
},
_ => {
let mut buf = [0; 4];
literal.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
}
}
}
flush_printf_literal(&mut parts, &mut literal);
Some(PrintfPlan {
parts,
slots: string_slots,
stop_after_render,
})
}
fn parse_printf_quoted_char(arg: &str) -> Option<i128> {
if !arg.starts_with(['\'', '"']) {
return None;
}
Some(arg.as_bytes().get(1).copied().unwrap_or(0) as i8 as i128)
}
fn parse_printf_integer_value(arg: &str) -> Option<i128> {
if let Some(value) = parse_printf_quoted_char(arg) {
return Some(value);
}
if arg.is_empty() {
return Some(0);
}
let arg = arg.trim_start_matches(|ch: char| ch.is_ascii_whitespace());
if arg.is_empty() {
return None;
}
let (negative, digits) = match arg.as_bytes()[0] {
b'+' => (false, &arg[1..]),
b'-' => (true, &arg[1..]),
_ => (false, arg),
};
let (base, digits) = if let Some(hex) = digits
.strip_prefix("0x")
.or_else(|| digits.strip_prefix("0X"))
{
(16, hex)
} else if digits.len() > 1 && digits.starts_with('0') {
(8, digits)
} else {
(10, digits)
};
if digits.is_empty() {
return None;
}
let magnitude = u128::from_str_radix(digits, base).ok()?;
if negative {
let limit = i128::MAX as u128 + 1;
Some(if magnitude >= limit {
i128::MIN
} else {
-(magnitude as i128)
})
} else {
Some(magnitude.min(i128::MAX as u128) as i128)
}
}
fn parse_printf_numeric<T>(
arg: Option<&String>,
invalid_numbers: &mut Vec<String>,
default: T,
parse_value: impl FnOnce(&str) -> Option<T>,
) -> T {
let Some(arg) = arg else {
return default;
};
match parse_value(arg) {
Some(value) => value,
None => {
invalid_numbers.push(arg.clone());
default
}
}
}
fn parse_printf_integer(arg: Option<&String>, invalid_numbers: &mut Vec<String>) -> i128 {
parse_printf_numeric(arg, invalid_numbers, 0, parse_printf_integer_value)
}
fn printf_signed_integer(value: i128) -> i64 {
value.clamp(i64::MIN as i128, i64::MAX as i128) as i64
}
fn printf_unsigned_integer(value: i128) -> u64 {
if value < 0 {
printf_signed_integer(value) as u64
} else {
value.min(u64::MAX as i128) as u64
}
}
fn parse_printf_float_value(arg: &str) -> Option<f64> {
if let Some(value) = parse_printf_quoted_char(arg) {
return Some(value as f64);
}
if arg.is_empty() {
return Some(0.0);
}
let value = arg.trim_start_matches(|ch: char| ch.is_ascii_whitespace());
if value.is_empty() {
return None;
}
let c_value = CString::new(value).ok()?;
let mut end = ptr::null_mut();
let parsed = unsafe { strtod(c_value.as_ptr(), &mut end) };
if end == c_value.as_ptr().cast_mut() || unsafe { *end } != 0 {
None
} else {
Some(parsed)
}
}
fn parse_printf_float(arg: Option<&String>, invalid_numbers: &mut Vec<String>) -> f64 {
parse_printf_numeric(arg, invalid_numbers, 0.0, parse_printf_float_value)
}
fn append_printf_display_unit(
bytes: &mut Vec<u8>,
display_width: &mut usize,
unit: &[u8],
precision: Option<usize>,
) {
if precision.is_none_or(|limit| *display_width < limit) {
bytes.extend_from_slice(unit);
*display_width += 1;
}
}
fn render_escaped_printf_arg(
arg: Option<&String>,
precision: Option<usize>,
) -> (PrintfValue, bool) {
let mut chars = arg.map(String::as_str).unwrap_or("").chars().peekable();
let mut bytes = Vec::new();
let mut display_width = 0;
while let Some(ch) = chars.next() {
if ch == '\\' {
let mut escaped = Vec::new();
match push_printf_escape(&mut chars, &mut escaped) {
Some(PrintfEscape::Stop) => {
return (
PrintfValue {
bytes,
display_width,
},
true,
);
}
Some(PrintfEscape::Continue) => {
append_printf_display_unit(&mut bytes, &mut display_width, &escaped, precision);
}
None => {
append_printf_display_unit(&mut bytes, &mut display_width, b"\\", precision);
}
}
} else {
let mut buf = [0; 4];
append_printf_display_unit(
&mut bytes,
&mut display_width,
ch.encode_utf8(&mut buf).as_bytes(),
precision,
);
}
}
(
PrintfValue {
bytes,
display_width,
},
false,
)
}
fn truncate_chars(value: String, precision: Option<usize>) -> PrintfValue {
let value = match precision {
Some(limit) => value.chars().take(limit).collect(),
None => value,
};
PrintfValue::from_string(value)
}
fn uppercase_special_float(mut rendered: String) -> String {
if !rendered.contains(['.', 'e', 'E']) {
rendered.make_ascii_uppercase();
}
rendered
}
fn normalize_float_exponent(rendered: String, upper: bool) -> String {
let Some(idx) = rendered.find(['e', 'E']) else {
return rendered;
};
let mantissa = &rendered[..idx];
let exponent = rendered[idx + 1..].parse::<i32>().unwrap_or(0);
format!(
"{mantissa}{}{:+03}",
if upper { 'E' } else { 'e' },
exponent
)
}
fn ensure_float_decimal_point(rendered: String) -> String {
let Some(idx) = rendered.find(['e', 'E']) else {
if rendered.contains('.') {
return rendered;
}
return format!("{rendered}.");
};
let mantissa = &rendered[..idx];
let exponent = &rendered[idx..];
if mantissa.contains('.') {
rendered
} else {
format!("{mantissa}.{exponent}")
}
}
fn trim_float_fraction(rendered: String) -> String {
let (mut mantissa, suffix) = match rendered.find(['e', 'E']) {
Some(idx) => (rendered[..idx].to_string(), rendered[idx..].to_string()),
None => (rendered, String::new()),
};
if mantissa.contains('.') {
while mantissa.ends_with('0') {
let _ = mantissa.pop();
}
if mantissa.ends_with('.') {
let _ = mantissa.pop();
}
}
mantissa.push_str(&suffix);
mantissa
}
fn apply_float_sign(mut rendered: String, format: PrintfFormat) -> String {
if rendered.starts_with(['-', '+', ' ']) {
return rendered;
}
if format.force_sign {
rendered.insert(0, '+');
} else if format.space_sign {
rendered.insert(0, ' ');
}
rendered
}
fn render_printf_exponential(value: f64, format: PrintfFormat, upper: bool) -> String {
let precision = format.precision.unwrap_or(6);
let rendered = if upper {
format!("{value:.precision$E}")
} else {
format!("{value:.precision$e}")
};
let mut rendered = normalize_float_exponent(rendered, upper);
if format.alternate {
rendered = ensure_float_decimal_point(rendered);
}
let rendered = if upper {
uppercase_special_float(rendered)
} else {
rendered
};
apply_float_sign(rendered, format)
}
fn render_printf_fixed(value: f64, format: PrintfFormat, upper: bool) -> String {
let precision = format.precision.unwrap_or(6);
let mut rendered = format!("{value:.precision$}");
if format.alternate {
rendered = ensure_float_decimal_point(rendered);
}
let rendered = if upper {
uppercase_special_float(rendered)
} else {
rendered
};
apply_float_sign(rendered, format)
}
fn render_printf_general(value: f64, format: PrintfFormat, upper: bool) -> String {
if !value.is_finite() {
let rendered = if upper {
uppercase_special_float(value.to_string())
} else {
value.to_string()
};
return apply_float_sign(rendered, format);
}
let precision = format.precision.unwrap_or(6).max(1);
let scientific = if upper {
format!("{value:.precision$E}", precision = precision - 1)
} else {
format!("{value:.precision$e}", precision = precision - 1)
};
let scientific = normalize_float_exponent(scientific, upper);
let exponent = scientific
.find(['e', 'E'])
.and_then(|idx| scientific[idx + 1..].parse::<i32>().ok())
.unwrap_or(0);
let mut rendered = if exponent < -4 || exponent >= precision as i32 {
scientific
} else {
let digits_after_decimal = (precision as i32 - (exponent + 1)).max(0) as usize;
format!("{value:.digits_after_decimal$}")
};
if format.alternate {
rendered = ensure_float_decimal_point(rendered);
} else {
rendered = trim_float_fraction(rendered);
}
let rendered = if upper {
uppercase_special_float(rendered)
} else {
rendered
};
apply_float_sign(rendered, format)
}
fn signed_prefix(value: i64, format: PrintfFormat) -> (&'static str, u64) {
if value < 0 {
("-", value.unsigned_abs())
} else if format.force_sign {
("+", value as u64)
} else if format.space_sign {
(" ", value as u64)
} else {
("", value as u64)
}
}
fn integer_digits(value: u64, conversion: PrintfConversion, alternate: bool) -> String {
match conversion {
PrintfConversion::Octal => {
let digits = format!("{value:o}");
if alternate && !digits.starts_with('0') {
format!("0{digits}")
} else {
digits
}
}
PrintfConversion::HexLower => {
let digits = format!("{value:x}");
if alternate && value != 0 {
format!("0x{digits}")
} else {
digits
}
}
PrintfConversion::HexUpper => {
let digits = format!("{value:X}");
if alternate && value != 0 {
format!("0X{digits}")
} else {
digits
}
}
_ => value.to_string(),
}
}
fn apply_numeric_precision(mut digits: String, precision: Option<usize>) -> String {
if let Some(precision) = precision {
if precision == 0 && digits == "0" {
digits.clear();
} else if digits.len() < precision {
let mut padded = String::with_capacity(precision);
padded.extend(std::iter::repeat('0').take(precision - digits.len()));
padded.push_str(&digits);
digits = padded;
}
}
digits
}
fn apply_printf_width(value: PrintfValue, format: PrintfFormat, numeric: bool) -> Vec<u8> {
let Some(width) = format.width else {
return value.bytes;
};
if value.display_width >= width {
return value.bytes;
}
let pad = width - value.display_width;
let pad_char =
if numeric && format.zero_pad && !format.left_justify && format.precision.is_none() {
b'0'
} else {
b' '
};
if format.left_justify {
let mut rendered = value.bytes;
rendered.extend(std::iter::repeat(b' ').take(pad));
return rendered;
}
let mut rendered = Vec::with_capacity(value.bytes.len() + pad);
if pad_char == b'0'
&& let Some(first) = value.bytes.first().copied()
&& matches!(first, b'-' | b'+' | b' ')
{
rendered.push(first);
rendered.extend(std::iter::repeat(b'0').take(pad));
rendered.extend_from_slice(&value.bytes[1..]);
return rendered;
}
if pad_char == b'0' && (value.bytes.starts_with(b"0x") || value.bytes.starts_with(b"0X")) {
rendered.extend_from_slice(&value.bytes[..2]);
rendered.extend(std::iter::repeat(b'0').take(pad));
rendered.extend_from_slice(&value.bytes[2..]);
return rendered;
}
rendered.extend(std::iter::repeat(pad_char).take(pad));
rendered.extend_from_slice(&value.bytes);
rendered
}
fn render_printf_format(
format: PrintfFormat,
arg: Option<&String>,
invalid_numbers: &mut Vec<String>,
) -> (Vec<u8>, bool) {
let (rendered, stop) = match format.conversion {
PrintfConversion::String => (
truncate_chars(arg.cloned().unwrap_or_default(), format.precision),
false,
),
PrintfConversion::Char => (
truncate_chars(
arg.and_then(|arg| arg.chars().next())
.map(|ch| ch.to_string())
.unwrap_or_default(),
format.precision,
),
false,
),
PrintfConversion::Escaped => {
let (value, stop) = render_escaped_printf_arg(arg, format.precision);
(value, stop)
}
PrintfConversion::Decimal => {
let value = printf_signed_integer(parse_printf_integer(arg, invalid_numbers));
let (prefix, magnitude) = signed_prefix(value, format);
let digits = apply_numeric_precision(magnitude.to_string(), format.precision);
(PrintfValue::from_string(format!("{prefix}{digits}")), false)
}
PrintfConversion::Unsigned
| PrintfConversion::Octal
| PrintfConversion::HexLower
| PrintfConversion::HexUpper => {
let value = printf_unsigned_integer(parse_printf_integer(arg, invalid_numbers));
let digits = integer_digits(value, format.conversion, format.alternate);
let digits = apply_numeric_precision(digits, format.precision);
(PrintfValue::from_string(digits), false)
}
PrintfConversion::FloatFixedLower => (
PrintfValue::from_string(render_printf_fixed(
parse_printf_float(arg, invalid_numbers),
format,
false,
)),
false,
),
PrintfConversion::FloatFixedUpper => (
PrintfValue::from_string(render_printf_fixed(
parse_printf_float(arg, invalid_numbers),
format,
true,
)),
false,
),
PrintfConversion::FloatExpLower => (
PrintfValue::from_string(render_printf_exponential(
parse_printf_float(arg, invalid_numbers),
format,
false,
)),
false,
),
PrintfConversion::FloatExpUpper => (
PrintfValue::from_string(render_printf_exponential(
parse_printf_float(arg, invalid_numbers),
format,
true,
)),
false,
),
PrintfConversion::FloatGeneralLower => (
PrintfValue::from_string(render_printf_general(
parse_printf_float(arg, invalid_numbers),
format,
false,
)),
false,
),
PrintfConversion::FloatGeneralUpper => (
PrintfValue::from_string(render_printf_general(
parse_printf_float(arg, invalid_numbers),
format,
true,
)),
false,
),
};
let numeric = matches!(
format.conversion,
PrintfConversion::Decimal
| PrintfConversion::Unsigned
| PrintfConversion::Octal
| PrintfConversion::HexLower
| PrintfConversion::HexUpper
| PrintfConversion::FloatFixedLower
| PrintfConversion::FloatFixedUpper
| PrintfConversion::FloatExpLower
| PrintfConversion::FloatExpUpper
| PrintfConversion::FloatGeneralLower
| PrintfConversion::FloatGeneralUpper
);
(apply_printf_width(rendered, format, numeric), stop)
}
fn render_printf_plan(plan: &PrintfPlan, args: &[String]) -> (Vec<u8>, Vec<String>) {
let iterations = if plan.slots == 0 {
1
} else {
args.len().max(1).div_ceil(plan.slots)
};
let mut output = Vec::new();
let mut invalid_numbers = Vec::new();
let mut arg_idx = 0;
for _ in 0..iterations {
for part in &plan.parts {
match part {
PrintfPart::Literal(text) => output.extend_from_slice(text),
PrintfPart::Conversion(format) => {
let (rendered, stop) =
render_printf_format(*format, args.get(arg_idx), &mut invalid_numbers);
output.extend_from_slice(&rendered);
arg_idx += 1;
if stop {
return (output, invalid_numbers);
}
}
}
}
if plan.stop_after_render {
break;
}
}
(output, invalid_numbers)
}
fn builtin_printf(state: &mut ShellState, args: &[String]) -> i32 {
let Some((format, values)) = args.split_first() else {
return 0;
};
let Some(plan) = parse_printf_plan(format) else {
shell_errln(state, &format!("printf: unsupported format: {format}"));
return 1;
};
let (output, invalid_numbers) = render_printf_plan(&plan, values);
for arg in &invalid_numbers {
shell_errln(state, &format!("printf: {arg}: invalid number"));
}
if shell_out_bytes(state, &output).is_ok() && invalid_numbers.is_empty() {
0
} else {
1
}
}
fn is_readonly_variable_name(state: &ShellState, name: &str) -> bool {
state
.variable_store
.vars
.get(name)
.is_some_and(|var| (var.attrib & VAR_READONLY) != 0)
|| state
.variable_store
.unset_var_attribs
.get(name)
.is_some_and(|attrib| (attrib & VAR_READONLY) != 0)
}
fn normalize_logical_path(path: PathBuf) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
normalized.push(component.as_os_str());
}
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
normalized.pop();
}
std::path::Component::Normal(part) => normalized.push(part),
}
}
if normalized.as_os_str().is_empty() {
PathBuf::from("/")
} else {
normalized
}
}
fn builtin_cd(state: &mut ShellState, args: &[String]) -> i32 {
let mut logical = true;
let mut i = 0usize;
while let Some(arg) = args.get(i) {
match arg.as_str() {
"-L" => logical = true,
"-P" => logical = false,
"--" => {
i += 1;
break;
}
"-" => break,
arg if arg.starts_with('-') => {
shell_errln(state, &format!("cd: {arg}: invalid option"));
return 2;
}
_ => break,
}
i += 1;
}
if args.len().saturating_sub(i) > 1 {
shell_errln(state, "cd: too many arguments");
return 1;
}
let mut print_new_pwd = false;
let operand = if let Some(arg) = args.get(i) {
if arg == "-" {
let Some(oldpwd) = state.env_get("OLDPWD").map(String::from) else {
shell_errln(state, "cd: OLDPWD not set");
return 1;
};
print_new_pwd = true;
oldpwd
} else {
arg.clone()
}
} else if let Some(home) = state.env_get("HOME").map(String::from) {
home
} else {
shell_errln(state, "cd: HOME not set");
return 1;
};
let (target, cdpath_matched) = super::path::resolve_cd_target(state, &operand);
print_new_pwd |= cdpath_matched;
let oldpwd = state.path_state.cwd.display().to_string();
let new_cwd = if logical {
if !fs::metadata(&target).is_ok_and(|meta| meta.is_dir()) {
shell_errln(state, &format!("cd: {}: not a directory", target.display()));
return 1;
}
normalize_logical_path(target)
} else {
match fs::canonicalize(&target) {
Ok(cwd) => cwd,
Err(err) => {
shell_errln(state, &format!("cd: {err}"));
return 1;
}
}
};
for name in ["PWD", "OLDPWD"] {
if is_readonly_variable_name(state, name) {
shell_errln(state, &format!("cd: {name}: readonly variable"));
return 1;
}
}
state.path_state.cwd = new_cwd;
let pwd = state.path_state.cwd.display().to_string();
let _ = state.env_set("PWD", pwd.clone(), VAR_EXPORT);
let _ = state.env_set("OLDPWD", oldpwd, VAR_EXPORT);
if print_new_pwd && shell_outln(state, &pwd).is_err() {
return 1;
}
0
}
fn builtin_pwd(state: &mut ShellState) -> i32 {
let pwd = state.path_state.cwd.display().to_string();
if shell_outln(state, &pwd).is_ok() {
0
} else {
1
}
}
pub(super) fn is_valid_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
fn run_attribute_builtin(
state: &mut ShellState,
args: &[String],
attrib: u32,
print_prefix: &str,
) -> i32 {
let mut status = 0;
if args.is_empty() || (args.len() == 1 && args[0] == "-p") {
let mut pairs: Vec<_> = state
.variable_store
.vars
.iter()
.filter(|(_, v)| (v.attrib & attrib) != 0)
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut pending: Vec<_> = state
.variable_store
.unset_var_attribs
.iter()
.filter(|(_, v)| (**v & attrib) != 0)
.map(|(k, _)| k.clone())
.collect();
pending.sort();
for (k, v) in pairs {
if shell_outln(
state,
&format!("{print_prefix} {k}={}", shell_quote_assignment_value(&v)),
)
.is_err()
{
return 1;
}
}
for k in pending {
if shell_outln(state, &format!("{print_prefix} {k}")).is_err() {
return 1;
}
}
return 0;
}
for pair in args {
if pair.starts_with('-') {
shell_errln(state, &format!("{print_prefix}: unknown option: {pair}"));
return 1;
}
let Some((k, v)) = pair.split_once('=') else {
if !is_valid_identifier(pair) {
shell_errln(
state,
&format!("{print_prefix}: {pair}: invalid identifier"),
);
status = 1;
continue;
}
if let Some(existing) = state.variable_store.vars.get_mut(pair) {
existing.attrib |= attrib;
continue;
}
if is_readonly_variable_name(state, pair) && (attrib & VAR_READONLY) == 0 {
shell_errln(state, &format!("{print_prefix}: {pair}: readonly variable"));
status = 1;
continue;
}
*state
.variable_store
.unset_var_attribs
.entry(pair.clone())
.or_default() |= attrib;
continue;
};
if !is_valid_identifier(k) {
shell_errln(state, &format!("{print_prefix}: {k}: invalid identifier"));
status = 1;
continue;
}
let new_attrib = state
.variable_store
.vars
.get(k)
.map(|var| var.attrib | attrib)
.or_else(|| {
state
.variable_store
.unset_var_attribs
.get(k)
.copied()
.map(|pending| pending | attrib)
})
.unwrap_or(attrib);
if !state.env_set(k, v.to_string(), new_attrib) {
shell_errln(state, &format!("{print_prefix}: {k}: readonly variable"));
status = 1;
}
}
status
}
fn builtin_export(state: &mut ShellState, args: &[String]) -> i32 {
run_attribute_builtin(state, args, VAR_EXPORT, "export")
}
fn builtin_readonly(state: &mut ShellState, args: &[String]) -> i32 {
run_attribute_builtin(state, args, VAR_READONLY, "readonly")
}
fn builtin_unset(state: &mut ShellState, args: &[String]) -> i32 {
let mut unset_funcs = false;
let mut status = 0;
let mut names = args.iter().peekable();
while let Some(arg) = names.peek() {
if arg.as_str() == "-f" {
unset_funcs = true;
names.next();
} else if arg.as_str() == "-v" {
unset_funcs = false;
names.next();
} else {
break;
}
}
for key in names {
if !is_valid_identifier(key) {
shell_errln(state, &format!("unset: {key}: invalid identifier"));
status = 1;
continue;
}
if unset_funcs {
state.definition_store.functions.remove(key);
} else if !state.env_unset(key) {
shell_errln(state, &format!("unset: {key}: readonly variable"));
status = 1;
}
}
status
}
fn builtin_shift(state: &mut ShellState, args: &[String]) -> i32 {
if args.len() > 1 {
shell_errln(state, "shift: too many arguments");
return 1;
}
let n: usize = match args.first() {
Some(arg) => match arg.parse::<usize>() {
Ok(n) => n,
Err(_) => {
shell_errln(state, &format!("shift: {arg}: invalid count"));
return 1;
}
},
None => 1,
};
if n == 0 {
return 0;
}
if n >= state.variable_store.frame.len() {
shell_errln(state, "shift: can't shift that many");
if !state.interactive {
state.set_exit_code(2);
}
return 2;
}
if state.variable_store.frame.len() > 1 {
let prog = state.variable_store.frame[0].clone();
state
.variable_store
.frame
.drain(1..=n.min(state.variable_store.frame.len() - 1));
if state.variable_store.frame.is_empty() {
state.variable_store.frame.push(prog);
}
}
0
}
fn builtin_eval<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
let text = args.join(" ");
let outcome = super::run::run_string_with_outcome(state, runtime, &text);
if outcome.parse_error && !state.interactive {
state.set_exit_code(outcome.status);
}
outcome.status
}
fn builtin_dot<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if !state.definition.security_policy.allow_source_builtin() {
return deny_builtin_by_security_policy(state, ".");
}
let Some(path) = args.first() else {
shell_errln(state, ".: filename argument required");
if !state.interactive {
state.set_exit_code(2);
}
return 2;
};
let path_var = shell_resolve::command_search_path(state);
let resolved = super::path::resolve_dot_path(state, &path_var, path);
match super::driver::read_source_file(&resolved) {
Ok(Some(file)) => {
let source_name = resolved.to_string_lossy();
let old_frame = state.variable_store.frame.clone();
if args.len() > 1 {
let mut frame = vec![
old_frame
.first()
.cloned()
.unwrap_or_else(|| state.shell_name().to_string()),
];
frame.extend(args[1..].iter().cloned());
state.variable_store.frame = frame;
}
state.dot_script_depth += 1;
let previous_context =
state.enter_local_execution_context(ExecutionContextKind::DotScript);
let outcome = super::run::run_source_reader_with_outcome(
state,
runtime,
file,
Some(source_name.as_ref()),
);
if outcome.parse_error && !state.interactive {
state.set_exit_code(outcome.status);
}
let status = outcome.status;
state.restore_execution_context(previous_context);
state.dot_script_depth -= 1;
if matches!(state.control_flow, ControlFlow::Return(_)) {
state.control_flow = ControlFlow::None;
}
if args.len() > 1 {
state.variable_store.frame = old_frame;
}
status
}
Ok(None) => {
shell_errln(state, &format!(".: {}: not found", resolved.display()));
if !state.interactive {
state.set_exit_code(1);
}
1
}
Err(err) => {
shell_errln(state, &format!(".: {}: {err}", resolved.display()));
if !state.interactive {
state.set_exit_code(1);
}
1
}
}
}
fn builtin_exec<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if args.is_empty() {
state.control_flow = ControlFlow::None;
return 0;
}
if !state
.definition
.security_policy
.allow_process_control_builtins()
{
return deny_builtin_by_security_policy(state, "exec");
}
let path_var = shell_resolve::command_search_path(state);
let resolved_program =
match super::path::resolve_command_path(state, runtime, &args[0], &path_var) {
Ok(path) => path.display().to_string(),
Err(err) => {
shell_errln(state, &format!("exec: {}: {err}", args[0]));
let status = sys::spawn_error_exit_status(&err);
if !state.interactive {
state.set_exit_code(status);
}
return status;
}
};
let launch = ChildLaunchPlan::new(
state,
resolved_program,
args.to_vec(),
ProcessGroupPlan::Inherit,
);
if state.process_isolated {
let status = match launch.spawn(runtime, sys::SpawnMode::Foreground) {
Ok(child) => runtime.wait_child(child.handle),
Err(err) => sys::spawn_error_exit_status(&err),
};
state.set_exit_code(status);
return status;
}
match launch.exec_replace(runtime) {
Ok(()) => 0,
Err(err) => {
shell_errln(state, &format!("exec: {}: {err}", args[0]));
let status = sys::spawn_error_exit_status(&err);
if !state.interactive {
state.set_exit_code(status);
}
status
}
}
}
fn builtin_read(state: &mut ShellState, args: &[String]) -> i32 {
let mut raw_mode = false;
let mut var_names: Vec<&str> = Vec::new();
for arg in args {
if arg == "-r" {
raw_mode = true;
} else if !is_valid_identifier(arg) {
shell_errln(state, &format!("read: {arg}: invalid identifier"));
return 1;
} else {
var_names.push(arg);
}
}
if var_names.is_empty() {
var_names.push("REPLY");
}
let input = match shell_read::read_stdin_input(state.stdin_fd, raw_mode) {
Ok(Some(input)) => input,
Ok(None) => shell_read::ReadInput {
line: String::new(),
terminated_by_newline: false,
eof_after_continuation: false,
eof_after_newline_continuation: false,
},
Err(_) => return 1,
};
let ifs = state.env_get("IFS").unwrap_or(" \t\n").to_string();
let read_result = shell_read::read_model(
input,
shell_read::ReadConfig {
ifs: &ifs,
raw_mode,
var_count: var_names.len(),
},
);
let mut status = 0;
for (i, name) in var_names.iter().enumerate() {
let value = read_result.fields.get(i).cloned().unwrap_or_default();
let attrib = if state.has_option(OPT_ALLEXPORT) {
VAR_EXPORT
} else {
0
};
if !state.env_set(name, value, attrib) {
status = readonly_assignment_status(state, name, "read", false);
}
}
if status == 0 {
read_result.status
} else {
status.max(read_result.status)
}
}
fn builtin_alias(state: &mut ShellState, args: &[String]) -> i32 {
if args.is_empty() {
let mut aliases: Vec<_> = state
.definition_store
.aliases
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
aliases.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in aliases {
if shell_outln(
state,
&format!("alias {k}={}", shell_quote_assignment_value(&v)),
)
.is_err()
{
return 1;
}
}
return 0;
}
for arg in args {
if let Some((name, value)) = arg.split_once('=') {
Arc::make_mut(&mut state.definition_store.aliases)
.insert(name.to_string(), value.to_string());
} else if let Some(value) = state.definition_store.aliases.get(arg) {
if shell_outln(
state,
&format!("alias {arg}={}", shell_quote_assignment_value(value)),
)
.is_err()
{
return 1;
}
} else {
shell_errln(state, &format!("alias: {arg}: not found"));
return 1;
}
}
0
}
fn builtin_unalias(state: &mut ShellState, args: &[String]) -> i32 {
for arg in args {
if arg == "-a" {
Arc::make_mut(&mut state.definition_store.aliases).clear();
return 0;
}
if Arc::make_mut(&mut state.definition_store.aliases)
.remove(arg)
.is_none()
{
shell_errln(state, &format!("unalias: {arg}: not found"));
return 1;
}
}
0
}
fn builtin_command<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if args.is_empty() {
return 0;
}
let mut i = 0;
let mut verbose = false;
let mut verify = false;
let mut default_path = false;
while i < args.len() {
match args[i].as_str() {
"-v" => verbose = true,
"-V" => {
shell_errln(
state,
"command: `-V` has an unspecified output format, use `-v` instead",
);
verify = true;
}
"-p" => default_path = true,
"--" => {
i += 1;
break;
}
arg if arg.starts_with('-') => {
shell_errln(state, &format!("command: unknown option: {arg}"));
shell_errln(
state,
"command: usage: command [-v|-V|-p] command_name [args...]",
);
return 1;
}
_ => break,
}
i += 1;
}
if i >= args.len() {
return 0;
}
let cmd_name = &args[i];
let shell_override_probe = vec![cmd_name.clone()];
if verbose || verify {
if let Some(alias) = state.definition_store.aliases.get(cmd_name) {
if shell_outln(
state,
&format!("alias {cmd_name}={}", shell_quote_assignment_value(alias)),
)
.is_err()
{
return 1;
}
} else if state.definition_store.functions.contains_key(cmd_name)
|| shell_resolve::has_shell_override(state, &shell_override_probe)
|| is_builtin_in(state, cmd_name)
|| SHELL_KEYWORDS.contains(&cmd_name.as_str())
{
if shell_outln(state, cmd_name).is_err() {
return 1;
}
} else {
let path_var = if default_path {
shell_resolve::default_command_search_path(state)
} else {
shell_resolve::command_search_path(state)
};
match super::path::resolve_command_path(state, runtime, cmd_name, &path_var) {
Ok(path) => {
if shell_outln(state, &path.display().to_string()).is_err() {
return 1;
}
}
Err(_) => return 1,
}
}
return 0;
}
let argv: Vec<String> = args[i..].to_vec();
if let Some(code) = shell_resolve::run_shell_override(state, runtime, &argv) {
return code;
}
if state
.definition
.command_policy
.is_unspecified_utility(cmd_name)
{
shell_errln(
state,
&format!("{cmd_name}: The behavior of this command is undefined."),
);
return 1;
}
if let Some(code) = run_builtin(state, runtime, &argv) {
code
} else {
let path_var = if default_path {
shell_resolve::default_command_search_path(state)
} else {
shell_resolve::command_search_path(state)
};
super::spawn_external_in_path(state, runtime, &argv, None, &path_var)
}
}
fn shell_quote_assignment_value(value: &str) -> String {
let mut quoted = String::from("'");
for ch in value.chars() {
if ch == '\'' {
quoted.push_str("'\"'\"'");
} else {
quoted.push(ch);
}
}
quoted.push('\'');
quoted
}
fn builtin_type<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
let mut ret = 0;
for name in args {
let shell_override_probe = vec![name.clone()];
if shell_resolve::has_shell_override(state, &shell_override_probe) {
if shell_outln(state, &format!("{name} is a shell override")).is_err() {
ret = 1;
}
} else if is_builtin_in(state, name) {
if shell_outln(state, &format!("{name} is a shell builtin")).is_err() {
ret = 1;
}
} else if state.definition_store.functions.contains_key(name) {
if shell_outln(state, &format!("{name} is a function")).is_err() {
ret = 1;
}
} else if state.definition_store.aliases.contains_key(name) {
if shell_outln(state, &format!("{name} is an alias")).is_err() {
ret = 1;
}
} else {
let path_var = shell_resolve::command_search_path(state);
match super::path::resolve_command_path(state, runtime, name, &path_var) {
Ok(path) => {
if shell_outln(state, &format!("{name} is {}", path.display())).is_err() {
ret = 1;
}
}
Err(_) => {
shell_errln(state, &format!("{name}: not found"));
ret = 1;
}
}
}
}
ret
}
fn builtin_set<R: Runtime>(state: &mut ShellState, _runtime: &mut R, args: &[String]) -> i32 {
let usage = "set: usage: set [(-|+)abCefmnuvx] [-o option] [args...]";
if args.is_empty() {
let mut vars: Vec<_> = state
.variable_store
.vars
.iter()
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect();
vars.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in vars {
if shell_outln(state, &format!("{k}={}", shell_quote_assignment_value(&v))).is_err() {
return 1;
}
}
return 0;
}
if args.len() == 1 && (args[0] == "-o" || args[0] == "+o") {
return if super::print_long_options(state).is_ok() {
0
} else {
1
};
}
let mut i = 0usize;
let mut force_positional = false;
while i < args.len() {
let arg = &args[i];
if arg == "--" {
force_positional = true;
i += 1;
break;
}
if !arg.starts_with('-') && !arg.starts_with('+') {
break;
}
if arg.len() == 1 {
i += 1;
break;
}
if matches!(arg.as_str(), "-o" | "+o") && i + 1 >= args.len() {
return if super::print_long_options(state).is_ok() {
0
} else {
1
};
}
match parse_option_args_with_schema(
&state.definition.option_schema,
state.options,
args,
i,
OptionParseMode::SetBuiltin,
) {
Ok(parsed) => {
state.options = parsed.options;
force_positional = parsed.force_positional;
if (state.options & OPT_VI) != 0 {
super::maybe_warn_vi_unsupported(state);
}
i = parsed.next_index;
super::sync_monitor_mode(state);
let next_is_option = args
.get(i)
.and_then(|arg| arg.as_bytes().first())
.is_some_and(|byte| *byte == b'-' || *byte == b'+');
if force_positional || i == args.len() || !next_is_option {
break;
}
}
Err(_) => {
shell_errln(state, usage);
return 1;
}
}
}
if i < args.len() || force_positional {
let argv0 = state
.variable_store
.frame
.first()
.cloned()
.unwrap_or_else(|| state.shell_name().to_string());
let mut frame = vec![argv0];
frame.extend(args[i..].iter().cloned());
state.variable_store.frame = frame;
}
0
}
fn builtin_times(state: &mut ShellState) -> i32 {
let mut user = libc::tms {
tms_utime: 0,
tms_stime: 0,
tms_cutime: 0,
tms_cstime: 0,
};
unsafe { libc::times(&mut user) };
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
let u_min = user.tms_utime as f64 / ticks / 60.0;
let u_sec = (user.tms_utime as f64 / ticks) % 60.0;
let s_min = user.tms_stime as f64 / ticks / 60.0;
let s_sec = (user.tms_stime as f64 / ticks) % 60.0;
let ok = shell_outln(
state,
&format!("{u_min:.0}m{u_sec:.3}s {s_min:.0}m{s_sec:.3}s"),
)
.is_ok();
let cu_min = user.tms_cutime as f64 / ticks / 60.0;
let cu_sec = (user.tms_cutime as f64 / ticks) % 60.0;
let cs_min = user.tms_cstime as f64 / ticks / 60.0;
let cs_sec = (user.tms_cstime as f64 / ticks) % 60.0;
let ok = ok
&& shell_outln(
state,
&format!("{cu_min:.0}m{cu_sec:.3}s {cs_min:.0}m{cs_sec:.3}s"),
)
.is_ok();
if ok { 0 } else { 1 }
}
fn parse_symbolic_umask(current: u32, spec: &str) -> Option<u32> {
let mut mask = current & 0o777;
for clause in spec.split(',') {
if clause.is_empty() {
return None;
}
let mut chars = clause.chars().peekable();
let mut who = 0u32;
while let Some(ch) = chars.peek().copied() {
match ch {
'u' => who |= 0o700,
'g' => who |= 0o070,
'o' => who |= 0o007,
'a' => who |= 0o777,
_ => break,
}
let _ = chars.next();
}
if who == 0 {
who = 0o777;
}
let op = chars.next()?;
if !matches!(op, '=' | '+' | '-') {
return None;
}
let mut perms = 0u32;
for ch in chars {
match ch {
'r' => perms |= 0o444,
'w' => perms |= 0o222,
'x' => perms |= 0o111,
_ => return None,
}
}
let bits = perms & who;
match op {
'=' => mask = (mask & !who) | (who & !bits),
'+' => mask &= !bits,
'-' => mask |= bits,
_ => unreachable!(),
}
}
Some(mask & 0o777)
}
fn builtin_umask(state: &mut ShellState, args: &[String]) -> i32 {
if !state
.definition
.security_policy
.allow_process_control_builtins()
{
return deny_builtin_by_security_policy(state, "umask");
}
if args.is_empty() {
let mask = sys::get_umask();
return if shell_outln(state, &format!("{mask:04o}")).is_ok() {
0
} else {
1
};
}
if let Ok(mode) = u32::from_str_radix(&args[0], 8) {
sys::set_umask(mode);
0
} else if let Some(mode) = parse_symbolic_umask(sys::get_umask(), &args[0]) {
sys::set_umask(mode);
0
} else {
shell_errln(state, &format!("umask: {}: invalid mode", args[0]));
1
}
}
fn builtin_getopts(state: &mut ShellState, args: &[String]) -> i32 {
if args.len() < 2 {
shell_errln(state, "getopts: usage: getopts optstring name [args]");
return 2;
}
let optstring = &args[0];
let silent_errors = optstring.starts_with(':');
let optstring = optstring.strip_prefix(':').unwrap_or(optstring);
let name = &args[1];
if !is_valid_identifier(name) {
shell_errln(state, &format!("getopts: {name}: invalid identifier"));
return 2;
}
let params: Vec<String> = if args.len() > 2 {
args[2..].to_vec()
} else {
state.variable_store.frame.iter().skip(1).cloned().collect()
};
let raw_optind = state
.env_get("OPTIND")
.and_then(|v| v.parse::<usize>().ok());
let optind = raw_optind.filter(|optind| *optind > 0).unwrap_or(1);
if raw_optind != Some(optind) {
state.env_set("OPTIND", optind.to_string(), 0);
}
let cursor = if state.variable_store.getopts_index == Some(optind) {
state.variable_store.getopts_cursor.unwrap_or(1)
} else {
1
};
if optind > params.len() {
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
state.env_set("OPTIND", (params.len() + 1).to_string(), 0);
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
return 1;
}
let arg = ¶ms[optind - 1];
if arg == "--" {
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
return 1;
}
if !arg.starts_with('-') || arg == "-" {
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
return 1;
}
let option_chars: Vec<char> = arg.chars().collect();
if cursor >= option_chars.len() {
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
return 1;
}
let ch = option_chars[cursor];
let next_cursor = cursor + 1;
let has_inline_arg = next_cursor < option_chars.len();
if let Some(pos) = optstring.find(ch) {
if optstring.as_bytes().get(pos + 1) == Some(&b':') {
if has_inline_arg {
let inline_arg: String = option_chars[next_cursor..].iter().collect();
state.env_set("OPTARG", inline_arg, 0);
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
} else if optind < params.len() {
state.env_set("OPTARG", params[optind].clone(), 0);
state.env_set("OPTIND", (optind + 2).to_string(), 0);
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
} else {
state.env_set("OPTIND", (optind + 1).to_string(), 0);
if silent_errors {
state.env_set(name, ":".to_string(), 0);
state.env_set("OPTARG", ch.to_string(), 0);
} else {
shell_errln(state, &format!("getopts: option requires argument -- {ch}"));
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
}
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
return 0;
}
} else {
state.env_unset("OPTARG");
if has_inline_arg {
state.variable_store.getopts_cursor = Some(next_cursor);
state.variable_store.getopts_index = Some(optind);
} else {
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
}
}
state.env_set(name, ch.to_string(), 0);
0
} else {
if silent_errors {
state.env_set(name, "?".to_string(), 0);
state.env_set("OPTARG", ch.to_string(), 0);
} else {
shell_errln(state, &format!("getopts: illegal option -- {ch}"));
state.env_set(name, "?".to_string(), 0);
state.env_unset("OPTARG");
}
if has_inline_arg {
state.variable_store.getopts_cursor = Some(next_cursor);
state.variable_store.getopts_index = Some(optind);
} else {
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
}
0
}
}
fn builtin_builtin<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if args.is_empty() {
let names = builtin_names_for_state(state);
for name in names {
if shell_outln(state, &name).is_err() {
return 1;
}
}
return 0;
}
if args[0] == "builtin" {
shell_errln(state, "builtin: recursive invocation");
return 1;
}
let argv = args.to_vec();
match run_builtin(state, runtime, &argv) {
Some(code) => code,
None => {
shell_errln(state, &format!("builtin: {}: not a shell builtin", args[0]));
1
}
}
}
fn builtin_ulimit(state: &mut ShellState, args: &[String]) -> i32 {
if !state
.definition
.security_policy
.allow_process_control_builtins()
{
return deny_builtin_by_security_policy(state, "ulimit");
}
let mut show_all = false;
let mut target = libc::RLIMIT_FSIZE as libc::c_int;
let mut use_hard = false;
let mut use_soft = false;
let mut i = 0usize;
while i < args.len() && args[i].starts_with('-') && args[i] != "-" {
for ch in args[i][1..].chars() {
match ch {
'a' => show_all = true,
'H' => use_hard = true,
'S' => use_soft = true,
'f' => target = libc::RLIMIT_FSIZE as libc::c_int,
'n' => target = libc::RLIMIT_NOFILE as libc::c_int,
_ => {
shell_errln(state, &format!("ulimit: illegal option -- {ch}"));
return 1;
}
}
}
i += 1;
}
if !use_hard && !use_soft {
use_soft = true;
}
let print_limit = |state: &mut ShellState, resource: libc::c_int, name: &str| -> i32 {
let Ok(lim) = sys::get_resource_limit(resource) else {
shell_errln(state, &format!("ulimit: getrlimit failed for {name}"));
return 1;
};
let mut raw = if use_hard { lim.rlim_max } else { lim.rlim_cur };
if raw == libc::RLIM_INFINITY {
if shell_outln(state, "unlimited").is_err() {
return 1;
}
} else {
if resource == libc::RLIMIT_FSIZE as libc::c_int {
raw /= 512;
}
if shell_outln(state, &raw.to_string()).is_err() {
return 1;
}
}
0
};
if show_all {
let mut rc = 0;
for limit in sys::SHELL_RESOURCE_LIMITS {
if shell_out(state, &format!("{}: ", limit.name)).is_err() {
rc = 1;
continue;
}
rc |= print_limit(state, limit.resource, limit.name);
}
return rc;
}
if i >= args.len() {
return print_limit(state, target, "limit");
}
let value = &args[i];
let parsed = if value == "unlimited" {
libc::RLIM_INFINITY
} else {
match value.parse::<u64>() {
Ok(v) => {
let mut v = v as libc::rlim_t;
if target == libc::RLIMIT_FSIZE as libc::c_int {
v = v.saturating_mul(512);
}
v
}
Err(_) => {
shell_errln(state, &format!("ulimit: invalid limit value: {value}"));
return 1;
}
}
};
let Ok(mut lim) = sys::get_resource_limit(target) else {
shell_errln(state, "ulimit: getrlimit failed");
return 1;
};
if state.process_isolated && use_hard && parsed != lim.rlim_max {
shell_errln(
state,
"ulimit: hard limit changes are not supported in isolated shell contexts",
);
return 1;
}
if use_soft {
lim.rlim_cur = parsed;
}
if use_hard {
lim.rlim_max = parsed;
}
if sys::set_resource_limit(target, &lim).is_err() {
shell_errln(state, "ulimit: setrlimit failed");
return 1;
}
0
}
fn resolve_job_spec<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
spec: &str,
) -> Option<u32> {
let jobs = shell_jobs::job_list(state, runtime);
if jobs.is_empty() {
return None;
}
if spec == "%" || spec == "%%" || spec == "%+" {
return jobs.iter().max_by_key(|j| j.job_id).map(|j| j.job_id);
}
if spec == "%-" {
let mut ids: Vec<u32> = jobs.iter().map(|j| j.job_id).collect();
ids.sort_unstable();
return ids.iter().rev().nth(1).copied();
}
if let Some(rest) = spec.strip_prefix('%') {
let id: u32 = rest.parse().ok()?;
return jobs.iter().any(|job| job.job_id == id).then_some(id);
}
None
}
fn builtin_jobs<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if !state.definition.security_policy.allow_background_jobs() {
return deny_builtin_by_security_policy(state, "jobs");
}
let mut show_pids = false;
let mut show_l = false;
let mut specs: Vec<&str> = Vec::new();
for arg in args {
if arg == "-p" {
show_pids = true;
} else if arg == "-l" {
show_l = true;
} else if arg.starts_with('-') {
shell_errln(state, &format!("jobs: unknown option: {arg}"));
return 1;
} else {
specs.push(arg);
}
}
if show_pids && show_l {
shell_errln(state, "jobs: -l and -p are mutually exclusive");
return 1;
}
let mut jobs = shell_jobs::job_list(state, runtime);
jobs.sort_by_key(|j| j.job_id);
if !specs.is_empty() {
let mut filtered = Vec::new();
for spec in specs {
let Some(job_id) = resolve_job_spec(state, runtime, spec) else {
shell_errln(state, &format!("jobs: {spec}: no such job"));
return 1;
};
let Some(job) = jobs.iter().find(|j| j.job_id == job_id) else {
shell_errln(state, &format!("jobs: {spec}: no such job"));
return 1;
};
filtered.push(job.clone());
}
jobs = filtered;
}
for job in jobs {
let status = match job.state {
JobState::Running => "Running".to_string(),
JobState::Stopped(sig) => format!("Stopped({sig})"),
JobState::Done(code) => format!("Done({code})"),
};
let pid = job
.display_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "?".to_string());
if show_pids {
if shell_outln(state, &pid).is_err() {
return 1;
}
} else if show_l {
if shell_outln(state, &format!("[{}] {} {}", job.job_id, pid, status)).is_err() {
return 1;
}
} else {
if shell_outln(state, &format!("[{}] {} {}", job.job_id, status, pid)).is_err() {
return 1;
}
}
}
0
}
fn builtin_bg<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if !state.definition.security_policy.allow_background_jobs() {
return deny_builtin_by_security_policy(state, "bg");
}
if args.len() > 1 {
shell_errln(state, "bg: usage: bg [job]");
return 1;
}
let spec = args.first().map(String::as_str).unwrap_or("%%");
let Some(job_id) = resolve_job_spec(state, runtime, spec) else {
shell_errln(state, &format!("bg: {spec}: no such job"));
return 1;
};
if shell_jobs::continue_job_background(state, runtime, job_id) {
0
} else {
shell_errln(state, &format!("bg: {spec}: unable to resume job"));
1
}
}
fn builtin_fg<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if !state.definition.security_policy.allow_background_jobs() {
return deny_builtin_by_security_policy(state, "fg");
}
if args.len() > 1 {
shell_errln(state, "fg: usage: fg [job]");
return 1;
}
let spec = args.first().map(String::as_str).unwrap_or("%%");
let Some(job_id) = resolve_job_spec(state, runtime, spec) else {
shell_errln(state, &format!("fg: {spec}: no such job"));
return 1;
};
let Some(status) = shell_jobs::continue_job_foreground(state, runtime, job_id) else {
shell_errln(state, &format!("fg: {spec}: unable to resume job"));
return 1;
};
state.set_last_status(status);
status
}
fn builtin_wait<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
if !state.definition.security_policy.allow_background_jobs() {
return deny_builtin_by_security_policy(state, "wait");
}
if args.is_empty() {
let status = shell_jobs::wait_all_jobs(state, runtime);
state.set_last_status(status);
return status;
}
let mut status = 0;
for arg in args {
let waited = if arg.starts_with('%') {
resolve_job_spec(state, runtime, arg)
.and_then(|job_id| shell_jobs::wait_job(state, runtime, job_id))
} else {
match arg.parse::<u32>() {
Ok(pid) => match shell_jobs::wait_pid(state, runtime, pid) {
Ok(waited) => waited,
Err(err) => {
shell_errln(state, &format!("wait: {arg}: {err}"));
status = 1;
continue;
}
},
Err(_) => None,
}
};
match waited {
Some(code) => status = code,
None => {
shell_errln(state, &format!("wait: {arg}: unknown job or pid"));
status = 127;
}
}
}
state.set_last_status(status);
status
}
fn parse_status_arg(state: &ShellState, name: &str, arg: Option<&String>) -> Result<i32, i32> {
let Some(arg) = arg else {
return Ok(normalize_exit_status(state.last_status));
};
match arg.parse::<i64>() {
Ok(code) => Ok(normalize_exit_status(code)),
Err(_) => {
shell_errln(state, &format!("{name}: {arg}: numeric argument required"));
Err(2)
}
}
}
fn parse_branch_count(arg: Option<&String>, name: &str, state: &ShellState) -> Option<u32> {
let Some(arg) = arg else {
return Some(1);
};
let Ok(n) = arg.parse::<u32>() else {
shell_errln(state, &format!("{name}: {arg}: numeric argument required"));
return None;
};
if n == 0 {
shell_errln(state, &format!("{name}: {arg}: loop count out of range"));
return None;
}
Some(n)
}
fn abort_non_interactive_builtin_error(state: &mut ShellState, status: i32) {
if !state.interactive {
state.set_exit_code(status);
}
}
pub(super) fn run_builtin<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
argv: &[String],
) -> Option<i32> {
let (cmd, args) = argv.split_first()?;
match lookup_builtin(state, cmd)? {
RegisteredBuiltin::Custom { handler, .. } => {
let _ = runtime;
let mut context = BuiltinHost::new(state);
Some(handler(&mut context, args))
}
RegisteredBuiltin::Standard {
kind: Builtin::Colon | Builtin::True,
..
} => Some(0),
RegisteredBuiltin::Standard {
kind: Builtin::False,
..
} => Some(1),
RegisteredBuiltin::Standard {
kind: Builtin::Echo,
..
} => Some(builtin_echo(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Printf,
..
} => Some(builtin_printf(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Cd, ..
} => Some(builtin_cd(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Pwd, ..
} => Some(builtin_pwd(state)),
RegisteredBuiltin::Standard {
kind: Builtin::Export,
..
} => Some(builtin_export(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Readonly,
..
} => Some(builtin_readonly(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Unset,
..
} => Some(builtin_unset(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Shift,
..
} => Some(builtin_shift(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Eval,
..
} => Some(builtin_eval(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Dot, ..
} => Some(builtin_dot(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Exec,
..
} => Some(builtin_exec(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Read,
..
} => Some(builtin_read(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Alias,
..
} => Some(builtin_alias(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Unalias,
..
} => Some(builtin_unalias(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Command,
..
} => Some(builtin_command(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Type,
..
} => Some(builtin_type(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Set, ..
} => Some(builtin_set(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Times,
..
} => Some(builtin_times(state)),
RegisteredBuiltin::Standard {
kind: Builtin::Umask,
..
} => Some(builtin_umask(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Getopts,
..
} => Some(builtin_getopts(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Meta,
..
} => Some(builtin_builtin(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Jobs,
..
} => Some(builtin_jobs(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Bg, ..
} => Some(builtin_bg(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Fg, ..
} => Some(builtin_fg(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Ulimit,
..
} => Some(builtin_ulimit(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Wait,
..
} => Some(builtin_wait(state, runtime, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Trap,
..
} => Some(super::traps::builtin_trap(state, args)),
RegisteredBuiltin::Standard {
kind: Builtin::Break,
..
} => {
if args.len() > 1 {
shell_errln(state, "break: too many arguments");
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
let n = match parse_branch_count(args.first(), "break", state) {
Some(n) => n,
None => {
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
};
if state.loop_depth == 0 {
shell_errln(
state,
"break: only meaningful in a `for', `while', or `until' loop",
);
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
state.control_flow = ControlFlow::Break(n);
Some(0)
}
RegisteredBuiltin::Standard {
kind: Builtin::Continue,
..
} => {
if args.len() > 1 {
shell_errln(state, "continue: too many arguments");
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
let n = match parse_branch_count(args.first(), "continue", state) {
Some(n) => n,
None => {
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
};
if state.loop_depth == 0 {
shell_errln(
state,
"continue: only meaningful in a `for', `while', or `until' loop",
);
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
state.control_flow = ControlFlow::Continue(n);
Some(0)
}
RegisteredBuiltin::Standard {
kind: Builtin::Return,
..
} => {
if state.function_depth == 0 && state.dot_script_depth == 0 {
shell_errln(
state,
"return: can only `return' from a function or dot script",
);
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
if args.len() > 1 {
shell_errln(state, "return: too many arguments");
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
let code = match parse_status_arg(state, "return", args.first()) {
Ok(code) => code,
Err(code) => {
abort_non_interactive_builtin_error(state, code);
return Some(code);
}
};
state.set_last_status(code);
state.control_flow = ControlFlow::Return(code);
Some(code)
}
RegisteredBuiltin::Standard {
kind: Builtin::Exit,
..
} => {
if args.len() > 1 {
shell_errln(state, "exit: too many arguments");
abort_non_interactive_builtin_error(state, 1);
return Some(1);
}
let code = match parse_status_arg(state, "exit", args.first()) {
Ok(code) => code,
Err(code) => {
state.set_exit_code(code);
state.control_flow = ControlFlow::Exit(code);
return Some(code);
}
};
state.set_exit_code(code);
state.control_flow = ControlFlow::Exit(code);
Some(code)
}
}
}