Skip to content

linux

5 posts with the tag “linux”

Awk文本处理语言

AWK是一种处理文本文件的语言。它将文件作为记录序列处理。在一般情况下,文件内容的每行都是一个记录。每行内容都会被分割成一系列的域,因此,我们可以认为一行的第一个词为第一个域,第二个词为第二个,以此类推。AWK程序是由一些处理特定模式的语句块构成的。AWK一次可以读取一个输入行。对每个输入行,AWK解释器会判断它是否符合程序中出现的各个模式,并执行符合的模式所对应的动作。

—— 阿尔佛雷德·艾侯The A-Z of Programming Languages: AWK

Awk 是一个维护和处理文本数据文件的强大语言。在文本数据有一定的格式,即每行数据包 含多个以分界符分隔的字段时,显得尤其有用。它非常强大,专为文本处理而设计。 它的名字来源于其作者的姓氏Alfred Aho, Peter Weinberger, and Brian Kernighan。

AWK有下面几个变种:

  • AWK是最原始的AWK, 来自 AT&T 实验室的原始AWK
  • NAWK - 来自AT&T实验室的更新和改进的AWK版本
  • GAWK是GNU AWK。 所有GNU/Linux发行版都默认提供GAWK, 它与AWK和NAWK完全兼容

AWK 可以用来处理很多任务,如文字处理、格式化的文本报告等。本文中AWK如果没有特别说明,那么指的就是GAWK

awk

Awk的基础语法:

Terminal window
awk options '/pattern/{action}' input-file

awk options '{action}' input-file
  • options 是可选参数,主要有-F-f-v:
    • -F fs 或 --field-separator fs:字段分界符,如不指定,默认是使用空格作为分界符
    • -f scripfile or --file scriptfile:从文件中读取awk命令,文件可以使用任意扩展名(也可不用),使用.awk扩展名便于维护
    • -v var=value or --asign var=value:赋值一个用户定义变量
  • /pattern/{action} 需要用单引号包围起来
  • /pattern 是可选的,如不指定,awk将处理input-file中所有的记录,如果指定了,那么只会对处理匹配模式的记录
  • {action} 是 awk 命令, 可以是单个命令,也可以是多个命令,所有的命令必须放在{}中间
  • input-file 是指要处理的文件
Terminal window
$ awk '/screen/{print $1}' /etc/passwd
_screensaver:*:203:203:Screensaver:/var/empty:/usr/bin/false

在典型的awk程序中包含三个区域(BEGIN区域、body区域、END区域):

awk 'BEGIN{awk commands} /pattern/{action} END{awk commands}'

BEGIN区域:

该区域的语法:

BEGIN {awk commands}

BEGIN区域的命令在读取文件之前、在执行body区域命令之前执行,而且仅执行一次。 这里块区域适合初始化变量、打印报告头部信息等工作。此外需要注意的是BEGIN是关键字且必须是大写的,同时BEGIN区域是可选的 BEGIN区域的命令包含在{}中,可以是一个也可以是多个命令

body区域: 该区域的语法:

/pattern/{action}

body区域的命令会在文件读取一行就执行一次, /pattern/是可选的。

END区域 该区域的语法:

END{awk commands}

END区域会在执行完所有操作后再执行,且只执行一次,这里适合执行一些清理操作,或打印报文结尾信息等。END是关键字需要大写,END区域可以有一个或多个命令,需要包含在 {} 中,同时END区域是可选的。

awk

awk在读取文件并执行body区域命令前,执行BEGIN区域命令一次。然后每读取一次输入行,就会执行一次body区域命令,该区域命令可以由多个/pattern/{action} 组成,会依次执行。最后程序在结束时,会执行一次BEGIN区域命令。

Terminal window
$ date | awk 'BEGIN{print "----------Current Time-----------"} \
{print $4}END{print "----------- END -----------------"}'
----------Current Time-----------
15:51:45
----------- END -----------------

Awk提供了很多内置变量。如awk默认分隔符是空格,可以使用-F选项来指定它,如:

Terminal window
awk -F':' '{print $1}' /etc/passwd

这样可以通过内置变量 FS 来完成,如:

Terminal window
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd

awk还提供了其他的内置变量,如:

变量说明
ARGC保存着传递给awk脚本的所有参数的个数
ARGVARGV 是一个数组,保存着传递给awk脚本的所有参数,其索引范围从0到ARGC
ARGINDARGINDARGV 的一个索引, ARGV[ARGIND] 会返回当前正在处理的文 件名
FILENAME当前处理的文件名(awk处理多个输入文件时很有用)
FS输入字段分隔符
OFS输出字段分隔符
RS记录分隔符
ORS输出记录分隔符
NF”number of fields in the current record”, 当前记录的字段个数
NR”ordinal number of the current record”,当前记录在所有记录中的序号
FNR当前处理的记录号,在处理一个新的文件时FNR会被重置为1,而NR不会被重置
ENVIRONENVIRON 是一个包含所有 shell 环境变 量的数组,其索引就是环境变量的名称。
IGNORECASEIGNORECASE 的默认值是0,所有awk区分大小写。值设置为 1 时,则不区分大小写
ERRNO当执行 I/O 操作(比如 getline)出错时,变量 ERRNO 会保存错误信息
FIELDWIDTHS按固定宽度来解析字段,如文件中有三列,每列分别含有4、5、6个字符,那么可以设置成BEGIN {FIELDWIDTHS="4 5 6"}
RSTARTmatch() 函数匹配的字符串的起始位置,如果没有匹配则为0(匹配时从1开始)
RLENGTHmatch() 函数匹配的字符串的长度
SUBSEP数组中下标分隔符
Terminal window
$ date | awk 'BEGIN{OFS="-"} {print $2, $3, $6}'
May-23-2022

和其他程序设计语言一样,awk允许在程序中设置变量。变量以字母开头,后续字符可以是数字、字符、下划线,但关键字 不能作为awk变量。变量可以直接使用而不需要事先声明,而且没有数据类型的概念,一个awk变量是number还是string 取决于变量所处的上下文。如果要初始化变量,最好在 BEGIN 区域中操作(因为 BEGIN 区域只会执行一次)

也可以使用 -v 选项对用户定义的变量进行赋值,该变量在 BEGIN 区域也是可用的,如:

$ awk -v hello=$date '{print hello}'

awk 支持多种运算,这些运算与 C 语言基本相同。

操作符描述
+取正(返回数字本身)
-取反
++自增
自减
操作符描述
+
-
*
/
%取余
操作符描述
空格空格是连字符的操作符,如str3=str2 str1 str3为str2 连接str1后的内容
操作符描述
=赋值
+=加法赋值
-=减法赋值
*=乘法赋值
/=触发赋值
%/取余赋值
操作符描述
>大于
>=大于等于
<小于
<=小于等于
==等于
!=不等于
&&
||
操作符描述
~匹配
!~不匹配

awk提供完备的流程控制语句类似于 C 语言:if, if-else, while, do-while, for, break, continue

Awk 的数组,都是关联数组,即一个数组包含多个”索引/值”的元素。索引没必要是一系列 连续的数字,实际上,它可以使字符串或者数字,并且不需要指定数组长度。 语法:

arrayname[string]=value
- arrayname是数组名称
- string是数组索引
- value是为数组元素赋的值

可以使用arrayname[index]访问数组中的某个特定元素:

Terminal window
$ cat array-assign.awk
BEGIN {
item[101]="HD Camcorder";
item[102]="Refrigerator";
item[103]="MP3 Player";
item[104]="Tennis Racket";
item[105]="Laser Printer";
item[1001]="Tennis Ball";
item[55]="Laptop";
item["na"]="Not Available";
print item["101"];
print item[102];
print item["103"];
print item[104];
print item["105"];
print item[1001];
print item["na"];
}
$ awk -f array-assign.awk
HD Camcorder
Refrigerator
MP3 Player
Tennis Racket
Laser Printer
Tennis Ball
Not Available

如果视图访问一个不存在的数组元素,awk 会自动以访问时指定的索引建立该元素,并赋予 null 值。为了避免这种情况,在使用前最后检测元素是否存在。

可以使用if语句检查元素是否存在,返回true,则表示元素存在数组中,语法:

if(index in array-name)
Terminal window
$ cat array-refer.awk
BEGIN {
x = item[55];
if (55 in item)
print "Array index 55 contains",item[55];
item[101]="HD Camcorder";
if (101 in item)
print "Array index 101 contains",item[101];
if (1010 in item)
print "Array index 1010 contains",item[1010];
}
$ awk -f array-refer.awk
Array index 55 contains
Array index 101 contains HD Camcorder

可以使用for来遍历数组,语法:

for (var in array-name)
actions

如:

Terminal window
$ cat array-for-loop.awk
BEGIN {
item[101]="HD Camcorder";
item[102]="Refrigerator";
item[1001]="Tennis Ball";
item[55]="Laptop";
item["no"]="Not Available";
for(x in item)
print item[x]
}
$ awk -f array-for-loop.awk
Not Available
Laptop
HD Camcorder
Refrigerator
Tennis Ball

可以使用 delete 语句删除数组元素,语法:

delete arrayname[index]

如:

Terminal window
BEGIN {
item[101]="HD Camcorder";
item[102]="Refrigerator";
item[1001]="Tennis Ball";
item[55]="Laptop";
item["no"]="Not Available";
for(x in item){
print item[x]
}
print "------- After deleted --------"
delete item[101]
delete item[102]
delete item["no"]
for(x in item){
print item[x]
}
}
$ awk -f array-for-loop.awk
Not Available
Laptop
HD Camcorder
Refrigerator
Tennis Ball
------- After deleted --------
Laptop
Tennis Ball

awk定义并支持一系列的内置函数,这使得awk提供的功能更为完善、强大。

函数描述
int(n)返回给定参数的整数部分值
log(n)返回给定参数的自然对数,参数 n 必须是正数,否则会抛出错误
sqrt(n)返回指定整数的正平方根,该函数参数也必须是整数,如果传递负数将会报错
exp(n)返回e的n次幂
sin(n)返回 n 的正弦值,n 是弧度值
cos(n)返回 n 的余弦值,n 是弧度值
atan2(m, n)该函数返回 m/n 的反正切值,m 和 n 是弧度值。
函数描述
index用来获取给定字符串在输入字符串中的索引(位置)
length返回字符串的长度
split(input-string,output-array,separator)split 函数把字符串分割成单个数组元素
substr(input-string,location,length)substr 函数从字符串中取指定的部分(子串)
sub(original-string,replacement-string,string-variable)string-variable中用replacement-string替换第一次出现的original-string
gsub(original-string,replacement-string,string-variable)gsub 和 sub 类似,只是gsub 会把所有的 original-string 替换成 replacement-string
match(input-string, search-string)函数从输入字符串中检索给定的字符串(或正则表达式),当检索到字符串时,返回一个正数值
tolower(input-string)把给定的字符串转换为小写
toupper(input-string)把给定的字符串转换为大写
printf “print format”, variable1,variable2,etc.格式化输出结果

printf 可以非常灵活、简单的进行格式化输出结果,printf中可以使用的特殊字符:

特殊字符描述
\n换行
\t制表符
\v垂直制表符
\b退格
\r回车符
\f换页

printf 格式化字符

格式化字符描述
s字符串
c单个字符
d数值
e指数
f浮点数
g根据值决定使用e 或 f 中较短的输出
o八进制
x十六进制
%百分号

printf 支持复杂的格式化控制输出,比如:

  • 指定字符串宽度时,在%和格式化字符之间加上一个-,表示左对齐, 如:
Terminal window
$ awk 'BEGIN{printf("|%10s|%-10s|\n", "Hello", "Hello")}'
| Hello|Hello |
  • 在字符串长度不够时可以进行补0,%05d%.5d效果相同,如:
Terminal window
$ awk 'BEGIN{ printf "|%5d|%05d|%.5d|\n", 10, 10, 10}'
| 10|00010|00010|

更多的情况如下:

FormatVariableResults
%c100”d”
%10c100” d”
%010c100” 000000000d”
%d10”10”
%10d10” 10”
%10.4d10.123456789” 0010”
%10.8d10.123456789” 00000010”
%.8d10.123456789” 00000010”
%010d10.123456789”0000000010”
%e987.1234567890”9.871235e+02”
%10.4e987.1234567890”9.8712e+02”
%10.8e987.1234567890”9.87123457e+02”
%f987.1234567890”987.123457”
%10.4f987.1234567890” 987.1235”
%010.4f987.1234567890”00987.1235”
%10.8f987.1234567890”987.12345679”
%g987.1234567890”987.123”
%10g987.1234567890” 987.123”
%10.4g987.1234567890” 987.1”
%010.4g987.1234567890”00000987.1”
%.8g987.1234567890”987.12346”
%o987.1234567890”1733”
%10o987.1234567890” 1733”
%010o987.1234567890”0000001733”
%.8o987.1234567890”00001733”
%s987.123”987.123”
%10s987.123” 987.123”
%10.4s987.123” 987.”
%010.8s987.123”000987.123”
%x987.1234567890”3db”
%10x987.1234567890” 3db”
%010x987.1234567890”00000003db”
%.8x987.1234567890”000003db”
$ cat format.awk
#!/usr/bin/env awk -f
BEGIN{
printf "%s\n", 987.123
printf "%10s\n", 987.123
printf "%10.4s\n", 987.123
printf "%x\n", 987.1234567890
printf "%10x\n", 987.1234567890
printf "%010x\n", 987.1234567890
printf "%.8x\n", 987.1234567890
}
$ awk -f format.awk
987.123
987.123
987.
3db
3db
00000003db
000003db

需要编写大量代码同时又要多次重复执行其中某些片段时,可以使用自定义函数。语法:

function fn-name(parameters)
{
#.....
}
  • fn-name: 函数名,名称规则和变量名一样,以字母开头,后续可以是字母、数值、下划线,关键字不能作为函数名
  • parameters: 多个参数使用逗号进行分隔,也可以没有参数

和 C 语言类似,awk 也可以进行位操作。

操作符描述
and按位与
or按位或
xor按位异或
compl取反, 如:15 = 01111, 15 compl = 10000
lshift左移,函数把操作数向左位移,可以指定位移多少次,位移后右边补 0
rshift右移,该函数把操作数向右位移,可以指定位移多少次,位移后左边补 0

简单的位操作示例:

Terminal window
$ cat bits.awk
BEGIN{
num1=15
num2=25
print "AND: " and(num1,num2);
print "OR: " or(num1,num2);
print "XOR: " xor(num1,num2);
print "LSHIFT: " lshift(num1,2);
print "RSHIFT: " rshift(num1,2);
}
$ awk -f bits.awk
AND: 9
OR: 31
XOR: 22
LSHIFT: 60
RSHIFT: 3

systime()函数返回系统的 POSIX 时间,即自1970 年 1 月 1 日起至今经过的 秒数。

Terminal window
$ awk 'BEGIN { print systime() }'
1365585325

可以使用 strftime(string)strftime(string, timestamp) 函数对时间进行格式化。strftime 支持的格式标识符如下:

格式标识符描述
%Y年份的完整格式,如 2011
%y两位数字的年份,如 2011 显示为 11
%m两位数字月份,一月显示为 01
%d两位数字日期,4 号显示为 04
%H24 小时格式, 1 p.m 显示为 13
%I12 小时格式, 1 p.m 显示为 01
%M两位数字分钟,9 分显示为 09
%S两位数字秒,5 秒显示为 05
%c显示本地时间的完整格式,如:Fri May 20 21:24:25 2022
%D简单日期格式,和%m/%d/%y 相同
%F简单日期格式,和%Y-%m-%d 相同
%T简单时间格式,和%H:%M:%S 相同
%x基于本地设置的时间格式
%X基于本地设置的时间格式
%r简单时间格式,和%I:%M%:%S %p相同
%R简单时间格式,和%H:%M相同
%B月份完整单词,一月显示为 January
%b月份缩写,一月显示为 Jan
%p显示 AM 或 PM,和%l 搭配使用
%a三位字符星期,周一显示为 Mon
%A完整的日期,周一显示为 Monday
%Z时区,太平洋地区时区显示为 PST
%n换行符
%t制表符
Terminal window
$ cat time.awk
BEGIN{
print strftime("%Y-%m-%d %H:%M:%S")
print strftime("%Y-%m-%d %H:%M:%S", systime())
print strftime("%F %T")
}
$ awk -f time.awk
2022-05-20 16:36:43
2022-05-20 16:36:43
2022-05-20 16:36:43

awk 和 shell 一样是一个解释型语言,也可以用来写可以执行的脚本程序。

和shell脚本类似,awk脚本以下面一行开头:

#!/path/to/awk/utility -f

如在我的系统中,awk安装在/usr/local/bin/awk,所以我的脚本开头第一行是:

#!/usr/local/bin/awk -f
  • #!,指明使用那个解释器来执行脚本中的命令
  • /usr/local/bin/awk,解释器
  • -f,解释器选项,用来指定读取程序文件

需要注意的是,直接指定解释器位置,有可能导致一个问题,换到其他机器,同样脚本却无法执行,因为脚本解释器可能安装在不同的目录中。更好的解决办法是用#!/usr/bin/env awk, env 会在系统PATH目录中查找awk。

编辑保存好脚本bits.awk,如:

#! /usr/bin/env awk -f
BEGIN{
num1=15
num2=25
print "AND: " and(num1,num2);
print "OR: " or(num1,num2);
print "XOR: " xor(num1,num2);
print "LSHIFT: " lshift(num1,2);
print "RSHIFT: " rshift(num1,2);
}

然后,给脚本添加可自行权限:

Terminal window
$ chmod +x bits.awk

此时就可以执行它了:

Terminal window
$ ./bits.awk
AND: 9
OR: 31
XOR: 22
LSHIFT: 60
RSHIFT: 3

可以使用内置变量NR(表示当前记录在所有记录中的行号)进行处理。

myheart文件内容如下:

1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on

输出文件myheart的奇数行:

Terminal window
$ awk "NR % 2 == 1" myheart
1 Every night in my dreams
3 That is how I know you go on

内置变量 NF 可以获取到一行的字段数量,使用$NF就可以获取到一行的最后一个字段值,如:

Terminal window
$ cat myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on
$ awk '{print $1, $NF}' myheart
1 dreams
2 you
3 on

10.3、在Makefile中输出命令帮助信息

Section titled “10.3、在Makefile中输出命令帮助信息”

下面是来自项目kratos-layoutMakefile:

GOPATH:=$(shell go env GOPATH)
VERSION=$(shell git describe --tags --always)
INTERNAL_PROTO_FILES=$(shell find internal -name *.proto)
API_PROTO_FILES=$(shell find api -name *.proto)
.PHONY: init
# init env
init:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
.PHONY: config
# generate internal proto
config:
protoc --proto_path=./internal \
--proto_path=./third_party \
--go_out=paths=source_relative:./internal \
$(INTERNAL_PROTO_FILES)
.PHONY: api
# generate api proto
api:
protoc --proto_path=./api \
--proto_path=./third_party \
--go_out=paths=source_relative:./api \
--go-http_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
--openapi_out==paths=source_relative:. \
$(API_PROTO_FILES)
.PHONY: build
# build
build:
mkdir -p bin/ && go build -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./...
.PHONY: generate
# generate
generate:
go mod tidy
go get github.com/google/wire/cmd/wire@latest
go generate ./...
.PHONY: all
# generate all
all:
make api;
make config;
make generate;
# show help
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

其中help命令输出帮助信息是用awk从Makefile文件中收集注释信息生成的,提取awk命令如下:

Terminal window
awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($1, 0, index($1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
} \
} \
{ lastLine = $0 }' $(MAKEFILE_LIST)

这段awk程序中没有BEGINEND区域命令,只有body部分,且是由两个body区域命令构成。 第一个由awk '/^[a-zA-Z\-\_0-9]+:/ { \开始的这段body区域,由一段模式匹配开始,只有匹配的行才会交给 其后的{}中命令进行执行,具体也就是第一个body区只处理Makefile中的命令行,也就是init:config:help:这些行。

第二个body区域中只有一行命令:lastLine = $0 , 把读取到的行保存到变量lastLine中,此时awk读取到下一行进行处理时,lastLine保存的就是它上一行的内容。

$(MAKEFILE_LIST) 指定是当前的Makefile文件

此时再回头看第一个body区域命令,就很清晰了。该区域命令,在遇到当前行是Markfile中的命令,且上一行是以#开始的注释行时,使用substrindex函数进行截取出Markfile指令(不包含:), 并使用substr截取上一行的注释内容(去掉开头的# ),再使用printf 函数进行格式化输出指令和它的说明信息,%-22s以左对齐、最小22个字符宽度格式化指令进行输出,如:

init init env
config generate internal proto
api generate api proto
build build
generate generate
all generate all
help show help

逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),以纯文本形式存储表格数据(数字和文本)。 典型情况下每行一条记录,用分隔符来分隔字段。CSV格式的标准定义参见RFC 4180

awk 内置变量FS, 指定分隔符,也就是出现两个字段之间的部分。FS 定义了字段不是什么,而不是直接去定义字段是什么。

在对于仅仅使用分隔符(如逗号)分割数据,字段中没有嵌入的分隔符时,如文件imgs.csv内容:

sj_mino1001.jpg,715282,4FB55FE8
sj_mino1002.jpg,471289,93203C5C
sj_mino1003.jpg,451929,C4E80467

对于这种情况,用FS 就可以正确解析出每个字段内容, 如:

Terminal window
$ awk -v FS="," '{print $1, $2, $3}' demo.csv
sj_mino1001.jpg 715282 4FB55FE8
sj_mino1002.jpg 471289 93203C5C
sj_mino1003.jpg 451929 C4E80467

但对于字段中嵌套了分隔符的CSV,如在双引号(double quotes)中嵌入逗号作为字段的情况,如下面的数据:

"SELECT `id`, `name`, `phone` FROM `customer` ORDER BY `id` DESC LIMIT ?, ?",saas,"17",3.146,"3.251","0","0","7.53","7530192","3.53","5"

变量FPAT为这种情况提供了解决方案,变量 FPAT 值是一个正则表达式字符串,描述了每个字段的内容。上面的数据中,字段有用双引号包围并且其中 嵌套了逗号、没有逗号分隔符、用双引号包围没有逗号分隔符。这种情况下,可以用正则表达式 /([^,]+)|("[^"]+")/来匹配,也就是匹配不还有"的一个或多个字符,或者匹配 用双引号包围,但是包围的部分是非引号的一个或多个字符。赋值给FPAT时需要将这个正则表达式转换为字符串,并对其中的双引号进行转义,也就是:

FPAT = "([^,]+)|(\"[^\"]+\")"

用这个来解析上面的csv数据:

Terminal window
$ cat sql.awk
BEGIN{
FPAT = "([^,]+)|(\"[^\"]+\")"
}
{
for(i=1;i<=NF;i++){
printf("$%d = %s\n", i, $i)
}
}
$ awk -f sql.awk sql.csv
$1 = "SELECT `id`, `name`, `phone` FROM `customer` ORDER BY `id` DESC LIMIT ?, ?"
$2 = saas
$3 = "17"
$4 = 3.146
$5 = "3.251"
$6 = "0"
$7 = "0"
$8 = "7.53"
$9 = "7530192"
$10 = "3.53"
$11 = "5"

上面的数据中,字段都是非空的,如果允许字段为空,那么可以把正则表达式中的+改为*来处理这种情况:

FPAT = "([^,]*)|(\"[^\"]*\")"

10.5、查看服务器当前80端口网络连接数

Section titled “10.5、查看服务器当前80端口网络连接数”

netstat 是查看网络相关数据的常用命令,可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。ss 直接从Linux内核中获取TCP和连接状态信息, 效率比 netstat 好:

Terminal window
$ netstat -tan | awk '$4~/:80$/{++state[$NF]} END {for(key in state) print key, "\t", state[key]}'
LISTEN 1
$ ss -tan | awk '$4~/:80$/{++state[$1]} END {for(key in state) print key, "\t", state[key]}'
LISTEN 1
Terminal window
$ history | awk '{++a[$2]}END{for(i in a){print a[i] " " i}}' | sort -rn | head

Terminal window
$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head

学习Sed文本编辑器

Sed 代表 Strem Editor(流编辑器),是操作、过滤和转换文本内容的强大工具。 它最初是为 AT&T 最初的 Unix 操作系统第 7 版创建的,此后可能每一个 Unix 和 Linux 操作系统都包含了它。 sed 以行为处理单位,针对每一行进行处理。 功能上同awk类似,sed功能更简单,针对列处理功能要差很多。

Terminal window
sed [options] '{sed-commands}' {input-file}

可选参数

  • -n, --quiet, --silent: 仅显示sed-commands处理后的结果
  • -e script, --expression=script: 指定sed-commands来处理输输入的文件
  • -f script-file, --file=script-file: {sed-commands}即可是单个命令也可以是多个命令,将多个命令合并到一个文件(被称为:sed脚本)后,可以使用-f选项调用它
  • -i: 直接修改原始文件内容(危险动作,需要慎重使用)
  • --version: 显示版本号

sed脚本执行的步骤很容易记住:读取(Read),执行(Execute),打印(Print),重复(Repeat)。我们可以利用这几个步骤的首字母REPR来记忆sed执行的步骤。

我们来看一下这几个步骤。 sed将会:

  • 读取一行内容到模式空间(sed内部的一个临时缓存,用于存放读取到的内容)
  • 执行:对模式空间里的内容执行sed命令。 如果使用了 {} 或 -e 指定多个命令,sed将依次执行每个命令
  • 打印(输出)模式空间的内容。然后清空模式空间
  • 重复以上流程,直到文件结束

sed

  • a: 追加命令,可以在指定位置后面插入新行
  • i: 插入命令,在指定位置之前插入新行
  • c: 修改命令,取代指定位置旧行
  • d: 删除命令,删除指定行
  • p: 打印命令,打印当前模式空间的内容,通常和可选参数-n一同使用
  • s: 替换命令,进行字符串替换
  • y: 转换字符,根据对应位置转换字符,如进行大小写转换
  • =: 打印行号,会在每一行的后显示改行的行号
  • q: 终止正在执行的命令并退出sed。sed正常执行流程是读取数据、执行命令、打印结果、重复循环。当sed遇到q命令时,便停止执行后续循环立即退出
  • r: 从指定的文件读取内容,并在指定的位置将其打印出来

有一段歌词保存在文件myheart,内容如下:

1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on

完整歌词可以在My Heaert Will Go On 获取完成歌词

使用 a 命令可以在指定位置后插入新行,语法格式:

Terminal window
$ sed '[address] a the-line-to-append' input-file
Terminal window
$ sed '1a hello world' myheart
1 Every night in my dreams
hello world
2 I see you, I feel you
That is how I know you go on

在mac上使用系统自带sed版本执行时,会返回错误信息sed: 1: "1a hello world": command a expects \ followed by text。 可以安装GNU版sed来解决:

brew install gnu-sed

alias sed=gsed

Terminal window
$ sed '$a Far across the distance' myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on
Far across the distance
Terminal window
$ sed '/you/a Far across the distance' myheart
1 Every night in my dreams
2 I see you, I feel you
Far across the distance
3 That is how I know you go on
Far across the distance

使用c命令可以,用给定的行替换指定位置的旧行。

Terminal window
$ gsed '1c Far across the distance' myheart
Far across the distance
2 I see you, I feel you
3 That is how I know you go on

将第一行替换为Far across the distance

Terminal window
$sed '/you/c You have come to show you go on' myheart
1 Every night in my dreams
You have come to show you go on
You have come to show you go on
4.2.3、用多行新数据替代匹配you的行
Section titled “4.2.3、用多行新数据替代匹配you的行”
Terminal window
$sed '/you/c Far across the distance\
>And spaces between us' myheart
1 Every night in my dreams
Far across the distance
And spaces between us
Far across the distance
And spaces between us

/you/匹配到了两行,所以这两行都进行了替换

Terminal window
$ sed G myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on

G 命令把当前保持空间的内容作为新行追加到模式空间中, 模式空间的内容不会被覆 盖,该命令在模式空间后面加上换行符\n,然后把保持空间内容追加进去。

Terminal window
$ sed '1d' myheart
2 I see you, I feel you
3 That is how I know you go on
Terminal window
$ sed '1!d' myheart
1 Every night in my dreams
Terminal window
$ sed '$d' myheart
1 Every night in my dreams
2 I see you, I feel you
Terminal window
$ sed '/Every/d' myheart
2 I see you, I feel you
3 That is how I know you go on
Terminal window
$ sed '/^2/d' myheart
1 Every night in my dreams
3 That is how I know you go on
4.5.5 删除从第一次匹配行到最后一行
Section titled “4.5.5 删除从第一次匹配行到最后一行”
Terminal window
$ sed '/^2/,$ d' myheart
1 Every night in my dreams
4.5.6 删除从第一次匹配行和它后面的一行
Section titled “4.5.6 删除从第一次匹配行和它后面的一行”
Terminal window
$ sed '/^2/,+1 d' myheart
1 Every night in my dreams
4.5.7 删除从第一次匹配行到第二行
Section titled “4.5.7 删除从第一次匹配行到第二行”
Terminal window
$ sed '/Every/,2 d' myheart
3 That is how I know you go on

sed在执行完成命令后会默认打印模式空间的内容,而命令p也会输出当前模式空间的内容。所以, 通常使用命令p时,还需要使用-n选项来屏蔽sed的默认输出,否则使用p命令后,每行记录会输出两次。

Terminal window
$sed -n '1p' myheart
1 Every night in my dreams
Terminal window
$ sed -n '$p' myheart
3 That is how I know you go on
Terminal window
$ sed -n '2,3p' myheart
2 I see you, I feel you
3 That is how I know you go on
Terminal window
$ gsed -n '/Every/p' myheart
1 Every night in my dreams

显示包含Every的行

sed除了整行的新增、删除、替换外,还可以行为单位进行部分数据的查找替换。

替换命令语法

sed '[address-range|pattern-range] s/original-string/replacement-string/[substitute-flags]' input file
  • address-rangepattern-range (即地址范围或模式范围)是可选的,如果没有指定,那么 sed 将在所有行上进行替换
  • s 即执行替换命令 substitute
  • original-string 是被 sed 搜索然后被替换的字符串,它可以是一个正则表达式
  • replacement-string 是替换后的字符串
  • substitute-flags 是可选的
Terminal window
$ sed 's/you/YOU/' myheart
1 Every night in my dreams
2 I see YOU, I feel you
3 That is how I know YOU go on

在上面的例子(4.7.1)可以发现第2行中有两个 you 只有第一个被替换成 YOU,因为默认情况下,sed会替换每行中第一个original-string,可以使用 全局标志 g 将每行中所有匹配项都进行替换。

Terminal window
$ sed 's/you/YOU/g' myheart
1 Every night in my dreams
2 I see YOU, I feel YOU
3 That is how I know YOU go on
4.7.3、在指定的目标范围进行替换
Section titled “4.7.3、在指定的目标范围进行替换”

4.7.1中,第2、3行中的you 被成功替换成 YOU,如果只想要期中的部分行进行替换,那么可以通过直接指定地址范围 或者模式范围,而不是默认的所有行上,进行替换操作。

指定地址范围

只在第三行进行,将 you 替换 YOU

Terminal window
$ sed '3 s/you/YOU/g' myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know YOU go on

指定模式范围

在包含 know 的行,将 you 替换 YOU

Terminal window
$ sed '/know/ s/you/YOU/g' myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know YOU go on
4.7.4、指定匹配的original-string次序进行替换
Section titled “4.7.4、指定匹配的original-string次序进行替换”

4.7.1中,第二行中有两个 you 可以匹配到,默认是第一个匹配项被替换,可以通过指定数字标志来指定次序匹配项被替换。 如,要第二个匹配项 you 被替换,可以使用:

Terminal window
$ sed 's/you/YOU/2' myheart
1 Every night in my dreams
2 I see you, I feel YOU
3 That is how I know you go on

使用s/you/YOU/2 后,每一行都是第二个匹配项才会被替换,所以第三行的 you 没有被替换。

4.7.4中,将每行中的第二个 you 替换成 YOU, 可以配置 p命令和 -n 选项,将发生替换的行显示出来,而其他行不输出。

Terminal window
$ sed -n 's/you/YOU/2p' myheart
2 I see you, I feel YOU

可以使用i标志,在模式匹配中忽略大小写,进行匹配替换。如可以改写4.7.5,使用yOu进行匹配替换:

Terminal window
sed -n 's/yOu/YOU/2pi' myheart
2 I see you, I feel YOU

如果没有i标志时,就不会发生更改替换:

Terminal window
$ sed 's/yOu/YOU/2' myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on
$ sed -n 's/yOu/YOU/2p' myheart

进行大小写替换可以使用命令 y, 该命令根据对应位置进行字符转换:

Terminal window
$ sed 'y/you/YOU/' myheart
1 EverY night in mY dreams
2 I see YOU, I feel YOU
3 That is hOw I knOw YOU gO On

将所有的y->Yo->O,u->U,也可以和4.7.3一样进行指定范围或模式范围进行限定,这样可以避免在所有的行进行操作。

也可以在命令 s 中,使用特殊的功能来实现。如:\L 将匹配文本替换为小写,\l 则只是转换下一个字符为小写,\U 将匹配的文本替换为大写, \u 将只是将下一个字符转为大写,& 指的是匹配到的模式:

Terminal window
$ gsed 's/you/\U&/' myheart
1 Every night in my dreams
2 I see YOU, I feel you
3 That is how I know YOU go on

同样的可以使用 g 标志,将所有匹配项都进行替换:

Terminal window
$ gsed 's/you/\U&/g' myheart
1 Every night in my dreams
2 I see YOU, I feel YOU
3 That is how I know YOU go on

可以使用 G 命令,也可以是用 s 命令完成,如:

Terminal window
$ sed G myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on
$ sed 's/.*/&\n/' myheart
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on

G 命令把当前保持空间的内容作为新行追加到模式空间,模式空间中的内容不会被覆盖, 该命令在模式空间后面加上换行符 \n,然后保持空间内容添加进去,sed G myheart 中保持空间没有内容, 所以达到仅仅只是换行的目的。

sed 's/.*/&\n/' myheart 使用 s 命令利用正则表达式匹配一整行,并是用 & 表示当期匹配的内容,紧跟 换行符 \n,达到每一行后添加一个换行的目的。

用可选项 -i 可以直接修改原始文件内容,如:

sed -i 's/one/two' demo.txt

上面的命令,将文件中demo.txt,每行中的第一个 one 修改成 two。这条命令在Mac 系统中会提示错误(Linux 能正常运行):

sed: 1: "demo.txt": command c expects \ followed by text

对于这个问题,StackOverflow有个答案:sed command with -i option failing on Mac, but works on Linux

下面有一个不错的方案:

This works with both GNU and BSD versions of sed:

sed -i'' -e 's/old_link/new_link/g' *

or with backup:

sed -i'.bak' -e 's/old_link/new_link/g' *

如果没有其他备份机制,使用-i'.bak来保存备份,来避免错误修改且丢失原始文件造成灾难。

回到主题:批量修改文件,可以联合 xargsfindrg 命令来完成。

如当前目录下有大量的 Markdown 文件,需要删除文件中的一行 contentCopyright: true,那么可以使用下面的命令:

Terminal window
$ rg --type md --files-with-matches 'contentCopyright: true' | xargs sed -i'' '/contentCopyright: true/d'

首先使用 rg 找到包含 contentCopyright: trueMarkdown 文件, 然后交给 sed 修改。

  • --type md 指定 rg 在Markdown文件中进行匹配
  • --files-with-matches 选项指定 rg 只返回匹配项的文件, 不返回匹配的内容详情
  • -i'' sed 直接修改原始文件内容

初次登录服务器的那些事

经常需要在Linux服务器上进行一些操作,这里记录一些有用的知识,帮助我更好的观察、理解以及使用服务器。

初次登录服务器马上做的几件事

Section titled “初次登录服务器马上做的几件事”

初次登录一台服务器,第一件事就是检查操作系统、内核和硬件架构,以及运行多次时间等等。

Terminal window
cat /etc/redhat-release
uname -a
hostnamectl
uptime

登录进系统后,当是否有其用户访问系统。

Terminal window
who
who -Hu
grep sh$ /etc/passwd

grep sh$ /etc/passwd 命令是查找 /etc/passwd 文件中具有 shell 访问权限的用户

通过下面的命令可以确认,当前机器是物理机还是虚拟机,以及相关信息

Terminal window
dmidecode -s system-manufacturer
dmidecode -s system-product-name
lshw -c system | grep product | head -1
cat /sys/class/dmi/id/product_name
cat /sys/class/dmi/id/sys_vendor

查看当前机器硬件能力,如CPU类型、有多少个核心、内存情况等

Terminal window
lscpu or cat /proc/cpuinfo
lsmem or cat /proc/meminfo
ifconfig -a
ethtool <devname>
lshw
lspci
dmidecode

通过检查服务器当前正在运行的进程,可以进一步了解服务器的运行情况

Terminal window
pstree -pa 1
ps -ef
ps auxf
systemctl

检查服务器开放的端口,当前服务器的网络连接,防火墙状况

Terminal window
netstat -tulpn
netstat -anp
lsof -i
ss
iptables -L -n
cat /etc/resolv.conf

生成一个 20 个字符的随机数密码

Section titled “生成一个 20 个字符的随机数密码”
Terminal window
openssl rand -base64 20

完整命令太长可以,设置别名

Terminal window
alias getpass="openssl rand -base64 20"

查看句柄数

Terminal window
$ ulimit -a
Maximum size of core files created (kB, -c) 0
Maximum size of a process’s data segment (kB, -d) unlimited
Maximum size of files created by the shell (kB, -f) unlimited
Maximum size that may be locked into memory (kB, -l) unlimited
Maximum resident set size (kB, -m) unlimited
Maximum number of open file descriptors (-n) 256
Maximum stack size (kB, -s) 8192
Maximum amount of cpu time in seconds (seconds, -t) unlimited
Maximum number of processes available to a single user (-u) 2784
Maximum amount of virtual memory available to the shell (kB, -v) unlimited
Terminal window
lsof|awk '{print $2}'|wc -l

根据打开文件句柄的数量降序排列

Section titled “根据打开文件句柄的数量降序排列”
Terminal window
lsof|awk '{print $2}'|sort|uniq -c|sort -nr|more

Linux统计文件数量

统计当前文件夹下文件的个数,包括子文件夹里的

Section titled “统计当前文件夹下文件的个数,包括子文件夹里的”
Terminal window
ls -lR | grep '^-' | wc -l

统计当前文件夹下文件夹的个数,包括子文件夹下的

Section titled “统计当前文件夹下文件夹的个数,包括子文件夹下的”
Terminal window
ls -lR | grep '^d' | wc -l

统计当前文件夹下的文件夹个数

Section titled “统计当前文件夹下的文件夹个数”
Terminal window
ls -l | grep '^d' | wc -l
  • 方式一
Terminal window
ls -l | grep '^-' | wc -l
  • 方式二
Terminal window
find DIR_NAME -type f | wc -l

注:DIR_NAME指定的目录