From 88f00ca7945181183beecfab3bcec7cfc630c0f3 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Thu, 3 Oct 2024 17:36:10 +0700 Subject: [PATCH 01/19] feat: initial command registration --- cli/cmd/cmds/devx.go | 28 ++++++++++++++++++++++++++++ cli/cmd/main.go | 1 + cli/go.mod | 1 + cli/go.sum | 2 ++ cli/tests/Developer.md | 31 +++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 cli/cmd/cmds/devx.go create mode 100644 cli/tests/Developer.md diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go new file mode 100644 index 00000000..67449dd8 --- /dev/null +++ b/cli/cmd/cmds/devx.go @@ -0,0 +1,28 @@ +package cmds + +import ( + "fmt" + "log/slog" + "os" +) + +type DevX struct { + MarkdownPath string `arg:"" help:"Path to the markdown file."` + CommandName string `arg:"" help:"Command to be executed."` +} + +func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { + fmt.Println("MarkdownPath:", c.MarkdownPath, "CommandName:", c.CommandName) + + // read the file from the specified path + raw, err := os.ReadFile(c.MarkdownPath) + if err != nil { + return fmt.Errorf("could not read file at %s: %v", c.MarkdownPath, err) + } + + content := string(raw) + + fmt.Println(content) + + return nil +} diff --git a/cli/cmd/main.go b/cli/cmd/main.go index 82820c3e..3560b546 100644 --- a/cli/cmd/main.go +++ b/cli/cmd/main.go @@ -19,6 +19,7 @@ var cli struct { cmds.GlobalArgs Dump cmds.DumpCmd `cmd:"" help:"Dumps a project's blueprint to JSON."` + Devx cmds.DevX `cmd:"" help:"Reads a forge markdown file and executes a command."` CI cmds.CICmd `cmd:"" help:"Simulate a CI run."` Run cmds.RunCmd `cmd:"" help:"Run an Earthly target."` Scan cmds.ScanCmd `cmd:"" help:"Scan for Earthfiles."` diff --git a/cli/go.mod b/cli/go.mod index 9e7e3409..78717bb1 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -71,6 +71,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/yuin/goldmark v1.7.4 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 81e04176..36cc568b 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -185,6 +185,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/cli/tests/Developer.md b/cli/tests/Developer.md new file mode 100644 index 00000000..3517ba16 --- /dev/null +++ b/cli/tests/Developer.md @@ -0,0 +1,31 @@ +# My cool dev docs + +## Run Some Command !! + +''' sh + echo "the fallback, if nothing else is defined" +''' + +### Linux + +''' python + print("Cool") +''' + +### Linux:arm64 + +''' sh + echo "Because maybe python is broken??" +''' + +### Windows + +''' powershell + echo "Does this even work in powershell??" +''' + +### Mac:Arm64 + +''' sh + echo "Because its always got to be different, even though its really just BSD Unix" +''' \ No newline at end of file From f0bff4fce85643228a97c6a8efa471013e8b7c2a Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Thu, 3 Oct 2024 20:00:50 +0700 Subject: [PATCH 02/19] feat: extractor function --- cli/cmd/cmds/devx.go | 91 +++++++++++++++++++++++++++++++++++++++++- cli/tests/Developer.md | 20 +++++----- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 67449dd8..44290f50 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -1,9 +1,15 @@ package cmds import ( + "bytes" + "errors" "fmt" "log/slog" "os" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" ) type DevX struct { @@ -11,6 +17,17 @@ type DevX struct { CommandName string `arg:"" help:"Command to be executed."` } +type command struct { + content string + lang *string + platform *string +} + +type commandGroup struct { + name string + commands []command +} + func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { fmt.Println("MarkdownPath:", c.MarkdownPath, "CommandName:", c.CommandName) @@ -20,9 +37,79 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { return fmt.Errorf("could not read file at %s: %v", c.MarkdownPath, err) } - content := string(raw) + // parse the file with prepared options + commandGroups, err := extractCommandGroups(raw) + if err != nil { + return fmt.Errorf("could not extract commands: %v", err) + } - fmt.Println(content) + // Output the command groups + for _, group := range commandGroups { + fmt.Printf("Command Group: %s\n", group.name) + fmt.Printf("Command Count: %v\n", len(group.commands)) + for _, cmd := range group.commands { + fmt.Printf(" Command (lang: %s, platform: %s):\n%s\n", *cmd.lang, *cmd.platform, cmd.content) + } + } return nil } + +func extractCommandGroups(data []byte) ([]commandGroup, error) { + md := goldmark.New() + reader := text.NewReader(data) + doc := md.Parser().Parse(reader) + + // store the command groups and commands + var groups []commandGroup + var currentGroup *commandGroup + var currentPlatform *string + + // walk through the ast nodes + ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if heading, ok := n.(*ast.Heading); ok && entering { + if heading.Level == 2 { + currentPlatform = nil + commandName := string(heading.Text(data)) + + currentGroup = &commandGroup{ + name: commandName, + commands: []command{}, + } + groups = append(groups, *currentGroup) + } + + if heading.Level == 3 && currentGroup != nil { + platform := string(heading.Text(data)) + currentPlatform = &platform + } + } + + if block, ok := n.(*ast.FencedCodeBlock); ok && entering && currentGroup != nil { + lang := string(block.Language(data)) + + var buf bytes.Buffer + for i := 0; i < block.Lines().Len(); i++ { + line := block.Lines().At(i) + buf.Write(line.Value(data)) + } + + currentGroup.commands = append(currentGroup.commands, command{ + content: buf.String(), + lang: &lang, + platform: currentPlatform, + }) + + fmt.Println(len(currentGroup.commands)) + } + + return ast.WalkContinue, nil + }) + + // Check if any groups were found + if len(groups) == 0 { + return nil, errors.New("no command groups found in the markdown") + } + + return groups, nil +} diff --git a/cli/tests/Developer.md b/cli/tests/Developer.md index 3517ba16..415f3b48 100644 --- a/cli/tests/Developer.md +++ b/cli/tests/Developer.md @@ -2,30 +2,30 @@ ## Run Some Command !! -''' sh +``` sh echo "the fallback, if nothing else is defined" -''' +``` ### Linux -''' python +``` python print("Cool") -''' +``` ### Linux:arm64 -''' sh +``` sh echo "Because maybe python is broken??" -''' +``` ### Windows -''' powershell +``` powershell echo "Does this even work in powershell??" -''' +``` ### Mac:Arm64 -''' sh +``` sh echo "Because its always got to be different, even though its really just BSD Unix" -''' \ No newline at end of file +``` \ No newline at end of file From bd7ffe33e5248bb8c15e6dd2aa19900e4710d6cc Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Thu, 3 Oct 2024 20:48:30 +0700 Subject: [PATCH 03/19] feat: complete parsing --- cli/cmd/cmds/devx.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 44290f50..7fb1c21e 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -48,7 +48,15 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { fmt.Printf("Command Group: %s\n", group.name) fmt.Printf("Command Count: %v\n", len(group.commands)) for _, cmd := range group.commands { - fmt.Printf(" Command (lang: %s, platform: %s):\n%s\n", *cmd.lang, *cmd.platform, cmd.content) + fmt.Printf("---\n") + fmt.Printf("Command Content: %v", cmd.content) + + if cmd.lang != nil { + fmt.Printf("Command Lang: %v\n", *cmd.lang) + } + if cmd.platform != nil { + fmt.Printf("Command Platform: %v\n", *cmd.platform) + } } } @@ -61,8 +69,7 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { doc := md.Parser().Parse(reader) // store the command groups and commands - var groups []commandGroup - var currentGroup *commandGroup + groups := []commandGroup{} var currentPlatform *string // walk through the ast nodes @@ -72,20 +79,20 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { currentPlatform = nil commandName := string(heading.Text(data)) - currentGroup = &commandGroup{ + groups = append(groups, commandGroup{ name: commandName, commands: []command{}, - } - groups = append(groups, *currentGroup) + }) } - if heading.Level == 3 && currentGroup != nil { + if heading.Level == 3 && len(groups) > 0 { platform := string(heading.Text(data)) currentPlatform = &platform } } - if block, ok := n.(*ast.FencedCodeBlock); ok && entering && currentGroup != nil { + if block, ok := n.(*ast.FencedCodeBlock); ok && entering && len(groups) > 0 { + i := len(groups) - 1 lang := string(block.Language(data)) var buf bytes.Buffer @@ -94,19 +101,16 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { buf.Write(line.Value(data)) } - currentGroup.commands = append(currentGroup.commands, command{ + groups[i].commands = append(groups[i].commands, command{ content: buf.String(), lang: &lang, platform: currentPlatform, }) - - fmt.Println(len(currentGroup.commands)) } return ast.WalkContinue, nil }) - // Check if any groups were found if len(groups) == 0 { return nil, errors.New("no command groups found in the markdown") } From 4dd7024ed958dee7439c521a2e53d44b733107f8 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Thu, 3 Oct 2024 21:01:59 +0700 Subject: [PATCH 04/19] feat: id formatter --- cli/cmd/cmds/devx.go | 50 ++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 7fb1c21e..28ffde16 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -6,6 +6,9 @@ import ( "fmt" "log/slog" "os" + "regexp" + "strings" + "unicode" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" @@ -17,17 +20,6 @@ type DevX struct { CommandName string `arg:"" help:"Command to be executed."` } -type command struct { - content string - lang *string - platform *string -} - -type commandGroup struct { - name string - commands []command -} - func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { fmt.Println("MarkdownPath:", c.MarkdownPath, "CommandName:", c.CommandName) @@ -45,8 +37,7 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { // Output the command groups for _, group := range commandGroups { - fmt.Printf("Command Group: %s\n", group.name) - fmt.Printf("Command Count: %v\n", len(group.commands)) + fmt.Printf("Command Id: %s (%v)\n", group.GetId(), len(group.commands)) for _, cmd := range group.commands { fmt.Printf("---\n") fmt.Printf("Command Content: %v", cmd.content) @@ -63,6 +54,37 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { return nil } +type command struct { + content string + lang *string + platform *string +} + +type commandGroup struct { + name string + commands []command +} + +func (c *commandGroup) GetId() string { + var result []rune + + for _, char := range c.name { + if unicode.IsLetter(char) || unicode.IsDigit(char) { + result = append(result, unicode.ToLower(char)) + } else if unicode.IsSpace(char) { + result = append(result, '-') + } + } + + joined := string(result) + + re := regexp.MustCompile(`-+`) + joined = re.ReplaceAllString(joined, "-") + + // Remove leading or trailing dashes if any + return strings.Trim(joined, "-") +} + func extractCommandGroups(data []byte) ([]commandGroup, error) { md := goldmark.New() reader := text.NewReader(data) @@ -74,6 +96,7 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { // walk through the ast nodes ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + // look up for headers if heading, ok := n.(*ast.Heading); ok && entering { if heading.Level == 2 { currentPlatform = nil @@ -91,6 +114,7 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { } } + // look up for code blocks if block, ok := n.(*ast.FencedCodeBlock); ok && entering && len(groups) > 0 { i := len(groups) - 1 lang := string(block.Language(data)) From 4a9e746e93a70897a1d41261e4d10f52ea2ef3e2 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Fri, 4 Oct 2024 19:43:50 +0700 Subject: [PATCH 05/19] feat: process cmd --- cli/cmd/cmds/devx.go | 82 ++++++++++++++++++++++++++++-------------- cli/tests/Developer.md | 24 ++++--------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 28ffde16..da89bf02 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "os/exec" "regexp" "strings" "unicode" @@ -21,8 +22,6 @@ type DevX struct { } func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { - fmt.Println("MarkdownPath:", c.MarkdownPath, "CommandName:", c.CommandName) - // read the file from the specified path raw, err := os.ReadFile(c.MarkdownPath) if err != nil { @@ -32,26 +31,11 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { // parse the file with prepared options commandGroups, err := extractCommandGroups(raw) if err != nil { - return fmt.Errorf("could not extract commands: %v", err) - } - - // Output the command groups - for _, group := range commandGroups { - fmt.Printf("Command Id: %s (%v)\n", group.GetId(), len(group.commands)) - for _, cmd := range group.commands { - fmt.Printf("---\n") - fmt.Printf("Command Content: %v", cmd.content) - - if cmd.lang != nil { - fmt.Printf("Command Lang: %v\n", *cmd.lang) - } - if cmd.platform != nil { - fmt.Printf("Command Platform: %v\n", *cmd.platform) - } - } + return fmt.Errorf("%v", err) } - return nil + // exec the command + return processCmd(commandGroups, c.CommandName) } type command struct { @@ -60,15 +44,33 @@ type command struct { platform *string } +func (cmd *command) Exec() error { + executor := getLangExecutor(cmd.lang) + if executor == nil { + return fmt.Errorf("only commands running with `sh` can be executed") + } + + execCmd := exec.Command(cmd.content) + + output, err := execCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s", err) + } + + fmt.Println(string(output)) + + return nil +} + type commandGroup struct { name string commands []command } -func (c *commandGroup) GetId() string { +func (cg *commandGroup) GetId() string { var result []rune - for _, char := range c.name { + for _, char := range cg.name { if unicode.IsLetter(char) || unicode.IsDigit(char) { result = append(result, unicode.ToLower(char)) } else if unicode.IsSpace(char) { @@ -81,7 +83,6 @@ func (c *commandGroup) GetId() string { re := regexp.MustCompile(`-+`) joined = re.ReplaceAllString(joined, "-") - // Remove leading or trailing dashes if any return strings.Trim(joined, "-") } @@ -98,7 +99,7 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { // look up for headers if heading, ok := n.(*ast.Heading); ok && entering { - if heading.Level == 2 { + if heading.Level == 3 { currentPlatform = nil commandName := string(heading.Text(data)) @@ -108,10 +109,10 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { }) } - if heading.Level == 3 && len(groups) > 0 { + /* if heading.Level == 4 && len(groups) > 0 { platform := string(heading.Text(data)) currentPlatform = &platform - } + } */ } // look up for code blocks @@ -141,3 +142,32 @@ func extractCommandGroups(data []byte) ([]commandGroup, error) { return groups, nil } + +func processCmd(list []commandGroup, cmd string) error { + var foundCmd *command + for _, v := range list { + if v.GetId() == cmd { + // TODO: should get the command corresponding to the current host platform + foundCmd = &v.commands[0] + } + } + + if foundCmd == nil { + return fmt.Errorf("command not found") + } + + return foundCmd.Exec() +} + +func getLangExecutor(lang *string) *string { + if lang == nil { + return nil + } + + if *lang == "sh" { + executor := "sh" + return &executor + } else { + return nil + } +} diff --git a/cli/tests/Developer.md b/cli/tests/Developer.md index 415f3b48..0c31bf5f 100644 --- a/cli/tests/Developer.md +++ b/cli/tests/Developer.md @@ -1,31 +1,21 @@ # My cool dev docs -## Run Some Command !! +### Run Some Command !! ``` sh - echo "the fallback, if nothing else is defined" + echo "should run this command" ``` -### Linux - -``` python - print("Cool") -``` - -### Linux:arm64 - ``` sh - echo "Because maybe python is broken??" + echo "should not run this command" ``` -### Windows +### Extra $Cool$ Command -``` powershell - echo "Does this even work in powershell??" +``` sh + echo "should run this command" ``` -### Mac:Arm64 - ``` sh - echo "Because its always got to be different, even though its really just BSD Unix" + echo "should not run this command" ``` \ No newline at end of file From 7319379d45d42a224a863dcbd0b6e3ac6b390f5c Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Fri, 4 Oct 2024 20:27:24 +0700 Subject: [PATCH 06/19] feat: executable --- cli/cmd/cmds/devx.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index da89bf02..826476ef 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -45,19 +45,19 @@ type command struct { } func (cmd *command) Exec() error { - executor := getLangExecutor(cmd.lang) - if executor == nil { + executorCmd, executorArgs := getLangExecutor(cmd.lang) + if executorCmd == "" { return fmt.Errorf("only commands running with `sh` can be executed") } - execCmd := exec.Command(cmd.content) + execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) output, err := execCmd.CombinedOutput() if err != nil { return fmt.Errorf("%s", err) } - fmt.Println(string(output)) + fmt.Print(string(output)) return nil } @@ -159,15 +159,24 @@ func processCmd(list []commandGroup, cmd string) error { return foundCmd.Exec() } -func getLangExecutor(lang *string) *string { +func getLangExecutor(lang *string) (string, []string) { if lang == nil { - return nil + return "", nil } if *lang == "sh" { - executor := "sh" - return &executor + return "sh", []string{"-c", "$"} } else { - return nil + return "", nil } } + +func formatArgs(base []string, replacement string) []string { + replaced := make([]string, len(base)) + + for i, str := range base { + replaced[i] = strings.ReplaceAll(str, "$", replacement) + } + + return replaced +} From 1ea1931d5e1908666a166e6338f79d599b6c0f8c Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Fri, 4 Oct 2024 21:13:08 +0700 Subject: [PATCH 07/19] feat: stream output --- cli/cmd/cmds/devx.go | 28 +++++++++++++++++++++++----- cli/tests/Developer.md | 9 +++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 826476ef..20b42021 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -1,6 +1,7 @@ package cmds import ( + "bufio" "bytes" "errors" "fmt" @@ -31,7 +32,7 @@ func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { // parse the file with prepared options commandGroups, err := extractCommandGroups(raw) if err != nil { - return fmt.Errorf("%v", err) + return err } // exec the command @@ -50,14 +51,30 @@ func (cmd *command) Exec() error { return fmt.Errorf("only commands running with `sh` can be executed") } + // start executing the command execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) - output, err := execCmd.CombinedOutput() + stdout, err := execCmd.StdoutPipe() if err != nil { - return fmt.Errorf("%s", err) + return err } - fmt.Print(string(output)) + if err := execCmd.Start(); err != nil { + return err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading output:", err) + } + + if err := execCmd.Wait(); err != nil { + fmt.Println("Error waiting for command:", err) + } return nil } @@ -147,7 +164,7 @@ func processCmd(list []commandGroup, cmd string) error { var foundCmd *command for _, v := range list { if v.GetId() == cmd { - // TODO: should get the command corresponding to the current host platform + // TODO: should get the fisrt (most specified) command corresponding to the current host platform foundCmd = &v.commands[0] } } @@ -164,6 +181,7 @@ func getLangExecutor(lang *string) (string, []string) { return "", nil } + // TODO: get more supported commands if *lang == "sh" { return "sh", []string{"-c", "$"} } else { diff --git a/cli/tests/Developer.md b/cli/tests/Developer.md index 0c31bf5f..264a8549 100644 --- a/cli/tests/Developer.md +++ b/cli/tests/Developer.md @@ -3,19 +3,20 @@ ### Run Some Command !! ``` sh - echo "should run this command" + echo "should run this command (1)" ``` ``` sh - echo "should not run this command" + echo "should not run this command (1)" ``` ### Extra $Cool$ Command ``` sh - echo "should run this command" + echo "should run this command (2)" + echo "should run another command (2)" ``` ``` sh - echo "should not run this command" + echo "should not run this command (2)" ``` \ No newline at end of file From 2f2b52aef145ed463067f3c1731f6395f1aae6e7 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Fri, 4 Oct 2024 21:33:13 +0700 Subject: [PATCH 08/19] chore: msg --- cli/cmd/cmds/devx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 20b42021..c4419133 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -69,11 +69,11 @@ func (cmd *command) Exec() error { } if err := scanner.Err(); err != nil { - fmt.Println("Error reading output:", err) + fmt.Println("error reading output:", err) } if err := execCmd.Wait(); err != nil { - fmt.Println("Error waiting for command:", err) + fmt.Println("error waiting for command:", err) } return nil From 65cf45b3ada086fe51aaad332b4415e5237b8529 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Mon, 7 Oct 2024 19:00:00 +0700 Subject: [PATCH 09/19] chore: sync main --- cli/cmd/cmds/devx.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index c4419133..72ba14c9 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -15,6 +15,8 @@ import ( "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" ) type DevX struct { @@ -22,7 +24,7 @@ type DevX struct { CommandName string `arg:"" help:"Command to be executed."` } -func (c *DevX) Run(logger *slog.Logger, global GlobalArgs) error { +func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error { // read the file from the specified path raw, err := os.ReadFile(c.MarkdownPath) if err != nil { From 118c7e0c16fa037c93850368b9802640fa5c5e68 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Mon, 7 Oct 2024 19:05:37 +0700 Subject: [PATCH 10/19] feat: check available command --- cli/cmd/cmds/devx.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 72ba14c9..048e2c18 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -53,6 +53,11 @@ func (cmd *command) Exec() error { return fmt.Errorf("only commands running with `sh` can be executed") } + // check if the command is available + if _, err := exec.LookPath(executorCmd); err != nil { + return fmt.Errorf("command '%s' not found in PATH", executorCmd) + } + // start executing the command execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) From 8448731fb6e1fed88f0000bd7363a5033082ac2e Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Mon, 7 Oct 2024 19:11:54 +0700 Subject: [PATCH 11/19] chore: move test devx to testdata --- cli/{tests => cmd/testdata/devx}/Developer.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cli/{tests => cmd/testdata/devx}/Developer.md (100%) diff --git a/cli/tests/Developer.md b/cli/cmd/testdata/devx/Developer.md similarity index 100% rename from cli/tests/Developer.md rename to cli/cmd/testdata/devx/Developer.md From 9999874c7d3e76ba47269c6103090549c66b96ef Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 14:09:03 +0700 Subject: [PATCH 12/19] refactor: move utils --- cli/cmd/cmds/devx.go | 176 ------------------------------------- cli/pkg/command/program.go | 84 ++++++++++++++++++ cli/pkg/command/utils.go | 108 +++++++++++++++++++++++ 3 files changed, 192 insertions(+), 176 deletions(-) create mode 100644 cli/pkg/command/program.go create mode 100644 cli/pkg/command/utils.go diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 048e2c18..f50d8074 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -1,20 +1,9 @@ package cmds import ( - "bufio" - "bytes" - "errors" "fmt" "log/slog" "os" - "os/exec" - "regexp" - "strings" - "unicode" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/text" "github.com/input-output-hk/catalyst-forge/cli/pkg/run" ) @@ -40,168 +29,3 @@ func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error { // exec the command return processCmd(commandGroups, c.CommandName) } - -type command struct { - content string - lang *string - platform *string -} - -func (cmd *command) Exec() error { - executorCmd, executorArgs := getLangExecutor(cmd.lang) - if executorCmd == "" { - return fmt.Errorf("only commands running with `sh` can be executed") - } - - // check if the command is available - if _, err := exec.LookPath(executorCmd); err != nil { - return fmt.Errorf("command '%s' not found in PATH", executorCmd) - } - - // start executing the command - execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) - - stdout, err := execCmd.StdoutPipe() - if err != nil { - return err - } - - if err := execCmd.Start(); err != nil { - return err - } - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - fmt.Println(scanner.Text()) - } - - if err := scanner.Err(); err != nil { - fmt.Println("error reading output:", err) - } - - if err := execCmd.Wait(); err != nil { - fmt.Println("error waiting for command:", err) - } - - return nil -} - -type commandGroup struct { - name string - commands []command -} - -func (cg *commandGroup) GetId() string { - var result []rune - - for _, char := range cg.name { - if unicode.IsLetter(char) || unicode.IsDigit(char) { - result = append(result, unicode.ToLower(char)) - } else if unicode.IsSpace(char) { - result = append(result, '-') - } - } - - joined := string(result) - - re := regexp.MustCompile(`-+`) - joined = re.ReplaceAllString(joined, "-") - - return strings.Trim(joined, "-") -} - -func extractCommandGroups(data []byte) ([]commandGroup, error) { - md := goldmark.New() - reader := text.NewReader(data) - doc := md.Parser().Parse(reader) - - // store the command groups and commands - groups := []commandGroup{} - var currentPlatform *string - - // walk through the ast nodes - ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - // look up for headers - if heading, ok := n.(*ast.Heading); ok && entering { - if heading.Level == 3 { - currentPlatform = nil - commandName := string(heading.Text(data)) - - groups = append(groups, commandGroup{ - name: commandName, - commands: []command{}, - }) - } - - /* if heading.Level == 4 && len(groups) > 0 { - platform := string(heading.Text(data)) - currentPlatform = &platform - } */ - } - - // look up for code blocks - if block, ok := n.(*ast.FencedCodeBlock); ok && entering && len(groups) > 0 { - i := len(groups) - 1 - lang := string(block.Language(data)) - - var buf bytes.Buffer - for i := 0; i < block.Lines().Len(); i++ { - line := block.Lines().At(i) - buf.Write(line.Value(data)) - } - - groups[i].commands = append(groups[i].commands, command{ - content: buf.String(), - lang: &lang, - platform: currentPlatform, - }) - } - - return ast.WalkContinue, nil - }) - - if len(groups) == 0 { - return nil, errors.New("no command groups found in the markdown") - } - - return groups, nil -} - -func processCmd(list []commandGroup, cmd string) error { - var foundCmd *command - for _, v := range list { - if v.GetId() == cmd { - // TODO: should get the fisrt (most specified) command corresponding to the current host platform - foundCmd = &v.commands[0] - } - } - - if foundCmd == nil { - return fmt.Errorf("command not found") - } - - return foundCmd.Exec() -} - -func getLangExecutor(lang *string) (string, []string) { - if lang == nil { - return "", nil - } - - // TODO: get more supported commands - if *lang == "sh" { - return "sh", []string{"-c", "$"} - } else { - return "", nil - } -} - -func formatArgs(base []string, replacement string) []string { - replaced := make([]string, len(base)) - - for i, str := range base { - replaced[i] = strings.ReplaceAll(str, "$", replacement) - } - - return replaced -} diff --git a/cli/pkg/command/program.go b/cli/pkg/command/program.go new file mode 100644 index 00000000..aaf8c8c6 --- /dev/null +++ b/cli/pkg/command/program.go @@ -0,0 +1,84 @@ +package command + +import ( + "bufio" + "fmt" + "os/exec" + "regexp" + "strings" + "unicode" +) + +type program struct { + name string + groups []commandGroup +} + +type commandGroup struct { + name string + commands []command +} + +type command struct { + content string + lang *string + platform *string +} + +func (cmd *command) Exec() error { + executorCmd, executorArgs := GetLangExecutor(cmd.lang) + if executorCmd == "" { + return fmt.Errorf("only commands running with `sh` can be executed") + } + + // check if the command is available + if _, err := exec.LookPath(executorCmd); err != nil { + return fmt.Errorf("command '%s' not found in PATH", executorCmd) + } + + // start executing the command + execCmd := exec.Command(executorCmd, FormatArgs(executorArgs, cmd.content)...) + + stdout, err := execCmd.StdoutPipe() + if err != nil { + return err + } + + if err := execCmd.Start(); err != nil { + return err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + + if err := scanner.Err(); err != nil { + fmt.Println("error reading output:", err) + } + + if err := execCmd.Wait(); err != nil { + fmt.Println("error waiting for command:", err) + } + + return nil +} + +func (cg *commandGroup) GetId() string { + var result []rune + + for _, char := range cg.name { + if unicode.IsLetter(char) || unicode.IsDigit(char) { + result = append(result, unicode.ToLower(char)) + } else if unicode.IsSpace(char) { + result = append(result, '-') + } + } + + joined := string(result) + + re := regexp.MustCompile(`-+`) + joined = re.ReplaceAllString(joined, "-") + + return strings.Trim(joined, "-") +} diff --git a/cli/pkg/command/utils.go b/cli/pkg/command/utils.go new file mode 100644 index 00000000..7e3a1bd6 --- /dev/null +++ b/cli/pkg/command/utils.go @@ -0,0 +1,108 @@ +package command + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +func ExtractCommandMarkdown(data []byte) ([]commandGroup, error) { + md := goldmark.New() + reader := text.NewReader(data) + doc := md.Parser().Parse(reader) + + // store the command groups and commands + groups := []commandGroup{} + var currentPlatform *string + + // walk through the ast nodes + ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + // look up for headers + if heading, ok := n.(*ast.Heading); ok && entering { + if heading.Level == 3 { + currentPlatform = nil + commandName := string(heading.Text(data)) + + groups = append(groups, commandGroup{ + name: commandName, + commands: []command{}, + }) + } + + /* if heading.Level == 4 && len(groups) > 0 { + platform := string(heading.Text(data)) + currentPlatform = &platform + } */ + } + + // look up for code blocks + if block, ok := n.(*ast.FencedCodeBlock); ok && entering && len(groups) > 0 { + i := len(groups) - 1 + lang := string(block.Language(data)) + + var buf bytes.Buffer + for i := 0; i < block.Lines().Len(); i++ { + line := block.Lines().At(i) + buf.Write(line.Value(data)) + } + + groups[i].commands = append(groups[i].commands, command{ + content: buf.String(), + lang: &lang, + platform: currentPlatform, + }) + } + + return ast.WalkContinue, nil + }) + + if len(groups) == 0 { + return nil, errors.New("no command groups found in the markdown") + } + + return groups, nil +} + +func ProcessCmd(list []commandGroup, cmd string) error { + var foundCmd *command + for _, v := range list { + if v.GetId() == cmd { + // TODO: should get the fisrt (most specified) command corresponding to the current host platform + foundCmd = &v.commands[0] + } + } + + if foundCmd == nil { + return fmt.Errorf("command not found") + } + + return foundCmd.Exec() +} + +func GetLangExecutor(lang *string) (string, []string) { + if lang == nil { + return "", nil + } + + // TODO: get more supported commands + if *lang == "sh" { + return "sh", []string{"-c", "$"} + } else { + return "", nil + } +} + +func FormatArgs(base []string, replacement string) []string { + replaced := make([]string, len(base)) + + for i, str := range base { + replaced[i] = strings.ReplaceAll(str, "$", replacement) + } + + return replaced +} From 73e5ea56201469165048e56c0056480759b4887d Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 14:22:37 +0700 Subject: [PATCH 13/19] refactor: restructure --- cli/cmd/cmds/devx.go | 5 +++-- cli/pkg/command/program.go | 34 ++++++++++++++++++++++-------- cli/pkg/command/utils.go | 42 ++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index f50d8074..6f2b98ba 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" + "github.com/input-output-hk/catalyst-forge/cli/pkg/command" "github.com/input-output-hk/catalyst-forge/cli/pkg/run" ) @@ -21,11 +22,11 @@ func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error { } // parse the file with prepared options - commandGroups, err := extractCommandGroups(raw) + prog, err := command.ExtractDevXMarkdown(raw) if err != nil { return err } // exec the command - return processCmd(commandGroups, c.CommandName) + return prog.ProcessCmd(c.CommandName) } diff --git a/cli/pkg/command/program.go b/cli/pkg/command/program.go index aaf8c8c6..e03984dc 100644 --- a/cli/pkg/command/program.go +++ b/cli/pkg/command/program.go @@ -9,24 +9,24 @@ import ( "unicode" ) -type program struct { +type Program struct { name string - groups []commandGroup + groups []CommandGroup } -type commandGroup struct { +type CommandGroup struct { name string - commands []command + commands []Command } -type command struct { +type Command struct { content string lang *string platform *string } -func (cmd *command) Exec() error { - executorCmd, executorArgs := GetLangExecutor(cmd.lang) +func (cmd *Command) Exec() error { + executorCmd, executorArgs := getLangExecutor(cmd.lang) if executorCmd == "" { return fmt.Errorf("only commands running with `sh` can be executed") } @@ -37,7 +37,7 @@ func (cmd *command) Exec() error { } // start executing the command - execCmd := exec.Command(executorCmd, FormatArgs(executorArgs, cmd.content)...) + execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) stdout, err := execCmd.StdoutPipe() if err != nil { @@ -64,7 +64,7 @@ func (cmd *command) Exec() error { return nil } -func (cg *commandGroup) GetId() string { +func (cg *CommandGroup) GetId() string { var result []rune for _, char := range cg.name { @@ -82,3 +82,19 @@ func (cg *commandGroup) GetId() string { return strings.Trim(joined, "-") } + +func (prog *Program) ProcessCmd(cmd string) error { + var foundCmd *Command + for _, v := range prog.groups { + if v.GetId() == cmd { + // TODO: should get the fisrt (most specified) command corresponding to the current host platform + foundCmd = &v.commands[0] + } + } + + if foundCmd == nil { + return fmt.Errorf("command not found") + } + + return foundCmd.Exec() +} diff --git a/cli/pkg/command/utils.go b/cli/pkg/command/utils.go index 7e3a1bd6..6c326a4b 100644 --- a/cli/pkg/command/utils.go +++ b/cli/pkg/command/utils.go @@ -3,7 +3,6 @@ package command import ( "bytes" "errors" - "fmt" "strings" "github.com/yuin/goldmark" @@ -11,26 +10,33 @@ import ( "github.com/yuin/goldmark/text" ) -func ExtractCommandMarkdown(data []byte) ([]commandGroup, error) { +func ExtractDevXMarkdown(data []byte) (*Program, error) { md := goldmark.New() reader := text.NewReader(data) doc := md.Parser().Parse(reader) // store the command groups and commands - groups := []commandGroup{} + groups := []CommandGroup{} + var progName *string var currentPlatform *string // walk through the ast nodes ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { // look up for headers if heading, ok := n.(*ast.Heading); ok && entering { + if heading.Level == 1 { + title := string(heading.Text(data)) + + progName = &title + } + if heading.Level == 3 { currentPlatform = nil commandName := string(heading.Text(data)) - groups = append(groups, commandGroup{ + groups = append(groups, CommandGroup{ name: commandName, - commands: []command{}, + commands: []Command{}, }) } @@ -51,7 +57,7 @@ func ExtractCommandMarkdown(data []byte) ([]commandGroup, error) { buf.Write(line.Value(data)) } - groups[i].commands = append(groups[i].commands, command{ + groups[i].commands = append(groups[i].commands, Command{ content: buf.String(), lang: &lang, platform: currentPlatform, @@ -64,27 +70,19 @@ func ExtractCommandMarkdown(data []byte) ([]commandGroup, error) { if len(groups) == 0 { return nil, errors.New("no command groups found in the markdown") } - - return groups, nil -} - -func ProcessCmd(list []commandGroup, cmd string) error { - var foundCmd *command - for _, v := range list { - if v.GetId() == cmd { - // TODO: should get the fisrt (most specified) command corresponding to the current host platform - foundCmd = &v.commands[0] - } + if progName == nil { + return nil, errors.New("no title found in the markdown") } - if foundCmd == nil { - return fmt.Errorf("command not found") + prog := Program{ + name: *progName, + groups: groups, } - return foundCmd.Exec() + return &prog, nil } -func GetLangExecutor(lang *string) (string, []string) { +func getLangExecutor(lang *string) (string, []string) { if lang == nil { return "", nil } @@ -97,7 +95,7 @@ func GetLangExecutor(lang *string) (string, []string) { } } -func FormatArgs(base []string, replacement string) []string { +func formatArgs(base []string, replacement string) []string { replaced := make([]string, len(base)) for i, str := range base { From 2b3322c3f1cbde94ac5fada1b08a6252e0802d86 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 14:23:37 +0700 Subject: [PATCH 14/19] chore: remove comments --- cli/cmd/cmds/devx.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 6f2b98ba..0f873842 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -15,18 +15,15 @@ type DevX struct { } func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error { - // read the file from the specified path raw, err := os.ReadFile(c.MarkdownPath) if err != nil { return fmt.Errorf("could not read file at %s: %v", c.MarkdownPath, err) } - // parse the file with prepared options prog, err := command.ExtractDevXMarkdown(raw) if err != nil { return err } - // exec the command return prog.ProcessCmd(c.CommandName) } From 75f0b52df1e016a7c445ae791ad93d8d95027440 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 14:44:40 +0700 Subject: [PATCH 15/19] refactor: using executor --- cli/cmd/cmds/devx.go | 2 +- cli/pkg/command/program.go | 70 ++++++++++++++------------------------ 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/cli/cmd/cmds/devx.go b/cli/cmd/cmds/devx.go index 0f873842..0b4955b3 100644 --- a/cli/cmd/cmds/devx.go +++ b/cli/cmd/cmds/devx.go @@ -25,5 +25,5 @@ func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error { return err } - return prog.ProcessCmd(c.CommandName) + return prog.ProcessCmd(c.CommandName, logger) } diff --git a/cli/pkg/command/program.go b/cli/pkg/command/program.go index e03984dc..ac663129 100644 --- a/cli/pkg/command/program.go +++ b/cli/pkg/command/program.go @@ -1,12 +1,14 @@ package command import ( - "bufio" "fmt" + "log/slog" "os/exec" "regexp" "strings" "unicode" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/executor" ) type Program struct { @@ -25,43 +27,39 @@ type Command struct { platform *string } -func (cmd *Command) Exec() error { - executorCmd, executorArgs := getLangExecutor(cmd.lang) - if executorCmd == "" { - return fmt.Errorf("only commands running with `sh` can be executed") - } - - // check if the command is available - if _, err := exec.LookPath(executorCmd); err != nil { - return fmt.Errorf("command '%s' not found in PATH", executorCmd) +func (prog *Program) ProcessCmd(cmd string, logger *slog.Logger) error { + var foundCmd *Command + for _, v := range prog.groups { + if v.GetId() == cmd { + // TODO: should get the fisrt (most specified) command corresponding to the current host platform + foundCmd = &v.commands[0] + } } - // start executing the command - execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...) - - stdout, err := execCmd.StdoutPipe() - if err != nil { - return err + if foundCmd == nil { + return fmt.Errorf("command not found") } - if err := execCmd.Start(); err != nil { - return err - } + return foundCmd.exec(logger) +} - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - fmt.Println(scanner.Text()) +func (cmd *Command) exec(logger *slog.Logger) error { + executorCmd, executorArgs := getLangExecutor(cmd.lang) + if executorCmd == "" { + return fmt.Errorf("only commands running with `sh` can be executed") } - if err := scanner.Err(); err != nil { - fmt.Println("error reading output:", err) + if _, err := exec.LookPath(executorCmd); err != nil { + return fmt.Errorf("command '%s' not found in PATH", executorCmd) } - if err := execCmd.Wait(); err != nil { - fmt.Println("error waiting for command:", err) - } + localExec := executor.NewLocalExecutor( + logger, + executor.WithRedirect(), + ) + _, err := localExec.Execute(executorCmd, formatArgs(executorArgs, cmd.content)) - return nil + return err } func (cg *CommandGroup) GetId() string { @@ -82,19 +80,3 @@ func (cg *CommandGroup) GetId() string { return strings.Trim(joined, "-") } - -func (prog *Program) ProcessCmd(cmd string) error { - var foundCmd *Command - for _, v := range prog.groups { - if v.GetId() == cmd { - // TODO: should get the fisrt (most specified) command corresponding to the current host platform - foundCmd = &v.commands[0] - } - } - - if foundCmd == nil { - return fmt.Errorf("command not found") - } - - return foundCmd.Exec() -} From d9d9b2b85b89ad18766752a661febf1cdef07704 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 16:54:16 +0700 Subject: [PATCH 16/19] refactor: command executor matching --- cli/pkg/command/executor.go | 29 +++++++++++++++++++++++++++++ cli/pkg/command/program.go | 17 ++++++++++------- cli/pkg/command/utils.go | 13 ------------- 3 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 cli/pkg/command/executor.go diff --git a/cli/pkg/command/executor.go b/cli/pkg/command/executor.go new file mode 100644 index 00000000..68574762 --- /dev/null +++ b/cli/pkg/command/executor.go @@ -0,0 +1,29 @@ +package command + +type LanguageExecutor struct { + executor map[string]Executor +} + +func NewDefaultLanguageExecutor() LanguageExecutor { + return LanguageExecutor{ + executor: map[string]Executor{ + "sh": ShellLanguageExecutor{}, + }, + } +} + +type Executor interface { + GetExecutorCommand() string + GetExecutorArgs(content string) []string +} + +// shell +type ShellLanguageExecutor struct{} + +func (e ShellLanguageExecutor) GetExecutorCommand() string { + return "sh" +} + +func (e ShellLanguageExecutor) GetExecutorArgs(content string) []string { + return formatArgs([]string{"-c", "$"}, content) +} diff --git a/cli/pkg/command/program.go b/cli/pkg/command/program.go index ac663129..e226fd6d 100644 --- a/cli/pkg/command/program.go +++ b/cli/pkg/command/program.go @@ -37,27 +37,30 @@ func (prog *Program) ProcessCmd(cmd string, logger *slog.Logger) error { } if foundCmd == nil { - return fmt.Errorf("command not found") + return fmt.Errorf("command '%s' not found in markdown", cmd) } return foundCmd.exec(logger) } func (cmd *Command) exec(logger *slog.Logger) error { - executorCmd, executorArgs := getLangExecutor(cmd.lang) - if executorCmd == "" { - return fmt.Errorf("only commands running with `sh` can be executed") + if cmd.lang == nil { + return fmt.Errorf("command block without specified language") } - if _, err := exec.LookPath(executorCmd); err != nil { - return fmt.Errorf("command '%s' not found in PATH", executorCmd) + lang, ok := NewDefaultLanguageExecutor().executor[*cmd.lang] + if !ok { + return fmt.Errorf("only commands running with `sh` can be executed") + } + if _, err := exec.LookPath(lang.GetExecutorCommand()); err != nil { + return fmt.Errorf("command '%s' is unavailable", lang.GetExecutorCommand()) } localExec := executor.NewLocalExecutor( logger, executor.WithRedirect(), ) - _, err := localExec.Execute(executorCmd, formatArgs(executorArgs, cmd.content)) + _, err := localExec.Execute(lang.GetExecutorCommand(), lang.GetExecutorArgs(cmd.content)) return err } diff --git a/cli/pkg/command/utils.go b/cli/pkg/command/utils.go index 6c326a4b..ddd7a7e1 100644 --- a/cli/pkg/command/utils.go +++ b/cli/pkg/command/utils.go @@ -82,19 +82,6 @@ func ExtractDevXMarkdown(data []byte) (*Program, error) { return &prog, nil } -func getLangExecutor(lang *string) (string, []string) { - if lang == nil { - return "", nil - } - - // TODO: get more supported commands - if *lang == "sh" { - return "sh", []string{"-c", "$"} - } else { - return "", nil - } -} - func formatArgs(base []string, replacement string) []string { replaced := make([]string, len(base)) From 8de6e57711c4f9732addfc1d4c93011ba36b3ee4 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 8 Oct 2024 18:43:29 +0700 Subject: [PATCH 17/19] test: for devx --- cli/cmd/main_test.go | 6 ++++++ cli/cmd/testdata/devx/1.txt | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 cli/cmd/testdata/devx/1.txt diff --git a/cli/cmd/main_test.go b/cli/cmd/main_test.go index 76dfe043..8d36b7a1 100644 --- a/cli/cmd/main_test.go +++ b/cli/cmd/main_test.go @@ -32,6 +32,12 @@ func TestScan(t *testing.T) { }) } +func TestDevX(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata/devx", + }) +} + func mockEarthly() int { for _, arg := range os.Args { fmt.Println(arg) diff --git a/cli/cmd/testdata/devx/1.txt b/cli/cmd/testdata/devx/1.txt new file mode 100644 index 00000000..713adf25 --- /dev/null +++ b/cli/cmd/testdata/devx/1.txt @@ -0,0 +1,19 @@ +exec git init . + +exec forge devx ./Developer.md run-some-command +cmp stdout expect.txt + +-- Developer.md -- +# My cool dev docs + +### Run Some Command !! + +``` sh + echo "should run this command (1)" +``` + +``` sh + echo "should not run this command (1)" +``` +-- expect.txt -- +should run this command (1) \ No newline at end of file From 1f51099b6bf4e79ed530421c1bab3a996a38bd40 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 21 Oct 2024 18:30:49 +0700 Subject: [PATCH 18/19] docs: Add example Developer.md for further work --- docs/src/reference/devx/Developer.md | 150 +++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/src/reference/devx/Developer.md diff --git a/docs/src/reference/devx/Developer.md b/docs/src/reference/devx/Developer.md new file mode 100644 index 00000000..7f9befdf --- /dev/null +++ b/docs/src/reference/devx/Developer.md @@ -0,0 +1,150 @@ +# Example Developer.md file + +This is an example of a Developer.md file. +It is used to help explain, informally, the structure of Developer.md files. + +First level headings are just documentation. + +## Second level headings are command names + +They are normalized, so this command would become: `second-level-headings-are-command-names`. + +Only the first matching code block that has a recognized type is used. + +This command uses `sh` which means, run the command in the shell of the caller. +Because it's not possible to know exactly what shell the caller is using, +these kinds of commands should be simple and not have any logic. + +```sh +echo "This is a command run in the shell of the caller" +``` + +## A command that uses bash + +To ensure that a known shell is used, `bash` can be used to ensure the command is run inside a `bash` shell. +The system must have `bash` installed, or it will fail. + +Scripts are run together, not as a distinct set of commands. +So it's easy to do loops or multi-line statements without using backslash. +Comments can be used to better explain the script. + +```bash +for i in 1 2 3; do + echo "Executing command $i" + # Pause for half a second to make it easier to see the output. + sleep 0.5 +done +``` + +## A command that uses python 3 + +Currently, only `sh`, `bash` and `python` are intended to be supported. + +This would never get executed. +Its just documentation. +The `python` script below could be written in `rust` like so: + +```rust +use std::thread; +use std::time::Duration; + +fn main() { + for i in 1..=3 { + println!("Executing command {}", i); + // Pause for half a second to make it easier to see the output. + thread::sleep(Duration::from_secs_f64(0.5)); + } +} +``` + +Similar to bash, commands can be run inside a python interpreter. +The system must have `python` installed, or it will fail. +These scripts should restrict themselves to the python standard library. + +```python +import time +print("This is a command run inside python") + +for i in range(1, 4): + print("Executing command", i) + # Pause for half a second to make it easier to see the output. + time.sleep(0.5) +``` + +The list of supported interpreted/scripting languages could grow. +It will never include complied languages like C, Rust, Go, etc. + +## What about parameters + +Currently, parameters are not defined. +However, Environment variables will be passed through from the caller to a command. + +Environment variables can be used to parameterize any command. +If they are, they should be documented in the Developer.md file with the command. + +This command will show all the current caller's environment variables. + +```python +import os + +for key, value in os.environ.items(): + print(f"Key: {key}, Value: {value}") +``` + +## System-specific commands + +Sometimes different systems require different commands. +This can be accommodated by placing a `platforms` table before a command. + +IF the current platform matches one in the list, the command is used. +Otherwise, it is skipped as being documentation. + +The ***FIRST*** command to match the users platform will run. +All others are ignored. +Therefore, specific platforms should be listed first. + +### Linux/Mac on ARM + +| Platforms | +| --- | +| linux/aarch64 | +| darwin/aarch64 | + +This only is executed on Linux and Mac if the CPU is ARM-Based. + +If we had specified `linux` or `darwin` by themselves, then the CPU type would not matter for that platform. + +If we specified `aarch64` by itself, then any platform using an Arm processor would match. + +```sh +echo "This only runs on Linux or Mac if the CPU is ARM Based." +``` + +### That somewhat popular OS from Redmond + +| Platforms | +| --- | +| windows | + +This is executed on all variants of Windows. + +```sh +@echo off +echo This only runs on Windows. +``` + +### Default + +Third level headings are just documentation, they have no special meaning. + +But in this case, they can be used to break up the platforms to make the intention clearer. +This would still execute the same without the third level headings. + +As this is the last command block, if none of the above executed, it will execute. + +It has to be listed last. +Otherwise it will match first and none of the platform-specific commands will run. + +```sh +echo "This will run on all other platforms." +``` From 3668e0a69bd52c5ed1e51dd6e9cc50d205f33c55 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj Date: Tue, 22 Oct 2024 13:39:56 +0700 Subject: [PATCH 19/19] fix: sync interface --- cli/cmd/main.go | 1 + cli/pkg/command/program.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/cmd/main.go b/cli/cmd/main.go index 0b6e04fc..a9075f9f 100644 --- a/cli/cmd/main.go +++ b/cli/cmd/main.go @@ -26,6 +26,7 @@ var cli struct { Deploy cmds.DeployCmd `kong:"cmd" help:"Deploy a project."` Dump cmds.DumpCmd `kong:"cmd" help:"Dumps a project's blueprint to JSON."` + Devx cmds.DevX `kong:"cmd" help:"Reads a forge markdown file and executes a command."` CI cmds.CICmd `kong:"cmd" help:"Simulate a CI run."` Release cmds.ReleaseCmd `kong:"cmd" help:"Release a project."` Run cmds.RunCmd `kong:"cmd" help:"Run an Earthly target."` diff --git a/cli/pkg/command/program.go b/cli/pkg/command/program.go index e226fd6d..871da2ae 100644 --- a/cli/pkg/command/program.go +++ b/cli/pkg/command/program.go @@ -60,7 +60,7 @@ func (cmd *Command) exec(logger *slog.Logger) error { logger, executor.WithRedirect(), ) - _, err := localExec.Execute(lang.GetExecutorCommand(), lang.GetExecutorArgs(cmd.content)) + _, err := localExec.Execute(lang.GetExecutorCommand(), lang.GetExecutorArgs(cmd.content)...) return err }