diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 0c5709a65..e9b6f8bd4 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" path = "src/nohup.rs" [dependencies] -getopts = "0.2.18" +clap = "2.33" libc = "0.2.42" uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["fs"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 5fce208da..afbf2541b 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -10,6 +10,7 @@ #[macro_use] extern crate uucore; +use clap::{App, AppSettings, Arg}; use libc::{c_char, dup2, execvp, signal}; use libc::{SIGHUP, SIG_IGN}; use std::env; @@ -20,50 +21,42 @@ use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; use uucore::fs::{is_stderr_interactive, is_stdin_interactive, is_stdout_interactive}; -static NAME: &str = "nohup"; static VERSION: &str = env!("CARGO_PKG_VERSION"); +static ABOUT: &str = "Run COMMAND ignoring hangup signals."; +static LONG_HELP: &str = " +If standard input is terminal, it'll be replaced with /dev/null. +If standard output is terminal, it'll be appended to nohup.out instead, +or $HOME/nohup.out, if nohup.out open failed. +If standard error is terminal, it'll be redirected to stdout. +"; +static NOHUP_OUT: &str = "nohup.out"; +// exit codes that match the GNU implementation +static EXIT_CANCELED: i32 = 125; +static EXIT_CANNOT_INVOKE: i32 = 126; +static EXIT_ENOENT: i32 = 127; +static POSIX_NOHUP_FAILURE: i32 = 127; -#[cfg(target_vendor = "apple")] -extern "C" { - fn _vprocmgr_detach_from_console(flags: u32) -> *const libc::c_int; -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int { - std::ptr::null() +mod options { + pub const CMD: &str = "cmd"; } pub fn uumain(args: impl uucore::Args) -> i32 { - let args = args.collect_str(); + let usage = get_usage(); - let mut opts = getopts::Options::new(); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&usage[..]) + .after_help(LONG_HELP) + .arg( + Arg::with_name(options::CMD) + .hidden(true) + .required(true) + .multiple(true), + ) + .setting(AppSettings::TrailingVarArg) + .get_matches_from(args); - opts.optflag("h", "help", "Show help and exit"); - opts.optflag("V", "version", "Show version and exit"); - - let matches = match opts.parse(&args[1..]) { - Ok(m) => m, - Err(f) => { - show_error!("{}", f); - show_usage(&opts); - return 1; - } - }; - - if matches.opt_present("V") { - println!("{} {}", NAME, VERSION); - return 0; - } - if matches.opt_present("h") { - show_usage(&opts); - return 0; - } - - if matches.free.is_empty() { - show_error!("Missing operand: COMMAND"); - println!("Try `{} --help` for more information.", NAME); - return 1; - } replace_fds(); unsafe { signal(SIGHUP, SIG_IGN) }; @@ -73,13 +66,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 { }; let cstrs: Vec = matches - .free - .iter() + .values_of(options::CMD) + .unwrap() .map(|x| CString::new(x.as_bytes()).unwrap()) .collect(); let mut args: Vec<*const c_char> = cstrs.iter().map(|s| s.as_ptr()).collect(); args.push(std::ptr::null()); - unsafe { execvp(args[0], args.as_mut_ptr()) } + + let ret = unsafe { execvp(args[0], args.as_mut_ptr()) }; + match ret { + libc::ENOENT => EXIT_ENOENT, + _ => EXIT_CANNOT_INVOKE, + } } fn replace_fds() { @@ -108,23 +106,32 @@ fn replace_fds() { } fn find_stdout() -> File { + let internal_failure_code = match std::env::var("POSIXLY_CORRECT") { + Ok(_) => POSIX_NOHUP_FAILURE, + Err(_) => EXIT_CANCELED, + }; + match OpenOptions::new() .write(true) .create(true) .append(true) - .open(Path::new("nohup.out")) + .open(Path::new(NOHUP_OUT)) { Ok(t) => { - show_warning!("Output is redirected to: nohup.out"); + show_info!("ignoring input and appending output to '{}'", NOHUP_OUT); t } - Err(e) => { + Err(e1) => { let home = match env::var("HOME") { - Err(_) => crash!(2, "Cannot replace STDOUT: {}", e), + Err(_) => { + show_info!("failed to open '{}': {}", NOHUP_OUT, e1); + exit!(internal_failure_code) + } Ok(h) => h, }; let mut homeout = PathBuf::from(home); - homeout.push("nohup.out"); + homeout.push(NOHUP_OUT); + let homeout_str = homeout.to_str().unwrap(); match OpenOptions::new() .write(true) .create(true) @@ -132,30 +139,29 @@ fn find_stdout() -> File { .open(&homeout) { Ok(t) => { - show_warning!("Output is redirected to: {:?}", homeout); + show_info!("ignoring input and appending output to '{}'", homeout_str); t } - Err(e) => crash!(2, "Cannot replace STDOUT: {}", e), + Err(e2) => { + show_info!("failed to open '{}': {}", NOHUP_OUT, e1); + show_info!("failed to open '{}': {}", homeout_str, e2); + exit!(internal_failure_code) + } } } } } -fn show_usage(opts: &getopts::Options) { - let msg = format!( - "{0} {1} - -Usage: - {0} COMMAND [ARG]... - {0} OPTION - -Run COMMAND ignoring hangup signals. -If standard input is terminal, it'll be replaced with /dev/null. -If standard output is terminal, it'll be appended to nohup.out instead, -or $HOME/nohup.out, if nohup.out open failed. -If standard error is terminal, it'll be redirected to stdout.", - NAME, VERSION - ); - - print!("{}", opts.usage(&msg)); +fn get_usage() -> String { + format!("{0} COMMAND [ARG]...\n {0} FLAG", executable!()) +} + +#[cfg(target_vendor = "apple")] +extern "C" { + fn _vprocmgr_detach_from_console(flags: u32) -> *const libc::c_int; +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int { + std::ptr::null() } diff --git a/tests/by-util/test_nohup.rs b/tests/by-util/test_nohup.rs index 651491045..b98ae007c 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -1 +1,19 @@ -// ToDO: add tests +use crate::common::util::*; +use std::thread::sleep; + +// General observation: nohup.out will not be created in tests run by cargo test +// because stdin/stdout is not attached to a TTY. +// All that can be tested is the side-effects. + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_vendor = "apple"))] +fn test_nohup_multiple_args_and_flags() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.args(&["touch", "-t", "1006161200", "file1", "file2"]) + .succeeds(); + sleep(std::time::Duration::from_millis(10)); + + assert!(at.file_exists("file1")); + assert!(at.file_exists("file2")); +}