260 lines
7.3 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|