Skip to content

Commit 2f25b12

Browse files
authored
Add support for fish (#669)
## Summary Adds support for `fish` shell. When running `devbox shell`: We write the post-initialization steps into a "shellrc" file (see `shellrc_fish.tmpl`). I attempted to reuse the existing `shellrc.tmpl`, but the differences in syntax between fish and other shells were large enough that I had to separate them. Next, we invoke `fish -C <shellrc>`. This will make `fish` use the user's existing config first, and _then_ run whatever is in `<shellrc>`, which is similar to what we do for non-fish shells. When running `devbox run`: - If unified env is ON, then we invoke `sh -c <script>` just like we do for every other shell. - If unified env is OFF, then we'll invoke `fish -C <shellrc> -c <script>`. However, note that #650 already enables unified env by default, so this branch is mainly here just in case something goes wrong with unified env and we need to revert. Caveats: - We don't check any of the syntax in devbox.json's init hook or scripts. If an init hook uses fish-specific syntax, then it will work for users using `fish`, but it won't for users using a different shell. Similarly for scripts. Conversely, if an init hook uses POSIX syntax that fish doesn't support (e.g. `FOO=bar`), then the init hook will fail for fish users. - Any plugins that use init hooks should write them in such a way that they work for both fish and non-fish. So far, it seems only `pip` and `ruby` use init hooks, and they're written in a compatible way (AFAIK). ## How was it tested? I tested that both `devbox shell` and `devbox run` worked with and without unified env. I verified that `PATH` was being set, user's config is being run (for shell only), init hooks are being run, history file is set (shell only), prompt is set (shell only), and scripts are written and run. To test with fish, it's as easy as invoking devbox as such: ``` SHELL=fish devbox shell ```
1 parent 200c6cf commit 2f25b12

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed

internal/nix/shell.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@ import (
1919
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2020
"go.jetpack.io/devbox/internal/boxcli/usererr"
2121
"go.jetpack.io/devbox/internal/debug"
22+
"go.jetpack.io/devbox/internal/xdg"
2223
)
2324

2425
//go:embed shellrc.tmpl
2526
var shellrcText string
2627
var shellrcTmpl = template.Must(template.New("shellrc").Parse(shellrcText))
2728

29+
//go:embed shellrc_fish.tmpl
30+
var fishrcText string
31+
var fishrcTmpl = template.Must(template.New("shellrc_fish").Parse(fishrcText))
32+
2833
type name string
2934

3035
const (
3136
shUnknown name = ""
3237
shBash name = "bash"
3338
shZsh name = "zsh"
3439
shKsh name = "ksh"
40+
shFish name = "fish"
3541
shPosix name = "posix"
3642
)
3743

@@ -85,6 +91,9 @@ func DetectShell(opts ...ShellOption) (*Shell, error) {
8591
case "ksh":
8692
sh.name = shKsh
8793
sh.userShellrcPath = rcfilePath(".kshrc")
94+
case "fish":
95+
sh.name = shFish
96+
sh.userShellrcPath = fishConfig()
8897
case "dash", "ash", "sh":
8998
sh.name = shPosix
9099
sh.userShellrcPath = os.Getenv("ENV")
@@ -172,6 +181,10 @@ func rcfilePath(basename string) string {
172181
return filepath.Join(home, basename)
173182
}
174183

184+
func fishConfig() string {
185+
return filepath.Join(xdg.ConfigDir(), "fish", "config.fish")
186+
}
187+
175188
func (s *Shell) Run(nixShellFilePath, nixFlakesFilePath string) error {
176189
// Copy the current PATH into nix-shell, but clean and remove some
177190
// directories that are incompatible.
@@ -342,6 +355,13 @@ func (s *Shell) shellRCOverrides(shellrc string) (extraEnv []string, extraArgs [
342355
extraEnv = []string{fmt.Sprintf(`ZDOTDIR=%s`, shellescape.Quote(filepath.Dir(shellrc)))}
343356
case shKsh, shPosix:
344357
extraEnv = []string{fmt.Sprintf(`ENV=%s`, shellescape.Quote(shellrc))}
358+
case shFish:
359+
if featureflag.UnifiedEnv.Enabled() {
360+
extraArgs = []string{"-C", ". " + shellrc}
361+
} else {
362+
// Needs quotes because it's wrapped inside the nix-shell command
363+
extraArgs = []string{"-C", shellescape.Quote(". " + shellrc)}
364+
}
345365
}
346366
return extraEnv, extraArgs
347367
}
@@ -394,7 +414,12 @@ func (s *Shell) writeDevboxShellrc() (path string, err error) {
394414
pathPrepend = s.pkgConfigDir + ":" + pathPrepend
395415
}
396416

397-
err = shellrcTmpl.Execute(shellrcf, struct {
417+
tmpl := shellrcTmpl
418+
if s.name == shFish {
419+
tmpl = fishrcTmpl
420+
}
421+
422+
err = tmpl.Execute(shellrcf, struct {
398423
ProjectDir string
399424
OriginalInit string
400425
OriginalInitPath string

internal/nix/shellrc_fish.tmpl

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{{- /*
2+
3+
This template defines the shellrc file that the devbox shell will run at
4+
startup when using the fish shell.
5+
6+
It does _not_ include the user's original fish config, because unlike other
7+
shells, fish has multiple files as part of its config, and it's difficult
8+
to start a fish shell with a custom fish config. Instead, we let fish read
9+
the user's original config directly, and run these commands next.
10+
11+
Devbox needs to ensure that the shell's PATH, prompt, and a few other things are
12+
set correctly after the user's shellrc runs. The commands to do this are in
13+
the "Devbox Post-init Hook" section.
14+
15+
This file is useful for debugging shell errors, so try to keep the generated
16+
content readable.
17+
18+
*/ -}}
19+
20+
{{- if .UnifiedEnv -}}
21+
# Run the shell hook defined in shell.nix or flake.nix
22+
eval $shellHook
23+
24+
{{ end -}}
25+
26+
# Begin Devbox Post-init Hook
27+
28+
{{- /*
29+
NOTE: fish_add_path doesn't play nicely with colon:separated:paths, and I'd rather not
30+
add string-splitting logic here nor parametrize computeNixEnv based on the shell being
31+
used. So here we (ab)use the fact that using "export" ahead of the variable definition
32+
makes fish do exactly what we want and behave in the same way as other shells.
33+
*/ -}}
34+
{{ if .UnifiedEnv -}}
35+
export PATH="$DEVBOX_PATH_PREPEND:$PATH"
36+
{{- else -}}
37+
export PATH="{{ .PathPrepend }}:$PATH"
38+
{{- end }}
39+
40+
{{- /*
41+
Set the history file by setting fish_history. This is not exactly the same as with other
42+
shells, because we're not setting the file, but rather the session name, but it's a good
43+
enough approximation for now.
44+
*/ -}}
45+
{{- if .HistoryFile }}
46+
set fish_history devbox
47+
{{- end }}
48+
49+
# Prepend to the prompt to make it clear we're in a devbox shell.
50+
functions -c fish_prompt __devbox_fish_prompt_orig
51+
function fish_prompt
52+
echo "(devbox)" (__devbox_fish_prompt_orig)
53+
end
54+
55+
{{- if .ShellStartTime }}
56+
# log that the shell is ready now!
57+
devbox log shell-ready {{ .ShellStartTime }}
58+
{{ end }}
59+
60+
# End Devbox Post-init Hook
61+
62+
# Switch to the directory where devbox.json config is
63+
set workingDir $(pwd)
64+
cd {{ .ProjectDir }}
65+
66+
{{- if .PluginInitHook }}
67+
68+
# Begin Plugin Init Hook
69+
70+
{{ .PluginInitHook }}
71+
72+
# End Plugin Init Hook
73+
74+
{{- end }}
75+
76+
{{- if .UserHook }}
77+
78+
# Begin Devbox User Hook
79+
80+
{{ .UserHook }}
81+
82+
# End Devbox User Hook
83+
84+
{{- end }}
85+
86+
cd $workingDir
87+
88+
{{- if .ShellStartTime }}
89+
# log that the shell is interactive now!
90+
devbox log shell-interactive {{ .ShellStartTime }}
91+
{{ end }}
92+
93+
# Begin Script command
94+
95+
{{- if .ScriptCommand }}
96+
97+
function run_script
98+
set workingDir $(pwd)
99+
cd {{ .ProjectDir }}
100+
101+
{{ .ScriptCommand }}
102+
103+
cd $workingDir
104+
end
105+
{{- end }}
106+
107+
# End Script command

0 commit comments

Comments
 (0)