Clean up the README and style the CLI
This commit is contained in:
parent
46060a473d
commit
ed05a981e7
@ -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:
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user