使用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
```