Refactor context command (#13948)

### What problem does this PR solve?

As title

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-04-07 11:30:09 +08:00
committed by GitHub
parent ff27ce86d6
commit 5673245134
11 changed files with 341 additions and 106 deletions

View File

@@ -40,7 +40,7 @@ func main() {
}()
// Check if we have a single command to execute
if args.Command != "" {
if args.Command != nil {
// Single command mode
if err = cliApp.RunSingleCommand(args.Command); err != nil {
fmt.Printf("Error: %v\n", err)

View File

@@ -29,7 +29,6 @@ import (
"unicode/utf8"
"github.com/peterh/liner"
"golang.org/x/term"
"gopkg.in/yaml.v3"
"ragflow/internal/cli/contextengine"
@@ -59,7 +58,7 @@ type ConnectionArgs struct {
Password string
APIToken string
UserName string
Command string // Original command string (for SQL mode)
Command *string // Original command string (for SQL mode)
CommandArgs []string // Split command arguments (for ContextEngine mode)
IsSQLMode bool // true=SQL mode (quoted), false=ContextEngine mode (unquoted)
ShowHelp bool
@@ -304,22 +303,12 @@ func ParseConnectionArgs(args []string) (*ConnectionArgs, error) {
}
}
// Get command from remaining args (non-flag arguments)
// Get command from remaining args (non-flag arguments)
if len(nonFlagArgs) > 0 {
// Check if this is SQL mode or ContextEngine mode
// SQL mode: single argument that looks like SQL (e.g., "LIST DATASETS")
// ContextEngine mode: multiple arguments (e.g., "ls", "datasets")
if len(nonFlagArgs) == 1 && looksLikeSQL(nonFlagArgs[0]) {
// SQL mode: single argument that looks like SQL
result.IsSQLMode = true
result.Command = nonFlagArgs[0]
} else {
// ContextEngine mode: multiple arguments
result.IsSQLMode = false
result.CommandArgs = nonFlagArgs
// Also store joined version for backward compatibility
result.Command = strings.Join(nonFlagArgs, " ")
}
command := strings.Join(nonFlagArgs, " ")
result.Command = &command
fmt.Printf("COMMAND: %s\n", command)
}
return result, nil
@@ -485,17 +474,9 @@ func (c *CLI) Run() error {
for attempt := 1; attempt <= maxAttempts; attempt++ {
fmt.Print("Please input your password: ")
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
password, err := ReadPassword()
if err != nil {
fmt.Printf("Error reading password: %v\n", err)
return err
}
input := strings.TrimSpace(string(passwordBytes))
if input == "" {
if password == "" {
if attempt < maxAttempts {
fmt.Println("Password cannot be empty, please try again")
continue
@@ -503,7 +484,7 @@ func (c *CLI) Run() error {
return errors.New("no password provided after 3 attempts")
}
c.args.Password = input
c.args.Password = password
if err = c.VerifyAuth(); err != nil {
if attempt < maxAttempts {
@@ -557,7 +538,7 @@ func (c *CLI) Run() error {
c.line.AppendHistory(input)
}
if err = c.execute(input); err != nil {
if err = c.executeNew(input); err != nil {
fmt.Printf("CLI error: %v\n", err)
}
}
@@ -565,6 +546,31 @@ func (c *CLI) Run() error {
return nil
}
func (c *CLI) executeNew(input string) error {
p := NewParser(input)
cmd, err := p.Parse(c.args.AdminMode)
if err != nil {
return err
}
if cmd == nil {
return nil
}
// Handle meta commands
if cmd.Type == "meta" {
return c.handleMetaCommand(cmd)
}
// Execute the command using the client
var result ResponseIf
result, err = c.client.ExecuteCommand(cmd)
if result != nil {
result.PrintOut()
}
return err
}
func (c *CLI) execute(input string) error {
// Determine execution mode based on input and args
input = strings.TrimSpace(input)
@@ -699,9 +705,9 @@ func (c *CLI) executeContextEngine(input string) error {
fmt.Println("(empty file)")
} else if isBinaryContent(content) {
return fmt.Errorf("cannot display binary file content")
} else {
fmt.Println(string(content))
}
fmt.Println(string(content))
return nil
default:
return fmt.Errorf("unknown context engine command: %s", cmdType)
@@ -1068,12 +1074,12 @@ func RunInteractive() error {
}
// RunSingleCommand executes a single command and exits
func (c *CLI) RunSingleCommand(command string) error {
func (c *CLI) RunSingleCommand(command *string) error {
// Ensure cleanup is called on exit to restore terminal settings
defer c.Cleanup()
// Execute the command
if err := c.execute(command); err != nil {
if err := c.executeNew(*command); err != nil {
return err
}
return nil

View File

@@ -254,6 +254,12 @@ func (c *RAGFlowClient) ExecuteUserCommand(cmd *Command) (ResponseIf, error) {
case "show_current_model":
return c.ShowCurrentModel(cmd)
// ContextEngine commands
case "context_list":
return c.ContextList(cmd)
case "context_cat":
return c.ContextCat(cmd)
case "context_search":
return c.ContextSearch(cmd)
case "ce_ls":
return c.CEList(cmd)
case "ce_search":

View File

@@ -73,7 +73,7 @@ func (c *RAGFlowClient) LoginUserInteractive(username, password string) error {
if password == "" {
fmt.Printf("password for %s: ", username)
var err error
password, err = readPassword()
password, err = ReadPassword()
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
@@ -145,7 +145,7 @@ func (c *RAGFlowClient) LoginUser(cmd *Command) error {
if !ok {
// Get password from user input (hidden)
fmt.Printf("password for %s: ", email)
password, err = readPassword()
password, err = ReadPassword()
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
@@ -374,9 +374,9 @@ func (c *RAGFlowClient) ShowModel(cmd *Command) (ResponseIf, error) {
}
// readPassword reads password from terminal without echoing
func readPassword() (string, error) {
func ReadPassword() (string, error) {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return readPasswordFallback()
return ReadPasswordFallback()
}
fmt.Print("Password: ")
@@ -391,7 +391,7 @@ func readPassword() (string, error) {
}
// readPasswordFallback reads password as plain text (fallback mode)
func readPasswordFallback() (string, error) {
func ReadPasswordFallback() (string, error) {
fmt.Print("Password (will be visible): ")
reader := bufio.NewReader(os.Stdin)
password, err := reader.ReadString('\n')

View File

@@ -0,0 +1,135 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
"fmt"
)
func (c *RAGFlowClient) ContextList(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var path string
var ok bool
if cmd.Params["path"] != nil {
path, ok = cmd.Params["path"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'path' to string")
}
}
if path == "" {
path = "."
}
var parameter string
if cmd.Params["parameter"] != nil {
parameter, ok = cmd.Params["parameter"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'parameter' to string")
}
}
if parameter == "" {
fmt.Printf("ls %s\n", path)
} else {
fmt.Printf("ls %s -%s\n", path, parameter)
}
// Convert to response
var response ContextListResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Data = nil
return &response, nil
}
func (c *RAGFlowClient) ContextCat(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
path, ok := cmd.Params["filename"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'filename' to string")
}
fmt.Printf("cat %s\n", path)
// Convert to response
var response ContextListResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Data = nil
return &response, nil
}
func (c *RAGFlowClient) ContextSearch(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
path, ok := cmd.Params["path"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'path' to string")
}
query, ok := cmd.Params["query"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'parameter' to float64")
}
number := 10
if cmd.Params["number"] != nil {
number, ok = cmd.Params["number"].(int)
if !ok {
return nil, fmt.Errorf("fail to convert 'number' to int")
}
}
threshold := 0.0
if cmd.Params["threshold"] != nil {
threshold, ok = cmd.Params["threshold"].(float64)
if !ok {
return nil, fmt.Errorf("fail to convert 'threshold' to float64")
}
}
fmt.Printf("search query: %s, path: %s, threshold: %f, number: %d\n", query, path, threshold, number)
// Convert to response
var response ContextSearchResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Total = 0
response.Data = nil
return &response, nil
}

View File

@@ -72,6 +72,12 @@ func (l *Lexer) NextToken() Token {
case ',':
tok = newToken(TokenComma, l.ch)
l.readChar()
case '/':
tok = newToken(TokenSlash, l.ch)
l.readChar()
case '-':
tok = newToken(TokenDash, l.ch)
l.readChar()
case '\'':
tok.Type = TokenQuotedString
tok.Value = l.readQuotedString('\'')
@@ -93,10 +99,10 @@ func (l *Lexer) NextToken() Token {
tok.Type = TokenNumber
tok.Value = l.readNumber()
return tok
} else {
tok = newToken(TokenIllegal, l.ch)
l.readChar()
}
tok = newToken(TokenIllegal, l.ch)
l.readChar()
}
return tok
@@ -257,6 +263,10 @@ func (l *Lexer) lookupIdent(ident string) Token {
return Token{Type: TokenChat, Value: ident}
case "THINK":
return Token{Type: TokenThink, Value: ident}
case "LS":
return Token{Type: TokenLS, Value: ident}
case "CAT":
return Token{Type: TokenCat, Value: ident}
case "FILES":
return Token{Type: TokenFiles, Value: ident}
case "AS":

View File

@@ -18,6 +18,7 @@ package cli
import (
"fmt"
"math"
"strconv"
"strings"
)
@@ -56,12 +57,11 @@ func (p *Parser) Parse(adminCommand bool) (*Command, error) {
}
// Check for ContextEngine commands (ls, cat, search)
if p.curToken.Type == TokenIdentifier && isCECommand(p.curToken.Value) {
return p.parseCECommand()
}
//if p.curToken.Type == TokenIdentifier && isCECommand(p.curToken.Value) {
// return p.parseCECommand()
//}
// Parse SQL-like command
return p.parseSQLCommand(adminCommand)
return p.parseCommand(adminCommand)
}
func (p *Parser) parseMetaCommand() (*Command, error) {
@@ -194,6 +194,10 @@ func (p *Parser) parseUserCommand() (*Command, error) {
return p.parseChatCommand()
case TokenThink:
return p.parseThinkCommand()
case TokenLS:
return p.parseContextListCommand()
case TokenCat:
return p.parseContextCatCommand()
case TokenUse:
return p.parseUseCommand()
case TokenUpdate:
@@ -205,7 +209,7 @@ func (p *Parser) parseUserCommand() (*Command, error) {
}
}
func (p *Parser) parseSQLCommand(adminCommand bool) (*Command, error) {
func (p *Parser) parseCommand(adminCommand bool) (*Command, error) {
if p.curToken.Type != TokenIdentifier && !isKeyword(p.curToken.Type) {
return nil, fmt.Errorf("expected command, got %s", p.curToken.Value)
}
@@ -272,6 +276,18 @@ func (p *Parser) parseNumber() (int, error) {
return strconv.Atoi(p.curToken.Value)
}
func (p *Parser) parseFloat() (float64, error) {
if p.curToken.Type != TokenNumber {
return math.NaN(), fmt.Errorf("expected number, got %s", p.curToken.Value)
}
result, err := strconv.ParseFloat(p.curToken.Value, 64)
if err != nil {
return math.NaN(), err
}
return result, nil
}
func tokenTypeToString(t int) string {
// Simplified for error messages
return fmt.Sprintf("token(%d)", t)

View File

@@ -30,7 +30,7 @@ type CommonResponse struct {
Data []map[string]interface{} `json:"data"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *CommonResponse) Type() string {
@@ -42,12 +42,12 @@ func (r *CommonResponse) TimeCost() float64 {
}
func (r *CommonResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *CommonResponse) PrintOut() {
if r.Code == 0 {
PrintTableSimpleByFormat(r.Data, r.outputFormat)
PrintTableSimpleByFormat(r.Data, r.OutputFormat)
} else {
fmt.Println("ERROR")
fmt.Printf("%d, %s\n", r.Code, r.Message)
@@ -59,7 +59,7 @@ type CommonDataResponse struct {
Data map[string]interface{} `json:"data"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *CommonDataResponse) Type() string {
@@ -71,14 +71,14 @@ func (r *CommonDataResponse) TimeCost() float64 {
}
func (r *CommonDataResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *CommonDataResponse) PrintOut() {
if r.Code == 0 {
table := make([]map[string]interface{}, 0)
table = append(table, r.Data)
PrintTableSimpleByFormat(table, r.outputFormat)
PrintTableSimpleByFormat(table, r.OutputFormat)
} else {
fmt.Println("ERROR")
fmt.Printf("%d, %s\n", r.Code, r.Message)
@@ -89,7 +89,7 @@ type SimpleResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *SimpleResponse) Type() string {
@@ -101,7 +101,7 @@ func (r *SimpleResponse) TimeCost() float64 {
}
func (r *SimpleResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *SimpleResponse) PrintOut() {
@@ -117,7 +117,7 @@ type MessageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *MessageResponse) Type() string {
@@ -129,7 +129,7 @@ func (r *MessageResponse) TimeCost() float64 {
}
func (r *MessageResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *MessageResponse) PrintOut() {
@@ -145,7 +145,7 @@ type StreamMessageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *StreamMessageResponse) Type() string {
@@ -157,7 +157,7 @@ func (r *StreamMessageResponse) TimeCost() float64 {
}
func (r *StreamMessageResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *StreamMessageResponse) PrintOut() {
@@ -171,7 +171,7 @@ type RegisterResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *RegisterResponse) Type() string {
@@ -183,7 +183,7 @@ func (r *RegisterResponse) TimeCost() float64 {
}
func (r *RegisterResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *RegisterResponse) PrintOut() {
@@ -201,7 +201,7 @@ type BenchmarkResponse struct {
SuccessCount int `json:"success_count"`
FailureCount int `json:"failure_count"`
Concurrency int
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *BenchmarkResponse) Type() string {
@@ -209,7 +209,7 @@ func (r *BenchmarkResponse) Type() string {
}
func (r *BenchmarkResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *BenchmarkResponse) PrintOut() {
@@ -239,7 +239,7 @@ type KeyValueResponse struct {
Key string `json:"key"`
Value string `json:"data"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *KeyValueResponse) Type() string {
@@ -251,7 +251,7 @@ func (r *KeyValueResponse) TimeCost() float64 {
}
func (r *KeyValueResponse) SetOutputFormat(format OutputFormat) {
r.outputFormat = format
r.OutputFormat = format
}
func (r *KeyValueResponse) PrintOut() {
@@ -262,7 +262,7 @@ func (r *KeyValueResponse) PrintOut() {
"key": r.Key,
"value": r.Value,
})
PrintTableSimpleByFormat(table, r.outputFormat)
PrintTableSimpleByFormat(table, r.OutputFormat)
} else {
fmt.Println("ERROR")
fmt.Printf("%d\n", r.Code)
@@ -271,44 +271,44 @@ func (r *KeyValueResponse) PrintOut() {
// ==================== ContextEngine Commands ====================
// CEListResponse represents the response for ls command
type CEListResponse struct {
// ContextListResponse represents the response for ls command
type ContextListResponse struct {
Code int `json:"code"`
Data []map[string]interface{} `json:"data"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *CEListResponse) Type() string { return "ce_ls" }
func (r *CEListResponse) TimeCost() float64 { return r.Duration }
func (r *CEListResponse) SetOutputFormat(format OutputFormat) { r.outputFormat = format }
func (r *CEListResponse) PrintOut() {
func (r *ContextListResponse) Type() string { return "ce_ls" }
func (r *ContextListResponse) TimeCost() float64 { return r.Duration }
func (r *ContextListResponse) SetOutputFormat(format OutputFormat) { r.OutputFormat = format }
func (r *ContextListResponse) PrintOut() {
if r.Code == 0 {
PrintTableSimpleByFormat(r.Data, r.outputFormat)
PrintTableSimpleByFormat(r.Data, r.OutputFormat)
} else {
fmt.Println("ERROR")
fmt.Printf("%d, %s\n", r.Code, r.Message)
}
}
// CESearchResponse represents the response for search command
type CESearchResponse struct {
// ContextSearchResponse represents the response for search command
type ContextSearchResponse struct {
Code int `json:"code"`
Data []map[string]interface{} `json:"data"`
Total int `json:"total"`
Message string `json:"message"`
Duration float64
outputFormat OutputFormat
OutputFormat OutputFormat
}
func (r *CESearchResponse) Type() string { return "ce_search" }
func (r *CESearchResponse) TimeCost() float64 { return r.Duration }
func (r *CESearchResponse) SetOutputFormat(format OutputFormat) { r.outputFormat = format }
func (r *CESearchResponse) PrintOut() {
func (r *ContextSearchResponse) Type() string { return "ce_search" }
func (r *ContextSearchResponse) TimeCost() float64 { return r.Duration }
func (r *ContextSearchResponse) SetOutputFormat(format OutputFormat) { r.OutputFormat = format }
func (r *ContextSearchResponse) PrintOut() {
if r.Code == 0 {
fmt.Printf("Found %d results:\n", r.Total)
PrintTableSimpleByFormat(r.Data, r.outputFormat)
PrintTableSimpleByFormat(r.Data, r.OutputFormat)
} else {
fmt.Println("ERROR")
fmt.Printf("%d, %s\n", r.Code, r.Message)

View File

@@ -112,6 +112,8 @@ const (
TokenEnable
TokenUse
TokenThink
TokenLS
TokenCat
TokenInsert
TokenFile
TokenMetadata
@@ -129,7 +131,9 @@ const (
// Special
TokenSemicolon
TokenComma
TokenSlash
TokenEOF
TokenDash
TokenIllegal
)

View File

@@ -1346,8 +1346,8 @@ func (c *RAGFlowClient) CEList(cmd *Command) (ResponseIf, error) {
}
// Convert to response
var response CEListResponse
response.outputFormat = c.OutputFormat
var response ContextListResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Data = ce.FormatNodes(result.Nodes, string(c.OutputFormat))
@@ -1385,8 +1385,8 @@ func (c *RAGFlowClient) CESearch(cmd *Command) (ResponseIf, error) {
}
// Convert to response
var response CESearchResponse
response.outputFormat = c.OutputFormat
var response ContextSearchResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Total = result.Total
response.Data = ce.FormatNodes(result.Nodes, string(c.OutputFormat))

View File

@@ -1863,30 +1863,88 @@ func (p *Parser) parseSearchCommand() (*Command, error) {
if err != nil {
return nil, err
}
p.nextToken()
if p.curToken.Type != TokenOn {
return nil, fmt.Errorf("expected ON")
}
p.nextToken()
if p.curToken.Type != TokenDatasets {
return nil, fmt.Errorf("expected DATASETS")
}
p.nextToken()
datasets, err := p.parseQuotedString()
if err != nil {
return nil, err
}
if p.curToken.Type == TokenOn {
p.nextToken() // skip on
cmd := NewCommand("search_on_datasets")
cmd.Params["question"] = question
cmd.Params["datasets"] = datasets
p.nextToken()
// Semicolon is optional for UNSET TOKEN
if p.curToken.Type == TokenSemicolon {
if p.curToken.Type != TokenDatasets {
return nil, fmt.Errorf("expected DATASETS")
}
p.nextToken()
datasets, err := p.parseQuotedString()
if err != nil {
return nil, err
}
cmd := NewCommand("search_on_datasets")
cmd.Params["question"] = question
cmd.Params["datasets"] = datasets
p.nextToken()
// Semicolon is optional for UNSET TOKEN
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
return cmd, nil
}
cmd := NewCommand("context_search")
cmd.Params["query"] = question
if p.curToken.Type == TokenEOF {
cmd.Params["path"] = "."
return cmd, nil
}
for p.curToken.Type != TokenEOF {
if p.curToken.Type == TokenDash {
p.nextToken() // skip dash
if p.curToken.Type != TokenIdentifier {
return nil, fmt.Errorf("expect identifier")
}
if strings.ToLower(p.curToken.Value) == "n" {
p.nextToken()
var err error
if p.curToken.Type != TokenNumber {
return nil, fmt.Errorf("expect number")
}
cmd.Params["number"], err = p.parseNumber()
if err != nil {
return nil, err
}
p.nextToken()
continue
}
if strings.ToLower(p.curToken.Value) == "t" {
p.nextToken()
var err error
if p.curToken.Type != TokenNumber {
return nil, fmt.Errorf("expect number")
}
cmd.Params["threshold"], err = p.parseFloat()
if err != nil {
return nil, err
}
p.nextToken()
continue
}
return nil, fmt.Errorf("unknow parameter: %s", p.curToken.Value)
} else if p.curToken.Type == TokenIdentifier {
if cmd.Params["path"] == nil {
cmd.Params["path"] = p.curToken.Value
} else {
cmd.Params["path"] = fmt.Sprintf("%s %s", cmd.Params["path"], p.curToken.Value)
}
p.nextToken() // skip path
continue
}
return nil, fmt.Errorf("syntax error")
}
return cmd, nil
}