Contents

使用YQ处理YAML内容

1. 概述

许多程序和服务需要以结构化的方式写下它们的配置。实现此目标的方法之一是使用 YAML 文件。

在 Bash shell 中,我们需要工具来处理来自命令行或脚本的 YAML 内容。

在本教程中,我们将学习yq 实用程序。

2. yq基础

yq命令通常不是标准 Linux 发行版的一部分,因此我们需要手动安装它。然后,让我们了解一下命令的基础知识。

2.1. 版本检查

首先,让我们检查一下版本:

$ yq -V #or --version
yq (https://github.com/mikefarah/yq/) version 4.24.2

我们应该知道存在两种广泛使用的yq实现。**整个教程中使用的这个称为“Go 实现”,而第二个是基于 Python 的。**两者具有非常相似的功能,但语法略有不同。

有关更多详细信息,我们应该访问项目的文档页面

2.2. 默认工作模式

我们注意到默认的yq模式是eval,它允许读取、搜索和编辑 YAML 文件。因此,让我们打印文件personal_data.yaml的全部内容:

$ yq personal_data.yaml
name: "My name"
surname: "Foo"
street_address:
  city: "My city"
  street: "My street"
  number: 1

3.访问属性

**我们应该使用 YAML 文件中的路径来检索属性的值。**首先,让我们打印personal_data.yaml中的name属性:

$ yq '.name' personal_data.yaml
My name

接下来,方括号[]提供结构street_address*中的所有值:*

$ yq '.street_address[]' personal_data.yaml
My city
My street
1

3.1. 寻找价值

**我们可以使用select运算符来查找特定值。**因此,让我们在 YAML 文件中查找“Foo”:

$ yq '.[] | select(. == "Foo")' personal_data.yaml
Foo

让我们注意管道运算符 | 的使用。将根节点的值传递给select运算符。

现在让我们在street_address节点中找到所有以“My”开头的值。所以,我们应用通配符“*”:

$ yq '.street_address[] | select(. == "My*")' personal_data.yaml
My city
My street

3.2. 递归搜索

**使用双点 .. 运算符,我们可以从给定节点开始递归遍历文档。**因此,让我们找到所有以“My”开头的值:

$ yq '.. | select(. == "My*")' personal_data.yaml
My name
My city
My street

接下来,让我们将搜索范围缩小到street_address节点的子节点

$ yq '.street_address | .. | select(. == "My*") ' personal_data.yaml
My city
My street

3.3. 改变价值观

**我们可以使用赋值运算符“=”更改或更新属性。**然后,让我们更改街道号码:

$ yq '.street_address.number = 256' personal_data.yaml
name: "My name"
surname: "Foo"
street_address:
  city: "My city"
  street: "My street"
  number: 256

结果仅显示在标准输出中。因此,我们需要使用i选项就地修改文件。

4. 使用 YAML 的节点

我们也可以查询和修改 YAML 结构。因此,我们可以添加、删除和查找节点。

4.1. 添加和删除节点

让我们在street_address中创建一个新的属性zip_code只需将其添加到路径即可:

$ yq -i '.street_address.zip_code = 16' personal_data.yaml && cat personal_data.yaml
name: "My name"
surname: "Bar"
street_address:
  city: "My city"
  street: "My street"
  number: 1
  zip_code: 16

接下来,以类似简单的方式,让我们使用 del 运算符删除节点:

$ yq -i 'del(.street_address.zip_code)' personal_data.yaml && cat personal_data.yaml
name: "My name"
surname: "Bar"
street_address:
  city: "My city"
  street: "My street"
  number: 1

4.2. 检索节点名称

** to_entries运算符返回键及其值。**然后,让我们在根级别使用它:

$ yq 'to_entries' personal_data.yaml
- key: name
  value: "My name"
- key: surname
  value: "Foo"
- key: street_address
  value:
    city: "My city"
    street: "My street"
    number: 1

**结果以数组元素的形式提供。**此外,我们可以提取密钥:

$ yq 'to_entries | .[] | .key' personal_data.yaml
name
surname
street_address

和价值:

$ yq 'to_entries | .[] | .value' personal_data.yaml
My name
Foo
city: "My city"
street: "My street"
number: 1

4.3. 搜索具有has的节点

让我们尝试在语言数据文件languages.yaml中查找Bash条目:

$ yq languages.yaml
languages:
  - language:
      name: Bash the Bourne-Again shell
      feature: interpreted
  - language:
      name: C++
      feature: compiled, comes together with Bash well
  - language:
      name: Java
      feauture: compiled and interpreted, different from Bash and C++
```
显然,我们不应该在文件范围内搜索像\*Bash\*这样的模式,因为我们也会获取 Java 和 C++ 数据。**因此,我们将仅搜索*name*节点:**
```bash
$ yq  '.. | select(has("name")) | select(.name == "*Bash*")' languages.yaml
name: Bash
feature: interpreted
```
**让我们注意运算符*has*的使用。**对于匹配的节点,它返回 true。此外,搜索通过 next select进行细化,该 select 检查节点的内容。

## 5. 处理多个文件
*yq*可以处理多个文件,这些文件作为参数提供。**而且,我们可以索引这些文件并引用它们。**

作为示例,让我们使用*versions.yaml*文件中的语言版本来修改*languages.yaml*:
```bash
$ yq versions.yaml
Java: openjdk 11.0.14.1 2022-02-08
C++: gcc (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
Bash: GNU bash, version 5.1.8(1)-release (x86_64-redhat-linux-gnu)
```
我们需要转入*eval-all*模式将这两个文件读入内存:
```bash
$ yq eval-all '
    select(fi == 0) as $versions |
    select(fi == 1) |
    .languages[0].language.version = $versions.Bash|
    .languages[1].language.version = $versions.C++ |
    .languages[2].language.version = $versions.Java
' versions.yaml languages.yaml
languages:
  - language:
      name: Bash the Bourne-Again shell
      feature: interpreted
      version: GNU bash, version 5.1.8(1)-release (x86_64-redhat-linux-gnu)
  - language:
      name: C++
      feature: compiled, comes together with Bash well
      version: gcc (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
  - language:
      name: Java
      feature: compiled and interpreted, different from Bash and C++
      version: openjdk 11.0.14.1 2022-02-08
```
**让我们重点介绍如何使用文件索引fi来选择适当的文件。此外,通过*$versions*,我们定义了一个包含第一个 YAML 内容的变量。**此外,为了添加*version*节点,我们通过索引引用*language*数组元素。

## 6. 与 Bash 脚本交互
在 Bash 中编程时,我们需要使用该语言的构造来访问 YAML 结构。

### 6.1. 将 Bash 变量注入到 YAML 文件中
让我们使用 bash 变量更新 YAML 文件中的配置。**因此,我们将使用*env*运算符。**例如,让我们使用主机名创建 YAML 内容:
```bash
$ yq --null-input '.hostname = env(HOSTNAME)'
hostname: fedora35
```
让我们注意*null-input*开关,它告诉命令在没有输入文件的情况下创建 YAML 内容。

### 6.2. 变量的值作为搜索目标
**现在,我们在 YAML 内容中搜索与变量值匹配的值或节点。**首先,我们在*individual_data.yaml*中找到 'Foo' 值:
```bash
$ targetVal=Foo yq '.[] | select(. == env(targetVal))' personal_data.yaml
Foo
```
接下来,让我们使用该变量作为键表的查找键:
```bash
$ targetKey=name yq  ' .[env(targetKey)] ' personal_data.yaml
My name
```
最后,让我们使用*has*搜索包含*city*键的节点:
```bash
$ targetKey=city yq '.. | select(has(env(targetKey)))' personal_data.yaml
city: "My city"
street: "My street"
number: 1
```

### 6.3. 使用*envsubst*进行变量替换
让我们考虑一个简单的 YAML 模板来收集基本系统信息:
```bash
$ yq system_data.yaml
hostname: ${HOSTNAME}
user: ${USER}
shell: ${SHELL}
```
现在我们将用实际值替换所有 *${}* 占位符。**因此,我们应该在*yq*中使用[*envsubst*](/envsubst_command):**
```bash
$ yq '.[] |= envsubst' system_data.yaml
hostname: fedora35
user: joe
shell: /bin/bash
```

### 6.4. 将 YAML 内容读入数组
现在让我们访问脚本内的 YAML。**因此,我们将使用[关联数组](/bash_array),其中节点的名称是键。**因此,让我们使用*yaml_reader*脚本从*individual_data.yaml*导入*street_address*:
```bash
#!/bin/bash
declare -A content
while IFS="=" read -r key value; do content["$key"]=$value; done < <(
  yq '.street_address | to_entries | map([.key, .value] | join("=")) | .[]' personal_data.yaml
)
for key in "${!content[@]}"; do printf "key %s, value %s\n" "$key" "${content[$key]}"; done
```
我们使用[while IFS循环](/ifs_shell_variable)来填充数组。**此外,to_entries结果馈送到映射运算符,以将每个键值对与连接运算符连接成IFS格式。**

现在让我们检查一下结果:
```bash
$ ./yaml_reader
key city, value My city
key number, value 1
key street, value My street
```