Skip to content

Commit 1bb026b

Browse files
committed
Add mvx shell command for executing commands in mvx environment
- Add new 'mvx shell' command that executes shell commands with full mvx environment setup - Automatically configures PATH and environment variables (JAVA_HOME, etc.) for all configured tools - Uses mvx-shell interpreter for cross-platform compatibility - Supports variable expansion with $VAR and ${VAR} syntax - Includes comprehensive tests covering environment setup, tool execution, and error handling - Add documentation in website/content/shell-command.md with examples and use cases - Update README.md and commands.md with shell command references The shell command is useful for: - Testing tool installations and environment setup - Running ad-hoc commands with mvx-managed tools - Debugging environment variable issues - Accessing tools interactively without defining custom commands Examples: mvx shell 'echo $JAVA_HOME' mvx shell 'java -version' mvx shell 'go version' mvx shell env
1 parent 60378e2 commit 1bb026b

File tree

5 files changed

+482
-0
lines changed

5 files changed

+482
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Imagine cloning any project and running:
2727
# Or use tools directly with natural syntax
2828
./mvx mvn -V clean install # Maven with version flag
2929
./mvx --verbose mvn -X test # mvx verbose + Maven debug
30+
31+
# Execute shell commands in mvx environment
32+
./mvx shell 'echo $JAVA_HOME' # Show Java home with mvx tools
33+
./mvx shell 'java -version' # Run Java with mvx environment
3034
```
3135

3236
No more "works on my machine" - every developer gets the exact same environment.
@@ -386,6 +390,7 @@ The bootstrap scripts (`mvx` and `mvx.cmd`) are **shell/batch scripts** (not bin
386390
- [x] `mvx build` - Execute configured build commands
387391
- [x] `mvx test` - Execute configured test commands
388392
- [x] `mvx run` - Execute custom commands from configuration
393+
- [x] `mvx shell` - Execute shell commands in mvx environment
389394
- [x] `mvx tools` - Tool management and discovery
390395
- [x] `mvx info` - Detailed command information
391396

cmd/shell.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/gnodet/mvx/pkg/config"
9+
"github.com/gnodet/mvx/pkg/shell"
10+
"github.com/gnodet/mvx/pkg/tools"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// shellCmd represents the shell command for executing shell commands in mvx environment
15+
var shellCmd = &cobra.Command{
16+
Use: "shell [command...]",
17+
Short: "Execute shell commands in the mvx environment",
18+
Long: `Execute shell commands with the mvx-managed tools and environment setup.
19+
20+
This command runs shell commands using the mvx-shell interpreter with access to
21+
all mvx-managed tools and their environment variables.
22+
23+
Examples:
24+
mvx shell echo '$JAVA_HOME' # Show Java home directory
25+
mvx shell env # Show all environment variables
26+
mvx shell "mvn --version" # Run Maven with mvx environment
27+
mvx shell "echo '$PATH' | grep mvx" # Show mvx paths in PATH
28+
mvx shell cd /tmp && pwd # Change directory and show current path`,
29+
30+
Run: func(cmd *cobra.Command, args []string) {
31+
if err := runShellCommand(args); err != nil {
32+
printError("%v", err)
33+
os.Exit(1)
34+
}
35+
},
36+
}
37+
38+
func init() {
39+
rootCmd.AddCommand(shellCmd)
40+
}
41+
42+
// runShellCommand executes shell commands in the mvx environment
43+
func runShellCommand(args []string) error {
44+
// Get current working directory
45+
workDir, err := os.Getwd()
46+
if err != nil {
47+
return fmt.Errorf("failed to get current directory: %w", err)
48+
}
49+
50+
// Load configuration
51+
cfg, err := config.LoadConfig(workDir)
52+
if err != nil {
53+
printVerbose("No configuration found, using defaults: %v", err)
54+
cfg = &config.Config{}
55+
}
56+
57+
// Create tool manager
58+
manager, err := tools.NewManager()
59+
if err != nil {
60+
return fmt.Errorf("failed to create tool manager: %w", err)
61+
}
62+
63+
// Setup environment with mvx-managed tools
64+
env, err := setupShellEnvironment(cfg, manager, workDir)
65+
if err != nil {
66+
return fmt.Errorf("failed to setup environment: %w", err)
67+
}
68+
69+
// Join all arguments into a single command string
70+
var command string
71+
if len(args) == 0 {
72+
return fmt.Errorf("no command specified. Use 'mvx shell <command>' to execute shell commands")
73+
} else if len(args) == 1 {
74+
command = args[0]
75+
} else {
76+
command = strings.Join(args, " ")
77+
}
78+
79+
printVerbose("Executing shell command: %s", command)
80+
printVerbose("Working directory: %s", workDir)
81+
printVerbose("Environment variables: %d", len(env))
82+
83+
// Create mvx-shell instance and execute command
84+
mvxShell := shell.NewMVXShell(workDir, env)
85+
return mvxShell.Execute(command)
86+
}
87+
88+
// setupShellEnvironment sets up the environment for shell execution
89+
func setupShellEnvironment(cfg *config.Config, manager *tools.Manager, workDir string) ([]string, error) {
90+
// Start with current environment
91+
env := os.Environ()
92+
93+
// Get environment variables from tool manager (this handles all configured tools)
94+
toolEnv, err := manager.SetupEnvironment(cfg)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to setup tool environment: %w", err)
97+
}
98+
99+
// Merge tool environment into shell environment
100+
env = mergeEnvironment(env, toolEnv)
101+
102+
return env, nil
103+
}
104+
105+
// mergeEnvironment merges tool environment variables into the shell environment
106+
func mergeEnvironment(baseEnv []string, toolEnv map[string]string) []string {
107+
// Create a map of existing environment variables
108+
envMap := make(map[string]string)
109+
for _, envVar := range baseEnv {
110+
parts := strings.SplitN(envVar, "=", 2)
111+
if len(parts) == 2 {
112+
envMap[parts[0]] = parts[1]
113+
}
114+
}
115+
116+
// Override with tool environment variables
117+
for key, value := range toolEnv {
118+
envMap[key] = value
119+
}
120+
121+
// Convert back to slice
122+
result := make([]string, 0, len(envMap))
123+
for key, value := range envMap {
124+
result = append(result, fmt.Sprintf("%s=%s", key, value))
125+
}
126+
127+
return result
128+
}

test/shell_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package test
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// TestShellCommand tests the mvx shell command functionality
12+
func TestShellCommand(t *testing.T) {
13+
// Create a temporary directory for testing
14+
tempDir := t.TempDir()
15+
16+
// Create a test configuration with Java and Go
17+
configContent := `{
18+
project: {
19+
name: "shell-test",
20+
description: "Test configuration for shell command"
21+
},
22+
tools: {
23+
java: {
24+
version: "17",
25+
distribution: "zulu"
26+
},
27+
go: {
28+
version: "1.24.2"
29+
}
30+
}
31+
}`
32+
33+
// Create .mvx directory and config file
34+
mvxDir := filepath.Join(tempDir, ".mvx")
35+
if err := os.MkdirAll(mvxDir, 0755); err != nil {
36+
t.Fatalf("Failed to create .mvx directory: %v", err)
37+
}
38+
39+
configPath := filepath.Join(mvxDir, "config.json5")
40+
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
41+
t.Fatalf("Failed to write config file: %v", err)
42+
}
43+
44+
// Get the mvx binary path
45+
mvxBinary := findMvxBinary(t)
46+
47+
tests := []struct {
48+
name string
49+
command string
50+
expectError bool
51+
contains []string // Strings that should be present in output
52+
}{
53+
{
54+
name: "echo simple message",
55+
command: `echo "Hello from mvx shell"`,
56+
expectError: false,
57+
contains: []string{"Hello from mvx shell"},
58+
},
59+
{
60+
name: "show java home",
61+
command: `echo $JAVA_HOME`,
62+
expectError: false,
63+
contains: []string{".mvx/tools/java"},
64+
},
65+
{
66+
name: "run java version",
67+
command: `java -version`,
68+
expectError: false,
69+
contains: []string{"openjdk version", "17.0"},
70+
},
71+
{
72+
name: "run go version",
73+
command: `go version`,
74+
expectError: false,
75+
contains: []string{"go version", "1.24.2"},
76+
},
77+
{
78+
name: "check PATH contains mvx tools",
79+
command: `echo $PATH`,
80+
expectError: false,
81+
contains: []string{".mvx/tools/java", ".mvx/tools/go"},
82+
},
83+
{
84+
name: "create and list directory",
85+
command: `mkdir test-shell-dir && ls -la`,
86+
expectError: false,
87+
contains: []string{"test-shell-dir"},
88+
},
89+
{
90+
name: "change directory and show path",
91+
command: `cd test-shell-dir && pwd`,
92+
expectError: false,
93+
contains: []string{"test-shell-dir"},
94+
},
95+
}
96+
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
cmd := exec.Command(mvxBinary, "shell", tt.command)
100+
cmd.Dir = tempDir
101+
cmd.Env = os.Environ() // Don't set MVX_VERSION=dev to avoid using global dev binary
102+
103+
output, err := cmd.CombinedOutput()
104+
outputStr := string(output)
105+
106+
if tt.expectError && err == nil {
107+
t.Errorf("Expected error for command %s, but it succeeded. Output: %s", tt.command, outputStr)
108+
} else if !tt.expectError && err != nil {
109+
t.Errorf("Command %s failed unexpectedly: %v. Output: %s", tt.command, err, outputStr)
110+
}
111+
112+
// Check that expected strings are present in output
113+
for _, expected := range tt.contains {
114+
if !strings.Contains(outputStr, expected) {
115+
t.Errorf("Command %s output does not contain expected string '%s'. Output: %s", tt.command, expected, outputStr)
116+
}
117+
}
118+
119+
t.Logf("Command %s output: %s", tt.command, outputStr)
120+
})
121+
}
122+
}
123+
124+
// TestShellCommandHelp tests the shell command help functionality
125+
func TestShellCommandHelp(t *testing.T) {
126+
tempDir := t.TempDir()
127+
mvxBinary := findMvxBinary(t)
128+
129+
cmd := exec.Command(mvxBinary, "shell", "--help")
130+
cmd.Dir = tempDir
131+
cmd.Env = os.Environ()
132+
133+
output, err := cmd.CombinedOutput()
134+
if err != nil {
135+
t.Fatalf("Shell help command failed: %v. Output: %s", err, string(output))
136+
}
137+
138+
outputStr := string(output)
139+
expectedStrings := []string{
140+
"Execute shell commands",
141+
"mvx-managed tools",
142+
"Examples:",
143+
"mvx shell echo",
144+
"Usage:",
145+
}
146+
147+
for _, expected := range expectedStrings {
148+
if !strings.Contains(outputStr, expected) {
149+
t.Errorf("Help output does not contain expected string '%s'. Output: %s", expected, outputStr)
150+
}
151+
}
152+
}
153+
154+
// TestShellCommandNoArgs tests shell command without arguments
155+
func TestShellCommandNoArgs(t *testing.T) {
156+
tempDir := t.TempDir()
157+
mvxBinary := findMvxBinary(t)
158+
159+
cmd := exec.Command(mvxBinary, "shell")
160+
cmd.Dir = tempDir
161+
cmd.Env = os.Environ()
162+
163+
output, err := cmd.CombinedOutput()
164+
if err == nil {
165+
t.Errorf("Expected error when running shell command without arguments, but it succeeded. Output: %s", string(output))
166+
}
167+
168+
outputStr := string(output)
169+
if !strings.Contains(outputStr, "no command specified") {
170+
t.Errorf("Error message should mention 'no command specified'. Output: %s", outputStr)
171+
}
172+
}

website/content/commands.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ mvx --help
8484
### Environment Management
8585

8686
```bash
87+
# Execute shell commands in mvx environment
88+
./mvx shell 'echo $JAVA_HOME'
89+
./mvx shell 'java -version'
90+
./mvx shell env
91+
8792
# Show environment information
8893
./mvx env
8994

@@ -100,6 +105,14 @@ mvx --help
100105
./mvx clean tools
101106
```
102107

108+
The `shell` command is particularly useful for:
109+
- Testing tool installations and environment setup
110+
- Running ad-hoc commands with mvx-managed tools
111+
- Debugging environment variable issues
112+
- Accessing tools interactively without defining custom commands
113+
114+
See the [Shell Command](/shell-command) page for detailed examples and usage patterns.
115+
103116
## Custom Commands
104117

105118
Define custom commands in your `.mvx/config.json5` file. These become available as top-level commands.

0 commit comments

Comments
 (0)