-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
interp: Initial support for fs.FS. #799
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import ( | |
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"math/rand" | ||
"os" | ||
"path/filepath" | ||
|
@@ -19,6 +20,7 @@ import ( | |
"time" | ||
|
||
"golang.org/x/sync/errgroup" | ||
|
||
"mvdan.cc/sh/v3/expand" | ||
"mvdan.cc/sh/v3/syntax" | ||
) | ||
|
@@ -59,6 +61,9 @@ type Runner struct { | |
|
||
alias map[string]alias | ||
|
||
// Filesystem to use. Defaults to os.DirFS("/") | ||
fs fs.FS | ||
|
||
// callHandler is a function allowing to replace a simple command's | ||
// arguments. It may be nil. | ||
callHandler CallHandlerFunc | ||
|
@@ -164,6 +169,7 @@ func (r *Runner) optByFlag(flag byte) *bool { | |
func New(opts ...RunnerOption) (*Runner, error) { | ||
r := &Runner{ | ||
usedNew: true, | ||
fs: os.DirFS("/"), | ||
execHandler: DefaultExecHandler(2 * time.Second), | ||
openHandler: DefaultOpenHandler(), | ||
} | ||
|
@@ -221,7 +227,7 @@ func Dir(path string) RunnerOption { | |
if err != nil { | ||
return fmt.Errorf("could not get absolute dir: %w", err) | ||
} | ||
info, err := os.Stat(path) | ||
info, err := r.stat(path) | ||
if err != nil { | ||
return fmt.Errorf("could not stat: %w", err) | ||
} | ||
|
@@ -313,6 +319,14 @@ func OpenHandler(f OpenHandlerFunc) RunnerOption { | |
} | ||
} | ||
|
||
// FS sets the fs.FS used for filesystem file and directory read operations. | ||
func FS(fs fs.FS) RunnerOption { | ||
return func(r *Runner) error { | ||
r.fs = fs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would mimic what StdIO does with io.Discard; otherwise you can end up with a nil This also enables the user to overwrite a previous option with whatever our default is, without having to write out |
||
return nil | ||
} | ||
} | ||
|
||
// StdIO configures an interpreter's standard input, standard output, and | ||
// standard error. If out or err are nil, they default to a writer that discards | ||
// the output. | ||
|
@@ -410,6 +424,7 @@ func (r *Runner) Reset() { | |
// reset the internal state | ||
*r = Runner{ | ||
Env: r.Env, | ||
fs: r.fs, | ||
callHandler: r.callHandler, | ||
execHandler: r.execHandler, | ||
openHandler: r.openHandler, | ||
|
@@ -562,6 +577,7 @@ func (r *Runner) Subshell() *Runner { | |
r2 := &Runner{ | ||
Dir: r.Dir, | ||
Params: r.Params, | ||
fs: r.fs, | ||
callHandler: r.callHandler, | ||
execHandler: r.execHandler, | ||
openHandler: r.openHandler, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import ( | |
"context" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
|
@@ -38,6 +39,9 @@ type HandlerContext struct { | |
// variables. | ||
Env expand.Environ | ||
|
||
// FS is the interpreter's filesystem. | ||
FS fs.FS | ||
|
||
// Dir is the interpreter's current directory. | ||
Dir string | ||
|
||
|
@@ -87,7 +91,7 @@ type ExecHandlerFunc func(ctx context.Context, args []string) error | |
func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc { | ||
return func(ctx context.Context, args []string) error { | ||
hc := HandlerCtx(ctx) | ||
path, err := LookPathDir(hc.Dir, hc.Env, args[0]) | ||
path, err := LookPathDirFS(hc.FS, hc.Dir, hc.Env, args[0]) | ||
if err != nil { | ||
fmt.Fprintln(hc.Stderr, err) | ||
return NewExitStatus(127) | ||
|
@@ -151,11 +155,11 @@ func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc { | |
} | ||
} | ||
|
||
func checkStat(dir, file string, checkExec bool) (string, error) { | ||
func checkStat(vfs fs.FS, dir, file string, checkExec bool) (string, error) { | ||
if !filepath.IsAbs(file) { | ||
file = filepath.Join(dir, file) | ||
} | ||
info, err := os.Stat(file) | ||
info, err := fs.Stat(vfs, pathForFS(file)) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
@@ -178,28 +182,28 @@ func winHasExt(file string) bool { | |
} | ||
|
||
// findExecutable returns the path to an existing executable file. | ||
func findExecutable(dir, file string, exts []string) (string, error) { | ||
func findExecutable(vfs fs.FS, dir, file string, exts []string) (string, error) { | ||
if len(exts) == 0 { | ||
// non-windows | ||
return checkStat(dir, file, true) | ||
return checkStat(vfs, dir, file, true) | ||
} | ||
if winHasExt(file) { | ||
if file, err := checkStat(dir, file, true); err == nil { | ||
if file, err := checkStat(vfs, dir, file, true); err == nil { | ||
return file, nil | ||
} | ||
} | ||
for _, e := range exts { | ||
f := file + e | ||
if f, err := checkStat(dir, f, true); err == nil { | ||
if f, err := checkStat(vfs, dir, f, true); err == nil { | ||
return f, nil | ||
} | ||
} | ||
return "", fmt.Errorf("not found") | ||
} | ||
|
||
// findFile returns the path to an existing file. | ||
func findFile(dir, file string, _ []string) (string, error) { | ||
return checkStat(dir, file, false) | ||
func findFile(vfs fs.FS, dir, file string, _ []string) (string, error) { | ||
return checkStat(vfs, dir, file, false) | ||
} | ||
|
||
// LookPath is deprecated. See LookPathDir. | ||
|
@@ -213,13 +217,21 @@ func LookPath(env expand.Environ, file string) (string, error) { | |
// | ||
// If no error is returned, the returned path must be valid. | ||
func LookPathDir(cwd string, env expand.Environ, file string) (string, error) { | ||
return lookPathDir(cwd, env, file, findExecutable) | ||
return lookPathDir(os.DirFS("/"), cwd, env, file, findExecutable) | ||
} | ||
|
||
// LookPathDirFS is like LookPathDir but retrieves all state from the Runner. | ||
func LookPathDirFS(vfs fs.FS, cwd string, env expand.Environ, file string) (string, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, what runner? I think you should also document that you expect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops, this is a leftover comment from a previous version of the function. |
||
return lookPathDir(vfs, cwd, env, file, findExecutable) | ||
} | ||
|
||
// findAny defines a function to pass to lookPathDir. | ||
type findAny = func(dir string, file string, exts []string) (string, error) | ||
type findAny = func(vfs fs.FS, dir string, file string, exts []string) (string, error) | ||
|
||
func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (string, error) { | ||
func lookPathDir(vfs fs.FS, cwd string, env expand.Environ, file string, find findAny) (string, error) { | ||
if vfs == nil { | ||
panic("no fs.FS found") | ||
} | ||
if find == nil { | ||
panic("no find function found") | ||
} | ||
|
@@ -234,7 +246,7 @@ func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (str | |
} | ||
exts := pathExts(env) | ||
if strings.ContainsAny(file, chars) { | ||
return find(cwd, file, exts) | ||
return find(vfs, cwd, file, exts) | ||
} | ||
for _, elem := range pathList { | ||
var path string | ||
|
@@ -245,7 +257,7 @@ func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (str | |
default: | ||
path = filepath.Join(elem, file) | ||
} | ||
if f, err := find(cwd, path, exts); err == nil { | ||
if f, err := find(vfs, cwd, path, exts); err == nil { | ||
return f, nil | ||
} | ||
} | ||
|
@@ -254,8 +266,8 @@ func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (str | |
|
||
// scriptFromPathDir is similar to LookPathDir, with the difference that it looks | ||
// for both executable and non-executable files. | ||
func scriptFromPathDir(cwd string, env expand.Environ, file string) (string, error) { | ||
return lookPathDir(cwd, env, file, findFile) | ||
func scriptFromPathDir(vfs fs.FS, cwd string, env expand.Environ, file string) (string, error) { | ||
return lookPathDir(vfs, cwd, env, file, findFile) | ||
} | ||
|
||
func pathExts(env expand.Environ) []string { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -427,7 +427,7 @@ var runTests = []runTest{ | |
{`arr=(0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h); echo ${arr[@]:3:4}`, "3 4 5 6\n"}, | ||
{`echo ${foo[@]}; echo ${foo[*]}`, "\n\n"}, | ||
// TODO: reenable once we figure out the broken pipe error | ||
//{`$ENV_PROG | while read line; do if test -z "$line"; then echo empty; fi; break; done`, ""}, // never begin with an empty element | ||
// {`$ENV_PROG | while read line; do if test -z "$line"; then echo empty; fi; break; done`, ""}, // never begin with an empty element | ||
|
||
// inline variables have special scoping | ||
{ | ||
|
@@ -2435,10 +2435,10 @@ set +o pipefail | |
"xxx xxx\n", | ||
}, | ||
// TODO: figure this one out | ||
//{ | ||
// { | ||
// "declare -n foo=bar bar=baz; foo=xxx; echo $foo $bar; echo $baz", | ||
// "xxx xxx\nxxx\n", | ||
//}, | ||
// }, | ||
|
||
// read-only vars | ||
{"declare -r foo=bar; echo $foo", "bar\n"}, | ||
|
@@ -2903,7 +2903,7 @@ var runTestsUnix = []runTest{ | |
}, | ||
{ | ||
"cd /; sure/is/missing", | ||
"stat /sure/is/missing: no such file or directory\nexit status 127 #JUSTERR", | ||
"stat //sure/is/missing: no such file or directory\nexit status 127 #JUSTERR", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not yet clear to me why this is happening. |
||
}, | ||
{ | ||
"echo '#!/bin/sh\necho b' >a; chmod 0755 a; PATH=; a", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ import ( | |
"context" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"io/fs" | ||
"math" | ||
"math/rand" | ||
"os" | ||
|
@@ -144,13 +144,29 @@ func (r *Runner) updateExpandOpts() { | |
if r.opts[optNoGlob] { | ||
r.ecfg.ReadDir = nil | ||
} else { | ||
r.ecfg.ReadDir = ioutil.ReadDir | ||
r.ecfg.ReadDir = r.readFSDir | ||
} | ||
r.ecfg.GlobStar = r.opts[optGlobStar] | ||
r.ecfg.NullGlob = r.opts[optNullGlob] | ||
r.ecfg.NoUnset = r.opts[optNoUnset] | ||
} | ||
|
||
func (r *Runner) readFSDir(s string) ([]os.FileInfo, error) { | ||
dirents, err := fs.ReadDir(r.fs, pathForFS(s)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
infos := make([]os.FileInfo, len(dirents)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd perhaps add here:
|
||
for i, dirent := range dirents { | ||
info, err := dirent.Info() | ||
if err != nil { | ||
return nil, err | ||
} | ||
infos[i] = info | ||
} | ||
return infos, nil | ||
} | ||
|
||
func (r *Runner) expandErr(err error) { | ||
if err != nil { | ||
r.errf("%v\n", err) | ||
|
@@ -211,6 +227,7 @@ func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) { | |
func (r *Runner) handlerCtx(ctx context.Context) context.Context { | ||
hc := HandlerContext{ | ||
Env: &overlayEnviron{parent: r.writeEnv}, | ||
FS: r.fs, | ||
Dir: r.Dir, | ||
Stdin: r.stdin, | ||
Stdout: r.stdout, | ||
|
@@ -907,5 +924,13 @@ func (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileM | |
} | ||
|
||
func (r *Runner) stat(name string) (os.FileInfo, error) { | ||
return os.Stat(r.absPath(name)) | ||
return fs.Stat(r.fs, pathForFS(r.absPath(name))) | ||
} | ||
|
||
// fs.FS does not allow "rooted" paths, so we have to remap them. | ||
func pathForFS(path string) string { | ||
if path == "/" { | ||
return "." | ||
} | ||
return strings.TrimPrefix(path, "/") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also sounds like it won't support Windows :) |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.