diff --git a/README.md b/README.md index 51c674c..fedde13 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ name=linuxsuren echo hello $name ``` +### Run Python Script +```python3 +#!title: Python Hello World +print('hello python world'); +``` + ## Limitation Please make sure the Markdown files meet Linux end-of-line. You could turn it via: `dos2unix your.md` diff --git a/cli/python_runner.go b/cli/python_runner.go new file mode 100644 index 0000000..5c06312 --- /dev/null +++ b/cli/python_runner.go @@ -0,0 +1,49 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" +) + +// PythonScript represents the Python script +type PythonScript struct { + Script +} + +// Run executes the script +func (s *PythonScript) Run() (err error) { + var shellFile string + if shellFile, err = writeAsShell(s.Content, s.Dir); err != nil { + fmt.Println(err) + return + } + if !s.KeepScripts { + defer func() { + _ = os.RemoveAll(shellFile) + }() + } + + var pyExec string + if pyExec, err = exec.LookPath("python3"); err != nil { + return + } + + cmd := exec.Command(pyExec, shellFile) + cmd.Env = os.Environ() + + var output []byte + if output, err = cmd.CombinedOutput(); err != nil { + fmt.Println(string(output), err) + return + } + fmt.Print(string(output)) + return +} + +// GetTitle returns the title of this script +func (s *PythonScript) GetTitle() string { + return s.Title +} + +var _ ScriptRunner = &PythonScript{} diff --git a/cli/root.go b/cli/root.go index 9e21a5f..2de7d7d 100644 --- a/cli/root.go +++ b/cli/root.go @@ -3,11 +3,8 @@ package cli import ( "fmt" - "io" "os" - "os/exec" "path" - "regexp" "strings" "github.com/AlecAivazis/survey/v2" @@ -59,7 +56,8 @@ func (o *option) runMarkdown(mdFilePath string) (err error) { md := markdown.New(markdown.XHTMLOutput(true), markdown.Nofollow(true)) tokens := md.Parse(mdFile) - cmdMap := map[string][]string{} + // cmdMap := map[string][]string{} + scriptList := NewScriptRunners() // Print the result var title string @@ -75,40 +73,41 @@ func (o *option) runMarkdown(mdFilePath string) (err error) { lang = tok.Params } - if content != "" && lang == "shell" { - originalContent := content - - // handle the break line - breakline := regexp.MustCompile(`\\\n`) - content = breakline.ReplaceAllString(content, "") - - whitespaces := regexp.MustCompile(` +`) - content = whitespaces.ReplaceAllString(content, " ") + if content == "" { + continue + } - lines := strings.Split(content, "\n") - if len(lines) < 2 { - continue - } - title = lines[0] - if !strings.HasPrefix(title, "#!title: ") { - continue - } - title = strings.TrimPrefix(title, "#!title: ") - // support multiple lines mode - if strings.Contains(title, "+f") { - title = strings.ReplaceAll(title, "+f", "") - title = strings.TrimSpace(title) + originalContent := content + lines := strings.Split(content, "\n") + if len(lines) < 2 { + continue + } + title = lines[0] + if !strings.HasPrefix(title, "#!title: ") { + continue + } + title = strings.TrimPrefix(title, "#!title: ") + + script := Script{ + Kind: lang, + Title: title, + Content: originalContent, + Dir: path.Dir(mdFilePath), + KeepScripts: o.keepScripts, + } - cmdMap[title] = append(cmdMap[title], originalContent) - } else { - cmdMap[title] = append(cmdMap[title], lines[1:]...) - } + switch lang { + case "shell", "bash": + scriptList = append(scriptList, &ShellScript{ + Script: script, + }) + case "python3": + scriptList = append(scriptList, &PythonScript{ + Script: script, + }) } } - - contextDir := path.Dir(mdFilePath) - // TODO this should be a treemap instead of hashmap - err = o.execute(cmdMap, contextDir) + err = o.executeScripts(scriptList) return } @@ -118,16 +117,10 @@ type option struct { keepScripts bool } -func (o *option) execute(cmdMap map[string][]string, contextDir string) (err error) { - var items []string - for key := range cmdMap { - items = append(items, key) - } - - items = append(items, "Quit") +func (o *option) executeScripts(scriptRunners ScriptRunners) (err error) { selector := &survey.MultiSelect{ Message: "Choose the code block to run", - Options: items, + Options: scriptRunners.GetTitles(), } titles := []string{} if err = survey.AskOne(selector, &titles, survey.WithKeepFilter(o.keepFilter)); err != nil { @@ -139,119 +132,12 @@ func (o *option) execute(cmdMap map[string][]string, contextDir string) (err err o.loop = false break } - preDefinedEnv := os.Environ() - cmds := cmdMap[title] - for _, cmdLine := range cmds { - var pair []string - var ok bool - ok, pair, err = isInputRequest(cmdLine) - if err != nil { - break - } - - if ok { - if pair, err = inputRequest(pair); err != nil { - break - } - os.Setenv(pair[0], pair[1]) - continue - } - - err = runCmdLine(cmdLine, contextDir, o.keepScripts) - if err != nil { - break - } - } - // reset the env - os.Clearenv() - for _, pair := range preDefinedEnv { - os.Setenv(strings.Split(pair, "=")[0], strings.Split(pair, "=")[1]) - } - } - return -} - -func isInputRequest(cmdLine string) (ok bool, pair []string, err error) { - var reg *regexp.Regexp - if reg, err = regexp.Compile(`^\w+=.+$`); err == nil { - items := strings.Split(cmdLine, "=") - if reg.MatchString(cmdLine) && len(items) == 2 { - pair = []string{strings.TrimSpace(items[0]), strings.TrimSpace(items[1])} - ok = true + if runner := scriptRunners.GetRunner(title); runner == nil { + fmt.Println("cannot found runner:", title) + } else if err = runner.Run(); err != nil { + break } } return } - -func inputRequest(pair []string) (result []string, err error) { - input := survey.Input{ - Message: pair[0], - Default: pair[1], - } - result = pair - - var value string - if err = survey.AskOne(&input, &value); err == nil { - result[1] = value - } - - return -} - -func runCmdLine(cmdLine, contextDir string, keepScripts bool) (err error) { - var shellFile string - if shellFile, err = writeAsShell(cmdLine, contextDir); err != nil { - fmt.Println(err) - return - } - if !keepScripts { - defer func() { - _ = os.RemoveAll(shellFile) - }() - } - - cmd := exec.Command("bash", path.Base(shellFile)) - cmd.Dir = contextDir - cmd.Env = os.Environ() - - var output []byte - if output, err = cmd.CombinedOutput(); err != nil { - fmt.Println(string(output), err) - return - } - fmt.Print(string(output)) - return -} - -func writeAsShell(content, dir string) (targetPath string, err error) { - var f *os.File - if f, err = os.CreateTemp(dir, "sh"); err == nil { - defer func() { - _ = f.Close() - }() - - targetPath = f.Name() - _, err = io.WriteString(f, content) - } - return -} - -func runAsInlineCommand(cmdLine, contextDir string) (err error) { - args := strings.Split(cmdLine, " ") - cmd := strings.TrimSpace(args[0]) - if cmd, err = exec.LookPath(cmd); err != nil { - err = fmt.Errorf("failed to find '%s'", cmd) - return - } - - fmt.Printf("start to run: %s %v\n", cmd, args[1:]) - var output []byte - cmdRun := exec.Command(cmd, args[1:]...) - cmdRun.Dir = contextDir - cmdRun.Env = os.Environ() - if output, err = cmdRun.CombinedOutput(); err == nil { - fmt.Print(string(output)) - } - return -} diff --git a/cli/root_test.go b/cli/root_test.go index 1827ee6..f863053 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -6,42 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsInputRequest(t *testing.T) { - tests := []struct { - name string - cmdLine string - expectOK bool - expectPair []string - expectErr bool - }{{ - name: "normal", - cmdLine: "name=linuxsuren", - expectOK: true, - expectPair: []string{"name", "linuxsuren"}, - expectErr: false, - }, { - name: "abnormal variable expression - with extra whitespace", - cmdLine: "name = linuxsuren", - expectOK: false, - expectPair: nil, - expectErr: false, - }, { - name: "complex characters in pair", - cmdLine: "vm=i-dy87owjl", - expectOK: true, - expectPair: []string{"vm", "i-dy87owjl"}, - expectErr: false, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ok, pair, err := isInputRequest(tt.cmdLine) - assert.Equal(t, tt.expectOK, ok) - assert.Equal(t, tt.expectPair, pair) - assert.Equal(t, tt.expectErr, err != nil) - }) - } -} - func TestNewRootCommand(t *testing.T) { tests := []struct { name string diff --git a/cli/shell_runner.go b/cli/shell_runner.go new file mode 100644 index 0000000..9134d57 --- /dev/null +++ b/cli/shell_runner.go @@ -0,0 +1,132 @@ +package cli + +import ( + "fmt" + "io" + "os" + "os/exec" + "path" + "regexp" + "strings" + + "github.com/AlecAivazis/survey/v2" +) + +// ShellScript represents the shell script +type ShellScript struct { + Script +} + +// Run executes the script +func (s *ShellScript) Run() (err error) { + // handle the break line + breakline := regexp.MustCompile(`\\\n`) + s.Content = breakline.ReplaceAllString(s.Content, "") + + whitespaces := regexp.MustCompile(` +`) + s.Content = whitespaces.ReplaceAllString(s.Content, " ") + + lines := strings.Split(s.Content, "\n")[1:] + + preDefinedEnv := os.Environ() + for _, cmdLine := range lines { + var pair []string + var ok bool + ok, pair, err = isInputRequest(cmdLine) + if err != nil { + break + } + + if ok { + if pair, err = inputRequest(pair); err != nil { + break + } + os.Setenv(pair[0], pair[1]) + continue + } + + err = runCmdLine(cmdLine, s.Dir, s.KeepScripts) + if err != nil { + break + } + } + + // reset the env + os.Clearenv() + for _, pair := range preDefinedEnv { + os.Setenv(strings.Split(pair, "=")[0], strings.Split(pair, "=")[1]) + } + return +} + +// GetTitle returns the title of this script +func (s *ShellScript) GetTitle() string { + return s.Title +} + +func isInputRequest(cmdLine string) (ok bool, pair []string, err error) { + var reg *regexp.Regexp + if reg, err = regexp.Compile(`^\w+=.+$`); err == nil { + items := strings.Split(cmdLine, "=") + if reg.MatchString(cmdLine) && len(items) == 2 { + pair = []string{strings.TrimSpace(items[0]), strings.TrimSpace(items[1])} + ok = true + } + } + return +} + +func inputRequest(pair []string) (result []string, err error) { + input := survey.Input{ + Message: pair[0], + Default: pair[1], + } + result = pair + + var value string + if err = survey.AskOne(&input, &value); err == nil { + result[1] = value + } + + return +} + +func runCmdLine(cmdLine, contextDir string, keepScripts bool) (err error) { + var shellFile string + if shellFile, err = writeAsShell(cmdLine, contextDir); err != nil { + fmt.Println(err) + return + } + if !keepScripts { + defer func() { + _ = os.RemoveAll(shellFile) + }() + } + + cmd := exec.Command("bash", path.Base(shellFile)) + cmd.Dir = contextDir + cmd.Env = os.Environ() + + var output []byte + if output, err = cmd.CombinedOutput(); err != nil { + fmt.Println(string(output), err) + return + } + fmt.Print(string(output)) + return +} + +func writeAsShell(content, dir string) (targetPath string, err error) { + var f *os.File + if f, err = os.CreateTemp(dir, "sh"); err == nil { + defer func() { + _ = f.Close() + }() + + targetPath = f.Name() + _, err = io.WriteString(f, content) + } + return +} + +var _ ScriptRunner = &ShellScript{} diff --git a/cli/shell_runner_test.go b/cli/shell_runner_test.go new file mode 100644 index 0000000..ba43614 --- /dev/null +++ b/cli/shell_runner_test.go @@ -0,0 +1,43 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsInputRequest(t *testing.T) { + tests := []struct { + name string + cmdLine string + expectOK bool + expectPair []string + expectErr bool + }{{ + name: "normal", + cmdLine: "name=linuxsuren", + expectOK: true, + expectPair: []string{"name", "linuxsuren"}, + expectErr: false, + }, { + name: "abnormal variable expression - with extra whitespace", + cmdLine: "name = linuxsuren", + expectOK: false, + expectPair: nil, + expectErr: false, + }, { + name: "complex characters in pair", + cmdLine: "vm=i-dy87owjl", + expectOK: true, + expectPair: []string{"vm", "i-dy87owjl"}, + expectErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ok, pair, err := isInputRequest(tt.cmdLine) + assert.Equal(t, tt.expectOK, ok) + assert.Equal(t, tt.expectPair, pair) + assert.Equal(t, tt.expectErr, err != nil) + }) + } +} diff --git a/cli/types.go b/cli/types.go new file mode 100644 index 0000000..b2ae228 --- /dev/null +++ b/cli/types.go @@ -0,0 +1,46 @@ +package cli + +// Script represents a script object +type Script struct { + Kind string + Title string + Content string + Dir string + KeepScripts bool +} + +type ScriptRunner interface { + Run() error + GetTitle() string +} + +func NewScriptRunners() ScriptRunners { + return []ScriptRunner{&QuitRunner{}} +} + +type ScriptRunners []ScriptRunner + +func (s ScriptRunners) GetTitles() (titles []string) { + titles = make([]string, len(s)) + for i, r := range s { + titles[i] = r.GetTitle() + } + return +} +func (s ScriptRunners) GetRunner(title string) ScriptRunner { + for _, runner := range s { + if runner.GetTitle() == title { + return runner + } + } + return nil +} + +type QuitRunner struct{} + +func (r *QuitRunner) Run() error { + return nil +} +func (r *QuitRunner) GetTitle() string { + return "Quit" +}