Skip to content

Commit ebecff9

Browse files
committed
feat: implement development and utility commands with pipeline support
1 parent 59b8ac3 commit ebecff9

30 files changed

+2887
-3048
lines changed

cmd/runshell/cmd/exec.go

Lines changed: 81 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,89 +11,103 @@ import (
1111
)
1212

1313
var (
14-
workDir string
15-
envVars []string
14+
execDockerImage string
15+
execWorkDir string
16+
execEnvVars []string
1617
)
1718

18-
// execCmd represents the exec command
1919
var execCmd = &cobra.Command{
2020
Use: "exec [command] [args...]",
2121
Short: "Execute a command",
22-
Long: `Execute a command with optional environment variables and working directory.
23-
24-
Example:
25-
runshell exec ls -l
26-
runshell exec --env KEY=VALUE --workdir /tmp ls -l
27-
runshell exec --docker-image ubuntu:latest ls -l`,
28-
Args: cobra.MinimumNArgs(1),
29-
RunE: runExec,
30-
}
31-
32-
func init() {
33-
rootCmd.AddCommand(execCmd)
22+
Long: `Execute a command with optional Docker container and environment variables.`,
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
if len(args) < 1 {
25+
return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args))
26+
}
3427

35-
execCmd.Flags().StringVar(&workDir, "workdir", "", "Working directory for command execution")
36-
execCmd.Flags().StringArrayVar(&envVars, "env", nil, "Environment variables (KEY=VALUE)")
37-
execCmd.Flags().StringVar(&dockerImage, "docker-image", "", "Docker image to run command in")
28+
// 创建执行器
29+
var exec types.Executor
30+
if execDockerImage != "" {
31+
exec = executor.NewDockerExecutor(execDockerImage)
32+
} else {
33+
exec = executor.NewLocalExecutor()
34+
}
3835

39-
// 禁用标志解析,这样可以正确处理命令参数中的标志
40-
execCmd.Flags().SetInterspersed(false)
41-
}
36+
// 创建管道执行器
37+
pipeExec := executor.NewPipelineExecutor(exec)
38+
39+
// 检查是否包含管道符
40+
cmdStr := strings.Join(args, " ")
41+
if strings.Contains(cmdStr, "|") {
42+
// 解析管道命令
43+
pipeline, err := pipeExec.ParsePipeline(cmdStr)
44+
if err != nil {
45+
return fmt.Errorf("failed to parse pipeline: %w", err)
46+
}
47+
48+
// 设置执行选项
49+
pipeline.Options = &types.ExecuteOptions{
50+
WorkDir: execWorkDir,
51+
Env: parseEnvVars(execEnvVars),
52+
Stdin: os.Stdin,
53+
Stdout: os.Stdout,
54+
Stderr: os.Stderr,
55+
}
56+
pipeline.Context = cmd.Context()
57+
58+
// 执行管道命令
59+
result, err := pipeExec.ExecutePipeline(pipeline)
60+
if err != nil {
61+
return fmt.Errorf("failed to execute pipeline: %w", err)
62+
}
63+
64+
if result.ExitCode != 0 {
65+
return fmt.Errorf("pipeline failed with exit code %d", result.ExitCode)
66+
}
67+
68+
return nil
69+
}
4270

43-
func runExec(cmd *cobra.Command, args []string) error {
44-
// 创建本地执行器
45-
localExec := executor.NewLocalExecutor()
71+
// 非管道命令的处理
72+
ctx := &types.ExecuteContext{
73+
Context: cmd.Context(),
74+
Args: args,
75+
Options: &types.ExecuteOptions{
76+
WorkDir: execWorkDir,
77+
Env: parseEnvVars(execEnvVars),
78+
Stdin: os.Stdin,
79+
Stdout: os.Stdout,
80+
Stderr: os.Stderr,
81+
},
82+
}
4683

47-
// 如果指定了 Docker 镜像,创建 Docker 执行器
48-
var exec interface{} = localExec
49-
if dockerImage != "" {
50-
dockerExec, err := executor.NewDockerExecutor(dockerImage)
84+
result, err := exec.Execute(ctx)
5185
if err != nil {
52-
return fmt.Errorf("failed to create Docker executor: %v", err)
86+
return fmt.Errorf("failed to execute command: %w", err)
5387
}
54-
exec = dockerExec
55-
}
5688

57-
// 准备执行选项
58-
opts := &types.ExecuteOptions{
59-
WorkDir: workDir,
60-
Env: make(map[string]string),
61-
}
62-
63-
// 解析环境变量
64-
for _, env := range envVars {
65-
key, value, found := strings.Cut(env, "=")
66-
if !found {
67-
return fmt.Errorf("invalid environment variable format: %s", env)
89+
if result.ExitCode != 0 {
90+
return fmt.Errorf("command failed with exit code %d", result.ExitCode)
6891
}
69-
opts.Env[key] = value
70-
}
71-
72-
// 执行命令
73-
result, err := exec.(types.Executor).Execute(cmd.Context(), args[0], args[1:], opts)
74-
if err != nil {
75-
return fmt.Errorf("failed to execute command: %v", err)
76-
}
7792

78-
// 输出结果
79-
if result.Output != "" {
80-
fmt.Print(result.Output)
81-
}
93+
return nil
94+
},
95+
}
8296

83-
// 如果有错误,输出到标准错误
84-
if result.Error != nil {
85-
fmt.Fprintf(os.Stderr, "Error: %v\n", result.Error)
86-
}
97+
func init() {
98+
rootCmd.AddCommand(execCmd)
99+
execCmd.Flags().StringVar(&execDockerImage, "docker-image", "", "Docker image to run command in")
100+
execCmd.Flags().StringVar(&execWorkDir, "workdir", "", "Working directory for command execution")
101+
execCmd.Flags().StringArrayVar(&execEnvVars, "env", nil, "Environment variables (KEY=VALUE)")
102+
}
87103

88-
// 如果是测试模式,返回错误而不是退出
89-
if cmd.Context() != nil {
90-
if result.ExitCode != 0 {
91-
return fmt.Errorf("command failed with exit code %d", result.ExitCode)
104+
func parseEnvVars(vars []string) map[string]string {
105+
env := make(map[string]string)
106+
for _, v := range vars {
107+
parts := strings.SplitN(v, "=", 2)
108+
if len(parts) == 2 {
109+
env[parts[0]] = parts[1]
92110
}
93-
return nil
94111
}
95-
96-
// 使用命令的退出码退出
97-
os.Exit(result.ExitCode)
98-
return nil
112+
return env
99113
}

cmd/runshell/cmd/exec_test.go

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,89 +2,43 @@ package cmd
22

33
import (
44
"context"
5-
"os"
65
"testing"
76

87
"github.com/stretchr/testify/assert"
98
)
109

1110
func TestExecCommand(t *testing.T) {
12-
// 创建带取消的上下文
13-
ctx, cancel := context.WithCancel(context.Background())
14-
defer cancel()
15-
16-
// 保存原始命令状态
17-
origArgs := rootCmd.Args
18-
origDockerImage := dockerImage
19-
defer func() {
20-
rootCmd.Args = origArgs
21-
dockerImage = origDockerImage
22-
}()
23-
24-
// 在测试环境中禁用 Docker
25-
dockerImage = ""
26-
2711
tests := []struct {
2812
name string
2913
args []string
3014
wantErr bool
31-
skipCI bool // 在 CI 环境中跳过的测试
3215
}{
3316
{
3417
name: "no args",
35-
args: []string{"exec"},
18+
args: []string{},
3619
wantErr: true,
3720
},
3821
{
39-
name: "valid command",
40-
args: []string{"exec", "echo", "test"},
22+
name: "echo command",
23+
args: []string{"echo", "hello"},
4124
wantErr: false,
4225
},
4326
{
4427
name: "invalid command",
45-
args: []string{"exec", "invalidcommand123"},
28+
args: []string{"invalidcmd123"},
4629
wantErr: true,
4730
},
48-
{
49-
name: "command with workdir",
50-
args: []string{"exec", "--workdir", "/tmp", "echo", "test"},
51-
wantErr: false,
52-
},
53-
{
54-
name: "command with env",
55-
args: []string{"exec", "--env", "TEST=value", "env"},
56-
wantErr: false,
57-
},
58-
{
59-
name: "docker command",
60-
args: []string{"exec", "--docker-image", "ubuntu:latest", "ls"},
61-
wantErr: false,
62-
skipCI: true, // 在 CI 环境中跳过 Docker 测试
63-
},
6431
}
6532

6633
for _, tt := range tests {
6734
t.Run(tt.name, func(t *testing.T) {
68-
// 检查是否在 CI 环境中且需要跳过
69-
if tt.skipCI && os.Getenv("CI") == "true" {
70-
t.Skip("Skipping in CI environment")
71-
}
72-
73-
// 重置命令状态
74-
rootCmd.ResetFlags()
75-
rootCmd.SetArgs(tt.args)
76-
77-
// 设置上下文
78-
rootCmd.SetContext(ctx)
79-
80-
// 执行命令
81-
err := rootCmd.Execute()
35+
execCmd.SetContext(context.Background())
8236

83-
// 验证结果
37+
err := execCmd.RunE(execCmd, tt.args)
8438
if tt.wantErr {
85-
assert.Error(t, err, "Expected error for args: %v", tt.args)
39+
assert.Error(t, err)
8640
} else {
87-
assert.NoError(t, err, "Unexpected error for args: %v", tt.args)
41+
assert.NoError(t, err)
8842
}
8943
})
9044
}

cmd/runshell/cmd/root.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,19 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10-
var (
11-
auditDir string
12-
httpAddr string
13-
dockerImage string
14-
)
15-
16-
// rootCmd represents the base command when called without any subcommands
1710
var rootCmd = &cobra.Command{
1811
Use: "runshell",
19-
Short: "A powerful command executor",
20-
Long: `RunShell is a powerful command executor that supports local and Docker execution,
21-
with built-in commands, audit logging, and HTTP server capabilities.
22-
23-
Example:
24-
runshell exec ls -l
25-
runshell exec --docker-image alpine:latest ls -l
26-
runshell server --http :8080`,
12+
Short: "A modern shell command executor",
13+
Long: `RunShell is a modern shell command executor that supports:
14+
- Local and Docker command execution
15+
- HTTP API for remote execution
16+
- Command piping and scripting
17+
- Audit logging and security controls`,
2718
}
2819

29-
// Execute adds all child commands to the root command and sets flags appropriately.
3020
func Execute() {
3121
if err := rootCmd.Execute(); err != nil {
3222
fmt.Println(err)
3323
os.Exit(1)
3424
}
3525
}
36-
37-
func init() {
38-
rootCmd.PersistentFlags().StringVar(&auditDir, "audit-dir", "", "Directory for audit logs")
39-
rootCmd.PersistentFlags().StringVar(&dockerImage, "docker-image", "ubuntu:latest", "Docker image to use for container execution")
40-
}

0 commit comments

Comments
 (0)