-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b3aebc1
commit 9fd78b4
Showing
8 changed files
with
628 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# This is a basic workflow to help you get started with Actions | ||
|
||
name: Build | ||
|
||
# Controls when the action will run. Triggers the workflow on push or pull request | ||
on: [push, pull_request] | ||
|
||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel | ||
jobs: | ||
# The "build" workflow | ||
build: | ||
# The type of runner that the job will run on | ||
runs-on: ubuntu-latest | ||
|
||
# Steps represent a sequence of tasks that will be executed as part of the job | ||
steps: | ||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it | ||
- uses: actions/checkout@v2 | ||
|
||
# Setup Go | ||
- name: Setup Go | ||
uses: actions/setup-go@v2 | ||
with: | ||
go-version: '1.21' | ||
|
||
# Run build of the application | ||
- name: Run build | ||
run: go build ./... | ||
|
||
# Run testing on the code | ||
- name: Run testing | ||
run: go test -v ./... | ||
|
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,91 @@ | ||
# commander [![Build Status](https://github.com/slack-io/commander/actions/workflows/ci.yml/badge.svg)](https://github.com/slack-io/commander/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/slack-io/commander)](https://goreportcard.com/report/github.com/slack-io/commander) [![GoDoc](https://godoc.org/github.com/slack-io/commander?status.svg)](https://godoc.org/github.com/slack-io/commander) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | ||
|
||
Command evaluator and parser | ||
|
||
## Features | ||
|
||
* Matches commands against provided text | ||
* Extracts parameters from matching input | ||
* Provides default values for missing parameters | ||
* Supports String, Integer, Float and Boolean parameters | ||
* Supports "word" {} vs "sentence" <> parameter matching | ||
|
||
## Dependencies | ||
|
||
* `proper` [github.com/slack-io/proper](https://github.com/slack-io/proper) | ||
|
||
|
||
# Examples | ||
|
||
## Example 1 | ||
|
||
In this example, we are matching a few strings against a command format, then parsing parameters if found or returning default values. | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"github.com/slack-io/commander" | ||
) | ||
|
||
func main() { | ||
properties, isMatch := commander.NewCommand("ping").Match("ping") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties) // {} | ||
|
||
properties, isMatch = commander.NewCommand("ping").Match("pong") | ||
fmt.Println(isMatch) // false | ||
fmt.Println(properties) // nil | ||
|
||
properties, isMatch = commander.NewCommand("echo {word}").Match("echo hello world!") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties.StringParam("word", "")) // hello | ||
|
||
properties, isMatch = commander.NewCommand("echo <sentence>").Match("echo hello world!") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties.StringParam("sentence", "")) // hello world! | ||
|
||
properties, isMatch = commander.NewCommand("repeat {word} {number}").Match("repeat hey 5") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties.StringParam("word", "")) // hey | ||
fmt.Println(properties.IntegerParam("number", 0)) // 5 | ||
|
||
properties, isMatch = commander.NewCommand("repeat {word} {number}").Match("repeat hey") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties.StringParam("word", "")) // hey | ||
fmt.Println(properties.IntegerParam("number", 0)) // 0 | ||
|
||
properties, isMatch = commander.NewCommand("search <stuff> {size}").Match("search hello there everyone 10") | ||
fmt.Println(isMatch) // true | ||
fmt.Println(properties.StringParam("stuff", "")) // hello there everyone | ||
fmt.Println(properties.IntegerParam("size", 0)) // 10 | ||
} | ||
``` | ||
|
||
## Example 2 | ||
|
||
In this example, we are tokenizing the command format and returning each token with a number that determines whether it is a parameter (word vs sentence) or not | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"github.com/slack-io/commander" | ||
) | ||
|
||
func main() { | ||
tokens := commander.NewCommand("echo {word} <sentence>").Tokenize() | ||
for _, token := range tokens { | ||
fmt.Println(token) | ||
} | ||
} | ||
``` | ||
|
||
Output: | ||
``` | ||
&{echo NOT_PARAMETER} | ||
&{word WORD_PARAMETER} | ||
&{sentence SENTENCE_PARAMETER} | ||
``` |
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,165 @@ | ||
package commander | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/slack-io/proper" | ||
) | ||
|
||
const ( | ||
escapeCharacter = "\\" | ||
ignoreCase = "(?i)" | ||
wordParameterPattern = "{\\S+}" | ||
sentenceParameterPattern = "<\\S+>" | ||
spacePattern = "\\s+" | ||
wordInputPattern = "(.+?)" | ||
sentenceInputPattern = "(.+)" | ||
preCommandPattern = "(\\s|^)" | ||
postCommandPattern = "(\\s|$)" | ||
) | ||
|
||
const ( | ||
notParameter = "NOT_PARAMETER" | ||
wordParameter = "WORD_PARAMETER" | ||
sentenceParameter = "SENTENCE_PARAMETER" | ||
) | ||
|
||
var ( | ||
regexCharacters = []string{"\\", "(", ")", "{", "}", "[", "]", "?", ".", "+", "|", "^", "$"} | ||
) | ||
|
||
// NewCommand creates a new Command object from the format passed in | ||
func NewCommand(format string) *Command { | ||
tokens := tokenize(format) | ||
expressions := generate(tokens) | ||
return &Command{tokens: tokens, expressions: expressions} | ||
} | ||
|
||
// Token represents the Token object | ||
type Token struct { | ||
Word string | ||
Type string | ||
} | ||
|
||
func (t Token) IsParameter() bool { | ||
return t.Type != notParameter | ||
} | ||
|
||
// Command represents the Command object | ||
type Command struct { | ||
tokens []*Token | ||
expressions []*regexp.Regexp | ||
} | ||
|
||
// Match takes in the command and the text received, attempts to find the pattern and extract the parameters | ||
func (c *Command) Match(text string) (*proper.Properties, bool) { | ||
if len(c.expressions) == 0 { | ||
return nil, false | ||
} | ||
|
||
for _, expression := range c.expressions { | ||
matches := expression.FindStringSubmatch(text) | ||
if len(matches) == 0 { | ||
continue | ||
} | ||
|
||
values := matches[2 : len(matches)-1] | ||
|
||
valueIndex := 0 | ||
parameters := make(map[string]string) | ||
for i := 0; i < len(c.tokens) && valueIndex < len(values); i++ { | ||
token := c.tokens[i] | ||
if !token.IsParameter() { | ||
continue | ||
} | ||
|
||
parameters[token.Word] = values[valueIndex] | ||
valueIndex++ | ||
} | ||
return proper.NewProperties(parameters), true | ||
} | ||
return nil, false | ||
} | ||
|
||
// Tokenize returns Command info as tokens | ||
func (c *Command) Tokenize() []*Token { | ||
return c.tokens | ||
} | ||
|
||
func escape(text string) string { | ||
for _, character := range regexCharacters { | ||
text = strings.Replace(text, character, escapeCharacter+character, -1) | ||
} | ||
return text | ||
} | ||
|
||
func tokenize(format string) []*Token { | ||
parameterRegex := regexp.MustCompile(sentenceParameterPattern) | ||
lazyParameterRegex := regexp.MustCompile(wordParameterPattern) | ||
words := strings.Fields(format) | ||
tokens := make([]*Token, len(words)) | ||
for i, word := range words { | ||
switch { | ||
case lazyParameterRegex.MatchString(word): | ||
tokens[i] = &Token{Word: word[1 : len(word)-1], Type: wordParameter} | ||
case parameterRegex.MatchString(word): | ||
tokens[i] = &Token{Word: word[1 : len(word)-1], Type: sentenceParameter} | ||
default: | ||
tokens[i] = &Token{Word: word, Type: notParameter} | ||
} | ||
} | ||
return tokens | ||
} | ||
|
||
func generate(tokens []*Token) []*regexp.Regexp { | ||
regexps := []*regexp.Regexp{} | ||
if len(tokens) == 0 { | ||
return regexps | ||
} | ||
|
||
for index := len(tokens) - 1; index >= -1; index-- { | ||
regex := compile(create(tokens, index)) | ||
if regex != nil { | ||
regexps = append(regexps, regex) | ||
} | ||
} | ||
|
||
return regexps | ||
} | ||
|
||
func create(tokens []*Token, boundary int) []*Token { | ||
newTokens := []*Token{} | ||
for i := 0; i < len(tokens); i++ { | ||
if !tokens[i].IsParameter() || i <= boundary { | ||
newTokens = append(newTokens, tokens[i]) | ||
} | ||
} | ||
return newTokens | ||
} | ||
|
||
func compile(tokens []*Token) *regexp.Regexp { | ||
if len(tokens) == 0 { | ||
return nil | ||
} | ||
|
||
pattern := preCommandPattern + getInputPattern(tokens[0]) | ||
for index := 1; index < len(tokens); index++ { | ||
currentToken := tokens[index] | ||
pattern += spacePattern + getInputPattern(currentToken) | ||
} | ||
pattern += postCommandPattern | ||
|
||
return regexp.MustCompile(ignoreCase + pattern) | ||
} | ||
|
||
func getInputPattern(token *Token) string { | ||
switch token.Type { | ||
case wordParameter: | ||
return wordInputPattern | ||
case sentenceParameter: | ||
return sentenceInputPattern | ||
default: | ||
return escape(token.Word) | ||
} | ||
} |
Oops, something went wrong.