33use anyhow:: { Context , Result } ;
44use comfy_table:: Table ;
55use jmespath:: Runtime ;
6+ use regex:: Regex ;
67use serde:: Serialize ;
78use serde_json:: Value ;
89use std:: sync:: OnceLock ;
@@ -20,11 +21,54 @@ pub fn get_jmespath_runtime() -> &'static Runtime {
2021 } )
2122}
2223
23- /// Compile a JMESPath expression using the extended runtime
24+ /// Normalize backtick literals in JMESPath expressions.
25+ ///
26+ /// The JMESPath specification allows "elided quotes" in backtick literals,
27+ /// meaning `` `foo` `` is equivalent to `` `"foo"` ``. However, the Rust
28+ /// jmespath crate requires valid JSON inside backticks.
29+ ///
30+ /// This function converts unquoted string literals like `` `foo` `` to
31+ /// properly quoted JSON strings like `` `"foo"` ``.
32+ ///
33+ /// Examples:
34+ /// - `` `foo` `` -> `` `"foo"` ``
35+ /// - `` `true` `` -> `` `true` `` (unchanged, valid JSON boolean)
36+ /// - `` `123` `` -> `` `123` `` (unchanged, valid JSON number)
37+ /// - `` `"already quoted"` `` -> `` `"already quoted"` `` (unchanged)
38+ fn normalize_backtick_literals ( query : & str ) -> String {
39+ static BACKTICK_RE : OnceLock < Regex > = OnceLock :: new ( ) ;
40+ let re = BACKTICK_RE . get_or_init ( || {
41+ // Match backtick-delimited content, handling escaped backticks
42+ Regex :: new ( r"`([^`\\]*(?:\\.[^`\\]*)*)`" ) . unwrap ( )
43+ } ) ;
44+
45+ re. replace_all ( query, |caps : & regex:: Captures | {
46+ let content = & caps[ 1 ] ;
47+ let trimmed = content. trim ( ) ;
48+
49+ // Check if it's already valid JSON
50+ if serde_json:: from_str :: < Value > ( trimmed) . is_ok ( ) {
51+ // Already valid JSON (number, boolean, null, quoted string, array, object)
52+ format ! ( "`{}`" , content)
53+ } else {
54+ // Not valid JSON - treat as unquoted string literal and add quotes
55+ // Escape any double quotes in the content
56+ let escaped = trimmed. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " ) ;
57+ format ! ( "`\" {}\" `" , escaped)
58+ }
59+ } )
60+ . into_owned ( )
61+ }
62+
63+ /// Compile a JMESPath expression using the extended runtime.
64+ ///
65+ /// This function normalizes backtick literals to handle the JMESPath
66+ /// specification's "elided quotes" feature before compilation.
2467pub fn compile_jmespath (
2568 query : & str ,
2669) -> Result < jmespath:: Expression < ' static > , jmespath:: JmespathError > {
27- get_jmespath_runtime ( ) . compile ( query)
70+ let normalized = normalize_backtick_literals ( query) ;
71+ get_jmespath_runtime ( ) . compile ( & normalized)
2872}
2973
3074#[ derive( Debug , Clone , Copy , clap:: ValueEnum , Default ) ]
@@ -54,10 +98,11 @@ pub fn print_output<T: Serialize>(
5498
5599 // Apply JMESPath query if provided (using extended runtime with 300+ functions)
56100 if let Some ( query_str) = query {
101+ let normalized = normalize_backtick_literals ( query_str) ;
57102 let runtime = get_jmespath_runtime ( ) ;
58103 let expr = runtime
59- . compile ( query_str )
60- . context ( "Invalid JMESPath expression" ) ?;
104+ . compile ( & normalized )
105+ . with_context ( || format ! ( "Invalid JMESPath expression: {}" , query_str ) ) ?;
61106 // Convert Value to string then parse as Variable
62107 let json_str = serde_json:: to_string ( & json_value) ?;
63108 let data = jmespath:: Variable :: from_json ( & json_str)
@@ -142,3 +187,125 @@ fn format_value(value: &Value) -> String {
142187 Value :: Object ( obj) => format ! ( "{{{} fields}}" , obj. len( ) ) ,
143188 }
144189}
190+
191+ #[ cfg( test) ]
192+ mod tests {
193+ use super :: * ;
194+
195+ #[ test]
196+ fn test_normalize_backtick_unquoted_string ( ) {
197+ // Standard JMESPath backtick literal without quotes
198+ assert_eq ! (
199+ normalize_backtick_literals( r#"[?name==`foo`]"# ) ,
200+ r#"[?name==`"foo"`]"#
201+ ) ;
202+ }
203+
204+ #[ test]
205+ fn test_normalize_backtick_already_quoted ( ) {
206+ // Already properly quoted - should not double-quote
207+ assert_eq ! (
208+ normalize_backtick_literals( r#"[?name==`"foo"`]"# ) ,
209+ r#"[?name==`"foo"`]"#
210+ ) ;
211+ }
212+
213+ #[ test]
214+ fn test_normalize_backtick_number ( ) {
215+ // Numbers are valid JSON - should not be quoted
216+ assert_eq ! (
217+ normalize_backtick_literals( r#"[?count==`123`]"# ) ,
218+ r#"[?count==`123`]"#
219+ ) ;
220+ }
221+
222+ #[ test]
223+ fn test_normalize_backtick_boolean ( ) {
224+ // Booleans are valid JSON - should not be quoted
225+ assert_eq ! (
226+ normalize_backtick_literals( r#"[?enabled==`true`]"# ) ,
227+ r#"[?enabled==`true`]"#
228+ ) ;
229+ assert_eq ! (
230+ normalize_backtick_literals( r#"[?enabled==`false`]"# ) ,
231+ r#"[?enabled==`false`]"#
232+ ) ;
233+ }
234+
235+ #[ test]
236+ fn test_normalize_backtick_null ( ) {
237+ // null is valid JSON - should not be quoted
238+ assert_eq ! (
239+ normalize_backtick_literals( r#"[?value==`null`]"# ) ,
240+ r#"[?value==`null`]"#
241+ ) ;
242+ }
243+
244+ #[ test]
245+ fn test_normalize_backtick_array ( ) {
246+ // Arrays are valid JSON - should not be modified
247+ assert_eq ! (
248+ normalize_backtick_literals( r#"`[1, 2, 3]`"# ) ,
249+ r#"`[1, 2, 3]`"#
250+ ) ;
251+ }
252+
253+ #[ test]
254+ fn test_normalize_backtick_object ( ) {
255+ // Objects are valid JSON - should not be modified
256+ assert_eq ! (
257+ normalize_backtick_literals( r#"`{"key": "value"}`"# ) ,
258+ r#"`{"key": "value"}`"#
259+ ) ;
260+ }
261+
262+ #[ test]
263+ fn test_normalize_multiple_backticks ( ) {
264+ // Multiple backtick literals in one expression
265+ assert_eq ! (
266+ normalize_backtick_literals( r#"[?name==`foo` && type==`bar`]"# ) ,
267+ r#"[?name==`"foo"` && type==`"bar"`]"#
268+ ) ;
269+ }
270+
271+ #[ test]
272+ fn test_jmespath_backtick_literal_compiles ( ) {
273+ // The original failing case should now work
274+ let query = r#"[?module_name==`jmespath`]"# ;
275+ let result = compile_jmespath ( query) ;
276+ assert ! (
277+ result. is_ok( ) ,
278+ "Backtick literals should be supported: {:?}" ,
279+ result
280+ ) ;
281+ }
282+
283+ #[ test]
284+ fn test_jmespath_complex_filter ( ) {
285+ // Complex filter expression from the bug report
286+ let query = r#"[?module_name==`jmespath`].uid | [0]"# ;
287+ let result = compile_jmespath ( query) ;
288+ assert ! (
289+ result. is_ok( ) ,
290+ "Complex filter with backtick should work: {:?}" ,
291+ result
292+ ) ;
293+ }
294+
295+ #[ test]
296+ fn test_jmespath_double_quote_literal ( ) {
297+ // Double quotes work as field references, not literals
298+ let query = r#"[?module_name=="jmespath"]"# ;
299+ let result = compile_jmespath ( query) ;
300+ // This compiles but semantically compares field to field, not field to literal
301+ assert ! ( result. is_ok( ) ) ;
302+ }
303+
304+ #[ test]
305+ fn test_jmespath_single_quote_literal ( ) {
306+ // Single quotes are raw string literals in JMESPath
307+ let query = "[?module_name=='jmespath']" ;
308+ let result = compile_jmespath ( query) ;
309+ assert ! ( result. is_ok( ) ) ;
310+ }
311+ }
0 commit comments