Skip to content

Commit

Permalink
feat: implement development and utility commands with pipeline support
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhahalong committed Dec 8, 2024
1 parent 59b8ac3 commit ebecff9
Show file tree
Hide file tree
Showing 30 changed files with 2,887 additions and 3,048 deletions.
148 changes: 81 additions & 67 deletions cmd/runshell/cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,89 +11,103 @@ import (
)

var (
workDir string
envVars []string
execDockerImage string
execWorkDir string
execEnvVars []string
)

// execCmd represents the exec command
var execCmd = &cobra.Command{
Use: "exec [command] [args...]",
Short: "Execute a command",
Long: `Execute a command with optional environment variables and working directory.
Example:
runshell exec ls -l
runshell exec --env KEY=VALUE --workdir /tmp ls -l
runshell exec --docker-image ubuntu:latest ls -l`,
Args: cobra.MinimumNArgs(1),
RunE: runExec,
}

func init() {
rootCmd.AddCommand(execCmd)
Long: `Execute a command with optional Docker container and environment variables.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("requires at least 1 arg(s), only received %d", len(args))
}

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

// 禁用标志解析,这样可以正确处理命令参数中的标志
execCmd.Flags().SetInterspersed(false)
}
// 创建管道执行器
pipeExec := executor.NewPipelineExecutor(exec)

// 检查是否包含管道符
cmdStr := strings.Join(args, " ")
if strings.Contains(cmdStr, "|") {
// 解析管道命令
pipeline, err := pipeExec.ParsePipeline(cmdStr)
if err != nil {
return fmt.Errorf("failed to parse pipeline: %w", err)
}

// 设置执行选项
pipeline.Options = &types.ExecuteOptions{
WorkDir: execWorkDir,
Env: parseEnvVars(execEnvVars),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
pipeline.Context = cmd.Context()

// 执行管道命令
result, err := pipeExec.ExecutePipeline(pipeline)
if err != nil {
return fmt.Errorf("failed to execute pipeline: %w", err)
}

if result.ExitCode != 0 {
return fmt.Errorf("pipeline failed with exit code %d", result.ExitCode)
}

return nil
}

func runExec(cmd *cobra.Command, args []string) error {
// 创建本地执行器
localExec := executor.NewLocalExecutor()
// 非管道命令的处理
ctx := &types.ExecuteContext{
Context: cmd.Context(),
Args: args,
Options: &types.ExecuteOptions{
WorkDir: execWorkDir,
Env: parseEnvVars(execEnvVars),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
},
}

// 如果指定了 Docker 镜像,创建 Docker 执行器
var exec interface{} = localExec
if dockerImage != "" {
dockerExec, err := executor.NewDockerExecutor(dockerImage)
result, err := exec.Execute(ctx)
if err != nil {
return fmt.Errorf("failed to create Docker executor: %v", err)
return fmt.Errorf("failed to execute command: %w", err)
}
exec = dockerExec
}

// 准备执行选项
opts := &types.ExecuteOptions{
WorkDir: workDir,
Env: make(map[string]string),
}

// 解析环境变量
for _, env := range envVars {
key, value, found := strings.Cut(env, "=")
if !found {
return fmt.Errorf("invalid environment variable format: %s", env)
if result.ExitCode != 0 {
return fmt.Errorf("command failed with exit code %d", result.ExitCode)
}
opts.Env[key] = value
}

// 执行命令
result, err := exec.(types.Executor).Execute(cmd.Context(), args[0], args[1:], opts)
if err != nil {
return fmt.Errorf("failed to execute command: %v", err)
}

// 输出结果
if result.Output != "" {
fmt.Print(result.Output)
}
return nil
},
}

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

// 如果是测试模式,返回错误而不是退出
if cmd.Context() != nil {
if result.ExitCode != 0 {
return fmt.Errorf("command failed with exit code %d", result.ExitCode)
func parseEnvVars(vars []string) map[string]string {
env := make(map[string]string)
for _, v := range vars {
parts := strings.SplitN(v, "=", 2)
if len(parts) == 2 {
env[parts[0]] = parts[1]
}
return nil
}

// 使用命令的退出码退出
os.Exit(result.ExitCode)
return nil
return env
}
62 changes: 8 additions & 54 deletions cmd/runshell/cmd/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,43 @@ package cmd

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExecCommand(t *testing.T) {
// 创建带取消的上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// 保存原始命令状态
origArgs := rootCmd.Args
origDockerImage := dockerImage
defer func() {
rootCmd.Args = origArgs
dockerImage = origDockerImage
}()

// 在测试环境中禁用 Docker
dockerImage = ""

tests := []struct {
name string
args []string
wantErr bool
skipCI bool // 在 CI 环境中跳过的测试
}{
{
name: "no args",
args: []string{"exec"},
args: []string{},
wantErr: true,
},
{
name: "valid command",
args: []string{"exec", "echo", "test"},
name: "echo command",
args: []string{"echo", "hello"},
wantErr: false,
},
{
name: "invalid command",
args: []string{"exec", "invalidcommand123"},
args: []string{"invalidcmd123"},
wantErr: true,
},
{
name: "command with workdir",
args: []string{"exec", "--workdir", "/tmp", "echo", "test"},
wantErr: false,
},
{
name: "command with env",
args: []string{"exec", "--env", "TEST=value", "env"},
wantErr: false,
},
{
name: "docker command",
args: []string{"exec", "--docker-image", "ubuntu:latest", "ls"},
wantErr: false,
skipCI: true, // 在 CI 环境中跳过 Docker 测试
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 检查是否在 CI 环境中且需要跳过
if tt.skipCI && os.Getenv("CI") == "true" {
t.Skip("Skipping in CI environment")
}

// 重置命令状态
rootCmd.ResetFlags()
rootCmd.SetArgs(tt.args)

// 设置上下文
rootCmd.SetContext(ctx)

// 执行命令
err := rootCmd.Execute()
execCmd.SetContext(context.Background())

// 验证结果
err := execCmd.RunE(execCmd, tt.args)
if tt.wantErr {
assert.Error(t, err, "Expected error for args: %v", tt.args)
assert.Error(t, err)
} else {
assert.NoError(t, err, "Unexpected error for args: %v", tt.args)
assert.NoError(t, err)
}
})
}
Expand Down
27 changes: 6 additions & 21 deletions cmd/runshell/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,19 @@ import (
"github.com/spf13/cobra"
)

var (
auditDir string
httpAddr string
dockerImage string
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "runshell",
Short: "A powerful command executor",
Long: `RunShell is a powerful command executor that supports local and Docker execution,
with built-in commands, audit logging, and HTTP server capabilities.
Example:
runshell exec ls -l
runshell exec --docker-image alpine:latest ls -l
runshell server --http :8080`,
Short: "A modern shell command executor",
Long: `RunShell is a modern shell command executor that supports:
- Local and Docker command execution
- HTTP API for remote execution
- Command piping and scripting
- Audit logging and security controls`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
rootCmd.PersistentFlags().StringVar(&auditDir, "audit-dir", "", "Directory for audit logs")
rootCmd.PersistentFlags().StringVar(&dockerImage, "docker-image", "ubuntu:latest", "Docker image to use for container execution")
}
Loading

0 comments on commit ebecff9

Please sign in to comment.