Jq語言

jq是領域特定高級的純函數式程式語言，它採用詞法作用域，其中所有JSON值都是常量。jq支持回溯並可管理JSON數據的無限長字串流。jq支持基於名字空間的模塊系統，並對閉包有一定支持，尤其是它的函數和泛函表達式可以用作其他函數的參數。

## 歷史

jq由Stephen Dolan創建並在2012年10月發行。它被設計為「針對JSON數據的sed類似者」。

jq最初與Icon和Haskell程式語言有關，它採用Haskell實現。隨即移植至C語言。在jq版本1.5中增加支持了正則表達式。

用Go實現的gojq最初發行於2019年，gojq顯著的擴展jq包括了支持YAML。

用Rust實現的jaq，它規定了指稱語義，其項目目標是更快速和更準確的jq實現，仍保持與jq在大多數情況下的兼容性。在2024年3月於其目標中排除了jq的特定高級特徵，比如模塊、SQL風格算子和給非常大JSON文檔的串流解析器。

fq是針對二進位文件的jq，最初發行於2021年。用jq實現的jqjq，最初發行於2022年。jqjq顯著的可以運行自身，擁有REPL並支持`eval`。

針對jq的叫做yq的「包裝器」，它首次發行於2017年，增加支持了YAML、XML和TOML。

## 用法

### 命令行用法

jq典型的用於命令行，並可以協作於其他命令行實用工具，比如curl。下面的例子展示如何將curl命令的輸出通過管道接轉到jq過濾器 (軟體)，從而確定同這個Wikipedia頁面關聯的範疇名字：

```console
$ URL=$(echo 'jq语言' | jq -rR '@uri "https://zh.wikipedia.org/w/api.php?action=parse&page=\(.)&format=json"')
$ curl -s ${URL} | jq '.parse.categories[]."*"'
```

這裡的流水線產生的輸出，由JSON字串的串流組成，它們是：

```console
"小寫標題"
"CS1英语来源_(en)"
"动态类型编程语言"
"函数式编程语言"
"面向文本编程语言"
"2012年建立的程式語言"
"数据查询语言"
"2012年软件"
```

上述curl命令對這個頁面使用了MediaWiki API來產生JSON響應針對寫作本條目之時目標網頁的當前狀況，上述效果也可以實現為：

bash

echo 'Jq语言' | jq -rR '@uri "https://zh.wikipedia.org/wiki/\(.)"' | xargs curl -s | sed -n 's/.*RLCONF=\(.*\);/\1/p' | jq '.wgCategories[]'

。管道（pipe）符号允许curl的输出由jq来访问，它是标准的Unix shell的进程间通信机制。

这里展示的jq过滤器的方法链是如下流水线（pipeline）的简写：

```bash
.["parse"] | .["categories"] | .[] | .["*"]
```

這對應於curl調用所產生的嵌套JSON結構。jq流水線的構造方式，同Unix風格流水線一樣，採用管道符號。

### 嵌入式用法

C語言和Go實現二者都提供函數庫，使得jq功能可以嵌入到其他應用和編程環境之中。

例如，gojq已經集成於SQLite，故而函數可以在其SQL語句中獲得到。這些函數被標記為「確定性的」，故而可以被用在`CREATE INDEX`命令中。

## 運算的模態

jq預設的充當針對JSON輸入的「串流編輯器」，非常像被當作多行文本的「串流編輯器」的sed實用工具。但是jq有一些其他運算模態：

- 它可以將來自一個或多個來源的輸入當作文本的諸行。
- 它可以將來自特定來源的輸入的串流收集到一個JSON陣列之中。
- 它可以使用所謂的「串流解析器」解析其輸入，產生針對所有「葉子」路徑的`[PATH, VALUE]`陣列的串流。

「串流解析器」（streaming parser），在一個或多個JSON輸入太大無法載入記憶體之時特別有用，因為它需求的記憶體典型的相當小。例如，對於任意大的JSON對象的陣列，峰值記憶體需求不比處理最大頂層對象所需要的多出很多。

這些運算模態可以在特定限制下組合起來。

## 語法和語義

### 類型

所有JSON值自身是jq中的值，它們從而有在下列表格中展示的類型。gojq和jaq實現將數區分為整數和浮點數。gojq實現支持無界精度整數算術，同於jq採用Haskell的最初實現。

類型｜過濾器｜例子

數｜`numbers`｜* * 3、3.2、1e6 * * infinite

字串｜`strings`｜* * "Hello" *

布爾值｜`booleans`｜* * true、false

陣列｜`arrays`

對象｜`objects`

空值｜`nulls`｜* * null

null是通常表示空值的一個值，就像任何其他JSON標量一樣；它不是空指針。（對應於NaN）和`<span style="color:green;font-weight: bold;">infinite</span>`（參見IEEE 754），是僅有的兩個不是JSON值的jq標量。

### 形式

jq有特殊語法形式，比如：

- `EXP as $var | ……`：變量綁定。
- `PATH |= VALUE`和`PATH = VALUES`：更新賦值和平凡（plain）賦值。
- `<span style="color:green;font-weight: bold;">if</span> …… <span style="color:green;font-weight: bold;">then</span> …… <span style="color:green;font-weight: bold;">elif</span> …… <span style="color:green;font-weight: bold;">then</span> …… <span style="color:green;font-weight: bold;">else</span> …… <span style="color:green;font-weight: bold;">end</span>`：條件構造。
- `<span style="color:green;font-weight: bold;">reduce</span> EXP <span style="color:green;font-weight: bold;">as</span> $<span style="color:#B00040;">var</span> (INIT; UPDATE)`：串流歸約。
- foreach EXP as $var (INIT; UPDATE; EXTRACT)：輸出中間值的串流歸約。
- def func : EXP;：函數創建。
- `<span style="color:green;font-weight: bold;">include</span> MOD;`和`<span style="color:green;font-weight: bold;">import</span> MOD <span style="color:green;font-weight: bold;">as</span> $<span style="color:#B00040;">name</span>;`：模塊的包含和導入。

jq中有兩種類型的符號，可稱為「變量」的值綁定和函數。二者都是詞法作用域的，表達式只能提及其左側即前面最近的定義的符號，但是函數可以提及自身來創建遞歸函數。

### 過濾器

jq是面向JSON的程式語言，使用`|`符號來連接過濾器形成流水線。例如：

```console
$ echo '[1,2]' | jq 'add'
3
$ jq -n '[1,2] | add'
3
```

這裡jq內的JSON陣列`[1,2]`是求值為陣列的一個jq過濾器。

儘管類似於Unix流水線，jq流水線允許將到來數據，如同並行的發送到在`|`右手端的多於一個接收者。例如，程序`add/length`將計算陣列中數的平均，故而：

```console
$ jq -n '[1,2] | add/length'
1.5
$ jq -nc '[1,2] | [length, add, add/length]'
[2,3,1.5]
```

單獨的點號`.`可以充任占位符（placeholder）或通配符，例如：

```console
$ jq -nc '1 | [., .]'
[1,1]
$ jq -n '2 | pow(.; .)'
4
```

在jq中採用隱式編程風格，階乘可以通過共遞歸運算`recurse()`定義為`[0,1] | recurse([first+1, last*(first+1)]) | last`，下面是其使用例子：

```console
$ echo '0 1 2 6' | jq -c '[limit(.+1; [0,1] | recurse([first+1, last*(first+1)]) | last)]'
[1]
[1,1]
[1,1,2]
[1,1,2,6,24,120,720]
$ jq -n 'first([0,1] | recurse([first+1, last*(first+1)]) | last)'
1
$ jq -n '[0,1,2,6][] | nth(.; [0,1] | recurse([first+1, last*(first+1)]) | last)'
1
1
2
720
$ jq -nc '[0,1,2,6] | map(nth(.; [0,1] | recurse([first+1, last*(first+1)]) | last))'
[1,1,2,720]
```

斐波那契數列可以通過共遞歸運算`recurse()`定義為`[0,1] | recurse([last, add]) | first`，下面是其使用例子：

```console
$ echo '0 1 2 6' | jq -c '[limit(.+1; [0,1] | recurse([last, add]) | first)]'
[0]
[0,1]
[0,1,1]
[0,1,1,2,3,5,8]
$ jq -n 'first([0,1] | recurse([last, add]) | first)'
0
$ jq -n '[0,1,2,6][] | nth(.; [0,1] | recurse([last, add]) | first)'
0
1
1
8
$ jq -nc '[0,1,2,6] | map(nth(.; [0,1] | recurse([last, add]) | first))'
[0,1,1,8]
```

這種無限列表不可以不加限定的使用，也不可以對其進行`last()`運算。下面將其定義為新的命名過濾器：

```python
def fac: nth(.; [0,1] | recurse([first+1, last*(first+1)]) | last);
def fib: nth(.; [0,1] | recurse([last, add]) | first);
```

共遞歸運算`recurse()`可以設置遞推條件：

```console
$ jq -nc '[0 | recurse(.+1; . < 0)]'
[0]
$ jq -nc '[0 | recurse(select(. < 0) | .+1)]'
[0]
$ jq -nc '[0 | recurse(.+1; . <= 6)]'
[0,1,2,3,4,5,6]
$ jq -nc '[0 | recurse(select(. < 6) | .+1)]'
[0,1,2,3,4,5,6]
```

下面的例子展示如何定義參數化的命名過濾器，它格式化從2到36含二者的任何底數的整數：

```python
def tobase($b):
    def digit: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[.:.+1];
    def mod: .%$b;
    def div: (.-mod)/$b;
    def place_values: recurse(select(. >= $b) | div) | mod;
    select(2 <= $b and $b <= 36)
    | [place_values | digit] | reverse | add;
```

這裡的`tobase($b)`充當了庫函數，所以為了應對使用者將`.`作為實際參數傳遞給它的潛在情況，而對其形式參數採用了變量形式。下面將它保存入`tobase.jq`文件，接著使用這個函數：

```console
$ echo '15 16' | jq 'include "./tobase"; tobase(16)'
"F"
"10"
```

下一個例子求解經典的覆面算數學遊戲：

$$\begin{matrix} <pre> & & \text{S} & \text{E} & \text{N} &\text{D} \\ + & & \text{M} & \text{O} & \text{R} & \text{E} \\ \hline = & \text{M} & \text{O} & \text{N} & \text{E} & \text{Y} \\ </pre> \end{matrix}$$

從`M`是在最高位的進位可知：`M`為`1`，`S`為`8`或`9`，`O`為`0`，代碼展示了基於`range()`和`.[]`的生成器：

```python
def send_more_money:
    def choose(m; n; k): . as $used
        | ([range(m; n+1)]-[$used[]])[] | $used+{(k):.};
    def num: reduce .[] as $i (0; 10*. + $i);
    {"m":1,"o":0}
    | choose(8; 9; "s") | choose(2; 9; "e")
    | choose(2; 9; "n") | choose(2; 9; "d")
    | choose(2; 9; "r") | choose(2; 9; "y") 
    | select(([.s,.e,.n,.d] | num) 
        +    ([.m,.o,.r,.e] | num)
        ==([.m,.o,.n,.e,.y] | num))
    | [.s,.e,.n,.d,"+",.m,.o,.r,.e,"=",.m,.o,.n,.e,.y];
send_more_money
```

將上述代碼保存入`send_more_money.jq`文件，接著使用這個函數得到這個謎題僅有的一個解：

```console
$ jq -nc -f ./send_more_money.jq
[9,5,6,7,"+",1,0,8,5,"=",1,0,6,5,2]
```

## 解析表達式文法

在jq和解析表達式文法（PEG）形式化之間有密切關聯。這種關聯源於下列表格中展示的PEG七個基本運算與jq構造之間的等價性。

PEG運算名字｜PEG表示法｜jq運算或過濾器定義

序列有序選擇零或多個一或多個可選

與斷言｜def amp(E): . as $in `| E |` $in;

非斷言

## 八皇后問題例子

回溯法求解八皇后問題，採用函數式編程風格的條件表達式和遞歸函數可以寫為：

```python
def queens:
    def q(r): . as $pl
        | if r < 8 then
            $pl[3] as $cl | $cl[] | . as $c
            | (r+$c | tostring) as $k0
            | (r-$c | tostring) as $k1
            | def place:
                ($k0 | in($pl[1]) | not)
                and ($k1 | in($pl[2]) | not);
              select(place)
            | [$pl[0]+[$c], $pl[1]+{$k0:null},
                $pl[2]+{$k1:null}, $cl-[$c]]
            | q(r+1)
          else
            .[0] end;
    [[], {}, {}, [range(0; 8)]] | q(0)
    | map("abcdefgh"[.:.+1]) | to_entries
    | map(.value+(.key+1 | tostring)) | sort;
queens
```

將這段代碼保存入`queens.jq`文件中，下面演示其執行結果並提取其`92`個解中的前兩個解：

```console
$ jq -nc -f ./queens.jq | wc -l
92
$ jq -nc -f ./queens.jq | sed -n '1,2p'
["a1","b7","c5","d8","e2","f4","g6","h3"]
["a1","b7","c4","d6","e8","f2","g5","h3"]
```

在能採用可逆（reversible）賦值和靜態變量的語言比如Icon的實現中，其3個位置列表共有`8+15+15 = 38`個元素，而這個存儲傳遞風格實現對3個位置列表進行了Zip (電腦科學)變換，共有`3*8*(1+8)/2 = 108`個元素。

進一步採用隱式編程風格，上述代碼可以寫為：

```python
def queens:
   def q(r): . as $pl 
        | $pl[3] as $cl | $cl[] | . as $c
        | (r+$c | tostring) as $k0
        | (r-$c | tostring) as $k1
        | def place: 
            ($k0 | in($pl[1]) | not)
            and ($k1 | in($pl[2]) | not); 
          select(place)
        | [$pl[0]+[$c], $pl[1]+{$k0:null},
            $pl[2]+{$k1:null}, $cl-[$c]];
    def pipeline(i; n): 
        q(i) | if i+1 < n then pipeline(i+1; n) end;
    [[], {}, {}, [range(0; 8)]] | pipeline(0; 8) | .[0]
    | map("abcdefgh"[.:.+1]) | to_entries
    | map(.value+(.key+1 | tostring)) | sort;
queens
```

它還可以採用共遞歸運算`recurse()`寫為：

```python
def queens:
    def q: . as $pl 
        | $pl[4] as $r 
        | $pl[3] as $cl | $cl[] | . as $c
        | ($r+$c | tostring) as $k0
        | ($r-$c | tostring) as $k1
        | def place: 
            ($k0 | in($pl[1]) | not)
            and ($k1 | in($pl[2]) | not); 
          select(place)
        | [$pl[0]+[$c], $pl[1]+{$k0:null}, 
            $pl[2]+{$k1:null}, $cl-[$c], $r+1];
    [[], {}, {}, [range(0; 8)], 0] | recurse(q) 
    | select(.[4] == 8) | .[0]
    | map("abcdefgh"[.:.+1]) | to_entries
    | map(.value+(.key+1 | tostring)) | sort;
queens
```

最後，通過旋轉和反射將可行解歸併為獨立解：

```python
def queens(n):
    def q: . as $pl 
        | $pl[4] as $r 
        | $pl[3] as $cl | $cl[] | . as $c
        | ($r+$c | tostring) as $k0
        | ($r-$c | tostring) as $k1
        | def place: 
            ($k0 | in($pl[1]) | not)
            and ($k1 | in($pl[2]) | not); 
          select(place)
        | [$pl[0]+[$c], $pl[1]+{$k0:null},
            $pl[2]+{$k1:null}, $cl-[$c], $r+1];
    def pipeline(n): 
        q | if n > 1 then pipeline(n-1) end;
    def toletter: 
        "abcdefghijklmnopqrstuvwxyz"[.:.+1];
    def fund_solut(f):
        def inverse: . as $xl
            | reduce range(0; n) as $i
                ([]; .+[$xl | index($i)]);
        def variants:
            [., inverse] | map(., reverse)
            | map(., map(n-1-.)) 
            | map(map(toletter) | add);
        foreach f as $i 
            ([null, {}]; .[1] as $ml
            | ($i | variants) as $nl
            | if all($nl[]; in($ml) | not) then
                [$i, ($ml | .[$nl[]]=null)]
              else
                [null, $ml] end;
            .[0])
        | select (. != null);
    fund_solut([[], {}, {}, [range(0; n)], 0]
        | pipeline(n) | .[0]) 
    | map(toletter) | to_entries
    | map(.value+(.key+1 | tostring)) | sort;
queens(8)
```

下面演示其執行結果：

```console
$ jq -nc -f ./queens.jq | wc -l
12
$ jq -nc -f ./queens.jq 
["a1","b7","c5","d8","e2","f4","g6","h3"]
["a1","b7","c4","d6","e8","f2","g5","h3"]
["a6","b1","c5","d2","e8","f3","g7","h4"]
["a4","b1","c5","d8","e2","f7","g3","h6"]
["a5","b1","c8","d4","e2","f7","g3","h6"]
["a3","b1","c7","d5","e8","f2","g4","h6"]
["a5","b1","c4","d6","e8","f2","g7","h3"]
["a7","b1","c3","d8","e6","f4","g2","h5"]
["a5","b1","c8","d6","e3","f7","g2","h4"]
["a5","b3","c1","d7","e2","f8","g6","h4"]
["a5","b7","c1","d4","e2","f8","g6","h3"]
["a6","b3","c1","d8","e4","f2","g7","h5"]
```

## 外部連結

- jq homepage
- gojq - the Pure Go implementation
- jaq - the Rust implementation
- jqjq - the jq implementation
- jq FAQ
- Awesome jq - curated listing of jq-related resources
- The jq Programming Language page on the Rosetta Code comparative programming tasks project site

## 基本資料

- 日語原文：jq
- 域：jq官方圖標
- 編程范型：純函數式，面向JSON，隱式，管道
- 發行時間：2012年8月21日
- 設計者：Stephen Dolan
- 實作語言：jq：C，gojq：Go，jaq：Rust，jqjq：jq
- 平台：跨平台
- 類型系統：動態
- 作用域：詞法
- 作業系統：跨平台
- 許可證：MIT
- 受影響於：Haskell，Icon，JSON，Unix shell，sed