diff --git a/README.md b/README.md index 1f71cc2..ee5c3cd 100644 --- a/README.md +++ b/README.md @@ -170,9 +170,9 @@ coincidence. ### Semicolons Dust borrowed Rust's approach to semicolons and their effect on evaluation and relaxed the rules to -accomated different styles of coding. Rust, for example, isn't design for command lines or REPLs but -Dust could be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc -one-liner sent to the CLI. Thus, semicolons are optional in most cases. +accomated different styles of coding. Rust, isn't designed for command lines or REPLs but Dust could +be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc one-liner +sent to the CLI. Thus, semicolons are optional in most cases. There are two things you need to know about semicolons in Dust: diff --git a/dust-cli/Cargo.toml b/dust-cli/Cargo.toml index b1953c6..1c7ee30 100644 --- a/dust-cli/Cargo.toml +++ b/dust-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dust-cli" -description = "Tool for running and debugging Dust programs" +description = "Command line interface for the Dust programming language" authors = ["Jeff Anderson"] edition.workspace = true license.workspace = true @@ -13,7 +13,13 @@ name = "dust" path = "src/main.rs" [dependencies] -clap = { version = "4.5.14", features = ["cargo", "color", "derive", "help", "wrap_help"] } +clap = { version = "4.5.14", features = [ + "cargo", + "color", + "derive", + "help", + "wrap_help", +] } color-print = "0.3.7" dust-lang = { path = "../dust-lang" } env_logger = "0.11.5" diff --git a/dust-cli/src/main.rs b/dust-cli/src/main.rs index ffcc090..935e4ed 100644 --- a/dust-cli/src/main.rs +++ b/dust-cli/src/main.rs @@ -3,46 +3,64 @@ use std::time::{Duration, Instant}; use std::{fs::read_to_string, path::PathBuf}; use clap::builder::StyledStr; +use clap::Args; use clap::{ builder::{styling::AnsiColor, Styles}, - crate_authors, crate_description, crate_version, ArgAction, Args, ColorChoice, Parser, - Subcommand, ValueHint, + crate_authors, crate_description, crate_version, ColorChoice, Parser, Subcommand, ValueHint, }; use color_print::cstr; use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm}; use log::{Level, LevelFilter}; -const HELP_TEMPLATE: &str = cstr!( - "\ -Dust CLI -──────── -{about} -Version: {version} -Author: {author} -License: GPL-3.0 ⚖️ +const CLI_HELP_TEMPLATE: &str = cstr!( + r#" +Dust CLI +──────── +{about} -Usage -───── + ☑ Version: {version} + ✎ Author: {author} + ⚖️ License: GPL-3.0 + 🌐 Repository: git.jeffa.io/jeff/dust + +Usage +───── {tab}{usage} -Options -─────── -{options} - -Modes -───── +Modes +───── {subcommands} -Arguments -───────── -{positionals} +Options +─────── +{options} +"# +); -" +const MODE_HELP_TEMPLATE: &str = cstr!( + r#" +Dust CLI +──────── +{about} + + ☑ Version: {version} + ✎ Author: {author} + ⚖️ License: GPL-3.0 + 🌐 Repository: git.jeffa.io/jeff/dust + +Usage +───── +{tab}{usage} + +Options +─────── +{options} +"# ); const STYLES: Styles = Styles::styled() .header(AnsiColor::BrightMagenta.on_default().bold()) - .usage(AnsiColor::BrightWhite.on_default().bold()) + .usage(AnsiColor::BrightCyan.on_default().bold()) .literal(AnsiColor::BrightCyan.on_default()) .placeholder(AnsiColor::BrightMagenta.on_default()) .error(AnsiColor::BrightRed.on_default().bold()) @@ -55,175 +73,148 @@ const STYLES: Styles = Styles::styled() author = crate_authors!(), about = crate_description!(), color = ColorChoice::Auto, - disable_help_flag = true, - disable_version_flag = true, - help_template = StyledStr::from(HELP_TEMPLATE), + help_template = StyledStr::from(CLI_HELP_TEMPLATE), styles = STYLES, - term_width = 80, )] struct Cli { - /// Print help information for this or the selected subcommand - #[arg(short, long, action = ArgAction::Help)] - help: bool, - - /// Print version information - #[arg(short, long, action = ArgAction::Version)] - version: bool, - - /// Log level, overrides the DUST_LOG environment variable - #[arg( - short, - long, - value_name = "LOG_LEVEL", - value_parser = ["info", "trace", "debug"], - )] + /// Overrides the DUST_LOG environment variable + #[arg(short, long, value_name = "LOG_LEVEL")] log: Option, + #[command(subcommand)] + mode: Mode, +} + +#[derive(Args)] +#[clap( + styles = STYLES, +)] +#[group()] +struct Input { /// Source code to run instead of a file - #[arg(short, long, value_hint = ValueHint::Other, value_name = "SOURCE")] + #[arg(short, long, value_hint = ValueHint::Other, value_name = "INPUT")] command: Option, /// Read source code from stdin #[arg(long)] stdin: bool, - #[command(subcommand)] - mode: Mode, - /// Path to a source code file #[arg(value_hint = ValueHint::FilePath)] file: Option, } #[derive(Subcommand)] -#[clap( - help_template = StyledStr::from(HELP_TEMPLATE), - styles = STYLES, -)] +#[clap(subcommand_value_name = "MODE", flatten_help = true)] enum Mode { /// Compile and run the program (default) - #[command(short_flag = 'r')] + #[command( + short_flag = 'r', + help_template = MODE_HELP_TEMPLATE + )] Run { - #[arg(short, long, action = ArgAction::Help)] - #[clap(help_heading = Some("Options"))] - help: bool, - /// Print the time taken for compilation and execution #[arg(long)] - #[clap(help_heading = Some("Run Options"))] time: bool, /// Do not print the program's return value #[arg(long)] - #[clap(help_heading = Some("Run Options"))] no_output: bool, /// Custom program name, overrides the file name #[arg(long)] - #[clap(help_heading = Some("Run Options"))] name: Option, + + #[command(flatten)] + input: Input, }, /// Compile and print the bytecode disassembly - #[command(short_flag = 'd')] + #[command( + short_flag = 'd', + help_template = MODE_HELP_TEMPLATE + )] Disassemble { - #[arg(short, long, action = ArgAction::Help)] - #[clap(help_heading = Some("Options"))] - help: bool, - /// Style disassembly output #[arg(short, long, default_value = "true")] - #[clap(help_heading = Some("Disassemble Options"))] style: bool, /// Custom program name, overrides the file name #[arg(long)] - #[clap(help_heading = Some("Disassemble Options"))] name: Option, + + #[command(flatten)] + input: Input, }, /// Lex the source code and print the tokens - #[command(short_flag = 't')] + #[command( + short_flag = 't', + help_template = MODE_HELP_TEMPLATE + )] Tokenize { - #[arg(short, long, action = ArgAction::Help)] - #[clap(help_heading = Some("Options"))] - help: bool, - /// Style token output #[arg(short, long, default_value = "true")] - #[clap(help_heading = Some("Tokenize Options"))] style: bool, + + #[command(flatten)] + input: Input, }, } -#[derive(Args, Clone)] -#[group(required = true, multiple = false)] -struct Source {} - -fn main() { - let start_time = Instant::now(); - // let mut logger = env_logger::builder(); - - // logger.format(move |buf, record| { - // let elapsed = format!("T+{:.04}", start_time.elapsed().as_secs_f32()).dimmed(); - // let level_display = match record.level() { - // Level::Info => "INFO".bold().white(), - // Level::Debug => "DEBUG".bold().blue(), - // Level::Warn => "WARN".bold().yellow(), - // Level::Error => "ERROR".bold().red(), - // Level::Trace => "TRACE".bold().purple(), - // }; - // let display = format!("[{elapsed}] {level_display:5} {args}", args = record.args()); - - // writeln!(buf, "{display}") - // }); - - let Cli { - log, - command, - stdin, - mode, - file, - .. - } = Cli::parse(); - - // if let Some(level) = log { - // logger.filter_level(level).init(); - // } else { - // logger.parse_env("DUST_LOG").init(); - // } - - let (source, file_name) = if let Some(source) = command { - (source, None) - } else if stdin { - let mut source = String::new(); - - io::stdin() - .read_to_string(&mut source) - .expect("Failed to read from stdin"); - - (source, None) - } else { - let path = file.expect("Path is required when command is not provided"); - let source = read_to_string(&path).expect("Failed to read file"); +fn get_source_and_file_name(input: Input) -> (String, Option) { + if let Some(path) = input.file { + let source = read_to_string(&path).expect("Failed to read source file"); let file_name = path .file_name() .and_then(|os_str| os_str.to_str()) .map(DustString::from); - (source, file_name) - }; - let program_name = match &mode { - Mode::Run { name, .. } => name, - Mode::Disassemble { name, .. } => name, - Mode::Tokenize { .. } => &None, + return (source, file_name); } - .iter() - .next() - .cloned() - .or(file_name); - if let Mode::Disassemble { style, .. } = mode { + if input.stdin { + let mut source = String::new(); + io::stdin() + .read_to_string(&mut source) + .expect("Failed to read from stdin"); + + return (source, None); + } + + let source = input.command.expect("No source code provided"); + + (source, None) +} + +fn main() { + let start_time = Instant::now(); + let mut logger = env_logger::builder(); + + logger.format(move |buf, record| { + let elapsed = format!("T+{:.04}", start_time.elapsed().as_secs_f32()); + let level_display = match record.level() { + Level::Info => cstr!("INFO"), + Level::Trace => cstr!("TRACE"), + Level::Debug => cstr!("DEBUG"), + Level::Warn => cstr!("WARN"), + Level::Error => cstr!("ERROR"), + }; + let display = format!("[{elapsed}] {level_display:5} {args}", args = record.args()); + + writeln!(buf, "{display}") + }); + + let Cli { log, mode } = Cli::parse(); + + if let Some(level) = log { + logger.filter_level(level).init(); + } else { + logger.parse_env("DUST_LOG").init(); + } + + if let Mode::Disassemble { style, name, input } = mode { + let (source, file_name) = get_source_and_file_name(input); let lexer = Lexer::new(&source); let mut compiler = match Compiler::new(lexer) { Ok(compiler) => compiler, @@ -243,21 +234,22 @@ fn main() { } } - let chunk = compiler.finish(program_name); + let chunk = compiler.finish(file_name); let mut stdout = stdout().lock(); chunk .disassembler(&mut stdout) .style(style) .source(&source) - .width(80) + .width(65) .disassemble() .expect("Failed to write disassembly to stdout"); return; } - if let Mode::Tokenize { style, .. } = mode { + if let Mode::Tokenize { input, .. } = mode { + let (source, _) = get_source_and_file_name(input); let mut lexer = Lexer::new(&source); let mut next_token = || -> Option<(Token, Span, bool)> { match lexer.next_token() { @@ -303,9 +295,13 @@ fn main() { } if let Mode::Run { - time, no_output, .. + name, + time, + no_output, + input, } = mode { + let (source, file_name) = get_source_and_file_name(input); let lexer = Lexer::new(&source); let mut compiler = match Compiler::new(lexer) { Ok(compiler) => compiler, @@ -325,7 +321,7 @@ fn main() { } } - let chunk = compiler.finish(program_name); + let chunk = compiler.finish(name.or(file_name)); let compile_end = start_time.elapsed(); if time {