mod builtin;
mod env;
mod error;
mod exec;
mod expand;
mod interactive;
mod lexer;
mod parser;
mod plugin;
mod signal;
use std::env as std_env;
use std::fs;
use std::io::{self, Read};
use std::process;
use exec::Executor;
use owo_colors::OwoColorize;
fn should_colorize() -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if let Some(val) = std::env::var_os("CLICOLOR_FORCE") {
if val != "0" {
return true;
}
}
nix::unistd::isatty(std::io::stdout()).unwrap_or(false)
}
fn print_help() {
let color = should_colorize();
let header = "yosh - A POSIX-compliant shell";
if color {
println!("{}", header.bold());
} else {
println!("{}", header);
}
println!();
if color {
println!(
"{} yosh [options] [file [argument...]]",
"Usage:".yellow().bold()
);
} else {
println!("Usage: yosh [options] [file [argument...]]");
}
println!();
struct HelpSection {
heading: &'static str,
items: &'static [(&'static str, &'static str)],
}
const SECTIONS: &[HelpSection] = &[
HelpSection {
heading: "Options",
items: &[
("-c <command>", "Read commands from command_string"),
("--parse <code>", "Parse and dump AST (debug)"),
("-h, --help", "Show this help message"),
("--version", "Show version information"),
],
},
HelpSection {
heading: "Subcommands",
items: &[("plugin", "Manage shell plugins (see 'yosh plugin --help')")],
},
];
for section in SECTIONS {
if color {
println!("{}", format!("{}:", section.heading).yellow().bold());
} else {
println!("{}:", section.heading);
}
for &(flag, desc) in section.items {
if color {
println!(" {} {}", flag.green(), desc);
} else {
println!(" {:16}{}", flag, desc);
}
}
println!();
}
}
fn print_version() {
println!(
"yosh {} ({} {})",
env!("CARGO_PKG_VERSION"),
env!("YOSH_GIT_HASH"),
env!("YOSH_BUILD_DATE")
);
}
fn main() {
let args: Vec<String> = std_env::args().collect();
let shell_name = args.first().map_or("yosh".to_string(), |a| a.clone());
match args.len() {
1 => {
if nix::unistd::isatty(std::io::stdin()).unwrap_or(false) {
let mut repl = interactive::Repl::new(shell_name);
process::exit(repl.run());
} else {
let mut input = String::new();
io::stdin().read_to_string(&mut input).unwrap_or_else(|e| {
eprintln!("yosh: {}", e);
process::exit(1);
});
let status = run_string(&input, shell_name, vec![], false);
process::exit(status);
}
}
_ => {
if args[1] == "--help" || args[1] == "-h" {
print_help();
process::exit(0);
} else if args[1] == "--version" {
print_version();
process::exit(0);
} else if args[1] == "-c" {
if args.len() < 3 {
eprintln!("yosh: -c requires an argument");
process::exit(2);
}
let rest_start = if args.len() > 3 && args[3] == "--" {
4
} else {
3
};
let sn = if rest_start < args.len() {
args[rest_start].clone()
} else {
shell_name
};
let positional: Vec<String> = if rest_start + 1 < args.len() {
args[rest_start + 1..].to_vec()
} else {
vec![]
};
let status = run_string(&args[2], sn, positional, true);
process::exit(status);
} else if args[1] == "--parse" {
if args.len() < 3 {
eprintln!("yosh: --parse requires an argument");
process::exit(2);
}
let input = if args[2] == "-" {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).unwrap();
buf
} else {
args[2].clone()
};
match parser::Parser::new(&input).parse_program() {
Ok(ast) => println!("{:#?}", ast),
Err(e) => {
eprintln!("{}", e);
process::exit(2);
}
}
} else if let Some(status) = try_subcommand(&args[1..]) {
process::exit(status);
} else {
let positional: Vec<String> = args[2..].to_vec();
let status = run_file(&args[1], shell_name, positional);
process::exit(status);
}
}
}
}
fn try_subcommand(args: &[String]) -> Option<i32> {
let sub = args.first()?;
if sub.starts_with('-') || sub.contains('/') || sub.contains('.') {
return None;
}
let bin_name = format!("yosh-{}", sub);
let found = std_env::var_os("PATH")
.and_then(|paths| std_env::split_paths(&paths).find(|dir| dir.join(&bin_name).is_file()));
let bin_path = found?.join(&bin_name);
let status = process::Command::new(bin_path)
.args(&args[1..])
.status()
.unwrap_or_else(|e| {
eprintln!("yosh: {}: {}", bin_name, e);
process::exit(126);
});
Some(status.code().unwrap_or(1))
}
fn run_string(input: &str, shell_name: String, positional: Vec<String>, cmd_string: bool) -> i32 {
signal::init_signal_handling();
let mut executor = Executor::new(shell_name, positional);
env::default_path::ensure_default_path(&mut executor.env);
executor.load_plugins();
executor.env.mode.options.cmd_string = cmd_string;
executor.verbose_print(input);
let mut remaining = input;
let mut current_line: usize = 1;
let mut status = 0;
loop {
let before = remaining;
let trimmed = remaining.trim_start_matches([' ', '\t', '\n']);
if trimmed.is_empty() {
break;
}
let skipped = &before[..before.len() - trimmed.len()];
current_line += skipped.chars().filter(|&c| c == '\n').count();
remaining = trimmed;
let mut p = parser::Parser::new_with_aliases_at_line(
remaining,
&executor.env.aliases,
current_line,
);
if p.is_at_end() {
break;
}
match p.parse_complete_command() {
Ok(cmd) => {
let consumed = p.consumed_bytes();
if consumed == 0 {
break;
}
let consumed_text = &remaining[..consumed];
current_line += consumed_text.chars().filter(|&c| c == '\n').count();
drop(p);
status = executor.exec_complete_command(&cmd);
if executor.env.exec.flow_control.is_some() {
break;
}
executor.check_errexit(status);
remaining = &remaining[consumed..];
}
Err(e) => {
eprintln!("{}", e);
executor.process_pending_signals();
executor.execute_exit_trap();
return 2;
}
}
}
executor.process_pending_signals();
executor.execute_exit_trap();
status
}
fn run_file(path: &str, shell_name: String, positional: Vec<String>) -> i32 {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("yosh: {}: {}", path, e);
return 127;
}
};
run_string(&content, shell_name, positional, false)
}