mirror of
https://github.com/eiblog/eiblog.git
synced 2026-02-10 16:42:26 +08:00
265 lines
6.0 KiB
Go
265 lines
6.0 KiB
Go
package cmdline
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
. "qiniupkg.com/x/ctype.v7"
|
|
)
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
Shell 基础规则:
|
|
|
|
* 多行字符串:用 '...' 或 "..."。其中 " 会自动转义 $(var),而 ' 不会。
|
|
* 普通字符串:用 [ \t] 分隔。转义符以 \ 开头。
|
|
* 外部命令:`...`。
|
|
|
|
七牛规则:
|
|
|
|
* 外部命令: `...` 或 |...| 。
|
|
* 多行字符串:用 '...' 或 ```\n...``` 或 ===\n...=== 不转义。用 "...",支持以 \ 开头的转义,也支持外部命令。
|
|
* 普通字符串:用 [ \t] 分隔。转义符以 \ 开头,同时也支持外部命令。
|
|
* 关于 $(var) 支持:每个命令自己执行 $(var) 的展开。不统一执行的原因是,在不同上下文需要不同的转义方式。
|
|
|
|
样例:
|
|
|
|
post http://rs.qiniu.com/delete/`base64 Bucket:Key`
|
|
auth `qbox AccessKey SecretKey`
|
|
ret 200
|
|
|
|
post http://rs.qiniu.com/batch
|
|
auth qboxtest
|
|
form op=/delete/`base64 Bucket:Key`&op=/delete/`base64 Bucket2:Key2`
|
|
ret 200
|
|
|
|
post http://rs.qiniu.com/batch
|
|
auth qboxtest
|
|
form op=/delete/`base64 Bucket:Key`&op=/delete/`base64 Bucket:NotExistKey`
|
|
ret 298
|
|
json '[
|
|
{"code": 200}, {"code": 612}
|
|
]'
|
|
equal $(code1) 200
|
|
|
|
// -------------------------------------------------------------------------*/
|
|
|
|
var (
|
|
EOF = errors.New("end of file")
|
|
ErrUnsupportedFeatureSubCmd = errors.New("unsupported feature: sub command")
|
|
ErrUnsupportedFeatureMultiCmds = errors.New("unsupported feature: multi commands")
|
|
ErrInvalidEscapeChar = errors.New("invalid escape char")
|
|
ErrIncompleteStringExpectQuot = errors.New("incomplete string, expect \"")
|
|
ErrIncompleteStringExpectSquot = errors.New("incomplete string, expect '")
|
|
ErrIncompleteStringExpectBacktick = errors.New("incomplete string, expect ` or |")
|
|
)
|
|
|
|
var (
|
|
errEOL = errors.New("end of line")
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func Skip(str string, typeMask uint32) string {
|
|
|
|
for i := 0; i < len(str); i++ {
|
|
if !Is(typeMask, rune(str[i])) {
|
|
return str[i:]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func Find(str string, typeMask uint32) (n int) {
|
|
|
|
for n = 0; n < len(str); n++ {
|
|
if Is(typeMask, rune(str[n])) {
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// EOL = \r\n? | \n
|
|
//
|
|
func requireEOL(code string) (hasEOL bool, codeNext string) {
|
|
|
|
if strings.HasPrefix(code, "\r") {
|
|
if strings.HasPrefix(code[1:], "\n") {
|
|
return true, code[2:]
|
|
}
|
|
} else if !strings.HasPrefix(code, "\n") {
|
|
return false, code
|
|
}
|
|
return true, code[1:]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type Parser struct {
|
|
ExecSub func(code string) (string, error)
|
|
Escape func(c byte) string
|
|
comment bool
|
|
}
|
|
|
|
func NewParser() *Parser {
|
|
|
|
return &Parser{
|
|
ExecSub: defaultExecSub,
|
|
Escape: defaultEscape,
|
|
}
|
|
}
|
|
|
|
func defaultExecSub(code string) (string, error) {
|
|
return "", ErrUnsupportedFeatureSubCmd
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const (
|
|
endOfLine = EOL | SEMICOLON // [\r\n;]
|
|
blanks = SPACE_BAR | TAB
|
|
blankAndEOLs = SPACE_BAR | TAB | endOfLine
|
|
)
|
|
|
|
const (
|
|
endMask_QuotString = RDIV | BACKTICK | OR | QUOT // [\\`|"]
|
|
endMask_NonquotString = RDIV | BACKTICK | OR | blankAndEOLs // [\\`| \t\r\n;]
|
|
)
|
|
|
|
func (p *Parser) parseString(
|
|
code string, endMask uint32) (item string, ok bool, codeNext string, err error) {
|
|
|
|
codeNext = code
|
|
for {
|
|
n := Find(codeNext, endMask)
|
|
if n > 0 {
|
|
item += codeNext[:n]
|
|
ok = true
|
|
}
|
|
if len(codeNext) == n {
|
|
codeNext = ""
|
|
if endMask == endMask_QuotString {
|
|
err = ErrIncompleteStringExpectQuot
|
|
} else {
|
|
err = EOF
|
|
}
|
|
return
|
|
}
|
|
switch codeNext[n] {
|
|
case '\\':
|
|
if len(codeNext) == n+1 {
|
|
err = ErrInvalidEscapeChar
|
|
return
|
|
}
|
|
item += p.Escape(codeNext[n+1])
|
|
codeNext = codeNext[n+2:]
|
|
case '`', '|':
|
|
c := codeNext[n]
|
|
codeNext = codeNext[n+1:]
|
|
len := strings.IndexByte(codeNext, c)
|
|
if len < 0 {
|
|
err = ErrIncompleteStringExpectBacktick
|
|
return
|
|
}
|
|
if !p.comment {
|
|
valSub, errSub := p.ExecSub(codeNext[:len])
|
|
if errSub != nil {
|
|
err = errors.New("Exec `" + codeNext[:len] + "` failed: " + errSub.Error())
|
|
return
|
|
}
|
|
item += valSub
|
|
}
|
|
codeNext = codeNext[len+1:]
|
|
case '"':
|
|
ok = true
|
|
codeNext = codeNext[n+1:]
|
|
return
|
|
default:
|
|
if Is(endOfLine, rune(codeNext[n])) {
|
|
err = errEOL
|
|
}
|
|
codeNext = codeNext[n+1:]
|
|
return
|
|
}
|
|
ok = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func (p *Parser) parseItem(
|
|
code string, skipMask uint32) (item string, ok bool, codeNext string, err error) {
|
|
|
|
codeNext = Skip(code, skipMask)
|
|
if len(codeNext) == 0 {
|
|
err = EOF
|
|
return
|
|
}
|
|
|
|
switch codeNext[0] {
|
|
case '"':
|
|
return p.parseString(codeNext[1:], endMask_QuotString)
|
|
case '\'':
|
|
codeNext = codeNext[1:]
|
|
len := strings.IndexByte(codeNext, '\'')
|
|
if len < 0 {
|
|
err = ErrIncompleteStringExpectSquot
|
|
return
|
|
}
|
|
return codeNext[:len], true, codeNext[len+1:], nil
|
|
default:
|
|
if strings.HasPrefix(codeNext, "```") || strings.HasPrefix(codeNext, "===") {
|
|
endMark := codeNext[:3]
|
|
_, codeNext = requireEOL(codeNext[3:])
|
|
len := strings.Index(codeNext, endMark)
|
|
if len < 0 {
|
|
err = errors.New("incomplete string, expect " + endMark)
|
|
return
|
|
}
|
|
return codeNext[:len], true, codeNext[len+3:], nil
|
|
}
|
|
return p.parseString(codeNext, endMask_NonquotString)
|
|
}
|
|
}
|
|
|
|
func (p *Parser) ParseCmd(cmdline string) (cmd []string, err error) {
|
|
|
|
cmd, _, err = p.ParseCode(cmdline)
|
|
if err == EOF && len(cmd) > 0 {
|
|
return cmd, nil
|
|
}
|
|
if err == nil {
|
|
err = ErrUnsupportedFeatureMultiCmds
|
|
}
|
|
return
|
|
}
|
|
|
|
func (p *Parser) ParseCode(code string) (cmd []string, codeNext string, err error) {
|
|
|
|
item, ok, codeNext, err := p.parseItem(code, blankAndEOLs)
|
|
if !ok {
|
|
return
|
|
}
|
|
p.comment = strings.HasPrefix(item, "#")
|
|
|
|
cmd = append(cmd, item)
|
|
for err == nil {
|
|
item, ok, codeNext, err = p.parseItem(codeNext, blanks)
|
|
if ok {
|
|
cmd = append(cmd, item)
|
|
}
|
|
}
|
|
if err == errEOL {
|
|
err = nil
|
|
}
|
|
if p.comment {
|
|
cmd = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|