AWK 实战总结 -- 参数的传递

AWK 实战总结 -- 参数的传递

[TOC]

本文为 AWK 实战时遇到的一个 debug 记录.

区分 arguments 与 input files

背景

我想实现这么一种功能, 有一批 markdown 文件用来发布 blog, 因此需要在其开头(第一行前)插入 front-matter, 如何使用脚本避免一个个地复制粘贴?
首先我把通用的 front-matter 格式放入 front-matter.txt 文件中, 然后希望读取此文件依次插入到所有 .md 文件头部.

下面是 front-matter.txt (jason 格式 + 加上一个 html 的标签)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
title:
date: 2022-01-01 01:00:00
cover: 1.jpg
comment: true
summary:
tags:
- Note
categories:
- Note
toc: true
top:
img:
mathjax:
author:
password:
---
[TOC]

<!-- more -->

思路

  1. 常规 shell 脚本(最容易想到的方法)
    复制 front-matter.txt 到中间文件 temp, 然后利用 >> 把 cat a.md 追加式重新定向到 temp 中完成拼接.
1
2
3
4
temp=$(mktemp -p .)
cp front-matter.txt ${temp}
cat a.md >> ${temp}
mv ${temp} a.md

这种思路的一种变化, 使用 pipe + 重定向:

1
2
3
4
5
file='a.md'
title='front-matter.txt'

echo $title | cat - $file >$file.new
mv $file.new $file
  1. sed 指令原地修改(最优雅, 文本的插入与替换上还是 sed 更有优势.)
    命令行一行指令即可搞定
1
sed -i '1r "front-matter.txt" a.md'
  1. AWK getline 输入

想多练习一下 AWK, 因此我尝试使用 AWK 脚本解决此问题.

首先写一个通用的 AWK 脚本 insert.awk

1
2
3
4
5
6
7
NR==1{
while((getline line < ARGV[2]) > 0 ) {
t=t line "\n";}
print t $0;
close(ARGV[2]);
next
}

调用 insert.awk

1
2
awk -f insert.AWK a.md front-matte.txt > temp
mv temp a.md

此时就出现了问题, 我发现导出的 temp 文件不仅是开头, 在结尾处也被插入了 front-matter.txt 完完整整的内容. debug 后我突然发现原来是自己对 AWK 命令行参数的理解没有到位.

AWK 命令行输入依次为

1
2
ARGV[1]=a.md
ARGV[2]=front-matte.txt

我预想的是 ARGV[1] 作为 input file, ARGV[2] 作为 argument, 也就是被读入的文件. 但是其实这两个参数都是 input file, 都会被处理. 当处理 ARGV[1] 时, ARGV[2]=front-matte.txt 生效, 处理没有问题. 接下来 AWK 会 shift 到 front-matte.txt, 此时的 ARGV[1]=front-matte.txt, ARGV[2] 为空, 因此, ARGV[1]=front-matte.txt 里的内容被原原本本地打印了出来(insert.AWK 的最后一行代码).

解决

AWK 有 -v 的选项可以把参数传入, 以实现把参数以及输入文件区分开来. 修改过后的程序如下:

1
2
3
4
5
6
7
8
NR==1{
while((getline line < read_file) > 0 ) {
t=t line "\n";}
print t $0;
close(read_file);
next
}
//

调用时如下

1
2
AWK -f insert.AWK a.md -v read_file="front-matte.txt" > temp
mv temp a.md

如果不使用 AWK 脚本而是直接在命令行输入整条命令的话, 有更加简洁方式, 即在命令里直接把文件传进去:

1
2
3
awk 'NR==1{while((getline line < "./front-matte.txt") > 0 ) {t=t line "\n";}\
print t $0;close("./front-matte.txt");next} ' a.md > temp &&
mv temp a.md

拓展–AWK 与 shell 之间的参数传递

除了上面的方式, 还有别的方式可以在 AWK 与 shell 之间传递参数吗?
我查了一下, 整理如下:

AWK 中使用 shell 中的变量

  1. "'$var'": 使双括号变为单括号的常量, 传递给 AWK.
1
2
var="test"
awk 'BEGIN{print "'$var'"}'

注意如果 var 中含空格, 为了 shell 不把空格作为分格符, 便应该如下使用(再套一层 ""最内侧):

1
2
var="this is a test"
awk 'BEGIN{print "'"$var"'"}'
  1. '"$var"': 单引号在外, 双引号在内. 类似地, 如果变量含空格, 变为 '""$var""'.

  2. export 变量, 使用 ENVIRON["var"] 形式, 获取环境变量的值:

1
2
3
var="this is a test"
export var
awk 'BEGIN{print ENVIRON["var"]}'
  1. -v 选项(适合于变量个数不多的场景)
1
2
var="this is a test"
awk -v awk_var="$var" 'BEGIN {print awk_var}'
  1. 使用管道

模式 echo $1 $2 | awk "$AWKSCRIPT". 一个具体的例子如下, 计算直角三角形斜边长度.
例子来源于Advanced Bash-Scripting Guide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# hypotenuse.sh: Returns the "hypotenuse" of a right triangle.
# (square root of sum of squares of the "legs")

ARGS=2 # Script needs sides of triangle passed.
E_BADARGS=85 # Wrong number of arguments.

if [ $# -ne "$ARGS" ] # Test number of arguments to script.
then
echo "Usage: `basename $0` side_1 side_2"
exit $E_BADARGS
fi


AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
# command(s) / parameters passed to awk


# Now, pipe the parameters to awk.
echo -n "Hypotenuse of $1 and $2 = "
echo $1 $2 | awk "$AWKSCRIPT"
# ^^^^^^^^^^^^
# An echo-and-pipe is an easy way of passing shell parameters to awk.

exit
  1. 使用 indirect reference

下面是对输入的 filename 的第 column_number 进行 sum up 的代码片段.
可以注意到使用 indirect reference \$${column_number} 可以向 awk 传递参数.
并且不需要要像常规的 indirect reference 那样进行 eval \$${varname}.
代码来自Advanced Bash-Scripting Guide.

1
2
3
4
5
6
7
8
9
10
11
filename=$1         # Name of file to operate on.
column_number=$2 # Which column to total up.
awk "

{ total += \$${column_number} # Indirect reference
}
END {
print total
}

" "$filename"

这几种方法还是有些不同的, 尤其是涉及到一些 元字符(Meta Characters) 的转义时. 虽然有 POSIX 标准规定了元字符, 但是很多既存的环境里, 元字符的使用很不统一, 例如不同 shell, 不同文本处理程序 awk perl 等. 这时 "" 还是 '' 在最内侧就有很大的区别了. 单引号 '' 里面禁止对符号进行解析, 所见即所得, 双引号 "" 阻止部分解析, 但是允许一部分规则进行展开, 再加上引用单双引号的环境对不同字符的格式要求不同, 上面所说的第一二种方法的结果有可能是不同的. 例如, 如果使用 "' '' " 即用双引号包裹单引号的形式在进行 AWK 的正则表达式模式匹配时, 外层的 "" 也会变成待匹配的字符, 而 ' "" ' 即单引号包裹双引号的形式则不会匹配 "". 例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ echo -e "aa\nbb" > t #构造输入文本 t
$ cat t #确认文本内容
aa
bb

$ echo -e "aa\nbb"|xargs -I word awk '/"'word'"/ {print "MATCHED"}' t #使用第一种方法无法匹配, 无输出

$ echo -e "aa\nbb"|xargs -I word awk '/'"word"'/ {print "MATCHED"}' t #使用第二种方法实现匹配
MATCHED
MATCHED

$ echo -e "aa\nbb"|xargs -I word awk '/'word'/ {print "MATCHED"}' t #这种情况下也可以把双引号去掉
MATCHED
MATCHED

而第三第四种方式则更加通用一些, 强制所见即所得. 例子如下, 希望使用 AWK 将 q.md 文件中的 ![](NAME.png) 替换为 <img src="NAME.png" style="zoom:50%;" />, 批量将显示的图片的尺寸缩小一半. 如果使用第一二种方法 <img src="" style="zoom:50%;" /> 这个字符串拆开的 2 个字符串不太好处理, 可能需要插入各种转义符, 再传入 AWK 中需要再用 AWK 的方式重新理解一遍. 使用第三种方法的例子如下:

1
a='<img src="';b='" style="zoom:50%;" />';export a;export b; cat q.md | grep "\[\](" | awk -F"(" '{print $2}'| awk -F")" '{print $1}'| xargs -I file awk -i inplace '/(!)(\[\]\('file'\))/ {r=ENVIRON["a"] "'file'" ENVIRON["b"];num=gsub(/(!)(\[\]\('file'\))/,r);};{print}' q.md

AWK 向 shell 变量传递值

eval 命令执行 AWK 语句.

1
eval $(awk '{printf("var1=%s; var2=%s; var3=%s;",$1,$2,$3)}' abc.txt)

参考链接: https://blog.csdn.net/wsclinux/article/details/72885277

AWK 实战总结 -- 参数的传递

https://www.chuxin911.com/awk_practice_1_20220508/

作者

cx

发布于

2022-05-08

更新于

2023-01-10

许可协议