Skip to content

Commit

Permalink
interp: support subshells with FuncEnviron as Env
Browse files Browse the repository at this point in the history
FuncEnviron can't support listing all env vars via Environ.Each,
and Runner.Subshell relied on that to make a full copy of an Environ.

Instead of trying to make the full copy upfront, which might not work,
the runner now makes a copy of each variable value in setVar
when it is about to be modified, to prevent subshells from modifying
the values in the parent shell.

This method is still not perfect, hence the added TODOs,
but it's certainly more correct as we fix a significant footgun.

Fixes #1043.
  • Loading branch information
mvdan committed Nov 15, 2023
1 parent bb3e036 commit 87850d3
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 34 deletions.
24 changes: 3 additions & 21 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -814,27 +814,9 @@ func (r *Runner) Subshell() *Runner {

origStdout: r.origStdout, // used for process substitutions
}
// Env vars and funcs are copied, since they might be modified.
// TODO(v4): lazy copying? it would probably be enough to add a
// copyOnWrite bool field to Variable, then a Modify method that must be
// used when one needs to modify a variable. ideally with some way to
// catch direct modifications without the use of Modify and panic,
// perhaps via a check when getting or setting vars at some level.
oenv := &overlayEnviron{parent: expand.ListEnviron()}
r.writeEnv.Each(func(name string, vr expand.Variable) bool {
vr2 := vr
// Make deeper copies of List and Map, but ensure that they remain nil
// if they are nil in vr.
vr2.List = append([]string(nil), vr.List...)
if vr.Map != nil {
vr2.Map = make(map[string]string, len(vr.Map))
for k, vr := range vr.Map {
vr2.Map[k] = vr
}
}
oenv.Set(name, vr2)
return true
})
// Funcs are copied, since they might be modified.
// Env vars aren't copied; setVar will copy lists and maps as needed.
oenv := &overlayEnviron{parent: r.writeEnv}
r2.writeEnv = oenv
r2.Funcs = make(map[string]*syntax.Stmt, len(r.Funcs))
for k, v := range r.Funcs {
Expand Down
10 changes: 10 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3868,6 +3868,16 @@ func TestRunnerOpts(t *testing.T) {
"set bar_interp_missing; echo $@",
"bar_interp_missing\n",
},
{
opts(interp.Env(expand.FuncEnviron(func(name string) string {
if name == "foo" {
return "bar"
}
return ""
}))),
"(echo $foo); echo x | echo $foo",
"bar\nbar\n",
},
}
p := syntax.NewParser()
for _, c := range cases {
Expand Down
30 changes: 17 additions & 13 deletions interp/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,19 @@ func (o *overlayEnviron) Get(name string) expand.Variable {
}

func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
// Manipulation of a global var inside a function
// Manipulation of a global var inside a function.
if o.funcScope && !vr.Local && !o.values[name].Local {
// "foo=bar" on a global var in a function updates the global scope
if vr.IsSet() {
return o.parent.(expand.WriteEnviron).Set(name, vr)
}
// "foo=bar" followed by "export foo" or "readonly foo"
if vr.Exported || vr.ReadOnly {
// "foo=bar" on a global var in a function updates the global scope
} else if vr.Exported || vr.ReadOnly {
// "foo=bar" followed by "export foo" or "readonly foo"
prev := o.Get(name)
prev.Exported = prev.Exported || vr.Exported
prev.ReadOnly = prev.ReadOnly || vr.ReadOnly
vr = prev
return o.parent.(expand.WriteEnviron).Set(name, vr)
}
// "unset" is handled below
// In a function, the parent environment is ours, so it's always read-write.
return o.parent.(expand.WriteEnviron).Set(name, vr)
}

prev := o.Get(name)
Expand All @@ -71,10 +69,6 @@ func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
return nil
}
delete(o.values, name)
if writeEnv, _ := o.parent.(expand.WriteEnviron); writeEnv != nil {
writeEnv.Set(name, vr)
return nil
}
} else if prev.Exported {
// variable is set and was marked as exported
vr.Exported = true
Expand Down Expand Up @@ -234,7 +228,9 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable
case expand.String:
list = append(list, cur.Str)
case expand.Indexed:
list = cur.List
// TODO: slices.Clone
// TODO: only clone when inside a subshell and getting a var from outside for the first time
list = append([]string(nil), cur.List...)
case expand.Associative:
// if the existing variable is already an AssocArray, try our
// best to convert the key to a string
Expand All @@ -243,6 +239,14 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable
return
}
k := r.literal(w)

// TODO: maps.Clone
// TODO: only clone when inside a subshell and getting a var from outside for the first time
m2 := make(map[string]string, len(cur.Map))
for k, vr := range cur.Map {
m2[k] = vr
}
cur.Map = m2
cur.Map[k] = valStr
r.setVarInternal(name, cur)
return
Expand Down

0 comments on commit 87850d3

Please sign in to comment.