Compare commits

...

10 Commits

Author SHA1 Message Date
Greg Shuflin
8d80f83795 group attribute on import
https://github.com/casey/just/issues/2087
2024-06-25 23:59:51 -07:00
dependabot[bot]
c900b6f478
Update softprops/action-gh-release (#2183) 2024-06-24 12:42:39 -07:00
Casey Rodarmor
af86a471e2
Don't analyze comments when ignore-comments is set (#2180) 2024-06-21 20:39:34 +00:00
Casey Rodarmor
e4564f45a3
Don't exit process in run() on argument parse error (#2176) 2024-06-20 03:57:46 +00:00
Casey Rodarmor
aa43a664ee
Document remote justfile workaround (#2175) 2024-06-19 17:18:03 -07:00
Casey Rodarmor
553adc1004
Document library interface (#2174) 2024-06-19 23:38:02 +00:00
Casey Rodarmor
e572b93d84
Allow passing command-line arguments into run() (#2173) 2024-06-19 23:25:36 +00:00
Ryan McGuire
fcac7ee768
Ignore env_logger initialization errors (#2170) 2024-06-19 07:50:37 +00:00
Blair Noctis
71b72c4a53
Remove dependency on cradle (#2169) 2024-06-18 02:42:16 +00:00
Casey Rodarmor
0e8f660d6d
Add datetime() and datetime_utc() functions (#2167) 2024-06-14 22:48:34 -07:00
22 changed files with 317 additions and 67 deletions

View File

@ -95,7 +95,7 @@ jobs:
shell: bash shell: bash
- name: Publish Archive - name: Publish Archive
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.6
if: ${{ startsWith(github.ref, 'refs/tags/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: false draft: false
@ -105,7 +105,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Changelog - name: Publish Changelog
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.6
if: >- if: >-
${{ ${{
startsWith(github.ref, 'refs/tags/') startsWith(github.ref, 'refs/tags/')

10
Cargo.lock generated
View File

@ -306,15 +306,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cradle"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7096122c1023d53de7298f322590170540ad3eba46bbc2750b495f098c27c09a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.5" version = "0.8.5"
@ -609,7 +600,6 @@ dependencies = [
"clap 4.5.7", "clap 4.5.7",
"clap_complete", "clap_complete",
"clap_mangen", "clap_mangen",
"cradle",
"ctrlc", "ctrlc",
"derivative", "derivative",
"dirs", "dirs",

View File

@ -54,7 +54,6 @@ unicode-width = "0.1.0"
uuid = { version = "1.0.0", features = ["v4"] } uuid = { version = "1.0.0", features = ["v4"] }
[dev-dependencies] [dev-dependencies]
cradle = "0.2.0"
executable-path = "1.0.0" executable-path = "1.0.0"
pretty_assertions = "1.0.0" pretty_assertions = "1.0.0"
temptree = "0.2.0" temptree = "0.2.0"

View File

@ -1642,6 +1642,16 @@ which will halt execution.
characters. For example, `choose('64', HEX)` will generate a random characters. For example, `choose('64', HEX)` will generate a random
64-character lowercase hex string. 64-character lowercase hex string.
#### Datetime
- `datetime(format)`<sup>master</sup> - Return local time with `format`.
- `datetime_utc(format)`<sup>master</sup> - Return UTC time with `format`.
The arguments to `datetime` and `datetime_utc` are `strftime`-style format
strings, see the
[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
for details.
#### Semantic Versions #### Semantic Versions
- `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a - `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a
@ -3658,6 +3668,22 @@ ls:
echo '{{absolute_path(".")}}' echo '{{absolute_path(".")}}'
``` ```
### Remote Justfiles
If you wish to include a `mod` or `import` source file in many `justfiles`
without needing to duplicate it, you can use an optional `mod` or `import`,
along with a recipe to fetch the module source:
```just
import? 'foo.just'
fetch:
curl https://raw.githubusercontent.com/casey/just/master/justfile > foo.just
```
Given the above `justfile`, after running `just fetch`, the recipes in
`foo.just` will be available.
### Alternatives and Prior Art ### Alternatives and Prior Art
There is no shortage of command runners! Some more or less similar alternatives There is no shortage of command runners! Some more or less similar alternatives

View File

@ -3,7 +3,7 @@ use super::*;
/// An alias, e.g. `name := target` /// An alias, e.g. `name := target`
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> { pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
pub(crate) attributes: BTreeSet<Attribute<'src>>, pub(crate) attributes: AttributeSet<'src>,
pub(crate) name: Name<'src>, pub(crate) name: Name<'src>,
#[serde( #[serde(
bound(serialize = "T: Keyed<'src>"), bound(serialize = "T: Keyed<'src>"),

View File

@ -79,7 +79,15 @@ impl<'src> Analyzer<'src> {
assignments.push(assignment); assignments.push(assignment);
} }
Item::Comment(_) => (), Item::Comment(_) => (),
Item::Import { absolute, .. } => { Item::Import {
absolute,
attributes,
..
} => {
//TODO check attributes for validity
let _groups = attributes.groups();
if let Some(absolute) = absolute { if let Some(absolute) = absolute {
stack.push(asts.get(absolute).unwrap()); stack.push(asts.get(absolute).unwrap());
} }
@ -149,7 +157,7 @@ impl<'src> Analyzer<'src> {
} }
} }
let recipes = RecipeResolver::resolve_recipes(recipe_table, &self.assignments)?; let recipes = RecipeResolver::resolve_recipes(&self.assignments, &settings, recipe_table)?;
let mut aliases = Table::new(); let mut aliases = Table::new();
while let Some(alias) = self.aliases.pop() { while let Some(alias) = self.aliases.pop() {
@ -231,7 +239,7 @@ impl<'src> Analyzer<'src> {
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
let name = alias.name.lexeme(); let name = alias.name.lexeme();
for attribute in &alias.attributes { for attribute in &alias.attributes.inner {
if *attribute != Attribute::Private { if *attribute != Attribute::Private {
return Err(alias.name.token.error(AliasInvalidAttribute { return Err(alias.name.token.error(AliasInvalidAttribute {
alias: name, alias: name,

View File

@ -120,6 +120,49 @@ impl<'src> Display for Attribute<'src> {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct AttributeSet<'src> {
#[serde(flatten)]
pub(crate) inner: BTreeSet<Attribute<'src>>,
}
impl<'src> AttributeSet<'src> {
pub(crate) fn empty() -> Self {
Self {
inner: BTreeSet::new(),
}
}
pub(crate) fn from_map<T>(input: BTreeMap<Attribute<'src>, T>) -> Self {
Self {
inner: input.into_keys().collect(),
}
}
pub(crate) fn to_btree_set(self) -> BTreeSet<Attribute<'src>> {
self.inner
}
pub(crate) fn contains(&self, attribute: &Attribute) -> bool {
self.inner.contains(attribute)
}
/// Get the names of all Group attributes defined in this attribute set
pub(crate) fn groups(&self) -> Vec<&StringLiteral<'src>> {
self
.inner
.iter()
.filter_map(|attr| {
if let Attribute::Group(name) = attr {
Some(name)
} else {
None
}
})
.collect()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -79,6 +79,7 @@ impl Compiler {
absolute, absolute,
optional, optional,
path, path,
attributes: _,
} => { } => {
let import = current let import = current
.path .path

View File

@ -47,6 +47,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)), "config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
"data_directory" => Nullary(|_| dir("data", dirs::data_dir)), "data_directory" => Nullary(|_| dir("data", dirs::data_dir)),
"data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)), "data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)),
"datetime" => Unary(datetime),
"datetime_utc" => Unary(datetime_utc),
"encode_uri_component" => Unary(encode_uri_component), "encode_uri_component" => Unary(encode_uri_component),
"env" => UnaryOpt(env), "env" => UnaryOpt(env),
"env_var" => Unary(env_var), "env_var" => Unary(env_var),
@ -235,6 +237,14 @@ fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> FunctionResult {
} }
} }
fn datetime(_context: Context, format: &str) -> FunctionResult {
Ok(chrono::Local::now().format(format).to_string())
}
fn datetime_utc(_context: Context, format: &str) -> FunctionResult {
Ok(chrono::Utc::now().format(format).to_string())
}
fn encode_uri_component(_context: Context, s: &str) -> FunctionResult { fn encode_uri_component(_context: Context, s: &str) -> FunctionResult {
static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
.remove(b'-') .remove(b'-')

View File

@ -7,6 +7,7 @@ pub(crate) enum Item<'src> {
Assignment(Assignment<'src>), Assignment(Assignment<'src>),
Comment(&'src str), Comment(&'src str),
Import { Import {
attributes: AttributeSet<'src>,
absolute: Option<PathBuf>, absolute: Option<PathBuf>,
optional: bool, optional: bool,
path: Token<'src>, path: Token<'src>,

View File

@ -13,31 +13,99 @@
overlapping_range_endpoints overlapping_range_endpoints
)] )]
//! `just` is primarily used as a command-line binary, but does provide a
//! limited public library interface.
//!
//! Please keep in mind that there are no semantic version guarantees for the
//! library interface. It may break or change at any time.
pub(crate) use { pub(crate) use {
crate::{ crate::{
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment, alias::Alias,
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, analyzer::Analyzer,
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, argument_parser::ArgumentParser,
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, assignment::Assignment,
condition::Condition, conditional_operator::ConditionalOperator, config::Config, assignment_resolver::AssignmentResolver,
config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter, ast::Ast,
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error, attribute::{Attribute, AttributeSet},
evaluator::Evaluator, execution_context::ExecutionContext, expression::Expression, binding::Binding,
fragment::Fragment, function::Function, interrupt_guard::InterruptGuard, color::Color,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed, color_display::ColorDisplay,
keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv, command_ext::CommandExt,
loader::Loader, module_path::ModulePath, name::Name, namepath::Namepath, ordinal::Ordinal, compilation::Compilation,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, compile_error::CompileError,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position, compile_error_kind::CompileErrorKind,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe, compiler::Compiler,
recipe_resolver::RecipeResolver, recipe_signature::RecipeSignature, scope::Scope, condition::Condition,
search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, conditional_operator::ConditionalOperator,
setting::Setting, settings::Settings, shebang::Shebang, shell::Shell, config::Config,
show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind, config_error::ConfigError,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, constants::constants,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, count::Count,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, delimiter::Delimiter,
verbosity::Verbosity, warning::Warning, dependency::Dependency,
dump_format::DumpFormat,
enclosure::Enclosure,
error::Error,
evaluator::Evaluator,
execution_context::ExecutionContext,
expression::Expression,
fragment::Fragment,
function::Function,
interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler,
item::Item,
justfile::Justfile,
keyed::Keyed,
keyword::Keyword,
lexer::Lexer,
line::Line,
list::List,
load_dotenv::load_dotenv,
loader::Loader,
module_path::ModulePath,
name::Name,
namepath::Namepath,
ordinal::Ordinal,
output::output,
output_error::OutputError,
parameter::Parameter,
parameter_kind::ParameterKind,
parser::Parser,
platform::Platform,
platform_interface::PlatformInterface,
position::Position,
positional::Positional,
ran::Ran,
range_ext::RangeExt,
recipe::Recipe,
recipe_resolver::RecipeResolver,
recipe_signature::RecipeSignature,
scope::Scope,
search::Search,
search_config::SearchConfig,
search_error::SearchError,
set::Set,
setting::Setting,
settings::Settings,
shebang::Shebang,
shell::Shell,
show_whitespace::ShowWhitespace,
source::Source,
string_kind::StringKind,
string_literal::StringLiteral,
subcommand::Subcommand,
suggestion::Suggestion,
table::Table,
thunk::Thunk,
token::Token,
token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe,
use_color::UseColor,
variables::Variables,
verbosity::Verbosity,
warning::Warning,
}, },
camino::Utf8Path, camino::Utf8Path,
derivative::Derivative, derivative::Derivative,

View File

@ -1,5 +1,5 @@
fn main() { fn main() {
if let Err(code) = just::run() { if let Err(code) = just::run(std::env::args_os()) {
std::process::exit(code); std::process::exit(code);
} }
} }

View File

@ -334,7 +334,7 @@ impl<'run, 'src> Parser<'run, 'src> {
} else if self.next_is(Identifier) { } else if self.next_is(Identifier) {
match Keyword::from_lexeme(next.lexeme()) { match Keyword::from_lexeme(next.lexeme()) {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(BTreeSet::new())?)); items.push(Item::Alias(self.parse_alias(AttributeSet::empty())?));
} }
Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
self.presume_keyword(Keyword::Export)?; self.presume_keyword(Keyword::Export)?;
@ -354,15 +354,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|| self.next_are(&[Identifier, Identifier, StringToken]) || self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) => || self.next_are(&[Identifier, QuestionMark]) =>
{ {
self.presume_keyword(Keyword::Import)?; items.push(self.parse_import(AttributeSet::empty())?);
let optional = self.accepted(QuestionMark)?;
let (path, relative) = self.parse_string_literal_token()?;
items.push(Item::Import {
absolute: None,
optional,
path,
relative,
});
} }
Some(Keyword::Mod) Some(Keyword::Mod)
if self.next_are(&[Identifier, Identifier, StringToken]) if self.next_are(&[Identifier, Identifier, StringToken])
@ -408,7 +400,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe( items.push(Item::Recipe(self.parse_recipe(
doc, doc,
false, false,
BTreeSet::new(), AttributeSet::empty(),
)?)); )?));
} }
} }
@ -418,7 +410,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe( items.push(Item::Recipe(self.parse_recipe(
doc, doc,
true, true,
BTreeSet::new(), AttributeSet::empty(),
)?)); )?));
} else if let Some(attributes) = self.parse_attributes()? { } else if let Some(attributes) = self.parse_attributes()? {
let next_keyword = Keyword::from_lexeme(self.next()?.lexeme()); let next_keyword = Keyword::from_lexeme(self.next()?.lexeme());
@ -426,6 +418,13 @@ impl<'run, 'src> Parser<'run, 'src> {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(attributes)?)); items.push(Item::Alias(self.parse_alias(attributes)?));
} }
Some(Keyword::Import)
if self.next_are(&[Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) =>
{
items.push(self.parse_import(attributes)?);
}
_ => { _ => {
let quiet = self.accepted(At)?; let quiet = self.accepted(At)?;
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
@ -450,10 +449,22 @@ impl<'run, 'src> Parser<'run, 'src> {
} }
} }
fn parse_import(&mut self, attributes: AttributeSet<'src>) -> CompileResult<'src, Item<'src>> {
self.presume_keyword(Keyword::Import)?;
let optional = self.accepted(QuestionMark)?;
let (path, relative) = self.parse_string_literal_token()?;
Ok(Item::Import {
absolute: None,
attributes,
optional,
path,
relative,
})
}
/// Parse an alias, e.g `alias name := target` /// Parse an alias, e.g `alias name := target`
fn parse_alias( fn parse_alias(
&mut self, &mut self,
attributes: BTreeSet<Attribute<'src>>, attributes: AttributeSet<'src>,
) -> CompileResult<'src, Alias<'src, Name<'src>>> { ) -> CompileResult<'src, Alias<'src, Name<'src>>> {
self.presume_keyword(Keyword::Alias)?; self.presume_keyword(Keyword::Alias)?;
let name = self.parse_name()?; let name = self.parse_name()?;
@ -745,7 +756,7 @@ impl<'run, 'src> Parser<'run, 'src> {
&mut self, &mut self,
doc: Option<&'src str>, doc: Option<&'src str>,
quiet: bool, quiet: bool,
attributes: BTreeSet<Attribute<'src>>, attributes: AttributeSet<'src>,
) -> CompileResult<'src, UnresolvedRecipe<'src>> { ) -> CompileResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?; let name = self.parse_name()?;
@ -807,7 +818,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Ok(Recipe { Ok(Recipe {
shebang: body.first().map_or(false, Line::is_shebang), shebang: body.first().map_or(false, Line::is_shebang),
attributes, attributes: attributes.to_btree_set(),
body, body,
dependencies, dependencies,
doc, doc,
@ -984,7 +995,7 @@ impl<'run, 'src> Parser<'run, 'src> {
} }
/// Parse recipe attributes /// Parse recipe attributes
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute<'src>>>> { fn parse_attributes(&mut self) -> CompileResult<'src, Option<AttributeSet<'src>>> {
let mut attributes = BTreeMap::new(); let mut attributes = BTreeMap::new();
while self.accepted(BracketL)? { while self.accepted(BracketL)? {
@ -1024,7 +1035,7 @@ impl<'run, 'src> Parser<'run, 'src> {
if attributes.is_empty() { if attributes.is_empty() {
Ok(None) Ok(None)
} else { } else {
Ok(Some(attributes.into_keys().collect())) Ok(Some(AttributeSet::from_map(attributes)))
} }
} }
} }

View File

@ -19,6 +19,7 @@ fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: Exit
/// A recipe, e.g. `foo: bar baz` /// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug, Clone, Serialize)] #[derive(PartialEq, Debug, Clone, Serialize)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) struct Recipe<'src, D = Dependency<'src>> {
//TODO make this be attributeset too
pub(crate) attributes: BTreeSet<Attribute<'src>>, pub(crate) attributes: BTreeSet<Attribute<'src>>,
pub(crate) body: Vec<Line<'src>>, pub(crate) body: Vec<Line<'src>>,
pub(crate) dependencies: Vec<D>, pub(crate) dependencies: Vec<D>,

View File

@ -8,8 +8,9 @@ pub(crate) struct RecipeResolver<'src: 'run, 'run> {
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
pub(crate) fn resolve_recipes( pub(crate) fn resolve_recipes(
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
assignments: &'run Table<'src, Assignment<'src>>, assignments: &'run Table<'src, Assignment<'src>>,
settings: &Settings,
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> { ) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
let mut resolver = Self { let mut resolver = Self {
resolved_recipes: Table::new(), resolved_recipes: Table::new(),
@ -39,6 +40,10 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
} }
for line in &recipe.body { for line in &recipe.body {
if line.is_comment() && settings.ignore_comments {
continue;
}
for fragment in &line.fragments { for fragment in &line.fragments {
if let Fragment::Interpolation { expression, .. } = fragment { if let Fragment::Interpolation { expression, .. } = fragment {
for variable in expression.variables() { for variable in expression.variables() {

View File

@ -1,8 +1,8 @@
use super::*; use super::*;
/// Main entry point into just binary. /// Main entry point into `just`. Parse arguments from `args` and run.
#[allow(clippy::missing_errors_doc)] #[allow(clippy::missing_errors_doc)]
pub fn run() -> Result<(), i32> { pub fn run(args: impl Iterator<Item = impl Into<OsString> + Clone>) -> Result<(), i32> {
#[cfg(windows)] #[cfg(windows)]
ansi_term::enable_ansi_support().ok(); ansi_term::enable_ansi_support().ok();
@ -11,12 +11,16 @@ pub fn run() -> Result<(), i32> {
.filter("JUST_LOG") .filter("JUST_LOG")
.write_style("JUST_LOG_STYLE"), .write_style("JUST_LOG_STYLE"),
) )
.init(); .try_init()
.ok();
let app = Config::app(); let app = Config::app();
info!("Parsing command line arguments…"); info!("Parsing command line arguments…");
let matches = app.get_matches(); let matches = app.try_get_matches_from(args).map_err(|err| {
err.print().ok();
err.exit_code()
})?;
let config = Config::from_matches(&matches).map_err(Error::from); let config = Config::from_matches(&matches).map_err(Error::from);

View File

@ -185,7 +185,13 @@ fn status_error() {
"exit-2": "#!/usr/bin/env bash\nexit 2\n", "exit-2": "#!/usr/bin/env bash\nexit 2\n",
}; };
("chmod", "+x", tmp.path().join("exit-2")).run(); let output = Command::new("chmod")
.arg("+x")
.arg(tmp.path().join("exit-2"))
.output()
.unwrap();
assert!(output.status.success());
let path = env::join_paths( let path = env::join_paths(
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),

27
tests/datetime.rs Normal file
View File

@ -0,0 +1,27 @@
use super::*;
#[test]
fn datetime() {
Test::new()
.justfile(
"
x := datetime('%Y-%m-%d %z')
",
)
.args(["--eval", "x"])
.stdout_regex(r"\d\d\d\d-\d\d-\d\d [+-]\d\d\d\d")
.run();
}
#[test]
fn datetime_utc() {
Test::new()
.justfile(
"
x := datetime_utc('%Y-%m-%d %Z')
",
)
.args(["--eval", "x"])
.stdout_regex(r"\d\d\d\d-\d\d-\d\d UTC")
.run();
}

View File

@ -64,7 +64,13 @@ fn status_error() {
"exit-2": "#!/usr/bin/env bash\nexit 2\n", "exit-2": "#!/usr/bin/env bash\nexit 2\n",
}; };
("chmod", "+x", tmp.path().join("exit-2")).run(); let output = Command::new("chmod")
.arg("+x")
.arg(tmp.path().join("exit-2"))
.output()
.unwrap();
assert!(output.status.success());
let path = env::join_paths( let path = env::join_paths(
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),

View File

@ -126,7 +126,13 @@ fn write_error() {
let justfile_path = test.justfile_path(); let justfile_path = test.justfile_path();
("chmod", "400", &justfile_path).run(); let output = Command::new("chmod")
.arg("400")
.arg(&justfile_path)
.output()
.unwrap();
assert!(output.status.success());
let _tempdir = test.run(); let _tempdir = test.run();

View File

@ -97,3 +97,41 @@ fn dont_evaluate_comments() {
) )
.run(); .run();
} }
#[test]
fn dont_analyze_comments() {
Test::new()
.justfile(
"
set ignore-comments
some_recipe:
# {{ bar }}
",
)
.run();
}
#[test]
fn comments_still_must_be_parsable_when_ignored() {
Test::new()
.justfile(
"
set ignore-comments
some_recipe:
# {{ foo bar }}
",
)
.stderr(
"
error: Expected '}}', '(', '+', or '/', but found identifier
justfile:4:12
4 # {{ foo bar }}
^^^
",
)
.status(EXIT_FAILURE)
.run();
}

View File

@ -5,7 +5,6 @@ pub(crate) use {
tempdir::tempdir, tempdir::tempdir,
test::{assert_eval_eq, Output, Test}, test::{assert_eval_eq, Output, Test},
}, },
cradle::input::Input,
executable_path::executable_path, executable_path::executable_path,
just::unindent, just::unindent,
libc::{EXIT_FAILURE, EXIT_SUCCESS}, libc::{EXIT_FAILURE, EXIT_SUCCESS},
@ -47,6 +46,7 @@ mod completions;
mod conditional; mod conditional;
mod confirm; mod confirm;
mod constants; mod constants;
mod datetime;
mod delimiters; mod delimiters;
mod directories; mod directories;
mod dotenv; mod dotenv;