use std::{ fs::File,
io::{ BufReader, IsTerminal, stdin, stderr },
path::PathBuf,
process::ExitCode,
env::args_os };
use clap::Parser;
use supports_color::{ Stream, on };
use crate::{ language::{ interpreter::{ Interpreter, Startup, Interactive, RcFile },
text::{ buffer::{ Buffer, SimpleBuffer }, read_buffer::ReadBuffer } },
runtime::{ color::TtyColorMode,
repl::Repl,
result::{ RuntimeResult, RuntimeError } } };
mod language;
mod runtime;
#[derive(Parser, Debug)]
#[command(name = env!("CARGO_BIN_NAME"),
version,
about = "The Shelly interactive shell and scripting language.")]
struct CommandArguments
{
#[arg(short = 'i', conflicts_with_all = ["stdin_code"])]
interactive: bool,
#[arg(short = 'l', long)]
login: bool,
#[arg(short = 'c', long,
value_name = "code",
conflicts_with_all = ["stdin_code", "interactive"])]
code: Option<String>,
#[arg(short = 's', long = "stdin")]
stdin_code: bool,
#[arg(short = 'b', long)]
no_banner: bool,
#[arg(short = 'm', long = "mono")]
mono: bool,
#[arg(short = 't', long)]
tab_width: Option<usize>,
#[arg(long)]
norc: bool,
#[arg(long, conflicts_with = "norc")]
rcfile: Option<PathBuf>,
#[arg(value_name = "script & arguments", trailing_var_arg = true, allow_hyphen_values = true)]
script_arguments: Vec<String>,
}
enum RunningMode
{
Interactive,
Script(PathBuf),
Code(String),
Stdin
}
fn determine_running_mode(args: &CommandArguments) -> (RunningMode, Vec<String>)
{
if args.interactive
{
return (RunningMode::Interactive, args.script_arguments.clone());
}
if args.stdin_code
{
return (RunningMode::Stdin, args.script_arguments.clone());
}
if args.code.is_some()
{
return (
RunningMode::Code(args.code.as_ref().unwrap().clone()),
args.script_arguments.clone(),
);
}
if args.script_arguments.len() >= 1
{
return (RunningMode::Script(PathBuf::from(&args.script_arguments[0])),
args.script_arguments[1..].to_vec());
}
if stdin().is_terminal()
&& stderr().is_terminal()
{
return (RunningMode::Interactive, args.script_arguments.clone());
}
(RunningMode::Stdin, args.script_arguments.clone())
}
fn determine_color_mode(args: &CommandArguments) -> TtyColorMode
{
if args.mono
{
return TtyColorMode::TtyMonochrome;
}
match on(Stream::Stdout)
{
Some(c) if c.has_16m => TtyColorMode::TtyTrueColor,
Some(c) if c.has_256 => TtyColorMode::Tty256,
Some(c) if c.has_basic => TtyColorMode::TtyBasic,
_ => TtyColorMode::TtyMonochrome
}
}
fn run_as_repl(tab_width: usize,
color_mode: TtyColorMode,
startup: Startup,
suppress_banner: bool,
rc_file: RcFile,
script_args: Vec<String>) -> RuntimeResult<ExitCode>
{
let mut repl = Repl::new(color_mode, tab_width, startup, suppress_banner, rc_file, script_args);
repl.run()
}
fn interpret(buffer: &mut dyn Buffer,
startup: Startup,
color_mode: TtyColorMode,
tab_width: usize,
script_args: Vec<String>) -> RuntimeResult<ExitCode>
{
let mut interpreter = Interpreter::new(startup,
Interactive::No,
color_mode,
tab_width,
RcFile::None,
script_args);
interpreter.execute_from_buffer(buffer)?;
Ok(ExitCode::from(interpreter.exit_code))
}
fn run_script(script: &PathBuf,
tab_width: usize,
color_mode: TtyColorMode,
startup: Startup,
script_args: Vec<String>) -> RuntimeResult<ExitCode>
{
let file = File::open(script);
if let Err(e) = file
{
return Err(RuntimeError::FileOpenError(script.clone(), e));
}
let file = file.unwrap();
let script = script.to_str().unwrap_or("input script");
let mut file_buffer = BufReader::new(file);
let mut buffer = ReadBuffer::new(&script, &mut file_buffer, Some(tab_width));
interpret(&mut buffer, startup, color_mode, tab_width, script_args)
}
fn run_code(code: &String,
tab_width: usize,
color_mode: TtyColorMode,
startup: Startup,
script_args: Vec<String>) -> RuntimeResult<ExitCode>
{
let mut buffer = SimpleBuffer::new("command line", &code, Some(tab_width));
interpret(&mut buffer, startup, color_mode, tab_width, script_args)
}
fn run_stdin(tab_width: usize,
color_mode: TtyColorMode,
startup: Startup,
script_args: Vec<String>) -> RuntimeResult<ExitCode>
{
let mut buffer = BufReader::new(stdin());
let mut buffer = ReadBuffer::new("standard input", &mut buffer, Some(tab_width));
interpret(&mut buffer, startup, color_mode, tab_width, script_args)
}
fn main() -> RuntimeResult<ExitCode>
{
let args = CommandArguments::parse();
if let Some(tab_width) = args.tab_width && tab_width == 0
{
return Err(RuntimeError::InvalidTabWidth);
}
let color_mode = determine_color_mode(&args);
let invoked_as_login = args_os()
.next()
.is_some_and(|name| name.as_encoded_bytes().starts_with(b"-"));
let rc_file = if let Some(custom_rc) = &args.rcfile
{
RcFile::Custom(custom_rc.clone())
}
else if args.norc
{
RcFile::None
}
else
{
RcFile::Default
};
let startup = if args.login || invoked_as_login
{
Startup::Login
}
else
{
Startup::NonLogin
};
let tab_width = args.tab_width.unwrap_or(4);
let (mode, script_args) = determine_running_mode(&args);
match mode
{
RunningMode::Interactive => run_as_repl(tab_width,
color_mode,
startup,
args.no_banner,
rc_file,
script_args),
RunningMode::Script(path) => run_script(&path,
tab_width,
color_mode,
startup,
script_args),
RunningMode::Code(code) => run_code(&code,
tab_width,
color_mode,
startup,
script_args),
RunningMode::Stdin => run_stdin(tab_width,
color_mode,
startup,
script_args)
}
}