feat: implement initial terminal command parsing

Signed-off-by: cpendery <cpendery@vt.edu>
This commit is contained in:
cpendery
2023-09-10 17:41:57 -04:00
parent 46a0a51404
commit 4cd3cc1225
9 changed files with 247 additions and 7 deletions
+17 -5
View File
@@ -1,17 +1,29 @@
package autocomplete
import "strings"
import (
"log"
"regexp"
"github.com/cpendery/clac/autocomplete/specs"
)
type Suggestion struct {
Name string
Description string
}
var (
cmdDelimiter = regexp.MustCompile(`(\|\|)|(&&)|(;)`)
)
func LoadSuggestions(cmd string) []Suggestion {
params := strings.Split(cmd, " ")
lastParam := params[len(params)-1]
if lastParam == "git" {
return []Suggestion{{"go", "the only way to go"}, {"get", "the right way to get"}, {"gone", "here today, bye tomorrow"}}
activeCmd := ParseCommand(cmd)
if len(activeCmd) <= 0 {
return []Suggestion{}
}
log.Println(activeCmd, activeCmd[0].token)
if spec, ok := specs.Specs[activeCmd[0].token]; ok {
log.Println(spec.Description)
}
return []Suggestion{}
}
+64 -2
View File
@@ -2,6 +2,7 @@ import * as fsAsync from "fs/promises";
import * as fs from "fs";
import * as path from "path";
import * as process from "process";
import * as child_process from "child_process";
const main = async () => {
const basePath = path.join(process.cwd(), ".fig");
@@ -28,12 +29,73 @@ const main = async () => {
return;
}
const filenameWithoutExtension = path.parse(directoryItem.name).name;
const subcommand = spec as unknown as Fig.Subcommand;
console.log(subcommand?.description);
const generatedCode = generateGolang(
subcommand,
filenameWithoutExtension
);
await fsAsync.writeFile(
path.join(
process.cwd(),
"..",
"specs",
`${filenameWithoutExtension}.go`
),
generatedCode
);
})
);
const generatedFilesPath = path.join(process.cwd(), "..", "specs");
child_process.exec(`gofmt -w ${generatedFilesPath}`);
};
const generateGolang = (subcommand: Fig.Subcommand) => {};
const generateArgs = (args: Fig.SingleOrArray<Fig.Arg>): string => {
return "";
};
const generateOptions = (options: Fig.Option[]): string => {
return "";
};
const generateSubcommand = (subcommand: Fig.Subcommand): string => {
const name = Array.isArray(subcommand.name)
? `Name: []string{${subcommand.name.map((n) => `"${n}"`).join(",")}},`
: `Name: "${subcommand.name}",`;
const description =
subcommand.description != null
? `Description: "${subcommand.description}",`
: "";
const args = subcommand.args != null ? generateArgs(subcommand.args) : "";
const options =
subcommand.options != null ? generateOptions(subcommand.options) : "";
return `model.Subcommand{
${name}
${description}
${args}
${options}
}`.replaceAll(/\s/g, "");
};
const generateGolang = (
subcommand: Fig.Subcommand,
filenameWithoutExtension: string
) => {
return `// Code generated by autocomplete/extract/extract.ts. DO NOT EDIT.
package specs
import (
"github.com/cpendery/clac/autocomplete/model"
)
func init() {
Specs["${filenameWithoutExtension}"] = ${generateSubcommand(subcommand)}
}`;
};
main();
+32
View File
@@ -0,0 +1,32 @@
package model
type Subcommand struct {
Name interface{} //single or array string, required
Description string
Args interface{} //single or array Arg, optional
Options []Option
}
type Option struct {
Name interface{} //single or array string, required
Args interface{} //single or array Arg, optional
Description string //single, optional
}
type Arg struct {
Name string //single, optional
Description string //single, optional
Template Template //single, optional
}
type Template string
const (
TemplateFilepaths Template = "filepaths"
TemplateFolders Template = "folders"
TemplateHistory Template = "history"
)
var (
Templates = []Template{TemplateFilepaths, TemplateFolders, TemplateHistory}
)
+62
View File
@@ -0,0 +1,62 @@
package autocomplete
import (
"regexp"
"strings"
"unicode"
)
var (
quoteRegex = regexp.MustCompile(`['"]`)
)
type commandToken struct {
token string
complete bool
}
func ParseCommand(cmd string) []commandToken {
commands := cmdDelimiter.Split(cmd, -1)
command := commands[len(commands)-1]
cleanCommand := strings.TrimLeftFunc(command, unicode.IsSpace)
return parse([]rune(cleanCommand))
}
func parse(cmd []rune) []commandToken {
results := []commandToken{}
readingQuotedStr := false
readingFlag := false
readingCmd := false
readingIdx := 0
var reading bool
for idx, r := range cmd {
reading = readingQuotedStr || readingFlag || readingCmd
if !reading && quoteRegex.MatchString(string(r)) {
readingQuotedStr, readingIdx = true, idx
continue
} else if !reading && r == '-' {
readingFlag, readingIdx = true, idx
continue
} else if !reading && !unicode.IsSpace(r) {
readingCmd, readingIdx = true, idx
continue
}
if readingQuotedStr && quoteRegex.MatchString(string(r)) {
readingQuotedStr = false
complete := idx+1 < len(cmd) && unicode.IsSpace(cmd[idx+1])
results = append(results, commandToken{complete: complete, token: string(cmd[readingIdx : idx+1])})
} else if readingFlag && (unicode.IsSpace(r) || r == '=') {
readingFlag = false
results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx])})
} else if readingCmd && unicode.IsSpace(r) {
readingCmd = false
results = append(results, commandToken{complete: true, token: string(cmd[readingIdx:idx])})
}
}
reading = readingQuotedStr || readingFlag || readingCmd
if reading {
results = append(results, commandToken{complete: false, token: string(cmd[readingIdx:])})
}
return results
}
+40
View File
@@ -0,0 +1,40 @@
package autocomplete
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseCommand(t *testing.T) {
tests := []struct {
cmd string
expected []commandToken
}{
{"cmd --flag value ", []commandToken{{"cmd", true}, {"--flag", true}, {"value", true}}},
{"cmd --flag 'value' ", []commandToken{{"cmd", true}, {"--flag", true}, {"'value'", true}}},
{"cmd --flag=value ", []commandToken{{"cmd", true}, {"--flag", true}, {"value", true}}},
{"cmd --flag='value' ", []commandToken{{"cmd", true}, {"--flag", true}, {"'value'", true}}},
{"cmd 'value' ", []commandToken{{"cmd", true}, {"'value'", true}}},
{"cmd value ", []commandToken{{"cmd", true}, {"value", true}}},
{"cmd -f", []commandToken{{"cmd", true}, {"-f", false}}},
{"cmd -f=value ", []commandToken{{"cmd", true}, {"-f", true}, {"value", true}}},
{"cmd -f value ", []commandToken{{"cmd", true}, {"-f", true}, {"value", true}}},
{"cmd -f 'value' ", []commandToken{{"cmd", true}, {"-f", true}, {"'value'", true}}},
{"cmd -f='value' ", []commandToken{{"cmd", true}, {"-f", true}, {"'value'", true}}},
{"cmd -f='val", []commandToken{{"cmd", true}, {"-f", true}, {"'val", false}}},
{"cmd -f", []commandToken{{"cmd", true}, {"-f", false}}},
{"cmd -f=", []commandToken{{"cmd", true}, {"-f", true}}},
{"cmd -f ", []commandToken{{"cmd", true}, {"-f", true}}},
{"cmd", []commandToken{{"cmd", false}}},
{"cmd ", []commandToken{{"cmd", true}}},
}
for _, test := range tests {
t.Run(test.cmd, func(tc *testing.T) {
result := ParseCommand(test.cmd)
assert.Equal(tc, test.expected, result)
})
}
}
+11
View File
@@ -0,0 +1,11 @@
// Code generated by autocomplete/extract/extract.ts. DO NOT EDIT.
package specs
import (
"github.com/cpendery/clac/autocomplete/model"
)
func init() {
Specs["afplay"] = model.Subcommand{Name: "afplay", Description: "AudioFilePlay"}
}
+7
View File
@@ -0,0 +1,7 @@
package specs
import "github.com/cpendery/clac/autocomplete/model"
var (
Specs = make(map[string]model.Subcommand)
)
+4
View File
@@ -8,6 +8,7 @@ require (
github.com/charmbracelet/lipgloss v0.8.0
github.com/mattn/go-runewidth v0.0.14
github.com/muesli/reflow v0.3.0
github.com/stretchr/testify v1.8.4
golang.org/x/term v0.6.0
)
@@ -15,14 +16,17 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.3.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+10
View File
@@ -10,6 +10,8 @@ github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdy
github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
@@ -27,9 +29,13 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -40,3 +46,7 @@ golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=