168 lines
4.1 KiB
Rust

#![feature(
if_let_guard,
iter_chain,
anonymous_pipe,
trait_alias,
exitcode_exit_method
)]
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
#![allow(clippy::new_without_default, clippy::should_implement_trait)]
use std::{
fs::File,
io::{self, stdin, stdout, BufRead, BufReader, Write},
process::ExitCode,
};
use clap::Parser;
use env::{Env, Expand};
use exec::Outcome;
use syntax::parse::parse_interactive;
pub mod builtin;
pub mod env;
pub mod exec;
pub mod readline;
pub mod syntax;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Parser)]
pub struct ShellArgs {
#[arg(short)]
login: bool,
script: Option<String>,
args: Vec<String>,
}
pub enum ShellInput {
File(BufReader<File>),
Interactive,
}
impl ShellInput {
pub fn read_line(&mut self, line: &mut String) -> Result<usize, Error> {
match self {
Self::File(file) => Ok(file.read_line(line)?),
Self::Interactive => {
let mut stdout = stdout();
let mut stdin = stdin();
readline::readline(&mut stdin, &mut stdout, line, |stdout| {
let cwd = std::env::current_dir();
let cwd = match cwd {
Ok(cwd) => format!("{}", cwd.display()),
Err(_) => "???".into(),
};
let prompt = format!("{cwd} $ ");
stdout.write_all(prompt.as_bytes()).ok();
})
}
}
}
}
fn run(mut input: ShellInput, env: &Env) -> Result<(), Error> {
let mut line = String::new();
loop {
line.clear();
let len = input.read_line(&mut line)?;
if len == 0 {
break Ok(());
}
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let command = match parse_interactive(line) {
Ok(c) => c,
Err(e) => {
eprintln!("Syntax error: {e}");
continue;
}
};
let command = match command.expand(env) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
continue;
}
};
let (outcome, exit) = match exec::eval(command) {
Ok(res) => res,
Err(error) => {
eprintln!("{error}");
continue;
}
};
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 run_wrapper(args: ShellArgs, env: &Env) -> Result<(), Error> {
match args.script {
Some(script) => {
let script = BufReader::new(File::open(script)?);
run(ShellInput::File(script), env)
}
None => run(ShellInput::Interactive, env),
}
}
fn main() -> ExitCode {
const PROFILE_PATH: &str = "/etc/profile";
let args = ShellArgs::parse();
let mut env = Env::new();
env.setup_builtin();
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), &env) {
Ok(()) => (),
Err(error) => {
eprintln!("{PROFILE_PATH}: {error}");
}
}
}
}
match run_wrapper(args, &env) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}