-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from qiniu/develop
Release v7.0.8 - qiniupkg.com/x/cmdline.v7
- Loading branch information
Showing
5 changed files
with
577 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
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 | ||
} | ||
|
||
// --------------------------------------------------------------------------- | ||
|
Oops, something went wrong.