Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/run1/Runfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ tasks:
echo:
cmd:
- echo "hello from run1"

node:shell:
interactive: true
cmd:
- node
88 changes: 58 additions & 30 deletions pkg/runfile/run.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package runfile

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"

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

cmd string

stdout io.Writer
stderr io.Writer
interactive bool
stdout io.Writer
stderr io.Writer
}

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

if args.interactive {
c.Stdin = os.Stdin
}

return c
}

Expand All @@ -55,6 +61,33 @@ type runTaskArgs struct {
envOverrides map[string]string
}

func processOutput(writer io.Writer, reader io.Reader, prefix *string) {
prevByte := byte('\n')
msg := make([]byte, 1)
for {
n, err := reader.Read(msg)
if err != nil {
// logger.Info("stdout", "msg", string(msg[:n]), "err", err)
if errors.Is(err, io.EOF) {
os.Stdout.Write(msg[:n])
return
}
}

if n != 1 {
continue
}

if prevByte == '\n' && prefix != nil {
// os.Stdout.WriteString(fmt.Sprintf("HERE... msg: '%s'", msg[:n]))
os.Stdout.WriteString(*prefix)
}

writer.Write(msg[:n])
prevByte = msg[0]
}
}

func runTask(ctx Context, rf *Runfile, args runTaskArgs) *Error {
runfilePath := fn.Must(filepath.Rel(rf.attrs.RootRunfilePath, rf.attrs.RunfilePath))

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

wg := sync.WaitGroup{}

wg.Add(1)
go func() {
r := bufio.NewReader(stdoutR)
for {
b, err := r.ReadBytes('\n')
if err != nil {
logger.Info("stdout", "msg", string(b), "err", err)
// return
break
}
fmt.Fprintf(os.Stdout, "%s %s", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))), b)
}
defer wg.Done()
logPrefix := fmt.Sprintf("%s ", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))))
processOutput(os.Stdout, stdoutR, &logPrefix)
}()

wg.Add(1)
go func() {
r := bufio.NewReader(stderrR)
for {
b, err := r.ReadBytes('\n')
if err != nil {
fmt.Printf("hello err: %+v\n", err)
logger.Info("stderr", "err", err)
// return
break
}
fmt.Fprintf(os.Stderr, "%s %s", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))), b)
}
defer wg.Done()
logPrefix := fmt.Sprintf("%s ", ctx.theme.TaskPrefixStyle.Render(fmt.Sprintf("[%s]", strings.Join(trail, "/"))))
processOutput(os.Stderr, stderrR, &logPrefix)
}()

cmd := createCommand(ctx, cmdArgs{
shell: pt.Shell,
env: ToEnviron(pt.Env),
cmd: command.Command,
workingDir: pt.WorkingDir,
stdout: stdoutW,
stderr: stderrW,
shell: pt.Shell,
env: ToEnviron(pt.Env),
cmd: command.Command,
workingDir: pt.WorkingDir,
interactive: pt.Interactive,
stdout: stdoutW,
stderr: stderrW,
})
if err := cmd.Run(); err != nil {
return formatErr(CommandFailed).WithErr(err)
}

stdoutW.Close()
stderrW.Close()

wg.Wait()
}

return nil
Expand Down
18 changes: 10 additions & 8 deletions pkg/runfile/task-parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import (
)

type ParsedTask struct {
Shell []string `json:"shell"`
WorkingDir string `json:"workingDir"`
Env map[string]string `json:"environ"`
Commands []CommandJson `json:"commands"`
Shell []string `json:"shell"`
WorkingDir string `json:"workingDir"`
Env map[string]string `json:"environ"`
Interactive bool `json:"interactive,omitempty"`
Commands []CommandJson `json:"commands"`
}

func ParseTask(ctx Context, rf *Runfile, task Task) (*ParsedTask, *Error) {
Expand Down Expand Up @@ -136,10 +137,11 @@ func ParseTask(ctx Context, rf *Runfile, task Task) (*ParsedTask, *Error) {
}

return &ParsedTask{
Shell: task.Shell,
WorkingDir: *task.Dir,
Env: fn.MapMerge(globalEnv, taskDotenvVars, taskEnvVars),
Commands: commands,
Shell: task.Shell,
WorkingDir: *task.Dir,
Interactive: task.Interactive,
Env: fn.MapMerge(globalEnv, taskDotenvVars, taskEnvVars),
Commands: commands,
}, nil
}

Expand Down
43 changes: 43 additions & 0 deletions pkg/runfile/task-parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func TestParseTask(t *testing.T) {
return false
}

if got.Interactive != want.Interactive {
t.Logf("interactive not equal")
return false
}

if len(got.Env) != len(want.Env) {
t.Logf("environments not equal")
return false
Expand Down Expand Up @@ -629,6 +634,44 @@ echo "hi"
},
wantErr: true,
},

{
name: "[task] interactive task",
args: args{
ctx: nil,
rf: &Runfile{
Tasks: map[string]Task{
"test": {
ignoreSystemEnv: true,
Interactive: true,
Commands: []any{
"echo i will call hello, now",
map[string]any{
"run": "hello",
},
},
},
"hello": {
ignoreSystemEnv: true,
Commands: []any{
"echo hello everyone",
},
},
},
},
taskName: "test",
},
want: &ParsedTask{
Shell: []string{"sh", "-c"},
WorkingDir: fn.Must(os.Getwd()),
Interactive: true,
Commands: []CommandJson{
{Command: "echo i will call hello, now"},
{Run: "hello"},
},
},
wantErr: false,
},
}

testGlobalEnvVars := []test{
Expand Down
2 changes: 2 additions & 0 deletions pkg/runfile/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type Task struct {

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

Interactive bool `json:"interactive,omitempty"`

// List of commands to be executed in given shell (default: sh)
// can take multiple forms
// - simple string
Expand Down