1
0

Clean up the README and style the CLI

This commit is contained in:
Jeff 2024-12-18 08:49:45 -05:00
parent 46060a473d
commit ed05a981e7
3 changed files with 144 additions and 142 deletions

View File

@ -170,9 +170,9 @@ coincidence.
### Semicolons ### Semicolons
Dust borrowed Rust's approach to semicolons and their effect on evaluation and relaxed the rules to 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 accomated different styles of coding. Rust, isn't designed for command lines or REPLs but Dust could
Dust could be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc one-liner
one-liner sent to the CLI. Thus, semicolons are optional in most cases. sent to the CLI. Thus, semicolons are optional in most cases.
There are two things you need to know about semicolons in Dust: There are two things you need to know about semicolons in Dust:

View File

@ -1,6 +1,6 @@
[package] [package]
name = "dust-cli" name = "dust-cli"
description = "Tool for running and debugging Dust programs" description = "Command line interface for the Dust programming language"
authors = ["Jeff Anderson"] authors = ["Jeff Anderson"]
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -13,7 +13,13 @@ name = "dust"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [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" color-print = "0.3.7"
dust-lang = { path = "../dust-lang" } dust-lang = { path = "../dust-lang" }
env_logger = "0.11.5" env_logger = "0.11.5"

View File

@ -3,46 +3,64 @@ use std::time::{Duration, Instant};
use std::{fs::read_to_string, path::PathBuf}; use std::{fs::read_to_string, path::PathBuf};
use clap::builder::StyledStr; use clap::builder::StyledStr;
use clap::Args;
use clap::{ use clap::{
builder::{styling::AnsiColor, Styles}, builder::{styling::AnsiColor, Styles},
crate_authors, crate_description, crate_version, ArgAction, Args, ColorChoice, Parser, crate_authors, crate_description, crate_version, ColorChoice, Parser, Subcommand, ValueHint,
Subcommand, ValueHint,
}; };
use color_print::cstr; use color_print::cstr;
use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm}; use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm};
use log::{Level, LevelFilter}; use log::{Level, LevelFilter};
const HELP_TEMPLATE: &str = cstr!( const CLI_HELP_TEMPLATE: &str = cstr!(
"\ r#"
<bold,bright-magenta>Dust CLI</bold,bright-magenta> <bright-magenta><bold>Dust CLI
</bold>
{about} {about}</bright-magenta>
Version: {version}
Author: {author}
License: GPL-3.0
<bold,bright-magenta>Usage</bold,bright-magenta> Version: {version}
Author: {author}
License: GPL-3.0
🌐 Repository: git.jeffa.io/jeff/dust
<bright-magenta,bold>Usage
</bright-magenta,bold>
{tab}{usage} {tab}{usage}
<bold,bright-magenta>Options</bold,bright-magenta> <bright-magenta,bold>Modes
</bright-magenta,bold>
{options}
<bold,bright-magenta>Modes</bold,bright-magenta>
{subcommands} {subcommands}
<bold,bright-magenta>Arguments</bold,bright-magenta> <bright-magenta,bold>Options
</bright-magenta,bold>
{positionals} {options}
"#
);
" const MODE_HELP_TEMPLATE: &str = cstr!(
r#"
<bright-magenta><bold>Dust CLI
</bold>
{about}</bright-magenta>
Version: {version}
Author: {author}
License: GPL-3.0
🌐 Repository: git.jeffa.io/jeff/dust
<bright-magenta,bold>Usage
</bright-magenta,bold>
{tab}{usage}
<bright-magenta,bold>Options
</bright-magenta,bold>
{options}
"#
); );
const STYLES: Styles = Styles::styled() const STYLES: Styles = Styles::styled()
.header(AnsiColor::BrightMagenta.on_default().bold()) .header(AnsiColor::BrightMagenta.on_default().bold())
.usage(AnsiColor::BrightWhite.on_default().bold()) .usage(AnsiColor::BrightCyan.on_default().bold())
.literal(AnsiColor::BrightCyan.on_default()) .literal(AnsiColor::BrightCyan.on_default())
.placeholder(AnsiColor::BrightMagenta.on_default()) .placeholder(AnsiColor::BrightMagenta.on_default())
.error(AnsiColor::BrightRed.on_default().bold()) .error(AnsiColor::BrightRed.on_default().bold())
@ -55,175 +73,148 @@ const STYLES: Styles = Styles::styled()
author = crate_authors!(), author = crate_authors!(),
about = crate_description!(), about = crate_description!(),
color = ColorChoice::Auto, color = ColorChoice::Auto,
disable_help_flag = true, help_template = StyledStr::from(CLI_HELP_TEMPLATE),
disable_version_flag = true,
help_template = StyledStr::from(HELP_TEMPLATE),
styles = STYLES, styles = STYLES,
term_width = 80,
)] )]
struct Cli { struct Cli {
/// Print help information for this or the selected subcommand /// Overrides the DUST_LOG environment variable
#[arg(short, long, action = ArgAction::Help)] #[arg(short, long, value_name = "LOG_LEVEL")]
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"],
)]
log: Option<LevelFilter>, log: Option<LevelFilter>,
#[command(subcommand)]
mode: Mode,
}
#[derive(Args)]
#[clap(
styles = STYLES,
)]
#[group()]
struct Input {
/// Source code to run instead of a file /// 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<String>, command: Option<String>,
/// Read source code from stdin /// Read source code from stdin
#[arg(long)] #[arg(long)]
stdin: bool, stdin: bool,
#[command(subcommand)]
mode: Mode,
/// Path to a source code file /// Path to a source code file
#[arg(value_hint = ValueHint::FilePath)] #[arg(value_hint = ValueHint::FilePath)]
file: Option<PathBuf>, file: Option<PathBuf>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
#[clap( #[clap(subcommand_value_name = "MODE", flatten_help = true)]
help_template = StyledStr::from(HELP_TEMPLATE),
styles = STYLES,
)]
enum Mode { enum Mode {
/// Compile and run the program (default) /// Compile and run the program (default)
#[command(short_flag = 'r')] #[command(
short_flag = 'r',
help_template = MODE_HELP_TEMPLATE
)]
Run { Run {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Print the time taken for compilation and execution /// Print the time taken for compilation and execution
#[arg(long)] #[arg(long)]
#[clap(help_heading = Some("Run Options"))]
time: bool, time: bool,
/// Do not print the program's return value /// Do not print the program's return value
#[arg(long)] #[arg(long)]
#[clap(help_heading = Some("Run Options"))]
no_output: bool, no_output: bool,
/// Custom program name, overrides the file name /// Custom program name, overrides the file name
#[arg(long)] #[arg(long)]
#[clap(help_heading = Some("Run Options"))]
name: Option<DustString>, name: Option<DustString>,
#[command(flatten)]
input: Input,
}, },
/// Compile and print the bytecode disassembly /// Compile and print the bytecode disassembly
#[command(short_flag = 'd')] #[command(
short_flag = 'd',
help_template = MODE_HELP_TEMPLATE
)]
Disassemble { Disassemble {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Style disassembly output /// Style disassembly output
#[arg(short, long, default_value = "true")] #[arg(short, long, default_value = "true")]
#[clap(help_heading = Some("Disassemble Options"))]
style: bool, style: bool,
/// Custom program name, overrides the file name /// Custom program name, overrides the file name
#[arg(long)] #[arg(long)]
#[clap(help_heading = Some("Disassemble Options"))]
name: Option<DustString>, name: Option<DustString>,
#[command(flatten)]
input: Input,
}, },
/// Lex the source code and print the tokens /// Lex the source code and print the tokens
#[command(short_flag = 't')] #[command(
short_flag = 't',
help_template = MODE_HELP_TEMPLATE
)]
Tokenize { Tokenize {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Style token output /// Style token output
#[arg(short, long, default_value = "true")] #[arg(short, long, default_value = "true")]
#[clap(help_heading = Some("Tokenize Options"))]
style: bool, style: bool,
#[command(flatten)]
input: Input,
}, },
} }
#[derive(Args, Clone)] fn get_source_and_file_name(input: Input) -> (String, Option<DustString>) {
#[group(required = true, multiple = false)] if let Some(path) = input.file {
struct Source {} let source = read_to_string(&path).expect("Failed to read source file");
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");
let file_name = path let file_name = path
.file_name() .file_name()
.and_then(|os_str| os_str.to_str()) .and_then(|os_str| os_str.to_str())
.map(DustString::from); .map(DustString::from);
(source, file_name) return (source, file_name);
};
let program_name = match &mode {
Mode::Run { name, .. } => name,
Mode::Disassemble { name, .. } => name,
Mode::Tokenize { .. } => &None,
} }
.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!("<bright-magenta,bold>INFO<bright-magenta,bold>"),
Level::Trace => cstr!("<bright-cyan,bold>TRACE<bright-cyan,bold>"),
Level::Debug => cstr!("<bright-blue,bold>DEBUG<bright-blue,bold>"),
Level::Warn => cstr!("<bright-yellow,bold>WARN<bright-yellow,bold>"),
Level::Error => cstr!("<bright-red,bold>ERROR<bright-red,bold>"),
};
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 lexer = Lexer::new(&source);
let mut compiler = match Compiler::new(lexer) { let mut compiler = match Compiler::new(lexer) {
Ok(compiler) => compiler, 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(); let mut stdout = stdout().lock();
chunk chunk
.disassembler(&mut stdout) .disassembler(&mut stdout)
.style(style) .style(style)
.source(&source) .source(&source)
.width(80) .width(65)
.disassemble() .disassemble()
.expect("Failed to write disassembly to stdout"); .expect("Failed to write disassembly to stdout");
return; 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 lexer = Lexer::new(&source);
let mut next_token = || -> Option<(Token, Span, bool)> { let mut next_token = || -> Option<(Token, Span, bool)> {
match lexer.next_token() { match lexer.next_token() {
@ -303,9 +295,13 @@ fn main() {
} }
if let Mode::Run { if let Mode::Run {
time, no_output, .. name,
time,
no_output,
input,
} = mode } = mode
{ {
let (source, file_name) = get_source_and_file_name(input);
let lexer = Lexer::new(&source); let lexer = Lexer::new(&source);
let mut compiler = match Compiler::new(lexer) { let mut compiler = match Compiler::new(lexer) {
Ok(compiler) => compiler, 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(); let compile_end = start_time.elapsed();
if time { if time {