Skip to content

Commit

Permalink
feat: add exec command
Browse files Browse the repository at this point in the history
  • Loading branch information
hionay committed Jul 1, 2023
1 parent b343bb4 commit 40ddfda
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 29 deletions.
79 changes: 76 additions & 3 deletions cmd/tailer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package main

import (
"context"
"errors"
"io"
"log"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"

"github.com/urfave/cli/v2"
Expand All @@ -20,10 +24,29 @@ const (
flagNoColor = "no-color"
)

const (
commandExec = "exec"
commandExecShort = "e"
)

var version string

func main() {
app := &cli.App{
Name: "tailer",
Usage: "a simple CLI tool to insert lines when command output stops",
Name: "tailer",
Version: version,
Usage: "a simple CLI tool to insert lines when command output stops",
Before: func(c *cli.Context) error {
if c.IsSet(flagDash) {
if c.String(flagDash) == "" {
return errors.New("dash char cannot be empty")
}
if len(c.String(flagDash)) > 1 {
return errors.New("dash char cannot be longer than 1 character")
}
}
return nil
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flagNoColor,
Expand All @@ -37,7 +60,7 @@ func main() {
},
&cli.StringFlag{
Name: flagDash,
Usage: "dash string to print",
Usage: "dash character to print",
Value: tailer.DefaultDashString,
Aliases: []string{flagDashShort},
},
Expand All @@ -53,6 +76,20 @@ func main() {
tl := tailer.New(opts...)
return tl.Run(c.Context)
},
Commands: []*cli.Command{
{
Name: commandExec,
Usage: "execute a command and tail its output",
Before: func(c *cli.Context) error {
if c.NArg() == 0 {
return errors.New("arguments cannot be empty")
}
return nil
},
Aliases: []string{commandExecShort},
Action: execAction,
},
},
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
Expand All @@ -62,3 +99,39 @@ func main() {
log.Fatal(err)
}
}

func execAction(c *cli.Context) error {
first, tail := parseCommand(c.Args().First())
if tail == nil {
tail = c.Args().Tail()
}
cmd := exec.CommandContext(c.Context, first, tail...)
pr, pw := io.Pipe()
cmd.Stdout = pw
cmd.Stderr = pw

opts := []tailer.TailerOptionFunc{
tailer.WithAfterDuration(c.Duration(flagAfter)),
tailer.WithDashString(c.String(flagDash)),
tailer.WithInputReader(pr),
}
if c.Bool(flagNoColor) {
opts = append(opts, tailer.WithNoColor(true))
}
go func() {
if err := cmd.Run(); err != nil {
log.Printf("failed to run command: %v", err)
}
pw.Close()
}()
tl := tailer.New(opts...)
return tl.Run(c.Context)
}

func parseCommand(first string) (string, []string) {
if strings.Contains(first, " ") {
split := strings.Split(first, " ")
return split[0], split[1:]
}
return first, nil
}
2 changes: 1 addition & 1 deletion pkg/tailer/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"time"
)

const DefaultWaitDuration = 1 * time.Second
const DefaultDashString = "─"
const DefaultWaitDuration = 1 * time.Second

type options struct {
inrd io.Reader
Expand Down
49 changes: 24 additions & 25 deletions pkg/tailer/tailer.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,13 @@ func (tl *Tailer) Run(ctx context.Context) error {
_ = tl.Close()
}()

_, err := io.Copy(
writeFunc(func(p []byte) (int, error) {
tl.readch <- struct{}{}
tl.mu.Lock()
defer tl.mu.Unlock()
return tl.opts.outwr.Write(p)
}),
pr,
)
writerFn := writeFunc(func(p []byte) (int, error) {
tl.readch <- struct{}{}
tl.mu.Lock()
defer tl.mu.Unlock()
return tl.opts.outwr.Write(p)
})
_, err := io.Copy(writerFn, pr)
close(tl.readch)
tl.wg.Wait()
return err
Expand All @@ -73,9 +71,9 @@ func (tl *Tailer) Close() error {
func (tl *Tailer) worker(ctx context.Context) {
defer tl.wg.Done()

tm := time.NewTimer(0)
if !tm.Stop() {
<-tm.C
timer := time.NewTimer(0)
if !timer.Stop() {
<-timer.C
}

last := time.Now()
Expand All @@ -89,30 +87,31 @@ func (tl *Tailer) worker(ctx context.Context) {
_ = tl.Close()
return
}
if !tm.Stop() {
if !timer.Stop() {
select {
case <-tm.C:
case <-timer.C:
default:
}
}
tm.Reset(tl.opts.afterDuration)
case ts := <-tm.C:
timer.Reset(tl.opts.afterDuration)
case ts := <-timer.C:
tl.printLine(ts, last)
last = ts
}
}
}

func (tl *Tailer) printLine(ts, last time.Time) {
since := ts.Sub(last).Truncate(100 * time.Millisecond)
datestr := ts.Format("2006-01-02")
tmstr := ts.Format("15:04:05")
durstr := since.String()
filled := len(datestr) + len(tmstr) + len(durstr) + 3
var (
datestr = ts.Format("2006-01-02")
tmstr = ts.Format("15:04:05")
sincestr = ts.Sub(last).Truncate(100 * time.Millisecond).String()
)
filled := len(datestr) + len(tmstr) + len(sincestr) + 3
if !tl.opts.noColor {
datestr = color.GreenString(datestr)
tmstr = color.YellowString(tmstr)
durstr = color.BlueString(durstr)
sincestr = color.BlueString(sincestr)
}

width := 80
Expand All @@ -126,9 +125,9 @@ func (tl *Tailer) printLine(ts, last time.Time) {
}

var sb strings.Builder
sb.WriteString(datestr + " " + tmstr + " " + durstr + " ")
if rpt := width - filled; rpt > 0 {
sb.WriteString(strings.Repeat(tl.opts.dashString, rpt))
sb.WriteString(datestr + " " + tmstr + " " + sincestr + " ")
if count := width - filled; count > 0 {
sb.WriteString(strings.Repeat(tl.opts.dashString, count))
}
tl.mu.Lock()
_, _ = fmt.Fprintln(tl.opts.outwr, sb.String())
Expand Down

0 comments on commit 40ddfda

Please sign in to comment.