Use constants instead of literals in arg parser (#504)
- Differentiate between `arg`s, which are flags and options, and `cmd`s, which are mutually exclusive subcommands - Replace string literals, like "EVALUATE", with constants, like `cmd::EVALUATE`, since they're slightly less error prone. - Remove `Config::evaluate`, and handle it like other subcommands
This commit is contained in:
parent
c26c0e4ecc
commit
48b25c14b1
@ -11,7 +11,7 @@ pub(crate) use std::{
|
|||||||
ops::{Range, RangeInclusive},
|
ops::{Range, RangeInclusive},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{self, Command},
|
process::{self, Command},
|
||||||
str::Chars,
|
str::{self, Chars},
|
||||||
sync::{Mutex, MutexGuard},
|
sync::{Mutex, MutexGuard},
|
||||||
usize, vec,
|
usize, vec,
|
||||||
};
|
};
|
||||||
|
153
src/config.rs
153
src/config.rs
@ -7,7 +7,6 @@ pub(crate) const DEFAULT_SHELL: &str = "sh";
|
|||||||
pub(crate) struct Config<'a> {
|
pub(crate) struct Config<'a> {
|
||||||
pub(crate) subcommand: Subcommand<'a>,
|
pub(crate) subcommand: Subcommand<'a>,
|
||||||
pub(crate) dry_run: bool,
|
pub(crate) dry_run: bool,
|
||||||
pub(crate) evaluate: bool,
|
|
||||||
pub(crate) highlight: bool,
|
pub(crate) highlight: bool,
|
||||||
pub(crate) overrides: BTreeMap<&'a str, &'a str>,
|
pub(crate) overrides: BTreeMap<&'a str, &'a str>,
|
||||||
pub(crate) quiet: bool,
|
pub(crate) quiet: bool,
|
||||||
@ -20,17 +19,29 @@ pub(crate) struct Config<'a> {
|
|||||||
pub(crate) invocation_directory: Result<PathBuf, String>,
|
pub(crate) invocation_directory: Result<PathBuf, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod arg {
|
mod cmd {
|
||||||
pub(crate) const DUMP: &str = "DUMP";
|
pub(crate) const DUMP: &str = "DUMP";
|
||||||
pub(crate) const COLOR: &str = "COLOR";
|
|
||||||
pub(crate) const EDIT: &str = "EDIT";
|
pub(crate) const EDIT: &str = "EDIT";
|
||||||
|
pub(crate) const EVALUATE: &str = "EVALUATE";
|
||||||
pub(crate) const LIST: &str = "LIST";
|
pub(crate) const LIST: &str = "LIST";
|
||||||
pub(crate) const SHOW: &str = "SHOW";
|
pub(crate) const SHOW: &str = "SHOW";
|
||||||
pub(crate) const SUMMARY: &str = "SUMMARY";
|
pub(crate) const SUMMARY: &str = "SUMMARY";
|
||||||
|
}
|
||||||
|
|
||||||
|
mod arg {
|
||||||
|
pub(crate) const ARGUMENTS: &str = "ARGUMENTS";
|
||||||
|
pub(crate) const COLOR: &str = "COLOR";
|
||||||
|
pub(crate) const DRY_RUN: &str = "DRY-RUN";
|
||||||
|
pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT";
|
||||||
|
pub(crate) const JUSTFILE: &str = "JUSTFILE";
|
||||||
|
pub(crate) const QUIET: &str = "QUIET";
|
||||||
|
pub(crate) const SET: &str = "SET";
|
||||||
|
pub(crate) const SHELL: &str = "SHELL";
|
||||||
|
pub(crate) const VERBOSE: &str = "VERBOSE";
|
||||||
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
|
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
|
||||||
|
|
||||||
pub(crate) const COLOR_AUTO: &str = "auto";
|
|
||||||
pub(crate) const COLOR_ALWAYS: &str = "always";
|
pub(crate) const COLOR_ALWAYS: &str = "always";
|
||||||
|
pub(crate) const COLOR_AUTO: &str = "auto";
|
||||||
pub(crate) const COLOR_NEVER: &str = "never";
|
pub(crate) const COLOR_NEVER: &str = "never";
|
||||||
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
||||||
}
|
}
|
||||||
@ -43,7 +54,7 @@ impl<'a> Config<'a> {
|
|||||||
.setting(AppSettings::ColoredHelp)
|
.setting(AppSettings::ColoredHelp)
|
||||||
.setting(AppSettings::TrailingVarArg)
|
.setting(AppSettings::TrailingVarArg)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("ARGUMENTS")
|
Arg::with_name(arg::ARGUMENTS)
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("The recipe(s) to run, defaults to the first recipe in the justfile"),
|
.help("The recipe(s) to run, defaults to the first recipe in the justfile"),
|
||||||
)
|
)
|
||||||
@ -56,54 +67,54 @@ impl<'a> Config<'a> {
|
|||||||
.help("Print colorful output"),
|
.help("Print colorful output"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("DRY-RUN")
|
Arg::with_name(arg::DRY_RUN)
|
||||||
.long("dry-run")
|
.long("dry-run")
|
||||||
.help("Print what just would do without doing it")
|
.help("Print what just would do without doing it")
|
||||||
.conflicts_with("QUIET"),
|
.conflicts_with(arg::QUIET),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::DUMP)
|
Arg::with_name(cmd::DUMP)
|
||||||
.long("dump")
|
.long("dump")
|
||||||
.help("Print entire justfile"),
|
.help("Print entire justfile"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::EDIT)
|
Arg::with_name(cmd::EDIT)
|
||||||
.short("e")
|
.short("e")
|
||||||
.long("edit")
|
.long("edit")
|
||||||
.help("Open justfile with $EDITOR"),
|
.help("Open justfile with $EDITOR"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("EVALUATE")
|
Arg::with_name(cmd::EVALUATE)
|
||||||
.long("evaluate")
|
.long("evaluate")
|
||||||
.help("Print evaluated variables"),
|
.help("Print evaluated variables"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("HIGHLIGHT")
|
Arg::with_name(arg::HIGHLIGHT)
|
||||||
.long("highlight")
|
.long("highlight")
|
||||||
.help("Highlight echoed recipe lines in bold"),
|
.help("Highlight echoed recipe lines in bold"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("JUSTFILE")
|
Arg::with_name(arg::JUSTFILE)
|
||||||
.short("f")
|
.short("f")
|
||||||
.long("justfile")
|
.long("justfile")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Use <JUSTFILE> as justfile."),
|
.help("Use <JUSTFILE> as justfile."),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::LIST)
|
Arg::with_name(cmd::LIST)
|
||||||
.short("l")
|
.short("l")
|
||||||
.long("list")
|
.long("list")
|
||||||
.help("List available recipes and their arguments"),
|
.help("List available recipes and their arguments"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("QUIET")
|
Arg::with_name(arg::QUIET)
|
||||||
.short("q")
|
.short("q")
|
||||||
.long("quiet")
|
.long("quiet")
|
||||||
.help("Suppress all output")
|
.help("Suppress all output")
|
||||||
.conflicts_with("DRY-RUN"),
|
.conflicts_with(arg::DRY_RUN),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("SET")
|
Arg::with_name(arg::SET)
|
||||||
.long("set")
|
.long("set")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.number_of_values(2)
|
.number_of_values(2)
|
||||||
@ -112,14 +123,14 @@ impl<'a> Config<'a> {
|
|||||||
.help("Set <VARIABLE> to <VALUE>"),
|
.help("Set <VARIABLE> to <VALUE>"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("SHELL")
|
Arg::with_name(arg::SHELL)
|
||||||
.long("shell")
|
.long("shell")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.default_value(DEFAULT_SHELL)
|
.default_value(DEFAULT_SHELL)
|
||||||
.help("Invoke <SHELL> to run recipes"),
|
.help("Invoke <SHELL> to run recipes"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::SHOW)
|
Arg::with_name(cmd::SHOW)
|
||||||
.short("s")
|
.short("s")
|
||||||
.long("show")
|
.long("show")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
@ -127,12 +138,12 @@ impl<'a> Config<'a> {
|
|||||||
.help("Show information about <RECIPE>"),
|
.help("Show information about <RECIPE>"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::SUMMARY)
|
Arg::with_name(cmd::SUMMARY)
|
||||||
.long("summary")
|
.long("summary")
|
||||||
.help("List names of available recipes"),
|
.help("List names of available recipes"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("VERBOSE")
|
Arg::with_name(arg::VERBOSE)
|
||||||
.short("v")
|
.short("v")
|
||||||
.long("verbose")
|
.long("verbose")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
@ -144,16 +155,16 @@ impl<'a> Config<'a> {
|
|||||||
.long("working-directory")
|
.long("working-directory")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
||||||
.requires("JUSTFILE"),
|
.requires(arg::JUSTFILE),
|
||||||
)
|
)
|
||||||
.group(ArgGroup::with_name("EARLY-EXIT").args(&[
|
.group(ArgGroup::with_name("EARLY-EXIT").args(&[
|
||||||
arg::DUMP,
|
arg::ARGUMENTS,
|
||||||
arg::EDIT,
|
cmd::DUMP,
|
||||||
arg::LIST,
|
cmd::EDIT,
|
||||||
arg::SHOW,
|
cmd::EVALUATE,
|
||||||
arg::SUMMARY,
|
cmd::LIST,
|
||||||
"ARGUMENTS",
|
cmd::SHOW,
|
||||||
"EVALUATE",
|
cmd::SUMMARY,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
if cfg!(feature = "help4help2man") {
|
if cfg!(feature = "help4help2man") {
|
||||||
@ -189,7 +200,7 @@ impl<'a> Config<'a> {
|
|||||||
let invocation_directory =
|
let invocation_directory =
|
||||||
env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
|
env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
|
||||||
|
|
||||||
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of("VERBOSE"));
|
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE));
|
||||||
|
|
||||||
let color = Self::color_from_value(
|
let color = Self::color_from_value(
|
||||||
matches
|
matches
|
||||||
@ -197,10 +208,10 @@ impl<'a> Config<'a> {
|
|||||||
.expect("`--color` had no value"),
|
.expect("`--color` had no value"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let set_count = matches.occurrences_of("SET");
|
let set_count = matches.occurrences_of(arg::SET);
|
||||||
let mut overrides = BTreeMap::new();
|
let mut overrides = BTreeMap::new();
|
||||||
if set_count > 0 {
|
if set_count > 0 {
|
||||||
let mut values = matches.values_of("SET").unwrap();
|
let mut values = matches.values_of(arg::SET).unwrap();
|
||||||
for _ in 0..set_count {
|
for _ in 0..set_count {
|
||||||
overrides.insert(values.next().unwrap(), values.next().unwrap());
|
overrides.insert(values.next().unwrap(), values.next().unwrap());
|
||||||
}
|
}
|
||||||
@ -211,7 +222,7 @@ impl<'a> Config<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let raw_arguments: Vec<&str> = matches
|
let raw_arguments: Vec<&str> = matches
|
||||||
.values_of("ARGUMENTS")
|
.values_of(arg::ARGUMENTS)
|
||||||
.map(Iterator::collect)
|
.map(Iterator::collect)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@ -258,28 +269,29 @@ impl<'a> Config<'a> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<&str>>();
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
let subcommand = if matches.is_present(arg::EDIT) {
|
let subcommand = if matches.is_present(cmd::EDIT) {
|
||||||
Subcommand::Edit
|
Subcommand::Edit
|
||||||
} else if matches.is_present(arg::SUMMARY) {
|
} else if matches.is_present(cmd::SUMMARY) {
|
||||||
Subcommand::Summary
|
Subcommand::Summary
|
||||||
} else if matches.is_present(arg::DUMP) {
|
} else if matches.is_present(cmd::DUMP) {
|
||||||
Subcommand::Dump
|
Subcommand::Dump
|
||||||
} else if matches.is_present(arg::LIST) {
|
} else if matches.is_present(cmd::LIST) {
|
||||||
Subcommand::List
|
Subcommand::List
|
||||||
} else if let Some(name) = matches.value_of(arg::SHOW) {
|
} else if matches.is_present(cmd::EVALUATE) {
|
||||||
|
Subcommand::Evaluate
|
||||||
|
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
||||||
Subcommand::Show { name }
|
Subcommand::Show { name }
|
||||||
} else {
|
} else {
|
||||||
Subcommand::Run
|
Subcommand::Run
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
dry_run: matches.is_present("DRY-RUN"),
|
dry_run: matches.is_present(arg::DRY_RUN),
|
||||||
evaluate: matches.is_present("EVALUATE"),
|
highlight: matches.is_present(arg::HIGHLIGHT),
|
||||||
highlight: matches.is_present("HIGHLIGHT"),
|
quiet: matches.is_present(arg::QUIET),
|
||||||
quiet: matches.is_present("QUIET"),
|
shell: matches.value_of(arg::SHELL).unwrap(),
|
||||||
shell: matches.value_of("SHELL").unwrap(),
|
justfile: matches.value_of(arg::JUSTFILE).map(Path::new),
|
||||||
justfile: matches.value_of("JUSTFILE").map(Path::new),
|
working_directory: matches.value_of(arg::WORKING_DIRECTORY).map(Path::new),
|
||||||
working_directory: matches.value_of("WORKING-DIRECTORY").map(Path::new),
|
|
||||||
invocation_directory,
|
invocation_directory,
|
||||||
subcommand,
|
subcommand,
|
||||||
verbosity,
|
verbosity,
|
||||||
@ -295,7 +307,6 @@ impl<'a> Default for Config<'a> {
|
|||||||
Config {
|
Config {
|
||||||
subcommand: Subcommand::Run,
|
subcommand: Subcommand::Run,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
evaluate: false,
|
|
||||||
highlight: false,
|
highlight: false,
|
||||||
overrides: empty(),
|
overrides: empty(),
|
||||||
arguments: empty(),
|
arguments: empty(),
|
||||||
@ -310,3 +321,55 @@ impl<'a> Default for Config<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
// This test guards against unintended changes to the argument parser. We should have
|
||||||
|
// proper tests for all the flags, but this will do for now.
|
||||||
|
#[test]
|
||||||
|
fn help() {
|
||||||
|
const EXPECTED_HELP: &str = "just v0.4.4
|
||||||
|
Casey Rodarmor <casey@rodarmor.com>
|
||||||
|
🤖 Just a command runner - https://github.com/casey/just
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
just [FLAGS] [OPTIONS] [--] [ARGUMENTS]...
|
||||||
|
|
||||||
|
FLAGS:
|
||||||
|
--dry-run Print what just would do without doing it
|
||||||
|
--dump Print entire justfile
|
||||||
|
-e, --edit Open justfile with $EDITOR
|
||||||
|
--evaluate Print evaluated variables
|
||||||
|
--highlight Highlight echoed recipe lines in bold
|
||||||
|
-l, --list List available recipes and their arguments
|
||||||
|
-q, --quiet Suppress all output
|
||||||
|
--summary List names of available recipes
|
||||||
|
-v, --verbose Use verbose output
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--color <COLOR>
|
||||||
|
Print colorful output [default: auto] [possible values: auto, always, never]
|
||||||
|
|
||||||
|
-f, --justfile <JUSTFILE> Use <JUSTFILE> as justfile.
|
||||||
|
--set <VARIABLE> <VALUE> Set <VARIABLE> to <VALUE>
|
||||||
|
--shell <SHELL> Invoke <SHELL> to run recipes [default: sh]
|
||||||
|
-s, --show <RECIPE> Show information about <RECIPE>
|
||||||
|
-d, --working-directory <WORKING-DIRECTORY>
|
||||||
|
Use <WORKING-DIRECTORY> as working directory. --justfile must also be set
|
||||||
|
|
||||||
|
|
||||||
|
ARGS:
|
||||||
|
<ARGUMENTS>... The recipe(s) to run, defaults to the first recipe in the justfile";
|
||||||
|
|
||||||
|
let app = Config::app().setting(AppSettings::ColorNever);
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
app.write_help(&mut buffer).unwrap();
|
||||||
|
let help = str::from_utf8(&buffer).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(help, EXPECTED_HELP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -69,7 +69,7 @@ impl<'a> Justfile<'a> {
|
|||||||
config.dry_run,
|
config.dry_run,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if config.evaluate {
|
if config.subcommand == Subcommand::Evaluate {
|
||||||
let mut width = 0;
|
let mut width = 0;
|
||||||
for name in scope.keys() {
|
for name in scope.keys() {
|
||||||
width = cmp::max(name.len(), width);
|
width = cmp::max(name.len(), width);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub(crate) enum Subcommand<'a> {
|
pub(crate) enum Subcommand<'a> {
|
||||||
Edit,
|
|
||||||
Summary,
|
|
||||||
Dump,
|
Dump,
|
||||||
|
Edit,
|
||||||
|
Evaluate,
|
||||||
List,
|
List,
|
||||||
Show { name: &'a str },
|
|
||||||
Run,
|
Run,
|
||||||
|
Show { name: &'a str },
|
||||||
|
Summary,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user