#![feature( if_let_guard, iter_chain, anonymous_pipe, trait_alias, exitcode_exit_method, let_chains )] #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))] #![allow(clippy::new_without_default, clippy::should_implement_trait)] use std::{ fs::File, io::{stdout, BufRead, BufReader, Write}, path::Path, process::ExitCode, }; use clap::Parser; use command::env::Environment; use cross::{ io::{TerminalOptions, TerminalOptionsImpl}, term::TerminalInput, }; use error::Error; use exec::Outcome; pub mod builtin; pub mod command; pub mod error; pub mod exec; pub mod readline; pub mod syntax; #[derive(Debug, Parser)] pub struct ShellArgs { #[arg(short)] command: Option, #[arg(short)] login: bool, script: Option, args: Vec, } pub enum ShellInput { File(BufReader), Interactive(TerminalInput), } impl ShellInput { pub fn read_line(&mut self, line: &mut String, continuation: bool) -> Result { match self { Self::File(file) => Ok(file.read_line(line)?), Self::Interactive(stdin) => { let mut stdout = stdout(); readline::readline(stdin, &mut stdout, line, |stdout| { let prompt = if !continuation { let cwd = std::env::current_dir(); let cwd = match cwd { Ok(cwd) => format!("{}", cwd.display()), Err(_) => "???".into(), }; format!("{cwd} $ ") } else { "> ".to_owned() }; stdout.write_all(prompt.as_bytes()).ok(); }) } } } } fn run_single(_env: &Environment, _command: &str) -> Outcome { todo!() // let line = command.trim(); // let command = todo!(); // let command = match parse_interactive(line) { // Ok(c) => c, // Err(e) => { // eprintln!("Syntax error: {e}"); // return Outcome::err(); // } // }; // let command = match command.expand(env) { // Ok(c) => c, // Err(e) => { // eprintln!("{e}"); // return Outcome::err(); // } // }; // let (outcome, exit) = match exec::eval(command) { // Ok(res) => res, // Err(error) => { // eprintln!("{error}"); // return Outcome::err(); // } // }; // if let Some(exit) = exit { // exit.exit_process(); // } // outcome } fn run(mut input: ShellInput, env: &mut Environment) -> Result<(), Error> { let mut command_text = String::new(); let mut line = String::new(); loop { line.clear(); let len = input.read_line(&mut line, !command_text.is_empty())?; if len == 0 { break Ok(()); } let trimmed = line.trim(); if trimmed.starts_with('#') || trimmed.is_empty() { continue; } command_text.push_str(line.trim_matches([' ', '\t'])); let expr = match syntax::parse::parse_toplevel(&command_text) { Ok(("" | "\n", expr)) => expr, Ok((rest, _)) => { eprintln!("Trailing characters: {rest:?}"); command_text.clear(); continue; } Err(syntax::parse::Error::Incomplete) => { continue; } Err(syntax::parse::Error::Lex(e)) if e.is_incomplete() => { continue; } Err(e) => { eprintln!("{e}"); command_text.clear(); continue; } }; let old_termios = if let ShellInput::Interactive(interactive) = &mut input { let new = TerminalOptionsImpl::normal(); Some(interactive.set_options(new)?) } else { None }; let (outcome, exit) = command::eval::evaluate(&expr, env); if let ShellInput::Interactive(interactive) = &mut input { interactive.set_options(old_termios.unwrap()).ok(); } command_text.clear(); if !outcome.is_success() { eprintln!("{outcome:?}"); } if let Some(exit) = exit { exit.exit_process(); } match outcome { Outcome::ExitShell(_) => unreachable!(), Outcome::Process(status) => { if !status.success() { if let Some(code) = status.code() { eprintln!("Exit code: {code}"); } } } Outcome::Builtin(code) => { if code != ExitCode::SUCCESS { eprintln!("Builtin command failed"); } } } } } fn find_script>(arg: &P) -> &Path { #[cfg(any(target_os = "yggdrasil", rust_analyzer))] { let from_auxv = std::os::yggdrasil::real_binary_path(); if from_auxv.exists() { return from_auxv; } } arg.as_ref() } fn run_wrapper(args: ShellArgs, env: &mut Environment) -> Result<(), Error> { let shell_name = std::env::args().next().unwrap(); match (args.command, args.script) { (Some(_), Some(_)) => { eprintln!("{shell_name}: cannot mix '-c' and regular arguments"); Err(Error::InvalidUsage) } (Some(command), None) => match run_single(env, &command) { Outcome::ExitShell(_) => unreachable!(), Outcome::Process(status) if status.success() => ExitCode::SUCCESS.exit_process(), Outcome::Process(status) if let Some(code) = status.code() => std::process::exit(code), Outcome::Process(_) => todo!(), Outcome::Builtin(code) => code.exit_process(), }, (None, Some(script)) => { let script_path = find_script(&script); // let script_path_str = script_path.to_str().unwrap(); // env.put_var("0", script_path_str.into()); let script = BufReader::new(File::open(script_path)?); run(ShellInput::File(script), env) } (None, None) => { // env.put_var("0", shell_name.into()); let stdin = TerminalInput::open()?; run(ShellInput::Interactive(stdin), env) } } } fn main() -> ExitCode { #[cfg(any(rust_analyzer, unix))] unsafe { libc::setpgid(0, 0); libc::signal(libc::SIGTTOU, libc::SIG_IGN); } const PROFILE_PATH: &str = "/etc/profile"; logsink::setup_logging(false); let args = ShellArgs::parse(); let mut env = Environment::default(); // env.setup_builtin(&args.args); builtin::register_default(); if args.login { if let Ok(profile_script) = File::open(PROFILE_PATH) { let profile_script = BufReader::new(profile_script); match run(ShellInput::File(profile_script), &mut env) { Ok(()) => (), Err(error) => { eprintln!("{PROFILE_PATH}: {error}"); } } } } match run_wrapper(args, &mut env) { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } } }