1 use ag::pipeline::{OutputMode, Pipeline, QueryContainer, TermErrorReporter};
2 use human_panic::setup_panic;
3 use quicli::prelude::*;
4 
5 #[cfg(feature = "self_update")]
6 use self_update;
7 use std::fs::File;
8 use std::io;
9 use std::io::{stdout, BufReader};
10 use structopt::StructOpt;
11 
12 #[global_allocator]
13 static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
14 
15 use crate::InvalidArgs::{CantSupplyBoth, InvalidFormatString, InvalidOutputMode};
16 use structopt::clap::ArgGroup;
17 
18 // Needed to require either "--self-update" or a query
main_arg_group() -> ArgGroup<'static>19 fn main_arg_group() -> ArgGroup<'static> {
20     ArgGroup::with_name("main").required(true)
21 }
22 
23 #[derive(Debug, StructOpt)]
24 #[structopt(
25     after_help = "For more details + docs, see https://github.com/rcoh/angle-grinder",
26     raw(group = "main_arg_group()")
27 )]
28 struct Cli {
29     /// The query
30     #[structopt(group = "main")]
31     query: Option<String>,
32 
33     #[cfg(feature = "self_update")]
34     /// Update agrind to the latest published version Github (https://github.com/rcoh/angle-grinder)
35     #[structopt(long = "self-update", group = "main")]
36     update: bool,
37 
38     /// Optionally reads from a file instead of Stdin
39     #[structopt(long = "file", short = "f")]
40     file: Option<String>,
41 
42     /// DEPRECATED. Use -o format=... instead. Provide a Rust std::fmt string to format output
43     #[structopt(long = "format", short = "m")]
44     format: Option<String>,
45 
46     /// Set output format. One of (json|legacy|format=<rust fmt str>|logfmt)
47     #[structopt(
48         long = "output",
49         short = "o",
50         long_help = "Set output format. Options: \n\
51                      - `json`,\n\
52                      - `logfmt`\n\
53                      - `format=<rust format string>` (eg. -o format='{src} => {dst}'\n\
54                      - `legacy` The original output format, auto aligning [k=v]"
55     )]
56     output: Option<String>,
57 
58     #[structopt(flatten)]
59     verbosity: Verbosity,
60 }
61 
62 #[derive(Debug, Fail)]
63 pub enum InvalidArgs {
64     #[fail(display = "Query was missing. Usage: `agrind 'query'`")]
65     MissingQuery,
66 
67     #[fail(display = "Invalid output mode {}. Valid choices: {}", choice, choices)]
68     InvalidOutputMode { choice: String, choices: String },
69 
70     #[fail(
71         display = "Invalid format string. Expected something like `-o format='{{src}} => {{dst}}'`"
72     )]
73     InvalidFormatString,
74 
75     #[fail(display = "Can't supply a format string and an output mode")]
76     CantSupplyBoth,
77 }
78 
main() -> CliResult79 fn main() -> CliResult {
80     setup_panic!();
81     let args = Cli::from_args();
82     #[cfg(feature = "self_update")]
83     if args.update {
84         return update();
85     }
86     let query = QueryContainer::new(
87         args.query.ok_or(InvalidArgs::MissingQuery)?,
88         Box::new(TermErrorReporter {}),
89     );
90     args.verbosity.setup_env_logger("agrind")?;
91     let output_mode = match (args.output, args.format) {
92         (Some(_output), Some(_format)) => Err(CantSupplyBoth),
93         (Some(output), None) => parse_output(&output),
94         (None, Some(format)) => Ok(OutputMode::Format(format)),
95         (None, None) => parse_output("legacy"),
96     }?;
97     let pipeline = Pipeline::new(&query, stdout(), output_mode)?;
98     match args.file {
99         Some(file_name) => {
100             let f = File::open(file_name)?;
101             pipeline.process(BufReader::new(f))
102         }
103         None => {
104             let stdin = io::stdin();
105             let locked = stdin.lock();
106             pipeline.process(locked)
107         }
108     };
109     Ok(())
110 }
111 
parse_output(output_param: &str) -> Result<OutputMode, InvalidArgs>112 fn parse_output(output_param: &str) -> Result<OutputMode, InvalidArgs> {
113     // for some args, we split on `=` first
114     let (arg, val) = match output_param.find('=') {
115         None => (output_param, "="),
116         Some(idx) => output_param.split_at(idx),
117     };
118     let val = &val[1..];
119 
120     match (arg, val) {
121         ("legacy", "") => Ok(OutputMode::Legacy),
122         ("json", "") => Ok(OutputMode::Json),
123         ("logfmt", "") => Ok(OutputMode::Logfmt),
124         ("format", v) if !v.is_empty() => Ok(OutputMode::Format(v.to_owned())),
125         ("format", "") => Err(InvalidFormatString),
126         (other, _v) => Err(InvalidOutputMode {
127             choice: other.to_owned(),
128             choices: "legacy, json, logfmt, format".to_owned(),
129         }),
130     }
131 }
132 
133 #[cfg(feature = "self_update")]
update() -> CliResult134 fn update() -> CliResult {
135     let crate_version = self_update::cargo_crate_version!();
136     let status = self_update::backends::github::Update::configure()
137         .repo_owner("rcoh")
138         .repo_name("angle-grinder")
139         .bin_name("agrind")
140         .show_download_progress(true)
141         .current_version(crate_version)
142         .build()?
143         .update()?;
144 
145     if crate_version == status.version() {
146         println!(
147             "Currently running the latest version publicly available ({}). No changes",
148             status.version()
149         );
150     } else {
151         println!("Updated to version: {}", status.version());
152     }
153     Ok(())
154 }
155