1
0
mirror of https://github.com/fumiama/ReiBot.git synced 2026-06-05 00:50:25 +08:00
This commit is contained in:
源文雨
2022-06-02 14:36:43 +08:00
parent 7c90c8b7d3
commit 8b6817acea
13 changed files with 802 additions and 17 deletions

BIN
.github/Misaki.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

@@ -1,5 +1,14 @@
# ReiBot
Lightweight Telegram bot framework
<div align="center">
<a href="https://crypko.ai/crypko/GtWYDpVMx5GYm/">
<img src=".github/Misaki.png" alt="看板娘" width = "400">
</a><br>
<h1>ReiBot</h1>
Lightweight Telegram bot framework<br><br>
<img src="http://cmoe.azurewebsites.net/cmoe?name=ReiBot&theme=r34" /><br>
</div>
## Instructions
@@ -8,7 +17,7 @@ This framework is a simple wrapper for [go-telegram-bot-api](https://github.com/
## Quick Start
> Here is a plugin-based example
![example](https://user-images.githubusercontent.com/41315874/171227962-199ede01-e41a-4552-8b72-018ee23ad2e2.png)
![plugin-based example](https://user-images.githubusercontent.com/41315874/171567343-f61eba4e-2bc9-49b3-af05-6446f0a73c54.png)
```go
package main
@@ -45,7 +54,7 @@ func main() {
> If Handler in Bot is implemented, the plugin function will be disabled.
![example](https://user-images.githubusercontent.com/41315874/171180885-c888a031-7797-4b4b-a232-9ff23f031b32.png)
![event-based example](https://user-images.githubusercontent.com/41315874/171567349-5ff59cfa-cc3a-44a8-8158-6c76c8d433b7.png)
```go
package main

8
bot.go
View File

@@ -9,13 +9,15 @@ import (
type Bot struct {
// Token bot 的 token
// see https://core.telegram.org/bots#3-how-do-i-create-a-bot
Token string `json:"token"`
Token string
// Buffer 控制消息队列的长度
Buffer int `json:"buffer"`
Buffer int
// UpdateConfig 配置消息获取
tgba.UpdateConfig
// SuperUsers 超级用户
SuperUsers []int64
// Debug 控制调试信息的输出与否
Debug bool `json:"debug"`
Debug bool
// Handler 注册对各种事件的处理
Handler *Handler
// handlers 方便调用的 handler

View File

@@ -1,8 +1,22 @@
package rei
import tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5"
type Ctx struct {
Event
State
Caller *TelegramClient
ma *Matcher
}
// CheckSession 判断会话连续性
func (ctx *Ctx) CheckSession() Rule {
msg := ctx.Value.(*tgba.Message)
return func(ctx2 *Ctx) bool {
msg2, ok := ctx.Value.(*tgba.Message)
if !ok || msg.From == nil || msg.Chat == nil || msg2.From == nil || msg2.Chat == nil { // 确保无空
return false
}
return msg.From.ID == msg2.From.ID && msg.Chat.ID == msg2.Chat.ID
}
}

180
engine.go
View File

@@ -159,12 +159,12 @@ func (e *Engine) OnChatJoinRequest(rules ...Rule) *Matcher { return e.On("ChatJo
// OnChatJoinRequest ...
func OnChatJoinRequest(rules ...Rule) *Matcher { return On("ChatJoinRequest", rules...) }
// OnPrefix 前缀触发器
// OnMessagePrefix 前缀触发器
func OnMessagePrefix(prefix string, rules ...Rule) *Matcher {
return defaultEngine.OnMessagePrefix(prefix, rules...)
}
// OnPrefix 前缀触发器
// OnMessagePrefix 前缀触发器
func (e *Engine) OnMessagePrefix(prefix string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
@@ -174,3 +174,179 @@ func (e *Engine) OnMessagePrefix(prefix string, rules ...Rule) *Matcher {
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageSuffix 后缀触发器
func OnMessageSuffix(suffix string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageSuffix(suffix, rules...)
}
// OnMessageSuffix 后缀触发器
func (e *Engine) OnMessageSuffix(suffix string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{SuffixRule(suffix)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageCommand 命令触发器
func OnMessageCommand(commands string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageCommand(commands, rules...)
}
// OnMessageCommand 命令触发器
func (e *Engine) OnMessageCommand(commands string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{CommandRule(commands)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageRegex 正则触发器
func OnMessageRegex(regexPattern string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageRegex(regexPattern, rules...)
}
// OnRegex 正则触发器
func (e *Engine) OnMessageRegex(regexPattern string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{RegexRule(regexPattern)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageKeyword 关键词触发器
func OnMessageKeyword(keyword string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageKeyword(keyword, rules...)
}
// OnKeyword 关键词触发器
func (e *Engine) OnMessageKeyword(keyword string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{KeywordRule(keyword)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageFullMatch 完全匹配触发器
func OnMessageFullMatch(src string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageFullMatch(src, rules...)
}
// OnMessageFullMatch 完全匹配触发器
func (e *Engine) OnMessageFullMatch(src string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{FullMatchRule(src)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageFullMatchGroup 完全匹配触发器组
func OnMessageFullMatchGroup(src []string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageFullMatchGroup(src, rules...)
}
// OnMessageFullMatchGroup 完全匹配触发器组
func (e *Engine) OnMessageFullMatchGroup(src []string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{FullMatchRule(src...)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageKeywordGroup 关键词触发器组
func OnMessageKeywordGroup(keywords []string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageKeywordGroup(keywords, rules...)
}
// OnMessageKeywordGroup 关键词触发器组
func (e *Engine) OnMessageKeywordGroup(keywords []string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{KeywordRule(keywords...)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageCommandGroup 命令触发器组
func OnMessageCommandGroup(commands []string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageCommandGroup(commands, rules...)
}
// OnMessageCommandGroup 命令触发器组
func (e *Engine) OnMessageCommandGroup(commands []string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{CommandRule(commands...)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessagePrefixGroup 前缀触发器组
func OnMessagePrefixGroup(prefix []string, rules ...Rule) *Matcher {
return defaultEngine.OnMessagePrefixGroup(prefix, rules...)
}
// OnMessagePrefixGroup 前缀触发器组
func (e *Engine) OnMessagePrefixGroup(prefix []string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{PrefixRule(prefix...)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageSuffixGroup 后缀触发器组
func OnMessageSuffixGroup(suffix []string, rules ...Rule) *Matcher {
return defaultEngine.OnMessageSuffixGroup(suffix, rules...)
}
// OnMessageSuffixGroup 后缀触发器组
func (e *Engine) OnMessageSuffixGroup(suffix []string, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{SuffixRule(suffix...)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}
// OnMessageShell shell命令触发器
func OnMessageShell(command string, model interface{}, rules ...Rule) *Matcher {
return defaultEngine.OnMessageShell(command, model, rules...)
}
// OnMessageShell shell命令触发器
func (e *Engine) OnMessageShell(command string, model interface{}, rules ...Rule) *Matcher {
matcher := &Matcher{
Type: "Message",
Rules: append([]Rule{ShellRule(command, model)}, rules...),
Engine: e,
}
e.matchers = append(e.matchers, matcher)
return StoreMatcher(matcher)
}

18
example/echo/main.go Normal file
View File

@@ -0,0 +1,18 @@
package echo
import (
rei "github.com/fumiama/ReiBot"
tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func init() {
rei.OnMessagePrefix("echo").SetBlock(true).SecondPriority().
Handle(func(ctx *rei.Ctx) {
args := ctx.State["args"].(string)
if args == "" {
return
}
msg := ctx.Value.(*tgba.Message)
ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, args))
})
}

View File

@@ -1,19 +1,17 @@
package main
import (
_ "github.com/fumiama/ReiBot/example/echo"
rei "github.com/fumiama/ReiBot"
tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func main() {
rei.OnMessagePrefix("echo").SetBlock(true).SecondPriority().
rei.OnMessageFullMatch("help").SetBlock(true).SecondPriority().
Handle(func(ctx *rei.Ctx) {
args := ctx.State["args"].(string)
if args == "" {
return
}
msg := ctx.Value.(*tgba.Message)
ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, args))
ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, "echo string"))
})
rei.Run(rei.Bot{
Token: "",

98
future.go Normal file
View File

@@ -0,0 +1,98 @@
package rei
// FutureEvent 是 ZeroBot 交互式的核心,用于异步获取指定事件
type FutureEvent struct {
Type string
Priority int
Rule []Rule
Block bool
}
// NewFutureEvent 创建一个FutureEvent, 并返回其指针
func NewFutureEvent(Type string, Priority int, Block bool, rule ...Rule) *FutureEvent {
return &FutureEvent{
Type: Type,
Priority: Priority,
Rule: rule,
Block: Block,
}
}
// FutureEvent 返回一个 FutureEvent 实例指针,用于获取满足 Rule 的 未来事件
func (m *Matcher) FutureEvent(Type string, rule ...Rule) *FutureEvent {
return &FutureEvent{
Type: Type,
Priority: m.Priority,
Block: m.Block,
Rule: rule,
}
}
// Next 返回一个 chan 用于接收下一个指定事件
//
// 该 chan 必须接收,如需手动取消监听,请使用 Repeat 方法
func (n *FutureEvent) Next() <-chan *Ctx {
ch := make(chan *Ctx, 1)
StoreTempMatcher(&Matcher{
Type: n.Type,
Block: n.Block,
Priority: n.Priority,
Rules: n.Rule,
Engine: defaultEngine,
Process: func(ctx *Ctx) {
ch <- ctx
close(ch)
},
})
return ch
}
// Repeat 返回一个 chan 用于接收无穷个指定事件,和一个取消监听的函数
//
// 如果没有取消监听,将不断监听指定事件
func (n *FutureEvent) Repeat() (recv <-chan *Ctx, cancel func()) {
ch, done := make(chan *Ctx, 1), make(chan struct{})
go func() {
defer close(ch)
in := make(chan *Ctx, 1)
matcher := StoreMatcher(&Matcher{
Type: n.Type,
Block: n.Block,
Priority: n.Priority,
Rules: n.Rule,
Engine: defaultEngine,
Process: func(ctx *Ctx) {
in <- ctx
},
})
for {
select {
case e := <-in:
ch <- e
case <-done:
matcher.Delete()
close(in)
return
}
}
}()
return ch, func() {
close(done)
}
}
// Take 基于 Repeat 封装,返回一个 chan 接收指定数量的事件
//
// 该 chan 对象必须接收,否则将有 goroutine 泄漏,如需手动取消请使用 Repeat
func (n *FutureEvent) Take(num int) <-chan *Ctx {
recv, cancel := n.Repeat()
ch := make(chan *Ctx, num)
go func() {
defer close(ch)
for i := 0; i < num; i++ {
ch <- <-recv
}
cancel()
}()
return ch
}

11
go.mod
View File

@@ -2,4 +2,13 @@ module github.com/fumiama/ReiBot
go 1.18
require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
require (
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/stretchr/testify v1.7.1
)
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

11
go.sum
View File

@@ -1,2 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

276
rules.go
View File

@@ -1,12 +1,15 @@
package rei
import (
"reflect"
"regexp"
"strings"
"time"
tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// PrefixRule check if the message has the prefix and trim the prefix
// PrefixRule check if the text message has the prefix and trim the prefix
//
// 检查消息前缀
func PrefixRule(prefixes ...string) Rule {
@@ -26,3 +29,274 @@ func PrefixRule(prefixes ...string) Rule {
return false
}
}
// SuffixRule check if the text message has the suffix and trim the suffix
//
// 检查消息后缀
func SuffixRule(suffixes ...string) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" { // 确保无空
return false
}
for _, suffix := range suffixes {
if strings.HasSuffix(msg.Text, suffix) {
ctx.State["suffix"] = suffix
arg := strings.TrimRight(msg.Text[:len(msg.Text)-len(suffix)], " ")
ctx.State["args"] = arg
return true
}
}
return false
}
}
// CommandRule check if the message is a command and trim the command name
func CommandRule(commands ...string) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" || !msg.IsCommand() { // 确保无空
return false
}
ctx.State["command"] = msg.CommandWithAt()
ctx.State["args"] = msg.CommandArguments()
return true
}
}
// RegexRule check if the message can be matched by the regex pattern
func RegexRule(regexPattern string) Rule {
regex := regexp.MustCompile(regexPattern)
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" { // 确保无空
return false
}
if matched := regex.FindStringSubmatch(msg.Text); matched != nil {
ctx.State["regex_matched"] = matched
return true
}
return false
}
}
// ReplyRule check if the message is replying some message
func ReplyRule(messageID int) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.ReplyToMessage == nil { // 确保无空
return false
}
return messageID == msg.MessageID
}
}
// KeywordRule check if the message has a keyword or keywords
func KeywordRule(src ...string) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" { // 确保无空
return false
}
for _, str := range src {
if strings.Contains(msg.Text, str) {
ctx.State["keyword"] = str
return true
}
}
return false
}
}
// FullMatchRule check if src has the same copy of the message
func FullMatchRule(src ...string) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" { // 确保无空
return false
}
for _, str := range src {
if str == msg.Text {
ctx.State["matched"] = msg.Text
return true
}
}
return false
}
}
// ShellRule 定义shell-like规则
func ShellRule(cmd string, model interface{}) Rule {
cmdRule := CommandRule(cmd)
t := reflect.TypeOf(model)
return func(ctx *Ctx) bool {
if !cmdRule(ctx) {
return false
}
// bind flag to struct
args := ParseShell(ctx.State["args"].(string))
val := reflect.New(t)
fs := registerFlag(t, val)
err := fs.Parse(args)
if err != nil {
return false
}
ctx.State["args"] = fs.Args()
ctx.State["flag"] = val.Interface()
return true
}
}
// OnlyToMe only triggered in conditions of @bot or begin with the nicknames
func OnlyToMe(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Text == "" { // 确保无空
return false
}
name := ctx.Caller.Self.String()
if strings.HasPrefix(msg.Text, name) {
return true
}
n := 0
for _, e := range msg.Entities {
if e.IsMention() && e.Length > 0 && msg.Text[n+1:n+e.Length] == name {
return true
}
n += e.Length
}
return false
}
// CheckUser only triggered by specific person
func CheckUser(userId ...int64) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.From == nil { // 确保无空
return false
}
for _, uid := range userId {
if msg.From.ID == uid {
return true
}
}
return false
}
}
// CheckChat only triggered in specific chat
func CheckChat(chatId ...int64) Rule {
return func(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.Chat == nil { // 确保无空
return false
}
for _, cid := range chatId {
if msg.Chat.ID == cid {
return true
}
}
return false
}
}
// SuperUserPermission only triggered by the bot's owner
func SuperUserPermission(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.From == nil { // 确保无空
return false
}
for _, su := range ctx.Caller.b.SuperUsers {
if su == msg.From.ID {
return true
}
}
return false
}
// CreaterPermission only triggered by the group creater or higher permission
func CreaterPermission(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.From == nil || msg.Chat == nil { // 确保无空
return false
}
for _, su := range ctx.Caller.b.SuperUsers {
if su == msg.From.ID {
return true
}
}
m, err := ctx.Caller.GetChatMember(
tgba.GetChatMemberConfig{
ChatConfigWithUser: tgba.ChatConfigWithUser{
ChatID: msg.Chat.ID,
UserID: msg.From.ID,
},
},
)
if err != nil {
return false
}
return m.IsCreator()
}
// AdminPermission only triggered by the group admins or higher permission
func AdminPermission(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || msg.From == nil || msg.Chat == nil { // 确保无空
return false
}
for _, su := range ctx.Caller.b.SuperUsers {
if su == msg.From.ID {
return true
}
}
m, err := ctx.Caller.GetChatMember(
tgba.GetChatMemberConfig{
ChatConfigWithUser: tgba.ChatConfigWithUser{
ChatID: msg.Chat.ID,
UserID: msg.From.ID,
},
},
)
if err != nil {
return false
}
return m.IsCreator() || m.IsAdministrator()
}
// IsPhoto 消息是图片返回 true
func IsPhoto(ctx *Ctx) bool {
msg, ok := ctx.Value.(*tgba.Message)
if !ok || len(msg.Photo) == 0 { // 确保无空
return false
}
ctx.State["photos"] = msg.Photo
return true
}
// MustProvidePhoto 消息不存在图片阻塞120秒至有图片超时返回 false
func MustProvidePhoto(ctx *Ctx, needphohint, failhint string) bool {
msg, ok := ctx.Value.(*tgba.Message)
if ok && len(msg.Photo) > 0 { // 确保无空
ctx.State["photos"] = msg.Photo
return true
}
// 没有图片就索取
if needphohint != "" {
_, err := ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, needphohint))
if err != nil {
return false
}
}
next := NewFutureEvent("message", 999, false, ctx.CheckSession(), IsPhoto).Next()
select {
case <-time.After(time.Second * 120):
if failhint != "" {
_, _ = ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, failhint))
}
return false
case newCtx := <-next:
ctx.State["photos"] = newCtx.State["photos"]
ctx.Event = newCtx.Event
return true
}
}

131
shell.go Normal file
View File

@@ -0,0 +1,131 @@
package rei
import (
"flag"
"reflect"
"strings"
)
func isSpace(r rune) bool {
switch r {
case ' ', '\t', '\r', '\n':
return true
}
return false
}
type argType int
const (
argNo argType = iota
argSingle
argQuoted
)
// ParseShell 将指令转换为指令参数.
// modified from https://github.com/mattn/go-shellwords
func ParseShell(s string) []string {
var args []string
buf := strings.Builder{}
var escaped, doubleQuoted, singleQuoted, backQuote bool
backtick := ""
got := argNo
for _, r := range s {
if escaped {
buf.WriteRune(r)
escaped = false
got = argSingle
continue
}
if r == '\\' {
if singleQuoted {
buf.WriteRune(r)
} else {
escaped = true
}
continue
}
if isSpace(r) {
if singleQuoted || doubleQuoted || backQuote {
buf.WriteRune(r)
backtick += string(r)
} else if got != argNo {
args = append(args, buf.String())
buf.Reset()
got = argNo
}
continue
}
switch r {
case '`':
if !singleQuoted && !doubleQuoted {
backtick = ""
backQuote = !backQuote
}
case '"':
if !singleQuoted {
if doubleQuoted {
got = argQuoted
}
doubleQuoted = !doubleQuoted
}
case '\'':
if !doubleQuoted {
if singleQuoted {
got = argSingle
}
singleQuoted = !singleQuoted
}
default:
got = argSingle
buf.WriteRune(r)
if backQuote {
backtick += string(r)
}
}
}
if got != argNo {
args = append(args, buf.String())
}
return args
}
var (
boolType = reflect.TypeOf(false)
intType = reflect.TypeOf(0)
stringType = reflect.TypeOf("")
float64Type = reflect.TypeOf(float64(0))
)
func registerFlag(t reflect.Type, v reflect.Value) *flag.FlagSet {
v = v.Elem()
fs := flag.NewFlagSet("", flag.ContinueOnError)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
name := field.Tag.Get("flag")
if name == "" {
continue
}
help := field.Tag.Get("help")
switch field.Type {
case boolType:
fs.BoolVar(v.Field(i).Addr().Interface().(*bool), name, false, help)
case intType:
fs.IntVar(v.Field(i).Addr().Interface().(*int), name, 0, help)
case stringType:
fs.StringVar(v.Field(i).Addr().Interface().(*string), name, "", help)
case float64Type:
fs.Float64Var(v.Field(i).Addr().Interface().(*float64), name, 0, help)
default:
panic("unsupported type")
}
}
return fs
}

45
shell_test.go Normal file
View File

@@ -0,0 +1,45 @@
package rei
import (
"reflect"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_parse(t *testing.T) {
shellTests := [...]struct {
shell string
expected []string
}{
{`rm -rf /*`, []string{"rm", "-rf", "/*"}},
{`echo "cat cat" -n`, []string{"echo", "cat cat", "-n"}},
{`shutdown halt init`, []string{"shutdown", "halt", "init"}},
{`test test2`, []string{"test", "test2"}},
}
for i, v := range shellTests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
out := ParseShell(v.shell)
assert.Equal(t, v.expected, out)
})
}
}
func Test_registerFlag(t *testing.T) {
type args struct {
RF bool `flag:"rf"`
File string `flag:"file"`
Count int `flag:"count"`
}
got := args{}
expected := args{
RF: true,
File: "123",
Count: 10,
}
fs := registerFlag(reflect.TypeOf(args{}), reflect.ValueOf(&got))
err := fs.Parse([]string{"-rf", "-file=123", "-count", "10"})
assert.NoError(t, err)
assert.Equal(t, expected, got)
}