Skip to content

博客

Neovim拼写检查

这里记录如何在Neovim启用内置的拼写检查功能。

首先,添加拼写检查语言:

set spelllang=en,cjk

上面的配置,将拼写语言设置为encjkcjk选项用于防止将CJK字符被标记为拼写错误。

CJK: 中日韩统一表意文字(英语:CJK Unified Ideographs),也称统一汉字、统汉码(英语:Unihan),目的是要把分别来自中文、日文、韩文、越南文、壮文、琉球文中,起源相同、本义相同、形状一样或稍异的表意文字,在ISO 10646及Unicode标准赋予相同编码。 所谓“起源相同、本义相同”、主要是汉字,包括繁体字、简化字、日本汉字(漢字/かんじ)、韩国汉字(漢字/한자)、琉球汉字(漢字/ハンジ)、越南的喃字(𡨸喃/Chữ Nôm)与儒字(𡨸儒/Chữ Nho)、方块壮字(𭨡倱/sawgun)。

cjk配置项,也记录在 Nvim doc 文档中:

Chinese, Japanese and other East Asian characters are normally marked as errors, because spell checking of these characters is not supported. If ‘spelllang’ includes “cjk”, these characters are not marked as errors. This is useful when editing text with spell checking while some Asian words are present.

使Nvim启用拼写检查功能,需要运行命令:set spell。下面可以添加一个映射来切换拼写检查:

nnoremap <silent> <F11> :set spell!<cr>

或者

vim.api.nvim_set_keymap('n', '<F11>', [[<Cmd>set spell!<CR>]], { noremap = true, silent = true })

此时,可以按来切换拼写检查。

此外,也可以在init.lua中通过创建autocmd,根据文件类型(filetype),来自动切换拼写检查功能,如:

local spell_group = vim.api.nvim_create_augroup("spell_group", { clear = true })
vim.api.nvim_create_autocmd("FileType", {
pattern = { "lua", "python", "go" },
command = "setlocal spell spelllang=en_us,cjk",
group = spell_group,
})
vim.api.nvim_create_autocmd("FileType", {
pattern = { "markdown" },
command = "setlocal nospell",
group = spell_group,
})
vim.api.nvim_create_autocmd("TermOpen", {
pattern = "*", -- disable spellchecking in the embeded terminal
command = "setlocal nospell",
group = spell_group,
})

上面的配置,在luaptyhongo文件中启用拼写检查,并将拼写语言设置为en_us,cjk,在Markdown文件禁用拼写检查, 同时在内嵌终端(embeded terminal)中禁用拼写检查。

在插入模式时,当输入了NeoVim认为是拼写错误的单词时,会在其下方显示下划线。要更正错误,可以依次按下<C-x>s键,此时 会显示一个建议正确单词的菜单列表,可以选择其中一项来更正拼写。

normal模式,可以使用下面的快捷键来快速处理拼写错误问题:

  • [s: 跳转到上一处拼写错误
  • ]s: 跳转到下一处拼写错误
  • z=: 对光标下或之后的单词,显示建议正确的拼写单词
  • zg: 将光标下的单词作为正确拼写单词加入到’spellfile’
  • zw: 和zg类似,只是zw是将这个单词标记为拼写错误的单词

spelloptions配置选项默认值是"",此时,注释中的驼峰拼写的单词,会被判定为拼写错误,如: camel case

可以通过配spelloptions启动驼峰单词拼写:

vim.opt.spelloptions = "camel"

判断规则:

When a word is CamelCased, assume “Cased” is a

separate word: every upper-case character in a word
that comes after a lower case character indicates the
start of a new word.

Go中的数据竞争和竞争条件

并发是一个有趣的词,同时我们还可能听过其他词,如:“并行”,“串行”,“线程”,“多线程”等。在计算机科学中,并发是一个宽泛的话题,涉及到许多的主题。很有必要弄清楚他们的定义和区别。

并发 (Concurrent) : 多个事情在同时发生的过程,这个“同时”具体可以是一个时间段,也就是在同一时间段内多个事情正在进行。

并行 (Parallel) : 在同一时刻,多个事情同时在进行。

具体到操作系统中,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。

Concurrent

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

Parallel

引用Ealang之父Joe Aramstrong 给5岁小孩解释并发和并行间区别

difference

并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机。

并发的多个任务间是相互抢占资源的,而并行的多个任务是不相互抢占资源的。

  • 指串行通信(Serial communication)是指在计算机总线或其他数据通道上,每次传输一个位元数据,并连续进行以上单次过程的通信方式。与之对应的是并行通信,它在串行端口上通过一次同时传输若干位元数据的方式进行通信
  • 和并行相对应,做某个事情时,一个步骤一个步骤的进行处理

1.4、数据竞争(data race)和竞争条件(race condition)

Section titled “1.4、数据竞争(data race)和竞争条件(race condition)”

当计算机程序中有多个同时执行的代码路径时,就会出现竞争条件(race condition),如果多个代码执行路径花费的时间和预期不同,它们可能以与预期不同的顺序完成,这可能由于意外的行为导致软件bug。

数据竞争是一种竞争条件。数据竞争是各种形式内存模型的重要组成部分。C11C++11 标准中定义的内存模型指定包含数据竞争的 C 或 C++ 程序具有未定义的行为(undefined behavior )。

Go在1.19版本,修改了内存模型的定义,使它的内存模型同其他语言,如:C、C++、Java、JavaScript、Rust和Swift等保持一致。

竞争条件可能难以重现和调试,因为最终结果是不确定的,并且取决于干扰线程之间的相对时间。在调试模式下运行、添加额外的日志记录或者附加调试器时, 这种性质的问题可能因此而消失,因此最好通过仔细的软件设计来避免竞争条件。

数据竞争(data race) 的精准定义特定于所使用的并发模型,但通常情况指的是一种情况,即一个线程中的内存操作可能尝试访问内存位置,同时另一个线程中的内存操作正在写入改内存位置,这种情况下这是危险的。 这意味着数据竞争和竞争条件不同,因为即使在没有数据竞争的程序中,由于时序,也可能具有不确定性,如,在所有内存访问都使用原子操作的程序中。

许多平台中下面的操作是危险的,如果两个线程同时写入一个内存位置,这个内存地址可能保存一个值,该值是每个线程尝试写入值的bit位的任意且无意义的组合; 如果生成的值都不是两个线程试图写入的值,则可能导致内存损坏。同样的,一个线程从某个位置读取,而另一个线程正在写入该位置,则有可能返回一个值,这个值可能是写入值的bits位和已有值的bits位的任意且无意义的组合值。

Go语言中的每一个并发执行单元叫做协程(goroutine),一个程序启动时,其主函数即在一个单独的goroutine中运行,也叫它main goroutine。goroutine是轻量级的,一个goroutine会以一个很小的栈开始生命周期,一般只需要2KB,和操作OS线程一样,会保存其活跃或挂起的函数调用的本地变量。 不一样的地方是,一个goroutine的栈大小不固定,根据需要动态伸缩。

Go支持两种并发模型,一种是CSP(Communicating Sequential Process,通信顺序进程)模型,另一种是内存同步访问。

Go的内存模型中也有上述的数据竞争和竞争条件。

当两个或多个操作必须按照正确的顺序执行,而程序并未保证这个顺序,就会发生竞争条件。

当有两个或者多个 goroutine 同时访问一个变量,其中至少有一个访问是写入时,就会发生数据竞争(data race)。

如下面是一个可能导致崩溃和内存损坏的数据竞争示例:

func main() {
c := make(chan bool)
m := make(map[string]string)
go func() {
m["1"] = "a" // First conflicting access.
c <- true
}()
m["2"] = "b" // Second conflicting access.
<-c
for k, v := range m {
fmt.Println(k, v)
}
}

为了帮助诊断这类bug,Go内置了一个数据竞争检测器,可以通过添加-race 标志到go command中来使用它:

Terminal window
$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg

当竞争检测器在程序中发现数据竞争时,它会打印一份报告,其中包含冲突访问的堆栈跟踪,以及创建关联goroutine的堆栈,如:

Terminal window
$ go run --race datarace/main.go
==================
WARNING: DATA RACE
Write at 0x00c000112180 by goroutine 7:
runtime.mapassign_faststr()
src/runtime/map_faststr.go:203 +0x0
main.main.func1()
atmoic-pointer/datarace/main.go:9 +0x50
Previous write at 0x00c000112180 by main goroutine:
runtime.mapassign_faststr()
src/runtime/map_faststr.go:203 +0x0
main.main()
atmoic-pointer/datarace/main.go:12 +0x132
Goroutine 7 (running) created at:
main.main()
atmoic-pointer/datarace/main.go:8 +0x115
==================
1 a
2 b
Found 1 data race(s)
exit status 66
  • 如果多个goroutine并发访问修改数据时,必须保证各操作序列化执行(串行执行)
  • 为了序列化执行,可以使用channel或者其他同步原语(如sync 和 sync/atomic包里提供的)来保护被共享的数据

sync等包提供了Mutex(互斥锁)、RWMutext(读写锁)、Once、WaitGroup、Atomic Values 等传统锁机制,但Go语言更推荐使用channel进行同步通信,“不要通过共享内存来通信,相反,通过通信来共享内存”

Do not communicate by sharing memory; instead, share memory by communicating. - effective go

追求简洁,尽量使用channel,并且认为goroutine是低成本的。

Golang性能分析-pprof

遵从代码最佳实践或者代码规范指导,可以避免一些参见的坑,但在运行代码时遇到各种问题后,还是需要深入代码了解代码性能和瓶颈, 以便能够发现问题、解决问题和做出相应的优化。

性能分析器(Profiler)是一种动态的性能分析工具,可以提供程序在运行过程中的各种维度的关键性能分析,可以用来解决各种性能问题,定位内存泄露, 线程竞争死锁等问题。

Go运行时以pprof可视化工具预期的格式提供性能分析数据,分析数据可以在测试期间通过go test,或者从net/http/pprof包提供的endpoint收集。我们需要收集性能分析数据,并通过pprof工具对程序进行分析,并支持可视化数据。

Go 通过runtime/pprof包提对profile提供了内置的支持:

  • CPU: CPU分析采集一定周期内程序对CPU的使用情况,确定程序在主动消耗CPU周期是如何消耗时间的
  • Heap: Heap分析,记录内存分配,用于监视当前和历史内存使用情况,以及检查内存泄露
  • Goroutine: Goroutine分析报告所有当前正在运行goroutine的堆栈跟踪信息
  • threadcreate: 报告程序中导致创建新OS线程的堆栈跟踪信息
  • Block: 阻塞分析,显示goroutine阻塞等待同步(包括定时器通道)的位置,默认没有启用,可以使用 runtime.SetBlockProfileRate 启用它
  • Mutex: 互斥锁分析,报告互斥锁的竞争情况,当认为互斥锁争用导致CPU没有得到充分使用时,可以使用这种分析,默认没有启动,可以使用runtime.SetMutexProfileFraction 启用它

目前有三种方式进行数据采集:

  • go test: 通过运行测试用例继续采集
  • runtime/pprof: 直接在代码中指定采集程序运行数据
  • net/http/pprof: 采集http server运行时数据进行分析

测试标准库在go test中内置了对采集性能分析数据的内置支持,如下面的命令,在当前目录中运行测试,并将CPU和内存采样数据分别写入cpu.prof和 mem.prof:

Terminal window
go test -cpuprofile cpu.prof -memprofile mem.prof .

在长时间运行的http服务中,只需要导入net/http/pprof包,服务便可以进行采集数据,并多出一个终结点(endpoint)/debug/pprof,可用于观察采集到的 性能分析数据。包导入语句如:

_ "net/http/pprof"

net/http/pprof 包初始化时在DefaultServeMux中为给定的URL注册了处理程序

func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}

这些handler将采样数据写入到http.ResponseWriter,导入net/http/pprof后需要做的,只是启动一个HTTP server就可以了,如:

http.ListenAndServe(":8080", nil)

完整的最小采集demo:

package main
import (
"log"
"net/http"
"net/http/httputil"
_ "net/http/pprof"
)
func main() {
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
c, _ := httputil.DumpRequest(r, true)
log.Println(string(c))
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(201)
rw.Write([]byte("{}"))
})
http.ListenAndServe(":8080", nil)
}

运行程序后,就可以在浏览器访问localhost:8080/debug/pprof实时采样数据总览页面:

pprof

  • allocs: 所有已分配内存的采样
  • block: 导致阻塞同步的堆栈跟踪
  • cmdline: 显示程序启动的路径命令和参数
  • goroutine: 所有当前goroutine的堆栈跟踪
  • heap: 当前活动对象的内存采样
  • mutext: 互斥锁持有者的堆栈跟踪
  • profile: 进行CPU采样,采样完成得到一个供分析用的profile文件,默认采集间隔是30秒
  • threadcreate: 创建新OS线程的堆栈跟踪
  • trace: 程序的执行跟踪信息

直接通过上面的方式查看采样数据枯燥且缺乏直观性,后面介绍可以借助go tool pprof命令,可以更直观方便的分析采样数据。

通过使用runtime/pprof包,可以直接在代码中进行采样,如可以使用pprof.StartCPUProfile(io.Writer)开始CPU采样, 使用pprof.StopCPUProfile() 停止CPU采样,如:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close() // error handling omitted for example
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
// ... rest of the program ...
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close() // error handling omitted for example
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
}

运行程序:

Terminal window
go run -cpuprofile cpu.prof -memprofile mem.prof .

将CPU和内存采样数据分别写入文件cpu.prof 和 mem.prof。

通过go tool pprof命令在交互命令行终端中使用采用数据,对于采样数据文件profile可以使用:

Terminal window
go tool pprof profile

profile

对于通过「采样方式2.2」HTTP 终结点方式,可以使用go tool pprof url的方式,如:

Terminal window
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

profile-2

采样url,在可以在采样总览页面中获取到:

ulr

使用pprof可视化界面时,在go tool pprof命令通过指定端口参数-http,运行一个pprof分析用的站点,如:

  • 查看当前所有goroutine的堆栈跟踪
go tool pprof -http :9090 'http://localhost:8080/debug/pprof/goroutine'

pprof ui

通过pprof提供的可视化界面,可以更清晰直观的查看调用链等信息。

  • Top top

  • Graph graph

  • Flame Graph flame

  • Peek peek

  • Source source

  • Disassemble disassemble

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 直接修改原始文件内容