clap_file/
output.rs

1use std::{
2    fs::File,
3    io::{self, LineWriter, Write},
4    path::{Path, PathBuf},
5    str::FromStr,
6    sync::{Arc, Mutex, MutexGuard},
7};
8
9#[track_caller]
10fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<T> {
11    mutex.lock().unwrap_or_else(|e| e.into_inner())
12}
13
14/// Represents an output sink, which can be either standard output or a file.
15///
16/// # Examples
17///
18/// ```rust,no_run
19/// use std::io::{self, Write as _};
20///
21/// use clap::Parser as _;
22/// use clap_file::Output;
23///
24/// #[derive(Debug, clap::Parser)]
25/// struct Args {
26///     /// output file. If not provided, reads from standard output.
27///     output: Output,
28/// }
29///
30/// fn main() -> io::Result<()> {
31///     let args = Args::parse();
32///     let mut output = args.output.lock();
33///     writeln!(&mut output, "Hello, world!")?;
34///     Ok(())
35/// }
36/// ```
37
38// This struct should not implement `Clone`, but clap-derive requires Clone [1].
39// So, I added `Clone` to the struct and wrap `File` with `Arc` and `Mutex`.
40// This is not the best way to handle this, but it works for now.
41//
42// [1]: https://github.com/clap-rs/clap/issues/4286
43#[derive(Debug, Clone)]
44pub struct Output(OutputInner);
45
46#[derive(Debug, Clone)]
47enum OutputInner {
48    Stdout,
49    File {
50        path: Arc<PathBuf>,
51        writer: Arc<Mutex<LineWriter<File>>>,
52    },
53}
54
55impl Output {
56    /// Creates a new [`Output`] instance that writes to standard output.
57    pub fn stdout() -> Self {
58        Self(OutputInner::Stdout)
59    }
60
61    /// Creates a file at the given path and creates a new [`Output`] instance that writes to it.
62    pub fn create(path: PathBuf) -> io::Result<Self> {
63        let path = Arc::new(path);
64        let file = File::create(&*path)?;
65        let writer = Arc::new(Mutex::new(LineWriter::new(file)));
66        Ok(Self(OutputInner::File { path, writer }))
67    }
68
69    /// Returns `true` if this [`Output`] writes to standard output.
70    pub fn is_stdout(&self) -> bool {
71        matches!(self.0, OutputInner::Stdout)
72    }
73
74    /// Returns `true` if this [`Output`] writes to a file.
75
76    pub fn is_file(&self) -> bool {
77        matches!(self.0, OutputInner::File { .. })
78    }
79
80    /// Returns the path of the file this [`Output`] writes to.
81    ///
82    /// Returns `None` if this [`Output`] writes to standard output.
83    pub fn path(&self) -> Option<&Path> {
84        match &self.0 {
85            OutputInner::Stdout => None,
86            OutputInner::File { path, .. } => Some(path),
87        }
88    }
89
90    /// Locks this [`Output`] for writing and returns a writable guard.
91    ///
92    /// This lock is released when the returned [`LockedOutput`] instance is dropped.
93    /// The returned `LockedOutput` instance implements [`Write`] trait for writing data.
94
95    pub fn lock(&self) -> LockedOutput<'_> {
96        let inner = match &self.0 {
97            OutputInner::Stdout => {
98                let writer = io::stdout().lock();
99                LockedOutputInner::Stdout { writer }
100            }
101            OutputInner::File { path, writer: file } => {
102                let writer = lock(file);
103                LockedOutputInner::File {
104                    path: Arc::clone(path),
105                    writer,
106                }
107            }
108        };
109        LockedOutput(inner)
110    }
111}
112
113impl FromStr for Output {
114    type Err = io::Error;
115
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        if s == "-" {
118            return Ok(Self::stdout());
119        }
120        Self::create(PathBuf::from(s))
121    }
122}
123
124macro_rules! with_writer {
125    ($inner:expr, $var:ident => $e:expr) => {
126        match $inner {
127            OutputInner::Stdout => {
128                let mut $var = io::stdout();
129                $e
130            }
131            OutputInner::File { writer, .. } => {
132                let mut $var = lock(writer);
133                $e
134            }
135        }
136    };
137}
138
139impl Write for Output {
140    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
141        with_writer!(&self.0, writer => writer.write(buf))
142    }
143
144    fn flush(&mut self) -> io::Result<()> {
145        with_writer!(&self.0, writer => writer.flush())
146    }
147
148    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
149        with_writer!(&self.0, writer => writer.write_vectored(bufs))
150    }
151
152    // this method is not yet stable
153    // fn is_write_vectored(&self) -> bool {
154    //     with_writer!(&self.0, writer => writer.is_write_vectored())
155    // }
156
157    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
158        with_writer!(&self.0, writer => writer.write_all(buf))
159    }
160
161    // this method is not yet stable
162    // fn write_all_vectored(&mut self, bufs: &mut [io::IoSlice<'_>]) -> io::Result<()> {
163    //     with_writer!(&self.0, writer => writer.write_all_vectored(bufs))
164    // }
165}
166
167/// A locked output sink that can be written to.
168#[derive(Debug)]
169pub struct LockedOutput<'a>(LockedOutputInner<'a>);
170
171impl LockedOutput<'_> {
172    /// Returns `true` if this [`LockedOutput`] writes to standard output.
173    pub fn is_stdin(&self) -> bool {
174        matches!(self.0, LockedOutputInner::Stdout { .. })
175    }
176
177    /// Returns `true` if this [`LockedOutput`] writes to a file.
178    pub fn is_file(&self) -> bool {
179        matches!(self.0, LockedOutputInner::File { .. })
180    }
181
182    /// Returns the path of the file this [`LockedOutput`] writes to.
183    ///
184    /// Returns `None` if this [`LockedOutput`] writes to standard output.
185    pub fn path(&self) -> Option<&Path> {
186        match &self.0 {
187            LockedOutputInner::Stdout { .. } => None,
188            LockedOutputInner::File { path, .. } => Some(path),
189        }
190    }
191}
192
193#[derive(Debug)]
194enum LockedOutputInner<'a> {
195    Stdout {
196        writer: io::StdoutLock<'a>,
197    },
198    File {
199        path: Arc<PathBuf>,
200        writer: MutexGuard<'a, LineWriter<File>>,
201    },
202}
203
204macro_rules! with_locked_writer {
205    ($inner:expr, $var:ident => $e:expr) => {
206        match $inner {
207            LockedOutputInner::Stdout { writer } => {
208                let $var = writer;
209                $e
210            }
211            LockedOutputInner::File { writer, .. } => {
212                let $var = writer;
213                $e
214            }
215        }
216    };
217}
218
219impl Write for LockedOutput<'_> {
220    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
221        with_locked_writer!(&mut self.0, writer => writer.write(buf))
222    }
223
224    fn flush(&mut self) -> io::Result<()> {
225        with_locked_writer!(&mut self.0, writer => writer.flush())
226    }
227
228    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
229        with_locked_writer!(&mut self.0, writer => writer.write_vectored(bufs))
230    }
231
232    // this method is not yet stable
233    // fn is_write_vectored(&self) -> bool {
234    //     with_locked_writer!(&self.0, writer => writer.is_write_vectored())
235    // }
236
237    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
238        with_locked_writer!(&mut self.0, writer => writer.write_all(buf))
239    }
240
241    // this method is not yet stable
242    // fn write_all_vectored(&mut self, bufs: &mut [io::IoSlice<'_>]) -> io::Result<()> {
243    //     with_locked_writer!(&mut self.0, writer => writer.write_all_vectored(bufs))
244    // }
245}