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:
|
branches:
|
||||||
- master
|
- 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:
|
jobs:
|
||||||
all:
|
all:
|
||||||
name: All
|
name: All
|
||||||
@ -41,19 +47,19 @@ jobs:
|
|||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
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
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
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
|
- name: Cache cargo build
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: target
|
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
|
- name: Install Main Toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
|
37
README.adoc
37
README.adoc
@ -266,6 +266,14 @@ $ just --summary --unsorted
|
|||||||
test build
|
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
|
||||||
|
|
||||||
Aliases allow recipes to be invoked with alternative names:
|
Aliases allow recipes to be invoked with alternative names:
|
||||||
@ -958,6 +966,35 @@ echo 'Bar!'
|
|||||||
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
|
=== Invoking Justfiles in Other Directories
|
||||||
|
|
||||||
If the first argument passed to `just` contains a `/`, then the following occurs:
|
If the first argument passed to `just` contains a `/`, then the following occurs:
|
||||||
|
@ -20,13 +20,17 @@ _just() {
|
|||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
just)
|
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
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
|
|
||||||
|
--chooser)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
--color)
|
--color)
|
||||||
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
|
@ -14,6 +14,7 @@ edit:completion:arg-completer[just] = [@words]{
|
|||||||
}
|
}
|
||||||
completions = [
|
completions = [
|
||||||
&'just'= {
|
&'just'= {
|
||||||
|
cand --chooser 'Override binary invoked by `--choose`'
|
||||||
cand --color 'Print colorful output'
|
cand --color 'Print colorful output'
|
||||||
cand -f 'Use <JUSTFILE> as justfile.'
|
cand -f 'Use <JUSTFILE> as justfile.'
|
||||||
cand --justfile '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 --unsorted 'Return list and summary entries in source order'
|
||||||
cand -v 'Use verbose output'
|
cand -v 'Use verbose output'
|
||||||
cand --verbose '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 --dump 'Print entire justfile'
|
||||||
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
|
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`'
|
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)'
|
complete -c just -a '(__fish_just_complete_recipes)'
|
||||||
|
|
||||||
# autogenerated completions
|
# 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" -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" -s f -l justfile -d 'Use <JUSTFILE> as justfile.'
|
||||||
complete -c just -n "__fish_use_subcommand" -l set -d 'Override <VARIABLE> with <VALUE>'
|
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" -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 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" -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" -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" -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'
|
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) {
|
$completions = @(switch ($command) {
|
||||||
'just' {
|
'just' {
|
||||||
|
[CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')
|
||||||
[CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
|
[CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
|
||||||
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
|
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
|
||||||
[CompletionResult]::new('--justfile', 'justfile', [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('--unsorted', 'unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
|
||||||
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output')
|
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output')
|
||||||
[CompletionResult]::new('--verbose', 'verbose', [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('--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('-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`')
|
[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 context curcontext="$curcontext" state line
|
||||||
local common=(
|
local common=(
|
||||||
|
'--chooser=[Override binary invoked by `--choose`]' \
|
||||||
'--color=[Print colorful output]: :(auto always never)' \
|
'--color=[Print colorful output]: :(auto always never)' \
|
||||||
'-f+[Use <JUSTFILE> as justfile.]' \
|
'-f+[Use <JUSTFILE> as justfile.]' \
|
||||||
'--justfile=[Use <JUSTFILE> as justfile.]' \
|
'--justfile=[Use <JUSTFILE> as justfile.]' \
|
||||||
@ -37,6 +38,7 @@ _just() {
|
|||||||
'--unsorted[Return list and summary entries in source order]' \
|
'--unsorted[Return list and summary entries in source order]' \
|
||||||
'*-v[Use verbose output]' \
|
'*-v[Use verbose output]' \
|
||||||
'*--verbose[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]' \
|
'--dump[Print entire justfile]' \
|
||||||
'-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
|
'-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`]' \
|
'--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
|
||||||
|
@ -4,13 +4,14 @@ pub(crate) use std::{
|
|||||||
cmp,
|
cmp,
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
env,
|
env,
|
||||||
|
ffi::OsString,
|
||||||
fmt::{self, Debug, Display, Formatter},
|
fmt::{self, Debug, Display, Formatter},
|
||||||
fs,
|
fs,
|
||||||
io::{self, Cursor, Write},
|
io::{self, Cursor, Write},
|
||||||
iter::{self, FromIterator},
|
iter::{self, FromIterator},
|
||||||
ops::{Index, Range, RangeInclusive},
|
ops::{Index, Range, RangeInclusive},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{self, Command},
|
process::{self, Command, Stdio},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
str::{self, Chars},
|
str::{self, Chars},
|
||||||
sync::{Mutex, MutexGuard},
|
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};
|
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: &str = "sh";
|
||||||
pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu";
|
pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu";
|
||||||
pub(crate) const INIT_JUSTFILE: &str = "default:\n\techo 'Hello, world!'\n";
|
pub(crate) const INIT_JUSTFILE: &str = "default:\n\techo 'Hello, world!'\n";
|
||||||
@ -24,6 +31,7 @@ pub(crate) struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod cmd {
|
mod cmd {
|
||||||
|
pub(crate) const CHOOSE: &str = "CHOOSE";
|
||||||
pub(crate) const COMPLETIONS: &str = "COMPLETIONS";
|
pub(crate) const COMPLETIONS: &str = "COMPLETIONS";
|
||||||
pub(crate) const DUMP: &str = "DUMP";
|
pub(crate) const DUMP: &str = "DUMP";
|
||||||
pub(crate) const EDIT: &str = "EDIT";
|
pub(crate) const EDIT: &str = "EDIT";
|
||||||
@ -35,6 +43,7 @@ mod cmd {
|
|||||||
pub(crate) const VARIABLES: &str = "VARIABLES";
|
pub(crate) const VARIABLES: &str = "VARIABLES";
|
||||||
|
|
||||||
pub(crate) const ALL: &[&str] = &[
|
pub(crate) const ALL: &[&str] = &[
|
||||||
|
CHOOSE,
|
||||||
COMPLETIONS,
|
COMPLETIONS,
|
||||||
DUMP,
|
DUMP,
|
||||||
EDIT,
|
EDIT,
|
||||||
@ -60,6 +69,7 @@ mod cmd {
|
|||||||
|
|
||||||
mod arg {
|
mod arg {
|
||||||
pub(crate) const ARGUMENTS: &str = "ARGUMENTS";
|
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 CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS";
|
||||||
pub(crate) const COLOR: &str = "COLOR";
|
pub(crate) const COLOR: &str = "COLOR";
|
||||||
pub(crate) const DRY_RUN: &str = "DRY-RUN";
|
pub(crate) const DRY_RUN: &str = "DRY-RUN";
|
||||||
@ -88,6 +98,12 @@ impl Config {
|
|||||||
.version_message("Print version information")
|
.version_message("Print version information")
|
||||||
.setting(AppSettings::ColoredHelp)
|
.setting(AppSettings::ColoredHelp)
|
||||||
.setting(AppSettings::TrailingVarArg)
|
.setting(AppSettings::TrailingVarArg)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(arg::CHOOSER)
|
||||||
|
.long("chooser")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("Override binary invoked by `--choose`"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::COLOR)
|
Arg::with_name(arg::COLOR)
|
||||||
.long("color")
|
.long("color")
|
||||||
@ -192,6 +208,7 @@ impl Config {
|
|||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
|
.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(
|
||||||
Arg::with_name(cmd::COMPLETIONS)
|
Arg::with_name(cmd::COMPLETIONS)
|
||||||
.long("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 {
|
Subcommand::Completions {
|
||||||
shell: shell.to_owned(),
|
shell: shell.to_owned(),
|
||||||
}
|
}
|
||||||
@ -461,8 +483,10 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &self.subcommand {
|
match &self.subcommand {
|
||||||
|
Choose { overrides, chooser } =>
|
||||||
|
self.choose(justfile, &search, overrides, chooser.as_deref()),
|
||||||
Dump => Self::dump(justfile),
|
Dump => Self::dump(justfile),
|
||||||
Evaluate { overrides } => self.run(justfile, &search, overrides, &Vec::new()),
|
Evaluate { overrides } => self.run(justfile, &search, overrides, &[]),
|
||||||
List => self.list(justfile),
|
List => self.list(justfile),
|
||||||
Run {
|
Run {
|
||||||
arguments,
|
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> {
|
fn dump(justfile: Justfile) -> Result<(), i32> {
|
||||||
println!("{}", justfile);
|
println!("{}", justfile);
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -570,13 +681,9 @@ impl Config {
|
|||||||
let doc_color = self.color.stdout().doc();
|
let doc_color = self.color.stdout().doc();
|
||||||
println!("Available recipes:");
|
println!("Available recipes:");
|
||||||
|
|
||||||
for recipe in justfile.recipes(self.unsorted) {
|
for recipe in justfile.public_recipes(self.unsorted) {
|
||||||
let name = recipe.name();
|
let name = recipe.name();
|
||||||
|
|
||||||
if recipe.private {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, name) in iter::once(&name)
|
for (i, name) in iter::once(&name)
|
||||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@ -662,9 +769,8 @@ impl Config {
|
|||||||
eprintln!("Justfile contains no recipes.");
|
eprintln!("Justfile contains no recipes.");
|
||||||
} else {
|
} else {
|
||||||
let summary = justfile
|
let summary = justfile
|
||||||
.recipes(self.unsorted)
|
.public_recipes(self.unsorted)
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|recipe| recipe.public())
|
|
||||||
.map(|recipe| recipe.name())
|
.map(|recipe| recipe.name())
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
@ -704,6 +810,9 @@ USAGE:
|
|||||||
just [FLAGS] [OPTIONS] [--] [ARGUMENTS]...
|
just [FLAGS] [OPTIONS] [--] [ARGUMENTS]...
|
||||||
|
|
||||||
FLAGS:
|
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
|
--clear-shell-args Clear shell arguments
|
||||||
--dry-run Print what just would do without doing it
|
--dry-run Print what just would do without doing it
|
||||||
--dump Print entire justfile
|
--dump Print entire justfile
|
||||||
@ -722,6 +831,7 @@ FLAGS:
|
|||||||
-v, --verbose Use verbose output
|
-v, --verbose Use verbose output
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
|
--chooser <CHOOSER> Override binary invoked by `--choose`
|
||||||
--color <COLOR>
|
--color <COLOR>
|
||||||
Print colorful output [default: auto] [possible values: auto, always, never]
|
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! {
|
test! {
|
||||||
name: subcommand_completions,
|
name: subcommand_completions,
|
||||||
args: ["--completions", "bash"],
|
args: ["--completions", "bash"],
|
||||||
|
@ -263,11 +263,12 @@ impl<'src> Justfile<'src> {
|
|||||||
Ok(())
|
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
|
let mut recipes = self
|
||||||
.recipes
|
.recipes
|
||||||
.values()
|
.values()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
|
.filter(|recipe| recipe.public())
|
||||||
.collect::<Vec<&Recipe<Dependency>>>();
|
.collect::<Vec<&Recipe<Dependency>>>();
|
||||||
|
|
||||||
if source_order {
|
if source_order {
|
||||||
|
@ -2,6 +2,10 @@ use crate::common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
pub(crate) enum Subcommand {
|
pub(crate) enum Subcommand {
|
||||||
|
Choose {
|
||||||
|
overrides: BTreeMap<String, String>,
|
||||||
|
chooser: Option<String>,
|
||||||
|
},
|
||||||
Completions {
|
Completions {
|
||||||
shell: String,
|
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 crate::common::*;
|
||||||
|
|
||||||
use executable_path::executable_path;
|
|
||||||
use which::which;
|
|
||||||
|
|
||||||
use test_utilities::{assert_stdout, tmptree};
|
|
||||||
|
|
||||||
const JUSTFILE: &str = "Yooooooo, hopefully this never becomes valid syntax.";
|
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 completions;
|
||||||
mod dotenv;
|
mod dotenv;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
183
tests/misc.rs
183
tests/misc.rs
@ -1,185 +1,4 @@
|
|||||||
use std::{
|
use crate::common::*;
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: alias_listing,
|
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