use async_trait::async_trait;
use jaq_core::load::{Arena, File, Loader};
use jaq_core::{Compiler, Ctx, Vars, data};
use jaq_json::Val;
use jaq_std::input::{HasInputs, Inputs, RcIter};
use super::{Builtin, Context, read_text_file, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;
mod args;
mod compat;
mod convert;
mod errors;
mod format;
mod regex_compat;
#[cfg(test)]
mod tests;
use args::{FileVarKind, JqArgs, ParseOutcome};
use compat::{
ARGS_VAR_NAME, ENV_VAR_NAME, FILENAME_VAR_NAME, LINENO_VAR_NAME, PUBLIC_ENV_VAR_NAME,
build_compat_prefix,
};
use convert::{JqJson, MAX_JQ_JSON_DEPTH, jq_to_val, parse_json_stream, val_to_jq};
use errors::{format_compile_errors, format_load_errors, format_runtime_error};
use format::{Indent, render, sort_keys as sort_jq_keys};
struct InputData<V>(std::marker::PhantomData<V>);
impl<V: jaq_core::ValT + 'static> data::DataT for InputData<V> {
type V<'a> = V;
type Data<'a> = InputDataRef<'a, V>;
}
#[derive(Clone)]
struct InputDataRef<'a, V: jaq_core::ValT + 'static> {
lut: &'a jaq_core::Lut<InputData<V>>,
inputs: &'a RcIter<dyn Iterator<Item = std::result::Result<V, String>> + 'a>,
}
impl<'a, V: jaq_core::ValT + 'static> data::HasLut<'a, InputData<V>> for InputDataRef<'a, V> {
fn lut(&self) -> &'a jaq_core::Lut<InputData<V>> {
self.lut
}
}
impl<'a, V: jaq_core::ValT + 'static> HasInputs<'a, V> for InputDataRef<'a, V> {
fn inputs(&self) -> Inputs<'a, V> {
self.inputs
}
}
pub struct Jq;
struct FilterInput {
value: Val,
filename: Val,
lineno: Val,
}
#[async_trait]
impl Builtin for Jq {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let parsed = match args::parse(ctx.args) {
ParseOutcome::Args(a) => a,
ParseOutcome::Done(r) => return Ok(r),
};
run_jq(ctx, parsed).await
}
}
async fn run_jq(ctx: Context<'_>, parsed: JqArgs<'_>) -> Result<ExecResult> {
let mut all_var_bindings = parsed.var_bindings.clone();
let mut all_named_args = parsed.named_args.clone();
for req in &parsed.file_var_requests {
let path = resolve_path(ctx.cwd, req.path);
let text = match read_text_file(&*ctx.fs, &path, "jq").await {
Ok(t) => t,
Err(e) => return Ok(e),
};
let value = match req.kind {
FileVarKind::Raw => serde_json::Value::String(text),
FileVarKind::Slurp => match parse_json_stream(&text) {
Ok(vals) => {
let arr: Vec<serde_json::Value> = vals.iter().map(jq_to_serde_value).collect();
serde_json::Value::Array(arr)
}
Err(e) => return Ok(ExecResult::err(format!("{e}\n"), 5)),
},
};
all_var_bindings.push((format!("${}", req.name), value.clone()));
all_named_args.push((req.name.clone(), value));
}
let args_obj = build_args_obj(&parsed.positional_args, &all_named_args);
let file_content: String;
let input = if !parsed.file_args.is_empty() {
let mut combined = String::new();
for file_arg in &parsed.file_args {
let path = resolve_path(ctx.cwd, file_arg);
let text = match read_text_file(&*ctx.fs, &path, "jq").await {
Ok(t) => t,
Err(e) => return Ok(e),
};
if !combined.is_empty() && !combined.ends_with('\n') {
combined.push('\n');
}
combined.push_str(&text);
}
file_content = combined;
file_content.as_str()
} else {
ctx.stdin.unwrap_or("")
};
if input.trim().is_empty() && !parsed.null_input && !(parsed.raw_input && parsed.slurp) {
return Ok(ExecResult::ok(String::new()));
}
let env_obj = {
let mut map = serde_json::Map::new();
for (k, v) in ctx.variables.iter().chain(ctx.env.iter()) {
map.insert(k.clone(), serde_json::Value::String(v.clone()));
}
serde_json::Value::Object(map)
};
let prefix = build_compat_prefix();
let compat_filter = format!("{prefix}\n{}", parsed.filter);
let filter_src = compat_filter.as_str();
let defs = jaq_core::defs()
.chain(jaq_std::defs())
.chain(jaq_json::defs());
let loader = Loader::new(defs);
let arena = Arena::default();
let program = File {
code: filter_src,
path: (),
};
let modules = match loader.load(&arena, program) {
Ok(m) => m,
Err(errs) => {
return Ok(ExecResult::err(format_load_errors(errs), 3));
}
};
let mut var_names: Vec<&str> = all_var_bindings.iter().map(|(n, _)| n.as_str()).collect();
var_names.push(ENV_VAR_NAME);
var_names.push(PUBLIC_ENV_VAR_NAME);
var_names.push(FILENAME_VAR_NAME);
var_names.push(LINENO_VAR_NAME);
var_names.push(ARGS_VAR_NAME);
type D = InputData<Val>;
let input_funs: Vec<jaq_core::native::Fun<D>> = jaq_std::input::funs::<D>()
.into_vec()
.into_iter()
.map(|(name, arity, run)| (name, arity, jaq_core::Native::<D>::new(run)))
.collect();
let regex_funs: Vec<jaq_core::native::Fun<D>> = regex_compat::funs::<D>()
.into_vec()
.into_iter()
.map(|(name, arity, run)| (name, arity, jaq_core::Native::<D>::new(run)))
.collect();
let native_funs = jaq_core::funs::<D>()
.chain(jaq_std::funs::<D>().filter(|(name, _, _)| {
*name != "env" && !regex_compat::SHADOWED_NATIVE_NAMES.contains(name)
}))
.chain(input_funs)
.chain(regex_funs)
.chain(jaq_json::funs::<D>());
let compiler = Compiler::default()
.with_funs(native_funs)
.with_global_vars(var_names.iter().copied());
let filter = match compiler.compile(modules) {
Ok(f) => f,
Err(errs) => {
return Ok(ExecResult::err(format_compile_errors(errs), 3));
}
};
let env_val = jq_to_val(&jq_from_serde(&env_obj));
let args_val = jq_to_val(&jq_from_serde(&args_obj));
let pre_var_vals: Vec<Val> = all_var_bindings
.iter()
.map(|(_, v)| jq_to_val(&jq_from_serde(v)))
.collect();
let inputs_to_process: Vec<FilterInput> = if parsed.null_input {
vec![FilterInput {
value: Val::Null,
filename: Val::Null,
lineno: Val::from(0isize),
}]
} else if parsed.raw_input && parsed.slurp {
vec![FilterInput {
value: Val::from(input.to_string()),
filename: stdin_filename(&parsed),
lineno: Val::from(0isize),
}]
} else if parsed.raw_input {
let fname = stdin_filename(&parsed);
input
.lines()
.enumerate()
.map(|(i, line)| FilterInput {
value: Val::from(line.to_string()),
filename: fname.clone(),
lineno: Val::from(isize::try_from(i + 1).unwrap_or(isize::MAX)),
})
.collect()
} else if parsed.slurp {
match parse_json_stream(input) {
Ok(vals) => {
let arr: JqJson = JqJson::Array(vals);
vec![FilterInput {
value: jq_to_val(&arr),
filename: stdin_filename(&parsed),
lineno: Val::from(0isize),
}]
}
Err(e) => return Ok(ExecResult::err(format!("{e}\n"), 5)),
}
} else {
match parse_json_stream(input) {
Ok(jq_vals) => jq_vals
.iter()
.enumerate()
.map(|(i, v)| FilterInput {
value: jq_to_val(v),
filename: stdin_filename(&parsed),
lineno: Val::from(isize::try_from(i + 1).unwrap_or(isize::MAX)),
})
.collect(),
Err(e) => return Ok(ExecResult::err(format!("{e}\n"), 5)),
}
};
let indent = if parsed.compact_output {
Indent::Compact
} else {
parsed.indent
};
let mut output = String::new();
let mut has_output = false;
let mut all_null_or_false = true;
let metadata: Vec<(Val, Val)> = inputs_to_process
.iter()
.map(|fi| (fi.filename.clone(), fi.lineno.clone()))
.collect();
let value_iter: Box<dyn Iterator<Item = std::result::Result<Val, String>>> = Box::new(
inputs_to_process
.into_iter()
.map(|fi| Ok::<Val, String>(fi.value)),
);
let shared_inputs = RcIter::new(value_iter);
for (outer_idx, jaq_input) in (&shared_inputs).enumerate() {
let jaq_input: Val = match jaq_input {
Ok(v) => v,
Err(e) => {
return Ok(ExecResult::err(format!("jq: input error: {e}\n"), 5));
}
};
let (filename_val, lineno_val) = metadata
.get(outer_idx)
.cloned()
.unwrap_or((Val::Null, Val::from(0isize)));
let mut var_vals: Vec<Val> = pre_var_vals.clone();
var_vals.push(env_val.clone()); var_vals.push(env_val.clone()); var_vals.push(filename_val); var_vals.push(lineno_val); var_vals.push(args_val.clone());
let data = InputDataRef {
lut: &filter.lut,
inputs: &shared_inputs,
};
let cv_ctx = Ctx::<InputData<Val>>::new(data, Vars::new(var_vals));
for result in filter.id.run((cv_ctx, jaq_input)) {
match jaq_core::unwrap_valr(result) {
Ok(val) => {
has_output = true;
let mut jq = val_to_jq(&val);
if parsed.sort_keys {
jq = sort_jq_keys(jq);
}
if !(jq.is_null() || jq.is_false()) {
all_null_or_false = false;
}
let effective_raw = parsed.raw_output || parsed.join_output;
let formatted = if effective_raw {
if let JqJson::String(s) = &jq {
s.clone()
} else {
render(&jq, indent)
}
} else {
render(&jq, indent)
};
output.push_str(&formatted);
if !parsed.join_output {
output.push('\n');
}
}
Err(e) => {
return Ok(ExecResult::err(format_runtime_error(&e), 5));
}
}
}
}
if parsed.exit_status {
if !has_output {
return Ok(ExecResult::with_code(output, 4));
}
if all_null_or_false {
return Ok(ExecResult::with_code(output, 1));
}
}
Ok(ExecResult::ok(output))
}
fn build_args_obj(
positional: &[serde_json::Value],
named: &[(String, serde_json::Value)],
) -> serde_json::Value {
let mut named_map = serde_json::Map::new();
for (k, v) in named {
named_map.insert(k.clone(), v.clone());
}
serde_json::json!({
"positional": positional,
"named": serde_json::Value::Object(named_map),
})
}
fn stdin_filename(parsed: &JqArgs<'_>) -> Val {
match parsed.file_args.first() {
Some(p) => Val::from((*p).to_string()),
None => Val::Null,
}
}
fn jq_from_serde(v: &serde_json::Value) -> JqJson {
convert::serde_to_jq(v, 0, MAX_JQ_JSON_DEPTH).unwrap_or(JqJson::Null)
}
fn jq_to_serde_value(v: &JqJson) -> serde_json::Value {
match v {
JqJson::Null => serde_json::Value::Null,
JqJson::Bool(b) => serde_json::Value::Bool(*b),
JqJson::Number(s) => {
serde_json::from_str::<serde_json::Value>(s).unwrap_or(serde_json::Value::Null)
}
JqJson::String(s) => serde_json::Value::String(s.clone()),
JqJson::Array(arr) => serde_json::Value::Array(arr.iter().map(jq_to_serde_value).collect()),
JqJson::Object(map) => {
let mut out = serde_json::Map::new();
for (k, item) in map {
out.insert(k.clone(), jq_to_serde_value(item));
}
serde_json::Value::Object(out)
}
}
}