Files
yggdrasil/userspace/tools/shell/src/main.rs
T
2025-07-19 09:50:12 +03:00

260 lines
7.3 KiB
Rust

#![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<String>,
#[arg(short)]
login: bool,
script: Option<String>,
args: Vec<String>,
}
pub enum ShellInput {
File(BufReader<File>),
Interactive(TerminalInput),
}
impl ShellInput {
pub fn read_line(&mut self, line: &mut String, continuation: bool) -> Result<usize, Error> {
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<P: AsRef<Path>>(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
}
}
}