Skip to content

Commit

Permalink
Added template-expansion utility. (#10)
Browse files Browse the repository at this point in the history
* Added template-expansion utility.
  * And test-cases.
* Use our new template library for command-line expansion

This closes #9
  • Loading branch information
skx authored May 17, 2020
1 parent ac5d012 commit 01abeb1
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 112 deletions.
20 changes: 4 additions & 16 deletions cmd_choose_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/skx/sysbox/chooseui"
"github.com/skx/sysbox/templatedcmd"
)

// Structure for our options and state.
Expand Down Expand Up @@ -114,28 +115,15 @@ func (cf *chooseFileCommand) Execute(args []string) int {
//
// Split into command and arguments
//
pieces := strings.Fields(cf.exec)

//
// Expand the args - this is horrid
//
// Is a hack to ensure that things work if we
// have a selected filename with spaces inside it.
//
toRun := []string{}

for _, piece := range pieces {
piece = strings.ReplaceAll(piece, "{}", choice)
toRun = append(toRun, piece)
}
run := templatedcmd.Expand(cf.exec, choice, "")

//
// Run it.
//
cmd := exec.Command(toRun[0], toRun[1:]...)
cmd := exec.Command(run[0], run[1:]...)
out, errr := cmd.CombinedOutput()
if errr != nil {
fmt.Printf("Error running '%s': %s\n", cf.exec, errr.Error())
fmt.Printf("Error running '%v': %s\n", run, errr.Error())
return 1
}

Expand Down
20 changes: 4 additions & 16 deletions cmd_choose_stdin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/skx/sysbox/chooseui"
"github.com/skx/sysbox/templatedcmd"
)

// Structure for our options and state.
Expand Down Expand Up @@ -99,28 +100,15 @@ func (cs *chooseSTDINCommand) Execute(args []string) int {
//
// Split into command and arguments
//
pieces := strings.Fields(cs.exec)

//
// Expand the args - this is horrid
//
// Is a hack to ensure that things work if we
// have a selected filename with spaces inside it.
//
toRun := []string{}

for _, piece := range pieces {
piece = strings.ReplaceAll(piece, "{}", choice)
toRun = append(toRun, piece)
}
run := templatedcmd.Expand(cs.exec, choice, "")

//
// Run it.
//
cmd := exec.Command(toRun[0], toRun[1:]...)
cmd := exec.Command(run[0], run[1:]...)
out, errr := cmd.CombinedOutput()
if errr != nil {
fmt.Printf("Error running '%s': %s\n", cs.exec, errr.Error())
fmt.Printf("Error running '%v': %s\n", run, errr.Error())
return 1
}

Expand Down
90 changes: 10 additions & 80 deletions cmd_exec_stdin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"

"github.com/skx/sysbox/templatedcmd"
)

// Structure for our options and state.
Expand Down Expand Up @@ -38,7 +38,7 @@ func (es *execSTDINCommand) Info() (string, string) {
Details:
This command reads lines from STDIN, and executes the named command with
This command reads lines from STDIN, and executes the specified command with
that line as input.
The line read from STDIN will be available as '{}' and each space-separated
Expand Down Expand Up @@ -69,10 +69,10 @@ If you prefer you can split fields on specific characters, which is useful
for operating upon CSV files, or in case you wish to split '/etc/passwd' on
':' to work on usernames:
$ sysbox exec-stdin -split=: groups {1}
$ cat /etc/passwd | sysbox exec-stdin -split=: groups {1}
The only other flag is '-verbose', to show the command that would be
executed and 'dry-run' to avoid running anything.`
executed and '-dry-run' to avoid running anything.`
}

// Execute is invoked if the user specifies `exec-stdin` as the subcommand.
Expand Down Expand Up @@ -110,96 +110,26 @@ func (es *execSTDINCommand) Execute(args []string) int {
for err == nil && line != "" {

//
// Remove any leading/trailing whitespace
//
line = strings.TrimSpace(line)

//
// We're now going to build up the command
// to execute.
//
// {} -> The complete line read from STDIN
//
// {1} -> The first field of the input.
//
// {2} -> The second field of the input.
//
// {N} -> The Nth field of the input.
//
fields := strings.Fields(line)

//
// Different split character?
//
if es.split != "" {
fields = strings.Split(line, es.split)
}

//
// Copy the string
//
sh := cmd

//
// Look for {NNNN}
//
reg := regexp.MustCompile("({[0-9]+})")
matches := reg.FindAllStringSubmatch(sh, -1)

//
// For each match, perform the expansion
//
for _, v := range matches {

//
// Copy the match and remove the {}
//
// So we just have "1", "3", etc.
//
match := v[1]
match = strings.ReplaceAll(match, "{", "")
match = strings.ReplaceAll(match, "}", "")

//
// Convert the string to a number.
//
num, error := strconv.Atoi(match)
if error != nil {
fmt.Printf("failed to convert %s to number: %s", match, err.Error())
return 1
}

//
// If the field matches then we can replace it
//
if num >= 1 && num <= len(fields) {
sh = strings.ReplaceAll(sh, v[1], fields[num-1])
}

}

//
// Replace "{}" with the complete input.
// Create the command to execute
//
sh = strings.ReplaceAll(sh, "{}", line)
run := templatedcmd.Expand(cmd, line, es.split)

//
// Show command if being verbose
//
if es.verbose || es.dryRun {
fmt.Printf("%s\n", sh)
fmt.Printf("%s\n", strings.Join(run, " "))
}

//
// Run, unless we're not supposed to
//
if !es.dryRun {

pieces := strings.Fields(sh)
cmd := exec.Command(pieces[0], pieces[1:]...)
cmd := exec.Command(run[0], run[1:]...)
out, errr := cmd.CombinedOutput()
if errr != nil {
fmt.Printf("Error running '%s': %s\n", sh, errr.Error())
fmt.Printf("Error running '%v': %s\n", run, errr.Error())
return 1
}

Expand Down
124 changes: 124 additions & 0 deletions templatedcmd/templatedcmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Package templatedcmd allows expanding command-lines via a simple
// template-expansion process.
//
// For example the user might wish to run a command with an argument
// like so:
//
// command {}
//
// But we also support expanding the input into fields, and selecting
// only a single one, as per:
//
// $ echo "one two" | echo {1}
// # -> "one"
//
// All arguments are available via "{}" and "{N}" will refer to the
// Nth field of the given input.
//
package templatedcmd

import (
"regexp"
"strconv"
"strings"
)

// Expand performs the expansion of the given input, via the supplied
// template. As we allow input to be referred to as an array of fields
// we also let the user specify a split-string here.
//
// By default the input is split on whitespace, but you may supply another
// string instead.
func Expand(template string, input string, split string) []string {

//
// Regular expression for looking for ${1}, "${2}", "${3}", etc.
//
reg := regexp.MustCompile("({[0-9]+})")

//
// Trim our input of leading/trailing spaces.
//
input = strings.TrimSpace(input)

//
// Default to splitting the input on white-space.
//
fields := strings.Fields(input)
if split != "" {
fields = strings.Split(input, split)
}

//
// The return-value is an array of strings
//
cmd := []string{}

//
// We'll operate upon a temporary copy of our template,
// split into fields.
//
cmdTmp := strings.Fields(template)

//
// For each piece of the template-string look for
// "{}", and "{N}", expand appropriately.
//
for _, piece := range cmdTmp {

//
// Do we have a "{N}" ?
//
matches := reg.FindAllStringSubmatch(piece, -1)

//
// If so for each match, perform the expansion
//
for _, v := range matches {

//
// Copy the match and remove the {}
//
// So we just have "1", "3", etc.
//
match := v[1]
match = strings.ReplaceAll(match, "{", "")
match = strings.ReplaceAll(match, "}", "")

//
// Convert the string to a number, and if that
// worked we'll replace it with the appropriately
// numbered field.
//
num, err := strconv.Atoi(match)
if err == nil {

//
// If the field matches then we can replace it
//
if num >= 1 && num <= len(fields) {
piece = strings.ReplaceAll(piece, v[1], fields[num-1])
} else {
//
// Otherwise it's a field that doesn't
// exist. So it's replaced with ''.
//
piece = strings.ReplaceAll(piece, v[1], "")
}
}
}

//
// Now replace "{}" with the complete argument
//
piece = strings.ReplaceAll(piece, "{}", input)

// And append
cmd = append(cmd, piece)
}

//
// Now we should have an array of expanded strings.
//
return cmd
}
41 changes: 41 additions & 0 deletions templatedcmd/templatedcmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package templatedcmd

import (
"testing"
)

// Basic test
func TestBasic(t *testing.T) {

type TestCase struct {
template string
input string
split string
expected []string
}

tests := []TestCase{
{"xine {}", "This is a file", "", []string{"xine", "This is a file"}},
{"xine {1}", "This is a file", "", []string{"xine", "This"}},
{"xine {3}", "This is a file", "", []string{"xine", "a"}},
{"xine {10}", "This is a file", "", []string{"xine", ""}},
{"foo bar", "", "", []string{"foo", "bar"}},
{"id {1}", "root:0:0...", ":", []string{"id", "root"}},
}

for _, test := range tests {

out := Expand(test.template, test.input, test.split)

if len(out) != len(test.expected) {
t.Fatalf("Expected to have %d pieces, found %d", len(test.expected), len(out))
}

for i, x := range test.expected {

if out[i] != x {
t.Errorf("expected '%s' for piece %d, got '%s'", x, i, out[i])
}
}
}
}

0 comments on commit 01abeb1

Please sign in to comment.