use anyhow::{Context as _, Result};
use clap::{ArgAction, CommandFactory as _, Parser, Subcommand};
use clap_complete::generate;
use colored::Colorize;
use memmap2::{Mmap, MmapOptions};
use serde_json_borrow::Value;
use std::{
fs::OpenOptions,
io::{
self, BufWriter, ErrorKind, IsTerminal as _, Read as _, Write, stdout,
},
path::PathBuf,
str::Utf8Error,
};
use jsongrep::{
commands,
query::{DFAQueryEngine, Query, QueryDFA},
utils::{depth, write_colored_result},
};
#[derive(Parser)]
#[command(
name = "jg",
version,
about,
arg_required_else_help = true,
long_about = None,
disable_help_subcommand = true
)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
query: Option<String>,
#[arg(value_name = "FILE")]
input: Option<PathBuf>,
#[arg(short, long, action = ArgAction::SetTrue)]
ignore_case: bool,
#[arg(long, action = ArgAction::SetTrue)]
compact: bool,
#[arg(long, action = ArgAction::SetTrue)]
count: bool,
#[arg(long, action = ArgAction::SetTrue)]
depth: bool,
#[arg(short, long, action = ArgAction::SetTrue)]
no_display: bool,
#[arg(short = 'F', long, action = ArgAction::SetTrue)]
fixed_string: bool,
#[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_path")]
with_path: bool,
#[arg(long, action = ArgAction::SetTrue, conflicts_with = "with_path")]
no_path: bool,
}
#[derive(Subcommand)]
enum Commands {
#[command(subcommand)]
Generate(GenerateCommand),
}
#[derive(Subcommand)]
enum GenerateCommand {
Shell { shell: clap_complete::Shell },
Man {
#[clap(short, long)]
output_dir: Option<PathBuf>,
},
}
enum Input {
Stdin(String),
File(Mmap),
}
impl Input {
fn to_str(&self) -> Result<&str, Utf8Error> {
match self {
Self::Stdin(buffer) => Ok(buffer.as_str()),
Self::File(mmap) => str::from_utf8(mmap),
}
}
}
fn parse_input_content(input: Option<PathBuf>) -> Result<Input> {
if let Some(path) = input {
let fd =
OpenOptions::new().read(true).open(&path).with_context(|| {
format!("Failed to open file {}", path.display())
})?;
let map = unsafe {
MmapOptions::new().map(&fd).with_context(|| {
format!("Failed to mmap file {}", path.display())
})?
};
Ok(Input::File(map))
} else {
if io::stdin().is_terminal() {
let mut cmd = Args::command();
cmd.print_help()?;
anyhow::bail!("No input specified");
}
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(Input::Stdin(buffer))
}
}
fn main() -> Result<()> {
let args = Args::parse();
match args.command {
Some(Commands::Generate(cmd)) => match cmd {
GenerateCommand::Shell { shell } => {
let mut cmd = Args::command();
generate(shell, &mut cmd, "jg", &mut stdout().lock());
}
GenerateCommand::Man { output_dir } => {
commands::generate::generate_man_pages(
&Args::command(),
output_dir,
)?;
}
},
None => {
let raw_query = args.query.ok_or_else(|| {
anyhow::anyhow!("Query string required unless using subcommand")
})?;
let query: Query = if args.fixed_string {
Query::Sequence(vec![
Query::KleeneStar(Box::new(Query::Disjunction(vec![
Query::FieldWildcard,
Query::ArrayWildcard,
]))),
Query::Field(raw_query),
])
} else {
raw_query.parse().with_context(|| "Failed to parse query")?
};
let input_content = parse_input_content(args.input)?;
let json: Value = serde_json::from_str(
input_content
.to_str()
.context("File contents are not valid utf-8")?,
)
.with_context(|| "Failed to parse JSON")?;
let dfa = if args.ignore_case {
QueryDFA::from_query_ignore_case(&query)
} else {
QueryDFA::from_query(&query)
};
let results = DFAQueryEngine::find_with_dfa(&json, &dfa);
let stdout = stdout().lock();
let show_path = if args.with_path {
true
} else if args.no_path {
false
} else {
stdout.is_terminal()
};
let mut writer = BufWriter::new(stdout);
if args.count {
writeln!(
writer,
"{} {}",
"Found matches:".bold().blue(),
results.len()
)
.with_context(|| "Failed to write to stdout")?;
}
if args.depth {
writeln!(
writer,
"{} {}",
"Depth:".bold().blue(),
depth(&json)
)?;
}
if !args.no_display {
let pretty = !args.compact;
for result in &results {
write_colored_result(
&mut writer,
result.value,
&result.path,
pretty,
show_path,
)?;
}
}
match writer.flush() {
Ok(()) => {}
Err(err) if err.kind() == ErrorKind::BrokenPipe => {}
Err(err) => return Err(err.into()),
}
}
}
Ok(())
}