diff --git a/src/app.rs b/src/app.rs index 3335691..7b20913 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,6 @@ extern crate ansi_term; extern crate atty; extern crate clap; extern crate libc; -extern crate regex; use ::prelude::*; use std::{convert, ffi}; @@ -179,7 +178,7 @@ pub fn app() { } } - let override_re = regex::Regex::new("^([^=]+)=(.*)$").unwrap(); + let override_re = Regex::new("^([^=]+)=(.*)$").unwrap(); let raw_arguments = matches.values_of("ARGUMENTS").map(|values| values.collect::>()) .unwrap_or_default(); diff --git a/src/lib.rs b/src/lib.rs index 7ee6bde..957c04d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,12 +14,16 @@ mod unit; #[cfg(test)] mod integration; +mod platform; + mod app; mod prelude { + pub use std::io::prelude::*; + pub use libc::{EXIT_FAILURE, EXIT_SUCCESS}; + pub use regex::Regex; pub use std::path::Path; pub use std::{cmp, env, fs, fmt, io, iter, process}; - pub use libc::{EXIT_FAILURE, EXIT_SUCCESS}; } use prelude::*; @@ -27,13 +31,11 @@ use prelude::*; pub use app::app; use app::UseColor; -use regex::Regex; use std::borrow::Cow; use std::collections::{BTreeMap as Map, BTreeSet as Set}; use std::fmt::Display; -use std::io::prelude::*; use std::ops::Range; -use std::os::unix::fs::PermissionsExt; +use platform::{Platform, PlatformInterface}; macro_rules! warn { ($($arg:tt)*) => {{ @@ -47,7 +49,7 @@ macro_rules! die { ($($arg:tt)*) => {{ extern crate std; warn!($($arg)*); - std::process::exit(EXIT_FAILURE) + process::exit(EXIT_FAILURE) }}; } @@ -65,6 +67,25 @@ impl Slurp for fs::File { } } +/// Split a shebang line into a command and an optional argument +fn split_shebang(shebang: &str) -> Option<(&str, Option<&str>)> { + lazy_static! { + static ref EMPTY: Regex = re(r"^#!\s*$"); + static ref SIMPLE: Regex = re(r"^#!(\S+)\s*$"); + static ref ARGUMENT: Regex = re(r"^#!(\S+)\s+(\S.*?)?\s*$"); + } + + if EMPTY.is_match(shebang) { + Some(("", None)) + } else if let Some(captures) = SIMPLE.captures(shebang) { + Some((captures.at(1).unwrap(), None)) + } else if let Some(captures) = ARGUMENT.captures(shebang) { + Some((captures.at(1).unwrap(), Some(captures.at(2).unwrap()))) + } else { + None + } +} + fn re(pattern: &str) -> Regex { Regex::new(pattern).unwrap() } @@ -178,48 +199,31 @@ impl<'a> Display for Expression<'a> { } } -#[cfg(unix)] +/// Return a RunError::Signal if the process was terminated by a signal, +/// otherwise return an RunError::UnknownFailure fn error_from_signal( recipe: &str, line_number: Option, exit_status: process::ExitStatus ) -> RunError { - use std::os::unix::process::ExitStatusExt; - match exit_status.signal() { + match Platform::signal_from_exit_status(exit_status) { Some(signal) => RunError::Signal{recipe: recipe, line_number: line_number, signal: signal}, None => RunError::UnknownFailure{recipe: recipe, line_number: line_number}, } } -#[cfg(windows)] -fn error_from_signal( - recipe: &str, - line_number: Option, - exit_status: process::ExitStatus -) -> RunError { - RunError::UnknownFailure{recipe: recipe, line_number: line_number} -} - -#[cfg(unix)] +/// Return a RunError::BacktickSignal if the process was terminated by signal, +/// otherwise return a RunError::BacktickUnknownFailure fn backtick_error_from_signal<'a>( token: &Token<'a>, exit_status: process::ExitStatus ) -> RunError<'a> { - use std::os::unix::process::ExitStatusExt; - match exit_status.signal() { + match Platform::signal_from_exit_status(exit_status) { Some(signal) => RunError::BacktickSignal{token: token.clone(), signal: signal}, None => RunError::BacktickUnknownFailure{token: token.clone()}, } } -#[cfg(windows)] -fn backtick_error_from_signal<'a>( - token: &Token<'a>, - exit_status: process::ExitStatus -) -> RunError<'a> { - RunError::BacktickUnknownFailure{token: token.clone()} -} - fn export_env<'a>( command: &mut process::Command, scope: &Map<&'a str, String>, @@ -237,7 +241,6 @@ fn export_env<'a>( Ok(()) } - fn run_backtick<'a>( raw: &str, token: &Token<'a>, @@ -383,22 +386,27 @@ impl<'a> Recipe<'a> { .map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})?; } - // get current permissions - let mut perms = fs::metadata(&path) - .map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})? - .permissions(); - // make the script executable - let current_mode = perms.mode(); - perms.set_mode(current_mode | 0o100); - fs::set_permissions(&path, perms) + Platform::set_execute_permission(&path) .map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})?; - // run it! - let mut command = process::Command::new(path); + let shebang_line = evaluated_lines.first() + .ok_or_else(|| RunError::InternalError { + message: "evaluated_lines was empty".to_string() + })?; + let (shebang_command, shebang_argument) = split_shebang(shebang_line) + .ok_or_else(|| RunError::InternalError { + message: format!("bad shebang line: {}", shebang_line) + })?; + + // create a command to run the script + let mut command = Platform::make_shebang_command(&path, shebang_command, shebang_argument); + + // export environment variables export_env(&mut command, scope, exports)?; + // run it! match command.status() { Ok(exit_status) => if let Some(code) = exit_status.code() { if code != 0 { diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..007ce12 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,66 @@ +use ::prelude::*; + +pub struct Platform; + +pub trait PlatformInterface { + /// Construct a command equivelant to running the script at `path` with the + /// shebang line `shebang` + fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) -> process::Command; + + /// Set the execute permission on the file pointed to by `path` + fn set_execute_permission(path: &Path) -> Result<(), io::Error>; + + /// Extract the signal from a process exit status, if it was terminated by a signal + fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option; +} + +#[cfg(unix)] +impl PlatformInterface for Platform { + fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>) -> process::Command { + // shebang scripts can be executed directly on unix + process::Command::new(path) + } + + fn set_execute_permission(path: &Path) -> Result<(), io::Error> { + use std::os::unix::fs::PermissionsExt; + + // get current permissions + let mut permissions = fs::metadata(&path)?.permissions(); + + // set the execute bit + let current_mode = permissions.mode(); + permissions.set_mode(current_mode | 0o100); + + // set the new permissions + fs::set_permissions(&path, permissions) + } + + fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option { + use std::os::unix::process::ExitStatusExt; + exit_status.signal() + } +} + +#[cfg(windows)] +impl PlatformInterface for Platform { + fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) -> process::Command { + let mut cmd = process::Command::new(command); + if let Some(argument) = argument { + cmd.arg(argument); + } + cmd.arg(path); + cmd + } + + fn set_execute_permission(_path: &Path) -> Result<(), io::Error> { + // it is not necessary to set an execute permission on a script on windows, + // so this is a nop + Ok(()) + } + + fn signal_from_exit_status(_exit_status: process::ExitStatus) -> Option { + // The rust standard library does not expose a way to extract a signal + // from a process exit status, so just return None + None + } +} diff --git a/src/unit.rs b/src/unit.rs index f339d2d..b0af0e1 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -1111,3 +1111,23 @@ fn readme_test() { parse_success(&justfile); } } + +#[test] +fn split_shebang() { + use ::split_shebang; + + fn check(shebang: &str, expected_split: Option<(&str, Option<&str>)>) { + assert_eq!(split_shebang(shebang), expected_split); + } + + check("#! ", Some(("", None ))); + check("#!", Some(("", None ))); + check("#!/bin/bash", Some(("/bin/bash", None ))); + check("#!/bin/bash ", Some(("/bin/bash", None ))); + check("#!/usr/bin/env python", Some(("/usr/bin/env", Some("python" )))); + check("#!/usr/bin/env python ", Some(("/usr/bin/env", Some("python" )))); + check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x" )))); + check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x")))); + check("#!/usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x")))); + check("#/usr/bin/env python \t-x\t", None ); +}