shell.go

 1package util
 2
 3import (
 4	"context"
 5	"io"
 6	"strings"
 7
 8	tea "charm.land/bubbletea/v2"
 9	"mvdan.cc/sh/v3/interp"
10	"mvdan.cc/sh/v3/syntax"
11)
12
13// shellCommand wraps a shell interpreter to implement tea.ExecCommand.
14type shellCommand struct {
15	ctx    context.Context
16	file   *syntax.File
17	stdin  io.Reader
18	stdout io.Writer
19	stderr io.Writer
20}
21
22func (s *shellCommand) SetStdin(r io.Reader) {
23	s.stdin = r
24}
25
26func (s *shellCommand) SetStdout(w io.Writer) {
27	s.stdout = w
28}
29
30func (s *shellCommand) SetStderr(w io.Writer) {
31	s.stderr = w
32}
33
34func (s *shellCommand) Run() error {
35	runner, err := interp.New(
36		interp.StdIO(s.stdin, s.stdout, s.stderr),
37	)
38	if err != nil {
39		return err
40	}
41	return runner.Run(s.ctx, s.file)
42}
43
44// ExecShell executes a shell command string using tea.Exec.
45// The command is parsed and executed via mvdan.cc/sh/v3/interp, allowing
46// proper handling of shell syntax like quotes and arguments.
47func ExecShell(ctx context.Context, cmdStr string, callback tea.ExecCallback) tea.Cmd {
48	parsed, err := syntax.NewParser().Parse(strings.NewReader(cmdStr), "")
49	if err != nil {
50		return ReportError(err)
51	}
52
53	cmd := &shellCommand{
54		ctx:  ctx,
55		file: parsed,
56	}
57	return tea.Exec(cmd, callback)
58}