Skip to content

Commit

Permalink
Add support for running code snippets in addition to named commands
Browse files Browse the repository at this point in the history
- Resolves #3
- Adds support for perl, python, js, ruby code snippets
  • Loading branch information
saheljalal committed Sep 19, 2019
1 parent 88d2b14 commit d8994ed
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 45 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ nostromo init
- Build complex command trees
- Bash completion support
- Preserves flags and arguments
- Execute code snippets

## Usage

Expand Down Expand Up @@ -124,7 +125,7 @@ Subsequent calls to `foo bar` would replace the subs before running. This comman
```sh
foo bar baz sls
```
would finally run the following since the substitution is in scope:
would finally result in the following since the substitution is in scope:
```sh
oof rab zab //some/long/string
```
Expand All @@ -144,6 +145,19 @@ eval "$(nostromo completion)" # for bash
eval "$(nostromo completion --zsh)" # for zsh
```

### Execute Code Snippets
nostromo provides the ability to supply code snippets in the following languages for execution, in lieu of the standard shell command:
- `ruby` - runs ruby interpreter
- `python` - runs python interpreter
- `js` - runs node
- `perl` - runs perl interpreter

```sh
nostromo add cmd foo --code 'console.log("hello js")' --language js
```

For more complex snippets you can edit `~/.nostromo/manifest.json` directly but multiline JSON must be escaped correctly to work.

## Credits
- This tool was bootstrapped using [cobra](https://github.com/spf13/cobra).
- Colored logging provided by [aurora](https://github.com/logrusorgru/aurora).
Expand Down
43 changes: 39 additions & 4 deletions cmd/addcmd.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package cmd

import (
"fmt"
"strings"

"github.com/pokanop/nostromo/shell"
"github.com/pokanop/nostromo/task"
"github.com/spf13/cobra"
)

var description string
var (
description string
code string
language string
)

// addcmdCmd represents the addcmd command
var addcmdCmd = &cobra.Command{
Use: "cmd [key.path] [alias]",
Use: "cmd [key.path] [command] [options]",
Short: "Add a command to nostromo manifest",
Long: `Add a command to nostromo manifest for a given key path.
A key path is a '.' delimited string, e.g., "key.path" which represents
Expand All @@ -18,9 +26,13 @@ the alias which can be run as "key path" for the actual command provided.
This will create appropriate command scopes for all levels in
the provided key path. A command scope can a tree of sub commands
and substitutions.`,
Args: cobra.MinimumNArgs(2),
Args: addCmdArgs,
Run: func(cmd *cobra.Command, args []string) {
task.AddCommand(args[0], args[1], description)
name := ""
if len(args) > 1 {
name = args[1]
}
task.AddCommand(args[0], name, description, code, language)
},
}

Expand All @@ -29,4 +41,27 @@ func init() {

// Flags
addcmdCmd.Flags().StringVarP(&description, "description", "d", "", "Description of the command to add")
addcmdCmd.Flags().StringVarP(&code, "code", "c", "", "Code snippet to run for this command")
addcmdCmd.Flags().StringVarP(&language, "language", "l", "", "Language of code snippet (e.g., ruby, python, perl, js)")
}

func codeEmpty() bool {
return len(code) == 0 && len(language) == 0
}

func codeValid() bool {
return len(code) > 0 && len(language) > 0
}

func addCmdArgs(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("invalid number of arguments")
}
if len(args) < 2 && !codeValid() {
return fmt.Errorf("must provide command or code snippet")
}
if codeValid() && !shell.IsSupportedLanguage(language) {
return fmt.Errorf("invalid code snippet and language, must be in [%s]", strings.Join(shell.ValidLanguages(), ","))
}
return nil
}
15 changes: 2 additions & 13 deletions model/code.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
package model

import "fmt"

// Code container for snippet
type Code struct {
Language string `json:"language"`
Snippet string `json:"snippet"`
}

// executionString for execution translated from code snippet and language
func (c *Code) executionString() (string, error) {
switch c.Language {
case "ruby":
return fmt.Sprintf("ruby -e '%s'", c.Snippet), nil
case "python":
return fmt.Sprintf("python -c '%s'", c.Snippet), nil
case "shell":
return fmt.Sprintf("sh -c %s", c.Snippet), nil
}
return "", fmt.Errorf("unsupported language for code snippet: %s", c.Language)
func (c *Code) valid() bool {
return len(c.Snippet) > 0 && len(c.Language) > 0
}
13 changes: 11 additions & 2 deletions model/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (c *Command) Fields() map[string]interface{} {
"description": c.Description,
"commands": joinedCommands(c.Commands),
"substitutions": joinedSubs(c.Subs),
"code": c.Code != nil,
"code": c.Code.valid(),
}
}

Expand All @@ -53,6 +53,10 @@ func newCommand(name, alias, description string, code *Code) *Command {
alias = name
}

if code == nil {
code = &Code{}
}

return &Command{
KeyPath: alias,
Name: name,
Expand Down Expand Up @@ -168,7 +172,9 @@ func (c *Command) executionString(args []string) string {
func (c *Command) expand() string {
cmds := []string{}
c.reverseWalk(func(cmd *Command, stop *bool) {
if len(cmd.Name) > 0 {
if cmd.Code.valid() {
cmds = append(cmds, cmd.Code.Snippet)
} else if len(cmd.Name) > 0 {
cmds = append(cmds, cmd.Name)
}
})
Expand Down Expand Up @@ -228,6 +234,9 @@ func (c *Command) forwardWalk(fn func(*Command, *bool)) bool {

func (c *Command) link(parent *Command) {
c.parent = parent
if c.Code == nil {
c.Code = &Code{}
}
for _, cmd := range c.Commands {
cmd.link(c)
}
Expand Down
6 changes: 3 additions & 3 deletions model/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ func TestNewCommand(t *testing.T) {
code *Code
expected *Command
}{
{"empty alias", "cmd", "", "", nil, &Command{nil, "cmd", "cmd", "cmd", "", map[string]*Command{}, map[string]*Substitution{}, nil}},
{"empty name", "", "alias", "", nil, &Command{nil, "alias", "", "alias", "", map[string]*Command{}, map[string]*Substitution{}, nil}},
{"valid alias", "cmd", "cmd-alias", "description", nil, &Command{nil, "cmd-alias", "cmd", "cmd-alias", "description", map[string]*Command{}, map[string]*Substitution{}, nil}},
{"empty alias", "cmd", "", "", nil, &Command{nil, "cmd", "cmd", "cmd", "", map[string]*Command{}, map[string]*Substitution{}, &Code{}}},
{"empty name", "", "alias", "", nil, &Command{nil, "alias", "", "alias", "", map[string]*Command{}, map[string]*Substitution{}, &Code{}}},
{"valid alias", "cmd", "cmd-alias", "description", nil, &Command{nil, "cmd-alias", "cmd", "cmd-alias", "description", map[string]*Command{}, map[string]*Substitution{}, &Code{}}},
}

for _, test := range tests {
Expand Down
7 changes: 4 additions & 3 deletions model/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (m *Manifest) AsJSON() string {
}

// ExecutionString from input if possible or return error
func (m *Manifest) ExecutionString(args []string) (string, error) {
func (m *Manifest) ExecutionString(args []string) (string, string, error) {
for _, cmd := range m.Commands {
keyPath := cmd.shortestKeyPath(keypath.KeyPath(args))
if len(keyPath) > 0 {
Expand All @@ -131,15 +131,16 @@ func (m *Manifest) ExecutionString(args []string) (string, error) {
log.Debug("arguments:", args[count:])
}
}
return cmd.find(keyPath).executionString(args[count:]), nil
c := cmd.find(keyPath)
return c.Code.Language, c.executionString(args[count:]), nil
}
}

if m.Config.Verbose {
log.Debug("arguments:", args)
}

return "", fmt.Errorf("unable to find execution string")
return "", "", fmt.Errorf("unable to find execution string")
}

// Keys as ordered list of fields for logging
Expand Down
2 changes: 1 addition & 1 deletion model/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func TestManifestExecutionString(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := test.manifest.ExecutionString(keypath.Keys(test.keyPath))
_, actual, err := test.manifest.ExecutionString(keypath.Keys(test.keyPath))
if test.expErr && err == nil {
t.Errorf("expected error but got none")
} else if !test.expErr && err != nil {
Expand Down
42 changes: 36 additions & 6 deletions shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ const (
Zsh
)

var validLanguages = []string{"ruby", "python", "perl", "js", "sh"}

// Run a command on the shell
func Run(command string, verbose bool) error {
func Run(command, language string, verbose bool) error {
if len(command) == 0 {
return fmt.Errorf("cannot run empty command")
}

command = strings.TrimSuffix(command, "\n")

name, args := buildExecArgs(command)
name, args := buildExecArgs(command, language)
if verbose {
log.Debugf("executing: %s %s\n", name, strings.Join(args, " "))
}
Expand Down Expand Up @@ -97,9 +99,37 @@ func Which() Shell {
return Bash
}

func buildExecArgs(cmd string) (string, []string) {
if Which() == Zsh {
return "zsh", []string{"-ic", cmd}
// ValidLanguages that can be executed
func ValidLanguages() []string {
return validLanguages
}

// IsSupportedLanguage returns true if supported snippet language and false otherwise
func IsSupportedLanguage(language string) bool {
for _, l := range validLanguages {
if language == l {
return true
}
}
return false
}

func buildExecArgs(cmd, language string) (string, []string) {
switch language {
case "ruby":
return "ruby", []string{"-e", cmd}
case "python":
return "python", []string{"-c", cmd}
case "perl":
return "perl", []string{"-e", cmd}
case "js":
return "node", []string{"-e", cmd}
case "sh":
fallthrough
default:
if Which() == Zsh {
return "zsh", []string{"-ic", cmd}
}
return "bash", []string{"-ic", cmd}
}
return "bash", []string{"-ic", cmd}
}
19 changes: 15 additions & 4 deletions task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func GetConfig(key string) {
}

// AddCommand to the manifest
func AddCommand(keyPath, command, description string) {
func AddCommand(keyPath, command, description, code, language string) {
cfg := checkConfig()
if cfg == nil {
return
Expand All @@ -130,12 +130,23 @@ func AddCommand(keyPath, command, description string) {
return
}

cmd := cfg.Manifest.Find(keyPath)
if cmd == nil {
log.Error("unable to find newly created command")
return
}

cmd.Code = &model.Code{
Language: language,
Snippet: code,
}

err = saveConfig(cfg)
if err != nil {
log.Error(err)
}

log.Fields(cfg.Manifest.Find(keyPath))
log.Fields(cmd)
}

// RemoveCommand from the manifest
Expand Down Expand Up @@ -202,13 +213,13 @@ func Run(args []string) {
return
}

cmd, err := cfg.Manifest.ExecutionString(sanitizeArgs(args))
language, cmd, err := cfg.Manifest.ExecutionString(sanitizeArgs(args))
if err != nil {
log.Error(err)
return
}

err = shell.Run(cmd, cfg.Manifest.Config.Verbose)
err = shell.Run(cmd, language, cfg.Manifest.Config.Verbose)
if err != nil {
log.Error(err)
}
Expand Down

0 comments on commit d8994ed

Please sign in to comment.