Compare commits
10 Commits
1c3c1dd3c0
...
8d80f83795
Author | SHA1 | Date | |
---|---|---|---|
|
8d80f83795 | ||
|
c900b6f478 | ||
|
af86a471e2 | ||
|
e4564f45a3 | ||
|
aa43a664ee | ||
|
553adc1004 | ||
|
e572b93d84 | ||
|
fcac7ee768 | ||
|
71b72c4a53 | ||
|
0e8f660d6d |
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -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
10
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
26
README.md
26
README.md
@ -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
|
||||||
|
@ -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>"),
|
||||||
|
@ -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,
|
||||||
|
@ -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::*;
|
||||||
|
@ -79,6 +79,7 @@ impl Compiler {
|
|||||||
absolute,
|
absolute,
|
||||||
optional,
|
optional,
|
||||||
path,
|
path,
|
||||||
|
attributes: _,
|
||||||
} => {
|
} => {
|
||||||
let import = current
|
let import = current
|
||||||
.path
|
.path
|
||||||
|
@ -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'-')
|
||||||
|
@ -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>,
|
||||||
|
114
src/lib.rs
114
src/lib.rs
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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() {
|
||||||
|
12
src/run.rs
12
src/run.rs
@ -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);
|
||||||
|
|
||||||
|
@ -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
27
tests/datetime.rs
Normal 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();
|
||||||
|
}
|
@ -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())),
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user