Let Expressions (JEP-18)¶
Let expressions add lexical scoping to JMESPath, allowing you to bind intermediate results to named variables. This is the single biggest composability improvement for complex queries.
Syntax¶
Bind a value to $variable, then evaluate body with that variable in scope.
Multiple Bindings¶
Bind multiple variables in a single let expression. Bindings are evaluated left-to-right in the outer scope (earlier bindings are not visible to later ones in the same let).
Nested Lets¶
Variables from outer lets are available in inner lets.
Examples¶
Naming Intermediate Results¶
Without let expressions, complex queries repeat subexpressions:
echo '{"people": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, {"name": "Charlie", "age": 35}]}' \
| jpx 'people[?age > `28`] | [*].name | sort(@)'
# ["Alice", "Charlie"]
With let expressions, name the intermediate steps:
echo '{"people": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, {"name": "Charlie", "age": 35}]}' \
| jpx 'let $adults = people[?age > `28`] in sort($adults[*].name)'
# ["Alice", "Charlie"]
Parameterizing Thresholds¶
echo '{"scores": [85, 92, 67, 78, 95, 43]}' \
| jpx 'let $cutoff = `80` in scores[?@ >= $cutoff]'
# [85, 92, 95]
Multiple Bindings for Readable Transforms¶
echo '{"orders": [{"item": "Book", "qty": 2, "price": 15}, {"item": "Pen", "qty": 5, "price": 3}]}' \
| jpx 'let $items = orders[*].item, $totals = orders[*].{item: item, total: multiply(qty, price)} in $totals'
# [{"item": "Book", "total": 30}, {"item": "Pen", "total": 15}]
Nested Lets with Shadowing¶
Variables can be shadowed in inner scopes. The inner binding takes precedence within its scope, and the outer value is restored afterward:
Using Variables in Filters¶
echo '{"threshold": 30, "people": [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 35}]}' \
| jpx 'let $min_age = threshold in people[?age >= $min_age][*].name'
# ["Bob"]
Variables with Functions¶
echo '{"data": [3, 1, 4, 1, 5, 9, 2, 6]}' \
| jpx 'let $sorted = sort(data), $count = length(data) in {sorted: $sorted, count: $count}'
# {"sorted": [1, 1, 2, 3, 4, 5, 6, 9], "count": 8}
Scoping Rules¶
- Lexical scoping: Variables are visible only within the
inbody of the let that defines them. - Shadowing: An inner let can rebind a variable name. The original value is restored when the inner scope ends.
- Projection stopping: Let expressions stop projections, similar to pipe expressions. Inside a
let ... in body, the body evaluates against the current node, not projected elements. - Binding evaluation order: In
let $a = e1, $b = e2 in body, bothe1ande2are evaluated in the scope before the let (the outer scope).$ais not visible when evaluatinge2.
Strict Mode¶
Let expressions are a JEP-18 extension to the JMESPath specification. They are disabled in strict mode (--strict or JPX_STRICT=1), which enforces standard JMESPath compliance only.
echo '{"x": 1}' | jpx --strict 'let $a = x in $a'
# Error: Let expressions are not available in strict mode
When to Use Let Expressions¶
- Complex multi-step queries: Name intermediate results instead of deeply nesting pipes
- Repeated subexpressions: Compute a value once and reference it multiple times
- Parameterized filters: Bind thresholds or configuration values for readability
- Building structured output: Compute several derived values and assemble them into an object