diff options
author | Richard Burke <rich.g.burke@gmail.com> | 2018-12-29 11:15:45 +0000 |
---|---|---|
committer | Richard Burke <rich.g.burke@gmail.com> | 2018-12-29 11:15:45 +0000 |
commit | 9d1119dfdddcbbc18d69056ceba787e61bd269e6 (patch) | |
tree | 5ac66f5c4a87712599ce8e4fc21d95a06594b713 | |
parent | e8a9af6892869ab7f93cd089382dbe3c27dd20d1 (diff) |
Added def command
GRV can now parse command definitions
-rw-r--r-- | cmd/grv/config_command_help.go | 27 | ||||
-rw-r--r-- | cmd/grv/config_parse.go | 156 | ||||
-rw-r--r-- | cmd/grv/config_parse_test.go | 56 | ||||
-rw-r--r-- | doc/documentation.md | 21 |
4 files changed, 232 insertions, 28 deletions
diff --git a/cmd/grv/config_command_help.go b/cmd/grv/config_command_help.go index 3a838ef..5a85087 100644 --- a/cmd/grv/config_command_help.go +++ b/cmd/grv/config_command_help.go @@ -375,3 +375,30 @@ func GenerateHelpCommandHelpSections(config Config) (helpSections []*HelpSection }, } } + +// GenerateDefCommandHelpSections generates help documentation for the def command +func GenerateDefCommandHelpSections(config Config) (helpSections []*HelpSection) { + description := []HelpSectionText{ + {text: "def", themeComponentID: CmpHelpViewSectionSubTitle}, + {}, + {text: "The def command allows a custom GRV command to be defined. It has the form:"}, + {}, + {text: "def NAME {", themeComponentID: CmpHelpViewSectionCodeBlock}, + {text: "\tBODY", themeComponentID: CmpHelpViewSectionCodeBlock}, + {text: "}", themeComponentID: CmpHelpViewSectionCodeBlock}, + {}, + {text: "where NAME is the name of the new command and BODY is a sequence of commands to execute."}, + {text: "For example, to define a command \"maintab\" to open a new tab containing the CommitView for master:"}, + {}, + {text: "def maintab {", themeComponentID: CmpHelpViewSectionCodeBlock}, + {text: "\taddtab Main", themeComponentID: CmpHelpViewSectionCodeBlock}, + {text: "\taddview CommitView master", themeComponentID: CmpHelpViewSectionCodeBlock}, + {text: "}", themeComponentID: CmpHelpViewSectionCodeBlock}, + } + + return []*HelpSection{ + { + description: description, + }, + } +} diff --git a/cmd/grv/config_parse.go b/cmd/grv/config_parse.go index ebd8597..f71e1aa 100644 --- a/cmd/grv/config_parse.go +++ b/cmd/grv/config_parse.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "regexp" slice "github.com/bradfitz/slice" ) @@ -24,8 +25,16 @@ const ( gitCommand = "git" gitInteractiveCommand = "giti" helpCommand = "help" + defCommand = "def" ) +const ( + openingBrace = "{" + closingBrace = "}" +) + +var isIdentifier = regexp.MustCompile(`[[:alnum:]]+`).MatchString + type commandConstructor func(parser *ConfigParser, commandToken *ConfigToken, tokens []*ConfigToken) (ConfigCommand, error) // ConfigCommand represents a config command @@ -124,8 +133,16 @@ type ShellCommand struct { func (shellCommand *ShellCommand) configCommand() {} +// DefCommand represents a function definition command +type DefCommand struct { + commandName string + functionBody string +} + +func (defCommand *DefCommand) configCommand() {} + type commandHelpGenerator func(config Config) []*HelpSection -type commandCustomParser func(parser *ConfigParser, commandDescriptor *commandDescriptor, commandToken *ConfigToken) (command ConfigCommand, eof bool, err error) +type commandCustomParser func(parser *ConfigParser) (tokens []*ConfigToken, err error) type commandDescriptor struct { tokenTypes []ConfigTokenType @@ -202,6 +219,11 @@ var commandDescriptors = map[string]*commandDescriptor{ constructor: helpCommandConstructor, commandHelpGenerator: GenerateHelpCommandHelpSections, }, + defCommand: { + customParser: parseDefCommand, + constructor: defCommandConstructor, + commandHelpGenerator: GenerateDefCommandHelpSections, + }, } // GenerateConfigCommandHelpSections generates help documentation for all configuration commands @@ -313,6 +335,10 @@ func (parser *ConfigParser) scan() (token *ConfigToken, err error) { return } +func (parser *ConfigParser) scanRaw() (token *ConfigToken, err error) { + return parser.scanner.Scan() +} + func (parser *ConfigParser) generateParseError(token *ConfigToken, errorMessage string, args ...interface{}) error { return generateConfigError(parser.inputSource, token, errorMessage, args...) } @@ -355,16 +381,48 @@ func (parser *ConfigParser) parseCommand(commandToken *ConfigToken) (command Con return } + var tokens []*ConfigToken + if commandDescriptor.customParser != nil { - return commandDescriptor.customParser(parser, commandDescriptor, commandToken) + if tokens, err = commandDescriptor.customParser(parser); err != nil { + return + } + } else { + for i := 0; i < len(commandDescriptor.tokenTypes); i++ { + var token *ConfigToken + token, err = parser.scan() + expectedConfigTokenType := commandDescriptor.tokenTypes[i] + + switch { + case err != nil: + return + case token.err != nil: + err = parser.generateParseError(token, "Syntax Error") + return + case token.tokenType == CtkEOF: + err = parser.generateParseError(token, "Unexpected EOF") + eof = true + return + case (token.tokenType & expectedConfigTokenType) == 0: + err = parser.generateParseError(token, "Expected %v but got %v: \"%v\"", + ConfigTokenName(expectedConfigTokenType), ConfigTokenName(token.tokenType), token.value) + return + } + + tokens = append(tokens, token) + } } - var tokens []*ConfigToken + command, err = commandDescriptor.constructor(parser, commandToken, tokens) - for i := 0; i < len(commandDescriptor.tokenTypes); i++ { + return +} + +func parseVarArgsCommand(parser *ConfigParser) (tokens []*ConfigToken, err error) { +OuterLoop: + for { var token *ConfigToken token, err = parser.scan() - expectedConfigTokenType := commandDescriptor.tokenTypes[i] switch { case err != nil: @@ -373,47 +431,66 @@ func (parser *ConfigParser) parseCommand(commandToken *ConfigToken) (command Con err = parser.generateParseError(token, "Syntax Error") return case token.tokenType == CtkEOF: - err = parser.generateParseError(token, "Unexpected EOF") - eof = true - return - case (token.tokenType & expectedConfigTokenType) == 0: - err = parser.generateParseError(token, "Expected %v but got %v: \"%v\"", - ConfigTokenName(expectedConfigTokenType), ConfigTokenName(token.tokenType), token.value) - return + break OuterLoop + case token.tokenType == CtkTerminator: + break OuterLoop } tokens = append(tokens, token) } - command, err = commandDescriptor.constructor(parser, commandToken, tokens) - return } -func parseVarArgsCommand(parser *ConfigParser, commandDescriptor *commandDescriptor, commandToken *ConfigToken) (command ConfigCommand, eof bool, err error) { - var tokens []*ConfigToken +func parseDefCommand(parser *ConfigParser) (tokens []*ConfigToken, err error) { + commandNameToken, err := parser.scan() + if err != nil { + return + } else if commandNameToken.err != nil { + err = commandNameToken.err + return + } else if commandNameToken.tokenType != CtkWord { + err = parser.generateParseError(commandNameToken, "Expected function name but found %v", commandNameToken.value) + return + } else if !isIdentifier(commandNameToken.value) { + err = parser.generateParseError(commandNameToken, "Invalid function identifier %v", commandNameToken.value) + return + } + + tokens = append(tokens, commandNameToken) + + openingBraceToken, err := parser.scan() + if err != nil { + return + } else if openingBraceToken.err != nil { + err = openingBraceToken.err + return + } else if openingBraceToken.tokenType != CtkWord || openingBraceToken.value != openingBrace { + err = parser.generateParseError(openingBraceToken, "Expected %v but found %v", openingBrace, openingBraceToken.value) + return + } + + tokens = append(tokens, openingBraceToken) -OuterLoop: for { var token *ConfigToken - token, err = parser.scan() - - switch { - case err != nil: + if token, err = parser.scanRaw(); err != nil { return - case token.err != nil: - err = parser.generateParseError(token, "Syntax Error") + } else if token.err != nil { + err = token.err return - case token.tokenType == CtkEOF: - break OuterLoop - case token.tokenType == CtkTerminator: - break OuterLoop } tokens = append(tokens, token) + + if token.tokenType == CtkWord && token.value == closingBrace { + break + } else if token.tokenType == CtkEOF { + err = parser.generateParseError(token, "Expected %v but reached EOF", closingBrace) + return + } } - command, err = commandDescriptor.constructor(parser, commandToken, tokens) return } @@ -533,3 +610,26 @@ func gitCommandConstructor(parser *ConfigParser, commandToken *ConfigToken, toke func helpCommandConstructor(parser *ConfigParser, commandToken *ConfigToken, tokens []*ConfigToken) (ConfigCommand, error) { return &HelpCommand{}, nil } + +func defCommandConstructor(parser *ConfigParser, commandToken *ConfigToken, tokens []*ConfigToken) (configCommand ConfigCommand, err error) { + if len(tokens) < 4 { + err = parser.generateParseError(commandToken, "Too few tokens (%v) for function definition", len(tokens)) + return + } + + commandName := tokens[0].value + var functionBodyBuffer bytes.Buffer + + for i := 2; i < len(tokens)-1; i++ { + functionBodyBuffer.WriteString(tokens[i].value) + } + + functionBody := functionBodyBuffer.String() + + configCommand = &DefCommand{ + commandName: commandName, + functionBody: functionBody, + } + + return +} diff --git a/cmd/grv/config_parse_test.go b/cmd/grv/config_parse_test.go index d4c75c8..22e2338 100644 --- a/cmd/grv/config_parse_test.go +++ b/cmd/grv/config_parse_test.go @@ -254,6 +254,25 @@ func (shellCommandValues *ShellCommandValues) Equal(command ConfigCommand) bool return shellCommandValues.command == other.command.value } +type DefCommandValues struct { + commandName string + functionBody string +} + +func (defCommandValues *DefCommandValues) Equal(command ConfigCommand) bool { + if command == nil { + return false + } + + other, ok := command.(*DefCommand) + if !ok { + return false + } + + return defCommandValues.commandName == other.commandName && + defCommandValues.functionBody == other.functionBody +} + func TestParseSingleCommand(t *testing.T) { var singleCommandTests = []struct { input string @@ -359,6 +378,27 @@ func TestParseSingleCommand(t *testing.T) { command: "!git add -A", }, }, + { + input: "def myFunc { addview RefView }", + expectedCommand: &DefCommandValues{ + commandName: "myFunc", + functionBody: " addview RefView ", + }, + }, + { + input: "def myFunc {\n\taddview CommitView master\n\taddview RefView\n}", + expectedCommand: &DefCommandValues{ + commandName: "myFunc", + functionBody: "\n\taddview CommitView master\n\taddview RefView\n", + }, + }, + { + input: "def nop { }", + expectedCommand: &DefCommandValues{ + commandName: "nop", + functionBody: " ", + }, + }, } for _, singleCommandTest := range singleCommandTests { @@ -495,6 +535,22 @@ func TestErrorsAreReceivedForInvalidConfigTokenSequences(t *testing.T) { input: "addtab", expectedErrorMessage: ConfigFile + ":1:6 Unexpected EOF", }, + { + input: "def --name", + expectedErrorMessage: ConfigFile + ":1:5 Expected function name but found --name", + }, + { + input: "def {", + expectedErrorMessage: ConfigFile + ":1:5 Invalid function identifier {", + }, + { + input: "def myfunc (", + expectedErrorMessage: ConfigFile + ":1:12 Expected { but found (", + }, + { + input: "def myfunc { addview RefView ", + expectedErrorMessage: ConfigFile + ":1:29 Expected } but reached EOF", + }, } for _, errorTest := range errorTests { diff --git a/doc/documentation.md b/doc/documentation.md index 97f85ec..b0d07f5 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -27,6 +27,7 @@ The sections below provide an overview of the ways to configure and interact wit - [Configuration Commands](#configuration-commands) * [addtab](#addtab) * [addview](#addview) + * [def](#def) * [git](#git) * [giti](#giti) * [help](#help) @@ -272,6 +273,26 @@ addview GitStatusView addview RefView ``` +### def + +The def command allows a custom GRV command to be defined. It has the form: + +``` +def NAME { + BODY +} +``` + +where NAME is the name of the new command and BODY is a sequence of commands to execute. +For example, to define a command "maintab" to open a new tab containing the CommitView for master: + +``` +def maintab { + addtab Main + addview CommitView master +} +``` + ### git The git command is an alias to the git cli command. |