use super::*;
use crate::builtin::{BuiltinHost, BuiltinProperties, BuiltinRegistry, RegisteredBuiltin};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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()
}
enum AttributeAssignMode {
RespectReadonly,
BypassReadonly,
}
fn deny_builtin_by_security_policy(state: &ShellState, builtin: &str) -> i32 {
shell_errln(
state,
&format!("{builtin}: disabled by shell security policy"),
);
126
}
const GETOPTS_CURSOR_VAR: &str = "__MXSH_GETOPTS_CURSOR";
const GETOPTS_INDEX_VAR: &str = "__MXSH_GETOPTS_INDEX";
fn builtin_echo(state: &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');
}
let _ = state.stdout_fd.write_str(&output);
0
}
enum PrintfPart {
Literal(String),
StringArg,
}
struct PrintfPlan {
parts: Vec<PrintfPart>,
string_slots: usize,
stop_after_render: bool,
}
fn flush_printf_literal(parts: &mut Vec<PrintfPart>, literal: &mut String) {
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 String,
) -> Option<bool> {
let escaped = match chars.next()? {
'a' => '\u{0007}',
'b' => '\u{0008}',
'c' => return Some(true),
'f' => '\u{000c}',
'n' => '\n',
'r' => '\r',
't' => '\t',
'v' => '\u{000b}',
'\\' => '\\',
'0' => {
let mut value = 0u8;
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 u8;
let _ = chars.next();
}
literal.push(if saw_digit { value as char } else { '\0' });
return Some(false);
}
_ => return None,
};
literal.push(escaped);
Some(false)
}
fn parse_printf_plan(format: &str) -> Option<PrintfPlan> {
let mut chars = format.chars().peekable();
let mut parts = Vec::new();
let mut literal = String::new();
let mut string_slots = 0;
let mut stop_after_render = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if push_printf_escape(&mut chars, &mut literal)? {
stop_after_render = true;
break;
}
}
'%' => match chars.next()? {
'%' => literal.push('%'),
's' => {
flush_printf_literal(&mut parts, &mut literal);
parts.push(PrintfPart::StringArg);
string_slots += 1;
}
_ => return None,
},
_ => literal.push(ch),
}
}
flush_printf_literal(&mut parts, &mut literal);
Some(PrintfPlan {
parts,
string_slots,
stop_after_render,
})
}
fn render_printf_plan(plan: &PrintfPlan, args: &[String]) -> String {
let iterations = if plan.string_slots == 0 {
1
} else {
args.len().max(1).div_ceil(plan.string_slots)
};
let mut output = String::new();
let mut arg_idx = 0;
for _ in 0..iterations {
for part in &plan.parts {
match part {
PrintfPart::Literal(text) => output.push_str(text),
PrintfPart::StringArg => {
if let Some(arg) = args.get(arg_idx) {
output.push_str(arg);
arg_idx += 1;
}
}
}
}
if plan.stop_after_render {
break;
}
}
output
}
fn fallback_printf_external<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
args: &[String],
) -> i32 {
let mut argv = Vec::with_capacity(args.len() + 1);
argv.push("printf".to_string());
argv.extend(args.iter().cloned());
match runtime.spawn_external_command(
&sys::ExternalCommand {
program: "printf".to_string(),
argv,
env: state.exported_env(),
cwd: state.cwd.clone(),
create_process_group: false,
passed_fds: state.inherited_fds(),
},
sys::SpawnStdio {
stdin_fd: state.stdin_fd,
stdout_fd: state.stdout_fd,
stderr_fd: state.stderr_fd,
},
&[],
sys::SpawnMode::Foreground,
) {
Ok(child) => runtime.wait_child(child.handle),
Err(err) => sys::spawn_error_exit_status(&err),
}
}
fn builtin_printf<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
let Some((format, values)) = args.split_first() else {
return 0;
};
let Some(plan) = parse_printf_plan(format) else {
return fallback_printf_external(state, runtime, args);
};
let output = render_printf_plan(&plan, values);
let _ = state.stdout_fd.write_str(&output);
0
}
fn builtin_cd(state: &mut ShellState, args: &[String]) -> i32 {
let target = if let Some(arg) = args.first() {
if arg == "-" {
if let Some(oldpwd) = state.env_get("OLDPWD").map(String::from) {
shell_outln(state, &oldpwd);
PathBuf::from(oldpwd)
} else {
shell_errln(state, "cd: OLDPWD not set");
return 1;
}
} else {
PathBuf::from(arg)
}
} else if let Some(home) = state.env_get("HOME").map(String::from) {
PathBuf::from(home)
} else {
shell_errln(state, "cd: HOME not set");
return 1;
};
let target = super::resolve_path(&state.cwd, &target);
let oldpwd = state.cwd.display().to_string();
match fs::canonicalize(&target) {
Ok(cwd) => {
state.cwd = cwd;
state.env_set_internal("PWD", state.cwd.display().to_string(), VAR_EXPORT);
state.env_set("OLDPWD", oldpwd, VAR_EXPORT);
}
Err(err) => {
shell_errln(state, &format!("cd: {err}"));
return 1;
}
}
0
}
fn builtin_pwd(state: &ShellState) -> i32 {
shell_outln(state, &state.cwd.display().to_string());
0
}
fn run_attribute_builtin(
state: &mut ShellState,
args: &[String],
attrib: u32,
print_prefix: &str,
assign_mode: AttributeAssignMode,
) -> i32 {
let mut status = 0;
if args.is_empty() || (args.len() == 1 && args[0] == "-p") {
let mut pairs: Vec<_> = state
.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));
for (k, v) in pairs {
shell_outln(state, &format!("{print_prefix} {k}=\"{v}\""));
}
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 let Some(existing) = state.vars.get_mut(pair) {
existing.attrib |= attrib;
continue;
}
if !state.env_set(pair, String::new(), attrib) {
shell_errln(state, &format!("{print_prefix}: {pair}: readonly variable"));
status = 1;
}
continue;
};
match assign_mode {
AttributeAssignMode::RespectReadonly => {
if !state.env_set(k, shell_expand::expand_tilde_assignment(state, v), attrib) {
shell_errln(state, &format!("{print_prefix}: {k}: readonly variable"));
status = 1;
}
}
AttributeAssignMode::BypassReadonly => {
state.env_set_internal(k, shell_expand::expand_tilde_assignment(state, v), attrib)
}
}
}
status
}
fn builtin_export(state: &mut ShellState, args: &[String]) -> i32 {
run_attribute_builtin(
state,
args,
VAR_EXPORT,
"export",
AttributeAssignMode::RespectReadonly,
)
}
fn builtin_readonly(state: &mut ShellState, args: &[String]) -> i32 {
run_attribute_builtin(
state,
args,
VAR_READONLY,
"readonly",
AttributeAssignMode::BypassReadonly,
)
}
fn builtin_unset(state: &mut ShellState, args: &[String]) -> i32 {
let mut unset_funcs = false;
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 unset_funcs {
state.functions.remove(key);
} else {
state.env_unset(key);
}
}
0
}
fn builtin_shift(state: &mut ShellState, args: &[String]) -> i32 {
let n: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
if n >= state.frame.len() {
shell_errln(state, "shift: can't shift that many");
if !state.interactive {
state.set_exit_code(2);
}
return 2;
}
if state.frame.len() > 1 {
let prog = state.frame[0].clone();
state.frame.drain(1..=n.min(state.frame.len() - 1));
if state.frame.is_empty() {
state.frame.push(prog);
}
}
0
}
fn builtin_eval<R: Runtime>(state: &mut ShellState, runtime: &mut R, args: &[String]) -> i32 {
let text = args.join(" ");
super::run_string(state, runtime, &text)
}
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");
return 2;
};
let resolved = shell_resolve::resolve_dot_path(state, path);
let resolved = super::resolve_path(&state.cwd, &resolved);
match super::driver::read_source_file(&resolved) {
Ok(Some(text)) => super::driver::source_text(state, runtime, &text, Some(&resolved)),
Ok(None) => {
shell_errln(state, &format!(".: {}: not found", resolved.display()));
1
}
Err(err) => {
shell_errln(state, &format!(".: {}: {err}", resolved.display()));
1
}
}
}
fn builtin_exec<R: Runtime>(state: &mut ShellState, runtime: &R, args: &[String]) -> i32 {
if args.is_empty() {
return 0;
}
if !state
.definition
.security_policy
.allow_process_control_builtins()
{
return deny_builtin_by_security_policy(state, "exec");
}
match runtime.exec_replace(&args[0], args, &state.exported_env(), &state.cwd) {
Ok(()) => 0,
Err(err) => {
shell_errln(state, &format!("exec: {}: {err}", args[0]));
match err.raw_os_error() {
Some(libc::ENOENT) => 127,
_ => 126,
}
}
}
}
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 {
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();
if !state.env_set(name, value, 0) {
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.aliases.iter().collect();
aliases.sort_by(|a, b| a.0.cmp(b.0));
for (k, v) in aliases {
shell_outln(state, &format!("{k}='{v}'"));
}
return 0;
}
for arg in args {
if let Some((name, value)) = arg.split_once('=') {
Arc::make_mut(&mut state.aliases).insert(name.to_string(), value.to_string());
} else if let Some(value) = state.aliases.get(arg) {
shell_outln(state, &format!("{arg}='{value}'"));
} 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.aliases).clear();
return 0;
}
if Arc::make_mut(&mut state.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() {
shell_errln(
state,
"command: usage: command [-v|-V|-p] command_name [args...]",
);
return 1;
}
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,
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() {
shell_errln(
state,
"command: usage: command [-v|-V|-p] command_name [args...]",
);
return 1;
}
let cmd_name = &args[i];
let shell_override_probe = vec![cmd_name.clone()];
if verbose || verify {
if let Some(alias) = state.aliases.get(cmd_name) {
shell_outln(state, &format!("alias {cmd_name}='{alias}'"));
} else if state.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())
{
shell_outln(state, cmd_name);
} else {
let path_var = if default_path {
"/usr/bin:/bin"
} else {
state.env_get("PATH").unwrap_or("/usr/bin:/bin")
};
match runtime.resolve_command_path(cmd_name, path_var) {
Ok(path) => shell_outln(state, &path.display().to_string()),
Err(_) => return 1,
}
}
return 0;
}
let argv: Vec<String> = args[i..].to_vec();
if let Some(code) = run_builtin(state, runtime, &argv) {
code
} else {
super::spawn_external(state, runtime, &argv, None)
}
}
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) {
shell_outln(state, &format!("{name} is a shell override"));
} else if is_builtin_in(state, name) {
shell_outln(state, &format!("{name} is a shell builtin"));
} else if state.functions.contains_key(name) {
shell_outln(state, &format!("{name} is a function"));
} else if state.aliases.contains_key(name) {
shell_outln(state, &format!("{name} is an alias"));
} else {
let path_var = state.env_get("PATH").unwrap_or("/usr/bin:/bin");
match runtime.resolve_command_path(name, path_var) {
Ok(path) => shell_outln(state, &format!("{name} is {}", path.display())),
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.vars.iter().collect();
vars.sort_by(|a, b| a.0.cmp(b.0));
for (k, v) in vars {
shell_outln(state, &format!("{k}={}", v.value));
}
return 0;
}
if args.len() == 1 && (args[0] == "-o" || args[0] == "+o") {
super::print_long_options(state);
return 0;
}
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 {
shell_errln(state, usage);
return 1;
}
if matches!(arg.as_str(), "-o" | "+o") && i + 1 >= args.len() {
super::print_long_options(state);
return 0;
}
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
.frame
.first()
.cloned()
.unwrap_or_else(|| state.shell_name().to_string());
let mut frame = vec![argv0];
frame.extend(args[i..].iter().cloned());
state.frame = frame;
}
0
}
fn builtin_times(state: &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;
shell_outln(
state,
&format!("{u_min:.0}m{u_sec:.3}s {s_min:.0}m{s_sec:.3}s"),
);
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;
shell_outln(
state,
&format!("{cu_min:.0}m{cu_sec:.3}s {cs_min:.0}m{cs_sec:.3}s"),
);
0
}
fn builtin_umask(state: &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();
shell_outln(state, &format!("{mask:04o}"));
return 0;
}
if let Ok(mode) = u32::from_str_radix(&args[0], 8) {
sys::set_umask(mode);
0
} else {
shell_errln(state, &format!("umask: {}: invalid octal number", 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];
let params: Vec<String> = if args.len() > 2 {
args[2..].to_vec()
} else {
state.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
.env_get(GETOPTS_INDEX_VAR)
.and_then(|v| v.parse::<usize>().ok())
== Some(optind)
{
state
.env_get(GETOPTS_CURSOR_VAR)
.and_then(|v| v.parse().ok())
.unwrap_or(1)
} else {
1
};
if optind > params.len() {
state.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
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.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
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.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
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.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
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.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
} else if optind < params.len() {
state.env_set("OPTARG", params[optind].clone(), 0);
state.env_set("OPTIND", (optind + 2).to_string(), 0);
state.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
} 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.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
return 0;
}
} else {
state.env_unset("OPTARG");
if has_inline_arg {
state.env_set(GETOPTS_CURSOR_VAR, next_cursor.to_string(), 0);
state.env_set(GETOPTS_INDEX_VAR, optind.to_string(), 0);
} else {
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
}
}
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.env_set(GETOPTS_CURSOR_VAR, next_cursor.to_string(), 0);
state.env_set(GETOPTS_INDEX_VAR, optind.to_string(), 0);
} else {
state.env_set("OPTIND", (optind + 1).to_string(), 0);
state.env_unset(GETOPTS_CURSOR_VAR);
state.env_unset(GETOPTS_INDEX_VAR);
}
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 {
shell_outln(state, &name);
}
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: &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;
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,
'n' => target = libc::RLIMIT_NOFILE,
_ => {
shell_errln(state, &format!("ulimit: illegal option -- {ch}"));
return 1;
}
}
}
i += 1;
}
if !use_hard && !use_soft {
use_soft = true;
}
let print_limit = |resource: libc::c_int, name: &str| -> i32 {
let mut lim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(resource as _, &mut lim) } != 0 {
shell_errln(state, &format!("ulimit: getrlimit failed for {name}"));
return 1;
}
let raw = if use_hard { lim.rlim_max } else { lim.rlim_cur };
if raw == libc::RLIM_INFINITY {
shell_outln(state, "unlimited");
} else {
shell_outln(state, &raw.to_string());
}
0
};
if show_all {
let mut rc = 0;
for (res, name) in [
(libc::RLIMIT_FSIZE, "file size"),
(libc::RLIMIT_NOFILE, "open files"),
] {
shell_out(state, &format!("{name}: "));
rc |= print_limit(res as libc::c_int, name);
}
return rc;
}
if i >= args.len() {
return print_limit(target as libc::c_int, "limit");
}
let value = &args[i];
let parsed = if value == "unlimited" {
libc::RLIM_INFINITY
} else {
match value.parse::<u64>() {
Ok(v) => v as libc::rlim_t,
Err(_) => {
shell_errln(state, &format!("ulimit: invalid limit value: {value}"));
return 1;
}
}
};
let mut lim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(target, &mut lim) } != 0 {
shell_errln(state, "ulimit: getrlimit failed");
return 1;
}
if use_soft {
lim.rlim_cur = parsed;
}
if use_hard {
lim.rlim_max = parsed;
}
if unsafe { libc::setrlimit(target, &lim) } != 0 {
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 {
shell_outln(state, &pid);
} else if show_l {
shell_outln(state, &format!("[{}] {} {}", job.job_id, pid, status));
} else {
shell_outln(state, &format!("[{}] {} {}", job.job_id, status, pid));
}
}
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(state.last_status);
};
match arg.parse::<i32>() {
Ok(code) => Ok(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)
}
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, runtime, 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,
..
} => {
let n = match parse_branch_count(args.first(), "break", state) {
Some(n) => n,
None => return Some(1),
};
if state.loop_depth == 0 {
shell_errln(
state,
"break: only meaningful in a `for', `while', or `until' loop",
);
return Some(1);
}
state.branch = BranchControl::Break(n);
Some(0)
}
RegisteredBuiltin::Standard {
kind: Builtin::Continue,
..
} => {
let n = match parse_branch_count(args.first(), "continue", state) {
Some(n) => n,
None => return Some(1),
};
if state.loop_depth == 0 {
shell_errln(
state,
"continue: only meaningful in a `for', `while', or `until' loop",
);
return Some(1);
}
state.branch = BranchControl::Continue(n);
Some(0)
}
RegisteredBuiltin::Standard {
kind: Builtin::Return,
..
} => {
if state.function_depth == 0 {
shell_errln(state, "return: can only `return' from a function");
return Some(1);
}
let code = match parse_status_arg(state, "return", args.first()) {
Ok(code) => code,
Err(code) => return Some(code),
};
state.set_last_status(code);
state.branch = BranchControl::Return;
Some(code)
}
RegisteredBuiltin::Standard {
kind: Builtin::Exit,
..
} => {
let code = match parse_status_arg(state, "exit", args.first()) {
Ok(code) => code,
Err(code) => {
state.set_exit_code(code);
return Some(code);
}
};
state.set_exit_code(code);
Some(code)
}
}
}