Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions NUSHELL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Using Devbox with Nushell

Devbox now supports [nushell](https://github.com/nushell/nushell) through the `--format` flag on the `shellenv` command.

## Quick Start

**Add this to `~/.config/nushell/env.nu`:**

```nushell
devbox global shellenv --format nushell --preserve-path-stack -r
| lines
| parse "$env.{name} = \"{value}\""
| where name != null
| transpose -r
| into record
| load-env
```

This is equivalent to bash's `eval "$(devbox global shellenv)"` and runs on every fresh shell start.

---

## Global Configuration

To use devbox global packages with nushell, you need to load the environment similar to how bash/zsh use `eval "$(devbox global shellenv)"`.

### Dynamic loading with `load-env` - eval equivalent

Add this to `~/.config/nushell/env.nu` to regenerate and load devbox environment fresh every time, just like bash's `eval`:

```nushell
# Load devbox global environment dynamically (equivalent to bash eval)
devbox global shellenv --format nushell --preserve-path-stack -r
| lines
| parse "$env.{name} = \"{value}\""
| where name != null
| transpose -r
| into record
| load-env
```

- `--format nushell` - Output in nushell syntax
- `--preserve-path-stack` - Maintain existing PATH order if devbox is already active
- `-r` (recompute) - Always recompute the environment, prevents "out of date" warnings
6 changes: 4 additions & 2 deletions internal/boxcli/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ func ensureGlobalEnvEnabled(cmd *cobra.Command, args []string) error {
cmd.ErrOrStderr(),
`devbox global is not activated.

Add the following line to your shell's rcfile (e.g., ~/.bashrc or ~/.zshrc)
and restart your shell to fix this:
Add the following line to your shell's rcfile and restart your shell:

For bash/zsh (~/.bashrc or ~/.zshrc):
eval "$(devbox global shellenv)"

For nushell: See NUSHELL.md for setup instructions
`,
)
}
Expand Down
18 changes: 17 additions & 1 deletion internal/boxcli/shellenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type shellEnvCmdFlags struct {
pure bool
recomputeEnv bool
runInitHook bool
format string
}

// shellenvFlagDefaults are the flag default values that differ
Expand All @@ -46,7 +47,7 @@ func shellEnvCmd(defaults shellenvFlagDefaults) *cobra.Command {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), s)
if !strings.HasSuffix(os.Getenv("SHELL"), "fish") {
if flags.format != "nushell" && !strings.HasSuffix(os.Getenv("SHELL"), "fish") {
fmt.Fprintln(cmd.OutOrStdout(), "hash -r")
}
return nil
Expand Down Expand Up @@ -81,6 +82,11 @@ func shellEnvCmd(defaults shellenvFlagDefaults) *cobra.Command {
"Recompute environment if needed",
)

command.Flags().StringVar(
&flags.format, "format", "bash",
"Output format for shell environment (nushell)",
)

flags.config.register(command)
flags.envFlag.register(command)

Expand Down Expand Up @@ -115,6 +121,15 @@ func shellEnvFunc(
}
}

// Convert format string to ShellFormat type
var shellFormat devopt.ShellFormat
switch flags.format {
case "nushell":
shellFormat = devopt.ShellFormatNushell
default:
shellFormat = devopt.ShellFormatBash
}

envStr, err := box.EnvExports(ctx, devopt.EnvExportsOpts{
EnvOptions: devopt.EnvOptions{
Hooks: devopt.LifecycleHooks{
Expand All @@ -136,6 +151,7 @@ func shellEnvFunc(
},
NoRefreshAlias: flags.noRefreshAlias,
RunHooks: flags.runInitHook,
ShellFormat: shellFormat,
})
if err != nil {
return "", err
Expand Down
10 changes: 8 additions & 2 deletions internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,21 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st
return "", err
}

envStr := exportify(envs)
// Use the appropriate export format based on shell type
var envStr string
if opts.ShellFormat == devopt.ShellFormatNushell {
envStr = exportifyNushell(envs)
} else {
envStr = exportify(envs)
}

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

if !opts.NoRefreshAlias {
envStr += "\n" + d.refreshAlias()
envStr += "\n" + d.refreshAliasForShell(string(opts.ShellFormat))
}

return envStr, nil
Expand Down
8 changes: 8 additions & 0 deletions internal/devbox/devopt/devboxopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,18 @@ type UpdateOpts struct {
IgnoreMissingPackages bool
}

type ShellFormat string

const (
ShellFormatBash ShellFormat = "bash"
ShellFormatNushell ShellFormat = "nushell"
)

type EnvExportsOpts struct {
EnvOptions EnvOptions
NoRefreshAlias bool
RunHooks bool
ShellFormat ShellFormat
}

// EnvOptions configure the Devbox Environment in the `computeEnv` function.
Expand Down
51 changes: 51 additions & 0 deletions internal/devbox/envvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,57 @@ func exportify(vars map[string]string) string {
return strings.TrimSpace(strb.String())
}

// exportifyNushell formats vars as nushell environment variable assignments.
// Each line is of the form `$env.KEY = "value"` with special characters escaped.
func exportifyNushell(vars map[string]string) string {
// Nushell protected environment variables that cannot be set manually
// See: https://www.nushell.sh/book/environment.html#automatic-environment-variables
protectedVars := map[string]bool{
"CURRENT_FILE": true,
"FILE_PWD": true,
"LAST_EXIT_CODE": true,
"CMD_DURATION_MS": true,
"NU_VERSION": true,
"PWD": true, // Nushell manages this automatically
}

keys := make([]string, len(vars))
i := 0
for k := range vars {
keys[i] = k
i++
}
slices.Sort(keys) // for reproducibility

strb := strings.Builder{}
for _, key := range keys {
// Skip bash functions for nushell
if strings.HasPrefix(key, "BASH_FUNC_") && strings.HasSuffix(key, "%%") {
continue
}

// Skip nushell protected environment variables
if protectedVars[key] {
continue
}

// Nushell environment variable syntax: $env.KEY = "value"
strb.WriteString("$env.")
strb.WriteString(key)
strb.WriteString(` = "`)
for _, r := range vars[key] {
switch r {
// Escape special characters for nushell double-quoted strings
case '"', '\\':
strb.WriteRune('\\')
}
strb.WriteRune(r)
}
strb.WriteString("\"\n")
}
return strings.TrimSpace(strb.String())
}

// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
// but only if the key was not previously set by devbox
// Caveat, this won't mark the values as set by devbox automatically. Instead,
Expand Down
32 changes: 32 additions & 0 deletions internal/devbox/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ func (d *Devbox) refreshCmd() string {
return fmt.Sprintf(`eval "$(devbox %s)" && hash -r`, devboxCmd)
}

func (d *Devbox) refreshCmdForShell(format string) string {
devboxCmd := fmt.Sprintf("shellenv --preserve-path-stack -c %q", d.projectDir)
if d.isGlobal() {
devboxCmd = "global shellenv --preserve-path-stack -r --format " + format
} else {
devboxCmd = fmt.Sprintf("shellenv --preserve-path-stack -c %q --format %s", d.projectDir, format)
}

if format == "nushell" {
// Nushell doesn't have eval; use overlay or source with temporary file
return fmt.Sprintf(`devbox %s | save -f ~/.cache/devbox-env.nu; source ~/.cache/devbox-env.nu`, devboxCmd)
}
return fmt.Sprintf(`eval "$(devbox %s)" && hash -r`, devboxCmd)
}

func (d *Devbox) refreshAlias() string {
if isFishShell() {
return fmt.Sprintf(
Expand All @@ -74,3 +89,20 @@ fi`,
d.refreshCmd(),
)
}

func (d *Devbox) refreshAliasForShell(format string) string {
// For nushell format, provide instructions as a comment since aliases with pipes are complex
if format == "nushell" {
devboxCmd := "global shellenv --preserve-path-stack -r --format nushell"
if !d.isGlobal() {
devboxCmd = fmt.Sprintf("shellenv --preserve-path-stack -c %q --format nushell", d.projectDir)
}
return fmt.Sprintf(
`# To refresh your devbox environment in nushell, run:
# devbox %s | save -f ~/.cache/devbox-env.nu; source ~/.cache/devbox-env.nu`,
devboxCmd,
)
}
// Otherwise use the original refreshAlias function
return d.refreshAlias()
}