Add the --choose
subcommand (#680)
The `--choose` subcommand runs a chooser to select a recipe to run. The chooser should read lines containing recipe names from standard input, and write one of those names to standard output. The chooser defaults to `fzf`, a popular fuzzy finder, but can be overridden by setting $JUST_CHOOSER or passing `--chooser <CHOOSER>`.
This commit is contained in:
parent
55985aa242
commit
9d0246998d
12
.github/workflows/build.yaml
vendored
12
.github/workflows/build.yaml
vendored
@ -10,6 +10,12 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
# Increment to invalidate github actions caches if they become corrupt.
|
||||
# Errors of the form "can't find crate for `snafu_derive` which `snafu` depends on"
|
||||
# can usually be fixed by incrementing this value.
|
||||
CACHE_KEY_PREFIX: 1
|
||||
|
||||
jobs:
|
||||
all:
|
||||
name: All
|
||||
@ -41,19 +47,19 @@ jobs:
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: 0-${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: 0-${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: target
|
||||
key: 0-${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install Main Toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
37
README.adoc
37
README.adoc
@ -266,6 +266,14 @@ $ just --summary --unsorted
|
||||
test build
|
||||
```
|
||||
|
||||
If you'd like `just` to default to listing the recipes in the justfile, you can
|
||||
use this as your default recipe:
|
||||
|
||||
```make
|
||||
default:
|
||||
@just --list
|
||||
```
|
||||
|
||||
=== Aliases
|
||||
|
||||
Aliases allow recipes to be invoked with alternative names:
|
||||
@ -958,6 +966,35 @@ echo 'Bar!'
|
||||
Bar!
|
||||
```
|
||||
|
||||
=== Selecting a Recipe to Run With an Interactive Chooser
|
||||
|
||||
The `--choose` subcommand makes just invoke a chooser to select which recipe to
|
||||
run. Choosers should read lines containing recipe names from standard input and
|
||||
print one of those names to standard output.
|
||||
|
||||
Because there is currenly no way to run a recipe that requires arguments with
|
||||
`--choose`, such recipes will not be given to the chooser. Private recipes and
|
||||
aliases are also skipped.
|
||||
|
||||
The chooser can be overridden with the `--chooser` flag. If `--chooser` is not
|
||||
given, then `just` first checks if `$JUST_CHOOSER` is set. If it isn't, then
|
||||
the chooser defaults to `fzf`, a popular fuzzy finder.
|
||||
|
||||
Arguments can be included in the chooser, i.e. `fzf --exact`.
|
||||
|
||||
The chooser is invoked in the same way as recipe lines. For example, if the
|
||||
chooser is `fzf`, it will be invoked with `sh -cu 'fzf'`, and if the shell, or
|
||||
the shell arguments are overridden, the chooser invocation will respect those
|
||||
overrides.
|
||||
|
||||
If you'd like `just` to default to selecting a recipe with a chooser, you can
|
||||
use this as your default recipe:
|
||||
|
||||
```make
|
||||
default:
|
||||
@just --choose
|
||||
```
|
||||
|
||||
=== Invoking Justfiles in Other Directories
|
||||
|
||||
If the first argument passed to `just` contains a `/`, then the following occurs:
|
||||
|
@ -20,13 +20,17 @@ _just() {
|
||||
|
||||
case "${cmd}" in
|
||||
just)
|
||||
opts=" -q -u -v -e -l -h -V -f -d -s --dry-run --highlight --no-dotenv --no-highlight --quiet --clear-shell-args --unsorted --verbose --dump --edit --evaluate --init --list --summary --variables --help --version --color --justfile --set --shell --shell-arg --working-directory --completions --show <ARGUMENTS>... "
|
||||
opts=" -q -u -v -e -l -h -V -f -d -s --dry-run --highlight --no-dotenv --no-highlight --quiet --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --init --list --summary --variables --help --version --chooser --color --justfile --set --shell --shell-arg --working-directory --completions --show <ARGUMENTS>... "
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
fi
|
||||
case "${prev}" in
|
||||
|
||||
--chooser)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
|
||||
return 0
|
||||
|
@ -14,6 +14,7 @@ edit:completion:arg-completer[just] = [@words]{
|
||||
}
|
||||
completions = [
|
||||
&'just'= {
|
||||
cand --chooser 'Override binary invoked by `--choose`'
|
||||
cand --color 'Print colorful output'
|
||||
cand -f 'Use <JUSTFILE> as justfile.'
|
||||
cand --justfile 'Use <JUSTFILE> as justfile.'
|
||||
@ -36,6 +37,7 @@ edit:completion:arg-completer[just] = [@words]{
|
||||
cand --unsorted 'Return list and summary entries in source order'
|
||||
cand -v 'Use verbose output'
|
||||
cand --verbose 'Use verbose output'
|
||||
cand --choose 'Select a recipe to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'
|
||||
cand --dump 'Print entire justfile'
|
||||
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
|
||||
cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
|
||||
|
@ -9,6 +9,7 @@ complete -c just -n "__fish_is_first_arg" --no-files
|
||||
complete -c just -a '(__fish_just_complete_recipes)'
|
||||
|
||||
# autogenerated completions
|
||||
complete -c just -n "__fish_use_subcommand" -l chooser -d 'Override binary invoked by `--choose`'
|
||||
complete -c just -n "__fish_use_subcommand" -l color -d 'Print colorful output' -r -f -a "auto always never"
|
||||
complete -c just -n "__fish_use_subcommand" -s f -l justfile -d 'Use <JUSTFILE> as justfile.'
|
||||
complete -c just -n "__fish_use_subcommand" -l set -d 'Override <VARIABLE> with <VALUE>'
|
||||
@ -25,6 +26,7 @@ complete -c just -n "__fish_use_subcommand" -s q -l quiet -d 'Suppress all outpu
|
||||
complete -c just -n "__fish_use_subcommand" -l clear-shell-args -d 'Clear shell arguments'
|
||||
complete -c just -n "__fish_use_subcommand" -s u -l unsorted -d 'Return list and summary entries in source order'
|
||||
complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output'
|
||||
complete -c just -n "__fish_use_subcommand" -l choose -d 'Select a recipe to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'
|
||||
complete -c just -n "__fish_use_subcommand" -l dump -d 'Print entire justfile'
|
||||
complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
|
||||
complete -c just -n "__fish_use_subcommand" -l evaluate -d 'Print evaluated variables'
|
||||
|
@ -19,6 +19,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
|
||||
|
||||
$completions = @(switch ($command) {
|
||||
'just' {
|
||||
[CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')
|
||||
[CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
|
||||
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
|
||||
[CompletionResult]::new('--justfile', 'justfile', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
|
||||
@ -41,6 +42,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
|
||||
[CompletionResult]::new('--unsorted', 'unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
|
||||
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output')
|
||||
[CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Use verbose output')
|
||||
[CompletionResult]::new('--choose', 'choose', [CompletionResultType]::ParameterName, 'Select a recipe to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`')
|
||||
[CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print entire justfile')
|
||||
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
|
||||
[CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
|
||||
|
@ -15,6 +15,7 @@ _just() {
|
||||
|
||||
local context curcontext="$curcontext" state line
|
||||
local common=(
|
||||
'--chooser=[Override binary invoked by `--choose`]' \
|
||||
'--color=[Print colorful output]: :(auto always never)' \
|
||||
'-f+[Use <JUSTFILE> as justfile.]' \
|
||||
'--justfile=[Use <JUSTFILE> as justfile.]' \
|
||||
@ -37,6 +38,7 @@ _just() {
|
||||
'--unsorted[Return list and summary entries in source order]' \
|
||||
'*-v[Use verbose output]' \
|
||||
'*--verbose[Use verbose output]' \
|
||||
'--choose[Select a recipe to run using a binary. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`]' \
|
||||
'--dump[Print entire justfile]' \
|
||||
'-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
|
||||
'--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
|
||||
|
@ -4,13 +4,14 @@ pub(crate) use std::{
|
||||
cmp,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
env,
|
||||
ffi::OsString,
|
||||
fmt::{self, Debug, Display, Formatter},
|
||||
fs,
|
||||
io::{self, Cursor, Write},
|
||||
iter::{self, FromIterator},
|
||||
ops::{Index, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
process::{self, Command, Stdio},
|
||||
rc::Rc,
|
||||
str::{self, Chars},
|
||||
sync::{Mutex, MutexGuard},
|
||||
|
168
src/config.rs
168
src/config.rs
@ -2,6 +2,13 @@ use crate::common::*;
|
||||
|
||||
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, ArgSettings};
|
||||
|
||||
// These three strings should be kept in sync:
|
||||
pub(crate) const CHOOSER_DEFAULT: &str = "fzf";
|
||||
pub(crate) const CHOOSER_ENVIRONMENT_KEY: &str = "JUST_CHOOSER";
|
||||
pub(crate) const CHOOSE_HELP: &str = "Select a recipe to run using a binary. If `--chooser` is \
|
||||
not passed the chooser defaults to the value of \
|
||||
$JUST_CHOOSER, falling back to `fzf`";
|
||||
|
||||
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
||||
pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu";
|
||||
pub(crate) const INIT_JUSTFILE: &str = "default:\n\techo 'Hello, world!'\n";
|
||||
@ -24,6 +31,7 @@ pub(crate) struct Config {
|
||||
}
|
||||
|
||||
mod cmd {
|
||||
pub(crate) const CHOOSE: &str = "CHOOSE";
|
||||
pub(crate) const COMPLETIONS: &str = "COMPLETIONS";
|
||||
pub(crate) const DUMP: &str = "DUMP";
|
||||
pub(crate) const EDIT: &str = "EDIT";
|
||||
@ -35,6 +43,7 @@ mod cmd {
|
||||
pub(crate) const VARIABLES: &str = "VARIABLES";
|
||||
|
||||
pub(crate) const ALL: &[&str] = &[
|
||||
CHOOSE,
|
||||
COMPLETIONS,
|
||||
DUMP,
|
||||
EDIT,
|
||||
@ -60,6 +69,7 @@ mod cmd {
|
||||
|
||||
mod arg {
|
||||
pub(crate) const ARGUMENTS: &str = "ARGUMENTS";
|
||||
pub(crate) const CHOOSER: &str = "CHOOSER";
|
||||
pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS";
|
||||
pub(crate) const COLOR: &str = "COLOR";
|
||||
pub(crate) const DRY_RUN: &str = "DRY-RUN";
|
||||
@ -88,6 +98,12 @@ impl Config {
|
||||
.version_message("Print version information")
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.setting(AppSettings::TrailingVarArg)
|
||||
.arg(
|
||||
Arg::with_name(arg::CHOOSER)
|
||||
.long("chooser")
|
||||
.takes_value(true)
|
||||
.help("Override binary invoked by `--choose`"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(arg::COLOR)
|
||||
.long("color")
|
||||
@ -192,6 +208,7 @@ impl Config {
|
||||
.multiple(true)
|
||||
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
|
||||
)
|
||||
.arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP))
|
||||
.arg(
|
||||
Arg::with_name(cmd::COMPLETIONS)
|
||||
.long("completions")
|
||||
@ -359,7 +376,12 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
let subcommand = if let Some(shell) = matches.value_of(cmd::COMPLETIONS) {
|
||||
let subcommand = if matches.is_present(cmd::CHOOSE) {
|
||||
Subcommand::Choose {
|
||||
chooser: matches.value_of(arg::CHOOSER).map(str::to_owned),
|
||||
overrides,
|
||||
}
|
||||
} else if let Some(shell) = matches.value_of(cmd::COMPLETIONS) {
|
||||
Subcommand::Completions {
|
||||
shell: shell.to_owned(),
|
||||
}
|
||||
@ -461,8 +483,10 @@ impl Config {
|
||||
}
|
||||
|
||||
match &self.subcommand {
|
||||
Choose { overrides, chooser } =>
|
||||
self.choose(justfile, &search, overrides, chooser.as_deref()),
|
||||
Dump => Self::dump(justfile),
|
||||
Evaluate { overrides } => self.run(justfile, &search, overrides, &Vec::new()),
|
||||
Evaluate { overrides } => self.run(justfile, &search, overrides, &[]),
|
||||
List => self.list(justfile),
|
||||
Run {
|
||||
arguments,
|
||||
@ -475,6 +499,93 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn choose(
|
||||
&self,
|
||||
justfile: Justfile,
|
||||
search: &Search,
|
||||
overrides: &BTreeMap<String, String>,
|
||||
chooser: Option<&str>,
|
||||
) -> Result<(), i32> {
|
||||
let recipes = justfile
|
||||
.public_recipes(self.unsorted)
|
||||
.iter()
|
||||
.filter(|recipe| recipe.min_arguments() == 0)
|
||||
.cloned()
|
||||
.collect::<Vec<&Recipe<Dependency>>>();
|
||||
|
||||
if recipes.is_empty() {
|
||||
eprintln!("Justfile contains no choosable recipes.");
|
||||
return Err(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
let chooser = chooser
|
||||
.map(OsString::from)
|
||||
.or_else(|| env::var_os(CHOOSER_ENVIRONMENT_KEY))
|
||||
.unwrap_or_else(|| OsString::from(CHOOSER_DEFAULT));
|
||||
|
||||
let result = justfile
|
||||
.settings
|
||||
.shell_command(self)
|
||||
.arg(&chooser)
|
||||
.current_dir(&search.working_directory)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn();
|
||||
|
||||
let mut child = match result {
|
||||
Ok(child) => child,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Chooser `{}` invocation failed: {}",
|
||||
chooser.to_string_lossy(),
|
||||
error
|
||||
);
|
||||
return Err(EXIT_FAILURE);
|
||||
},
|
||||
};
|
||||
|
||||
for recipe in recipes {
|
||||
if let Err(error) = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.expect("Child was created with piped stdio")
|
||||
.write_all(format!("{}\n", recipe.name).as_bytes())
|
||||
{
|
||||
eprintln!(
|
||||
"Failed to write to chooser `{}`: {}",
|
||||
chooser.to_string_lossy(),
|
||||
error
|
||||
);
|
||||
return Err(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
let output = match child.wait_with_output() {
|
||||
Ok(output) => output,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"Failed to read output from chooser `{}`: {}",
|
||||
chooser.to_string_lossy(),
|
||||
error
|
||||
);
|
||||
return Err(EXIT_FAILURE);
|
||||
},
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Chooser `{}` returned error: {}",
|
||||
chooser.to_string_lossy(),
|
||||
output.status
|
||||
);
|
||||
return Err(output.status.code().unwrap_or(EXIT_FAILURE));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
self.run(justfile, search, overrides, &[stdout.trim().to_string()])
|
||||
}
|
||||
|
||||
fn dump(justfile: Justfile) -> Result<(), i32> {
|
||||
println!("{}", justfile);
|
||||
Ok(())
|
||||
@ -570,13 +681,9 @@ impl Config {
|
||||
let doc_color = self.color.stdout().doc();
|
||||
println!("Available recipes:");
|
||||
|
||||
for recipe in justfile.recipes(self.unsorted) {
|
||||
for recipe in justfile.public_recipes(self.unsorted) {
|
||||
let name = recipe.name();
|
||||
|
||||
if recipe.private {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (i, name) in iter::once(&name)
|
||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||
.enumerate()
|
||||
@ -662,9 +769,8 @@ impl Config {
|
||||
eprintln!("Justfile contains no recipes.");
|
||||
} else {
|
||||
let summary = justfile
|
||||
.recipes(self.unsorted)
|
||||
.public_recipes(self.unsorted)
|
||||
.iter()
|
||||
.filter(|recipe| recipe.public())
|
||||
.map(|recipe| recipe.name())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
@ -704,6 +810,9 @@ USAGE:
|
||||
just [FLAGS] [OPTIONS] [--] [ARGUMENTS]...
|
||||
|
||||
FLAGS:
|
||||
--choose Select a recipe to run using a binary. If `--chooser` is not passed \
|
||||
the chooser defaults
|
||||
to the value of $JUST_CHOOSER, falling back to `fzf`
|
||||
--clear-shell-args Clear shell arguments
|
||||
--dry-run Print what just would do without doing it
|
||||
--dump Print entire justfile
|
||||
@ -722,6 +831,7 @@ FLAGS:
|
||||
-v, --verbose Use verbose output
|
||||
|
||||
OPTIONS:
|
||||
--chooser <CHOOSER> Override binary invoked by `--choose`
|
||||
--color <COLOR>
|
||||
Print colorful output [default: auto] [possible values: auto, always, never]
|
||||
|
||||
@ -1089,6 +1199,46 @@ ARGS:
|
||||
},
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_summary,
|
||||
args: ["--list", "--summary"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_dump,
|
||||
args: ["--list", "--dump"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_init,
|
||||
args: ["--list", "--init"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_evaluate,
|
||||
args: ["--list", "--evaluate"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_show,
|
||||
args: ["--list", "--show"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_completions,
|
||||
args: ["--list", "--completions"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_variables,
|
||||
args: ["--list", "--variables"],
|
||||
}
|
||||
|
||||
error! {
|
||||
name: subcommand_conflict_choose,
|
||||
args: ["--list", "--choose"],
|
||||
}
|
||||
|
||||
test! {
|
||||
name: subcommand_completions,
|
||||
args: ["--completions", "bash"],
|
||||
|
@ -263,11 +263,12 @@ impl<'src> Justfile<'src> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn recipes(&self, source_order: bool) -> Vec<&Recipe<Dependency>> {
|
||||
pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<Dependency>> {
|
||||
let mut recipes = self
|
||||
.recipes
|
||||
.values()
|
||||
.map(AsRef::as_ref)
|
||||
.filter(|recipe| recipe.public())
|
||||
.collect::<Vec<&Recipe<Dependency>>>();
|
||||
|
||||
if source_order {
|
||||
|
@ -2,6 +2,10 @@ use crate::common::*;
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub(crate) enum Subcommand {
|
||||
Choose {
|
||||
overrides: BTreeMap<String, String>,
|
||||
chooser: Option<String>,
|
||||
},
|
||||
Completions {
|
||||
shell: String,
|
||||
},
|
||||
|
130
tests/choose.rs
Normal file
130
tests/choose.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use crate::common::*;
|
||||
|
||||
test! {
|
||||
name: env,
|
||||
justfile: "
|
||||
foo:
|
||||
echo foo
|
||||
|
||||
bar:
|
||||
echo bar
|
||||
",
|
||||
args: ("--choose"),
|
||||
env: {
|
||||
"JUST_CHOOSER": "head -n1",
|
||||
},
|
||||
stdout: "bar\n",
|
||||
stderr: "echo bar\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: chooser,
|
||||
justfile: "
|
||||
foo:
|
||||
echo foo
|
||||
|
||||
bar:
|
||||
echo bar
|
||||
",
|
||||
args: ("--choose", "--chooser", "head -n1"),
|
||||
stdout: "bar\n",
|
||||
stderr: "echo bar\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: override_variable,
|
||||
justfile: "
|
||||
baz := 'A'
|
||||
|
||||
foo:
|
||||
echo foo
|
||||
|
||||
bar:
|
||||
echo {{baz}}
|
||||
",
|
||||
args: ("--choose", "baz=B"),
|
||||
env: {
|
||||
"JUST_CHOOSER": "head -n1",
|
||||
},
|
||||
stdout: "B\n",
|
||||
stderr: "echo B\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: skip_private_recipes,
|
||||
justfile: "
|
||||
foo:
|
||||
echo foo
|
||||
|
||||
_bar:
|
||||
echo bar
|
||||
",
|
||||
args: ("--choose"),
|
||||
env: {
|
||||
"JUST_CHOOSER": "head -n1",
|
||||
},
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: skip_recipes_that_require_arguments,
|
||||
justfile: "
|
||||
foo:
|
||||
echo foo
|
||||
|
||||
bar BAR:
|
||||
echo {{BAR}}
|
||||
",
|
||||
args: ("--choose"),
|
||||
env: {
|
||||
"JUST_CHOOSER": "head -n1",
|
||||
},
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: no_choosable_recipes,
|
||||
justfile: "
|
||||
_foo:
|
||||
echo foo
|
||||
|
||||
bar BAR:
|
||||
echo {{BAR}}
|
||||
",
|
||||
args: ("--choose"),
|
||||
stdout: "",
|
||||
stderr: "Justfile contains no choosable recipes.\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let tmp = tmptree! {
|
||||
justfile: "foo:\n echo foo\n",
|
||||
};
|
||||
|
||||
let cat = which("cat").unwrap();
|
||||
let fzf = tmp.path().join(format!("fzf{}", env::consts::EXE_SUFFIX));
|
||||
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(cat, fzf).unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_file(cat, fzf).unwrap();
|
||||
|
||||
let path = env::join_paths(
|
||||
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(executable_path("just"))
|
||||
.arg("--choose")
|
||||
.current_dir(tmp.path())
|
||||
.env("PATH", path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert_stdout(&output, "foo\n");
|
||||
}
|
14
tests/common.rs
Normal file
14
tests/common.rs
Normal file
@ -0,0 +1,14 @@
|
||||
pub(crate) use std::{
|
||||
collections::BTreeMap,
|
||||
env, fs,
|
||||
io::Write,
|
||||
iter,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
str,
|
||||
};
|
||||
|
||||
pub(crate) use executable_path::executable_path;
|
||||
pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||
pub(crate) use test_utilities::{assert_stdout, tempdir, tmptree, unindent};
|
||||
pub(crate) use which::which;
|
@ -1,9 +1,4 @@
|
||||
use std::{env, iter, process::Command, str};
|
||||
|
||||
use executable_path::executable_path;
|
||||
use which::which;
|
||||
|
||||
use test_utilities::{assert_stdout, tmptree};
|
||||
use crate::common::*;
|
||||
|
||||
const JUSTFILE: &str = "Yooooooo, hopefully this never becomes valid syntax.";
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
#[macro_use]
|
||||
mod test;
|
||||
|
||||
mod common;
|
||||
|
||||
mod choose;
|
||||
mod completions;
|
||||
mod dotenv;
|
||||
mod edit;
|
||||
|
183
tests/misc.rs
183
tests/misc.rs
@ -1,185 +1,4 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, fs,
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
str,
|
||||
};
|
||||
|
||||
use executable_path::executable_path;
|
||||
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_utilities::{tempdir, unindent};
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
name: $name:ident,
|
||||
justfile: $justfile:expr,
|
||||
$(args: ($($arg:tt)*),)?
|
||||
$(env: {
|
||||
$($env_key:literal : $env_value:literal,)*
|
||||
},)?
|
||||
$(stdin: $stdin:expr,)?
|
||||
$(stdout: $stdout:expr,)?
|
||||
$(stderr: $stderr:expr,)?
|
||||
$(status: $status:expr,)?
|
||||
$(shell: $shell:expr,)?
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
#[allow(unused_mut)]
|
||||
let mut env = BTreeMap::new();
|
||||
|
||||
$($(env.insert($env_key.to_string(), $env_value.to_string());)*)?
|
||||
|
||||
Test {
|
||||
justfile: $justfile,
|
||||
$(args: &[$($arg)*],)?
|
||||
$(stdin: $stdin,)?
|
||||
$(stdout: $stdout,)?
|
||||
$(stderr: $stderr,)?
|
||||
$(status: $status,)?
|
||||
$(shell: $shell,)?
|
||||
env,
|
||||
..Test::default()
|
||||
}.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Test<'a> {
|
||||
justfile: &'a str,
|
||||
args: &'a [&'a str],
|
||||
env: BTreeMap<String, String>,
|
||||
stdin: &'a str,
|
||||
stdout: &'a str,
|
||||
stderr: &'a str,
|
||||
status: i32,
|
||||
shell: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for Test<'a> {
|
||||
fn default() -> Test<'a> {
|
||||
Test {
|
||||
justfile: "",
|
||||
args: &[],
|
||||
env: BTreeMap::new(),
|
||||
stdin: "",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
shell: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Test<'a> {
|
||||
fn run(self) {
|
||||
let tmp = tempdir();
|
||||
|
||||
let justfile = unindent(self.justfile);
|
||||
let stdout = unindent(self.stdout);
|
||||
let stderr = unindent(self.stderr);
|
||||
|
||||
let mut justfile_path = tmp.path().to_path_buf();
|
||||
justfile_path.push("justfile");
|
||||
fs::write(justfile_path, justfile).unwrap();
|
||||
|
||||
let mut dotenv_path = tmp.path().to_path_buf();
|
||||
dotenv_path.push(".env");
|
||||
fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();
|
||||
|
||||
let mut command = Command::new(&executable_path("just"));
|
||||
|
||||
if self.shell {
|
||||
command.args(&["--shell", "bash"]);
|
||||
}
|
||||
|
||||
let mut child = command
|
||||
.args(self.args)
|
||||
.envs(self.env)
|
||||
.current_dir(tmp.path())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("just invocation failed");
|
||||
|
||||
{
|
||||
let mut stdin_handle = child.stdin.take().expect("failed to unwrap stdin handle");
|
||||
|
||||
stdin_handle
|
||||
.write_all(self.stdin.as_bytes())
|
||||
.expect("failed to write stdin to just process");
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.expect("failed to wait for just process");
|
||||
|
||||
let have = Output {
|
||||
status: output.status.code().unwrap(),
|
||||
stdout: str::from_utf8(&output.stdout).unwrap(),
|
||||
stderr: str::from_utf8(&output.stderr).unwrap(),
|
||||
};
|
||||
|
||||
let want = Output {
|
||||
status: self.status,
|
||||
stdout: &stdout,
|
||||
stderr: &stderr,
|
||||
};
|
||||
|
||||
assert_eq!(have, want, "bad output");
|
||||
|
||||
if self.status == EXIT_SUCCESS {
|
||||
test_round_trip(tmp.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Output<'a> {
|
||||
stdout: &'a str,
|
||||
stderr: &'a str,
|
||||
status: i32,
|
||||
}
|
||||
|
||||
fn test_round_trip(tmpdir: &Path) {
|
||||
println!("Reparsing...");
|
||||
|
||||
let output = Command::new(&executable_path("just"))
|
||||
.current_dir(tmpdir)
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("dump failed: {}", output.status);
|
||||
}
|
||||
|
||||
let dumped = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let reparsed_path = tmpdir.join("reparsed.just");
|
||||
|
||||
fs::write(&reparsed_path, &dumped).unwrap();
|
||||
|
||||
let output = Command::new(&executable_path("just"))
|
||||
.current_dir(tmpdir)
|
||||
.arg("--justfile")
|
||||
.arg(&reparsed_path)
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("reparse failed: {}", output.status);
|
||||
}
|
||||
|
||||
let reparsed = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
assert_eq!(reparsed, dumped, "reparse mismatch");
|
||||
}
|
||||
use crate::common::*;
|
||||
|
||||
test! {
|
||||
name: alias_listing,
|
||||
|
172
tests/test.rs
Normal file
172
tests/test.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
name: $name:ident,
|
||||
justfile: $justfile:expr,
|
||||
$(args: ($($arg:tt)*),)?
|
||||
$(env: {
|
||||
$($env_key:literal : $env_value:literal,)*
|
||||
},)?
|
||||
$(stdin: $stdin:expr,)?
|
||||
$(stdout: $stdout:expr,)?
|
||||
$(stderr: $stderr:expr,)?
|
||||
$(status: $status:expr,)?
|
||||
$(shell: $shell:expr,)?
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
#[allow(unused_mut)]
|
||||
let mut env = std::collections::BTreeMap::new();
|
||||
|
||||
$($(env.insert($env_key.to_string(), $env_value.to_string());)*)?
|
||||
|
||||
crate::test::Test {
|
||||
justfile: $justfile,
|
||||
$(args: &[$($arg)*],)?
|
||||
$(stdin: $stdin,)?
|
||||
$(stdout: $stdout,)?
|
||||
$(stderr: $stderr,)?
|
||||
$(status: $status,)?
|
||||
$(shell: $shell,)?
|
||||
env,
|
||||
..crate::test::Test::default()
|
||||
}.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Test<'a> {
|
||||
pub(crate) justfile: &'a str,
|
||||
pub(crate) args: &'a [&'a str],
|
||||
pub(crate) env: BTreeMap<String, String>,
|
||||
pub(crate) stdin: &'a str,
|
||||
pub(crate) stdout: &'a str,
|
||||
pub(crate) stderr: &'a str,
|
||||
pub(crate) status: i32,
|
||||
pub(crate) shell: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for Test<'a> {
|
||||
fn default() -> Test<'a> {
|
||||
Test {
|
||||
justfile: "",
|
||||
args: &[],
|
||||
env: BTreeMap::new(),
|
||||
stdin: "",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
shell: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Test<'a> {
|
||||
pub(crate) fn run(self) {
|
||||
let tmp = tempdir();
|
||||
|
||||
let justfile = unindent(self.justfile);
|
||||
let stdout = unindent(self.stdout);
|
||||
let stderr = unindent(self.stderr);
|
||||
|
||||
let mut justfile_path = tmp.path().to_path_buf();
|
||||
justfile_path.push("justfile");
|
||||
fs::write(justfile_path, justfile).unwrap();
|
||||
|
||||
let mut dotenv_path = tmp.path().to_path_buf();
|
||||
dotenv_path.push(".env");
|
||||
fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();
|
||||
|
||||
let mut command = Command::new(&executable_path("just"));
|
||||
|
||||
if self.shell {
|
||||
command.args(&["--shell", "bash"]);
|
||||
}
|
||||
|
||||
let mut child = command
|
||||
.args(self.args)
|
||||
.envs(self.env)
|
||||
.current_dir(tmp.path())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("just invocation failed");
|
||||
|
||||
{
|
||||
let mut stdin_handle = child.stdin.take().expect("failed to unwrap stdin handle");
|
||||
|
||||
stdin_handle
|
||||
.write_all(self.stdin.as_bytes())
|
||||
.expect("failed to write stdin to just process");
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.expect("failed to wait for just process");
|
||||
|
||||
let have = Output {
|
||||
status: output.status.code().unwrap(),
|
||||
stdout: str::from_utf8(&output.stdout).unwrap(),
|
||||
stderr: str::from_utf8(&output.stderr).unwrap(),
|
||||
};
|
||||
|
||||
let want = Output {
|
||||
status: self.status,
|
||||
stdout: &stdout,
|
||||
stderr: &stderr,
|
||||
};
|
||||
|
||||
assert_eq!(have, want, "bad output");
|
||||
|
||||
if self.status == EXIT_SUCCESS {
|
||||
test_round_trip(tmp.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Output<'a> {
|
||||
stdout: &'a str,
|
||||
stderr: &'a str,
|
||||
status: i32,
|
||||
}
|
||||
|
||||
fn test_round_trip(tmpdir: &Path) {
|
||||
println!("Reparsing...");
|
||||
|
||||
let output = Command::new(&executable_path("just"))
|
||||
.current_dir(tmpdir)
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("dump failed: {}", output.status);
|
||||
}
|
||||
|
||||
let dumped = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let reparsed_path = tmpdir.join("reparsed.just");
|
||||
|
||||
fs::write(&reparsed_path, &dumped).unwrap();
|
||||
|
||||
let output = Command::new(&executable_path("just"))
|
||||
.current_dir(tmpdir)
|
||||
.arg("--justfile")
|
||||
.arg(&reparsed_path)
|
||||
.arg("--dump")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("reparse failed: {}", output.status);
|
||||
}
|
||||
|
||||
let reparsed = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
assert_eq!(reparsed, dumped, "reparse mismatch");
|
||||
}
|
Loading…
Reference in New Issue
Block a user