Skip to content
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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path/filepath"
Expand All @@ -19,6 +20,7 @@ import (
"time"

"golang.org/x/sync/errgroup"

"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/syntax"
)
Expand Down Expand Up @@ -59,6 +61,9 @@ type Runner struct {

alias map[string]alias

// Filesystem to use. Defaults to os.DirFS("/")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Filesystem to use. Defaults to os.DirFS("/")
// 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
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The 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 r.fs if the user provides FS(nil).

This also enables the user to overwrite a previous option with whatever our default is, without having to write out os.DirFS("/") themselves.

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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions interp/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
args := fp.args()
for _, arg := range args {
if mode == "-p" {
if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
if path, err := LookPathDirFS(r.fs, r.Dir, r.writeEnv, arg); err == nil {
r.outf("%s\n", path)
} else {
anyNotFound = true
Expand Down Expand Up @@ -310,7 +310,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
}
continue
}
if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
if path, err := LookPathDirFS(r.fs, r.Dir, r.writeEnv, arg); err == nil {
if mode == "-t" {
r.out("file\n")
} else {
Expand Down Expand Up @@ -341,7 +341,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
r.errf("%v: source: need filename\n", pos)
return 2
}
path, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0])
path, err := scriptFromPathDir(r.fs, r.Dir, r.writeEnv, args[0])
if err != nil {
// If the script was not found in PATH or there was any error, pass
// the source path to the open handler so it has a chance to look
Expand Down Expand Up @@ -455,7 +455,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
last = 0
if r.Funcs[arg] != nil || isBuiltin(arg) {
r.outf("%s\n", arg)
} else if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
} else if path, err := LookPathDirFS(r.fs, r.Dir, r.writeEnv, arg); err == nil {
r.outf("%s\n", path)
} else {
last = 1
Expand Down
2 changes: 1 addition & 1 deletion interp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func ExampleExecHandler() {
return nil
}

if _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil {
if _, err := interp.LookPathDirFS(hc.FS, hc.Dir, hc.Env, args[0]); err != nil {
fmt.Printf("%s is not installed\n", args[0])
return interp.NewExitStatus(1)
}
Expand Down
44 changes: 28 additions & 16 deletions interp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand All @@ -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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but retrieves all state from the Runner

Hmm, what runner?

I think you should also document that you expect vfs to correspond to the root of the filesystem. Perhaps call it rootFS to clarify. I'd also document the same assumption in lookPathDir.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
}
Expand All @@ -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
Expand All @@ -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
}
}
Expand All @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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",
Expand Down
31 changes: 28 additions & 3 deletions interp/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"io/fs"
"math"
"math/rand"
"os"
Expand Down Expand Up @@ -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))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd perhaps add here:

// TODO(v4): use io/fs directly in ReadDir

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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, "/")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also sounds like it won't support Windows :)

}