use clap::Parser;
use std::io::{self, Read, Write};
use oq::{
compile_filter, encode_to_format, parse_input, run_filter, CompiledFilter, Format,
InputFormat, OqError, OutputFormat,
};
#[derive(Parser, Debug)]
#[command(name = "oq")]
#[command(about = "Object Query - jq for JSON, YAML, TOML, and TOON")]
#[command(version)]
#[command(after_help = "EXAMPLES:
oq '.name' data.json Query JSON file
cat config.yaml | oq '.db' Query YAML from stdin
oq '.deps' Cargo.toml Query TOML file
oq -o yaml '.users' data.json Convert query result to YAML
oq '.' data.yaml -o json Convert YAML to JSON")]
struct Cli {
#[arg(default_value = ".")]
filter: String,
#[arg(value_name = "FILE")]
files: Vec<String>,
#[arg(short = 'i', long = "input", value_name = "FORMAT")]
input_format: Option<InputFormat>,
#[arg(short = 'o', long = "output", value_name = "FORMAT")]
output_format: Option<OutputFormat>,
#[arg(short = 'r', long)]
raw: bool,
#[arg(short = 'c', long)]
compact: bool,
#[arg(short = 's', long)]
slurp: bool,
#[arg(short = 'n', long)]
null_input: bool,
#[arg(long, default_value = "auto")]
color: ColorOption,
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
enum ColorOption {
#[default]
Auto,
Always,
Never,
}
fn main() {
if let Err(e) = run() {
eprintln!("oq: {}", e);
std::process::exit(1);
}
}
fn run() -> Result<(), OqError> {
let cli = Cli::parse();
match cli.color {
ColorOption::Always => yansi::enable(),
ColorOption::Never => yansi::disable(),
ColorOption::Auto => {
if !is_terminal() {
yansi::disable();
}
}
}
let filter = compile_filter(&cli.filter)?;
let stdout = io::stdout();
let mut out = stdout.lock();
if cli.null_input {
let results = run_filter(&filter, serde_json::Value::Null)?;
let out_fmt = cli.output_format.map(Format::from).unwrap_or(Format::Json);
for value in results {
output_value(&mut out, &value, out_fmt, cli.raw, cli.compact)?;
}
} else if cli.files.is_empty() {
let input = read_stdin()?;
process_input(&mut out, &input, &cli, &filter)?;
} else {
for path in &cli.files {
let input = std::fs::read_to_string(path)?;
process_input(&mut out, &input, &cli, &filter)?;
}
}
Ok(())
}
fn process_input(
out: &mut impl Write,
input: &str,
cli: &Cli,
filter: &CompiledFilter,
) -> Result<(), OqError> {
let input_fmt = cli
.input_format
.unwrap_or(InputFormat::Auto)
.detect(input);
let value = parse_input(input, input_fmt)?;
let results = run_filter(filter, value)?;
let output_fmt = cli
.output_format
.map(Format::from)
.unwrap_or(input_fmt);
for value in results {
output_value(out, &value, output_fmt, cli.raw, cli.compact)?;
}
Ok(())
}
fn output_value(
out: &mut impl Write,
value: &serde_json::Value,
format: Format,
raw: bool,
compact: bool,
) -> Result<(), OqError> {
if raw {
if let serde_json::Value::String(s) = value {
writeln!(out, "{}", s)?;
return Ok(());
}
}
let effective_format = match format {
Format::Toml if !value.is_object() => Format::Json,
other => other,
};
let output = match effective_format {
Format::Json => {
if compact {
serde_json::to_string(value)?
} else {
serde_json::to_string_pretty(value)?
}
}
_ => encode_to_format(value, effective_format)?,
};
writeln!(out, "{}", output)?;
Ok(())
}
fn read_stdin() -> Result<String, io::Error> {
let mut input = String::new();
io::stdin().lock().read_to_string(&mut input)?;
Ok(input)
}
fn is_terminal() -> bool {
std::env::var("TERM").is_ok()
}