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}