Skip to content

Commit f561391

Browse files
committed
[direnv] copy bash, fish, zsh shell code from direnv
1 parent 9a400eb commit f561391

File tree

5 files changed

+436
-0
lines changed

5 files changed

+436
-0
lines changed

internal/shell/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Code in this directory was copy-pasted from the direnv codebase:
2+
https://github.com/direnv/direnv/blob/master/internal/cmd/
3+
4+
We could not directly import this code because in the direnv
5+
code it is inside an `internal` directory, and hence not
6+
exported.
7+
8+
Full credit to the direnv authors.

internal/shell/shell.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package shell
2+
3+
import (
4+
"path/filepath"
5+
)
6+
7+
type Env map[string]string
8+
9+
// Shell is the interface that represents the interaction with the host shell.
10+
type Shell interface {
11+
// Hook is the string that gets evaluated into the host shell config and
12+
// setups direnv as a prompt hook.
13+
Hook() (string, error)
14+
15+
// Export outputs the ShellExport as an evaluatable string on the host shell
16+
Export(e ShellExport) string
17+
18+
// Dump outputs and evaluatable string that sets the env in the host shell
19+
Dump(env Env) string
20+
}
21+
22+
// ShellExport represents environment variables to add and remove on the host
23+
// shell.
24+
type ShellExport map[string]*string
25+
26+
// Add represents the addition of a new environment variable
27+
func (e ShellExport) Add(key, value string) {
28+
e[key] = &value
29+
}
30+
31+
// Remove represents the removal of a given `key` environment variable.
32+
func (e ShellExport) Remove(key string) {
33+
e[key] = nil
34+
}
35+
36+
// DetectShell returns a Shell instance from the given target.
37+
//
38+
// target is usually $0 and can also be prefixed by `-`
39+
func DetectShell(target string) Shell {
40+
target = filepath.Base(target)
41+
// $0 starts with "-"
42+
if target[0:1] == "-" {
43+
target = target[1:]
44+
}
45+
46+
switch target {
47+
case "bash":
48+
return Bash
49+
//case "elvish":
50+
// return Elvish
51+
case "fish":
52+
return Fish
53+
//case "gha":
54+
// return GitHubActions
55+
//case "gzenv":
56+
// return GzEnv
57+
//case "json":
58+
// return JSON
59+
//case "tcsh":
60+
// return Tcsh
61+
//case "vim":
62+
// return Vim
63+
case "zsh":
64+
return Zsh
65+
}
66+
67+
return nil
68+
}

internal/shell/shell_bash.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package shell
2+
3+
import "fmt"
4+
5+
type bash struct{}
6+
7+
// Bash shell instance
8+
var Bash Shell = bash{}
9+
10+
const bashHook = `
11+
_direnv_hook() {
12+
local previous_exit_status=$?;
13+
trap -- '' SIGINT;
14+
eval "$("{{.SelfPath}}" export bash)";
15+
trap - SIGINT;
16+
return $previous_exit_status;
17+
};
18+
if ! [[ "${PROMPT_COMMAND:-}" =~ _direnv_hook ]]; then
19+
PROMPT_COMMAND="_direnv_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
20+
fi
21+
`
22+
23+
func (sh bash) Hook() (string, error) {
24+
return bashHook, nil
25+
}
26+
27+
func (sh bash) Export(e ShellExport) (out string) {
28+
for key, value := range e {
29+
if value == nil {
30+
out += sh.unset(key)
31+
} else {
32+
out += sh.export(key, *value)
33+
}
34+
}
35+
return out
36+
}
37+
38+
func (sh bash) Dump(env Env) (out string) {
39+
for key, value := range env {
40+
out += sh.export(key, value)
41+
}
42+
return out
43+
}
44+
45+
func (sh bash) export(key, value string) string {
46+
return "export " + sh.escape(key) + "=" + sh.escape(value) + ";"
47+
}
48+
49+
func (sh bash) unset(key string) string {
50+
return "unset " + sh.escape(key) + ";"
51+
}
52+
53+
func (sh bash) escape(str string) string {
54+
return BashEscape(str)
55+
}
56+
57+
/*
58+
* Escaping
59+
*/
60+
61+
// nolint
62+
const (
63+
ACK = 6
64+
TAB = 9
65+
LF = 10
66+
CR = 13
67+
US = 31
68+
SPACE = 32
69+
AMPERSTAND = 38
70+
SINGLE_QUOTE = 39
71+
PLUS = 43
72+
NINE = 57
73+
QUESTION = 63
74+
UPPERCASE_Z = 90
75+
OPEN_BRACKET = 91
76+
BACKSLASH = 92
77+
UNDERSCORE = 95
78+
CLOSE_BRACKET = 93
79+
BACKTICK = 96
80+
LOWERCASE_Z = 122
81+
TILDA = 126
82+
DEL = 127
83+
)
84+
85+
// https://github.com/solidsnack/shell-escape/blob/master/Text/ShellEscape/Bash.hs
86+
/*
87+
A Bash escaped string. The strings are wrapped in @$\'...\'@ if any
88+
bytes within them must be escaped; otherwise, they are left as is.
89+
Newlines and other control characters are represented as ANSI escape
90+
sequences. High bytes are represented as hex codes. Thus Bash escaped
91+
strings will always fit on one line and never contain non-ASCII bytes.
92+
*/
93+
func BashEscape(str string) string {
94+
if str == "" {
95+
return "''"
96+
}
97+
in := []byte(str)
98+
out := ""
99+
i := 0
100+
l := len(in)
101+
escape := false
102+
103+
hex := func(char byte) {
104+
escape = true
105+
out += fmt.Sprintf("\\x%02x", char)
106+
}
107+
108+
backslash := func(char byte) {
109+
escape = true
110+
out += string([]byte{BACKSLASH, char})
111+
}
112+
113+
escaped := func(str string) {
114+
escape = true
115+
out += str
116+
}
117+
118+
quoted := func(char byte) {
119+
escape = true
120+
out += string([]byte{char})
121+
}
122+
123+
literal := func(char byte) {
124+
out += string([]byte{char})
125+
}
126+
127+
for i < l {
128+
char := in[i]
129+
switch {
130+
case char == ACK:
131+
hex(char)
132+
case char == TAB:
133+
escaped(`\t`)
134+
case char == LF:
135+
escaped(`\n`)
136+
case char == CR:
137+
escaped(`\r`)
138+
case char <= US:
139+
hex(char)
140+
case char <= AMPERSTAND:
141+
quoted(char)
142+
case char == SINGLE_QUOTE:
143+
backslash(char)
144+
case char <= PLUS:
145+
quoted(char)
146+
case char <= NINE:
147+
literal(char)
148+
case char <= QUESTION:
149+
quoted(char)
150+
case char <= UPPERCASE_Z:
151+
literal(char)
152+
case char == OPEN_BRACKET:
153+
quoted(char)
154+
case char == BACKSLASH:
155+
backslash(char)
156+
case char == UNDERSCORE:
157+
literal(char)
158+
case char <= CLOSE_BRACKET:
159+
quoted(char)
160+
case char <= BACKTICK:
161+
quoted(char)
162+
case char <= TILDA:
163+
quoted(char)
164+
case char == DEL:
165+
hex(char)
166+
default:
167+
hex(char)
168+
}
169+
i++
170+
}
171+
172+
if escape {
173+
out = "$'" + out + "'"
174+
}
175+
176+
return out
177+
}

internal/shell/shell_fish.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package shell
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
type fish struct{}
9+
10+
// Fish adds support for the fish shell as a host
11+
var Fish Shell = fish{}
12+
13+
const fishHook = `
14+
function __direnv_export_eval --on-event fish_prompt;
15+
"{{.SelfPath}}" export fish | source;
16+
17+
if test "$direnv_fish_mode" != "disable_arrow";
18+
function __direnv_cd_hook --on-variable PWD;
19+
if test "$direnv_fish_mode" = "eval_after_arrow";
20+
set -g __direnv_export_again 0;
21+
else;
22+
"{{.SelfPath}}" export fish | source;
23+
end;
24+
end;
25+
end;
26+
end;
27+
28+
function __direnv_export_eval_2 --on-event fish_preexec;
29+
if set -q __direnv_export_again;
30+
set -e __direnv_export_again;
31+
"{{.SelfPath}}" export fish | source;
32+
echo;
33+
end;
34+
35+
functions --erase __direnv_cd_hook;
36+
end;
37+
`
38+
39+
func (sh fish) Hook() (string, error) {
40+
return fishHook, nil
41+
}
42+
43+
func (sh fish) Export(e ShellExport) (out string) {
44+
for key, value := range e {
45+
if value == nil {
46+
out += sh.unset(key)
47+
} else {
48+
out += sh.export(key, *value)
49+
}
50+
}
51+
return out
52+
}
53+
54+
func (sh fish) Dump(env Env) (out string) {
55+
for key, value := range env {
56+
out += sh.export(key, value)
57+
}
58+
return out
59+
}
60+
61+
func (sh fish) export(key, value string) string {
62+
if key == "PATH" {
63+
command := "set -x -g PATH"
64+
for _, path := range strings.Split(value, ":") {
65+
command += " " + sh.escape(path)
66+
}
67+
return command + ";"
68+
}
69+
return "set -x -g " + sh.escape(key) + " " + sh.escape(value) + ";"
70+
}
71+
72+
func (sh fish) unset(key string) string {
73+
return "set -e -g " + sh.escape(key) + ";"
74+
}
75+
76+
func (sh fish) escape(str string) string {
77+
in := []byte(str)
78+
out := "'"
79+
i := 0
80+
l := len(in)
81+
82+
hex := func(char byte) {
83+
out += fmt.Sprintf("'\\X%02x'", char)
84+
}
85+
86+
backslash := func(char byte) {
87+
out += string([]byte{BACKSLASH, char})
88+
}
89+
90+
escaped := func(str string) {
91+
out += "'" + str + "'"
92+
}
93+
94+
literal := func(char byte) {
95+
out += string([]byte{char})
96+
}
97+
98+
for i < l {
99+
char := in[i]
100+
switch {
101+
case char == TAB:
102+
escaped(`\t`)
103+
case char == LF:
104+
escaped(`\n`)
105+
case char == CR:
106+
escaped(`\r`)
107+
case char <= US:
108+
hex(char)
109+
case char == SINGLE_QUOTE:
110+
backslash(char)
111+
case char == BACKSLASH:
112+
backslash(char)
113+
case char <= TILDA:
114+
literal(char)
115+
case char == DEL:
116+
hex(char)
117+
default:
118+
hex(char)
119+
}
120+
i++
121+
}
122+
123+
out += "'"
124+
125+
return out
126+
}

0 commit comments

Comments
 (0)