#![cfg_attr(docsrs, feature(doc_cfg))]
use cfg_if::cfg_if;
use either::Either;
use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, StdinLock, StdoutLock, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
cfg_if! {
if #[cfg(feature = "serde")] {
use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
}
}
cfg_if! {
if #[cfg(feature = "tokio")] {
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
use tokio_util::either::Either as AsyncEither;
use tokio_stream::wrappers::LinesStream;
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum InputArg {
#[default]
Stdin,
Path(PathBuf),
}
impl InputArg {
pub fn from_arg<S: Into<PathBuf>>(arg: S) -> InputArg {
let arg = arg.into();
if arg == Path::new("-") {
InputArg::Stdin
} else {
InputArg::Path(arg)
}
}
pub fn is_stdin(&self) -> bool {
self == &InputArg::Stdin
}
pub fn is_path(&self) -> bool {
matches!(self, InputArg::Path(_))
}
pub fn path_ref(&self) -> Option<&PathBuf> {
match self {
InputArg::Stdin => None,
InputArg::Path(p) => Some(p),
}
}
pub fn path_mut(&mut self) -> Option<&mut PathBuf> {
match self {
InputArg::Stdin => None,
InputArg::Path(p) => Some(p),
}
}
pub fn into_path(self) -> Option<PathBuf> {
match self {
InputArg::Stdin => None,
InputArg::Path(p) => Some(p),
}
}
pub fn open(&self) -> io::Result<InputArgReader> {
Ok(match self {
InputArg::Stdin => Either::Left(io::stdin().lock()),
InputArg::Path(p) => Either::Right(BufReader::new(fs::File::open(p)?)),
})
}
pub fn read(&self) -> io::Result<Vec<u8>> {
match self {
InputArg::Stdin => {
let mut vec = Vec::new();
io::stdin().lock().read_to_end(&mut vec)?;
Ok(vec)
}
InputArg::Path(p) => fs::read(p),
}
}
pub fn read_to_string(&self) -> io::Result<String> {
match self {
InputArg::Stdin => io::read_to_string(io::stdin().lock()),
InputArg::Path(p) => fs::read_to_string(p),
}
}
pub fn lines(&self) -> io::Result<Lines> {
Ok(self.open()?.lines())
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl InputArg {
pub async fn async_open(&self) -> io::Result<AsyncInputArgReader> {
Ok(match self {
InputArg::Stdin => AsyncEither::Left(tokio::io::stdin()),
InputArg::Path(p) => AsyncEither::Right(tokio::fs::File::open(p).await?),
})
}
pub async fn async_read(&self) -> io::Result<Vec<u8>> {
match self {
InputArg::Stdin => {
let mut vec = Vec::new();
tokio::io::stdin().read_to_end(&mut vec).await?;
Ok(vec)
}
InputArg::Path(p) => tokio::fs::read(p).await,
}
}
pub async fn async_read_to_string(&self) -> io::Result<String> {
match self {
InputArg::Stdin => {
let mut s = String::new();
tokio::io::stdin().read_to_string(&mut s).await?;
Ok(s)
}
InputArg::Path(p) => tokio::fs::read_to_string(p).await,
}
}
pub async fn async_lines(&self) -> io::Result<AsyncLines> {
Ok(LinesStream::new(
tokio::io::BufReader::new(self.async_open().await?).lines(),
))
}
}
impl fmt::Display for InputArg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InputArg::Stdin => {
if f.alternate() {
write!(f, "<stdin>")
} else {
write!(f, "-")
}
}
InputArg::Path(p) => write!(f, "{}", p.display()),
}
}
}
impl<S: Into<PathBuf>> From<S> for InputArg {
fn from(s: S) -> InputArg {
InputArg::from_arg(s)
}
}
impl FromStr for InputArg {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<InputArg, Self::Err> {
Ok(InputArg::from_arg(s))
}
}
impl From<InputArg> for OsString {
fn from(arg: InputArg) -> OsString {
match arg {
InputArg::Stdin => OsString::from("-"),
InputArg::Path(p) => p.into(),
}
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl Serialize for InputArg {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
InputArg::Stdin => "-".serialize(serializer),
InputArg::Path(p) => p.serialize(serializer),
}
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'de> Deserialize<'de> for InputArg {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
PathBuf::deserialize(deserializer).map(InputArg::from_arg)
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum OutputArg {
#[default]
Stdout,
Path(PathBuf),
}
impl OutputArg {
pub fn from_arg<S: Into<PathBuf>>(arg: S) -> OutputArg {
let arg = arg.into();
if arg == Path::new("-") {
OutputArg::Stdout
} else {
OutputArg::Path(arg)
}
}
pub fn is_stdout(&self) -> bool {
self == &OutputArg::Stdout
}
pub fn is_path(&self) -> bool {
matches!(self, OutputArg::Path(_))
}
pub fn path_ref(&self) -> Option<&PathBuf> {
match self {
OutputArg::Stdout => None,
OutputArg::Path(p) => Some(p),
}
}
pub fn path_mut(&mut self) -> Option<&mut PathBuf> {
match self {
OutputArg::Stdout => None,
OutputArg::Path(p) => Some(p),
}
}
pub fn into_path(self) -> Option<PathBuf> {
match self {
OutputArg::Stdout => None,
OutputArg::Path(p) => Some(p),
}
}
pub fn create(&self) -> io::Result<OutputArgWriter> {
Ok(match self {
OutputArg::Stdout => Either::Left(io::stdout().lock()),
OutputArg::Path(p) => Either::Right(fs::File::create(p)?),
})
}
pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> io::Result<()> {
match self {
OutputArg::Stdout => io::stdout().lock().write_all(contents.as_ref()),
OutputArg::Path(p) => fs::write(p, contents),
}
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl OutputArg {
pub async fn async_create(&self) -> io::Result<AsyncOutputArgWriter> {
Ok(match self {
OutputArg::Stdout => AsyncEither::Left(tokio::io::stdout()),
OutputArg::Path(p) => AsyncEither::Right(tokio::fs::File::create(p).await?),
})
}
#[allow(clippy::future_not_send)] pub async fn async_write<C: AsRef<[u8]>>(&self, contents: C) -> io::Result<()> {
match self {
OutputArg::Stdout => {
let mut stdout = tokio::io::stdout();
stdout.write_all(contents.as_ref()).await?;
stdout.flush().await
}
OutputArg::Path(p) => tokio::fs::write(p, contents).await,
}
}
}
impl fmt::Display for OutputArg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OutputArg::Stdout => {
if f.alternate() {
write!(f, "<stdout>")
} else {
write!(f, "-")
}
}
OutputArg::Path(p) => write!(f, "{}", p.display()),
}
}
}
impl<S: Into<PathBuf>> From<S> for OutputArg {
fn from(s: S) -> OutputArg {
OutputArg::from_arg(s)
}
}
impl FromStr for OutputArg {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<OutputArg, Self::Err> {
Ok(OutputArg::from_arg(s))
}
}
impl From<OutputArg> for OsString {
fn from(arg: OutputArg) -> OsString {
match arg {
OutputArg::Stdout => OsString::from("-"),
OutputArg::Path(p) => p.into(),
}
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl Serialize for OutputArg {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
OutputArg::Stdout => "-".serialize(serializer),
OutputArg::Path(p) => p.serialize(serializer),
}
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'de> Deserialize<'de> for OutputArg {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
PathBuf::deserialize(deserializer).map(OutputArg::from_arg)
}
}
pub type InputArgReader = Either<StdinLock<'static>, BufReader<fs::File>>;
pub type OutputArgWriter = Either<StdoutLock<'static>, fs::File>;
pub type Lines = io::Lines<InputArgReader>;
cfg_if! {
if #[cfg(feature = "tokio")] {
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub type AsyncInputArgReader = AsyncEither<tokio::io::Stdin, tokio::fs::File>;
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub type AsyncOutputArgWriter = AsyncEither<tokio::io::Stdout, tokio::fs::File>;
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub type AsyncLines = LinesStream<tokio::io::BufReader<AsyncInputArgReader>>;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
mod inputarg {
use super::*;
#[test]
fn test_assert_stdin_from_osstring() {
let s = OsString::from("-");
let p = InputArg::from(s);
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_osstring() {
let s = OsString::from("./-");
let p = InputArg::from(s);
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_stdin_from_osstr() {
let s = OsStr::new("-");
let p = InputArg::from(s);
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_osstr() {
let s = OsStr::new("./-");
let p = InputArg::from(s);
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_stdin_from_pathbuf() {
let s = PathBuf::from("-");
let p = InputArg::from(s);
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_pathbuf() {
let s = PathBuf::from("./-");
let p = InputArg::from(s);
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_stdin_from_path() {
let s = Path::new("-");
let p = InputArg::from(s);
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_path() {
let s = Path::new("./-");
let p = InputArg::from(s);
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_stdin_from_string() {
let s = String::from("-");
let p = InputArg::from(s);
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_string() {
let s = String::from("./-");
let p = InputArg::from(s);
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_stdin_from_str() {
let p = InputArg::from("-");
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_str() {
let p = InputArg::from("./-");
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_assert_parse_stdin() {
let p = "-".parse::<InputArg>().unwrap();
assert!(p.is_stdin());
assert!(!p.is_path());
}
#[test]
fn test_assert_parse_path() {
let p = "./-".parse::<InputArg>().unwrap();
assert!(!p.is_stdin());
assert!(p.is_path());
}
#[test]
fn test_default() {
assert_eq!(InputArg::default(), InputArg::Stdin);
}
#[test]
fn test_stdin_path_ref() {
let p = InputArg::Stdin;
assert_eq!(p.path_ref(), None);
}
#[test]
fn test_path_path_ref() {
let p = InputArg::Path(PathBuf::from("-"));
assert_eq!(p.path_ref(), Some(&PathBuf::from("-")));
}
#[test]
fn test_stdin_path_mut() {
let mut p = InputArg::Stdin;
assert_eq!(p.path_mut(), None);
}
#[test]
fn test_path_path_mut() {
let mut p = InputArg::Path(PathBuf::from("-"));
assert_eq!(p.path_mut(), Some(&mut PathBuf::from("-")));
}
#[test]
fn test_stdin_into_path() {
let p = InputArg::Stdin;
assert_eq!(p.into_path(), None);
}
#[test]
fn test_path_into_path() {
let p = InputArg::Path(PathBuf::from("-"));
assert_eq!(p.into_path(), Some(PathBuf::from("-")));
}
#[test]
fn test_display_stdin() {
let p = InputArg::Stdin;
assert_eq!(p.to_string(), "-");
}
#[test]
fn test_display_alternate_stdin() {
let p = InputArg::Stdin;
assert_eq!(format!("{p:#}"), "<stdin>");
}
#[test]
fn test_display_path() {
let p = InputArg::from_arg("./-");
assert_eq!(p.to_string(), "./-");
}
#[test]
fn test_stdin_into_osstring() {
let p = InputArg::Stdin;
assert_eq!(OsString::from(p), OsString::from("-"));
}
#[test]
fn test_path_into_osstring() {
let p = InputArg::Path(PathBuf::from("./-"));
assert_eq!(OsString::from(p), OsString::from("./-"));
}
#[cfg(feature = "serde")]
mod serding {
use super::*;
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
struct Input {
path: InputArg,
}
#[test]
fn test_stdin_to_json() {
let val = Input {
path: InputArg::Stdin,
};
assert_eq!(serde_json::to_string(&val).unwrap(), r#"{"path":"-"}"#);
}
#[test]
fn test_path_to_json() {
let val = Input {
path: InputArg::Path(PathBuf::from("foo.txt")),
};
assert_eq!(
serde_json::to_string(&val).unwrap(),
r#"{"path":"foo.txt"}"#
);
}
#[test]
fn test_stdin_from_json() {
let s = r#"{"path": "-"}"#;
assert_eq!(
serde_json::from_str::<Input>(s).unwrap(),
Input {
path: InputArg::Stdin
}
);
}
#[test]
fn test_path_from_json() {
let s = r#"{"path": "./-"}"#;
assert_eq!(
serde_json::from_str::<Input>(s).unwrap(),
Input {
path: InputArg::Path(PathBuf::from("./-"))
}
);
}
}
}
mod outputarg {
use super::*;
#[test]
fn test_assert_stdout_from_osstring() {
let s = OsString::from("-");
let p = OutputArg::from(s);
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_osstring() {
let s = OsString::from("./-");
let p = OutputArg::from(s);
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_stdout_from_osstr() {
let s = OsStr::new("-");
let p = OutputArg::from(s);
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_osstr() {
let s = OsStr::new("./-");
let p = OutputArg::from(s);
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_stdout_from_pathbuf() {
let s = PathBuf::from("-");
let p = OutputArg::from(s);
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_pathbuf() {
let s = PathBuf::from("./-");
let p = OutputArg::from(s);
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_stdout_from_path() {
let s = Path::new("-");
let p = OutputArg::from(s);
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_path() {
let s = Path::new("./-");
let p = OutputArg::from(s);
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_stdout_from_string() {
let s = String::from("-");
let p = OutputArg::from(s);
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_string() {
let s = String::from("./-");
let p = OutputArg::from(s);
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_stdout_from_str() {
let p = OutputArg::from("-");
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_path_from_str() {
let p = OutputArg::from("./-");
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_assert_parse_stdin() {
let p = "-".parse::<OutputArg>().unwrap();
assert!(p.is_stdout());
assert!(!p.is_path());
}
#[test]
fn test_assert_parse_path() {
let p = "./-".parse::<OutputArg>().unwrap();
assert!(!p.is_stdout());
assert!(p.is_path());
}
#[test]
fn test_default() {
assert_eq!(OutputArg::default(), OutputArg::Stdout);
}
#[test]
fn test_stdout_path_ref() {
let p = OutputArg::Stdout;
assert_eq!(p.path_ref(), None);
}
#[test]
fn test_path_path_ref() {
let p = OutputArg::Path(PathBuf::from("-"));
assert_eq!(p.path_ref(), Some(&PathBuf::from("-")));
}
#[test]
fn test_stdout_path_mut() {
let mut p = OutputArg::Stdout;
assert_eq!(p.path_mut(), None);
}
#[test]
fn test_path_path_mut() {
let mut p = OutputArg::Path(PathBuf::from("-"));
assert_eq!(p.path_mut(), Some(&mut PathBuf::from("-")));
}
#[test]
fn test_stdout_into_path() {
let p = OutputArg::Stdout;
assert_eq!(p.into_path(), None);
}
#[test]
fn test_path_into_path() {
let p = OutputArg::Path(PathBuf::from("-"));
assert_eq!(p.into_path(), Some(PathBuf::from("-")));
}
#[test]
fn test_display_stdout() {
let p = OutputArg::Stdout;
assert_eq!(p.to_string(), "-");
}
#[test]
fn test_display_alternate_stdout() {
let p = OutputArg::Stdout;
assert_eq!(format!("{p:#}"), "<stdout>");
}
#[test]
fn test_display_path() {
let p = OutputArg::from_arg("./-");
assert_eq!(p.to_string(), "./-");
}
#[test]
fn test_stdout_into_osstring() {
let p = OutputArg::Stdout;
assert_eq!(OsString::from(p), OsString::from("-"));
}
#[test]
fn test_path_into_osstring() {
let p = OutputArg::Path(PathBuf::from("./-"));
assert_eq!(OsString::from(p), OsString::from("./-"));
}
#[cfg(feature = "tokio")]
#[test]
fn test_async_write_is_send_if_content_is_send() {
fn require_send<T: Send>(_t: T) {}
let p = OutputArg::default();
let fut = p.async_write(b"This arg is Send.");
require_send(fut);
}
#[cfg(feature = "serde")]
mod serding {
use super::*;
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
struct Output {
path: OutputArg,
}
#[test]
fn test_stdout_to_json() {
let val = Output {
path: OutputArg::Stdout,
};
assert_eq!(serde_json::to_string(&val).unwrap(), r#"{"path":"-"}"#);
}
#[test]
fn test_path_to_json() {
let val = Output {
path: OutputArg::Path(PathBuf::from("foo.txt")),
};
assert_eq!(
serde_json::to_string(&val).unwrap(),
r#"{"path":"foo.txt"}"#
);
}
#[test]
fn test_stdout_from_json() {
let s = r#"{"path": "-"}"#;
assert_eq!(
serde_json::from_str::<Output>(s).unwrap(),
Output {
path: OutputArg::Stdout
}
);
}
#[test]
fn test_path_from_json() {
let s = r#"{"path": "./-"}"#;
assert_eq!(
serde_json::from_str::<Output>(s).unwrap(),
Output {
path: OutputArg::Path(PathBuf::from("./-"))
}
);
}
}
}
}