Wait for child processes to finish (#345)
Thanks to @bheisler for the feature request and initial implementation. Fixes #302
This commit is contained in:
parent
c615a3fb0b
commit
b14d1ec97c
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
Cargo.lock linguist-generated diff=nodiff
|
81
Cargo.lock
generated
81
Cargo.lock
generated
@ -88,6 +88,15 @@ dependencies = [
|
||||
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.13.0"
|
||||
@ -108,6 +117,18 @@ name = "either"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executable-path"
|
||||
version = "1.0.0"
|
||||
@ -151,6 +172,14 @@ name = "glob"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.7.8"
|
||||
@ -168,12 +197,15 @@ dependencies = [
|
||||
"atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"brev 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"edit-distance 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"env_logger 0.5.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -190,6 +222,14 @@ name = "libc"
|
||||
version = "0.2.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.0.1"
|
||||
@ -198,6 +238,23 @@ dependencies = [
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.3.15"
|
||||
@ -305,6 +362,14 @@ dependencies = [
|
||||
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"wincolor 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
@ -389,6 +454,14 @@ name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "wincolor"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
@ -401,19 +474,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "49ec142f5768efb5b7622aebc3fdbdbb8950a4b9ba996393cb76ef7466e8747d"
|
||||
"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18"
|
||||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
||||
"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e"
|
||||
"checksum dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d0a1279c96732bc6800ce6337b6a614697b0e74ae058dc03c62ebeb78b4d86"
|
||||
"checksum edit-distance 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3bd26878c3d921f89797a4e1a1711919f999a9f6946bb6f5a4ffda126d297b7e"
|
||||
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
|
||||
"checksum env_logger 0.5.12 (registry+https://github.com/rust-lang/crates.io-index)" = "f4d7e69c283751083d53d01eac767407343b8b69c4bd70058e08adc2637cb257"
|
||||
"checksum executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478"
|
||||
"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82"
|
||||
"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b"
|
||||
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||
"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e"
|
||||
"checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450"
|
||||
"checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739"
|
||||
"checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1"
|
||||
"checksum log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cba860f648db8e6f269df990180c2217f333472b4a6e901e97446858487971e2"
|
||||
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
|
||||
"checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17"
|
||||
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
||||
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
|
||||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
||||
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1"
|
||||
@ -428,6 +507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd"
|
||||
"checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a"
|
||||
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
"checksum termcolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "722426c4a0539da2c4ffd9b419d90ad540b4cff4a053be9069c908d4d07e2836"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
|
||||
@ -441,3 +521,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
"checksum wincolor 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b9dc3aa9dcda98b5a16150c54619c1ead22e3d3a5d458778ae914be760aa981a"
|
||||
|
@ -15,13 +15,19 @@ brev = "0.1.6"
|
||||
clap = "2.0.0"
|
||||
dotenv = "0.13.0"
|
||||
edit-distance = "2.0.0"
|
||||
env_logger = "0.5.12"
|
||||
itertools = "0.7"
|
||||
lazy_static = "1.0.0"
|
||||
libc = "0.2.21"
|
||||
log = "0.4.4"
|
||||
regex = "1.0.0"
|
||||
target = "1.0.0"
|
||||
tempdir = "0.3.5"
|
||||
unicode-width = "0.1.3"
|
||||
|
||||
[dependencies.ctrlc]
|
||||
version = "3.1"
|
||||
features = ['termination']
|
||||
|
||||
[dev-dependencies]
|
||||
executable-path = "1.0.0"
|
||||
|
2
justfile
2
justfile
@ -27,7 +27,7 @@ check:
|
||||
cargo check
|
||||
|
||||
watch COMMAND='test':
|
||||
cargo watch {{COMMAND}}
|
||||
cargo watch --clear --exec {{COMMAND}}
|
||||
|
||||
version = `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/v\1/p' Cargo.toml`
|
||||
|
||||
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
||||
tab_spaces = 2
|
||||
max_width = 100
|
@ -1,5 +1,3 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
|
||||
use brev;
|
||||
@ -154,8 +152,9 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
process::Stdio::inherit()
|
||||
});
|
||||
|
||||
brev::output(cmd)
|
||||
InterruptHandler::guard(|| brev::output(cmd)
|
||||
.map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ pub use std::io::prelude::*;
|
||||
pub use std::ops::Range;
|
||||
pub use std::path::{Path, PathBuf};
|
||||
pub use std::process::Command;
|
||||
pub use std::sync::{Mutex, MutexGuard};
|
||||
pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize};
|
||||
|
||||
pub use color::Color;
|
||||
@ -21,6 +22,7 @@ pub use cooked_string::CookedString;
|
||||
pub use expression::Expression;
|
||||
pub use fragment::Fragment;
|
||||
pub use function::{evaluate_function, resolve_function, FunctionContext};
|
||||
pub use interrupt_handler::InterruptHandler;
|
||||
pub use justfile::Justfile;
|
||||
pub use lexer::Lexer;
|
||||
pub use load_dotenv::load_dotenv;
|
||||
|
7
src/die.rs
Normal file
7
src/die.rs
Normal file
@ -0,0 +1,7 @@
|
||||
macro_rules! die {
|
||||
($($arg:tt)*) => {{
|
||||
extern crate std;
|
||||
eprintln!($($arg)*);
|
||||
process::exit(EXIT_FAILURE)
|
||||
}};
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
|
||||
use target;
|
||||
|
||||
use platform::{Platform, PlatformInterface};
|
||||
@ -106,14 +105,14 @@ pub fn invocation_directory(context: &FunctionContext) -> Result<String, String>
|
||||
|
||||
pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
||||
use std::env::VarError::*;
|
||||
|
||||
|
||||
if let Some(value) = context.dotenv.get(key) {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
|
||||
match env::var(key) {
|
||||
Err(NotPresent) => Err(format!("environment variable `{}` not present", key)),
|
||||
Err(NotUnicode(os_string)) =>
|
||||
Err(NotUnicode(os_string)) =>
|
||||
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
|
||||
Ok(value) => Ok(value),
|
||||
}
|
||||
@ -131,7 +130,7 @@ pub fn env_var_or_default(
|
||||
use std::env::VarError::*;
|
||||
match env::var(key) {
|
||||
Err(NotPresent) => Ok(default.to_string()),
|
||||
Err(NotUnicode(os_string)) =>
|
||||
Err(NotUnicode(os_string)) =>
|
||||
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
|
||||
Ok(value) => Ok(value),
|
||||
}
|
||||
|
93
src/interrupt_handler.rs
Normal file
93
src/interrupt_handler.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use common::*;
|
||||
|
||||
use ctrlc;
|
||||
|
||||
pub struct InterruptHandler {
|
||||
blocks: u32,
|
||||
interrupted: bool,
|
||||
}
|
||||
|
||||
impl InterruptHandler {
|
||||
pub fn install() -> Result<(), ctrlc::Error> {
|
||||
ctrlc::set_handler(|| InterruptHandler::instance().interrupt())
|
||||
}
|
||||
|
||||
fn instance() -> MutexGuard<'static, InterruptHandler> {
|
||||
lazy_static! {
|
||||
static ref INSTANCE: Mutex<InterruptHandler> = Mutex::new(InterruptHandler::new());
|
||||
}
|
||||
|
||||
match INSTANCE.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poison_error) => die!(
|
||||
"{}",
|
||||
RuntimeError::Internal {
|
||||
message: format!("interrupt handler mutex poisoned: {}", poison_error),
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn new() -> InterruptHandler {
|
||||
InterruptHandler {
|
||||
blocks: 0,
|
||||
interrupted: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt(&mut self) {
|
||||
self.interrupted = true;
|
||||
|
||||
if self.blocks > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
Self::exit();
|
||||
}
|
||||
|
||||
fn exit() {
|
||||
process::exit(130);
|
||||
}
|
||||
|
||||
fn block(&mut self) {
|
||||
self.blocks += 1;
|
||||
}
|
||||
|
||||
fn unblock(&mut self) {
|
||||
if self.blocks == 0 {
|
||||
die!(
|
||||
"{}",
|
||||
RuntimeError::Internal {
|
||||
message: "attempted to unblock interrupt handler, but handler was not blocked"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
self.blocks -= 1;
|
||||
|
||||
if self.interrupted {
|
||||
Self::exit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guard<T, F: FnOnce() -> T>(function: F) -> T {
|
||||
let _guard = InterruptGuard::new();
|
||||
function()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InterruptGuard;
|
||||
|
||||
impl InterruptGuard {
|
||||
fn new() -> InterruptGuard {
|
||||
InterruptHandler::instance().block();
|
||||
InterruptGuard
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InterruptGuard {
|
||||
fn drop(&mut self) {
|
||||
InterruptHandler::instance().unblock();
|
||||
}
|
||||
}
|
219
src/justfile.rs
219
src/justfile.rs
@ -1,16 +1,17 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
|
||||
use edit_distance::edit_distance;
|
||||
|
||||
pub struct Justfile<'a> {
|
||||
pub recipes: Map<&'a str, Recipe<'a>>,
|
||||
pub recipes: Map<&'a str, Recipe<'a>>,
|
||||
pub assignments: Map<&'a str, Expression<'a>>,
|
||||
pub exports: Set<&'a str>,
|
||||
pub exports: Set<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
impl<'a, 'b> Justfile<'a>
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
pub fn first(&self) -> Option<&Recipe> {
|
||||
let mut first: Option<&Recipe> = None;
|
||||
for recipe in self.recipes.values() {
|
||||
@ -30,13 +31,15 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
}
|
||||
|
||||
pub fn suggest(&self, name: &str) -> Option<&'a str> {
|
||||
let mut suggestions = self.recipes.keys()
|
||||
let mut suggestions = self
|
||||
.recipes
|
||||
.keys()
|
||||
.map(|suggestion| (edit_distance(suggestion, name), suggestion))
|
||||
.collect::<Vec<_>>();
|
||||
suggestions.sort();
|
||||
if let Some(&(distance, suggestion)) = suggestions.first() {
|
||||
if distance < 3 {
|
||||
return Some(suggestion)
|
||||
return Some(suggestion);
|
||||
}
|
||||
}
|
||||
None
|
||||
@ -45,15 +48,20 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
pub fn run(
|
||||
&'a self,
|
||||
invocation_directory: Result<PathBuf, String>,
|
||||
arguments: &[&'a str],
|
||||
arguments: &[&'a str],
|
||||
configuration: &Configuration<'a>,
|
||||
) -> RunResult<'a, ()> {
|
||||
let unknown_overrides = configuration.overrides.keys().cloned()
|
||||
let unknown_overrides = configuration
|
||||
.overrides
|
||||
.keys()
|
||||
.cloned()
|
||||
.filter(|name| !self.assignments.contains_key(name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !unknown_overrides.is_empty() {
|
||||
return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides});
|
||||
return Err(RuntimeError::UnknownOverrides {
|
||||
overrides: unknown_overrides,
|
||||
});
|
||||
}
|
||||
|
||||
let dotenv = load_dotenv()?;
|
||||
@ -82,7 +90,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
|
||||
let mut missing = vec![];
|
||||
let mut grouped = vec![];
|
||||
let mut rest = arguments;
|
||||
let mut rest = arguments;
|
||||
|
||||
while let Some((argument, mut tail)) = rest.split_first() {
|
||||
if let Some(recipe) = self.recipes.get(argument) {
|
||||
@ -94,9 +102,9 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
if !argument_range.range_contains(argument_count) {
|
||||
return Err(RuntimeError::ArgumentCountMismatch {
|
||||
recipe: recipe.name,
|
||||
found: tail.len(),
|
||||
min: recipe.min_arguments(),
|
||||
max: recipe.max_arguments(),
|
||||
found: tail.len(),
|
||||
min: recipe.min_arguments(),
|
||||
max: recipe.max_arguments(),
|
||||
});
|
||||
}
|
||||
grouped.push((recipe, &tail[0..argument_count]));
|
||||
@ -114,12 +122,23 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion});
|
||||
return Err(RuntimeError::UnknownRecipes {
|
||||
recipes: missing,
|
||||
suggestion,
|
||||
});
|
||||
}
|
||||
|
||||
let mut ran = empty();
|
||||
for (recipe, arguments) in grouped {
|
||||
self.run_recipe(&invocation_directory, recipe, arguments, &scope, &dotenv, configuration, &mut ran)?
|
||||
self.run_recipe(
|
||||
&invocation_directory,
|
||||
recipe,
|
||||
arguments,
|
||||
&scope,
|
||||
&dotenv,
|
||||
configuration,
|
||||
&mut ran,
|
||||
)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -128,19 +147,34 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
fn run_recipe<'c>(
|
||||
&'c self,
|
||||
invocation_directory: &Result<PathBuf, String>,
|
||||
recipe: &Recipe<'a>,
|
||||
arguments: &[&'a str],
|
||||
scope: &Map<&'c str, String>,
|
||||
dotenv: &Map<String, String>,
|
||||
recipe: &Recipe<'a>,
|
||||
arguments: &[&'a str],
|
||||
scope: &Map<&'c str, String>,
|
||||
dotenv: &Map<String, String>,
|
||||
configuration: &Configuration<'a>,
|
||||
ran: &mut Set<&'a str>,
|
||||
ran: &mut Set<&'a str>,
|
||||
) -> RunResult<()> {
|
||||
for dependency_name in &recipe.dependencies {
|
||||
if !ran.contains(dependency_name) {
|
||||
self.run_recipe(invocation_directory, &self.recipes[dependency_name], &[], scope, dotenv, configuration, ran)?;
|
||||
self.run_recipe(
|
||||
invocation_directory,
|
||||
&self.recipes[dependency_name],
|
||||
&[],
|
||||
scope,
|
||||
dotenv,
|
||||
configuration,
|
||||
ran,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
recipe.run(invocation_directory, arguments, scope, dotenv, &self.exports, configuration)?;
|
||||
recipe.run(
|
||||
invocation_directory,
|
||||
arguments,
|
||||
scope,
|
||||
dotenv,
|
||||
&self.exports,
|
||||
configuration,
|
||||
)?;
|
||||
ran.insert(recipe.name);
|
||||
Ok(())
|
||||
}
|
||||
@ -182,8 +216,14 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn unknown_recipes() {
|
||||
match parse_success("a:\nb:\nc:").run(no_cwd_err(), &["a", "x", "y", "z"], &Default::default()).unwrap_err() {
|
||||
UnknownRecipes{recipes, suggestion} => {
|
||||
match parse_success("a:\nb:\nc:")
|
||||
.run(no_cwd_err(), &["a", "x", "y", "z"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
UnknownRecipes {
|
||||
recipes,
|
||||
suggestion,
|
||||
} => {
|
||||
assert_eq!(recipes, &["x", "y", "z"]);
|
||||
assert_eq!(suggestion, None);
|
||||
}
|
||||
@ -191,7 +231,6 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn run_shebang() {
|
||||
// this test exists to make sure that shebang recipes
|
||||
@ -210,12 +249,19 @@ a:
|
||||
x
|
||||
";
|
||||
|
||||
match parse_success(text).run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
|
||||
Code{recipe, line_number, code} => {
|
||||
match parse_success(text)
|
||||
.run(no_cwd_err(), &["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
Code {
|
||||
recipe,
|
||||
line_number,
|
||||
code,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(code, 200);
|
||||
assert_eq!(line_number, None);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
@ -223,12 +269,18 @@ a:
|
||||
#[test]
|
||||
fn code_error() {
|
||||
match parse_success("fail:\n @exit 100")
|
||||
.run(no_cwd_err(), &["fail"], &Default::default()).unwrap_err() {
|
||||
Code{recipe, line_number, code} => {
|
||||
.run(no_cwd_err(), &["fail"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
Code {
|
||||
recipe,
|
||||
line_number,
|
||||
code,
|
||||
} => {
|
||||
assert_eq!(recipe, "fail");
|
||||
assert_eq!(code, 100);
|
||||
assert_eq!(line_number, Some(2));
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
@ -239,38 +291,61 @@ a:
|
||||
a return code:
|
||||
@x() { {{return}} {{code + "0"}}; }; x"#;
|
||||
|
||||
match parse_success(text).run(no_cwd_err(), &["a", "return", "15"], &Default::default()).unwrap_err() {
|
||||
Code{recipe, line_number, code} => {
|
||||
match parse_success(text)
|
||||
.run(no_cwd_err(), &["a", "return", "15"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
Code {
|
||||
recipe,
|
||||
line_number,
|
||||
code,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(code, 150);
|
||||
assert_eq!(line_number, Some(3));
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_some_arguments() {
|
||||
match parse_success("a b c d:").run(no_cwd_err(), &["a", "b", "c"], &Default::default()).unwrap_err() {
|
||||
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||
match parse_success("a b c d:")
|
||||
.run(no_cwd_err(), &["a", "b", "c"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
ArgumentCountMismatch {
|
||||
recipe,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 2);
|
||||
assert_eq!(min, 3);
|
||||
assert_eq!(max, 3);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_some_arguments_variadic() {
|
||||
match parse_success("a b c +d:").run(no_cwd_err(), &["a", "B", "C"], &Default::default()).unwrap_err() {
|
||||
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||
match parse_success("a b c +d:")
|
||||
.run(no_cwd_err(), &["a", "B", "C"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
ArgumentCountMismatch {
|
||||
recipe,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 2);
|
||||
assert_eq!(min, 3);
|
||||
assert_eq!(max, usize::MAX - 1);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
@ -278,39 +353,62 @@ a return code:
|
||||
#[test]
|
||||
fn missing_all_arguments() {
|
||||
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||
.run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
|
||||
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||
.run(no_cwd_err(), &["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
ArgumentCountMismatch {
|
||||
recipe,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 0);
|
||||
assert_eq!(min, 3);
|
||||
assert_eq!(max, 3);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_some_defaults() {
|
||||
match parse_success("a b c d='hello':").run(no_cwd_err(), &["a", "b"], &Default::default()).unwrap_err() {
|
||||
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||
match parse_success("a b c d='hello':")
|
||||
.run(no_cwd_err(), &["a", "b"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
ArgumentCountMismatch {
|
||||
recipe,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 1);
|
||||
assert_eq!(min, 2);
|
||||
assert_eq!(max, 3);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_all_defaults() {
|
||||
match parse_success("a b c='r' d='h':").run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
|
||||
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||
match parse_success("a b c='r' d='h':")
|
||||
.run(no_cwd_err(), &["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
ArgumentCountMismatch {
|
||||
recipe,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 0);
|
||||
assert_eq!(min, 1);
|
||||
assert_eq!(max, 3);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
@ -321,10 +419,12 @@ a return code:
|
||||
configuration.overrides.insert("foo", "bar");
|
||||
configuration.overrides.insert("baz", "bob");
|
||||
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
|
||||
.run(no_cwd_err(), &["a"], &configuration).unwrap_err() {
|
||||
UnknownOverrides{overrides} => {
|
||||
.run(no_cwd_err(), &["a"], &configuration)
|
||||
.unwrap_err()
|
||||
{
|
||||
UnknownOverrides { overrides } => {
|
||||
assert_eq!(overrides, &["baz", "foo"]);
|
||||
},
|
||||
}
|
||||
other => panic!("expected a code run error, but got: {}", other),
|
||||
}
|
||||
}
|
||||
@ -346,11 +446,18 @@ wut:
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match parse_success(text).run(no_cwd_err(), &["wut"], &configuration).unwrap_err() {
|
||||
Code{code: _, line_number, recipe} => {
|
||||
match parse_success(text)
|
||||
.run(no_cwd_err(), &["wut"], &configuration)
|
||||
.unwrap_err()
|
||||
{
|
||||
Code {
|
||||
code: _,
|
||||
line_number,
|
||||
recipe,
|
||||
} => {
|
||||
assert_eq!(recipe, "wut");
|
||||
assert_eq!(line_number, Some(8));
|
||||
},
|
||||
}
|
||||
other => panic!("expected a recipe code errror, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
11
src/main.rs
11
src/main.rs
@ -1,10 +1,13 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate brev;
|
||||
extern crate clap;
|
||||
extern crate ctrlc;
|
||||
extern crate dotenv;
|
||||
extern crate edit_distance;
|
||||
extern crate env_logger;
|
||||
extern crate itertools;
|
||||
extern crate libc;
|
||||
extern crate regex;
|
||||
@ -16,6 +19,9 @@ extern crate unicode_width;
|
||||
#[macro_use]
|
||||
mod testing;
|
||||
|
||||
#[macro_use]
|
||||
mod die;
|
||||
|
||||
mod assignment_evaluator;
|
||||
mod assignment_resolver;
|
||||
mod color;
|
||||
@ -41,6 +47,7 @@ mod run;
|
||||
mod runtime_error;
|
||||
mod shebang;
|
||||
mod token;
|
||||
mod interrupt_handler;
|
||||
|
||||
use common::*;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
use common::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::{ExitStatus, Command, Stdio};
|
||||
|
||||
use platform::{Platform, PlatformInterface};
|
||||
@ -161,7 +160,7 @@ impl<'a> Recipe<'a> {
|
||||
command.export_environment_variables(scope, dotenv, exports)?;
|
||||
|
||||
// run it!
|
||||
match command.status() {
|
||||
match InterruptHandler::guard(|| command.status()) {
|
||||
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
||||
if code != 0 {
|
||||
return Err(RuntimeError::Code{recipe: self.name, line_number: None, code})
|
||||
@ -235,7 +234,7 @@ impl<'a> Recipe<'a> {
|
||||
|
||||
cmd.export_environment_variables(scope, dotenv, exports)?;
|
||||
|
||||
match cmd.status() {
|
||||
match InterruptHandler::guard(|| cmd.status()) {
|
||||
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
||||
if code != 0 {
|
||||
return Err(RuntimeError::Code{
|
||||
|
20
src/run.rs
20
src/run.rs
@ -1,22 +1,16 @@
|
||||
use common::*;
|
||||
|
||||
use std::{convert, ffi, cmp};
|
||||
use std::{convert, ffi};
|
||||
use clap::{App, Arg, ArgGroup, AppSettings};
|
||||
use configuration::DEFAULT_SHELL;
|
||||
use misc::maybe_s;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use env_logger;
|
||||
use interrupt_handler::InterruptHandler;
|
||||
|
||||
#[cfg(windows)]
|
||||
use ansi_term::enable_ansi_support;
|
||||
|
||||
macro_rules! die {
|
||||
($($arg:tt)*) => {{
|
||||
extern crate std;
|
||||
eprintln!($($arg)*);
|
||||
process::exit(EXIT_FAILURE)
|
||||
}};
|
||||
}
|
||||
|
||||
fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
|
||||
let editor = env::var_os("EDITOR")
|
||||
.unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
|
||||
@ -47,6 +41,10 @@ pub fn run() {
|
||||
#[cfg(windows)]
|
||||
enable_ansi_support().ok();
|
||||
|
||||
env_logger::Builder::from_env(
|
||||
env_logger::Env::new().filter("JUST_LOG").write_style("JUST_LOG_STYLE")
|
||||
).init();
|
||||
|
||||
let invocation_directory = env::current_dir()
|
||||
.map_err(|e| format!("Error getting current directory: {}", e));
|
||||
|
||||
@ -357,6 +355,10 @@ pub fn run() {
|
||||
overrides,
|
||||
};
|
||||
|
||||
if let Err(error) = InterruptHandler::install() {
|
||||
warn!("Failed to set CTRL-C handler: {}", error)
|
||||
}
|
||||
|
||||
if let Err(run_error) = justfile.run(
|
||||
invocation_directory,
|
||||
&arguments,
|
||||
|
81
tests/interrupts.rs
Normal file
81
tests/interrupts.rs
Normal file
@ -0,0 +1,81 @@
|
||||
extern crate brev;
|
||||
extern crate executable_path;
|
||||
extern crate libc;
|
||||
extern crate tempdir;
|
||||
|
||||
use executable_path::executable_path;
|
||||
use tempdir::TempDir;
|
||||
use std::{process::Command, time::{Duration, Instant}, thread};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn kill(process_id: u32) {
|
||||
unsafe {
|
||||
libc::kill(process_id as i32, libc::SIGINT);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn interrupt_test(justfile: &str) {
|
||||
let tmp = TempDir::new("just-interrupts")
|
||||
.unwrap_or_else(
|
||||
|err| panic!("integration test: failed to create temporary directory: {}", err));
|
||||
|
||||
let mut justfile_path = tmp.path().to_path_buf();
|
||||
justfile_path.push("justfile");
|
||||
brev::dump(justfile_path, justfile);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut child = Command::new(&executable_path("just"))
|
||||
.current_dir(&tmp)
|
||||
.spawn()
|
||||
.expect("just invocation failed");
|
||||
|
||||
thread::sleep(Duration::new(1, 0));
|
||||
|
||||
kill(child.id());
|
||||
|
||||
let status = child.wait().unwrap();
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if elapsed > Duration::new(2, 500_000_000) {
|
||||
panic!("process returned too late: {:?}", elapsed);
|
||||
}
|
||||
|
||||
if elapsed < Duration::new(1, 500_000_000) {
|
||||
panic!("process returned too early : {:?}", elapsed);
|
||||
}
|
||||
|
||||
assert_eq!(status.code(), Some(130));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn interrupt_shebang() {
|
||||
interrupt_test("
|
||||
default:
|
||||
#!/usr/bin/env sh
|
||||
sleep 2
|
||||
");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn interrupt_line() {
|
||||
interrupt_test("
|
||||
default:
|
||||
@sleep 2
|
||||
");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn interrupt_backtick() {
|
||||
interrupt_test("
|
||||
foo = `sleep 2`
|
||||
|
||||
default:
|
||||
@echo hello
|
||||
");
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
extern crate brev;
|
||||
extern crate executable_path;
|
||||
extern crate libc;
|
||||
extern crate target;
|
||||
extern crate tempdir;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user