diff --git a/src/lib.rs b/src/lib.rs index b8ca8ad..0e36856 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,53 +2,33 @@ //! //! # Usage //! -//! The following should open the given URL in a web browser +//! Open the given URL in the default web browser. //! -//! ```test_harness,no_run -//! extern crate open; -//! -//! # #[test] -//! # fn doit() { +//! ```no_run //! open::that("http://rust-lang.org").unwrap(); -//! # } //! ``` -//! Alternatively, specify the program to open something with. It should expect to receive the path or URL as first argument. -//! ```test_harness,no_run -//! extern crate open; //! -//! # #[test] -//! # fn doit() { +//! Alternatively, specify the program to be used to open the path or URL. +//! +//! ```no_run //! open::with("http://rust-lang.org", "firefox").unwrap(); -//! # } //! ``` //! //! # Notes //! -//! As an operating system program is used, chances are that the open operation fails. -//! Therefore, you are advised to at least check the result with `.is_err()` and -//! behave accordingly, e.g. by letting the user know what you tried to open, and failed. +//! As an operating system program is used, the open operation can fail. +//! Therefore, you are advised to at least check the result and behave +//! accordingly, e.g. by letting the user know that the open operation failed. //! -//! ``` -//! # fn doit() { -//! match open::that("http://rust-lang.org") { -//! Ok(exit_status) => { -//! if exit_status.success() { -//! println!("Look at your browser!"); -//! } else { -//! if let Some(code) = exit_status.code() { -//! println!("Command returned non-zero exit status {}!", code); -//! } else { -//! println!("Command returned with unknown exit status!"); -//! } -//! } -//! } -//! Err(why) => println!("Failure to execute command: {}", why), +//! ```no_run +//! let path = "http://rust-lang.org"; +//! +//! match open::that(path) { +//! Ok(()) => println!("Opened '{}' successfully.", path), +//! Err(err) => eprintln!("An error occurred when opening '{}': {}", path, err), //! } -//! # } //! ``` -use std::{ffi::OsStr, io, process::ExitStatus, thread}; - #[cfg(target_os = "windows")] pub use windows::{that, with}; @@ -83,11 +63,18 @@ pub use unix::{that, with}; )))] compile_error!("open is not supported on this platform"); +use std::{ + ffi::OsStr, + io, + process::{Command, Output, Stdio}, + thread, +}; + +type Result = io::Result<()>; + /// Convenience function for opening the passed path in a new thread. /// See documentation of `that(...)` for more details. -pub fn that_in_background + Sized>( - path: T, -) -> thread::JoinHandle> { +pub fn that_in_background + Sized>(path: T) -> thread::JoinHandle { let path = path.as_ref().to_os_string(); thread::spawn(|| that(path)) } @@ -95,25 +82,90 @@ pub fn that_in_background + Sized>( pub fn with_in_background + Sized>( path: T, app: impl Into, -) -> thread::JoinHandle> { +) -> thread::JoinHandle { let path = path.as_ref().to_os_string(); let app = app.into(); thread::spawn(|| with(path, app)) } +trait IntoResult { + fn into_result(self) -> T; +} + +impl IntoResult for io::Result { + fn into_result(self) -> Result { + match self { + Ok(o) if o.status.success() => Ok(()), + Ok(o) => Err(from_output(o)), + Err(err) => Err(err), + } + } +} + #[cfg(windows)] -mod windows { - use std::{ - ffi::OsStr, - io, - os::windows::{ffi::OsStrExt, process::ExitStatusExt}, - process::ExitStatus, - ptr, +impl IntoResult for winapi::ctypes::c_int { + fn into_result(self) -> Result { + match self { + i if i > 32 => Ok(()), + _ => Err(io::Error::last_os_error()), + } + } +} + +fn from_output(output: Output) -> io::Error { + let error_msg = match output.stderr.is_empty() { + true => output.status.to_string(), + false => format!( + "{} ({})", + String::from_utf8_lossy(&output.stderr).trim(), + output.status + ), }; + io::Error::new(io::ErrorKind::Other, error_msg) +} + +trait CommandExt { + fn output_stderr(&mut self) -> io::Result; +} + +impl CommandExt for Command { + fn output_stderr(&mut self) -> io::Result { + let mut process = self + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn()?; + + let status = process.wait()?; + + // Read up to 256 bytes from stderr. + use std::io::Read; + let mut stderr = vec![0; 256]; + let len = process + .stderr + .take() + .and_then(|mut err| err.read(&mut stderr).ok()) + .unwrap_or(0); + stderr.truncate(len); + + Ok(Output { + status, + stderr, + stdout: vec![], + }) + } +} + +#[cfg(windows)] +mod windows { + use std::{ffi::OsStr, io, os::windows::ffi::OsStrExt, ptr}; + use winapi::ctypes::c_int; use winapi::um::shellapi::ShellExecuteW; + use crate::{IntoResult, Result}; + fn convert_path(path: &OsStr) -> io::Result> { let mut maybe_result: Vec<_> = path.encode_wide().collect(); if maybe_result.iter().any(|&u| u == 0) { @@ -126,7 +178,7 @@ mod windows { Ok(maybe_result) } - pub fn that + Sized>(path: T) -> io::Result { + pub fn that + Sized>(path: T) -> Result { const SW_SHOW: c_int = 5; let path = convert_path(path.as_ref())?; @@ -141,17 +193,10 @@ mod windows { SW_SHOW, ) }; - if result as c_int > 32 { - Ok(ExitStatus::from_raw(0)) - } else { - Err(io::Error::last_os_error()) - } + (result as c_int).into_result() } - pub fn with + Sized>( - path: T, - app: impl Into, - ) -> io::Result { + pub fn with + Sized>(path: T, app: impl Into) -> Result { const SW_SHOW: c_int = 5; let path = convert_path(path.as_ref())?; @@ -169,67 +214,55 @@ mod windows { SW_SHOW, ) }; - if result as c_int > 32 { - Ok(ExitStatus::from_raw(0)) - } else { - Err(io::Error::last_os_error()) - } + (result as c_int).into_result() } } #[cfg(target_os = "macos")] mod macos { - use std::{ - ffi::OsStr, - io::Result, - process::{Command, ExitStatus, Stdio}, - }; + use std::{ffi::OsStr, process::Command}; - pub fn that + Sized>(path: T) -> Result { + use crate::{CommandExt, IntoResult, Result}; + + pub fn that + Sized>(path: T) -> Result { Command::new("open") - .stdout(Stdio::null()) - .stderr(Stdio::null()) .arg(path.as_ref()) - .spawn()? - .wait() + .output_stderr() + .into_result() } - pub fn with + Sized>(path: T, app: impl Into) -> Result { + pub fn with + Sized>(path: T, app: impl Into) -> Result { Command::new("open") .arg(path.as_ref()) .arg("-a") .arg(app.into()) - .spawn()? - .wait() + .output_stderr() + .into_result() } } #[cfg(target_os = "ios")] mod ios { - use std::{ - ffi::OsStr, - io::Result, - process::{Command, ExitStatus, Stdio}, - }; + use std::{ffi::OsStr, process::Command}; + + use crate::{CommandExt, IntoResult, Result}; - pub fn that + Sized>(path: T) -> Result { + pub fn that + Sized>(path: T) -> Result { Command::new("uiopen") - .stdout(Stdio::null()) - .stderr(Stdio::null()) .arg("--url") .arg(path.as_ref()) - .spawn()? - .wait() + .output_stderr() + .into_result() } - pub fn with + Sized>(path: T, app: impl Into) -> Result { + pub fn with + Sized>(path: T, app: impl Into) -> Result { Command::new("uiopen") .arg("--url") .arg(path.as_ref()) .arg("--bundleid") .arg(app.into()) - .spawn()? - .wait() + .output_stderr() + .into_result() } } @@ -246,12 +279,13 @@ mod unix { use std::{ env, ffi::{OsStr, OsString}, - io, path::{Path, PathBuf}, - process::{Command, ExitStatus, Stdio}, + process::Command, }; - pub fn that + Sized>(path: T) -> io::Result { + use crate::{CommandExt, IntoResult, Result}; + + pub fn that + Sized>(path: T) -> Result { let path = path.as_ref(); let open_handlers = [ ("xdg-open", &[path] as &[_]), @@ -262,32 +296,28 @@ mod unix { ]; let mut unsuccessful = None; - let mut error = None; + let mut io_error = None; for (command, args) in &open_handlers { - let result = Command::new(command) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(*args) - .status(); + let result = Command::new(command).args(*args).output_stderr(); match result { - Ok(status) if status.success() => return result, - Ok(_) => unsuccessful = unsuccessful.or(Some(result)), - Err(err) => error = error.or(Some(Err(err))), + Ok(o) if o.status.success() => return Ok(()), + Ok(o) => unsuccessful = unsuccessful.or_else(|| Some(crate::from_output(o))), + Err(err) => io_error = io_error.or(Some(err)), } } - unsuccessful - .or(error) - .expect("successful cases don't get here") + Err(unsuccessful + .or(io_error) + .expect("successful cases don't get here")) } - pub fn with + Sized>( - path: T, - app: impl Into, - ) -> io::Result { - Command::new(app.into()).arg(path.as_ref()).spawn()?.wait() + pub fn with + Sized>(path: T, app: impl Into) -> Result { + Command::new(app.into()) + .arg(path.as_ref()) + .output_stderr() + .into_result() } // Polyfill to workaround absolute path bug in wslu(wslview). In versions before diff --git a/src/main.rs b/src/main.rs index 66ca5ff..eabef0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,21 +10,10 @@ fn main() { }; match open::that(&path_or_url) { - Ok(status) if status.success() => (), - Ok(status) => match status.code() { - Some(code) => { - print_error_and_exit(code, &path_or_url, &format!("error code: {}", code)) - } - None => print_error_and_exit(3, &path_or_url, "error unknown"), - }, - Err(err) => print_error_and_exit(3, &path_or_url, &err.to_string()), + Ok(()) => println!("Opened '{}' successfully.", path_or_url), + Err(err) => { + eprintln!("An error occurred when opening '{}': {}", path_or_url, err); + process::exit(3); + } } } - -fn print_error_and_exit(code: i32, path: &str, error_message: &str) -> ! { - eprintln!( - "An error occurred when opening '{}': {}", - path, error_message - ); - process::exit(code); -}