Skip to content

Commit 0b9e37d

Browse files
omerxxLagojaclaude
authored
Nushell support for Global packages (#2743)
## Summary To load global packages with Nushell the user is required to `eval $(devbox global shellenv)`. Nushell doesn't have an `eval` equivalent and we need to: 1. Convert the output 2. Do this on the fly when the environment is loaded 3. Maintain previous behavior ## How was it tested? Locally with Nushell based setup trying to load both bash and nu scripts. ## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request, I represent that I have the right to license the contributions to the project maintainers under the Apache 2 License as stated in the [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). --------- Signed-off-by: John Lago <750845+Lagoja@users.noreply.github.com> Co-authored-by: John Lago <750845+Lagoja@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cae51c2 commit 0b9e37d

File tree

8 files changed

+150
-5
lines changed

8 files changed

+150
-5
lines changed

NUSHELL.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Using Devbox with Nushell
2+
3+
Devbox now supports [nushell](https://github.com/nushell/nushell) through the `--format` flag on the `shellenv` command.
4+
5+
## Quick Start
6+
7+
**Add this to `~/.config/nushell/env.nu`:**
8+
9+
```nushell
10+
devbox global shellenv --format nushell --preserve-path-stack -r
11+
| lines
12+
| parse "$env.{name} = \"{value}\""
13+
| where name != null
14+
| transpose -r
15+
| into record
16+
| load-env
17+
```
18+
19+
This is equivalent to bash's `eval "$(devbox global shellenv)"` and runs on every fresh shell start.
20+
21+
---
22+
23+
## Global Configuration
24+
25+
To use devbox global packages with nushell, you need to load the environment similar to how bash/zsh use `eval "$(devbox global shellenv)"`.
26+
27+
### Dynamic loading with `load-env` - eval equivalent
28+
29+
Add this to `~/.config/nushell/env.nu` to regenerate and load devbox environment fresh every time, just like bash's `eval`:
30+
31+
```nushell
32+
# Load devbox global environment dynamically (equivalent to bash eval)
33+
devbox global shellenv --format nushell --preserve-path-stack -r
34+
| lines
35+
| parse "$env.{name} = \"{value}\""
36+
| where name != null
37+
| transpose -r
38+
| into record
39+
| load-env
40+
```
41+
42+
- `--format nushell` - Output in nushell syntax
43+
- `--preserve-path-stack` - Maintain existing PATH order if devbox is already active
44+
- `-r` (recompute) - Always recompute the environment, prevents "out of date" warnings

internal/boxcli/global.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,12 @@ func ensureGlobalEnvEnabled(cmd *cobra.Command, args []string) error {
112112
cmd.ErrOrStderr(),
113113
`devbox global is not activated.
114114
115-
Add the following line to your shell's rcfile (e.g., ~/.bashrc or ~/.zshrc)
116-
and restart your shell to fix this:
115+
Add the following line to your shell's rcfile and restart your shell:
117116
117+
For bash/zsh (~/.bashrc or ~/.zshrc):
118118
eval "$(devbox global shellenv)"
119+
120+
For nushell: See NUSHELL.md for setup instructions
119121
`,
120122
)
121123
}

internal/boxcli/shellenv.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type shellEnvCmdFlags struct {
2424
pure bool
2525
recomputeEnv bool
2626
runInitHook bool
27+
format string
2728
}
2829

2930
// shellenvFlagDefaults are the flag default values that differ
@@ -46,7 +47,7 @@ func shellEnvCmd(defaults shellenvFlagDefaults) *cobra.Command {
4647
return err
4748
}
4849
fmt.Fprintln(cmd.OutOrStdout(), s)
49-
if !strings.HasSuffix(os.Getenv("SHELL"), "fish") {
50+
if flags.format != "nushell" && !strings.HasSuffix(os.Getenv("SHELL"), "fish") {
5051
fmt.Fprintln(cmd.OutOrStdout(), "hash -r")
5152
}
5253
return nil
@@ -81,6 +82,11 @@ func shellEnvCmd(defaults shellenvFlagDefaults) *cobra.Command {
8182
"Recompute environment if needed",
8283
)
8384

85+
command.Flags().StringVar(
86+
&flags.format, "format", "bash",
87+
"Output format for shell environment (nushell)",
88+
)
89+
8490
flags.config.register(command)
8591
flags.envFlag.register(command)
8692

@@ -115,6 +121,15 @@ func shellEnvFunc(
115121
}
116122
}
117123

124+
// Convert format string to ShellFormat type
125+
var shellFormat devopt.ShellFormat
126+
switch flags.format {
127+
case "nushell":
128+
shellFormat = devopt.ShellFormatNushell
129+
default:
130+
shellFormat = devopt.ShellFormatBash
131+
}
132+
118133
envStr, err := box.EnvExports(ctx, devopt.EnvExportsOpts{
119134
EnvOptions: devopt.EnvOptions{
120135
Hooks: devopt.LifecycleHooks{
@@ -136,6 +151,7 @@ func shellEnvFunc(
136151
},
137152
NoRefreshAlias: flags.noRefreshAlias,
138153
RunHooks: flags.runInitHook,
154+
ShellFormat: shellFormat,
139155
})
140156
if err != nil {
141157
return "", err

internal/devbox/devbox.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,15 +366,21 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st
366366
return "", err
367367
}
368368

369-
envStr := exportify(envs)
369+
// Use the appropriate export format based on shell type
370+
var envStr string
371+
if opts.ShellFormat == devopt.ShellFormatNushell {
372+
envStr = exportifyNushell(envs)
373+
} else {
374+
envStr = exportify(envs)
375+
}
370376

371377
if opts.RunHooks {
372378
hooksStr := ". \"" + shellgen.ScriptPath(d.ProjectDir(), shellgen.HooksFilename) + "\""
373379
envStr = fmt.Sprintf("%s\n%s;\n", envStr, hooksStr)
374380
}
375381

376382
if !opts.NoRefreshAlias {
377-
envStr += "\n" + d.refreshAlias()
383+
envStr += "\n" + d.refreshAliasForShell(string(opts.ShellFormat))
378384
}
379385

380386
return envStr, nil

internal/devbox/devopt/devboxopts.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,18 @@ type UpdateOpts struct {
6969
IgnoreMissingPackages bool
7070
}
7171

72+
type ShellFormat string
73+
74+
const (
75+
ShellFormatBash ShellFormat = "bash"
76+
ShellFormatNushell ShellFormat = "nushell"
77+
)
78+
7279
type EnvExportsOpts struct {
7380
EnvOptions EnvOptions
7481
NoRefreshAlias bool
7582
RunHooks bool
83+
ShellFormat ShellFormat
7684
}
7785

7886
// EnvOptions configure the Devbox Environment in the `computeEnv` function.

internal/devbox/envvars.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,57 @@ func exportify(vars map[string]string) string {
6060
return strings.TrimSpace(strb.String())
6161
}
6262

63+
// exportifyNushell formats vars as nushell environment variable assignments.
64+
// Each line is of the form `$env.KEY = "value"` with special characters escaped.
65+
func exportifyNushell(vars map[string]string) string {
66+
// Nushell protected environment variables that cannot be set manually
67+
// See: https://www.nushell.sh/book/environment.html#automatic-environment-variables
68+
protectedVars := map[string]bool{
69+
"CURRENT_FILE": true,
70+
"FILE_PWD": true,
71+
"LAST_EXIT_CODE": true,
72+
"CMD_DURATION_MS": true,
73+
"NU_VERSION": true,
74+
"PWD": true, // Nushell manages this automatically
75+
}
76+
77+
keys := make([]string, len(vars))
78+
i := 0
79+
for k := range vars {
80+
keys[i] = k
81+
i++
82+
}
83+
slices.Sort(keys) // for reproducibility
84+
85+
strb := strings.Builder{}
86+
for _, key := range keys {
87+
// Skip bash functions for nushell
88+
if strings.HasPrefix(key, "BASH_FUNC_") && strings.HasSuffix(key, "%%") {
89+
continue
90+
}
91+
92+
// Skip nushell protected environment variables
93+
if protectedVars[key] {
94+
continue
95+
}
96+
97+
// Nushell environment variable syntax: $env.KEY = "value"
98+
strb.WriteString("$env.")
99+
strb.WriteString(key)
100+
strb.WriteString(` = "`)
101+
for _, r := range vars[key] {
102+
switch r {
103+
// Escape special characters for nushell double-quoted strings
104+
case '"', '\\':
105+
strb.WriteRune('\\')
106+
}
107+
strb.WriteRune(r)
108+
}
109+
strb.WriteString("\"\n")
110+
}
111+
return strings.TrimSpace(strb.String())
112+
}
113+
63114
// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
64115
// but only if the key was not previously set by devbox
65116
// Caveat, this won't mark the values as set by devbox automatically. Instead,

internal/devbox/refresh.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,20 @@ fi`,
7474
d.refreshCmd(),
7575
)
7676
}
77+
78+
func (d *Devbox) refreshAliasForShell(format string) string {
79+
// For nushell format, provide instructions as a comment since aliases with pipes are complex
80+
if format == "nushell" {
81+
devboxCmd := "global shellenv --preserve-path-stack -r --format nushell"
82+
if !d.isGlobal() {
83+
devboxCmd = fmt.Sprintf("shellenv --preserve-path-stack -c %q --format nushell", d.projectDir)
84+
}
85+
return fmt.Sprintf(
86+
`# To refresh your devbox environment in nushell, run:
87+
# devbox %s | save -f ~/.cache/devbox-env.nu; source ~/.cache/devbox-env.nu`,
88+
devboxCmd,
89+
)
90+
}
91+
// Otherwise use the original refreshAlias function
92+
return d.refreshAlias()
93+
}

testscripts/add/add_platforms_flakeref.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exec devbox install
44

55
# aside: choose armv7l-linux to verify that the add actually works on the
66
# current host that is unlikely to be armv7l-linux
7+
78
exec devbox add github:F1bonacc1/process-compose/v1.87.0 --exclude-platform armv7l-linux
89
json.superset devbox.json expected_devbox1.json
910

0 commit comments

Comments
 (0)