use crate::ast::*;
use crate::error::{ExitStatus, ShellError, Span};
use crate::eval::Shell;
use crate::parser::Parser;
use crate::shell_bytes::ShellBytes;
use crate::sys;
use crate::test_cmd::test_eval;
use std::os::unix::ffi::OsStrExt;
impl Shell {
pub(crate) fn try_builtin(
&mut self,
name: &str,
args: &[String],
_assigns: &[Assignment],
redirs: &[Redir],
span: Span,
) -> crate::error::Result<Option<ExitStatus>> {
let status = match name {
":" | "true" => Some(ExitStatus::SUCCESS),
"false" => Some(ExitStatus::FAILURE),
"echo" => Some(self.builtin_echo(args)),
"cd" => Some(self.builtin_cd(args)),
"pwd" => {
let cwd = ShellBytes::from_os_str(self.cwd.as_os_str()).to_shell_string();
self.write_out(&format!("{cwd}\n"));
Some(ExitStatus::SUCCESS)
}
"exit" => {
let code = args
.get(1)
.and_then(|s| s.parse::<i32>().ok())
.map(ExitStatus::from)
.unwrap_or(self.exit_status);
return Err(ShellError::Exit(code));
}
"return" => {
let code = args
.get(1)
.and_then(|s| s.parse::<i32>().ok())
.map(ExitStatus::from)
.unwrap_or(self.exit_status);
return Err(ShellError::Return(code));
}
"break" => {
if let Some(arg) = args.get(1) {
match arg.parse::<usize>() {
Ok(0) => {
self.err_msg(&format!("break: Illegal number: {arg}"));
return Err(ShellError::Exit(ExitStatus::FAILURE));
}
Ok(n) => {
if self.loop_depth == 0 {
Some(ExitStatus::SUCCESS)
} else {
return Err(ShellError::Break(n.min(self.loop_depth)));
}
}
Err(_) => {
self.err_msg(&format!("break: Illegal number: {arg}"));
return Err(ShellError::Exit(ExitStatus::FAILURE));
}
}
} else if self.loop_depth == 0 {
Some(ExitStatus::SUCCESS)
} else {
return Err(ShellError::Break(1));
}
}
"continue" => {
if let Some(arg) = args.get(1) {
match arg.parse::<usize>() {
Ok(0) => {
self.err_msg(&format!("continue: Illegal number: {arg}"));
return Err(ShellError::Exit(ExitStatus::FAILURE));
}
Ok(n) => {
if self.loop_depth == 0 {
Some(ExitStatus::SUCCESS)
} else {
return Err(ShellError::Continue(n.min(self.loop_depth)));
}
}
Err(_) => {
self.err_msg(&format!("continue: Illegal number: {arg}"));
return Err(ShellError::Exit(ExitStatus::FAILURE));
}
}
} else if self.loop_depth == 0 {
Some(ExitStatus::SUCCESS)
} else {
return Err(ShellError::Continue(1));
}
}
"export" => Some(self.builtin_export(args)),
"readonly" => Some(self.builtin_readonly(args)),
"unset" => Some(self.builtin_unset(args)),
"set" => Some(self.builtin_set(args)),
"shift" => Some(self.builtin_shift(args)),
"eval" => return self.builtin_eval(args).map(Some),
"." | "source" => Some(self.builtin_dot(args, span)?),
"test" | "[" => Some(self.builtin_test(args)),
"read" => Some(self.builtin_read(args)),
"local" => Some(self.builtin_local(args)),
"exec" => return self.builtin_exec(args, redirs, span).map(Some),
"command" => Some(self.builtin_command(args, span)?),
"type" => Some(self.builtin_type(args)),
"wait" => Some(self.builtin_wait(args)),
"trap" => Some(self.builtin_trap(args)),
"kill" => Some(self.builtin_kill(args)),
"umask" => Some(self.builtin_umask(args)),
"getopts" => Some(self.builtin_getopts(args)),
"printf" => Some(self.builtin_printf(args)),
_ => None,
};
Ok(status)
}
fn builtin_echo(&self, args: &[String]) -> ExitStatus {
let mut i = 1;
let mut newline = true;
let mut escape = false;
while i < args.len() {
if args[i] == "-n" {
newline = false;
i += 1;
} else if args[i] == "-e" {
escape = true;
i += 1;
} else if args[i] == "-E" {
escape = false;
i += 1;
} else {
break;
}
}
let mut text = args[i..].join(" ");
if escape {
text = unescape_echo(&text);
}
if newline {
text.push('\n');
}
self.write_out(&text);
ExitStatus::SUCCESS
}
fn builtin_cd(&mut self, args: &[String]) -> ExitStatus {
let dir = if args.len() > 1 {
args[1].clone()
} else {
match self.vars.get_shell("HOME") {
Some(h) => h,
None => {
self.err_msg("cd: HOME not set");
return ExitStatus::FAILURE;
}
}
};
let target = self.resolve_path_bytes(&ShellBytes::from_str_lossless(&dir));
match target.canonicalize() {
Ok(canonical) => {
if canonical.is_dir() {
self.cwd = canonical;
let _ = self
.vars
.set_bytes("PWD", ShellBytes::from_os_str(self.cwd.as_os_str()));
ExitStatus::SUCCESS
} else {
self.err_msg(&format!("cd: {dir}: Not a directory"));
ExitStatus::FAILURE
}
}
Err(e) => {
self.err_msg(&format!("cd: {dir}: {e}"));
ExitStatus::FAILURE
}
}
}
fn builtin_export(&mut self, args: &[String]) -> ExitStatus {
if args.len() <= 1 {
for (k, v) in self.vars.exported_env() {
self.write_out(&format!("export {k}=\"{v}\"\n"));
}
return ExitStatus::SUCCESS;
}
for arg in &args[1..] {
if let Some(eq) = arg.find('=') {
let name = &arg[..eq];
let value = &arg[eq + 1..];
if let Err(e) = self.vars.set(name, value) {
self.err_msg(&format!("export: {e}"));
return ExitStatus::FAILURE;
}
self.vars.export(name);
} else {
self.vars.export(arg);
}
}
ExitStatus::SUCCESS
}
fn builtin_readonly(&mut self, args: &[String]) -> ExitStatus {
for arg in &args[1..] {
if let Some(eq) = arg.find('=') {
let name = &arg[..eq];
let value = &arg[eq + 1..];
if let Err(e) = self.vars.set(name, value) {
self.err_msg(&format!("readonly: {e}"));
return ExitStatus::FAILURE;
}
self.vars.set_readonly(name);
} else {
self.vars.set_readonly(arg);
}
}
ExitStatus::SUCCESS
}
fn builtin_unset(&mut self, args: &[String]) -> ExitStatus {
let mut status = ExitStatus::SUCCESS;
let mut unset_funcs = false;
let mut i = 1;
while i < args.len() {
if args[i] == "-v" {
unset_funcs = false;
i += 1;
} else if args[i] == "-f" {
unset_funcs = true;
i += 1;
} else {
break;
}
}
for arg in &args[i..] {
if unset_funcs {
self.functions.remove(arg.as_str());
} else if let Err(e) = self.vars.unset(arg) {
self.err_msg(&format!("unset: {e}"));
status = ExitStatus::FAILURE;
}
}
status
}
fn builtin_set(&mut self, args: &[String]) -> ExitStatus {
if args.len() <= 1 {
return ExitStatus::SUCCESS;
}
let mut i = 1;
while i < args.len() {
let arg = &args[i];
if arg == "--" {
i += 1;
self.vars.positional = args[i..].iter().cloned().map(Into::into).collect();
return ExitStatus::SUCCESS;
} else if arg == "-" {
self.opts.xtrace = false;
i += 1;
} else if (arg == "-o" || arg == "+o") && i + 1 < args.len() {
let enable = arg == "-o";
i += 1;
match args[i].as_str() {
"pipefail" => self.opts.pipefail = enable,
"errexit" => self.opts.errexit = enable,
"nounset" => self.opts.nounset = enable,
"xtrace" => self.opts.xtrace = enable,
"noglob" => self.opts.noglob = enable,
"noexec" => self.opts.noexec = enable,
other => {
self.err_msg(&format!("set: unknown option: {other}"));
return ExitStatus::FAILURE;
}
}
i += 1;
} else if arg.starts_with('-') || arg.starts_with('+') {
let enable = arg.starts_with('-');
for ch in arg[1..].chars() {
match ch {
'e' => self.opts.errexit = enable,
'u' => self.opts.nounset = enable,
'x' => self.opts.xtrace = enable,
'f' => self.opts.noglob = enable,
'n' => self.opts.noexec = enable,
_ => {
self.err_msg(&format!("set: unknown option: -{ch}"));
return ExitStatus::FAILURE;
}
}
}
i += 1;
} else {
self.vars.positional = args[i..].iter().cloned().map(Into::into).collect();
return ExitStatus::SUCCESS;
}
}
ExitStatus::SUCCESS
}
fn builtin_shift(&mut self, args: &[String]) -> ExitStatus {
let n = args
.get(1)
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
if n > self.vars.positional.len() {
self.err_msg("shift: can't shift that many");
return ExitStatus::FAILURE;
}
self.vars.positional = self.vars.positional[n..].to_vec();
ExitStatus::SUCCESS
}
fn run_inline(&mut self, source: &str) -> crate::error::Result<ExitStatus> {
let mut parser = Parser::new(source);
let program = match parser.parse() {
Ok(p) => p,
Err(e) => {
self.err_msg(&format!("epsh: {e}"));
return Ok(ExitStatus::MISUSE);
}
};
if program.commands.is_empty() {
return Ok(ExitStatus::SUCCESS);
}
let mut status = ExitStatus::SUCCESS;
for cmd in &program.commands {
status = self.eval_command(cmd)?;
self.exit_status = status;
}
Ok(status)
}
fn builtin_eval(&mut self, args: &[String]) -> crate::error::Result<ExitStatus> {
if args.len() <= 1 {
return Ok(ExitStatus::SUCCESS);
}
self.run_inline(&args[1..].join(" "))
}
fn builtin_dot(&mut self, args: &[String], _span: Span) -> crate::error::Result<ExitStatus> {
if args.len() <= 1 {
self.err_msg(".: filename argument required");
return Err(ShellError::Exit(ExitStatus::MISUSE));
}
let filename = &args[1];
let filepath = self.resolve_path_bytes(&ShellBytes::from_str_lossless(filename));
let content = match std::fs::read(&filepath) {
Ok(bytes) => crate::encoding::bytes_to_str(&bytes),
Err(e) => {
self.err_msg(&format!(".: {filename}: {e}"));
return Err(ShellError::Exit(ExitStatus::NOT_FOUND));
}
};
match self.run_inline(&content) {
Err(ShellError::Return(n)) => Ok(n),
other => other,
}
}
fn builtin_test(&self, args: &[String]) -> ExitStatus {
let args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args = if args[0] == "[" {
if args.last() != Some(&"]") {
self.err_msg("[: missing ]");
return ExitStatus::MISUSE;
}
&args[1..args.len() - 1]
} else {
&args[1..]
};
test_eval(args)
}
fn builtin_read(&mut self, args: &[String]) -> ExitStatus {
let mut i = 1;
let mut raw_mode = false;
while i < args.len() && args[i].starts_with('-') {
if args[i] == "-r" {
raw_mode = true;
}
i += 1;
}
let var_names: Vec<&str> = if i < args.len() {
args[i..].iter().map(|s| s.as_str()).collect()
} else {
vec!["REPLY"]
};
let mut line = String::new();
let mut buf = [0u8; 1];
let mut hit_eof = true; let mut continued = false;
loop {
let n = unsafe { sys::read(0, buf.as_mut_ptr().cast(), 1) };
if n <= 0 {
break; }
let ch = buf[0] as char;
if ch == '\n' {
if continued {
continued = false;
continue;
}
hit_eof = false;
break;
}
if ch == '\\' && !raw_mode {
let n2 = unsafe { sys::read(0, buf.as_mut_ptr().cast(), 1) };
if n2 <= 0 {
line.push('\\');
break;
}
if buf[0] == b'\n' {
continued = true;
continue;
}
line.push(buf[0] as char);
continued = false;
continue;
}
continued = false;
line.push(ch);
}
if hit_eof && line.is_empty() {
for name in &var_names {
if let Err(e) = self.vars.set(name, "") {
self.err_msg(&format!("read: {e}"));
return ExitStatus::FAILURE;
}
}
return ExitStatus::FAILURE;
}
let ifs = self.vars.ifs().to_string();
if var_names.len() == 1 {
let trimmed = line.trim_matches(|c: char| ifs.contains(c));
if let Err(e) = self.vars.set(var_names[0], trimmed) {
self.err_msg(&format!("read: {e}"));
return ExitStatus::FAILURE;
}
} else {
let mut fields = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut pos = 0;
while pos < chars.len() && ifs.contains(chars[pos]) && chars[pos].is_whitespace() {
pos += 1;
}
for vi in 0..var_names.len() {
if vi == var_names.len() - 1 {
let rest: String = chars[pos..].iter().collect();
let trimmed =
rest.trim_end_matches(|c: char| ifs.contains(c) && c.is_whitespace());
fields.push(trimmed.to_string());
} else {
let start = pos;
while pos < chars.len() && !ifs.contains(chars[pos]) {
pos += 1;
}
fields.push(chars[start..pos].iter().collect());
while pos < chars.len() && ifs.contains(chars[pos]) {
pos += 1;
}
}
}
for (vi, name) in var_names.iter().enumerate() {
let value = fields.get(vi).map(|s| s.as_str()).unwrap_or("");
if let Err(e) = self.vars.set(name, value) {
self.err_msg(&format!("read: {e}"));
return ExitStatus::FAILURE;
}
}
}
if hit_eof {
ExitStatus::FAILURE
} else {
ExitStatus::SUCCESS
}
}
fn builtin_local(&mut self, args: &[String]) -> ExitStatus {
for arg in &args[1..] {
if let Some(eq) = arg.find('=') {
let name = &arg[..eq];
let value = &arg[eq + 1..];
self.vars.make_local(name);
let _ = self.vars.set(name, value);
} else {
self.vars.make_local(arg);
}
}
ExitStatus::SUCCESS
}
fn builtin_exec(
&mut self,
args: &[String],
_redirs: &[Redir],
_span: Span,
) -> crate::error::Result<ExitStatus> {
if args.len() <= 1 {
return Ok(ExitStatus::SUCCESS);
}
let args_bytes: Vec<ShellBytes> = args.iter().cloned().map(Into::into).collect();
unsafe {
for (k, _) in std::env::vars_os() {
std::env::remove_var(k);
}
for (k, v) in self.vars.env_for_command_os(&[]) {
std::env::set_var(k, v);
}
let _ = std::env::set_current_dir(&self.cwd);
}
let err = exec::execvp(&args_bytes[0], &args_bytes);
self.err_msg(&format!("exec: {}: {err}", args[0]));
Ok(ExitStatus::NOT_EXECUTABLE)
}
fn builtin_command(&mut self, args: &[String], span: Span) -> crate::error::Result<ExitStatus> {
if args.len() <= 1 {
return Ok(ExitStatus::SUCCESS);
}
let mut mode = None; let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-v" => {
mode = Some('v');
i += 1;
}
"-V" => {
mode = Some('V');
i += 1;
}
"-p" => {
i += 1;
} "--" => {
i += 1;
break;
}
_ => break,
}
}
if i >= args.len() {
return Ok(ExitStatus::SUCCESS);
}
if let Some(m) = mode {
let mut status = ExitStatus::SUCCESS;
for name in &args[i..] {
if m == 'V' {
if self.functions.contains_key(name.as_str()) {
self.write_out(&format!("{name} is a function\n"));
} else if is_builtin(name) {
self.write_out(&format!("{name} is a shell builtin\n"));
} else if let Ok(path) = self.which(name) {
self.write_out(&format!("{name} is {path}\n"));
} else {
self.err_msg(&format!("{name}: not found"));
status = ExitStatus::FAILURE;
}
} else {
if self.functions.contains_key(name.as_str()) || is_builtin(name) {
self.write_out(&format!("{name}\n"));
} else if let Ok(path) = self.which(name) {
self.write_out(&format!("{path}\n"));
} else {
status = ExitStatus::FAILURE;
}
}
}
return Ok(status);
}
let new_args: Vec<String> = args[i..].to_vec();
if let Some(status) = self.try_builtin(&new_args[0], &new_args, &[], &[], span)? {
Ok(status)
} else {
self.eval_external(&new_args, &[], &[], span)
}
}
fn builtin_type(&self, args: &[String]) -> ExitStatus {
let mut status = ExitStatus::SUCCESS;
for name in &args[1..] {
if self.functions.contains_key(name.as_str()) {
self.write_out(&format!("{name} is a function\n"));
} else if is_builtin(name) {
self.write_out(&format!("{name} is a shell builtin\n"));
} else if let Ok(path) = self.which(name) {
self.write_out(&format!("{name} is {path}\n"));
} else {
self.err_msg(&format!("{name}: not found"));
status = ExitStatus::FAILURE;
}
}
status
}
fn which(&self, name: &str) -> Result<String, ()> {
let name_bytes = ShellBytes::from_str_lossless(name);
let path = self
.vars
.get_bytes("PATH")
.cloned()
.unwrap_or_else(|| ShellBytes::from(""));
for dir in path.as_bytes().split(|b| *b == b':') {
let mut candidate = Vec::new();
if dir.is_empty() {
candidate.extend_from_slice(self.cwd.as_os_str().as_bytes());
} else {
candidate.extend_from_slice(dir);
}
if !candidate.ends_with(b"/") {
candidate.push(b'/');
}
candidate.extend_from_slice(name_bytes.as_bytes());
let candidate = ShellBytes::from_vec(candidate).to_path_buf();
if candidate.is_file() {
return Ok(ShellBytes::from_os_str(candidate.as_os_str()).to_shell_string());
}
}
Err(())
}
fn builtin_wait(&mut self, _args: &[String]) -> ExitStatus {
loop {
let mut status = 0i32;
let pid = unsafe { sys::waitpid(-1, &mut status, 0) };
if pid <= 0 {
break;
}
}
ExitStatus::SUCCESS
}
fn builtin_trap(&mut self, args: &[String]) -> ExitStatus {
if args.len() <= 1 {
for (sig, action) in &self.traps {
self.write_out(&format!("trap -- '{}' {}\n", action, sig));
}
return ExitStatus::SUCCESS;
}
let offset = if args[1] == "--" { 2 } else { 1 };
if args.len() <= offset {
return ExitStatus::SUCCESS;
}
if args.len() == offset + 1 {
let sig = &args[offset];
if sig == "-" {
for (sig, action) in &self.traps {
self.write_out(&format!("trap -- '{}' {}\n", action, sig));
}
} else {
let normalized = sig.to_uppercase();
self.traps.remove(&normalized);
if let Some(signum) = crate::signal::name_to_signal(&normalized) {
crate::signal::reset_handler(signum);
}
}
return ExitStatus::SUCCESS;
}
let action = &args[offset];
for sig_name in &args[offset + 1..] {
let normalized = sig_name.to_uppercase();
if action == "-" {
self.traps.remove(&normalized);
if let Some(signum) = crate::signal::name_to_signal(&normalized) {
crate::signal::reset_handler(signum);
}
} else if action.is_empty() {
self.traps.insert(normalized.clone(), action.clone());
if let Some(signum) = crate::signal::name_to_signal(&normalized) {
crate::signal::ignore_signal(signum);
}
} else {
self.traps.insert(normalized.clone(), action.clone());
if let Some(signum) = crate::signal::name_to_signal(&normalized) {
crate::signal::install_handler(signum);
}
}
}
ExitStatus::SUCCESS
}
fn builtin_kill(&self, args: &[String]) -> ExitStatus {
if args.len() <= 1 {
self.err_msg("kill: usage: kill [-s signal | -signal] pid ...");
return ExitStatus::MISUSE;
}
let mut i = 1;
let mut signum = libc::SIGTERM;
if args[i] == "-l" || args[i] == "-L" {
if args.len() > i + 1 {
if let Ok(status) = args[i + 1].parse::<i32>() {
let sig = if status > 128 { status - 128 } else { status };
if let Some(name) = crate::signal::signal_to_name(sig) {
self.write_out(&format!("{name}\n"));
return ExitStatus::SUCCESS;
}
}
self.err_msg(&format!(
"kill: {}: invalid signal specification",
args[i + 1]
));
return ExitStatus::FAILURE;
}
for sig in 1..32 {
if let Some(name) = crate::signal::signal_to_name(sig) {
self.write_out(&format!("{sig}) SIG{name}\n"));
}
}
return ExitStatus::SUCCESS;
} else if args[i] == "-s" {
i += 1;
if i >= args.len() {
self.err_msg("kill: -s requires a signal name");
return ExitStatus::MISUSE;
}
let sig_name = args[i].to_uppercase();
match crate::signal::name_to_signal(&sig_name) {
Some(s) => signum = s,
None => {
self.err_msg(&format!("kill: {}: invalid signal specification", args[i]));
return ExitStatus::FAILURE;
}
}
i += 1;
} else if args[i].starts_with('-') && args[i].len() > 1 {
let spec = &args[i][1..];
if let Ok(n) = spec.parse::<i32>() {
signum = n;
} else {
let sig_name = spec.to_uppercase();
match crate::signal::name_to_signal(&sig_name) {
Some(s) => signum = s,
None => {
self.err_msg(&format!("kill: {}: invalid signal specification", spec));
return ExitStatus::FAILURE;
}
}
}
i += 1;
}
if i >= args.len() {
self.err_msg("kill: usage: kill [-s signal | -signal] pid ...");
return ExitStatus::MISUSE;
}
let mut status = ExitStatus::SUCCESS;
for pid_str in &args[i..] {
match pid_str.parse::<i32>() {
Ok(pid) => {
let ret = unsafe { libc::kill(pid, signum) };
if ret != 0 {
let err = std::io::Error::last_os_error();
self.err_msg(&format!("kill: ({pid}) - {err}"));
status = ExitStatus::FAILURE;
}
}
Err(_) => {
self.err_msg(&format!("kill: {pid_str}: arguments must be process IDs"));
status = ExitStatus::FAILURE;
}
}
}
status
}
fn builtin_umask(&self, args: &[String]) -> ExitStatus {
if args.len() <= 1 {
let mask = unsafe { sys::umask(0) };
unsafe {
sys::umask(mask);
}
self.write_out(&format!("{mask:04o}\n"));
return ExitStatus::SUCCESS;
}
if let Ok(mask) = u32::from_str_radix(&args[1], 8) {
unsafe {
sys::umask(mask as libc::mode_t);
}
ExitStatus::SUCCESS
} else {
self.err_msg(&format!("umask: {}: invalid mask", args[1]));
ExitStatus::FAILURE
}
}
fn builtin_getopts(&mut self, args: &[String]) -> ExitStatus {
if args.len() < 3 {
self.err_msg("getopts: usage: getopts optstring name [arg ...]");
return ExitStatus::MISUSE;
}
let optstring = &args[1];
let name = &args[2];
let argv: Vec<String> = if args.len() > 3 {
args[3..].to_vec()
} else {
self.vars.positional_shell_strings()
};
let optind: usize = self
.vars
.get("OPTIND")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
if optind < 1 || optind > argv.len() {
let _ = self.vars.set(name, "?");
return ExitStatus::FAILURE;
}
let arg = &argv[optind - 1];
if !arg.starts_with('-') || arg == "-" || arg == "--" {
let _ = self.vars.set(name, "?");
return ExitStatus::FAILURE;
}
let optchars: Vec<char> = optstring.chars().collect();
let arg_chars: Vec<char> = arg.chars().collect();
let optpos: usize = self
.vars
.get("_OPTPOS")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
if optpos >= arg_chars.len() {
let _ = self.vars.set("OPTIND", &(optind + 1).to_string());
let _ = self.vars.set("_OPTPOS", "1");
return self.builtin_getopts(args);
}
let opt = arg_chars[optpos];
let found = optchars.iter().position(|&c| c == opt);
match found {
Some(pos) => {
let _ = self.vars.set(name, &opt.to_string());
let needs_arg = pos + 1 < optchars.len() && optchars[pos + 1] == ':';
if needs_arg {
if optpos + 1 < arg_chars.len() {
let optarg: String = arg_chars[optpos + 1..].iter().collect();
let _ = self.vars.set("OPTARG", &optarg);
let _ = self.vars.set("OPTIND", &(optind + 1).to_string());
let _ = self.vars.set("_OPTPOS", "1");
} else if optind < argv.len() {
let _ = self.vars.set("OPTARG", &argv[optind]);
let _ = self.vars.set("OPTIND", &(optind + 2).to_string());
let _ = self.vars.set("_OPTPOS", "1");
} else {
self.err_msg(&format!("getopts: option requires argument -- {opt}"));
let _ = self.vars.set(name, "?");
let _ = self.vars.set("OPTIND", &(optind + 1).to_string());
return ExitStatus::SUCCESS;
}
} else {
let _ = self.vars.unset("OPTARG");
if optpos + 1 >= arg_chars.len() {
let _ = self.vars.set("OPTIND", &(optind + 1).to_string());
let _ = self.vars.set("_OPTPOS", "1");
} else {
let _ = self.vars.set("_OPTPOS", &(optpos + 1).to_string());
}
}
ExitStatus::SUCCESS
}
None => {
let silent = optstring.starts_with(':');
if silent {
let _ = self.vars.set("OPTARG", &opt.to_string());
} else {
self.err_msg(&format!("getopts: illegal option -- {opt}"));
}
let _ = self.vars.set(name, "?");
if optpos + 1 >= arg_chars.len() {
let _ = self.vars.set("OPTIND", &(optind + 1).to_string());
let _ = self.vars.set("_OPTPOS", "1");
} else {
let _ = self.vars.set("_OPTPOS", &(optpos + 1).to_string());
}
ExitStatus::SUCCESS
}
}
}
fn builtin_printf(&self, args: &[String]) -> ExitStatus {
if args.len() < 2 {
self.err_msg("printf: usage: printf format [arguments]");
return ExitStatus::FAILURE;
}
let format = &args[1];
let mut arg_idx = 2;
let fmt_chars: Vec<char> = format.chars().collect();
let mut out = String::new();
use std::fmt::Write;
loop {
let mut i = 0;
let arg_start = arg_idx;
while i < fmt_chars.len() {
if fmt_chars[i] == '\\' {
i += 1;
if i < fmt_chars.len() {
match fmt_chars[i] {
'n' => out.push('\n'),
't' => out.push('\t'),
'r' => out.push('\r'),
'\\' => out.push('\\'),
'"' => out.push('"'),
'a' => out.push('\x07'),
'b' => out.push('\x08'),
'f' => out.push('\x0c'),
'v' => out.push('\x0b'),
'0' => {
i += 1;
let start = i;
while i < fmt_chars.len()
&& i < start + 3
&& matches!(fmt_chars[i], '0'..='7')
{
i += 1;
}
let oct: String = fmt_chars[start..i].iter().collect();
let n = u8::from_str_radix(&oct, 8).unwrap_or(0);
out.push(n as char);
continue;
}
c => {
out.push('\\');
out.push(c);
}
}
i += 1;
} else {
out.push('\\');
}
} else if fmt_chars[i] == '%' {
i += 1;
if i >= fmt_chars.len() {
out.push('%');
break;
}
let mut flags = String::new();
while i < fmt_chars.len() && matches!(fmt_chars[i], '-' | '+' | ' ' | '0' | '#')
{
flags.push(fmt_chars[i]);
i += 1;
}
let mut width_s = String::new();
while i < fmt_chars.len() && fmt_chars[i].is_ascii_digit() {
width_s.push(fmt_chars[i]);
i += 1;
}
let width: usize = width_s.parse().unwrap_or(0);
let mut precision = None;
if i < fmt_chars.len() && fmt_chars[i] == '.' {
i += 1;
let mut prec = String::new();
while i < fmt_chars.len() && fmt_chars[i].is_ascii_digit() {
prec.push(fmt_chars[i]);
i += 1;
}
precision = Some(prec.parse::<usize>().unwrap_or(0));
}
if i >= fmt_chars.len() {
break;
}
let conv = fmt_chars[i];
i += 1;
let left_align = flags.contains('-');
let zero_pad = flags.contains('0') && !left_align;
match conv {
's' => {
let val = args.get(arg_idx).map(|s| s.as_str()).unwrap_or("");
arg_idx += 1;
let val = if let Some(p) = precision {
&val[..val.len().min(p)]
} else {
val
};
if left_align {
let _ = write!(out, "{val:<width$}");
} else {
let _ = write!(out, "{val:>width$}");
}
}
'd' | 'i' => {
let val = args
.get(arg_idx)
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
arg_idx += 1;
if zero_pad {
let _ = write!(out, "{val:0>width$}");
} else if left_align {
let _ = write!(out, "{val:<width$}");
} else {
let _ = write!(out, "{val:>width$}");
}
}
'o' => {
let val = args
.get(arg_idx)
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
arg_idx += 1;
let _ = write!(out, "{val:o}");
}
'x' => {
let val = args
.get(arg_idx)
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
arg_idx += 1;
let _ = write!(out, "{val:x}");
}
'X' => {
let val = args
.get(arg_idx)
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
arg_idx += 1;
let _ = write!(out, "{val:X}");
}
'c' => {
let val = args
.get(arg_idx)
.and_then(|s| s.chars().next())
.unwrap_or('\0');
arg_idx += 1;
if val != '\0' {
out.push(val);
}
}
'%' => out.push('%'),
'b' => {
let val = args.get(arg_idx).map(|s| s.as_str()).unwrap_or("");
arg_idx += 1;
out.push_str(&unescape_echo(val));
}
c => {
out.push('%');
out.push(c);
}
}
} else {
out.push(fmt_chars[i]);
i += 1;
}
}
if arg_idx == arg_start || arg_idx >= args.len() {
break;
}
}
self.write_out(&out);
ExitStatus::SUCCESS
}
}
pub const BUILTIN_NAMES: &[&str] = &[
":", "true", "false", "echo", "printf", "cd", "pwd", "exit", "return", "break", "continue",
"export", "readonly", "unset", "set", "shift", "eval", ".", "source", "test", "[", "read",
"local", "exec", "command", "type", "wait", "trap", "kill", "umask", "getopts",
];
pub fn is_builtin(name: &str) -> bool {
BUILTIN_NAMES.contains(&name)
}
fn unescape_echo(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('a') => result.push('\x07'),
Some('b') => result.push('\x08'),
Some('f') => result.push('\x0c'),
Some('r') => result.push('\r'),
Some('v') => result.push('\x0b'),
Some(c) => {
result.push('\\');
result.push(c);
}
None => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
pub(crate) mod exec {
use std::ffi::CString;
use crate::shell_bytes::ShellBytes;
pub fn execvp(cmd: &ShellBytes, args: &[ShellBytes]) -> std::io::Error {
let Ok(c_cmd) = cmd.to_cstring() else {
return std::io::Error::from_raw_os_error(libc::ENOENT);
};
let c_args: Vec<CString> = match args
.iter()
.map(ShellBytes::to_cstring)
.collect::<Result<Vec<_>, _>>()
{
Ok(v) => v,
Err(_) => return std::io::Error::from_raw_os_error(libc::EINVAL),
};
let c_argv: Vec<*const libc::c_char> = c_args
.iter()
.map(|a| a.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
unsafe {
crate::sys::execvp(c_cmd.as_ptr(), c_argv.as_ptr());
}
std::io::Error::last_os_error()
}
}