pax_global_header00006660000000000000000000000064145361501110014507gustar00rootroot0000000000000052 comment=576c7ea695f432a48a518dcf845c42d7224b3aff clio-0.3.5/000077500000000000000000000000001453615011100124425ustar00rootroot00000000000000clio-0.3.5/.github/000077500000000000000000000000001453615011100140025ustar00rootroot00000000000000clio-0.3.5/.github/workflows/000077500000000000000000000000001453615011100160375ustar00rootroot00000000000000clio-0.3.5/.github/workflows/rust.yml000066400000000000000000000026361453615011100175660ustar00rootroot00000000000000name: CI on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: doc: name: Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install latest nightly uses: actions-rs/toolchain@v1 with: toolchain: nightly profile: minimal - name: Spell Check Repo uses: crate-ci/typos@master - name: Format run: cargo fmt -- --check - name: Docs run: cargo +nightly doc --features http-ureq,clap-parse env: RUSTDOCFLAGS: --cfg docsrs - uses: baptiste0928/cargo-install@v2 with: crate: cargo-msrv - name: Verify minimum rust version run: cargo msrv verify build: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }}-latest strategy: matrix: os: [ubuntu, windows, macOS] steps: - uses: actions/checkout@v2 - name: Check run: cargo check --tests --features http-ureq - name: Check run: cargo check --tests --features clap-parse - name: Check run: cargo check --tests --features http-curl - name: Clippy ureq run: cargo clippy --no-deps --features http-ureq - name: Clippy clap & curl run: cargo clippy --no-deps --features http-curl,clap-parse - name: Run tests with default features (i.e. none) run: cargo test - name: Run clap & curl tests run: cargo test --features http-curl,clap-parse clio-0.3.5/.gitignore000066400000000000000000000000231453615011100144250ustar00rootroot00000000000000/target Cargo.lock clio-0.3.5/Cargo.toml000066400000000000000000000022071453615011100143730ustar00rootroot00000000000000[package] name = "clio" description = "A library for parsing CLI file names" keywords = ["cli", "stdin", "stdout"] authors = ["AJ Bagwell "] license = "MIT" version = "0.3.5" repository = "https://github.com/aj-bagwell/clio" documentation = "https://docs.rs/clio" readme = "README.md" edition = "2021" rust-version = "1.63.0" [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["http-ureq", "clap-parse"] [features] http = ["url"] http-curl = ["curl", "pipe", "http"] http-ureq = ["ureq", "pipe", "http"] clap-parse = ["clap"] [dependencies] curl = { version = "0.4", optional = true } ureq = { version = "2.0", optional = true } pipe = { version = "0.4", optional = true } clap = { version = ">=3.2, < 5.0", features = ["derive"], optional = true} url = { version = "2.3.1", optional = true } cfg-if = "1.0.0" tempfile = "3.3.0" walkdir = "2.3.3" is-terminal = "0.4.9" [target.'cfg(unix)'.dependencies] libc = "0.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.42", features = ["Win32_Foundation"] } [dev-dependencies] clap = { version = "4.3.0", features = ["derive"] } either = "1.8.1" clio-0.3.5/README.md000066400000000000000000000125561453615011100137320ustar00rootroot00000000000000# clio clio is a rust library for parsing CLI file names. It implements the standard unix conventions of when the file name is `"-"` then sending the data to stdin/stdout as appropriate. With the [`clap-parse`](#clap-parse) feature it also adds a bunch of useful filters to validate paths from command line parameters, e.g. it exists or is/isn't a directory. # Usage [`Input`](crate::Input)s and [`Output`](crate::Input)s can be created directly from args in [`args_os`](std::env::args_os). They will error if the file cannot be opened for any reason ```rust // a cat replacement fn main() -> clio::Result<()> { for arg in std::env::args_os().skip(1) { let mut input = clio::Input::new(&arg)?; std::io::copy(&mut input, &mut std::io::stdout())?; } Ok(()) } ``` If you want to defer opening the file you can use [`InputPath`](crate::InputPath)s and [`OutputPath`](crate::OutputPath)s. This avoid leaving empty Output files around if you error out very early. These check that the path exists, is a file and could in theory be opened when created to get nicer error messages from clap. Since that leaves room for [TOCTTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) bugs, they will still return a [`Err`](std::result::Result::Err) if something has changed when it comes time to actually open the file. With the [`clap-parse`](#clap-parse) feature they are also designed to be used with [clap 3.2+](https://docs.rs/clap). See the [older docs](https://docs.rs/clio/0.2.2/clio/index.html#usage) for examples of older [clap](https://docs.rs/clap)/[structopt](https://docs.rs/structopt) ```rust # #[cfg(feature="clap-parse")]{ use clap::Parser; use clio::*; use std::io::Write; #[derive(Parser)] #[clap(name = "cat")] struct Opt { /// Input file, use '-' for stdin #[clap(value_parser, default_value="-")] input: Input, /// Output file '-' for stdout #[clap(long, short, value_parser, default_value="-")] output: Output, /// Directory to store log files in #[clap(long, short, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")] log_dir: ClioPath, } fn main() { let mut opt = Opt::parse(); let mut log = opt.log_dir.join("cat.log").create().unwrap_or(Output::std_err()); match std::io::copy(&mut opt.input, &mut opt.output) { Ok(len) => writeln!(log, "Copied {} bytes", len), Err(e) => writeln!(log, "Error {:?}", e), }; } # } ``` # Alternative crates ## Nameless [Nameless](https://docs.rs/nameless) is an alternative to clap that provides full-service command-line parsing. This means you just write a main function with arguments with the types you want, add a conventional documentation comment, and it uses the magic of procedural macros to take care of the rest. It's input and output streams have the many of the same features as clio (e.g. '-' for stdin) but also support transparently decompressing inputs, and more remote options such as `scp://` ## Patharg If you are as horified as I am by the amount of code in this crate for what feels like it should have been a very simple task, then [`patharg`](https://docs.rs/patharg) is a much lighter crate that works with clap for treating '-' as stdin/stdout. It does not open the file, or otherwise validate the path until you ask it avoiding [TOCTTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) issues but in the process looses the nice clap error messages. It also avoids a whole pile of complexity for dealing with seeking and guessing up front if the input supports seeking. Also watch out patharg has no custom clap ValueParser so older versions of clap will convert via a String so path will need to be valid utf-8 which is not guarnatied by linux nor windows. ## Either If all you really need is support mapping `'-'` to `stdin()` try this lovely function distilled from [`patharg`](https://docs.rs/patharg). It works because [either](https://docs.rs/either) has helpfully added `impl`s for many common traits when both sides implement them. ```rust use either::Either; use std::io; use std::ffi::OsStr; use std::fs::File; pub fn open(path: &OsStr) -> io::Result { Ok(if path == "-" { Either::Left(io::stdin().lock()) } else { Either::Right(io::BufReader::new(File::open(path)?)) }) } ``` The corresponding `create` function is left as an exercise for the reader. # Features ### `clap-parse` Implements [`ValueParserFactory`](https://docs.rs/clap/latest/clap/builder/trait.ValueParserFactory.html) for all the types and adds a bad implementation of [`Clone`] to all types as well to keep `clap` happy. ## HTTP Client If a url is passed to [`Input::new`](crate::Input::new) then it will perform and HTTP `GET`. This has the advantage vs just piping in the output of curl as you know the input size, and can infer related urls, e.g. get the `Cargo.lock` to match the `Cargo.toml`. If a url is passed to [`Output::new`](crate::Output::new) then it will perform and HTTP `PUT`. The main advantage over just piping to curl is you can use [`OutputPath::create_with_len`](crate::OutputPath::create_with_len) to set the size before the upload starts e.g. needed if you are sending a file to S3. ### `http-ureq` bundles in [ureq](https://docs.rs/ureq) as a HTTP client. ### `http-curl` bundles in [curl](https://docs.rs/curl) as a HTTP client. clio-0.3.5/actions.sh000077500000000000000000000005611453615011100144430ustar00rootroot00000000000000#!/bin/bash set -euo pipefail dir=$(dirname $0) actions="$dir/.github/workflows/rust.yml" bold=$(tput bold) normal=$(tput sgr0) for step in $(yq '.jobs.build.steps[] | select(.run) | @json | @base64' "$actions"); do name=$(echo $step | base64 -d | jq -r .name) run=$(echo $step | base64 -d | jq -r .run) echo "$bold==== $name ====$normal" $run done clio-0.3.5/examples/000077500000000000000000000000001453615011100142605ustar00rootroot00000000000000clio-0.3.5/examples/cat.rs000066400000000000000000000013211453615011100153720ustar00rootroot00000000000000use clio::*; #[cfg(feature = "clap-parser")] use clap::Parser; #[cfg(feature = "clap-parser")] #[derive(Parser)] #[clap(name = "cat")] struct Opt { /// Input file, use '-' for stdin #[clap(value_parser, default_value = "-")] input: Input, /// Output file '-' for stdout #[clap(long, short, value_parser, default_value = "-")] output: Output, } #[cfg(feature = "clap-parser")] fn main() { let mut opt = Opt::parse(); std::io::copy(&mut opt.input, &mut opt.output).unwrap(); } #[cfg(not(feature = "clap-parser"))] fn main() { for arg in std::env::args_os() { let mut input = Input::new(&arg).unwrap(); std::io::copy(&mut input, &mut Output::std()).unwrap(); } } clio-0.3.5/publish.sh000077500000000000000000000003221453615011100144440ustar00rootroot00000000000000#!/bin/bash set -euo pipefail dir=$(dirname $0) $dir/actions.sh git push cargo publish v=$(sed -nr 's/^version = "([0-9.]+)"$/\1/p' Cargo.toml) git tag -a "v$v" -m "Release version $v"; git push origin v$v clio-0.3.5/src/000077500000000000000000000000001453615011100132315ustar00rootroot00000000000000clio-0.3.5/src/clapers.rs000066400000000000000000000174651453615011100152450ustar00rootroot00000000000000//! implementation of TypedValueParser for clio types so that they can be //! used with clap `value_parser` //! //! This module is only compiled if you enable the clap-parse feature use crate::{assert_exists, assert_is_dir, assert_not_dir, ClioPath, Error, Result}; use clap::builder::TypedValueParser; use clap::error::ErrorKind; use std::ffi::OsStr; use std::marker::PhantomData; /// A clap parser that converts [`&OsStr`](std::ffi::OsStr) to an [`Input`](crate::Input) or [`Output`](crate::Output) #[derive(Copy, Clone, Debug)] pub struct OsStrParser { exists: Option, is_dir: Option, is_file: Option, is_tty: Option, atomic: bool, default_name: Option<&'static str>, phantom: PhantomData, } impl OsStrParser { pub(crate) fn new() -> Self { OsStrParser { exists: None, is_dir: None, is_file: None, is_tty: None, default_name: None, atomic: false, phantom: PhantomData, } } /// This path must exist pub fn exists(mut self) -> Self { self.exists = Some(true); self } /// If this path exists it must point to a directory pub fn is_dir(mut self) -> Self { self.is_dir = Some(true); self.is_file = None; self } /// If this path exists it must point to a file pub fn is_file(mut self) -> Self { self.is_dir = None; self.is_file = Some(true); self } /// If this path is for stdin/stdout they must be a pipe not a tty pub fn not_tty(mut self) -> Self { self.is_tty = Some(false); self } /// Make writing atomic, by writing to a temp file then doing an /// atomic swap pub fn atomic(mut self) -> Self { self.atomic = true; self } /// The default name to use for the file if the path is a directory pub fn default_name(mut self, name: &'static str) -> Self { self.default_name = Some(name); self } fn validate(&self, value: &OsStr) -> Result { let mut path = ClioPath::new(value)?; path.atomic = self.atomic; if path.is_local() { if let Some(name) = self.default_name { if path.is_dir() || path.ends_with_slash() { path.push(name) } } if self.is_dir == Some(true) && path.exists() { assert_is_dir(&path)?; } if self.is_file == Some(true) { assert_not_dir(&path)?; } if self.exists == Some(true) { assert_exists(&path)?; } } else if self.is_dir == Some(true) { return Err(Error::not_dir_error()); } else if self.is_tty == Some(false) && path.is_tty() { return Err(Error::other( "blocked reading from stdin because it is a tty", )); } Ok(path) } } impl TypedValueParser for OsStrParser where for<'a> T: TryFrom, T: Clone + Sync + Send + 'static, { type Value = T; fn parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &OsStr, ) -> core::result::Result { self.validate(value).and_then(T::try_from).map_err(|orig| { cmd.clone().error( ErrorKind::InvalidValue, if let Some(arg) = arg { format!( "Invalid value for {}: Could not open {:?}: {}", arg, value, orig ) } else { format!("Could not open {:?}: {}", value, orig) }, ) }) } } impl TypedValueParser for OsStrParser { type Value = ClioPath; fn parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &OsStr, ) -> core::result::Result { self.validate(value).map_err(|orig| { cmd.clone().error( ErrorKind::InvalidValue, if let Some(arg) = arg { format!( "Invalid value for {}: Invalid path {:?}: {}", arg, value, orig ) } else { format!("Invalid path {:?}: {}", value, orig) }, ) }) } } #[cfg(test)] mod tests { use super::*; use std::fs::{create_dir, write}; use tempfile::{tempdir, TempDir}; fn temp() -> TempDir { let tmp = tempdir().expect("could not make tmp dir"); create_dir(&tmp.path().join("dir")).expect("could not create dir"); write(&tmp.path().join("file"), "contents").expect("could not create dir"); tmp } #[test] fn test_path_exists() { let tmp = temp(); let validator = OsStrParser::::new().exists(); validator .validate(tmp.path().join("file").as_os_str()) .unwrap(); validator .validate(tmp.path().join("dir").as_os_str()) .unwrap(); validator .validate(tmp.path().join("dir/").as_os_str()) .unwrap(); assert!(validator .validate(tmp.path().join("dir/missing").as_os_str()) .is_err()); } #[test] fn test_path_is_file() { let tmp = temp(); let validator = OsStrParser::::new().is_file(); validator .validate(tmp.path().join("file").as_os_str()) .unwrap(); validator .validate(tmp.path().join("dir/missing").as_os_str()) .unwrap(); validator.validate(OsStr::new("-")).unwrap(); assert!(validator .validate(tmp.path().join("dir/").as_os_str()) .is_err()); assert!(validator .validate(tmp.path().join("missing-dir/").as_os_str()) .is_err()); } #[test] fn test_path_is_existing_file() { let tmp = temp(); let validator = OsStrParser::::new().exists().is_file(); validator .validate(tmp.path().join("file").as_os_str()) .unwrap(); assert!(validator .validate(tmp.path().join("dir/missing").as_os_str()) .is_err()); assert!(validator .validate(tmp.path().join("dir/").as_os_str()) .is_err()); } #[test] fn test_path_is_dir() { let tmp = temp(); let validator = OsStrParser::::new().is_dir(); validator .validate(tmp.path().join("dir").as_os_str()) .unwrap(); validator .validate(tmp.path().join("dir/missing").as_os_str()) .unwrap(); assert!(validator .validate(tmp.path().join("file").as_os_str()) .is_err()); assert!(validator.validate(OsStr::new("-")).is_err()); } #[test] fn test_default_name() { let tmp = temp(); let validator = OsStrParser::::new().default_name("default.txt"); assert_eq!( validator .validate(tmp.path().join("dir").as_os_str()) .unwrap() .file_name() .unwrap(), "default.txt" ); assert_eq!( validator .validate(tmp.path().join("dir/file").as_os_str()) .unwrap() .file_name() .unwrap(), "file" ); assert_eq!( validator .validate(tmp.path().join("missing-dir/").as_os_str()) .unwrap() .file_name() .unwrap(), "default.txt" ); } } clio-0.3.5/src/error.rs000066400000000000000000000102051453615011100147260ustar00rootroot00000000000000use std::convert::{From, Infallible}; use std::ffi::{OsStr, OsString}; use std::fmt::Display; use std::io::Error as IoError; use std::io::ErrorKind; use tempfile::PersistError; /// Any error that happens when opening a stream. #[derive(Debug)] pub enum Error { /// the [`io::Error`](IoError) returned by the os when opening the file Io(IoError), #[cfg(feature = "http")] /// the HTTP response code and message returned by the sever /// /// code 499 may be returned in some instances when the connection to /// the server did not complete. Http { /// [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors) code: u16, /// the error message returned by the server message: String, }, } /// A result with a [`clio::Error`](Error) pub type Result = std::result::Result; macro_rules! io_error { ($func_name:ident, $unix:ident, $win:ident => ($kind:ident, $des:literal)) => { // When io_error_more graduates from nightly these can use the right kind directly #[cfg(unix)] pub(crate) fn $func_name() -> Error { Error::Io(IoError::from_raw_os_error(libc::$unix)) } #[cfg(windows)] pub(crate) fn $func_name() -> Error { Error::Io(IoError::from_raw_os_error( windows_sys::Win32::Foundation::$win as i32, )) } #[cfg(not(any(unix, windows)))] pub(crate) fn $func_name() -> Error { Error::Io(IoError::new(ErrorKind::$kind, $des)) } }; } impl Error { pub(crate) fn to_os_string(&self, path: &OsStr) -> OsString { let mut str = OsString::new(); str.push("Error opening "); str.push(path); str.push(": "); str.push(self.to_string()); str } /// Returns the corresponding [`ErrorKind`] for this error. pub fn kind(&self) -> ErrorKind { match self { Error::Io(err) => err.kind(), #[cfg(feature = "http")] Error::Http { code, message: _ } => match code { 404 | 410 => ErrorKind::NotFound, 401 | 403 => ErrorKind::PermissionDenied, _ => ErrorKind::Other, }, } } pub(crate) fn other(message: &'static str) -> Self { Error::Io(IoError::new(ErrorKind::Other, message)) } io_error!(seek_error, ESPIPE, ERROR_BROKEN_PIPE => (Other, "Cannot seek on stream")); io_error!(dir_error, EISDIR, ERROR_INVALID_NAME => (PermissionDenied, "Is a directory")); io_error!(not_dir_error, ENOTDIR, ERROR_ACCESS_DENIED => (PermissionDenied, "Is not a Directory")); io_error!(permission_error, EACCES, ERROR_ACCESS_DENIED => (PermissionDenied, "Permission denied")); io_error!(not_found_error, ENOENT, ERROR_FILE_NOT_FOUND => (NotFound, "The system cannot find the path specified.")); } impl From for Error { fn from(_err: Infallible) -> Self { unreachable!("Infallible should not exist") } } impl From for Error { fn from(err: PersistError) -> Self { Error::Io(err.error) } } impl From for Error { fn from(err: IoError) -> Self { Error::Io(err) } } impl From for Error { fn from(err: walkdir::Error) -> Self { Error::Io(err.into()) } } impl From for IoError { fn from(err: Error) -> Self { match err { Error::Io(err) => err, #[cfg(feature = "http")] Error::Http { .. } => IoError::new(err.kind(), err.to_string()), } } } #[cfg(feature = "http")] impl From for Error { fn from(err: url::ParseError) -> Self { Error::Http { code: 400, message: err.to_string(), } } } impl std::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { Error::Io(err) => err.fmt(f), #[cfg(feature = "http")] Error::Http { code, message } => write!(f, "{}: {}", code, message), } } } clio-0.3.5/src/http/000077500000000000000000000000001453615011100142105ustar00rootroot00000000000000clio-0.3.5/src/http/curl.rs000066400000000000000000000122411453615011100155230ustar00rootroot00000000000000use curl::easy::{Easy, ReadError}; use curl::Error; use pipe::{PipeBufWriter, PipeReader}; use std::convert::TryFrom; use std::fmt::{self, Debug}; use std::io::{Read, Write}; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::mpsc::{sync_channel, Receiver}; use std::sync::{Arc, Mutex}; use std::thread::spawn; pub struct HttpWriter { write: PipeBufWriter, rx: Mutex>>, } impl HttpWriter { pub fn new(url: &str, size: Option) -> Result { let mut easy = new_easy(url)?; let (mut read, write) = pipe::pipe_buffered(); let (done_tx, rx) = sync_channel(0); let connected_tx = done_tx.clone(); let mut connected = false; easy.put(true)?; easy.upload(true)?; if let Some(size) = size { easy.in_filesize(size)?; } easy.read_function(move |into| { if !connected { connected_tx.send(Ok(())).map_err(|_| ReadError::Abort)?; connected = true; } let len = read.read(into).unwrap(); eprintln!("read: {}", len); Ok(len) })?; spawn(move || { done_tx.send(easy.perform()).unwrap(); }); rx.recv().unwrap()?; let rx = Mutex::new(rx); Ok(HttpWriter { write, rx }) } pub fn finish(self) -> Result<(), Error> { drop(self.write); self.rx .try_lock() .expect("clio HttpReader lock should one ever be taken once while dropping") .recv() .unwrap()?; Ok(()) } } impl Write for HttpWriter { fn write(&mut self, buffer: &[u8]) -> Result { self.write.write(buffer) } fn flush(&mut self) -> Result<(), std::io::Error> { self.write.flush() } } impl fmt::Debug for HttpWriter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpWriter").finish() } } pub struct HttpReader { length: Option, read: PipeReader, rx: Mutex>>, } impl HttpReader { pub fn new(url: &str) -> Result { let url = url.to_owned(); let (read, mut write) = pipe::pipe(); let (done_tx, rx) = sync_channel(0); let connected_tx = done_tx.clone(); let mut connected = false; let length = Arc::new(AtomicI64::new(-1)); let mut easy = new_easy(&url)?; easy.header_function({ let length = length.clone(); move |data| { let data = std::str::from_utf8(data).unwrap().to_lowercase(); if let Some(length_string) = data.strip_prefix("content-length:") { length.store( length_string.trim().parse::().unwrap_or(-1), Ordering::Relaxed, ); } if data.starts_with("http/") { length.store(-1, Ordering::Relaxed); } true } })?; easy.write_function({ let length = length.clone(); move |data| { if !connected { if data.is_empty() { length.store(-1, Ordering::Relaxed); } if connected_tx.send(Ok(())).is_err() { // if the message queue is broken return 0 to curl to indicate a problem return Ok(0); } connected = true; } if write.write_all(data).is_err() { // if the pipe is broken return 0 to curl to indicate a problem return Ok(0); } Ok(data.len()) } })?; spawn(move || { let err = easy.perform(); drop(easy); done_tx.send(err).unwrap(); }); rx.recv().unwrap()?; let rx = Mutex::new(rx); let length = u64::try_from(length.load(Ordering::Relaxed)).ok(); Ok(HttpReader { length, read, rx }) } pub fn len(&self) -> Option { self.length } #[allow(dead_code)] pub fn finish(self) -> Result<(), Error> { drop(self.read); self.rx .try_lock() .expect("clio HttpWriter lock should one ever be taken once while dropping") .recv() .unwrap()?; Ok(()) } } impl Read for HttpReader { fn read(&mut self, buffer: &mut [u8]) -> Result { self.read.read(buffer) } } impl Debug for HttpReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpReader").finish() } } fn new_easy(url: &str) -> Result { let mut easy = Easy::new(); easy.url(url)?; easy.follow_location(true)?; easy.fail_on_error(true)?; Ok(easy) } #[cfg(feature = "http")] impl From for crate::Error { fn from(err: Error) -> Self { crate::Error::Http { code: 499, message: err.description().to_owned(), } } } clio-0.3.5/src/http/mod.rs000066400000000000000000000012351453615011100153360ustar00rootroot00000000000000#[cfg(feature = "http-curl")] mod curl; #[cfg(feature = "http-curl")] pub use self::curl::*; #[cfg(feature = "http-ureq")] mod ureq; #[cfg(feature = "http-ureq")] pub use self::ureq::*; use crate::{Error, Result}; use std::ffi::OsStr; use url::Url; pub(crate) fn try_to_url(url: &OsStr) -> Result { if let Some(str) = url.to_str() { Ok(Url::parse(str)?) } else { Err(Error::Http { code: 400, message: "url is not a valid UTF8 string".to_string(), }) } } pub(crate) fn is_http(url: &OsStr) -> bool { let url = url.to_string_lossy(); url.starts_with("http://") || url.starts_with("https://") } clio-0.3.5/src/http/ureq.rs000066400000000000000000000102451453615011100155340ustar00rootroot00000000000000use crate::{Error, Result}; use pipe::{PipeBufWriter, PipeReader}; use std::fmt::{self, Debug}; use std::io::{Error as IoError, ErrorKind, Read, Result as IoResult, Write}; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::Mutex; use std::thread::spawn; pub struct HttpWriter { write: PipeBufWriter, rx: Mutex>>, } /// A wrapper for the read end of the pipe that sniches on when data is first read /// by sending `Ok(())` down tx. /// /// This is used so that we can block the code making the put request until ethier: /// a) the data is tried to be read, or /// b) the request fails before trying to send the payload (bad hostname, invalid auth, etc) struct SnitchingReader { read: PipeReader, connected: bool, tx: SyncSender>, } impl Read for SnitchingReader { fn read(&mut self, buffer: &mut [u8]) -> IoResult { if !self.connected { self.tx .send(Ok(())) .map_err(|e| IoError::new(ErrorKind::Other, e))?; self.connected = true; } self.read.read(buffer) } } impl HttpWriter { pub fn new(url: &str, size: Option) -> Result { let (read, write) = pipe::pipe_buffered(); let mut req = ureq::put(url); if let Some(size) = size { req = req.set("content-length", &size.to_string()); } let (done_tx, rx) = sync_channel(0); let snitch = SnitchingReader { read, connected: false, tx: done_tx.clone(), }; spawn(move || { done_tx .send(req.send(snitch).map(|_| ()).map_err(|e| e.into())) .unwrap(); }); // either Ok(()) if the other thread started reading or the connection error rx.recv().unwrap()?; let rx = Mutex::new(rx); Ok(HttpWriter { write, rx }) } pub fn finish(self) -> Result<()> { drop(self.write); self.rx .try_lock() .expect("clio HttpWriter lock should one ever be taken once while dropping") .recv() .unwrap()?; Ok(()) } } impl Write for HttpWriter { fn write(&mut self, buffer: &[u8]) -> IoResult { self.write.write(buffer) } fn flush(&mut self) -> IoResult<()> { self.write.flush() } } impl fmt::Debug for HttpWriter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpWriter").finish() } } pub struct HttpReader { length: Option, #[cfg(feature = "clap-parse")] read: Mutex>, #[cfg(not(feature = "clap-parse"))] read: Box, } impl HttpReader { pub fn new(url: &str) -> Result { let resp = ureq::get(url).call()?; let length = resp .header("content-length") .and_then(|x| x.parse::().ok()); Ok(HttpReader { length, #[cfg(not(feature = "clap-parse"))] read: Box::new(resp.into_reader()), #[cfg(feature = "clap-parse")] read: Mutex::new(Box::new(resp.into_reader())), }) } pub fn len(&self) -> Option { self.length } } impl Read for HttpReader { #[cfg(not(feature = "clap-parse"))] fn read(&mut self, buffer: &mut [u8]) -> IoResult { self.read.read(buffer) } #[cfg(feature = "clap-parse")] fn read(&mut self, buffer: &mut [u8]) -> IoResult { self.read .lock() .map_err(|_| IoError::new(ErrorKind::Other, "Error locking HTTP reader"))? .read(buffer) } } impl Debug for HttpReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpReader").finish() } } impl From for Error { fn from(err: ureq::Error) -> Self { match err { ureq::Error::Status(code, resp) => Error::Http { code, message: resp.status_text().to_owned(), }, _ => Error::Http { code: 499, message: err.to_string(), }, } } } clio-0.3.5/src/input.rs000066400000000000000000000326661453615011100147530ustar00rootroot00000000000000#[cfg(feature = "http")] use crate::http::HttpReader; use crate::path::{ClioPathEnum, InOut}; use crate::{ assert_exists, assert_not_dir, assert_readable, impl_try_from, is_fifo, ClioPath, Error, Result, }; use is_terminal::IsTerminal; use std::convert::TryFrom; use std::ffi::OsStr; use std::fmt::{self, Debug, Display}; use std::fs::File; use std::io::{self, BufRead, BufReader, Cursor, Read, Result as IoResult, Seek, Stdin}; /// An enum that represents a command line input stream, /// either [`Stdin`] or [`File`] /// /// It is designed to be used with the [`clap` crate](https://docs.rs/clap/latest) when taking a file name as an /// argument to CLI app /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::Input; /// /// #[derive(Parser)] /// struct Opt { /// /// path to file, use '-' for stdin /// #[clap(value_parser)] /// input_file: Input, /// } /// # } /// ``` #[derive(Debug)] pub struct Input { path: ClioPath, stream: InputStream, } #[derive(Debug)] enum InputStream { /// a [`Stdin`] when the path was `-` Stdin(Stdin), /// a [`File`] representing the named pipe e.g. if called with `<(cat /dev/null)` Pipe(File), /// a normal [`File`] opened from the path File(File), #[cfg(feature = "http")] #[cfg_attr(docsrs, doc(cfg(feature = "http")))] /// a reader that will download response from the HTTP server Http(HttpReader), } impl Input { /// Constructs a new input either by opening the file or for '-' returning stdin pub fn new>(path: S) -> Result where crate::Error: From<>::Error>, { let path = path.try_into()?; let stream = match &path.path { ClioPathEnum::Std(_) => InputStream::Stdin(io::stdin()), ClioPathEnum::Local(file_path) => { let file = File::open(file_path)?; if file.metadata()?.is_dir() { return Err(Error::dir_error()); } if is_fifo(&file.metadata()?) { InputStream::Pipe(file) } else { InputStream::File(file) } } #[cfg(feature = "http")] ClioPathEnum::Http(url) => InputStream::Http(HttpReader::new(url.as_str())?), }; Ok(Input { path, stream }) } /// Constructs a new input for stdin pub fn std() -> Self { Input { path: ClioPath::std().with_direction(InOut::In), stream: InputStream::Stdin(io::stdin()), } } /// Constructs a new input either by opening the file or for '-' returning stdin /// /// The error is converted to a [`OsString`](std::ffi::OsString) so that [stuctopt](https://docs.rs/structopt/latest/structopt/#custom-string-parsers) can show it to the user. /// /// It is recommended that you use [`TryFrom::try_from`] and [clap 3.0](https://docs.rs/clap/latest/clap/index.html) instead. pub fn try_from_os_str(path: &OsStr) -> std::result::Result { TryFrom::try_from(path).map_err(|e: Error| e.to_os_string(path)) } /// If input is a file, returns the size of the file, in bytes /// otherwise if input is stdin returns none. /// /// # Examples /// /// ```no_run /// let file = clio::Input::new("foo.txt").unwrap(); /// /// assert_eq!(Some(3), file.len()); /// ``` pub fn len(&self) -> Option { match &self.stream { InputStream::Stdin(_) => None, InputStream::Pipe(_) => None, InputStream::File(file) => file.metadata().ok().map(|x| x.len()), #[cfg(feature = "http")] InputStream::Http(http) => http.len(), } } /// If input is a file, returns a reference to the file, /// otherwise if input is stdin or a pipe returns none. pub fn get_file(&mut self) -> Option<&mut File> { match &mut self.stream { InputStream::File(file) => Some(file), _ => None, } } /// Returns a boolean saying if the file is empty, if using stdin returns None /// /// # Examples /// /// ```no_run /// let file = clio::Input::new("foo.txt").unwrap(); /// /// assert_eq!(Some(true), file.is_empty()); /// ``` pub fn is_empty(&self) -> Option { self.len().map(|l| l == 0) } /// If the input is std in [locks](std::io::Stdin::lock) it, otherwise wraps the file in a buffered reader. /// This is useful to get the line iterator of the [`BufRead`](std::io::BufRead). /// /// # Examples /// /// ```no_run /// use std::io::BufRead; /// # fn main() -> Result<(), clio::Error> { /// let mut file = clio::Input::new("-")?; /// /// for line in file.lock().lines() { /// println!("line is: {}", line?); /// } /// # Ok(()) /// # } /// ``` pub fn lock<'a>(&'a mut self) -> Box { match &mut self.stream { InputStream::Stdin(stdin) => Box::new(stdin.lock()), InputStream::Pipe(pipe) => Box::new(BufReader::new(pipe)), InputStream::File(file) => Box::new(BufReader::new(file)), #[cfg(feature = "http")] InputStream::Http(http) => Box::new(BufReader::new(http)), } } /// Returns the path/url used to create the input pub fn path(&self) -> &ClioPath { &self.path } /// Returns true if this [`Input`] reads from stdin pub fn is_std(&self) -> bool { matches!(self.stream, InputStream::Stdin(_)) } /// Returns true if this [`Input`] points to the local file system, /// as opposed to point to stdin or a URL pub fn is_local(&self) -> bool { self.path.is_local() } /// Returns true if this is stdin and it is connected to a tty pub fn is_tty(&self) -> bool { self.is_std() && std::io::stdin().is_terminal() } /// Returns `true` if this [`Input`] is a file, /// and `false` if this [`Input`] is std out or a pipe pub fn can_seek(&self) -> bool { matches!(self.stream, InputStream::File(_)) } } impl_try_from!(Input); impl Read for Input { fn read(&mut self, buf: &mut [u8]) -> IoResult { match &mut self.stream { InputStream::Stdin(stdin) => stdin.read(buf), InputStream::Pipe(pipe) => pipe.read(buf), InputStream::File(file) => file.read(buf), #[cfg(feature = "http")] InputStream::Http(reader) => reader.read(buf), } } } impl Seek for Input { fn seek(&mut self, pos: io::SeekFrom) -> IoResult { match &mut self.stream { InputStream::Pipe(pipe) => pipe.seek(pos), InputStream::File(file) => file.seek(pos), _ => Err(Error::seek_error().into()), } } } /// A struct that contains all the components of a command line input stream, /// either std in or a file. /// /// It is designed to be used with the [`clap` crate](https://docs.rs/clap/latest) when taking a file name as an /// argument to CLI app /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::CachedInput; /// /// #[derive(Parser)] /// struct Opt { /// /// path to file, use '-' for stdin /// #[clap(value_parser)] /// input_file: CachedInput, /// } /// # } /// ``` #[derive(Debug, Clone)] pub struct CachedInput { path: ClioPath, data: Cursor>, } impl CachedInput { /// Reads all the data from an file (stdin for "-") into memory and stores it in a new CachedInput. /// If it detects it is trying to read from a TTY then it will return an error. /// /// Useful if you want to use the input twice (see [reset](Self::reset)), or /// need to know the size. /// /// This is mostly a wrapper around `Input::read_all()` so so that any errors /// reading the data will be shown automatically with claps pretty error formatting. pub fn new>(path: S) -> Result where crate::Error: From<>::Error>, { let mut source = Input::new(path)?; if source.is_tty() { return Err(Error::other( "blocked reading from stdin because it is a tty", )); } let capacity = source.len().unwrap_or(4096) as usize; let mut data = Cursor::new(Vec::with_capacity(capacity)); io::copy(&mut source, &mut data)?; data.set_position(0); Ok(CachedInput { path: source.path, data, }) } /// Reads all the data from stdin into memory and stores it in a new CachedInput. /// /// This will block until std in is closed. pub fn std() -> Result { Self::new(ClioPath::std().with_direction(InOut::In)) } /// Constructs a new [`CachedInput`] either by opening the file or for '-' stdin and reading /// all the data into memory. /// /// The error is converted to a [`OsString`](std::ffi::OsString) so that [stuctopt](https://docs.rs/structopt/latest/structopt/#custom-string-parsers) can show it to the user. /// /// It is recommended that you use [`TryFrom::try_from`] and [clap 3.0](https://docs.rs/clap/latest/clap/index.html) instead. pub fn try_from_os_str(path: &OsStr) -> std::result::Result { TryFrom::try_from(path).map_err(|e: Error| e.to_os_string(path)) } /// Returns the size of the file in bytes. /// /// # Examples /// /// ```no_run /// let file = clio::CachedInput::try_from_os_str("foo.txt".as_ref()).unwrap(); /// /// assert_eq!(3, file.len()); /// ``` pub fn len(&self) -> u64 { self.data.get_ref().len() as u64 } /// Returns a boolean saying if the file is empty /// /// # Examples /// /// ```no_run /// let file = clio::CachedInput::try_from_os_str("foo.txt".as_ref()).unwrap(); /// /// assert_eq!(true, file.is_empty()); /// ``` pub fn is_empty(&self) -> bool { self.data.get_ref().is_empty() } /// Returns the path/url used to create the input pub fn path(&self) -> &ClioPath { &self.path } /// Resets the reader back to the start of the file pub fn reset(&mut self) { self.data.set_position(0) } /// Returns data from the input as a [`Vec`] pub fn into_vec(self) -> Vec { self.data.into_inner() } /// Returns reference to the data from the input as a slice pub fn get_data(&self) -> &[u8] { self.data.get_ref() } } impl BufRead for CachedInput { fn fill_buf(&mut self) -> IoResult<&[u8]> { self.data.fill_buf() } fn consume(&mut self, amt: usize) { self.data.consume(amt) } } impl Read for CachedInput { fn read(&mut self, buf: &mut [u8]) -> IoResult { self.data.read(buf) } } impl Seek for CachedInput { fn seek(&mut self, pos: io::SeekFrom) -> IoResult { self.data.seek(pos) } } impl_try_from!(CachedInput: Clone - Default); /// A builder for [Input](crate::Input) that validates the path but /// defers creating it until you call the [open](crate::InputPath::open) method. /// /// It is designed to be used with the [`clap` crate](https://docs.rs/clap/latest) when taking a file name as an /// argument to CLI app /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::InputPath; /// /// #[derive(Parser)] /// struct Opt { /// /// path to file, use '-' for stdin /// #[clap(value_parser)] /// input_file: InputPath, /// } /// # } /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub struct InputPath { path: ClioPath, } impl InputPath { /// Constructs a new [`InputPath`] representing the path and checking that the file exists and is readable /// /// note: even if this passes open may still fail if e.g. the file was delete in between pub fn new>(path: S) -> Result where crate::Error: From<>::Error>, { let path: ClioPath = path.try_into()?.with_direction(InOut::In); if path.is_local() { assert_exists(&path)?; assert_not_dir(&path)?; assert_readable(&path)?; }; Ok(InputPath { path }) } /// Constructs a new [`InputPath`] to stdout ("-") pub fn std() -> Self { InputPath { path: ClioPath::std().with_direction(InOut::In), } } /// Returns true if this [`InputPath`] is stdin pub fn is_std(&self) -> bool { self.path.is_std() } /// Returns true if this is stdin and it is connected to a tty pub fn is_tty(&self) -> bool { self.is_std() && std::io::stdin().is_terminal() } /// Returns true if this [`InputPath`] is on the local file system, /// as opposed to point to stdin or a URL pub fn is_local(&self) -> bool { self.path.is_local() } /// Create an [`Input`] by opening the file or for '-' returning stdin. /// /// This is unlikely to error as the path is checked when the [`InputPath`] was created by [`new`](InputPath::new) /// but time of use/time of check means that things could have changed in-between e.g. the file /// could have been deleted. pub fn open(self) -> Result { self.path.open() } /// The original path used to create this [`InputPath`] pub fn path(&self) -> &ClioPath { &self.path } } impl_try_from!(InputPath: Clone); clio-0.3.5/src/lib.rs000066400000000000000000000247041453615011100143540ustar00rootroot00000000000000#![forbid(unsafe_code)] #![forbid(missing_docs)] #![warn(clippy::all)] #![deny(warnings)] #![deny(clippy::print_stdout)] #![allow(clippy::needless_doctest_main)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] #[cfg(feature = "clap-parse")] pub mod clapers; mod error; #[cfg(feature = "http")] mod http; mod input; mod output; mod path; pub use crate::error::Error; pub use crate::error::Result; pub use crate::input::CachedInput; pub use crate::input::Input; pub use crate::input::InputPath; pub use crate::output::Output; pub use crate::output::OutputPath; pub use crate::path::ClioPath; use std::ffi::OsStr; use std::fs::Metadata; use std::path::Path; #[cfg(not(unix))] fn is_fifo(_: &Metadata) -> bool { false } #[cfg(unix)] fn is_fifo(metadata: &Metadata) -> bool { use std::os::unix::fs::FileTypeExt; metadata.file_type().is_fifo() } fn assert_exists(path: &Path) -> Result<()> { if !path.try_exists()? { return Err(Error::not_found_error()); } // if the current working directory has been deleted then it will "exist()" // and have write permissions but you can put files in it or do anything really, if path == Path::new(".") { path.canonicalize()?; } Ok(()) } #[cfg(not(unix))] fn assert_readable(_path: &Path) -> Result<()> { Ok(()) } #[cfg(unix)] fn assert_readable(path: &Path) -> Result<()> { use std::os::unix::fs::PermissionsExt; let permissions = path.metadata()?.permissions(); if (permissions.mode() & 0o444) == 0 { return Err(Error::permission_error()); } Ok(()) } fn assert_writeable(path: &Path) -> Result<()> { let permissions = path.metadata()?.permissions(); if permissions.readonly() { return Err(Error::permission_error()); } Ok(()) } fn assert_not_dir(path: &ClioPath) -> Result<()> { if path.try_exists()? { if path.is_dir() { return Err(Error::dir_error()); } if path.ends_with_slash() { return Err(Error::not_dir_error()); } } if path.ends_with_slash() { return Err(Error::not_found_error()); } Ok(()) } fn assert_is_dir(path: &Path) -> Result<()> { assert_exists(path)?; if !path.is_dir() { return Err(Error::not_dir_error()); } Ok(()) } /// A predicate builder for filtering files based on extension /// /// ```no_run /// use clio::{ClioPath, has_extension}; /// /// let dir = ClioPath::new("/tmp/foo")?; /// for txt_file in dir.files(has_extension("txt"))? { /// txt_file.open()?; /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn has_extension>(ext: S) -> impl Fn(&ClioPath) -> bool { { move |path| path.extension() == Some(ext.as_ref()) } } /// A predicate for filtering files that accepts any file /// /// ```no_run /// use clio::{ClioPath, any_file}; /// /// let dir = ClioPath::new("/tmp/foo")?; /// for file in dir.files(any_file)? { /// file.open()?; /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn any_file(_: &ClioPath) -> bool { true } #[cfg(test)] #[cfg(feature = "clap-parse")] /// Trait to throw compile errors if a type will not be supported by clap trait Parseable: Clone + Sync + Send {} macro_rules! impl_try_from { ($struct_name:ident) => { impl_try_from!($struct_name Base); impl_try_from!($struct_name Default); impl_try_from!($struct_name TryFrom); #[cfg(feature = "clap-parse")] #[cfg_attr(docsrs, doc(cfg(feature = "clap-parse")))] /// Opens a new handle on the file from the path that was used to create it /// Probably a bad idea to have two write handles to the same file or to std in /// There is no effort done to make the clone be at the same position as the original /// /// This will panic if the file has been deleted /// /// Only included when using the `clap-parse` feature as it is needed for `value_parser` impl Clone for $struct_name { fn clone(&self) -> Self { $struct_name::new(self.path().clone()).unwrap() } } }; (ClioPath: Clone) => { impl_try_from!(ClioPath Base); impl_try_from!(ClioPath Default); }; ($struct_name:ident: Clone) => { impl_try_from!($struct_name Base); impl_try_from!($struct_name Default); impl_try_from!($struct_name TryFrom); }; ($struct_name:ident: Clone - Default) => { impl_try_from!($struct_name Base); impl_try_from!($struct_name TryFrom); }; ($struct_name:ident Default) => { impl Default for $struct_name { fn default() -> Self { $struct_name::std() } } #[cfg(test)] impl $struct_name { // Check that all clio types have the core methods #[allow(dead_code)] fn test_core_methods() { let s = crate::$struct_name::std(); assert!(s.is_std()); assert!(!s.is_local()); s.is_tty(); s.path(); } } }; ($struct_name:ident TryFrom) => { impl TryFrom for $struct_name { type Error = crate::Error; fn try_from(file_name: ClioPath) -> Result { $struct_name::new(file_name) } } }; ($struct_name:ident Base) => { impl TryFrom<&OsStr> for $struct_name { type Error = crate::Error; fn try_from(file_name: &OsStr) -> Result { $struct_name::new(file_name) } } impl TryFrom<&std::ffi::OsString> for $struct_name { type Error = crate::Error; fn try_from(file_name: &std::ffi::OsString) -> Result { $struct_name::new(file_name) } } impl TryFrom<&std::path::PathBuf> for $struct_name { type Error = crate::Error; fn try_from(file_name: &std::path::PathBuf) -> Result { $struct_name::new(file_name) } } impl TryFrom<&std::path::Path> for $struct_name { type Error = crate::Error; fn try_from(file_name: &std::path::Path) -> Result { $struct_name::new(file_name) } } impl TryFrom<&String> for $struct_name { type Error = crate::Error; fn try_from(file_name: &String) -> Result { $struct_name::new(file_name) } } impl TryFrom<&str> for $struct_name { type Error = crate::Error; fn try_from(file_name: &str) -> Result { $struct_name::new(file_name) } } /// formats as the path it was created from impl Display for $struct_name { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "{:?}", self.path().as_os_str()) } } #[cfg(feature = "clap-parse")] #[cfg_attr(docsrs, doc(cfg(feature = "clap-parse")))] impl clap::builder::ValueParserFactory for $struct_name { type Parser = crate::clapers::OsStrParser<$struct_name>; fn value_parser() -> Self::Parser { crate::clapers::OsStrParser::new() } } #[cfg(test)] #[cfg(feature = "clap-parse")] impl crate::Parseable for $struct_name {} }; } pub(crate) use impl_try_from; #[cfg(test)] mod tests { use super::*; use std::{ fs::{create_dir, set_permissions, write, File}, io::Read, }; use tempfile::{tempdir, TempDir}; fn set_mode(path: &Path, mode: u32) -> Result<()> { let mut perms = path.metadata()?.permissions(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; perms.set_mode(mode); } #[cfg(not(unix))] { perms.set_readonly((mode & 0o222) == 0); } set_permissions(path, perms)?; Ok(()) } fn temp() -> TempDir { let tmp = tempdir().expect("could not make tmp dir"); create_dir(&tmp.path().join("dir")).expect("could not create dir"); write(&tmp.path().join("file"), "contents").expect("could not create dir"); let ro = tmp.path().join("ro"); write(&ro, "contents").expect("could not create ro"); set_mode(&ro, 0o400).expect("could make ro read only"); let wo = tmp.path().join("wo"); write(&wo, "contents").expect("could not create wo"); set_mode(&wo, 0o200).expect("could make ro write only"); tmp } macro_rules! assert_all_eq { ($path:ident, $a:ident, $($b:expr),+) => { let a = comparable($a); $( assert_eq!( &a, &comparable($b), "mismatched error for path {:?} ({:?}) {}", $path, Path::new($path).canonicalize(), stringify!($a != $b) ); )+ }; } #[test] fn test_path_err_match_real_err() { let tmp = temp(); let tmp_w = temp(); for path in [ "file", "ro", "wo", "file/", "dir", "dir/", "missing-file", "missing-dir/", "missing-dir/file", ] { let tmp_path = tmp.path().join(path); let raw_r = File::open(&tmp_path).and_then(|mut f| { let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) }); let raw_w = write(&tmp_w.path().join(path), "junk"); let in_path_err = InputPath::new(&tmp_path); let open_err = Input::new(&tmp_path); assert_all_eq!(path, raw_r, in_path_err, open_err); let out_path_err = OutputPath::new(&tmp_path); let create_err = Output::new(&tmp_path); assert_all_eq!(path, raw_w, out_path_err, create_err); } } #[cfg(any(target_os = "linux", target_os = "macos"))] fn comparable( a: std::result::Result, ) -> std::result::Result<&'static str, String> { a.map(|_| "Ok").map_err(|e| e.to_string()) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] fn comparable(a: std::result::Result) -> bool { a.is_ok() } } clio-0.3.5/src/output.rs000066400000000000000000000317311453615011100151440ustar00rootroot00000000000000use crate::path::{ClioPathEnum, InOut}; use crate::{ assert_is_dir, assert_not_dir, assert_writeable, impl_try_from, is_fifo, ClioPath, Error, Result, }; use is_terminal::IsTerminal; use std::convert::TryFrom; use std::ffi::OsStr; use std::fmt::{self, Debug, Display}; use std::fs::{File, OpenOptions}; use std::io::{self, Result as IoResult, Seek, Stderr, Stdout, Write}; use std::path::Path; use tempfile::NamedTempFile; #[derive(Debug)] enum OutputStream { /// a [`Stdout`] when the path was `-` Stdout(Stdout), /// a [`Stderr`] Stderr(Stderr), /// a [`File`] representing the named pipe e.g. crated with `mkfifo` Pipe(File), /// a normal [`File`] opened from the path File(File), /// A normal [`File`] opened from the path that will be written to atomically AtomicFile(NamedTempFile), #[cfg(feature = "http")] #[cfg_attr(docsrs, doc(cfg(feature = "http")))] /// a writer that will upload the body the the HTTP server Http(Box), } #[cfg(feature = "http")] use crate::http::HttpWriter; /// A struct that represents a command line output stream, /// either [`Stdout`] or a [`File`] along with it's path /// /// It is designed to be used with the [`clap` crate](https://docs.rs/clap/latest) when taking a file name as an /// argument to CLI app /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::Output; /// /// #[derive(Parser)] /// struct Opt { /// /// path to file, use '-' for stdout /// #[clap(value_parser)] /// output_file: Output, /// /// /// default name for file is user passes in a directory /// #[clap(value_parser = clap::value_parser!(Output).default_name("run.log"))] /// log_file: Output, /// /// /// Write output atomically using temp file and atomic rename /// #[clap(value_parser = clap::value_parser!(Output).atomic())] /// config_file: Output, /// } /// # } /// ``` #[derive(Debug)] pub struct Output { path: ClioPath, stream: OutputStream, } /// A builder for [Output](crate::Output) that validates the path but /// defers creating it until you call the [create](crate::OutputPath::create) method. /// /// The [create_with_len](crate::OutputPath::create_with_len) allows setting the size before writing. /// This is mostly useful with the "http" feature for setting the Content-Length header /// /// It is designed to be used with the [`clap` crate](https://docs.rs/clap/latest) when taking a file name as an /// argument to CLI app /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::OutputPath; /// /// #[derive(Parser)] /// struct Opt { /// /// path to file, use '-' for stdout /// #[clap(value_parser)] /// output_file: OutputPath, /// } /// # } /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub struct OutputPath { path: ClioPath, } impl OutputStream { /// Constructs a new output either by opening/creating the file or for '-' returning stdout fn new(path: &ClioPath, size: Option) -> Result { Ok(match &path.path { ClioPathEnum::Std(_) => OutputStream::Stdout(io::stdout()), ClioPathEnum::Local(local_path) => { if path.atomic && !path.is_fifo() { assert_not_dir(path)?; if let Some(parent) = path.safe_parent() { assert_is_dir(parent)?; let tmp = tempfile::Builder::new() .prefix(".atomicwrite") .tempfile_in(parent)?; OutputStream::AtomicFile(tmp) } else { return Err(Error::not_found_error()); } } else { let file = open_rw(local_path)?; if is_fifo(&file.metadata()?) { OutputStream::Pipe(file) } else { if let Some(size) = size { file.set_len(size)?; } OutputStream::File(file) } } } #[cfg(feature = "http")] ClioPathEnum::Http(url) => { OutputStream::Http(Box::new(HttpWriter::new(url.as_str(), size)?)) } }) } } impl Output { /// Constructs a new output either by opening/creating the file or for '-' returning stdout pub fn new>(path: S) -> Result where crate::Error: From<>::Error>, { Output::maybe_with_len(path.try_into()?, None) } /// Convert to an normal [`Output`] setting the length of the file to size if it is `Some` pub(crate) fn maybe_with_len(path: ClioPath, size: Option) -> Result { Ok(Output { stream: OutputStream::new(&path, size)?, path, }) } /// Constructs a new output for stdout pub fn std() -> Self { Output { path: ClioPath::std().with_direction(InOut::Out), stream: OutputStream::Stdout(io::stdout()), } } /// Constructs a new output for stdout pub fn std_err() -> Self { Output { path: ClioPath::std().with_direction(InOut::Out), stream: OutputStream::Stderr(io::stderr()), } } /// Returns true if this Output is stout pub fn is_std(&self) -> bool { matches!(self.stream, OutputStream::Stdout(_)) } /// Returns true if this is stdout and it is connected to a tty pub fn is_tty(&self) -> bool { self.is_std() && std::io::stdout().is_terminal() } /// Returns true if this Output is on the local file system, /// as opposed to point to stdin/stout or a URL pub fn is_local(&self) -> bool { self.path.is_local() } /// Constructs a new output either by opening/creating the file or for '-' returning stdout /// /// The error is converted to a [`OsString`](std::ffi::OsString) so that [stuctopt](https://docs.rs/structopt/latest/structopt/#custom-string-parsers) can show it to the user. /// /// It is recommended that you use [`TryFrom::try_from`] and [clap 3.0](https://docs.rs/clap/latest/clap/index.html) instead. pub fn try_from_os_str(path: &OsStr) -> std::result::Result { TryFrom::try_from(path).map_err(|e: Error| e.to_os_string(path)) } /// Syncs the file to disk or closes any HTTP connections and returns any errors /// or on the file if a regular file /// For atomic files this must be called to perform the final atomic swap pub fn finish(mut self) -> Result<()> { self.flush()?; match self.stream { OutputStream::Stdout(_) => Ok(()), OutputStream::Stderr(_) => Ok(()), OutputStream::Pipe(_) => Ok(()), OutputStream::File(file) => Ok(file.sync_data()?), OutputStream::AtomicFile(tmp) => { tmp.persist(self.path.path())?; Ok(()) } #[cfg(feature = "http")] OutputStream::Http(http) => Ok(http.finish()?), } } /// If the output is std out [locks](std::io::Stdout::lock) it. /// useful in multithreaded context to write lines consistently /// /// # Examples /// /// ```no_run /// # fn main() -> Result<(), clio::Error> { /// let mut file = clio::Output::new("-")?; /// /// writeln!(file.lock(), "hello world")?; /// # Ok(()) /// # } /// ``` pub fn lock<'a>(&'a mut self) -> Box { match &mut self.stream { OutputStream::Stdout(stdout) => Box::new(stdout.lock()), OutputStream::Stderr(stderr) => Box::new(stderr.lock()), OutputStream::Pipe(pipe) => Box::new(pipe), OutputStream::File(file) => Box::new(file), OutputStream::AtomicFile(file) => Box::new(file), #[cfg(feature = "http")] OutputStream::Http(http) => Box::new(http), } } /// If output is a file, returns a reference to the file, /// otherwise if output is stdout or a pipe returns none. pub fn get_file(&mut self) -> Option<&mut File> { match &mut self.stream { OutputStream::File(file) => Some(file), OutputStream::AtomicFile(file) => Some(file.as_file_mut()), _ => None, } } /// The original path used to create this [`Output`] pub fn path(&self) -> &ClioPath { &self.path } /// Returns `true` if this [`Output`] is a file, /// and `false` if this [`Output`] is std out or a pipe pub fn can_seek(&self) -> bool { matches!( self.stream, OutputStream::File(_) | OutputStream::AtomicFile(_) ) } } impl_try_from!(Output); impl Write for Output { fn flush(&mut self) -> IoResult<()> { match &mut self.stream { OutputStream::Stdout(stdout) => stdout.flush(), OutputStream::Stderr(stderr) => stderr.flush(), OutputStream::Pipe(pipe) => pipe.flush(), OutputStream::File(file) => file.flush(), OutputStream::AtomicFile(file) => file.flush(), #[cfg(feature = "http")] OutputStream::Http(http) => http.flush(), } } fn write(&mut self, buf: &[u8]) -> IoResult { match &mut self.stream { OutputStream::Stdout(stdout) => stdout.write(buf), OutputStream::Stderr(stderr) => stderr.write(buf), OutputStream::Pipe(pipe) => pipe.write(buf), OutputStream::File(file) => file.write(buf), OutputStream::AtomicFile(file) => file.write(buf), #[cfg(feature = "http")] OutputStream::Http(http) => http.write(buf), } } } impl Seek for Output { fn seek(&mut self, pos: io::SeekFrom) -> IoResult { match &mut self.stream { OutputStream::File(file) => file.seek(pos), OutputStream::AtomicFile(file) => file.seek(pos), _ => Err(Error::seek_error().into()), } } } impl OutputPath { /// Construct a new [`OutputPath`] from an string /// /// It checks if an output file could plausibly be created at that path pub fn new>(path: S) -> Result where crate::Error: From<>::Error>, { let path: ClioPath = path.try_into()?.with_direction(InOut::Out); if path.is_local() { if path.is_file() && !path.atomic { assert_writeable(&path)?; } else { #[cfg(target_os = "linux")] if path.ends_with_slash() { return Err(Error::dir_error()); } assert_not_dir(&path)?; if let Some(parent) = path.safe_parent() { assert_is_dir(parent)?; assert_writeable(parent)?; } else { return Err(Error::not_found_error()); } } } Ok(OutputPath { path }) } /// Constructs a new [`OutputPath`] of `"-"` for stdout pub fn std() -> Self { OutputPath { path: ClioPath::std().with_direction(InOut::Out), } } /// convert to an normal [`Output`] setting the length of the file to size if it is `Some` pub fn maybe_with_len(self, size: Option) -> Result { Output::maybe_with_len(self.path, size) } /// Create the file with a predetermined length, either using [`File::set_len`] or as the `content-length` header of the http put pub fn create_with_len(self, size: u64) -> Result { self.maybe_with_len(Some(size)) } /// Create an [`Output`] without setting the length pub fn create(self) -> Result { self.maybe_with_len(None) } /// The original path represented by this [`OutputPath`] pub fn path(&self) -> &ClioPath { &self.path } /// Returns true if this [`Output`] is stdout pub fn is_std(&self) -> bool { self.path.is_std() } /// Returns true if this is stdout and it is connected to a tty pub fn is_tty(&self) -> bool { self.is_std() && std::io::stdout().is_terminal() } /// Returns true if this [`Output`] is on the local file system, /// as opposed to point to stout or a URL pub fn is_local(&self) -> bool { self.path.is_local() } /// Returns `true` if this [`OutputPath`] points to a file, /// and `false` if this [`OutputPath`] is std out or points to a pipe. /// Note that the file is not opened yet, so there are possible when you /// open the file it might have changed. pub fn can_seek(&self) -> bool { self.path.is_local() && !self.path.is_fifo() } } impl_try_from!(OutputPath: Clone); fn open_rw(path: &Path) -> io::Result { OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) .open(path) .or_else(|_| File::create(path)) } clio-0.3.5/src/path.rs000066400000000000000000000362431453615011100145430ustar00rootroot00000000000000use crate::{impl_try_from, is_fifo, CachedInput, Input, Output, Result}; use is_terminal::IsTerminal; use std::convert::TryFrom; use std::ffi::{OsStr, OsString}; use std::fmt::{self, Debug, Display}; use std::ops::Deref; use std::path::{Path, PathBuf}; use walkdir::WalkDir; #[cfg(feature = "http")] use { crate::http::{is_http, try_to_url}, url::Url, }; /// A builder for [Input](crate::Input) and [Output](crate::Output). /// /// It is designed to be used to get files related to the one passed in. /// /// e.g. Take an [Input](crate::Input) of `/tmp/foo.svg` and have a default [Output](crate::Output) of `/tmp/foo.png` /// /// ```no_run /// use clio::{Input, Output}; /// /// let input = Input::new("/tmp/foo.svg")?; /// let mut output_path = input.path().clone(); /// output_path.set_extension("png"); /// let output = output_path.create()?; /// /// assert_eq!(output.path().as_os_str().to_string_lossy(), "/tmp/foo.png"); /// # Ok::<(), clio::Error>(()) /// ``` /// Unlike [InputPath](crate::InputPath) and [OutputPath](crate::OutputPath) it does not /// validate the path until you try creating/opening it. /// /// However you can add extra validation using the [`clap`] parser. /// ``` /// # #[cfg(feature="clap-parse")]{ /// use clap::Parser; /// use clio::ClioPath; /// /// #[derive(Parser)] /// struct Opt { /// /// path to input file, use '-' for stdin /// #[clap(value_parser)] /// input_path: ClioPath, /// /// /// path to output file, use '-' for stdout point to directory to use default name /// #[clap(value_parser = clap::value_parser!(ClioPath).default_name("out.bin"))] /// output_file: ClioPath, /// /// /// path to directory /// #[clap(value_parser = clap::value_parser!(ClioPath).exists().is_dir())] /// log_dir: ClioPath, /// } /// # } /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub struct ClioPath { pub(crate) path: ClioPathEnum, pub(crate) atomic: bool, } #[derive(Debug, PartialEq, Eq, Clone)] pub enum InOut { In, Out, } #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) enum ClioPathEnum { /// stdin or stdout from a cli arg of `'-'` Std(Option), /// a path to local file which may or may not exist Local(PathBuf), #[cfg(feature = "http")] /// a http URL to a file on the web Http(Url), } impl ClioPathEnum { fn new(path: &OsStr, io: Option) -> Result { #[cfg(feature = "http")] if is_http(path) { return Ok(ClioPathEnum::Http(try_to_url(path)?)); } if path == "-" { Ok(ClioPathEnum::Std(io)) } else { Ok(ClioPathEnum::Local(path.into())) } } } impl ClioPath { /// Construct a new [`ClioPath`] from an string /// /// `'-'` is treated as stdin/stdout pub fn new>(path: S) -> Result { Ok(ClioPath { path: ClioPathEnum::new(path.as_ref(), None)?, atomic: false, }) } /// Constructs a new [`ClioPath`] of `"-"` for stdout pub fn std() -> Self { ClioPath { path: ClioPathEnum::Std(None), atomic: false, } } /// Constructs a new [`ClioPath`] for a local path pub fn local(path: PathBuf) -> Self { ClioPath { path: ClioPathEnum::Local(path), atomic: false, } } pub(crate) fn with_direction(self, direction: InOut) -> Self { ClioPath { path: match self.path { ClioPathEnum::Std(_) => ClioPathEnum::Std(Some(direction)), x => x, }, atomic: self.atomic, } } pub(crate) fn with_path_mut(&mut self, update: F) -> O where O: Default, F: FnOnce(&mut PathBuf) -> O, { match &mut self.path { ClioPathEnum::Std(_) => O::default(), ClioPathEnum::Local(path) => update(path), #[cfg(feature = "http")] ClioPathEnum::Http(url) => { let mut path = Path::new(url.path()).to_owned(); let r = update(&mut path); url.set_path(&path.to_string_lossy()); r } } } /// Updates [`self.file_name`](Path::file_name) to `file_name`. /// /// see [`PathBuf::set_file_name`] for more details /// /// # Examples /// /// ``` /// use clio::ClioPath; /// /// let mut buf = ClioPath::new("/")?; /// assert!(buf.file_name() == None); /// buf.set_file_name("bar"); /// assert!(buf == ClioPath::new("/bar")?); /// assert!(buf.file_name().is_some()); /// buf.set_file_name("baz.txt"); /// assert!(buf == ClioPath::new("/baz.txt")?); /// #[cfg(feature = "http")] { /// let mut p = ClioPath::new("https://example.com/bar.html?x=y#p2")?; /// p.set_file_name("baz.txt"); /// assert_eq!(Some("https://example.com/baz.txt?x=y#p2"), p.as_os_str().to_str()); /// } /// /// # Ok::<(), clio::Error>(()) /// ``` pub fn set_file_name>(&mut self, file_name: S) { self.with_path_mut(|path| path.set_file_name(file_name)) } /// Updates [`self.extension`](Path::extension) to `extension`. /// /// see [`PathBuf::set_extension`] for more details /// /// # Examples /// /// ``` /// use clio::ClioPath; /// /// let mut p = ClioPath::new("/feel/the")?; /// /// p.set_extension("force"); /// assert_eq!(ClioPath::new("/feel/the.force")?, p); /// /// p.set_extension("dark_side"); /// assert_eq!(ClioPath::new("/feel/the.dark_side")?, p); /// /// #[cfg(feature = "http")] { /// let mut p = ClioPath::new("https://example.com/the_force.html?x=y#p2")?; /// p.set_extension("txt"); /// assert_eq!(Some("https://example.com/the_force.txt?x=y#p2"), p.as_os_str().to_str()); /// } /// /// # Ok::<(), clio::Error>(()) /// ``` pub fn set_extension>(&mut self, extension: S) -> bool { self.with_path_mut(|path| path.set_extension(extension)) } /// Adds an extension to the end of the [`self.file_name`](Path::file_name). /// /// # Examples /// /// ``` /// use clio::ClioPath; /// /// let mut p = ClioPath::new("/tmp/log.txt")?; /// /// p.add_extension("gz"); /// assert_eq!(ClioPath::new("/tmp/log.txt.gz")?, p); /// # Ok::<(), clio::Error>(()) /// ``` /// /// ``` /// use clio::ClioPath; /// /// let mut p = ClioPath::new("/tmp/log")?; /// p.add_extension("gz"); /// assert_eq!(ClioPath::new("/tmp/log.gz")?, p); /// # Ok::<(), clio::Error>(()) /// ``` pub fn add_extension>(&mut self, extension: S) -> bool { if self.file_name().is_some() && !self.ends_with_slash() { if let Some(existing) = self.extension() { let mut existing = existing.to_os_string(); existing.push("."); existing.push(extension); self.with_path_mut(|path| path.set_extension(existing)) } else { self.with_path_mut(|path| path.set_extension(extension)) } } else { false } } /// Extends `self` with `path`. /// /// see [`PathBuf::push`] for more details /// /// /// ``` /// use clio::ClioPath; /// /// let mut path = ClioPath::new("/tmp")?; /// path.push("file.bk"); /// assert_eq!(path, ClioPath::new("/tmp/file.bk")?); /// /// #[cfg(feature = "http")] { /// let mut p = ClioPath::new("https://example.com/tmp?x=y#p2")?; /// p.push("file.bk"); /// assert_eq!(Some("https://example.com/tmp/file.bk?x=y#p2"), p.as_os_str().to_str()); /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn push>(&mut self, path: P) { self.with_path_mut(|base| base.push(path)) } /// Creates an owned [`ClioPath`] with `path` adjoined to `self`. /// /// If `path` is absolute, it replaces the current path. /// /// See [`PathBuf::push`] for more details on what it means to adjoin a path. /// /// # Examples /// /// ``` /// use clio::ClioPath; /// /// assert_eq!(ClioPath::new("/etc")?.join("passwd"), ClioPath::new("/etc/passwd")?); /// assert_eq!(ClioPath::new("/etc")?.join("/bin/sh"), ClioPath::new("/bin/sh")?); /// /// #[cfg(feature = "http")] { /// let mut p = ClioPath::new("https://example.com/tmp?x=y#p2")?; /// p.push("file.bk"); /// assert_eq!( /// ClioPath::new("https://example.com/tmp?x=y#p2")?.join("file.bk"), /// ClioPath::new("https://example.com/tmp/file.bk?x=y#p2")?); /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn join>(&mut self, path: P) -> Self { let mut new = self.clone(); new.push(path); new } /// Returns true if this path is stdin/stout i.e. it was created with `-` pub fn is_std(&self) -> bool { matches!(self.path, ClioPathEnum::Std(_)) } /// Returns true if this [`is_std`](Self::is_std) and it would connect to a tty pub fn is_tty(&self) -> bool { match self.path { ClioPathEnum::Std(Some(InOut::In)) => std::io::stdin().is_terminal(), ClioPathEnum::Std(Some(InOut::Out)) => std::io::stdout().is_terminal(), ClioPathEnum::Std(None) => { std::io::stdin().is_terminal() || std::io::stdout().is_terminal() } _ => false, } } /// Returns true if this path is on the local file system, /// as opposed to point to stdin/stout or a URL pub fn is_local(&self) -> bool { matches!(self.path, ClioPathEnum::Local(_)) } pub(crate) fn is_fifo(&self) -> bool { match &self.path { ClioPathEnum::Local(path) => { if let Ok(meta) = path.metadata() { is_fifo(&meta) } else { false } } ClioPathEnum::Std(_) => true, #[cfg(feature = "http")] ClioPathEnum::Http(_) => false, } } /// Returns `true` if this path ends with a `/` /// /// A trailing slash is often used by command line arguments /// to refer to a directory that may not exist. /// e.g. `cp foo /tmp/` pub fn ends_with_slash(&self) -> bool { cfg_if::cfg_if! { if #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; self.path().as_os_str().as_bytes().ends_with(b"/") } else if #[cfg(windows)] { use std::os::windows::ffi::OsStrExt; self.path().as_os_str().encode_wide().last() == Some('/' as u16) } else { self.path().as_os_str().to_string_lossy().ends_with("/") } } } /// If this is a folder returns all the files that match the filter found by looking recursively /// Otherwise returns just this path /// ```no_run /// use clio::has_extension; /// use clio::ClioPath; /// /// let dir = ClioPath::new("/tmp/foo")?; /// for txt_file in dir.files(has_extension("txt"))? { /// txt_file.open()?; /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn files

(self, mut predicate: P) -> Result> where P: FnMut(&ClioPath) -> bool, { if self.is_local() { let mut result = vec![]; for entry in WalkDir::new(self.path()).follow_links(true) { let entry = entry?; if entry.file_type().is_file() { let path = ClioPath::local(entry.into_path()); if predicate(&path) { result.push(path); } } } Ok(result) } else { Ok(vec![self]) } } /// Create the file with a predetermined length, either using [`File::set_len`](std::fs::File::set_len) or as the `content-length` header of the http put pub fn create_with_len(self, size: u64) -> Result { Output::maybe_with_len(self, Some(size)) } /// Create the file at this path and return it as an [`Output`] without setting the length pub fn create(self) -> Result { Output::maybe_with_len(self, None) } /// Open the file at this path and return it as an [`Input`] pub fn open(self) -> Result { Input::new(self) } /// Read the entire the file at this path and return it as an [`CachedInput`] pub fn read_all(self) -> Result { CachedInput::new(self) } /// A path represented by this [`ClioPath`] /// If it is `-` and it is no known if it is in or out then the path will be `-` /// If it is `-` and it is known to be in/out then it will be the pseudo device e.g `/dev/stdin` /// If it is a url it will be the path part of the url /// ``` /// use clio::{ClioPath, OutputPath}; /// use std::path::Path; /// /// let p = ClioPath::new("-")?; /// /// assert_eq!(Path::new("-"), p.path()); /// /// let stdout = OutputPath::new("-")?; /// let p:&ClioPath = stdout.path(); /// assert_eq!(Path::new("/dev/stdout"), p.path()); /// /// #[cfg(feature = "http")] { /// let p = ClioPath::new("https://example.com/foo/bar.html?x=y#p2")?; /// /// assert_eq!(Path::new("/foo/bar.html"), p.path()); /// } /// # Ok::<(), clio::Error>(()) /// ``` pub fn path(&self) -> &Path { match &self.path { ClioPathEnum::Std(None) => Path::new("-"), ClioPathEnum::Std(Some(InOut::In)) => Path::new("/dev/stdin"), ClioPathEnum::Std(Some(InOut::Out)) => Path::new("/dev/stdout"), ClioPathEnum::Local(path) => path.as_path(), #[cfg(feature = "http")] ClioPathEnum::Http(url) => Path::new(url.path()), } } pub(crate) fn safe_parent(&self) -> Option<&Path> { match &self.path { ClioPathEnum::Local(path) => { let parent = path.parent()?; if parent == Path::new("") { Some(Path::new(".")) } else { Some(parent) } } _ => None, } } /// The original string represented by this [`ClioPath`] pub fn as_os_str(&self) -> &OsStr { match &self.path { ClioPathEnum::Std(_) => OsStr::new("-"), ClioPathEnum::Local(path) => path.as_os_str(), #[cfg(feature = "http")] ClioPathEnum::Http(url) => OsStr::new(url.as_str()), } } /// Consumes the [`ClioPath`], yielding its internal OsString storage. pub fn to_os_string(self) -> OsString { match self.path { ClioPathEnum::Std(_) => OsStr::new("-").to_os_string(), ClioPathEnum::Local(path) => path.into_os_string(), #[cfg(feature = "http")] ClioPathEnum::Http(url) => OsStr::new(url.as_str()).to_os_string(), } } } impl Deref for ClioPath { type Target = Path; fn deref(&self) -> &Self::Target { self.path() } } impl_try_from!(ClioPath: Clone);