1use std::{
61 ffi::{OsStr, OsString},
62 fmt,
63 fs::File,
64 io::{self, Read, Write},
65 path::{Path, PathBuf},
66 str::FromStr,
67};
68
69use clap::{Args, ValueHint};
70
71const STDIO: &str = "-";
72const STDIN: &str = "<stdin>";
73const STDOUT: &str = "<stdout>";
74
75#[derive(Debug, Args)]
77pub struct InputOutput {
78 #[arg(
80 long,
81 default_value_os_t,
82 value_hint = ValueHint::FilePath,
83 )]
84 pub input: Input,
85
86 #[arg(
88 long,
89 default_value_os_t,
90 value_hint = ValueHint::FilePath,
91 )]
92 pub output: Output,
93}
94
95#[derive(Debug, Clone)]
97pub struct Input(Stream);
98
99impl Input {
100 pub fn open(self) -> io::Result<Box<dyn Read + 'static>> {
102 match self.0 {
103 Stream::File(_) => {
104 let file = self.open_file().unwrap()?;
105 Ok(Box::new(file))
106 }
107 Stream::Stdin { .. } => {
108 let stdin = self.open_stdin().unwrap();
109 Ok(Box::new(stdin))
110 }
111 Stream::Stdout { .. } => unreachable!("stdout is an output"),
112 }
113 }
114
115 pub fn open_stdin(self) -> Result<io::StdinLock<'static>, Self> {
117 match self.0 {
118 Stream::Stdin { .. } => {
119 let stdin = Box::leak(Box::new(io::stdin()));
120 Ok(stdin.lock())
121 }
122 _ => Err(self),
123 }
124 }
125
126 pub fn open_file(&self) -> Option<io::Result<File>> {
128 match &self.0 {
129 Stream::File(path) => match File::open(&path) {
130 Ok(file) => Some(Ok(file)),
131 Err(e) => Some(Err(io::Error::new(
132 e.kind(),
133 format!(
134 "Failed to open input file `{}`. Cause: {}",
135 path.display(),
136 e
137 ),
138 ))),
139 },
140 _ => None,
141 }
142 }
143
144 pub fn is_tty(&self) -> bool {
146 self.0.is_tty()
147 }
148
149 pub fn path(&self) -> Option<&Path> {
151 self.0.path()
152 }
153}
154
155impl Default for Input {
156 fn default() -> Self {
157 Self(Stream::stdin())
158 }
159}
160
161impl fmt::Display for Input {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 self.0.fmt(f)
164 }
165}
166
167impl FromStr for Input {
168 type Err = std::convert::Infallible;
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 Ok(Self::from(s.as_ref()))
172 }
173}
174
175impl From<&OsStr> for Input {
176 fn from(s: &OsStr) -> Self {
177 if s == STDIO || s == STDIN {
178 Self(Stream::stdin())
179 } else {
180 Self(Stream::file(s))
181 }
182 }
183}
184
185impl From<Input> for OsString {
186 fn from(input: Input) -> Self {
187 input.0.into()
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct Output(Stream);
194
195impl Output {
196 pub fn open(self) -> io::Result<Box<dyn Write + 'static>> {
198 match self.0 {
199 Stream::File(_) => {
200 let file = self.open_file().unwrap()?;
201 Ok(Box::new(file))
202 }
203 Stream::Stdout { .. } => {
204 let stdout = self.open_stdout().unwrap();
205 Ok(Box::new(stdout))
206 }
207 Stream::Stdin { .. } => unreachable!("stdin is an input"),
208 }
209 }
210
211 pub fn open_stdout(self) -> Result<io::StdoutLock<'static>, Self> {
213 match self.0 {
214 Stream::Stdout { .. } => {
215 let stdout = Box::leak(Box::new(io::stdout()));
216 Ok(stdout.lock())
217 }
218 _ => Err(self),
219 }
220 }
221
222 pub fn open_file(&self) -> Option<io::Result<File>> {
224 match &self.0 {
225 Stream::File(path) => match File::create(&path) {
226 Ok(file) => Some(Ok(file)),
227 Err(e) => Some(Err(io::Error::new(
228 e.kind(),
229 format!(
230 "Failed to open output file `{}`. Cause: {}",
231 path.display(),
232 e
233 ),
234 ))),
235 },
236 _ => None,
237 }
238 }
239
240 pub fn is_tty(&self) -> bool {
242 self.0.is_tty()
243 }
244
245 pub fn path(&self) -> Option<&Path> {
247 self.0.path()
248 }
249}
250
251impl Default for Output {
252 fn default() -> Self {
253 Self(Stream::stdout())
254 }
255}
256
257impl fmt::Display for Output {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 self.0.fmt(f)
260 }
261}
262
263impl FromStr for Output {
264 type Err = std::convert::Infallible;
265
266 fn from_str(s: &str) -> Result<Self, Self::Err> {
267 Ok(Self::from(s.as_ref()))
268 }
269}
270
271impl From<&OsStr> for Output {
272 fn from(s: &OsStr) -> Self {
273 if s == STDIO || s == STDOUT {
274 Self(Stream::stdout())
275 } else {
276 Self(Stream::file(s))
277 }
278 }
279}
280
281impl From<Output> for OsString {
282 fn from(output: Output) -> Self {
283 output.0.into()
284 }
285}
286
287#[derive(Debug, Clone)]
288enum Stream {
289 File(PathBuf),
290 Stdin { tty: bool },
291 Stdout { tty: bool },
292}
293
294impl Stream {
295 fn file(path: &OsStr) -> Self {
296 Self::File(path.into())
297 }
298
299 fn stdin() -> Self {
300 Self::Stdin {
301 tty: atty::is(atty::Stream::Stdin),
302 }
303 }
304
305 fn stdout() -> Self {
306 Self::Stdout {
307 tty: atty::is(atty::Stream::Stdout),
308 }
309 }
310
311 fn is_tty(&self) -> bool {
312 matches!(self, Self::Stdin { tty } | Self::Stdout { tty } if *tty)
313 }
314
315 fn path(&self) -> Option<&Path> {
316 if let Self::File(v) = self {
317 Some(v)
318 } else {
319 None
320 }
321 }
322}
323
324impl fmt::Display for Stream {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 match self {
327 Self::File(path) => path.display().fmt(f),
328 Self::Stdin { .. } => STDIN.fmt(f),
329 Self::Stdout { .. } => STDOUT.fmt(f),
330 }
331 }
332}
333
334impl From<Stream> for OsString {
335 fn from(s: Stream) -> OsString {
336 match s {
337 Stream::File(path) => path.into(),
338 Stream::Stdin { .. } => STDIN.into(),
339 Stream::Stdout { .. } => STDOUT.into(),
340 }
341 }
342}