markdown

Package github.com/gomarkdown/markdown is a Go library for parsing Markdown text and rendering as HTML.

安装:

1
go get github.com/gomarkdown/markdown

灵活用法:实现接口(文档中皆有例子)

1
2
3
4
5
type Renderer interface {
RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus
RenderHeader(w io.Writer, ast ast.Node)
RenderFooter(w io.Writer, ast ast.Node)
}

可以自定义渲染效果。


灵活用法:自定义语法解析(使用包提供的hook函数)

可以自定义hook函数挂上去

1
2
ps := parser.NewWithExtensions(formatted.Extensions)
ps.Opts.ParserHook = <自定义hook函数>

case:自定义列表解析

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package formatted

import (
"bytes"
"strconv"

"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
)

const (
IsOliStartsAtOne = true
MaxListLevel = 3
)

func NewParserHookV2(p *parser.Parser) func(data []byte) (ast.Node, []byte, int) {
return func(data []byte) (ast.Node, []byte, int) {
return parserHookV2(p, data)
}
}

func parserHookV2(p *parser.Parser, data []byte) (ast.Node, []byte, int) {
end := bytes.IndexByte(data, '\n')
if end == 0 {
return &ast.Hardbreak{}, nil, 1
}

if list, i := listV2(p, data, 0, 0); i != 0 {
return list, nil, i
}

if cb, i := codeBlock(data); i != 0 {
return cb, nil, i
}

para := &ast.Paragraph{}
if end < 0 {
end = len(data)
} else if end < len(data)-1 {
var hasMore bool
for _, c := range data[end:] {
if c != '\n' {
hasMore = true
}
}
if hasMore {
end++ // include first '\n'
}
}
para.Content = data[:end]
return para, nil, end
}

func isOverflow(data []byte, indexs ...int) bool {
for _, index := range indexs {
if index < 0 || index >= len(data) {
return true
}
}
return false
}

func uliPrefixV2(data []byte, listLevel int) int {
hasValidIndentation, indentionOffset := hasValidIndentation(data, listLevel)
if !hasValidIndentation {
return 0
}

if isOverflow(data, indentionOffset+2) {
return 0
}
i := indentionOffset
// need one of {'*', '+', '-'} followed by a space or a tab
if isOverflow(data, i, i+1) ||
(data[i] != '*' && data[i] != '+' && data[i] != '-') ||
(data[i+1] != ' ' && data[i+1] != '\t') {
return 0
}
return i + 2
}

func oliPrefixV2(data []byte, isListItemNode bool, idx int, listLevel int) int {
hasValidIndentation, indentionOffset := hasValidIndentation(data, listLevel)
if !hasValidIndentation {
return 0
}

if isOverflow(data, indentionOffset+3) {
return 0
}

i := indentionOffset
for i < len(data) && data[i] >= '0' && data[i] <= '9' {
i++
}

if IsOliStartsAtOne || isListItemNode {
if isOverflow(data, indentionOffset) || i > len(data) ||
!bytes.Equal(data[indentionOffset:i], []byte(strconv.Itoa(idx))) {
return 0
}
}

// we need >= 1 digits followed by a dot and a space or a tab
if isOverflow(data, i, i+1) ||
data[i] != '.' ||
(data[i+1] != ' ' && data[i+1] != '\t') {
return 0
}
return i + 2
}

func hasValidIndentation(data []byte, listLevel int) (hasValidIndentation bool, indentionOffset int) {
if listLevel <= 0 {
return true, 0
}

requiredSpaces := listLevel * 4
requiredTabs := listLevel

if isOverflow(data, requiredSpaces, requiredTabs) {
return false, 0
}

allSpaces := true
for i := 0; i < requiredSpaces; i++ {
if isOverflow(data, i) || data[i] != ' ' {
allSpaces = false
break
}
}
if allSpaces {
return true, requiredSpaces
}

allTabs := true
for i := 0; i < requiredTabs; i++ {
if isOverflow(data, i) || data[i] != '\t' {
allTabs = false
break
}
}
if allTabs {
return true, requiredTabs
}

return false, 0
}

func hasChildList(data []byte, listLevel int) bool {
hasValidIndentation, _ := hasValidIndentation(data, listLevel+1)
return hasValidIndentation
}

func listV2(p *parser.Parser, data []byte, dataIndex, listLevel int) (*ast.List, int) {
if listLevel >= MaxListLevel {
return nil, 0
}

l := &ast.List{
Tight: true,
BulletChar: '*',
Delimiter: '.',
}
if prefixOffset := uliPrefixV2(data, listLevel); !isOverflow(data, prefixOffset-2) && prefixOffset != 0 {
l.BulletChar = data[prefixOffset-2]
} else if prefixOffset = oliPrefixV2(data, false, 1, listLevel); !isOverflow(data, prefixOffset-2) && prefixOffset != 0 {
l.ListFlags = ast.ListTypeOrdered
l.Delimiter = data[prefixOffset-2]

numStr := string(data[:prefixOffset-2])
start, err := strconv.Atoi(numStr)
if err == nil {
l.Start = start
} else {
l.Start = 1
}
} else {
return nil, 0
}
l.ListFlags |= ast.ListItemBeginningOfList

idx := l.Start
for !isOverflow(data, dataIndex) {
offset := listItemV2(p, data[dataIndex:], l, idx, listLevel)
dataIndex += offset
idx++
if offset == 0 || l.ListFlags&ast.ListItemEndOfList != 0 {
break
}
l.ListFlags &= ^ast.ListItemBeginningOfList
}

return l, dataIndex
}

func listItemV2(p *parser.Parser, data []byte, l *ast.List, idx int, listLevel int) int {
offset := 0
prefixOffset := uliPrefixV2(data, listLevel)
if prefixOffset != 0 {
if l.ListFlags&ast.ListTypeOrdered != 0 || isOverflow(data, prefixOffset-2) || l.BulletChar != data[prefixOffset-2] {
return 0
}
} else if prefixOffset = oliPrefixV2(data, true, idx, listLevel); prefixOffset != 0 {
if l.ListFlags&ast.ListTypeOrdered == 0 || isOverflow(data, prefixOffset-2) || l.Delimiter != data[prefixOffset-2] {
return 0
}
} else {
return 0
}

beg := prefixOffset
for offset = prefixOffset; !isOverflow(data, offset-1, offset) && data[offset-1] != '\n'; offset++ {
}

item := &ast.ListItem{
ListFlags: l.ListFlags,
Tight: false,
BulletChar: l.BulletChar,
Delimiter: l.Delimiter,
}
ast.AppendChild(l, item)

para := &ast.Paragraph{}
para.Content = data[beg:offset]
ast.AppendChild(item, para)

if !isOverflow(data, offset) && hasChildList(data[offset:], listLevel) {
if childList, childOffset := listV2(p, data[offset:], 0, listLevel+1); childList != nil && childOffset != 0 {
ast.AppendChild(item, childList)
offset += childOffset
}
}

if l.ListFlags&ast.ListTypeOrdered == 0 {
if prefixOffset = uliPrefixV2(data[offset:], listLevel); isOverflow(data, offset+prefixOffset-2) || prefixOffset == 0 || l.BulletChar != data[offset+prefixOffset-2] {
l.ListFlags |= ast.ListItemEndOfList
}
} else {
if prefixOffset = oliPrefixV2(data[offset:], true, idx+1, listLevel); isOverflow(data, offset+prefixOffset-2) || prefixOffset == 0 || l.Delimiter != data[offset+prefixOffset-2] {
l.ListFlags |= ast.ListItemEndOfList
}
}

return offset
}

func codeBlock(data []byte) (ast.Node, int) {
if data[0] != '~' && data[0] != '`' {
return nil, 0
}
c := data[0]
marker := string([]byte{c, c, c})
if !bytes.HasPrefix(data, []byte(marker+"\n")) {
return nil, 0
}

end := 3
for end < len(data)-3 {
i := bytes.Index(data[end:], []byte("\n"+marker))
if i < 0 {
break
}
end += i
if endDoc := end+4 >= len(data); endDoc || data[end+4] == '\n' {
cb := &ast.CodeBlock{IsFenced: true}
if end > 4 {
cb.Content = data[4:end]
}
if !endDoc {
end++ // count '\n' after marker
}
return cb, end + 4 // count marker and '\n' before marker
}
end += 4
}
return nil, 0
}




go-shellwords

1. 功能简介

  • 作用:将命令字符串按 shell 解析规则拆分为参数数组,支持引号、转义字符、环境变量等。
  • 场景:解析用户输入的命令行字符串(如 /cmd --name="John Doe" -v),替代手动拆分。

2. 安装

1
go get github.com/mattn/go-shellwords

3. 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"log"
"github.com/mattn/go-shellwords"
)

func main() {
// 待解析的命令字符串
cmdStr := `/app --config="config.yaml" --port=8080 -v`

// 解析字符串为参数数组
args, err := shellwords.Parse(cmdStr)
if err != nil {
log.Fatalf("解析失败: %v", err)
}

// 输出结果
fmt.Println("解析后的参数:", args)
// 输出:[./app --config=config.yaml --port=8080 -v]
}

4. 处理引号和转义

1
2
3
4
5
6
7
8
func main() {
// 含空格、引号、转义的复杂字符串
cmdStr := `git commit -m "修复bug \n 第123号任务" --file="data.txt\""`

args, _ := shellwords.Parse(cmdStr)
fmt.Println(args)
// 输出:[git commit -m 修复bug \n 第123号任务 --file=data.txt"]
}

5. 解析环境变量

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// 假设环境变量 $USER=admin
cmdStr := `ssh $USER@example.com "ls /home/$USER"`

// 启用环境变量解析
parser := shellwords.NewParser()
parser.ParseEnv = true
args, _ := parser.Parse(cmdStr)

fmt.Println(args)
// 输出:[ssh admin@example.com ls /home/admin]
}

6. 自定义解析器

1
2
3
4
5
6
7
8
9
func main() {
// 创建自定义解析器(禁用引号解析)
parser := shellwords.NewParser()
parser.SkipQuotes = true // 跳过引号解析

args, _ := parser.Parse(`--name="Alice" --path='./data dir'`)
fmt.Println(args)
// 输出:[--name="Alice" --path='./data dir']
}

7. 错误处理

1
2
3
4
5
6
7
8
9
10
11
func main() {
// 非法格式(未闭合引号)
cmdStr := `run --path="/data`

args, err := shellwords.Parse(cmdStr)
if err != nil {
fmt.Println("错误类型:", err) // *shellwords.ParseError
fmt.Println("错误位置:", err.Pos) // 错误字符位置
fmt.Println("错误信息:", err) // unclosed quote
}
}

8. 与标准库对比

  • 优势:相比strings.Fields(),能正确处理:
    • 带空格的参数(如 "hello world"
    • 转义字符(如 \n, \"
    • 环境变量(如 $PATH
  • 场景:需要模拟 shell 解析逻辑时使用。

总结

1
2
3
4
5
// 核心步骤:
1. 导入包:import "github.com/mattn/go-shellwords"
2. 解析字符串:args, err := shellwords.Parse(cmdStr)
3. 处理特殊需求:通过 NewParser() 配置解析器选项
4. 结合 flag/pflag 包解析参数数组

实践用法:开发了一个bot,需要给bot输入一些指令去解析

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
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"flag"
"fmt"
"log"

"github.com/mattn/go-shellwords"
)

func main() {
cmdStr := `cmd123 --service=alert --port=8080`

parser := shellwords.NewParser()
args, err := parser.Parse(cmdStr)
if err != nil {
log.Fatalf("解析失败: %v", err)
}

if len(args) > 0 {
fmt.Printf("程序名: %v\n", args[0])
args = args[1:]
}

service := flag.String("service", "", "服务名称")
port := flag.Int("port", 0, "端口号")
flag.CommandLine.Parse(args)

fmt.Printf("服务: %s\n", *service)
fmt.Printf("端口: %d\n", *port)
}

/*
程序名: cmd123
服务: alert
端口: 8080
*/

flag.CommandLine.Parse(args)解析失败会直接退出程序,不符合预期,下面的实践比较合适。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package protocol

import (
"bytes"
"flag"
"fmt"

"github.com/mattn/go-shellwords"
)

const (
CmdPrefix = "cmd_"
)

// ================================
// general bot
// ================================
const (
CmdEncrypt = CmdPrefix + "encrypt" // cmd_encrypt <fid>
)

// ================================
// test env assistant
// ================================
const (
CmdRelease = CmdPrefix + "release" // cmd_release -service=<service_name> -env=<number> -build_branch=<branch>

CmdFollow = CmdPrefix + "follow" // cmd_follow -env=<number>
CmdUnfollow = CmdPrefix + "unfollow" // cmd_unfollow -env=<number>
CmdFollowerList = CmdPrefix + "follower_list" // cmd_follower_list -env=<number>
CmdMyFollowingList = CmdPrefix + "my_following_list" // cmd_my_following_list

CmdSetOwner = CmdPrefix + "set_owner" // cmd_set_owner -env=<number> -email=<email>
CmdSetHolder = CmdPrefix + "set_holder" // cmd_set_holder -env=<number> -email=<email>
CmdRemoveHolder = CmdPrefix + "remove_holder" // cmd_remove_holder -env=<number> -email=<email>
CmdSetDesc = CmdPrefix + "set_desc" // cmd_set_desc -env=<number> -desc=<desc>
)

func IsSupportCmd(cmd string) bool {
return cmd == CmdRelease || cmd == CmdFollow ||
cmd == CmdUnfollow || cmd == CmdFollowerList ||
cmd == CmdMyFollowingList || cmd == CmdSetOwner ||
cmd == CmdSetHolder || cmd == CmdRemoveHolder ||
cmd == CmdSetDesc
}

func ParseCmd(cmdStr string) (cmd string, args []string, err error) {
parser := shellwords.NewParser()
args, err = parser.Parse(cmdStr)
if err != nil {
return "", nil, err
}
if len(args) > 0 {
cmd = args[0]
args = args[1:]
} else {
return "", nil, fmt.Errorf("invalid command")
}
return cmd, args, nil
}

func ParseArgsCmdRelease(args []string) (serviceName *string, envNumber *int, buildBranch *string, err error) {
fs := flag.NewFlagSet(CmdRelease, flag.ContinueOnError)
var errBuf bytes.Buffer
fs.SetOutput(&errBuf)
defer func() {
if err != nil {
fs.Usage()
err = fmt.Errorf("%s\n%s", err.Error(), errBuf.String())
}
}()

serviceName = fs.String("service", "", "服务名称")
envNumber = fs.Int("env", 0, "环境编号")
buildBranch = fs.String("branch", "master", "构建分支")
if err := fs.Parse(args); err != nil {
return nil, nil, nil, err
}

if *serviceName == "" {
return nil, nil, nil, fmt.Errorf("invalid service name")
}

if !IsValidEnvNumber(*envNumber) {
return nil, nil, nil, fmt.Errorf("invalid env number")
}

return serviceName, envNumber, buildBranch, nil
}

func ParseArgsCmdFollow(args []string) (envNumber *int, err error) {
fs := flag.NewFlagSet(CmdFollow, flag.ContinueOnError)
var errBuf bytes.Buffer
fs.SetOutput(&errBuf)
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
if err := fs.Parse(args); err != nil {
return nil, err
}

if !IsValidEnvNumber(*envNumber) {
return nil, fmt.Errorf("invalid env number")
}

return envNumber, nil
}

func ParseArgsCmdUnfollow(args []string) (envNumber *int, err error) {
fs := flag.NewFlagSet(CmdUnfollow, flag.ContinueOnError)
var errBuf bytes.Buffer
fs.SetOutput(&errBuf)
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
if err := fs.Parse(args); err != nil {
return nil, err
}

if !IsValidEnvNumber(*envNumber) {
return nil, fmt.Errorf("invalid env number")
}

return envNumber, nil
}

func ParseArgsCmdFollowerList(args []string) (envNumber *int, err error) {
fs := flag.NewFlagSet(CmdFollowerList, flag.ContinueOnError)
var errBuf bytes.Buffer
fs.SetOutput(&errBuf)
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
if err := fs.Parse(args); err != nil {
return nil, err
}

if !IsValidEnvNumber(*envNumber) {
return nil, fmt.Errorf("invalid env number")
}

return envNumber, nil
}

func ParseArgsCmdSetOwner(args []string) (envNumber *int, email *string, err error) {
fs := flag.NewFlagSet(CmdSetOwner, flag.ContinueOnError)
var errBuf bytes.Buffer
fs.SetOutput(&errBuf)
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
email = fs.String("email", "", "邮箱")
if err := fs.Parse(args); err != nil {
return nil, nil, err
}

if *email == "" {
return nil, nil, fmt.Errorf("invalid email")
}

if !IsValidEnvNumber(*envNumber) {
return nil, nil, fmt.Errorf("invalid env number")
}

return envNumber, email, nil
}

func ParseArgsCmdSetHolder(args []string) (envNumber *int, email *string, err error) {
fs := flag.NewFlagSet(CmdSetHolder, flag.ContinueOnError)
var errBuf bytes.Buffer
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
email = fs.String("email", "", "邮箱")
if err := fs.Parse(args); err != nil {
return nil, nil, err
}

if *email == "" {
return nil, nil, fmt.Errorf("invalid email")
}

if !IsValidEnvNumber(*envNumber) {
return nil, nil, fmt.Errorf("invalid env number")
}

return envNumber, email, nil
}

func ParseArgsCmdRemoveHolder(args []string) (envNumber *int, email *string, err error) {
fs := flag.NewFlagSet(CmdRemoveHolder, flag.ContinueOnError)
var errBuf bytes.Buffer
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
email = fs.String("email", "", "邮箱")
if err := fs.Parse(args); err != nil {
return nil, nil, err
}

if *email == "" {
return nil, nil, fmt.Errorf("invalid email")
}

if !IsValidEnvNumber(*envNumber) {
return nil, nil, fmt.Errorf("invalid env number")
}

return envNumber, email, nil
}

func ParseArgsCmdSetDesc(args []string) (envNumber *int, desc *string, err error) {
fs := flag.NewFlagSet(CmdSetDesc, flag.ContinueOnError)
var errBuf bytes.Buffer
defer func() {
if err != nil {
flag.Usage()
err = fmt.Errorf("%s\n%s", errBuf.String(), err.Error())
}
}()

envNumber = fs.Int("env", 0, "环境编号")
desc = fs.String("desc", "", "描述")
if err := fs.Parse(args); err != nil {
return nil, nil, err
}

if *desc == "" {
return nil, nil, fmt.Errorf("invalid desc")
}

if !IsValidEnvNumber(*envNumber) {
return nil, nil, fmt.Errorf("invalid env number")
}

return envNumber, desc, nil
}




redis限流器

1
go get github.com/go-redis/redis_rate/v9