Skip to content

Commit 4234053

Browse files
committed
fix more shell scripts; add test
1 parent dbf95d7 commit 4234053

File tree

5 files changed

+77
-23
lines changed

5 files changed

+77
-23
lines changed

internal/devbox/devbox.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,16 @@ func (d *Devbox) RunScript(ctx context.Context, envOpts devopt.EnvOptions, cmdNa
270270
// better alternative since devbox run and devbox shell are not the same.
271271
env["DEVBOX_SHELL_ENABLED"] = "1"
272272

273-
// wrap the arg in double-quotes, and escape any double-quotes inside it
274-
for idx, arg := range cmdArgs {
275-
cmdArgs[idx] = strconv.Quote(arg)
276-
}
277-
278-
var cmdWithArgs []string
273+
cmdString := ""
279274
if _, ok := d.cfg.Scripts()[cmdName]; ok {
280275
// it's a script, so replace the command with the script file's path.
281-
cmdWithArgs = append([]string{shellgen.ScriptPath(d.ProjectDir(), cmdName)}, cmdArgs...)
276+
cmdBuilder := strings.Builder{}
277+
writeQuoted(&cmdBuilder, shellgen.ScriptPath(d.ProjectDir(), cmdName))
278+
for _, arg := range cmdArgs {
279+
cmdBuilder.WriteByte(' ')
280+
writeQuoted(&cmdBuilder, arg)
281+
}
282+
cmdString = cmdBuilder.String()
282283
} else {
283284
// Arbitrary commands should also run the hooks, so we write them to a file as well. However, if the
284285
// command args include env variable evaluations, then they'll be evaluated _before_ the hooks run,
@@ -293,11 +294,21 @@ func (d *Devbox) RunScript(ctx context.Context, envOpts devopt.EnvOptions, cmdNa
293294
if err != nil {
294295
return err
295296
}
296-
cmdWithArgs = []string{shellgen.ScriptPath(d.ProjectDir(), arbitraryCmdFilename)}
297-
env["DEVBOX_RUN_CMD"] = strings.Join(append([]string{cmdName}, cmdArgs...), " ")
297+
298+
cmdBuilder := strings.Builder{}
299+
writeQuoted(&cmdBuilder, shellgen.ScriptPath(d.ProjectDir(), arbitraryCmdFilename))
300+
cmdString = cmdBuilder.String()
301+
302+
cmdBuilder.Reset()
303+
writeQuoted(&cmdBuilder, cmdName)
304+
for _, arg := range cmdArgs {
305+
cmdBuilder.WriteByte(' ')
306+
writeQuoted(&cmdBuilder, arg)
307+
}
308+
env["DEVBOX_RUN_CMD"] = cmdBuilder.String()
298309
}
299310

300-
return nix.RunScript(d.projectDir, strings.Join(cmdWithArgs, " "), env)
311+
return nix.RunScript(d.projectDir, cmdString, env)
301312
}
302313

303314
// Install ensures that all the packages in the config are installed

internal/devbox/envvars.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,40 @@ func exportify(vars map[string]string) string {
3333
strb.WriteString("export ")
3434
strb.WriteString(k)
3535
strb.WriteString(`="`)
36-
for _, r := range vars[k] {
37-
switch r {
38-
// Special characters inside double quotes:
39-
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
40-
case '$', '`', '"', '\\', '\n':
41-
strb.WriteRune('\\')
42-
}
43-
strb.WriteRune(r)
44-
}
36+
writeQuoted(&strb, vars[k])
4537
strb.WriteString("\";\n")
4638
}
4739
return strings.TrimSpace(strb.String())
4840
}
4941

42+
func writeQuoted(dst *strings.Builder, str string) {
43+
needsQuote := strings.ContainsAny(str, ";\"'()$|&><` \t\r\n\\#{~*?[=")
44+
if !needsQuote {
45+
dst.WriteString(str)
46+
return
47+
}
48+
49+
canSingleQuote := !strings.Contains(str, "'")
50+
if canSingleQuote {
51+
dst.WriteByte('\'')
52+
dst.WriteString(str)
53+
dst.WriteByte('\'')
54+
return
55+
}
56+
57+
dst.WriteByte('"')
58+
for _, r := range str {
59+
switch r {
60+
// Special characters inside double quotes:
61+
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
62+
case '$', '`', '"', '\\':
63+
dst.WriteRune('\\')
64+
}
65+
dst.WriteRune(r)
66+
}
67+
dst.WriteByte('"')
68+
}
69+
5070
// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
5171
// but only if the key was not previously set by devbox
5272
// Caveat, this won't mark the values as set by devbox automatically. Instead,

internal/devbox/shellrc_fish.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ set workingDir (pwd)
5959
cd "{{ .ProjectDir }}" || exit
6060

6161
# Source the hooks file, which contains the project's init hooks and plugin hooks.
62-
source {{ .HooksFilePath }}
62+
source "{{ .HooksFilePath }}"
6363

6464
cd "$workingDir" || exit
6565

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
{{/*
2-
This wraps user scripts in devbox.json. The idea is to only run the init
1+
{{/*
2+
This wraps user scripts in devbox.json. The idea is to only run the init
33
hooks once, even if the init hook calls devbox run again. This will also
44
protect against using devbox service in the init hook.
55

@@ -8,7 +8,7 @@
88
*/ -}}
99

1010
if [ -z "${{ .SkipInitHookHash }}" ]; then
11-
. {{ .InitHookPath }}
11+
. "{{ .InitHookPath }}"
1212
fi
1313

1414
{{ .Body }}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Test that Devbox handles whitespace in project paths.
2+
3+
mkdir 'my project'
4+
cd 'my project'
5+
6+
exec devbox run -- hello
7+
stdout 'Hello, world!'
8+
9+
exec devbox run -- touch 'file1 with spaces'
10+
exists 'file1 with spaces'
11+
12+
exec devbox run test
13+
exists 'file2 with spaces'
14+
15+
-- my project/devbox.json --
16+
{
17+
"packages": ["hello@latest"],
18+
"shell": {
19+
"scripts": {
20+
"test": "touch 'file2 with spaces'"
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)