Skip to content

Commit

Permalink
Introduce scmpuff exec
Browse files Browse the repository at this point in the history
This is a replacement for the `eval $(scmpuff expand -- git ... )` approach.

Instead of needing to escape the arguments and then have the shell eval
them (which seems difficult/impossible in a cross-shell manner),
`scmpuff exec git ...` will expand the arguments and then exec git directly.
  • Loading branch information
jdelStrother authored and mroth committed Feb 8, 2020
1 parent 92aa7ea commit ffb3fc0
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 11 deletions.
155 changes: 155 additions & 0 deletions commands/exec/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package exec

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"

"github.com/spf13/cobra"
)

var expandRelative bool

// CommandExec expands numeric arguments then execs the command
//
// Allows expansion of numbered shortcuts, ranges of shortcuts, or standard paths.
// Numbered shortcut variables are produced by various commands, such as:
//
// * scmpuff_status() - git status implementation
func CommandExec() *cobra.Command {

var expandCmd = &cobra.Command{
Use: "exec <shortcuts...>",
Short: "Execute cmd with numeric shortcuts",
Long: `Expands numeric shortcuts to their full filepath and executes the command.
Takes a list of digits (1 4 5) or numeric ranges (1-5) or even both.`,
Run: func(cmd *cobra.Command, inputArgs []string) {
if len(inputArgs) < 1 {
cmd.Usage()
os.Exit(1)
}

expandedArgs := Process(inputArgs)
a := expandedArgs[1:]
subcmd := exec.Command(expandedArgs[0], a...)
subcmd.Stdin = os.Stdin
subcmd.Stdout = os.Stdout
subcmd.Stderr = os.Stderr
err := subcmd.Run()
if err == nil {
os.Exit(0)
}
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
} else {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
},
}

// --relative
expandCmd.Flags().BoolVarP(
&expandRelative,
"relative",
"r",
false,
"make path relative to current working directory",
)

return expandCmd
}

var expandArgDigitMatcher = regexp.MustCompile("^[0-9]{0,4}$")
var expandArgRangeMatcher = regexp.MustCompile("^([0-9]+)-([0-9]+)$")

// Process expands args and performs all substitution, then returns the argument array
func Process(args []string) []string {
var processedArgs []string
for _, arg := range expand(args) {
processed := evaluateEnvironment(arg)
processedArgs = append(processedArgs, processed)
}

return processedArgs
}

// Evaluates a string of arguments and expands environment variables.
func evaluateEnvironment(arg string) string {
expandedArg := os.ExpandEnv(arg)
if expandRelative {
return convertToRelativeIfFilePath(expandedArg)
}
return expandedArg
}

// For a given arg, try to determine if it represents a file, and if so, convert
// it to a relative filepath.
//
// Otherwise (or if any error conditions occur) return it unmolested.
func convertToRelativeIfFilePath(arg string) string {
if _, err := os.Stat(arg); err == nil {
wd, err1 := os.Getwd()
relPath, err2 := filepath.Rel(wd, arg)
if err1 == nil && err2 == nil {
return relPath
}
}
return arg
}

// Expand takes the list of arguments received from the command line and expands
// them given our special case rules.
//
// It handles converting numeric file placeholders and range placeholders into
// environment variable symbolic representation,
func expand(args []string) []string {
var results []string
for _, arg := range args {
results = append(results, expandArg(arg)...)
}
return results
}

// expandArg "expands" a single argument we received on the command line.
//
// It's possible that argument represents a numeric file placeholder, in which
// case we will replace it with the syntax to represent the environment variable
// that it will be held in (e.g. "$e1").
//
// It's also possible that argument may represent a range, in which case it will
// return multiple instances of environment variable placeholders.
func expandArg(arg string) []string {

// ...is it a single digit?
dm := expandArgDigitMatcher.FindString(arg)
if dm != "" {
// dont expand if its actually a numerically named file or directory!
if _, err := os.Stat(dm); err == nil {
return []string{arg} //return as-is
}

result := "$e" + dm
return []string{result}
}

// ...is it a range?
rm := expandArgRangeMatcher.FindStringSubmatch(arg)
if rm != nil {
lo, _ := strconv.Atoi(rm[1])
hi, _ := strconv.Atoi(rm[2])

var results []string
for i := lo; i <= hi; i++ {
results = append(results, "$e"+strconv.Itoa(i))
}
return results
}

// if it was neither, return as-is
return []string{arg}
}
48 changes: 48 additions & 0 deletions commands/exec/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package exec

import (
"reflect"
"strings"
"testing"
)

// Expansion of multiple args at the same time
var testExpandCases = []struct {
args, expected string
}{
{"1 3 7", "$e1 $e3 $e7"},
{"1-3 6", "$e1 $e2 $e3 $e6"},
{"seven 2-5 1", "seven $e2 $e3 $e4 $e5 $e1"},
}

func TestExpand(t *testing.T) {
for _, tc := range testExpandCases {
// split here to emulate what Cobra will pass us but still write tests with
// normal looking strings
args := strings.Split(tc.args, " ")
expected := strings.Split(tc.expected, " ")
actual := expand(args)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("ExpandArgs(%v): expected %v, actual %v", tc.args, expected, actual)
}
}
}

// Expansion of a single arg, which might still be a range
var testExpandArgCases = []struct {
arg string
expected []string
}{
{"1", []string{"$e1"}}, // single digit
{"1-3", []string{"$e1", "$e2", "$e3"}}, // range
{"seven", []string{"seven"}}, // no moleste
}

func TestExpandArg(t *testing.T) {
for _, tc := range testExpandArgCases {
actual := expandArg(tc.arg)
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("ExpandArg(%v): expected %v, actual %v", tc.arg, tc.expected, actual)
}
}
}
16 changes: 8 additions & 8 deletions commands/inits/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions commands/inits/data/git_wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ if type hub > /dev/null 2>&1; then export SCMPUFF_GIT_CMD="hub"; fi
function git() {
case $1 in
commit|blame|log|rebase|merge)
eval "$(scmpuff expand -- "$SCMPUFF_GIT_CMD" "$@")";;
scmpuff exec -- "$SCMPUFF_GIT_CMD" "$@";;
checkout|diff|rm|reset|restore)
eval "$(scmpuff expand --relative -- "$SCMPUFF_GIT_CMD" "$@")";;
scmpuff exec --relative -- "$SCMPUFF_GIT_CMD" "$@";;
add)
eval "$(scmpuff expand -- "$SCMPUFF_GIT_CMD" "$@")"
scmpuff exec -- "$SCMPUFF_GIT_CMD" "$@"
scmpuff_status;;
*)
"$SCMPUFF_GIT_CMD" "$@";;
Expand Down
25 changes: 25 additions & 0 deletions features/command_exec.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Feature: command expansion at command line

Background:
Given I am in a git repository
And an empty file named "a.txt"
And an empty file named "b.txt"
And I override the environment variables to:
| variable | value |
| e1 | a.txt |
| e2 | b.txt |

Scenario: Expand single digit case
When I successfully run `scmpuff exec -- git add 2`
And I successfully run `git status -s a.txt b.txt`
Then the stdout should contain exactly "A b.txt\n?? a.txt\n"

Scenario: Expand multiple digit case
When I successfully run `scmpuff exec -- git add 1 2`
And I successfully run `git status -s a.txt b.txt`
Then the stdout should contain exactly "A a.txt\nA b.txt\n"

Scenario: Expand ranged digit case
When I successfully run `scmpuff exec -- git add 1-2`
And I successfully run `git status -s a.txt b.txt`
Then the stdout should contain exactly "A a.txt\nA b.txt\n"
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"

"github.com/mroth/scmpuff/commands/exec"
"github.com/mroth/scmpuff/commands/expand"
"github.com/mroth/scmpuff/commands/inits"
"github.com/mroth/scmpuff/commands/status"
Expand Down Expand Up @@ -38,6 +39,7 @@ func main() {
puffCmd.AddCommand(introCmd)
puffCmd.AddCommand(versionCmd)
puffCmd.AddCommand(inits.CommandInit())
puffCmd.AddCommand(exec.CommandExec())
puffCmd.AddCommand(expand.CommandExpand())
puffCmd.AddCommand(status.CommandStatus())

Expand Down

0 comments on commit ffb3fc0

Please sign in to comment.