From bcf2ec8fa4d12f441a09fd4ad5bdeb4a88fc891e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 16:55:32 +0000 Subject: [PATCH 1/4] Add shell aliases feature to devbox.json Add a shell.aliases map to devbox.json that lets users define shell aliases declaratively instead of hand-writing them in the init_hook. Aliases are injected into the devbox shell after the init hook is sourced, so they can rely on anything the hook sets up. They use the shell's builtin alias command, which is compatible across bash, zsh, and fish. Values are single-quoted and escaped per-shell so they are passed verbatim. - configfile: add Aliases to shell config + accessor - config: add merged Aliases() accessor (root overrides included) - shell: render alias lines (sorted, escaped) into the shellrc - shellrc.tmpl / shellrc_fish.tmpl: emit aliases after hooks are sourced - schema: document shell.aliases - tests for parsing, merging, rendering, and escaping --- .schema/devbox.schema.json | 10 ++++ internal/devbox/shell.go | 46 +++++++++++++++ internal/devbox/shell_test.go | 81 +++++++++++++++++++++++++++ internal/devbox/shellrc.tmpl | 8 +++ internal/devbox/shellrc_fish.tmpl | 8 +++ internal/devconfig/config.go | 12 ++++ internal/devconfig/config_test.go | 43 ++++++++++++++ internal/devconfig/configfile/file.go | 15 +++++ 8 files changed, 223 insertions(+) diff --git a/.schema/devbox.schema.json b/.schema/devbox.schema.json index f540406976e..9dd8aba6c82 100644 --- a/.schema/devbox.schema.json +++ b/.schema/devbox.schema.json @@ -125,6 +125,16 @@ } } } + }, + "aliases": { + "description": "Shell aliases set up when entering the devbox shell, after the init_hook runs. Works in bash, zsh, and fish.", + "type": "object", + "patternProperties": { + ".*": { + "description": "The command the alias expands to.", + "type": "string" + } + } } }, "additionalProperties": false diff --git a/internal/devbox/shell.go b/internal/devbox/shell.go index 6de9327b8ea..e17e5df7dbf 100644 --- a/internal/devbox/shell.go +++ b/internal/devbox/shell.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "text/template" "time" @@ -325,6 +326,8 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) { ExportEnv string ShellName string + ShellAliases []string + RefreshAliasName string RefreshCmd string RefreshAliasEnvVar string @@ -337,6 +340,7 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) { HistoryFile: strings.TrimSpace(s.historyFile), ExportEnv: exportify(s.env), ShellName: string(s.name), + ShellAliases: s.aliasLines(), RefreshAliasName: s.devbox.refreshAliasName(), RefreshCmd: s.devbox.refreshCmd(), RefreshAliasEnvVar: s.devbox.refreshAliasEnvVar(), @@ -349,6 +353,48 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) { return path, nil } +// aliasLines returns the shell `alias` commands for the aliases defined in +// devbox.json, sorted by alias name for deterministic output. The returned +// lines use the current shell's builtin `alias` command (which is compatible +// across bash, zsh, and fish) and are injected into the shellrc after the init +// hook is sourced. It returns nil when there are no aliases or no config. +func (s *DevboxShell) aliasLines() []string { + if s.devbox == nil || s.devbox.Config() == nil { + return nil + } + aliases := s.devbox.Config().Aliases() + if len(aliases) == 0 { + return nil + } + + names := make([]string, 0, len(aliases)) + for name := range aliases { + names = append(names, name) + } + sort.Strings(names) + + lines := make([]string, 0, len(names)) + for _, name := range names { + lines = append(lines, fmt.Sprintf("alias %s=%s", name, s.quoteAliasValue(aliases[name]))) + } + return lines +} + +// quoteAliasValue single-quotes an alias value so it is passed verbatim to the +// shell's `alias` builtin, escaping any embedded quotes for the current shell. +func (s *DevboxShell) quoteAliasValue(value string) string { + if s.name == shFish { + // Inside fish single quotes, only \\ and \' are escape sequences. + value = strings.ReplaceAll(value, `\`, `\\`) + value = strings.ReplaceAll(value, `'`, `\'`) + return "'" + value + "'" + } + // POSIX shells (bash/zsh/ksh): close the quote, emit an escaped quote, + // then reopen, i.e. ' -> '\''. + value = strings.ReplaceAll(value, `'`, `'\''`) + return "'" + value + "'" +} + // setupShellStartupFiles creates initialization files for the shell by sourcing the user's originals. // We do this instead of linking or copying, so that we can set correct ZDOTDIR when sourcing // user's config files which may use the ZDOTDIR env var inside them. diff --git a/internal/devbox/shell_test.go b/internal/devbox/shell_test.go index ed466a67c4f..3aa03aacf7e 100644 --- a/internal/devbox/shell_test.go +++ b/internal/devbox/shell_test.go @@ -15,6 +15,7 @@ import ( "github.com/google/go-cmp/cmp" "go.jetify.com/devbox/internal/devbox/devopt" + "go.jetify.com/devbox/internal/devconfig" "go.jetify.com/devbox/internal/envir" "go.jetify.com/devbox/internal/shellgen" "go.jetify.com/devbox/internal/xdg" @@ -113,6 +114,86 @@ If the new shellrc is correct, you can update the golden file with: } } +func TestWriteDevboxShellrcAliases(t *testing.T) { + cfgJSON := `{ + "shell": { + "init_hook": "echo hi", + "aliases": { + "ll": "ls -la", + "gs": "git status", + "say": "echo it's here" + } + } +}` + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, "devbox.json"), []byte(cfgJSON), 0o644, + ); err != nil { + t.Fatal(err) + } + cfg, err := devconfig.Open(dir) + if err != nil { + t.Fatalf("Open config error: %v", err) + } + + tests := []struct { + shell name + want []string + }{ + { + shell: shBash, + want: []string{ + `alias gs='git status'`, + `alias ll='ls -la'`, + `alias say='echo it'\''s here'`, + }, + }, + { + shell: shFish, + want: []string{ + `alias gs='git status'`, + `alias ll='ls -la'`, + `alias say='echo it\'s here'`, + }, + }, + } + + for _, test := range tests { + t.Run(string(test.shell), func(t *testing.T) { + s := &DevboxShell{ + devbox: &Devbox{projectDir: dir, cfg: cfg}, + projectDir: dir, + name: test.shell, + } + path, err := s.writeDevboxShellrc() + if err != nil { + t.Fatalf("writeDevboxShellrc error: %v", err) + } + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + got := string(b) + + // Aliases must appear after the init hook is sourced. + hookIdx := strings.Index(got, ".hooks") + if hookIdx < 0 { + t.Fatalf("hooks not sourced in shellrc:\n%s", got) + } + for _, line := range test.want { + idx := strings.Index(got, line) + if idx < 0 { + t.Errorf("expected alias line %q in shellrc:\n%s", line, got) + continue + } + if idx < hookIdx { + t.Errorf("alias %q injected before init hook was sourced", line) + } + } + }) + } +} + func TestShellPath(t *testing.T) { tests := []struct { name string diff --git a/internal/devbox/shellrc.tmpl b/internal/devbox/shellrc.tmpl index 6cf050bd12c..037012a22c1 100644 --- a/internal/devbox/shellrc.tmpl +++ b/internal/devbox/shellrc.tmpl @@ -69,6 +69,14 @@ cd "{{ .ProjectDir }}" || exit cd "$working_dir" || exit +{{- if .ShellAliases }} + +# Set aliases defined in devbox.json. These run after the init hook so they can +# rely on anything the hook sets up. +{{ range .ShellAliases }}{{ . }} +{{ end -}} +{{- end }} + {{- if .ShellStartTime }} # log that the shell is interactive now! devbox log shell-interactive {{ .ShellStartTime }} diff --git a/internal/devbox/shellrc_fish.tmpl b/internal/devbox/shellrc_fish.tmpl index 26ecfb7eaf2..ea8d60dbcdf 100644 --- a/internal/devbox/shellrc_fish.tmpl +++ b/internal/devbox/shellrc_fish.tmpl @@ -63,6 +63,14 @@ source "{{ .HooksFilePath }}" cd "$workingDir" || exit +{{- if .ShellAliases }} + +# Set aliases defined in devbox.json. These run after the init hook so they can +# rely on anything the hook sets up. +{{ range .ShellAliases }}{{ . }} +{{ end -}} +{{- end }} + {{- if .ShellStartTime }} # log that the shell is interactive now! devbox log shell-interactive {{ .ShellStartTime }} diff --git a/internal/devconfig/config.go b/internal/devconfig/config.go index 445585dd04a..0d34d49e446 100644 --- a/internal/devconfig/config.go +++ b/internal/devconfig/config.go @@ -368,6 +368,18 @@ func (c *Config) InitHook() *shellcmd.Commands { return &commands } +// Aliases returns the merged shell aliases from this config and any included +// configs (plugins). Aliases defined in the root config take precedence over +// those from included configs. +func (c *Config) Aliases() map[string]string { + aliases := map[string]string{} + for _, i := range c.included { + maps.Copy(aliases, i.Aliases()) + } + maps.Copy(aliases, c.Root.Aliases()) + return aliases +} + func (c *Config) Scripts() configfile.Scripts { scripts := configfile.Scripts{} for _, i := range c.included { diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index 94d82cf7add..d62aaf07f25 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -297,6 +297,49 @@ func mkNestedDirs(t *testing.T) (root, child, nested string) { return root, child, nested } +func TestAliases(t *testing.T) { + dir := t.TempDir() + cfgJSON := `{ + "shell": { + "init_hook": "echo hi", + "aliases": { + "ll": "ls -la", + "gs": "git status" + } + } +}` + if err := os.WriteFile(filepath.Join(dir, configfile.DefaultName), []byte(cfgJSON), 0o644); err != nil { + t.Fatal(err) + } + cfg, err := Open(dir) + if err != nil { + t.Fatalf("Open error: %v", err) + } + got := cfg.Aliases() + want := map[string]string{"ll": "ls -la", "gs": "git status"} + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Aliases() mismatch (-want +got):\n%s", diff) + } +} + +func TestAliasesEmpty(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, configfile.DefaultName), + []byte(`{"shell": {"init_hook": "echo hi"}}`), + 0o644, + ); err != nil { + t.Fatal(err) + } + cfg, err := Open(dir) + if err != nil { + t.Fatalf("Open error: %v", err) + } + if got := cfg.Aliases(); len(got) != 0 { + t.Errorf("Aliases() = %v, want empty", got) + } +} + func TestDefault(t *testing.T) { path := filepath.Join(t.TempDir()) cfg := DefaultConfig() diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index de88ed952a5..4b645aa8db8 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -62,6 +62,12 @@ type shellConfig struct { // InitHook contains commands that will run at shell startup. InitHook *shellcmd.Commands `json:"init_hook,omitempty"` Scripts map[string]*shellcmd.Commands `json:"scripts,omitempty"` + // Aliases contains shell aliases that are set up when entering the devbox + // shell. They are injected after the init hook is sourced, so they can rely + // on anything the init hook sets up. The map key is the alias name and the + // value is the command it expands to. Aliases use the current shell's + // builtin `alias` command and work in bash, zsh, and fish. + Aliases map[string]string `json:"aliases,omitempty"` } type NixpkgsConfig struct { @@ -107,6 +113,15 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands { return c.Shell.InitHook } +// Aliases returns the shell aliases defined in this config file, keyed by +// alias name. It returns nil if no aliases are defined. +func (c *ConfigFile) Aliases() map[string]string { + if c == nil || c.Shell == nil { + return nil + } + return c.Shell.Aliases +} + // SaveTo writes the config to a file. func (c *ConfigFile) SaveTo(path string) error { return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644) From a1a0779103ba37959baaf9e0b4be58787b72df2d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 16:59:27 +0000 Subject: [PATCH 2/4] Move aliases to top-level devbox.json field Promote aliases from shell.aliases to a top-level "aliases" key in devbox.json. Behavior is unchanged: aliases are still injected into the devbox shell after the init hook is sourced. --- .schema/devbox.schema.json | 20 ++++++++++---------- internal/devbox/shell_test.go | 12 ++++++------ internal/devconfig/config.go | 2 +- internal/devconfig/config_test.go | 10 +++++----- internal/devconfig/configfile/file.go | 22 +++++++--------------- 5 files changed, 29 insertions(+), 37 deletions(-) diff --git a/.schema/devbox.schema.json b/.schema/devbox.schema.json index 9dd8aba6c82..eafd6a9f76e 100644 --- a/.schema/devbox.schema.json +++ b/.schema/devbox.schema.json @@ -125,20 +125,20 @@ } } } - }, - "aliases": { - "description": "Shell aliases set up when entering the devbox shell, after the init_hook runs. Works in bash, zsh, and fish.", - "type": "object", - "patternProperties": { - ".*": { - "description": "The command the alias expands to.", - "type": "string" - } - } } }, "additionalProperties": false }, + "aliases": { + "description": "Shell aliases set up when entering the devbox shell, after the init_hook runs. Works in bash, zsh, and fish.", + "type": "object", + "patternProperties": { + ".*": { + "description": "The command the alias expands to.", + "type": "string" + } + } + }, "include": { "description": "List of additional plugins to activate within your devbox shell", "type": "array", diff --git a/internal/devbox/shell_test.go b/internal/devbox/shell_test.go index 3aa03aacf7e..952286bfacb 100644 --- a/internal/devbox/shell_test.go +++ b/internal/devbox/shell_test.go @@ -117,12 +117,12 @@ If the new shellrc is correct, you can update the golden file with: func TestWriteDevboxShellrcAliases(t *testing.T) { cfgJSON := `{ "shell": { - "init_hook": "echo hi", - "aliases": { - "ll": "ls -la", - "gs": "git status", - "say": "echo it's here" - } + "init_hook": "echo hi" + }, + "aliases": { + "ll": "ls -la", + "gs": "git status", + "say": "echo it's here" } }` dir := t.TempDir() diff --git a/internal/devconfig/config.go b/internal/devconfig/config.go index 0d34d49e446..17fffc46cf6 100644 --- a/internal/devconfig/config.go +++ b/internal/devconfig/config.go @@ -376,7 +376,7 @@ func (c *Config) Aliases() map[string]string { for _, i := range c.included { maps.Copy(aliases, i.Aliases()) } - maps.Copy(aliases, c.Root.Aliases()) + maps.Copy(aliases, c.Root.Aliases) return aliases } diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index d62aaf07f25..9883d10e8ae 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -301,11 +301,11 @@ func TestAliases(t *testing.T) { dir := t.TempDir() cfgJSON := `{ "shell": { - "init_hook": "echo hi", - "aliases": { - "ll": "ls -la", - "gs": "git status" - } + "init_hook": "echo hi" + }, + "aliases": { + "ll": "ls -la", + "gs": "git status" } }` if err := os.WriteFile(filepath.Join(dir, configfile.DefaultName), []byte(cfgJSON), 0o644); err != nil { diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index 4b645aa8db8..f7a2c0e7f33 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -44,6 +44,13 @@ type ConfigFile struct { // Shell configures the devbox shell environment. Shell *shellConfig `json:"shell,omitempty"` + + // Aliases contains shell aliases that are set up when entering the devbox + // shell. They are injected after the init hook is sourced, so they can rely + // on anything the init hook sets up. The map key is the alias name and the + // value is the command it expands to. Aliases use the current shell's + // builtin `alias` command and work in bash, zsh, and fish. + Aliases map[string]string `json:"aliases,omitempty"` // Nixpkgs specifies the repository to pull packages from // Deprecated: Versioned packages don't need this Nixpkgs *NixpkgsConfig `json:"nixpkgs,omitempty"` @@ -62,12 +69,6 @@ type shellConfig struct { // InitHook contains commands that will run at shell startup. InitHook *shellcmd.Commands `json:"init_hook,omitempty"` Scripts map[string]*shellcmd.Commands `json:"scripts,omitempty"` - // Aliases contains shell aliases that are set up when entering the devbox - // shell. They are injected after the init hook is sourced, so they can rely - // on anything the init hook sets up. The map key is the alias name and the - // value is the command it expands to. Aliases use the current shell's - // builtin `alias` command and work in bash, zsh, and fish. - Aliases map[string]string `json:"aliases,omitempty"` } type NixpkgsConfig struct { @@ -113,15 +114,6 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands { return c.Shell.InitHook } -// Aliases returns the shell aliases defined in this config file, keyed by -// alias name. It returns nil if no aliases are defined. -func (c *ConfigFile) Aliases() map[string]string { - if c == nil || c.Shell == nil { - return nil - } - return c.Shell.Aliases -} - // SaveTo writes the config to a file. func (c *ConfigFile) SaveTo(path string) error { return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644) From 39c156a59879c40a0f6451200d0a451c5066b7f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 17:24:02 +0000 Subject: [PATCH 3/4] Validate alias names and commands in devbox.json Reject aliases with empty/whitespace names or empty commands at config load time, so they can't render invalid shell syntax (e.g. "alias =..." or "alias bad name=...") at shell startup. Mirrors script name validation. --- internal/devconfig/config_test.go | 22 ++++++++++++++++++++++ internal/devconfig/configfile/file.go | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index 9883d10e8ae..e453d4bb465 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -340,6 +340,28 @@ func TestAliasesEmpty(t *testing.T) { } } +func TestAliasesInvalid(t *testing.T) { + tests := map[string]string{ + "empty name": `{"aliases": {"": "ls -la"}}`, + "whitespace name": `{"aliases": {"bad name": "ls -la"}}`, + "empty command": `{"aliases": {"ll": ""}}`, + "whitespace command": `{"aliases": {"ll": " "}}`, + } + for name, cfgJSON := range tests { + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, configfile.DefaultName), []byte(cfgJSON), 0o644, + ); err != nil { + t.Fatal(err) + } + if _, err := Open(dir); err == nil { + t.Errorf("Open(%q) succeeded, want validation error", cfgJSON) + } + }) + } +} + func TestDefault(t *testing.T) { path := filepath.Join(t.TempDir()) cfg := DefaultConfig() diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index f7a2c0e7f33..490eee0ccc4 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -164,6 +164,7 @@ func validateConfig(cfg *ConfigFile) error { fns := []func(cfg *ConfigFile) error{ ValidateNixpkg, validateScripts, + validateAliases, } for _, fn := range fns { @@ -194,6 +195,23 @@ func validateScripts(cfg *ConfigFile) error { return nil } +func validateAliases(cfg *ConfigFile) error { + for name, command := range cfg.Aliases { + if strings.TrimSpace(name) == "" { + return errors.New("cannot have alias with empty name in devbox.json") + } + if whitespace.MatchString(name) { + return errors.Errorf( + "cannot have alias name with whitespace in devbox.json: %s", name) + } + if strings.TrimSpace(command) == "" { + return errors.Errorf( + "cannot have an empty alias command in devbox.json: %s", name) + } + } + return nil +} + func ValidateNixpkg(cfg *ConfigFile) error { hash := cfg.NixPkgsCommitHash() if hash == "" { From 2edffb518929cae7743556b8f9ebe19220cdc4d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 04:26:49 +0000 Subject: [PATCH 4/4] Support running aliases via devbox run Make aliases defined in devbox.json runnable with `devbox run `, not just inside the interactive shell. When the run target isn't a script, it's expanded against the merged aliases (mirroring how a shell expands an alias in command position), so aliases work even when the init hook hasn't defined them. Arguments are passed through. Also demonstrate plugin aliases + parent override in the local plugin example, with a test asserting the merged result, plus a run testscript exercising `devbox run ` and argument passthrough. --- examples/plugins/local/devbox.json | 3 ++ examples/plugins/local/my-plugin/plugin.json | 6 ++++ internal/devbox/devbox.go | 11 +++++- internal/devconfig/config_test.go | 35 ++++++++++++++++++++ testscripts/run/script.test.txt | 12 +++++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/examples/plugins/local/devbox.json b/examples/plugins/local/devbox.json index b537e30e930..b31437339c1 100644 --- a/examples/plugins/local/devbox.json +++ b/examples/plugins/local/devbox.json @@ -10,6 +10,9 @@ ] } }, + "aliases": { + "greet": "echo greeting-from-parent" + }, "include": [ "path:my-plugin/plugin.json" ] diff --git a/examples/plugins/local/my-plugin/plugin.json b/examples/plugins/local/my-plugin/plugin.json index 1a12917fc38..f1cb0f9e3ee 100644 --- a/examples/plugins/local/my-plugin/plugin.json +++ b/examples/plugins/local/my-plugin/plugin.json @@ -20,5 +20,11 @@ this is a comment inside the create files "echo \"ran local plugin init hook\"", "export MY_INIT_HOOK_VAR=BAR" ] + }, + // Aliases defined by the plugin. "greet" is overridden by the parent + // devbox.json, while "plugin_only" is provided solely by this plugin. + "aliases": { + "greet": "echo greeting-from-plugin", + "plugin_only": "echo from-plugin" } } diff --git a/internal/devbox/devbox.go b/internal/devbox/devbox.go index 43ce1f2935c..8ac001233d3 100644 --- a/internal/devbox/devbox.go +++ b/internal/devbox/devbox.go @@ -316,6 +316,15 @@ func (d *Devbox) RunScript(ctx context.Context, envOpts devopt.EnvOptions, cmdNa // which we don't want. So, one solution is to write the entire command and its arguments into the // file itself, but that may not be great if the variables contain sensitive information. Instead, // we save the entire command (with args) into the DEVBOX_RUN_CMD var, and then the script evals it. + // + // If cmdName is an alias defined in devbox.json, expand it to its command + // first, mirroring how a shell expands an alias that appears in command + // position. This lets `devbox run ` work without an interactive + // shell (i.e. even when the init hook hasn't defined the alias). + runCmd := cmdName + if alias, ok := d.cfg.Aliases()[cmdName]; ok { + runCmd = alias + } scriptBody, err := shellgen.ScriptBody(d, "eval $DEVBOX_RUN_CMD\n") if err != nil { return err @@ -326,7 +335,7 @@ func (d *Devbox) RunScript(ctx context.Context, envOpts devopt.EnvOptions, cmdNa } script := shellgen.ScriptPath(d.ProjectDir(), arbitraryCmdFilename) cmdWithArgs = []string{strconv.Quote(script)} - env["DEVBOX_RUN_CMD"] = strings.Join(append([]string{cmdName}, cmdArgs...), " ") + env["DEVBOX_RUN_CMD"] = strings.Join(append([]string{runCmd}, cmdArgs...), " ") } return nix.RunScript(d.projectDir, strings.Join(cmdWithArgs, " "), env) diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index e453d4bb465..9f747796156 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -362,6 +362,41 @@ func TestAliasesInvalid(t *testing.T) { } } +// TestExampleLocalPluginAliases loads the examples/plugins/local project and +// verifies that aliases defined in the plugin are merged into the parent +// config, and that an alias defined in the parent overrides the plugin's. +func TestExampleLocalPluginAliases(t *testing.T) { + exampleDir, err := filepath.Abs( + filepath.Join("..", "..", "examples", "plugins", "local"), + ) + if err != nil { + t.Fatal(err) + } + + cfg, err := Open(exampleDir) + if err != nil { + t.Fatalf("Open(%q) error: %v", exampleDir, err) + } + + lockfile, err := lock.GetFile(&testLockProject{dir: exampleDir}) + if err != nil { + t.Fatalf("lock.GetFile error: %v", err) + } + if err := cfg.LoadRecursive(lockfile); err != nil { + t.Fatalf("LoadRecursive error: %v", err) + } + + want := map[string]string{ + // Parent devbox.json overrides the plugin's "greet" alias. + "greet": "echo greeting-from-parent", + // "plugin_only" is contributed solely by the plugin. + "plugin_only": "echo from-plugin", + } + if diff := cmp.Diff(want, cfg.Aliases()); diff != "" { + t.Errorf("Aliases() mismatch (-want +got):\n%s", diff) + } +} + func TestDefault(t *testing.T) { path := filepath.Join(t.TempDir()) cfg := DefaultConfig() diff --git a/testscripts/run/script.test.txt b/testscripts/run/script.test.txt index 74e26846f18..f1042a83a57 100644 --- a/testscripts/run/script.test.txt +++ b/testscripts/run/script.test.txt @@ -19,6 +19,14 @@ stdout 'with script' exec devbox run -- hello -g directly stdout 'directly' +# An alias defined in devbox.json should be runnable via `devbox run` +exec devbox run greet +stdout 'hello from alias' + +# Alias arguments should be passed through to the expanded command +exec devbox run echo_args one two +stdout 'args: one two' + # TBD: Bad init hook should result in non-zero exit code #exec devbox --config bad_init run test #! stdout 'test' @@ -41,6 +49,10 @@ stdout 'directly' "hook_runs": "echo $HOOK", "hello_with_script": "hello -g \"with script\"" } + }, + "aliases": { + "greet": "echo \"hello from alias\"", + "echo_args": "echo args:" } }