1use std::{
17 env, ffi,
18 io::{self, IsTerminal},
19 path,
20 rc::Rc,
21 time,
22};
23
24use clap::Parser;
25
26use crate::{
27 conn_expr::{ConnectionExpr, ConnectionExprSource},
28 error::Error,
29 version::{Version, VersionRange},
30};
31
32pub use self::tristate::Tristate;
33
34#[derive(Debug)]
35pub struct Args {
36 pub version_range: Option<VersionRange>,
37 pub conn_expr_src: ConnectionExprSource,
38 pub stdin_from: Option<IoArg>,
39 pub stdout_to: Option<IoArg>,
40 pub stderr_to: Option<IoArg>,
41 pub results_to: Option<IoArg>,
42 pub source_args: Vec<SourceArg>,
43 pub template_args: Vec<TemplateArg>,
44 pub pretty: Tristate,
45 pub color: Tristate,
46}
47
48impl Args {
49 pub fn from_command_line() -> Result<Self, Error> {
50 let mut args_os = env::args_os().collect::<Vec<ffi::OsString>>();
51
52 if let Some(pos) =
56 args_os.iter().position(|arg| *arg == ffi::OsStr::new("-!"))
57 {
58 if args_os
59 .get(pos + 1)
60 .and_then(|value| value.to_str())
61 .map(|value| parse_version_range(value).is_err())
62 .unwrap_or(true)
63 {
64 args_os.insert(pos + 1, "..".into());
65 }
66 }
67
68 Self::try_from(Cli::parse_from(args_os))
69 }
70}
71
72#[derive(Debug, PartialEq)]
73pub enum IoArg {
74 Pipe,
75 File(path::PathBuf),
76}
77
78#[derive(Debug, PartialEq)]
79pub enum SourceArg {
80 Pipe,
81 Expr(String),
82 File(path::PathBuf),
83}
84
85impl SourceArg {
86 fn is_pipe(&self) -> bool {
87 matches!(*self, SourceArg::Pipe)
88 }
89}
90
91impl From<IoArg> for SourceArg {
92 fn from(io: IoArg) -> Self {
93 match io {
94 IoArg::Pipe => SourceArg::Pipe,
95 IoArg::File(p) => SourceArg::File(p),
96 }
97 }
98}
99
100#[derive(Debug)]
101pub struct TemplateArg {
102 pub pos: Option<usize>,
103 pub name: Option<Rc<str>>,
104 pub value: Rc<str>,
105}
106
107impl TryFrom<Cli> for Args {
108 type Error = Error;
109
110 fn try_from(cli: Cli) -> Result<Self, Self::Error> {
111 let (shebang_mode, assert_version) = match cli.shebang_guard {
112 Some(version) => (true, version),
113 None => (false, None),
114 };
115
116 let mut pos_arg_it = cli.pos_args.iter();
117
118 let source_args = if shebang_mode {
125 let pos_arg = pos_arg_it.next().ok_or(Error::NoInput)?;
130 let src_arg = IoArg::parse_from_path(pos_arg)
131 .map(SourceArg::from)
132 .map_err(|_| Error::BadSourceFile)?;
133 vec![src_arg]
134 } else if !cli.exprs.is_empty() {
135 cli
136 .exprs
137 .iter()
138 .map(|e| SourceArg::Expr(e.clone()))
139 .collect()
140 } else if !cli.files.is_empty() {
141 cli
142 .files
143 .iter()
144 .map(|f| {
145 IoArg::parse_from_path_or_pipe(f)
146 .map(SourceArg::from)
147 .map_err(|_| Error::BadSourceFile)
148 })
149 .collect::<Result<_, _>>()?
150 } else if !io::stdin().is_terminal() {
151 vec![SourceArg::Pipe]
152 } else if let Some(f) = pos_arg_it.next() {
153 vec![IoArg::parse_from_path_or_pipe(f)
154 .map(SourceArg::from)
155 .map_err(|_| Error::BadSourceFile)?]
156 } else if cli.wait_port_file.is_some() {
157 vec![]
158 } else {
159 return Err(Error::NoInput);
160 };
161
162 let stdin_reserved =
163 match source_args.iter().filter(|s| s.is_pipe()).count() {
164 0 => false,
165 1 => true,
166 _ => return Err(Error::StdInConflict),
167 };
168
169 let stdin_from = cli
170 .stdin
171 .as_ref()
172 .map(IoArg::try_from)
173 .transpose()
174 .map_err(|_| Error::BadStdIn)?;
175 if stdin_from.is_some() && stdin_reserved {
189 return Err(Error::StdInConflict);
190 }
191
192 let args = cli
193 .args
194 .iter()
195 .map(|arg| TemplateArg::parse(None, arg))
196 .chain(
197 pos_arg_it
198 .enumerate()
199 .map(|(i, arg)| TemplateArg::parse(Some(i), arg)),
200 )
201 .collect::<Result<_, _>>()?;
202
203 let conn_expr_src = if let Some(ref h) = cli.port {
204 h.into()
205 } else {
206 ConnectionExprSource::PortFile {
207 path: cli.port_file.clone(),
208 wait_for: cli.wait_port_file.map(time::Duration::from_secs),
209 }
210 };
211
212 fn tristate(on: bool, off: bool) -> Tristate {
213 use Tristate::*;
214 match (on, off) {
215 (true, false) => On,
216 (false, true) => Off,
217 _ => Auto,
218 }
219 }
220
221 Ok(Self {
222 version_range: assert_version,
223 conn_expr_src,
224 stdin_from,
225 stdout_to: if cli.no_stdout {
226 None
227 } else {
228 Some(
229 cli
230 .stdout
231 .as_ref()
232 .map(IoArg::from_path)
233 .unwrap_or(IoArg::Pipe),
234 )
235 },
236 stderr_to: if cli.no_stderr {
237 None
238 } else {
239 Some(
240 cli
241 .stderr
242 .as_ref()
243 .map(IoArg::from_path)
244 .unwrap_or(IoArg::Pipe),
245 )
246 },
247 results_to: if cli.no_results {
248 None
249 } else {
250 Some(
251 cli
252 .results
253 .as_ref()
254 .map(IoArg::from_path)
255 .unwrap_or(IoArg::Pipe),
256 )
257 },
258 source_args,
259 template_args: args,
260 pretty: tristate(cli.pretty, cli.no_pretty),
261 color: tristate(cli.color, cli.no_color),
262 })
263 }
264}
265
266impl TemplateArg {
267 fn parse(
268 pos: Option<usize>,
269 s: impl AsRef<ffi::OsStr>,
270 ) -> Result<Self, Error> {
271 if let Some(s) = s.as_ref().to_str() {
272 if let Some((name, value)) = s.split_once('=') {
273 Ok(Self {
274 pos,
275 name: Some(name.to_string().into()),
276 value: value.to_string().into(),
277 })
278 } else if pos.is_some() {
279 Ok(Self {
280 pos,
281 name: None,
282 value: s.to_string().into(),
283 })
284 } else {
285 Err(Error::UnnamedNonPositionalTemplateArgument)
287 }
288 } else {
289 Err(Error::NonUtf8TemplateArgument)
291 }
292 }
293}
294
295impl IoArg {
296 fn from_path(path: impl AsRef<path::Path>) -> Self {
297 IoArg::File(path.as_ref().to_owned())
298 }
299
300 fn parse_from_path(s: impl AsRef<ffi::OsStr>) -> Result<Self, IoParseError> {
301 let s = s.as_ref();
302 if let Ok(p) = path::PathBuf::from(s).canonicalize() {
303 Ok(IoArg::File(p))
304 } else {
305 Err(IoParseError(s.to_string_lossy().into()))
306 }
307 }
308
309 fn parse_from_path_or_pipe(
310 s: impl AsRef<ffi::OsStr>,
311 ) -> Result<Self, IoParseError> {
312 let s = s.as_ref();
313 if s == "-" {
314 Ok(IoArg::Pipe)
315 } else if let Ok(p) = path::PathBuf::from(s).canonicalize() {
316 Ok(IoArg::File(p))
317 } else {
318 Err(IoParseError(s.to_string_lossy().into()))
319 }
320 }
321}
322
323impl TryFrom<&ffi::OsString> for IoArg {
324 type Error = IoParseError;
325
326 fn try_from(s: &ffi::OsString) -> Result<Self, Self::Error> {
327 if s == "-" {
328 Ok(IoArg::Pipe)
329 } else if let Ok(p) = path::PathBuf::from(s).canonicalize() {
330 Ok(IoArg::File(p))
331 } else {
332 Err(IoParseError(s.to_string_lossy().into()))
333 }
334 }
335}
336
337#[derive(Debug, thiserror::Error)]
338#[error("bad file agument: {0}")]
339pub struct IoParseError(String);
340
341mod tristate {
342
343 use std::{fmt, str};
344
345 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
346 pub enum Tristate {
347 On,
348 Off,
349 Auto,
350 }
351
352 impl Tristate {
353 pub fn to_bool(&self, default: bool) -> bool {
354 use Tristate::*;
355 match self {
356 On => true,
357 Off => false,
358 Auto => default,
359 }
360 }
361 }
362
363 impl fmt::Display for Tristate {
364 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
365 use Tristate::*;
366 write!(
367 f,
368 "{}",
369 match self {
370 On => "on",
371 Off => "off",
372 Auto => "auto",
373 }
374 )
375 }
376 }
377
378 impl str::FromStr for Tristate {
379 type Err = ParseTristateSwitchError;
380
381 fn from_str(s: &str) -> Result<Self, Self::Err> {
382 use Tristate::*;
383 match s {
384 "on" => Ok(On),
385 "off" => Ok(Off),
386 "auto" => Ok(Auto),
387 _ => Err(ParseTristateSwitchError),
388 }
389 }
390 }
391
392 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
393 pub struct ParseTristateSwitchError;
394
395 #[cfg(test)]
396 mod test {
397 use super::*;
398 use Tristate::*;
399
400 #[test]
401 fn parse_tristate_switch() {
402 assert_eq!("on".parse::<Tristate>(), Ok(On));
403 assert_eq!("off".parse::<Tristate>(), Ok(Off));
404 assert_eq!("auto".parse::<Tristate>(), Ok(Auto));
405
406 assert_eq!("".parse::<Tristate>(), Err(ParseTristateSwitchError));
407 assert_eq!(" on".parse::<Tristate>(), Err(ParseTristateSwitchError));
408 assert_eq!(
409 "whatever".parse::<Tristate>(),
410 Err(ParseTristateSwitchError)
411 );
412 }
413 }
414}
415
416#[derive(Debug, clap::Parser)]
421#[command(
422 about = "Non-interactive nREPL client for scripts and command-line",
423 version,
424 max_term_width = 80
425)]
426struct Cli {
427 #[arg(
429 long,
430 short,
431 visible_alias = "host",
432 value_name = "[[[USER@]TUNNEL[:PORT]:]HOST:]PORT"
433 )]
434 port: Option<ConnectionExpr>,
435
436 #[arg(long, value_name = "FILE")]
438 port_file: Option<path::PathBuf>,
439
440 #[arg(long, visible_alias = "namespace", value_name = "NAMESPACE")]
442 ns: Option<String>,
443
444 #[arg(
446 long = "expr",
447 short,
448 value_name = "EXPRESSION",
449 conflicts_with = "files"
450 )]
451 exprs: Vec<String>,
452
453 #[arg(
455 long = "file",
456 short = 'f',
457 value_name = "FILE",
458 conflicts_with = "exprs"
459 )]
460 files: Vec<ffi::OsString>,
461
462 #[arg(long, visible_aliases = &["in", "input"], value_name = "FILE")]
464 stdin: Option<ffi::OsString>,
465
466 #[arg(long, visible_aliases = &["out", "output"], value_name = "FILE")]
468 stdout: Option<path::PathBuf>,
469
470 #[arg(
472 long,
473 visible_aliases = &["no-out", "no-output"],
474 conflicts_with = "stdout",
475 )]
476 no_stdout: bool,
477
478 #[arg(long, visible_alias = "err", value_name = "FILE")]
480 stderr: Option<path::PathBuf>,
481
482 #[arg(
484 long,
485 visible_aliases = &["no-err"],
486 conflicts_with = "stderr",
487 )]
488 no_stderr: bool,
489
490 #[arg(long, visible_aliases = &["res", "values"], value_name = "FILE")]
492 results: Option<path::PathBuf>,
493
494 #[arg(
496 long,
497 visible_aliases = &["no-res", "no-values"],
498 conflicts_with = "results",
499 )]
500 no_results: bool,
501
502 #[arg(long = "arg", short = 'a', value_name = "NAME=VALUE")]
504 args: Vec<String>,
505
506 #[arg(value_name = "ARG")]
508 pos_args: Vec<ffi::OsString>,
509
510 #[arg(
512 short = '!',
513 value_name = "VERSION_EXPR",
514 value_parser = parse_version_range,
515 conflicts_with_all = &["exprs", "files"],
516 )]
517 shebang_guard: Option<Option<VersionRange>>,
518
519 #[arg(long = "wait-port-file", value_name = "SECONDS")]
521 wait_port_file: Option<u64>,
522
523 #[arg(
525 long = "timeout",
526 value_name = "SECONDS",
527 value_parser = not_implemented::<u32>,
528 )]
529 _timeout: Option<u32>,
530
531 #[arg(long, conflicts_with = "no_pretty")]
533 pretty: bool,
534
535 #[arg(long, conflicts_with = "pretty")]
537 no_pretty: bool,
538
539 #[arg(long, conflicts_with = "no_color")]
541 color: bool,
542
543 #[arg(long, conflicts_with = "color")]
545 no_color: bool,
546}
547
548fn parse_version_range(s: &str) -> Result<VersionRange, &'static str> {
549 if let Ok(start) = s.parse::<Version>() {
550 let end = start.next_breaking();
551 Ok(VersionRange {
552 start: Some(start),
553 end: Some(end),
554 inclusive: false,
555 })
556 } else if let Ok(range) = s.parse::<VersionRange>() {
557 Ok(range)
558 } else {
559 Err("bad version or version range")
560 }
561}
562
563fn not_implemented<T>(_: &str) -> Result<T, &'static str> {
564 Err("this feature has not been implemented yet, sorry")
565}