Skip to content

Commit 83fb82a

Browse files
authored
Merge pull request [#22] (#22)
feat: interactive commands support
2 parents 9472489 + 652ecf4 commit 83fb82a

File tree

5 files changed

+118
-38
lines changed

5 files changed

+118
-38
lines changed

examples/run1/Runfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ tasks:
44
echo:
55
cmd:
66
- echo "hello from run1"
7+
8+
node:shell:
9+
interactive: true
10+
cmd:
11+
- node

pkg/runfile/run.go

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package runfile
22

33
import (
4-
"bufio"
4+
"errors"
55
"fmt"
66
"io"
77
"os"
88
"os/exec"
99
"path/filepath"
1010
"strings"
11+
"sync"
1112

1213
fn "github.com/nxtcoder17/runfile/pkg/functions"
1314
"golang.org/x/sync/errgroup"
@@ -20,8 +21,9 @@ type cmdArgs struct {
2021

2122
cmd string
2223

23-
stdout io.Writer
24-
stderr io.Writer
24+
interactive bool
25+
stdout io.Writer
26+
stderr io.Writer
2527
}
2628

2729
func createCommand(ctx Context, args cmdArgs) *exec.Cmd {
@@ -46,6 +48,10 @@ func createCommand(ctx Context, args cmdArgs) *exec.Cmd {
4648
c.Stdout = args.stdout
4749
c.Stderr = args.stderr
4850

51+
if args.interactive {
52+
c.Stdin = os.Stdin
53+
}
54+
4955
return c
5056
}
5157

@@ -55,6 +61,33 @@ type runTaskArgs struct {
5561
envOverrides map[string]string
5662
}
5763

64+
func processOutput(writer io.Writer, reader io.Reader, prefix *string) {
65+
prevByte := byte('\n')
66+
msg := make([]byte, 1)
67+
for {
68+
n, err := reader.Read(msg)
69+
if err != nil {
70+
// logger.Info("stdout", "msg", string(msg[:n]), "err", err)
71+
if errors.Is(err, io.EOF) {
72+
os.Stdout.Write(msg[:n])
73+
return
74+
}
75+
}
76+
77+
if n != 1 {
78+
continue
79+
}
80+
81+
if prevByte == '\n' && prefix != nil {
82+
// os.Stdout.WriteString(fmt.Sprintf("HERE... msg: '%s'", msg[:n]))
83+
os.Stdout.WriteString(*prefix)
84+
}
85+
86+
writer.Write(msg[:n])
87+
prevByte = msg[0]
88+
}
89+
}
90+
5891
func runTask(ctx Context, rf *Runfile, args runTaskArgs) *Error {
5992
runfilePath := fn.Must(filepath.Rel(rf.attrs.RootRunfilePath, rf.attrs.RunfilePath))
6093

@@ -108,44 +141,39 @@ func runTask(ctx Context, rf *Runfile, args runTaskArgs) *Error {
108141
stdoutR, stdoutW := io.Pipe()
109142
stderrR, stderrW := io.Pipe()
110143

144+
wg := sync.WaitGroup{}
145+
146+
wg.Add(1)
111147
go func() {
112-
r := bufio.NewReader(stdoutR)
113-
for {
114-
b, err := r.ReadBytes('\n')
115-
if err != nil {
116-
logger.Info("stdout", "msg", string(b), "err", err)
117-
// return
118-
break
119-
}
120-
fmt.Fprintf(os.Stdout, "%s %s", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))), b)
121-
}
148+
defer wg.Done()
149+
logPrefix := fmt.Sprintf("%s ", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))))
150+
processOutput(os.Stdout, stdoutR, &logPrefix)
122151
}()
123152

153+
wg.Add(1)
124154
go func() {
125-
r := bufio.NewReader(stderrR)
126-
for {
127-
b, err := r.ReadBytes('\n')
128-
if err != nil {
129-
fmt.Printf("hello err: %+v\n", err)
130-
logger.Info("stderr", "err", err)
131-
// return
132-
break
133-
}
134-
fmt.Fprintf(os.Stderr, "%s %s", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))), b)
135-
}
155+
defer wg.Done()
156+
logPrefix := fmt.Sprintf("%s ", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))))
157+
processOutput(os.Stderr, stderrR, &logPrefix)
136158
}()
137159

138160
cmd := createCommand(ctx, cmdArgs{
139-
shell: pt.Shell,
140-
env: ToEnviron(pt.Env),
141-
cmd: command.Command,
142-
workingDir: pt.WorkingDir,
143-
stdout: stdoutW,
144-
stderr: stderrW,
161+
shell: pt.Shell,
162+
env: ToEnviron(pt.Env),
163+
cmd: command.Command,
164+
workingDir: pt.WorkingDir,
165+
interactive: pt.Interactive,
166+
stdout: stdoutW,
167+
stderr: stderrW,
145168
})
146169
if err := cmd.Run(); err != nil {
147170
return formatErr(CommandFailed).WithErr(err)
148171
}
172+
173+
stdoutW.Close()
174+
stderrW.Close()
175+
176+
wg.Wait()
149177
}
150178

151179
return nil

pkg/runfile/task-parser.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313
)
1414

1515
type ParsedTask struct {
16-
Shell []string `json:"shell"`
17-
WorkingDir string `json:"workingDir"`
18-
Env map[string]string `json:"environ"`
19-
Commands []CommandJson `json:"commands"`
16+
Shell []string `json:"shell"`
17+
WorkingDir string `json:"workingDir"`
18+
Env map[string]string `json:"environ"`
19+
Interactive bool `json:"interactive,omitempty"`
20+
Commands []CommandJson `json:"commands"`
2021
}
2122

2223
func ParseTask(ctx Context, rf *Runfile, task Task) (*ParsedTask, *Error) {
@@ -136,10 +137,11 @@ func ParseTask(ctx Context, rf *Runfile, task Task) (*ParsedTask, *Error) {
136137
}
137138

138139
return &ParsedTask{
139-
Shell: task.Shell,
140-
WorkingDir: *task.Dir,
141-
Env: fn.MapMerge(globalEnv, taskDotenvVars, taskEnvVars),
142-
Commands: commands,
140+
Shell: task.Shell,
141+
WorkingDir: *task.Dir,
142+
Interactive: task.Interactive,
143+
Env: fn.MapMerge(globalEnv, taskDotenvVars, taskEnvVars),
144+
Commands: commands,
143145
}, nil
144146
}
145147

pkg/runfile/task-parser_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ func TestParseTask(t *testing.T) {
2929
return false
3030
}
3131

32+
if got.Interactive != want.Interactive {
33+
t.Logf("interactive not equal")
34+
return false
35+
}
36+
3237
if len(got.Env) != len(want.Env) {
3338
t.Logf("environments not equal")
3439
return false
@@ -629,6 +634,44 @@ echo "hi"
629634
},
630635
wantErr: true,
631636
},
637+
638+
{
639+
name: "[task] interactive task",
640+
args: args{
641+
ctx: nil,
642+
rf: &Runfile{
643+
Tasks: map[string]Task{
644+
"test": {
645+
ignoreSystemEnv: true,
646+
Interactive: true,
647+
Commands: []any{
648+
"echo i will call hello, now",
649+
map[string]any{
650+
"run": "hello",
651+
},
652+
},
653+
},
654+
"hello": {
655+
ignoreSystemEnv: true,
656+
Commands: []any{
657+
"echo hello everyone",
658+
},
659+
},
660+
},
661+
},
662+
taskName: "test",
663+
},
664+
want: &ParsedTask{
665+
Shell: []string{"sh", "-c"},
666+
WorkingDir: fn.Must(os.Getwd()),
667+
Interactive: true,
668+
Commands: []CommandJson{
669+
{Command: "echo i will call hello, now"},
670+
{Run: "hello"},
671+
},
672+
},
673+
wantErr: false,
674+
},
632675
}
633676

634677
testGlobalEnvVars := []test{

pkg/runfile/task.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type Task struct {
4141

4242
Requires []*Requires `json:"requires,omitempty"`
4343

44+
Interactive bool `json:"interactive,omitempty"`
45+
4446
// List of commands to be executed in given shell (default: sh)
4547
// can take multiple forms
4648
// - simple string

0 commit comments

Comments
 (0)