use std::io::{self, Write};
use std::os::unix::io::FromRawFd;
use crate::parser::ShellCommand;
use crate::state::ShellState;
pub struct ColoredWriter<W: Write> {
inner: W,
}
impl<W: Write> ColoredWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
}
impl<W: Write> Write for ColoredWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
pub struct BadFdWriter;
impl Write for BadFdWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::from_raw_os_error(libc::EBADF))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::from_raw_os_error(libc::EBADF))
}
}
mod builtin_alias;
mod builtin_bg;
mod builtin_break;
mod builtin_cd;
mod builtin_colon;
mod builtin_continue;
mod builtin_declare;
mod builtin_dirs;
mod builtin_env;
mod builtin_exit;
mod builtin_export;
mod builtin_fg;
mod builtin_help;
mod builtin_jobs;
mod builtin_kill;
mod builtin_popd;
mod builtin_pushd;
mod builtin_pwd;
mod builtin_return;
mod builtin_set;
mod builtin_set_color_scheme;
mod builtin_set_colors;
mod builtin_set_condensed;
mod builtin_shift;
mod builtin_source;
mod builtin_test;
mod builtin_times;
mod builtin_trap;
mod builtin_type;
mod builtin_unalias;
mod builtin_unset;
mod builtin_wait;
pub trait Builtin {
fn name(&self) -> &'static str;
fn names(&self) -> Vec<&'static str>;
fn description(&self) -> &'static str;
fn run(
&self,
cmd: &ShellCommand,
shell_state: &mut ShellState,
output_writer: &mut dyn Write,
) -> i32;
}
fn get_builtins() -> Vec<Box<dyn Builtin>> {
vec![
Box::new(builtin_cd::CdBuiltin),
Box::new(builtin_pwd::PwdBuiltin),
Box::new(builtin_env::EnvBuiltin),
Box::new(builtin_exit::ExitBuiltin),
Box::new(builtin_help::HelpBuiltin),
Box::new(builtin_source::SourceBuiltin),
Box::new(builtin_export::ExportBuiltin),
Box::new(builtin_unset::UnsetBuiltin),
Box::new(builtin_pushd::PushdBuiltin),
Box::new(builtin_popd::PopdBuiltin),
Box::new(builtin_dirs::DirsBuiltin),
Box::new(builtin_alias::AliasBuiltin),
Box::new(builtin_unalias::UnaliasBuiltin),
Box::new(builtin_test::TestBuiltin),
Box::new(builtin_set::SetBuiltin),
Box::new(builtin_set_colors::SetColorsBuiltin),
Box::new(builtin_set_color_scheme::SetColorSchemeBuiltin),
Box::new(builtin_set_condensed::SetCondensedBuiltin),
Box::new(builtin_shift::ShiftBuiltin),
Box::new(builtin_declare::DeclareBuiltin),
Box::new(builtin_times::TimesBuiltin),
Box::new(builtin_trap::TrapBuiltin),
Box::new(builtin_type::TypeBuiltin),
Box::new(builtin_return::ReturnBuiltin),
Box::new(builtin_break::BreakBuiltin),
Box::new(builtin_continue::ContinueBuiltin),
Box::new(builtin_colon::ColonBuiltin),
Box::new(builtin_jobs::JobsBuiltin),
Box::new(builtin_fg::FgBuiltin),
Box::new(builtin_bg::BgBuiltin),
Box::new(builtin_kill::KillBuiltin),
Box::new(builtin_wait::WaitBuiltin),
]
}
pub fn is_builtin(cmd: &str) -> bool {
get_builtins().iter().any(|b| b.names().contains(&cmd))
}
pub fn get_builtin_commands() -> Vec<String> {
let builtins = get_builtins();
let mut commands = Vec::new();
for b in builtins {
for &name in &b.names() {
commands.push(name.to_string());
}
}
commands
}
pub fn execute_builtin(
cmd: &ShellCommand,
shell_state: &mut ShellState,
output_override: Option<Box<dyn Write>>,
) -> i32 {
let colors_enabled = shell_state.colors_enabled;
let error_color = shell_state.color_scheme.error.clone();
let print_error = move |msg: &str| {
if colors_enabled {
eprintln!("{}{}\x1b[0m", error_color, msg);
} else {
eprintln!("{}", msg);
}
};
if let Some(mut output_writer) = output_override {
let builtins = get_builtins();
if let Some(builtin) = builtins
.into_iter()
.find(|b| b.names().contains(&cmd.args[0].as_str()))
{
return builtin.run(cmd, shell_state, &mut *output_writer);
} else {
return 1;
}
}
use crate::parser::Redirection;
let redirections = cmd.redirections.clone();
let mut files_to_expand: Vec<String> = Vec::new();
for redir in &redirections {
match redir {
Redirection::Input(file)
| Redirection::Output(file)
| Redirection::OutputClobber(file)
| Redirection::Append(file)
| Redirection::FdInput(_, file)
| Redirection::FdOutput(_, file)
| Redirection::FdOutputClobber(_, file)
| Redirection::FdAppend(_, file)
| Redirection::FdInputOutput(_, file) => {
files_to_expand.push(file.clone());
}
_ => {
files_to_expand.push(String::new()); }
}
}
let mut expanded_files: Vec<String> = Vec::new();
for f in &files_to_expand {
if f.is_empty() {
expanded_files.push(String::new());
} else {
expanded_files.push(crate::executor::expand_variables_in_string(f, shell_state));
}
}
let mut expanded_redirections: Vec<(Redirection, Option<String>)> = Vec::new();
for (i, redir) in redirections.iter().enumerate() {
let expanded_file = if expanded_files[i].is_empty() {
None
} else {
Some(expanded_files[i].clone())
};
expanded_redirections.push((redir.clone(), expanded_file));
}
if let Err(e) = shell_state.fd_table.borrow_mut().save_all_fds() {
print_error(&format!("Failed to save file descriptors: {}", e));
return 1;
}
for (redir, expanded_file) in &expanded_redirections {
let result = match redir {
Redirection::Input(_) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
0, file, true, false, false, false, false, )
}
Redirection::Output(_) | Redirection::OutputClobber(_) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
1, file, false, true, false, true, false, )
}
Redirection::Append(_) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
1, file, false, true, true, false, false, )
}
Redirection::FdInput(fd, _) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
*fd, file, true, false, false, false, false, )
}
Redirection::FdOutput(fd, _) | Redirection::FdOutputClobber(fd, _) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
*fd, file, false, true, false, true, false, )
}
Redirection::FdAppend(fd, _) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
*fd, file, false, true, true, false, false, )
}
Redirection::FdDuplicate(target_fd, source_fd) => shell_state
.fd_table
.borrow_mut()
.duplicate_fd(*source_fd, *target_fd),
Redirection::FdClose(fd) => shell_state.fd_table.borrow_mut().close_fd(*fd),
Redirection::FdInputOutput(fd, _) => {
let file = expanded_file.as_ref().unwrap();
shell_state.fd_table.borrow_mut().open_fd(
*fd, file, true, true, false, false, false, )
}
Redirection::HereDoc(_, _) | Redirection::HereString(_) => Ok(()),
};
if let Err(e) = result {
print_error(&format!("Redirection error: {}", e));
let _ = shell_state.fd_table.borrow_mut().restore_all_fds();
return 1;
}
}
let mut output_writer: Box<dyn Write> = {
let raw_fd = shell_state.fd_table.borrow().get_raw_fd(1);
match raw_fd {
Some(fd) => {
let dup_fd = unsafe { libc::dup(fd) };
if dup_fd >= 0 {
let file = unsafe { std::fs::File::from_raw_fd(dup_fd) };
Box::new(ColoredWriter::new(file))
} else {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EBADF) {
Box::new(BadFdWriter)
} else {
print_error(&format!("Failed to duplicate stdout: {}", err));
let _ = shell_state.fd_table.borrow_mut().restore_all_fds();
return 1;
}
}
}
None => {
Box::new(BadFdWriter)
}
}
};
let builtins = get_builtins();
let exit_code = if let Some(builtin) = builtins
.into_iter()
.find(|b| b.names().contains(&cmd.args[0].as_str()))
{
builtin.run(cmd, shell_state, &mut *output_writer)
} else {
1
};
if let Err(e) = shell_state.fd_table.borrow_mut().restore_all_fds() {
print_error(&format!("Failed to restore file descriptors: {}", e));
return 1;
}
exit_code
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_builtin() {
assert!(is_builtin("cd"));
assert!(is_builtin("pwd"));
assert!(is_builtin("env"));
assert!(is_builtin("exit"));
assert!(is_builtin("help"));
assert!(is_builtin("alias"));
assert!(is_builtin("unalias"));
assert!(is_builtin("test"));
assert!(is_builtin("["));
assert!(is_builtin("."));
assert!(!is_builtin("ls"));
assert!(!is_builtin("grep"));
assert!(!is_builtin("echo"));
}
#[test]
fn test_execute_builtin_unknown() {
let cmd = ShellCommand {
args: vec!["unknown".to_string()],
redirections: Vec::new(),
compound: None,
};
let mut shell_state = ShellState::new();
let exit_code = execute_builtin(&cmd, &mut shell_state, None);
assert_eq!(exit_code, 1);
}
#[test]
fn test_get_builtin_commands() {
let commands = get_builtin_commands();
assert!(commands.contains(&"cd".to_string()));
assert!(commands.contains(&"pwd".to_string()));
assert!(commands.contains(&"env".to_string()));
assert!(commands.contains(&"exit".to_string()));
assert!(commands.contains(&"help".to_string()));
assert!(commands.contains(&"source".to_string()));
assert!(commands.contains(&"export".to_string()));
assert!(commands.contains(&"unset".to_string()));
assert!(commands.contains(&"pushd".to_string()));
assert!(commands.contains(&"popd".to_string()));
assert!(commands.contains(&"dirs".to_string()));
assert!(commands.contains(&"alias".to_string()));
assert!(commands.contains(&"unalias".to_string()));
assert!(commands.contains(&"test".to_string()));
assert!(commands.contains(&"[".to_string()));
assert!(commands.contains(&".".to_string()));
assert!(commands.contains(&"set_colors".to_string()));
assert!(commands.contains(&"set_color_scheme".to_string()));
assert!(commands.contains(&"set_condensed".to_string()));
assert!(commands.contains(&"return".to_string()));
assert!(commands.contains(&"break".to_string()));
assert!(commands.contains(&"continue".to_string()));
assert!(commands.contains(&"set".to_string()));
assert!(commands.contains(&":".to_string()));
assert!(commands.contains(&"times".to_string()));
assert!(commands.contains(&"jobs".to_string()));
assert!(commands.contains(&"fg".to_string()));
assert!(commands.contains(&"bg".to_string()));
assert!(commands.contains(&"kill".to_string()));
assert!(commands.contains(&"wait".to_string()));
assert_eq!(commands.len(), 34);
}
}