-
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
92aa7ea
commit ffb3fc0
Showing
6 changed files
with
241 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters