clio/
clapers.rs

1//! implementation of TypedValueParser for clio types so that they can be
2//! used with clap `value_parser`
3//!
4//! This module is only compiled if you enable the clap-parse feature
5
6use crate::{assert_exists, assert_is_dir, assert_not_dir, ClioPath, Error, Result};
7use clap::builder::TypedValueParser;
8use clap::error::ErrorKind;
9use std::ffi::OsStr;
10use std::marker::PhantomData;
11
12/// A clap parser that converts [`&OsStr`](std::ffi::OsStr) to an [`Input`](crate::Input) or [`Output`](crate::Output)
13#[derive(Copy, Clone, Debug)]
14pub struct OsStrParser<T> {
15    exists: Option<bool>,
16    is_dir: Option<bool>,
17    is_file: Option<bool>,
18    is_tty: Option<bool>,
19    atomic: bool,
20    default_name: Option<&'static str>,
21    phantom: PhantomData<T>,
22}
23
24impl<T> OsStrParser<T> {
25    pub(crate) fn new() -> Self {
26        OsStrParser {
27            exists: None,
28            is_dir: None,
29            is_file: None,
30            is_tty: None,
31            default_name: None,
32            atomic: false,
33            phantom: PhantomData,
34        }
35    }
36
37    /// This path must exist
38    pub fn exists(mut self) -> Self {
39        self.exists = Some(true);
40        self
41    }
42
43    /// If this path exists it must point to a directory
44    pub fn is_dir(mut self) -> Self {
45        self.is_dir = Some(true);
46        self.is_file = None;
47        self
48    }
49
50    /// If this path exists it must point to a file
51    pub fn is_file(mut self) -> Self {
52        self.is_dir = None;
53        self.is_file = Some(true);
54        self
55    }
56
57    /// If this path is for stdin/stdout they must be a pipe not a tty
58    pub fn not_tty(mut self) -> Self {
59        self.is_tty = Some(false);
60        self
61    }
62
63    /// Make writing atomic, by writing to a temp file then doing an
64    /// atomic swap
65    pub fn atomic(mut self) -> Self {
66        self.atomic = true;
67        self
68    }
69
70    /// The default name to use for the file if the path is a directory
71    pub fn default_name(mut self, name: &'static str) -> Self {
72        self.default_name = Some(name);
73        self
74    }
75
76    fn validate(&self, value: &OsStr) -> Result<ClioPath> {
77        let mut path = ClioPath::new(value)?;
78        path.atomic = self.atomic;
79        if path.is_local() {
80            if let Some(name) = self.default_name {
81                if path.is_dir() || path.ends_with_slash() {
82                    path.push(name)
83                }
84            }
85            if self.is_dir == Some(true) && path.exists() {
86                assert_is_dir(&path)?;
87            }
88            if self.is_file == Some(true) {
89                assert_not_dir(&path)?;
90            }
91            if self.exists == Some(true) {
92                assert_exists(&path)?;
93            }
94        } else if self.is_dir == Some(true) {
95            return Err(Error::not_dir_error());
96        } else if self.is_tty == Some(false) && path.is_tty() {
97            return Err(Error::other(
98                "blocked reading from stdin because it is a tty",
99            ));
100        }
101        Ok(path)
102    }
103}
104
105impl<T> TypedValueParser for OsStrParser<T>
106where
107    for<'a> T: TryFrom<ClioPath, Error = crate::Error>,
108    T: Clone + Sync + Send + 'static,
109{
110    type Value = T;
111
112    fn parse_ref(
113        &self,
114        cmd: &clap::Command,
115        arg: Option<&clap::Arg>,
116        value: &OsStr,
117    ) -> core::result::Result<Self::Value, clap::Error> {
118        self.validate(value).and_then(T::try_from).map_err(|orig| {
119            cmd.clone().error(
120                ErrorKind::InvalidValue,
121                if let Some(arg) = arg {
122                    format!(
123                        "Invalid value for {}: Could not open {:?}: {}",
124                        arg, value, orig
125                    )
126                } else {
127                    format!("Could not open {:?}: {}", value, orig)
128                },
129            )
130        })
131    }
132}
133
134impl TypedValueParser for OsStrParser<ClioPath> {
135    type Value = ClioPath;
136    fn parse_ref(
137        &self,
138        cmd: &clap::Command,
139        arg: Option<&clap::Arg>,
140        value: &OsStr,
141    ) -> core::result::Result<Self::Value, clap::Error> {
142        self.validate(value).map_err(|orig| {
143            cmd.clone().error(
144                ErrorKind::InvalidValue,
145                if let Some(arg) = arg {
146                    format!(
147                        "Invalid value for {}: Invalid path {:?}: {}",
148                        arg, value, orig
149                    )
150                } else {
151                    format!("Invalid path {:?}: {}", value, orig)
152                },
153            )
154        })
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use std::fs::{create_dir, write};
162    use tempfile::{tempdir, TempDir};
163
164    fn temp() -> TempDir {
165        let tmp = tempdir().expect("could not make tmp dir");
166        create_dir(&tmp.path().join("dir")).expect("could not create dir");
167        write(&tmp.path().join("file"), "contents").expect("could not create dir");
168        tmp
169    }
170
171    #[test]
172    fn test_path_exists() {
173        let tmp = temp();
174        let validator = OsStrParser::<ClioPath>::new().exists();
175        validator
176            .validate(tmp.path().join("file").as_os_str())
177            .unwrap();
178        validator
179            .validate(tmp.path().join("dir").as_os_str())
180            .unwrap();
181        validator
182            .validate(tmp.path().join("dir/").as_os_str())
183            .unwrap();
184
185        assert!(validator
186            .validate(tmp.path().join("dir/missing").as_os_str())
187            .is_err());
188    }
189
190    #[test]
191    fn test_path_is_file() {
192        let tmp = temp();
193        let validator = OsStrParser::<ClioPath>::new().is_file();
194        validator
195            .validate(tmp.path().join("file").as_os_str())
196            .unwrap();
197        validator
198            .validate(tmp.path().join("dir/missing").as_os_str())
199            .unwrap();
200        validator.validate(OsStr::new("-")).unwrap();
201        assert!(validator
202            .validate(tmp.path().join("dir/").as_os_str())
203            .is_err());
204        assert!(validator
205            .validate(tmp.path().join("missing-dir/").as_os_str())
206            .is_err());
207    }
208
209    #[test]
210    fn test_path_is_existing_file() {
211        let tmp = temp();
212        let validator = OsStrParser::<ClioPath>::new().exists().is_file();
213        validator
214            .validate(tmp.path().join("file").as_os_str())
215            .unwrap();
216        assert!(validator
217            .validate(tmp.path().join("dir/missing").as_os_str())
218            .is_err());
219        assert!(validator
220            .validate(tmp.path().join("dir/").as_os_str())
221            .is_err());
222    }
223
224    #[test]
225    fn test_path_is_dir() {
226        let tmp = temp();
227        let validator = OsStrParser::<ClioPath>::new().is_dir();
228        validator
229            .validate(tmp.path().join("dir").as_os_str())
230            .unwrap();
231        validator
232            .validate(tmp.path().join("dir/missing").as_os_str())
233            .unwrap();
234        assert!(validator
235            .validate(tmp.path().join("file").as_os_str())
236            .is_err());
237        assert!(validator.validate(OsStr::new("-")).is_err());
238    }
239
240    #[test]
241    fn test_default_name() {
242        let tmp = temp();
243        let validator = OsStrParser::<ClioPath>::new().default_name("default.txt");
244        assert_eq!(
245            validator
246                .validate(tmp.path().join("dir").as_os_str())
247                .unwrap()
248                .file_name()
249                .unwrap(),
250            "default.txt"
251        );
252        assert_eq!(
253            validator
254                .validate(tmp.path().join("dir/file").as_os_str())
255                .unwrap()
256                .file_name()
257                .unwrap(),
258            "file"
259        );
260        assert_eq!(
261            validator
262                .validate(tmp.path().join("missing-dir/").as_os_str())
263                .unwrap()
264                .file_name()
265                .unwrap(),
266            "default.txt"
267        );
268    }
269}