2019-10-07 02:06:45 -07:00
|
|
|
use crate::common::*;
|
|
|
|
|
|
|
|
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches};
|
|
|
|
|
|
|
|
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
|
|
|
|
|
|
|
pub(crate) struct Config<'a> {
|
2019-10-07 04:04:39 -07:00
|
|
|
pub(crate) subcommand: Subcommand<'a>,
|
2019-10-07 02:06:45 -07:00
|
|
|
pub(crate) dry_run: bool,
|
|
|
|
pub(crate) highlight: bool,
|
|
|
|
pub(crate) overrides: BTreeMap<&'a str, &'a str>,
|
|
|
|
pub(crate) quiet: bool,
|
|
|
|
pub(crate) shell: &'a str,
|
|
|
|
pub(crate) color: Color,
|
|
|
|
pub(crate) verbosity: Verbosity,
|
|
|
|
pub(crate) arguments: Vec<&'a str>,
|
2019-10-09 00:18:53 -07:00
|
|
|
pub(crate) justfile: Option<&'a Path>,
|
|
|
|
pub(crate) working_directory: Option<&'a Path>,
|
|
|
|
pub(crate) invocation_directory: Result<PathBuf, String>,
|
2019-10-07 02:06:45 -07:00
|
|
|
}
|
|
|
|
|
2019-10-31 17:39:25 -07:00
|
|
|
mod cmd {
|
2019-10-07 04:04:39 -07:00
|
|
|
pub(crate) const DUMP: &str = "DUMP";
|
2019-10-09 00:18:53 -07:00
|
|
|
pub(crate) const EDIT: &str = "EDIT";
|
2019-10-31 17:39:25 -07:00
|
|
|
pub(crate) const EVALUATE: &str = "EVALUATE";
|
2019-10-07 04:04:39 -07:00
|
|
|
pub(crate) const LIST: &str = "LIST";
|
|
|
|
pub(crate) const SHOW: &str = "SHOW";
|
2019-10-09 00:18:53 -07:00
|
|
|
pub(crate) const SUMMARY: &str = "SUMMARY";
|
2019-10-31 17:39:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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";
|
2019-10-09 00:18:53 -07:00
|
|
|
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
|
|
|
|
|
|
|
|
pub(crate) const COLOR_ALWAYS: &str = "always";
|
2019-10-31 17:39:25 -07:00
|
|
|
pub(crate) const COLOR_AUTO: &str = "auto";
|
2019-10-09 00:18:53 -07:00
|
|
|
pub(crate) const COLOR_NEVER: &str = "never";
|
|
|
|
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
2019-10-07 04:04:39 -07:00
|
|
|
}
|
|
|
|
|
2019-10-07 02:06:45 -07:00
|
|
|
impl<'a> Config<'a> {
|
|
|
|
pub(crate) fn app() -> App<'static, 'static> {
|
|
|
|
let app = App::new(env!("CARGO_PKG_NAME"))
|
|
|
|
.help_message("Print help information")
|
|
|
|
.version_message("Print version information")
|
|
|
|
.setting(AppSettings::ColoredHelp)
|
|
|
|
.setting(AppSettings::TrailingVarArg)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::ARGUMENTS)
|
2019-10-07 02:06:45 -07:00
|
|
|
.multiple(true)
|
|
|
|
.help("The recipe(s) to run, defaults to the first recipe in the justfile"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-09 00:18:53 -07:00
|
|
|
Arg::with_name(arg::COLOR)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("color")
|
|
|
|
.takes_value(true)
|
2019-10-09 00:18:53 -07:00
|
|
|
.possible_values(arg::COLOR_VALUES)
|
|
|
|
.default_value(arg::COLOR_AUTO)
|
2019-10-07 02:06:45 -07:00
|
|
|
.help("Print colorful output"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::DRY_RUN)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("dry-run")
|
|
|
|
.help("Print what just would do without doing it")
|
2019-10-31 17:39:25 -07:00
|
|
|
.conflicts_with(arg::QUIET),
|
2019-10-07 02:06:45 -07:00
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::DUMP)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("dump")
|
|
|
|
.help("Print entire justfile"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::EDIT)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("e")
|
|
|
|
.long("edit")
|
|
|
|
.help("Open justfile with $EDITOR"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::EVALUATE)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("evaluate")
|
|
|
|
.help("Print evaluated variables"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::HIGHLIGHT)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("highlight")
|
|
|
|
.help("Highlight echoed recipe lines in bold"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::JUSTFILE)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("f")
|
|
|
|
.long("justfile")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("Use <JUSTFILE> as justfile."),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::LIST)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("l")
|
|
|
|
.long("list")
|
|
|
|
.help("List available recipes and their arguments"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::QUIET)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("q")
|
|
|
|
.long("quiet")
|
|
|
|
.help("Suppress all output")
|
2019-10-31 17:39:25 -07:00
|
|
|
.conflicts_with(arg::DRY_RUN),
|
2019-10-07 02:06:45 -07:00
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::SET)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("set")
|
|
|
|
.takes_value(true)
|
|
|
|
.number_of_values(2)
|
|
|
|
.value_names(&["VARIABLE", "VALUE"])
|
|
|
|
.multiple(true)
|
|
|
|
.help("Set <VARIABLE> to <VALUE>"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::SHELL)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("shell")
|
|
|
|
.takes_value(true)
|
|
|
|
.default_value(DEFAULT_SHELL)
|
|
|
|
.help("Invoke <SHELL> to run recipes"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::SHOW)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("s")
|
|
|
|
.long("show")
|
|
|
|
.takes_value(true)
|
|
|
|
.value_name("RECIPE")
|
|
|
|
.help("Show information about <RECIPE>"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(cmd::SUMMARY)
|
2019-10-07 02:06:45 -07:00
|
|
|
.long("summary")
|
|
|
|
.help("List names of available recipes"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-31 17:39:25 -07:00
|
|
|
Arg::with_name(arg::VERBOSE)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("v")
|
|
|
|
.long("verbose")
|
|
|
|
.multiple(true)
|
|
|
|
.help("Use verbose output"),
|
|
|
|
)
|
|
|
|
.arg(
|
2019-10-09 00:18:53 -07:00
|
|
|
Arg::with_name(arg::WORKING_DIRECTORY)
|
2019-10-07 02:06:45 -07:00
|
|
|
.short("d")
|
|
|
|
.long("working-directory")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
2019-10-31 17:39:25 -07:00
|
|
|
.requires(arg::JUSTFILE),
|
2019-10-07 02:06:45 -07:00
|
|
|
)
|
|
|
|
.group(ArgGroup::with_name("EARLY-EXIT").args(&[
|
2019-10-31 17:39:25 -07:00
|
|
|
arg::ARGUMENTS,
|
|
|
|
cmd::DUMP,
|
|
|
|
cmd::EDIT,
|
|
|
|
cmd::EVALUATE,
|
|
|
|
cmd::LIST,
|
|
|
|
cmd::SHOW,
|
|
|
|
cmd::SUMMARY,
|
2019-10-07 02:06:45 -07:00
|
|
|
]));
|
|
|
|
|
|
|
|
if cfg!(feature = "help4help2man") {
|
|
|
|
app.version(env!("CARGO_PKG_VERSION")).about(concat!(
|
|
|
|
"- Please see ",
|
|
|
|
env!("CARGO_PKG_HOMEPAGE"),
|
|
|
|
" for more information."
|
|
|
|
))
|
|
|
|
} else {
|
|
|
|
app
|
|
|
|
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
|
|
|
.author(env!("CARGO_PKG_AUTHORS"))
|
|
|
|
.about(concat!(
|
|
|
|
env!("CARGO_PKG_DESCRIPTION"),
|
|
|
|
" - ",
|
|
|
|
env!("CARGO_PKG_HOMEPAGE")
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-09 00:18:53 -07:00
|
|
|
fn color_from_value(value: &str) -> ConfigResult<Color> {
|
|
|
|
match value {
|
|
|
|
arg::COLOR_AUTO => Ok(Color::auto()),
|
|
|
|
arg::COLOR_ALWAYS => Ok(Color::always()),
|
|
|
|
arg::COLOR_NEVER => Ok(Color::never()),
|
|
|
|
_ => Err(ConfigError::Internal {
|
|
|
|
message: format!("Invalid argument `{}` to --color.", value),
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn from_matches(matches: &'a ArgMatches<'a>) -> ConfigResult<Config<'a>> {
|
|
|
|
let invocation_directory =
|
|
|
|
env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
|
|
|
|
|
2019-10-31 17:39:25 -07:00
|
|
|
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE));
|
2019-10-07 02:06:45 -07:00
|
|
|
|
2019-10-09 00:18:53 -07:00
|
|
|
let color = Self::color_from_value(
|
|
|
|
matches
|
|
|
|
.value_of(arg::COLOR)
|
|
|
|
.expect("`--color` had no value"),
|
|
|
|
)?;
|
2019-10-07 02:06:45 -07:00
|
|
|
|
2019-10-31 17:39:25 -07:00
|
|
|
let set_count = matches.occurrences_of(arg::SET);
|
2019-10-07 02:06:45 -07:00
|
|
|
let mut overrides = BTreeMap::new();
|
|
|
|
if set_count > 0 {
|
2019-10-31 17:39:25 -07:00
|
|
|
let mut values = matches.values_of(arg::SET).unwrap();
|
2019-10-07 02:06:45 -07:00
|
|
|
for _ in 0..set_count {
|
|
|
|
overrides.insert(values.next().unwrap(), values.next().unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_override(arg: &&str) -> bool {
|
|
|
|
arg.chars().skip(1).any(|c| c == '=')
|
|
|
|
}
|
|
|
|
|
|
|
|
let raw_arguments: Vec<&str> = matches
|
2019-10-31 17:39:25 -07:00
|
|
|
.values_of(arg::ARGUMENTS)
|
2019-10-07 02:06:45 -07:00
|
|
|
.map(Iterator::collect)
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
for argument in raw_arguments.iter().cloned().take_while(is_override) {
|
|
|
|
let i = argument
|
|
|
|
.char_indices()
|
|
|
|
.skip(1)
|
|
|
|
.find(|&(_, c)| c == '=')
|
|
|
|
.unwrap()
|
|
|
|
.0;
|
|
|
|
|
|
|
|
let name = &argument[..i];
|
|
|
|
let value = &argument[i + 1..];
|
|
|
|
|
|
|
|
overrides.insert(name, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
let arguments = raw_arguments
|
|
|
|
.into_iter()
|
|
|
|
.skip_while(is_override)
|
|
|
|
.enumerate()
|
|
|
|
.flat_map(|(i, argument)| {
|
|
|
|
if i == 0 {
|
|
|
|
if let Some(i) = argument.rfind('/') {
|
2019-10-09 00:18:53 -07:00
|
|
|
if matches.is_present(arg::WORKING_DIRECTORY) {
|
2019-10-07 02:06:45 -07:00
|
|
|
die!("--working-directory and a path prefixed recipe may not be used together.");
|
|
|
|
}
|
|
|
|
|
|
|
|
let (dir, recipe) = argument.split_at(i + 1);
|
|
|
|
|
|
|
|
if let Err(error) = env::set_current_dir(dir) {
|
|
|
|
die!("Error changing directory: {}", error);
|
|
|
|
}
|
|
|
|
|
|
|
|
if recipe.is_empty() {
|
|
|
|
return None;
|
|
|
|
} else {
|
|
|
|
return Some(recipe);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(argument)
|
|
|
|
})
|
|
|
|
.collect::<Vec<&str>>();
|
|
|
|
|
2019-10-31 17:39:25 -07:00
|
|
|
let subcommand = if matches.is_present(cmd::EDIT) {
|
2019-10-07 04:04:39 -07:00
|
|
|
Subcommand::Edit
|
2019-10-31 17:39:25 -07:00
|
|
|
} else if matches.is_present(cmd::SUMMARY) {
|
2019-10-07 04:04:39 -07:00
|
|
|
Subcommand::Summary
|
2019-10-31 17:39:25 -07:00
|
|
|
} else if matches.is_present(cmd::DUMP) {
|
2019-10-07 04:04:39 -07:00
|
|
|
Subcommand::Dump
|
2019-10-31 17:39:25 -07:00
|
|
|
} else if matches.is_present(cmd::LIST) {
|
2019-10-07 04:04:39 -07:00
|
|
|
Subcommand::List
|
2019-10-31 17:39:25 -07:00
|
|
|
} else if matches.is_present(cmd::EVALUATE) {
|
|
|
|
Subcommand::Evaluate
|
|
|
|
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
2019-10-07 04:04:39 -07:00
|
|
|
Subcommand::Show { name }
|
|
|
|
} else {
|
|
|
|
Subcommand::Run
|
|
|
|
};
|
|
|
|
|
2019-10-09 00:18:53 -07:00
|
|
|
Ok(Config {
|
2019-10-31 17:39:25 -07:00
|
|
|
dry_run: matches.is_present(arg::DRY_RUN),
|
|
|
|
highlight: matches.is_present(arg::HIGHLIGHT),
|
|
|
|
quiet: matches.is_present(arg::QUIET),
|
|
|
|
shell: matches.value_of(arg::SHELL).unwrap(),
|
|
|
|
justfile: matches.value_of(arg::JUSTFILE).map(Path::new),
|
|
|
|
working_directory: matches.value_of(arg::WORKING_DIRECTORY).map(Path::new),
|
2019-10-09 00:18:53 -07:00
|
|
|
invocation_directory,
|
2019-10-07 04:04:39 -07:00
|
|
|
subcommand,
|
2019-10-07 02:06:45 -07:00
|
|
|
verbosity,
|
|
|
|
color,
|
|
|
|
overrides,
|
|
|
|
arguments,
|
2019-10-09 00:18:53 -07:00
|
|
|
})
|
2019-10-07 02:06:45 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Default for Config<'a> {
|
|
|
|
fn default() -> Config<'static> {
|
|
|
|
Config {
|
2019-10-07 04:04:39 -07:00
|
|
|
subcommand: Subcommand::Run,
|
2019-10-07 02:06:45 -07:00
|
|
|
dry_run: false,
|
|
|
|
highlight: false,
|
|
|
|
overrides: empty(),
|
|
|
|
arguments: empty(),
|
|
|
|
quiet: false,
|
|
|
|
shell: DEFAULT_SHELL,
|
|
|
|
color: default(),
|
|
|
|
verbosity: Verbosity::from_flag_occurrences(0),
|
2019-10-09 00:18:53 -07:00
|
|
|
justfile: None,
|
|
|
|
working_directory: None,
|
|
|
|
invocation_directory: env::current_dir()
|
|
|
|
.map_err(|e| format!("Error getting current directory: {}", e)),
|
2019-10-07 02:06:45 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-10-31 17:39:25 -07:00
|
|
|
|
|
|
|
#[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() {
|
2019-10-31 19:19:01 -07:00
|
|
|
const EXPECTED_HELP: &str = "just v0.4.5
|
2019-10-31 17:39:25 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|