-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
cmd_exec_stdin.go
214 lines (167 loc) · 4.4 KB
/
cmd_exec_stdin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package main
import (
"bufio"
"flag"
"fmt"
"os"
"os/exec"
"strings"
"github.com/skx/sysbox/templatedcmd"
)
// Structure for our options and state.
type execSTDINCommand struct {
// testing the command
dryRun bool
// parallel job count
parallel int
// verbose flag
verbose bool
// field separator
split string
}
// Command holds a command we're going to execute in a worker-process.
//
// (Command in this sense is a system-binary / external process.)
type Command struct {
// args holds the command + args to execute.
args []string
}
// Arguments adds per-command args to the object.
func (es *execSTDINCommand) Arguments(f *flag.FlagSet) {
f.BoolVar(&es.dryRun, "dry-run", false, "Don't run the command.")
f.BoolVar(&es.verbose, "verbose", false, "Be verbose.")
f.IntVar(&es.parallel, "parallel", 1, "How many jobs to run in parallel.")
f.StringVar(&es.split, "split", "", "Split on a different character.")
}
// worker reads a command to execute from the channel, and executes it.
//
// The result is pushed back, but ignored.
func (es *execSTDINCommand) worker(id int, jobs <-chan Command, results chan<- int) {
for j := range jobs {
// Run the command, and get the output?
cmd := exec.Command(j.args[0], j.args[1:]...)
out, errr := cmd.CombinedOutput()
// error?
if errr != nil {
fmt.Printf("Error running '%s': %s\n", strings.Join(j.args, " "), errr.Error())
} else {
// Show the output
fmt.Printf("%s", out)
}
// Send a result to our output channel.
results <- 1
}
}
// Info returns the name of this subcommand.
func (es *execSTDINCommand) Info() (string, string) {
return "exec-stdin", `Execute a command for each line of STDIN.
Details:
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
field will be available as {1}, {2}, etc.
Examples:
$ echo -e "foo\tbar\nbar\tSteve" | sysbox exec-stdin echo {1}
foo
bar
Here you see that STDIN would contain:
foo bar
bar Steve
However only the first field was displayed, because {1} means the first field.
To show all input you'd run:
$ echo -e "foo\tbar\nbar\tSteve" | sysbox exec-stdin echo {}
foo bar
bar Steve
Flags:
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:
$ cat /etc/passwd | sysbox exec-stdin -split=: groups {1}
If you wish you can run the commands in parallel, using the -parallel flag
to denote how many simultaneous executions are permitted.
The only other flag is '-verbose', to show the command that would be
executed and '-dry-run' to avoid running anything.`
}
// Execute is invoked if the user specifies `exec-stdin` as the subcommand.
func (es *execSTDINCommand) Execute(args []string) int {
//
// Join all arguments, in case we have been given "{1}", "{2}", etc.
//
cmd := ""
for _, arg := range args {
cmd += arg
cmd += " "
}
//
// Ensure we have a command.
//
if cmd == "" {
fmt.Printf("Usage: sysbox exec-stdin command .. args {}..\n")
return 1
}
//
// Prepare to read line-by-line
//
scanner := bufio.NewReader(os.Stdin)
//
// The jobs we're going to add.
//
// We save these away so that we can allow parallel execution later.
//
var toRun []Command
//
// Read a line
//
line, err := scanner.ReadString(byte('\n'))
for err == nil && line != "" {
//
// Create the command to execute
//
run := templatedcmd.Expand(cmd, line, es.split)
//
// Show command if being verbose
//
if es.verbose || es.dryRun {
fmt.Printf("%s\n", strings.Join(run, " "))
}
//
// If we're not in "pretend"-mode then we'll save the
// constructed command away.
//
if !es.dryRun {
toRun = append(toRun, Command{args: run})
}
//
// Loop again
//
line, err = scanner.ReadString(byte('\n'))
}
//
// We've built up all the commands we're going to run now.
//
// Get the number, and create suitable channels.
//
num := len(toRun)
jobs := make(chan Command, num)
results := make(chan int, num)
//
// Launch the appropriate number of parallel workers.
//
for w := 1; w <= es.parallel; w++ {
go es.worker(w, jobs, results)
}
//
// Add all the pending jobs.
//
for _, j := range toRun {
jobs <- j
}
close(jobs)
//
// Await all the results.
//
for a := 1; a <= num; a++ {
<-results
}
return 0
}