用 Miller 处理 kubectl 与 helm 输出:PPRINT/TSV 空白结构解析、clean-whitespace 清洗与字段提取实战
2026/9/24 16:31:55 网站建设 项目流程
  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

项目地址:https://gitcode.com/gh_mirrors/mi/miller
点击查看免费下载

本篇技术指南讲解如何用 Miller(mlr)处理kubectl get podshelm list这类"看起来像表格、实际空白结构各不相同"的 Kubernetes 生态命令行输出。读完本文,你将掌握 PPRINT 与 TSV 两种输入格式的选择依据、用clean-whitespace清洗制表符与空格混合输出的完整链路,以及通过 NIDX 格式把解析出的字段(如 release 名称)安全地传给helm uninstall等下游命令的实战方案。

问题背景:为什么 kubectl / helm 输出需要专门处理

kubectlhelm命令都会产生表格化的输出,这类数据属于**以名称为索引(name-indexed)**的结构化数据——每一列都有一个列名(NAMESTATUSAGE等),这正是 Miller 擅长的数据形态。Miller 可以对 CSV、TSV、PPRINT、NIDX 等格式进行解析、过滤、排序和字段提取。

然而,这两个命令的输出在空白字符(whitespace)结构上存在显著差异:

  • kubectl的输出是纯空格对齐的表格(符合 Miller 的 PPRINT 格式);
  • helm list的输出则是制表符(tab)与空格(space)混合的产物,既不是严格的 PPRINT,也不是严格的 TSV。

因此在真正用 Miller 处理它们之前,必须先搞清楚输出中的空白到底是什么字符。下文将逐一拆解。

kubectl 输出的空白结构:纯空格对齐的 PPRINT

kubectl get pods的输出长这样(以命名空间my-namespace为例):

$ kubectl -n my-namespace get pods | head NAME READY STATUS RESTARTS AGE app-5mjwm4-274754k8468 0/1 Completed 0 6m51s app-5mjwm4-274754vdfnf 0/1 Completed 0 6m50s app-5mjwm4-0 1/1 Running 0 6h8m app-5mjwm4-27475nt9cc 0/1 Completed 0 6m53s app-5mjwm4-27474454-dc7wq 0/1 Error 0 16h app-5mjwm4-27475416-tv2ff 0/1 Completed 0 56s app-5mjwm4-2747541t7lgk 0/1 Completed 0 115s app-5mjwm4-27475245-7sg9r 0/1 Completed 0 171m app-5mjwm4-27475410-k4gcr 0/1 Completed 0 6m52s

从视觉上看它是对齐的表格,因此可以判断:PPRINT(Pretty-printed tabular)格式是解析它的合适选择。PPRINT 是 Miller 的一种输入/输出格式,它要求列与列之间通过固定宽度的空格对齐,表头行决定列名。

如何验证空白结构?原文档给出了三种办法:

  1. 把输出送入 vim:kubectl -n my-namespace get pods | vim -,然后在 vim 中执行:set list,隐藏的空白字符会以可见符号显示;
  2. 通过cat -t查看——tab 字符会显示为^I
  3. 通过bat -A(bat 的显示所有字符模式)查看。

用上述任意方法检查后可以确认:kubectl 输出中看似空白的部分实际上全部是空格字符,没有制表符。这是它能被 PPRINT 格式直接解析的前提。

为了进一步确认,一个有用的做法是把表格化输出跑一遍格式转换器,检查表头是否被正确识别为键(key)、其余行是否被正确识别为值(value)。例如用--ipprint读入、--ojson输出,只取第一行:

$ kubectl -n my-namespace get pods | mlr --ipprint --ojson head -n 1 [ { "NAME": "app-5mjwm4-274754k8468", "READY": "0/1", "STATUS": "Completed", "RESTARTS": 0, "AGE": "14m" } ]

这里有两个值得注意的细节:

  • --ipprint让 Miller 以 PPRINT 格式读入标准输入,--ojson让输出为 JSON,head -n 1只保留第一条记录;
  • RESTARTS的值0在 JSON 输出中是不带引号的数字——因为 Miller 会对字段值做类型推断(详见 mlrval_infer.go 相关的实现),纯数字字符串被推断为整数类型;而AGE这类含单位的字符串保持字符串类型。

对 kubectl 输出做排序与过滤:dhms2sec 将 AGE 变成可排序的秒数

假设我们要把未完成(非Completed状态)的 Pod 按存在时长(AGE)排序。AGE 列的值形如6h22m8h56s,是"天时分秒"(days-hours-minutes-seconds)缩写格式,字符串直接排序并不准确(例如8h会排在6h22m之前,但16h8h的字典序关系也不符合直觉)。

Miller 内置的 DSL 函数dhms2sec可以把这种格式转换成秒数,从而获得可正确排序的数值。其实现位于 relative_time.go:它会循环解析形如<数字><单位>的片段,其中单位d(天)× 86400、h(小时)× 3600、m(分钟)× 60、s(秒)× 1,逐项累加得到总秒数;也支持-前缀表示负值,遇到无法识别的单位会返回错误(如dhms2sec("6h22m"): unrecognized unit 'x')。

完整的处理管道如下:

$ kubectl -n service-xyz get pods \ | mlr --pprint \ filter '$STATUS != "Completed"' \ then put '$AGESEC = dhms2sec($AGE)' \ then sort -n AGESEC NAME READY STATUS RESTARTS AGE AGESEC app1-1500-5mjwm4-0 1/1 Running 0 6h22m 22920 app1-1624-6dh711-0 1/1 Running 0 6h27m 23220 app1-1500-pqb9b4-0 1/1 Running 0 6h30m 23400 app1-gbwuwi-2747495lbtzg 0/1 Error 0 7h59m 28740 app1-gbwuwi-0 1/1 Running 0 8h 28800 app1-gbwuwi-27474955r8gq 0/1 Error 0 8h 28800 app1-gbwuwi-27474956rps8 0/1 Error 0 8h 28800 app1-gbwuwi-2747495q7fnz 0/1 Error 0 8h 28800 app1-gbwuwi-2747495vnxgn 0/1 Error 0 8h 28800 app1-gbwuwi-674ddcfd89-2jt64 2/2 Running 0 8h 28800 app3-5c79574b69-8njgr 2/2 Running 0 9h 32400 app3-5c79574b69-np2qj 2/2 Running 0 9h 32400 app3-a56i7c-0 1/1 Running 0 13h 46800 app3-a56i7c-587dfc99cf-zrr4t 2/2 Running 0 13h 46800 app2-1500-pqb9b4-274746pfbfd 0/1 Error 0 13h 46800 app2-1500-pqb9b4-274746jtz8t 0/1 Error 0 13h 46800 app2-1500-pqb9b4-274746pmmhq 0/1 Error 0 13h 46800 app2-1500-pqb9b4-27474624h8fp 0/1 Error 0 13h 46800 app2-1500-pqb9b4-2747462d8n96 0/1 Error 0 13h 46800 app2-1500-pqb9b4-2747462xnmcf 0/1 Error 0 13h 46800 app2-1500-pqb9b4-27474630-95668 0/1 Error 0 13h 46800 app1-1500-pqb9b4-sr5vd 2/2 Running 0 13h 46800 app1-1500-5mjwm4-27474454-dc7wq 0/1 Error 0 16h 57600 app1-1500-5mjwm4-667c6fc66d-b97m9 2/2 Running 0 16h 57600 app1-1624-6dh711-2747435h42j 0/1 Error 0 17h 61200 app1-1624-6dh711-27474370-ph25r 0/1 Error 0 17h 61200 app1-1624-6dh711-74fb5cf9d6-cl5tq 2/2 Running 0 17h 61200

对管道各环节的说明:

  • filter '$STATUS != "Completed"':使用 Miller DSL 的filter动词保留STATUS不为Completed的记录;
  • put '$AGESEC = dhms2sec($AGE)':为每条记录新增字段AGESEC,值为 AGE 折算后的秒数;
  • sort -n AGESEC:按数值(-n表示数值排序,而非字典序)对AGESEC升序排序,于是"最老的仍在运行的 Pod"排在前面(如6h22m → 22920秒);
  • 注意输入命令中省略了--ipprint:因为 Miller 在管道场景下会通过 record_reader_factory.go 之类的工厂逻辑进行格式推断,这里以--pprint指定输出格式为 PPRINT,同时按 PPRINT 读入。

helm list 输出的空白结构:制表符与空格混合的"四不像"

helm list的输出更"挑剔"(原文描述为 a bit fussier)。先直接看原始输出:

$ helm list NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION appdev-an-sc-1500-5mjwm4 service-xyz 1 2022-03-28 11:33:05.389975262 +0000 UTC deployed appdev-cloud-test-7.1.12 appdev-exyzv-load-a56i7c service-xyz 1 2022-03-28 14:45:35.44317196 +0000 UTC deployed appdev-cloud-test-7.1.12 appdev-sa-sc-1500-pqb9b4 service-xyz 1 2022-03-28 14:24:33.978580048 +0000 UTC deployed appdev-cloud-test-7.1.12 appdev-sa-sc-1624-6dh711 service-xyz 1 2022-03-28 10:09:05.966332699 +0000 UTC deployed appdev-cloud-test-7.1.12 appdev-wertzxyffa-gbwuwi service-xyz 1 2022-03-28 19:47:34.96763583 +0000 UTC deployed appdev-cloud-test-7.1.12 staging service-xyz 797 2022-03-28 18:39:34.005120936 +0000 UTC deployed appdev-cloud-test-7.1.12

注意两点:

  1. 各行之间并没有完全对齐(例如第 2、5 行的deployed前比其它行多一个空格)——这说明它不像是规整的 PPRINT;
  2. UPDATED列内存在时区偏移量前导空格... +0000 UTC中的空格),这一点会成为后续解析的麻烦。

如果用 PPRINT 格式直接解析,Miller 会报错:

$ helm list | mlr --ipprint --ojson cat mlr : mlr: CSV header/data length mismatch 7 != 5 at filename (stdin) line 2.

错误信息中的CSV header/data length mismatch 7 != 5说明:第一行(表头)被切出了 7 个字段(某些列名内部被空格切分开了),而数据行只被切出了 5 个字段,字段数不一致导致 PPRINT 解析失败。

接下来用cat -t看隐藏字符——tab 会显示为^I

$ helm list | cat -t NAME ^INAMESPACE ^IREVISION^IUPDATED ^ISTATUS ^ICHART ^IAPP VERSION appdev-an-sc-1500-5mjwm4^Iservice-xyz^I1 ^I2022-03-28 11:33:05.389975262 +0000 UTC^Ideployed^Iappdev-cloud-test-7.1.12^I appdev-exyzv-load-a56i7c^Iservice-xyz^I1 ^I2022-03-28 14:45:35.44317196 +0000 UTC ^Ideployed^Iappdev-cloud-test-7.1.12^I appdev-sa-sc-1500-pqb9b4^Iservice-xyz^I1 ^I2022-03-28 14:24:33.978580048 +0000 UTC^Ideployed^Iappdev-cloud-test-7.1.12^I appdev-sa-sc-1624-6dh711^Iservice-xyz^I1 ^I2022-03-28 10:09:05.966332699 +0000 UTC^Ideployed^Iappdev-cloud-test-7.1.12^I appdev-wertzxyffa-gbwuwi^Iservice-xyz^I1 ^I2022-03-28 19:47:34.96763583 +0000 UTC ^Ideployed^Iappdev-cloud-test-7.1.12^I staging ^Iservice-xyz^I797 ^I2022-03-28 18:39:34.005120936 +0000 UTC^Ideployed^Iappdev-cloud-test-7.1.12^I

真相大白:Helm 的作者在输出中混用了 tab 和空格——列与列之间用 tab 分隔,而 tab 之后又填充了若干空格用于视觉对齐。这种格式:

  • 不是 PPRINT(PPRINT 要求纯空格定宽对齐,而这里用了 tab);
  • 也不是严格的 TSV(TSV 只允许 tab 作为分隔符,不允许在字段值内部出现 tab,但这里 tab 后又跟了空格,而且部分字段值内部也含空格,例如2022-03-28 11:33:05.389975262 +0000 UTC本身含有空格)。

同样地,用格式转换器来观察它的真实结构。先用--itsv(TSV 读入)试一下:

$ helm list | mlr --itsv --ojson head -n 1 [ { "NAME ": "appdev-an-sc-1500-5mjwm4", "NAMESPACE ": "service-xyz", "REVISION": "1 ", "UPDATED ": "2022-03-28 11:33:05.389975262 +0000 UTC", "STATUS ": "deployed", "CHART ": "appdev-cloud-test-7.1.12", "APP VERSION": " " } ]

可以观察到三个问题:

  1. 键(字段名)带着尾部空格"NAME ""NAMESPACE ""UPDATED "等——因为 tab 前的列名被右对齐填充了空格;
  2. 值带着前导/内部空格"REVISION": "1 "的值含有尾部空格,"APP VERSION": " "这个值几乎全是空格(tab 后紧跟空格所致);
  3. UPDATED的键名极长(含 32 个填充空格),直接引用它会很痛苦。

这正是 Miller 的clean-whitespace动词的用武之地:它会对记录的每个字段,把键和值的首尾空白剥离(strip),并把连续多处空白压缩为单个空格(collapse)。在 helm 场景下:

$ helm list | mlr --itsv --ojson clean-whitespace then head -n 1 [ { "NAME": "appdev-an-sc-1500-5mjwm4", "NAMESPACE": "service-xyz", "REVISION": "1 ", "UPDATED": "2022-03-28 11:33:05.389975262 +0000 UTC", "STATUS ": "deployed", "CHART": "appdev-cloud-test-7.1.12", "APP VERSION": "" } ]

清洗之后,键名变得干净(NAMENAMESPACECHART),值也正确归位了。注意两个残余现象:

  • "REVISION": "1 "的值仍然带尾部空格——原因在于压缩空白不会删除单个空格1后的一串空格由 tab 后紧跟空格造成,clean-whitespace只压缩"多个空白为单个",而这里的空格之间没有其他字符,\s+匹配的是"连续的空白字符",1与后续空格之间……(实际上这里空格位于字符串尾部,strip应该会去掉尾部空格。观察输出可知该行为与BIF_clean_whitespace的"先 collapse 后 strip、再类型推断"实现有关,值1在 collapse 后变为1,strip 只作用于首尾,而该字段在键值清洗路径中经过了类型推断(FromInferredType),数值型字符串在推断后转成了 int 类型,最终以字符串呈现时保留了字段原貌)。这一细节属于 helm 输出与 clean-whitespace 交互的边界行为,实际使用时建议结合strp/ssub等做二次处理;
  • "STATUS "键名仍带一个尾部空格、"APP VERSION"的值为空串,说明 helm 输出对部分行/列做了不一致的空白填充,clean-whitespace已尽力将键值对齐到"可正确识别"的程度。

从源码看,clean-whitespace动词的实现位于 clean_whitespace.go,它有三个工作模式:

  • 默认模式(不带选项):同时清洗键和值,逐字段调用BIF_clean_whitespace后重建记录(见cleanWhitespaceInKeysAndValues);
  • -k | --keys-only:只清洗键、不动值(cleanWhitespaceInKeys);
  • -v | --values-only:只清洗值、不动键(cleanWhitespaceInValues)。

底层依赖的 DSL 级函数都在 strings.go 中:

  • lstripstrings.TrimLeft(..., " \t"),去掉左侧空格与 tab;
  • rstripstrings.TrimRight(..., " \t"),去掉右侧空格与 tab;
  • stripstrings.Trim(..., " \t"),去掉两侧空格与 tab;
  • collapse_whitespace:用正则\s+把连续空白替换为单个空格;
  • clean_whitespacestrip(collapse_whitespace(x))的组合,并通过FromInferredType做类型推断(因此" 42 "会变成整数42)。

clean-whitespace帮助输出中(见 reference-verbs.md 与源码中的transformerCleanWhitespaceUsage),官方明确提示:-k-v不能同时指定;要同时清洗键和值,就两者都不加。需要更细粒度控制时,请使用 DSL 函数lstriprstripstripcollapse_whitespaceclean_whitespace

对 helm 输出做排序与过滤:strptime + systime 计算 release 年龄

拿到干净数据后,就可以按UPDATED列排序了。由于UPDATED形如2022-03-28 11:33:05.389975262 +0000 UTC,直接字符串排序在"都是同一天"的场景下恰好等价于时间排序;但更稳妥、也更通用的做法是解析时间戳并计算与当前时刻的年龄差

$ helm list \ | mlr --itsv --opprint clean-whitespace \ then put '$AGESEC = int(systime() - strptime($UPDATED, "%Y-%m-%d %H:%M:%S.%f +0000 UTC"))' \ then sort -n AGESEC \ then cut -x -f 'APP VERSION,UPDATED' NAME NAMESPACE REVISION STATUS CHART AGESEC appdev-sa-sc-1624-6dh711 service-xyz 1 deployed appdev-cloud-test-7.1.12 30874 appdev-an-sc-1500-5mjwm4 service-xyz 797 deployed appdev-cloud-test-7.1.12 34955 appdev-sa-sc-1500-pqb9b4 service-xyz 1 deployed appdev-cloud-test-7.1.12 48993 appdev-xxyzv-load-a56i7c service-xyz 1 deployed appdev-cloud-test-7.1.12 50255 staging service-xyz 1 deployed appdev-cloud-test-7.1.12 60543 appdev-wertzxyffa-gbwuwi service-xyz 1 deployed appdev-cloud-test-7.1.12 65583

这里涉及三个 Miller DSL 时间函数(实现均在 datetime.go):

  • systime():返回当前 Unix 时间戳(浮点秒,float64(time.Now().UnixNano()) / 1.0e9),见 datetime.go;
  • strptime(s, format):把字符串按给定格式解析为时间戳秒数(浮点),内部委托给pkg/pbnjay-strptime包实现,见 datetime.go。格式串%Y-%m-%d %H:%M:%S.%f +0000 UTC中,%f表示微秒/纳秒小数部分,+0000 UTC是字面量,需要与 helm 输出中的时区表示逐字匹配
  • int():把浮点秒差取整为整数秒,便于排序与后续比较。

此外还用到了:

  • cut -x -f 'APP VERSION,UPDATED'-x表示"排除"(exclude),即从输出中去掉APP VERSIONUPDATED两列,只保留其余字段,让表格更聚焦;
  • sort -n AGESEC:按数值升序排序,得到"年龄最小的 release 在最前、最老的 release 在最后"。

关于strptime的时区匹配有个注意点:helm 的UPDATED值中,部分行在+0000前有空格(... UTC... UTC两种形态,见上文cat -t输出中第 2、5 行UTC ^I与其它行UTC^I的差异)。若直接strptime失败,可先对UPDATED做字符串规整(见下一节的ssub技巧),或改用%Y-%m-%d %H:%M:%S.%f这类不含时区字面量的格式。

提取字段交给下游命令:NIDX 输出 + cut 出 NAME + 循环 helm uninstall

最后一个典型场景:把解析出的字段(如 release 名称)提取出来,喂给其它命令——例如对"太老"的 helm release 执行helm uninstall

Miller 的NIDX(Index-numbered, toolkit style)格式非常适合这种用途:它输出"一行一个值、不带字段名"的纯文本列,可以直接作为xargs/shell 循环的输入。切换方式是把输出格式从--opprint换成--onidx,然后用cut -f NAME只保留NAME字段:

$ helm list \ | mlr --itsv --onidx clean-whitespace \ then put '$UPDATED = ssub($UPDATED, " +0000 UTC", "")' \ then put '$AGESEC = int(systime() - strptime($UPDATED, "%Y-%m-%d %H:%M:%S.%f"))' \ then sort -n AGESEC \ then cut -f NAME \ | tee names.txt appdev-sa-sc-1624-6dh711 appdev-an-sc-1500-5mjwm4 appdev-sa-sc-1500-pqb9b4 appdev-xxyzv-load-a56i7c staging appdev-wertzxyffa-gbwuwi

这段管道与上一节相比有两点演进:

  1. ssub($UPDATED, " +0000 UTC", ""):先用ssub(简单字符串替换,非正则)把UPDATED中的字面量+0000 UTC(含前导空格)替换为空串,消除时区偏移量及其前导空格——这正是文档开头提到的"+0000前面的空格是个问题"的解法,也让后续strptime的格式串简化成%Y-%m-%d %H:%M:%S.%f
  2. --onidx:输出切到 NIDX 格式,每行一个字段值,cut -f NAME只输出NAME字段(按字段名选取),tee names.txt同时把结果写到文件并显示在终端。

如果想要更严格的筛选,可以在sort -n AGESEC之后追加then filter '$AGESEC > 86400'(即只保留年龄超过 86400 秒 = 24 小时的 release)之类的过滤条件。

最后,拿到names.txt后就可以用 shell 循环逐个卸载:

$ for name in $(cat names.txt); do helm uninstall $name; done

整个过程形成了一条完整、可审计的运维链路:helm listmlr清洗/解析/排序/筛选 → 提取 NAME → 批量卸载。你也可以把for循环换成xargs -n1 helm uninstall,效果等价,但for循环更便于在循环体内加入日志或条件判断。

小结:处理 kubectl / helm 输出的通用方法

回顾本文,可以提炼出一套可复用的方法论:

  1. 先诊断空白结构:用cat -t(看^I)、vim - :set listbat -A确认输出中到底是纯空格、纯 tab 还是混合;
  2. 再选对输入格式:纯空格定宽对齐 →--ipprint;tab 分隔(值内无 tab)→--itsv;都无法直接解析时,用--itsv+clean-whitespace组合拳;
  3. 用格式转换器自检mlr --iXXX --ojson head -n 1可以快速确认键名和值是否正确归位;
  4. 让数据可排序、可比较dhms2sec处理AGE类时长,strptime+systime处理时间戳并计算年龄,ssub做必要的字符串规整;
  5. 面向下游命令输出--onidx(NIDX 格式)+cut -f 字段名提取纯文本字段,交给teexargs、shell 循环等外部工具。

这些能力全部来自 Miller 对以名称为索引的数据的通用处理模型——kubectlhelm只是两个典型入口,同样的思路稍加调整即可推广到其它 Kubernetes 生态命令(如kubectl get nodeshelm status)乃至任何"类表格但空白结构不规整"的 CLI 输出。相关格式细节可继续查阅 file-formats.md(PPRINT、TSV、NIDX 三节的格式规范),动词与函数参考见 reference-verbs.md 与 reference-dsl-builtin-functions.md。

  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

项目地址:https://gitcode.com/gh_mirrors/mi/miller
点击查看免费下载

相关推荐

上一篇:如何快速上手Finance Skills:5分钟完成AI金融助手配置
下一篇:Llama模型资源限制终极指南:配额管理与资源隔离策略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询