Skip to content

Commit

Permalink
expand: preserve glob prefixes such as ./
Browse files Browse the repository at this point in the history
For example, "./foo/*" should match "./foo/bar", not "foo/bar". This can
matter for some programs, such as "go build ./foo/*", since the leading
dot directory indicates a path on disk as opposed to a URL to be fetched
via the network.

We didn't support this well because we were using filepath.Join
everywhere, which cleans paths, thus removing dot directory components.

To fix this, avoid joining paths whenever possible, and use custom logic
when joining to avoid cleaning the result.

Thanks to Andrey Nering and Marc Trudel for finding this bug in the
go-task/task project.

Fixes mvdan#336.
  • Loading branch information
mvdan committed Dec 16, 2018
1 parent 230fc13 commit 7d245b2
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 19 deletions.
60 changes: 41 additions & 19 deletions expand/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Config struct {
// variables.
Env Environ

// TODO(mvdan): consider replacing NoGlob==true with ReadDir==nil.

// NoGlob corresponds to the shell option that disables globbing.
NoGlob bool
// GlobStar corresponds to the shell option that allows globbing with
Expand Down Expand Up @@ -316,7 +318,6 @@ func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
cfg = prepareConfig(cfg)
fields := make([]string, 0, len(words))
dir := cfg.envGet("PWD")
baseDir := syntax.QuotePattern(dir)
for _, word := range words {
expWord, _ := syntax.SplitBraces(word)
for _, word2 := range Braces(expWord) {
Expand All @@ -329,10 +330,11 @@ func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
var matches []string
abs := filepath.IsAbs(path)
if doGlob && !cfg.NoGlob {
base := ""
if !abs {
path = filepath.Join(baseDir, path)
base = dir
}
matches, err = cfg.glob(path)
matches, err = cfg.glob(base, path)
if err != nil {
return nil, err
}
Expand All @@ -343,11 +345,7 @@ func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
}
for _, match := range matches {
if !abs {
endSeparator := strings.HasSuffix(match, string(filepath.Separator))
match, _ = filepath.Rel(dir, match)
if endSeparator {
match += string(filepath.Separator)
}
match = strings.TrimPrefix(match, dir)
}
fields = append(fields, match)
}
Expand Down Expand Up @@ -611,9 +609,26 @@ func hasGlob(path string) bool {

var rxGlobStar = regexp.MustCompile(".*")

func (cfg *Config) glob(pattern string) ([]string, error) {
parts := strings.Split(pattern, string(filepath.Separator))
matches := []string{"."}
// pathJoin2 is a simpler version of filepath.Join without cleaning the result,
// since that's needed for globbing.
func pathJoin2(elem1, elem2 string) string {
if elem1 == "" {
return elem2
}
if strings.HasSuffix(elem1, string(filepath.Separator)) {
return elem1 + elem2
}
return elem1 + string(filepath.Separator) + elem2
}

// pathSplit is the opposite of pathJoin.
func pathSplit(path string) []string {
return strings.Split(path, string(filepath.Separator))
}

func (cfg *Config) glob(base, pattern string) ([]string, error) {
parts := pathSplit(pattern)
matches := []string{""}
if filepath.IsAbs(pattern) {
if parts[0] == "" {
// unix-like
Expand All @@ -626,20 +641,26 @@ func (cfg *Config) glob(pattern string) ([]string, error) {
parts = parts[1:]
}
for _, part := range parts {
if part == "**" && cfg.GlobStar {
for i := range matches {
switch {
case part == ".", part == "..":
for i, dir := range matches {
matches[i] = pathJoin2(dir, part)
}
continue
case part == "**" && cfg.GlobStar:
for i, match := range matches {
// "a/**" should match "a/ a/b a/b/cfg ..."; note
// how the zero-match case has a trailing
// separator.
matches[i] += string(filepath.Separator)
matches[i] = pathJoin2(match, "")
}
// expand all the possible levels of **
latest := matches
for {
var newMatches []string
for _, dir := range latest {
var err error
newMatches, err = cfg.globDir(dir, rxGlobStar, newMatches)
newMatches, err = cfg.globDir(base, dir, rxGlobStar, newMatches)
if err != nil {
return nil, err
}
Expand All @@ -662,7 +683,7 @@ func (cfg *Config) glob(pattern string) ([]string, error) {
rx := regexp.MustCompile("^" + expr + "$")
var newMatches []string
for _, dir := range matches {
newMatches, err = cfg.globDir(dir, rx, newMatches)
newMatches, err = cfg.globDir(base, dir, rx, newMatches)
if err != nil {
return nil, err
}
Expand All @@ -672,11 +693,12 @@ func (cfg *Config) glob(pattern string) ([]string, error) {
return matches, nil
}

func (cfg *Config) globDir(dir string, rx *regexp.Regexp, matches []string) ([]string, error) {
func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matches []string) ([]string, error) {
if cfg.ReadDir == nil {
// TODO(mvdan): check this at the beginning of a glob?
return nil, nil
}
infos, err := cfg.ReadDir(dir)
infos, err := cfg.ReadDir(filepath.Join(base, dir))
if err != nil {
return nil, err
}
Expand All @@ -686,7 +708,7 @@ func (cfg *Config) globDir(dir string, rx *regexp.Regexp, matches []string) ([]s
continue
}
if rx.MatchString(name) {
matches = append(matches, filepath.Join(dir, name))
matches = append(matches, pathJoin2(dir, name))
}
}
return matches, nil
Expand Down
8 changes: 8 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,14 @@ set +o pipefail
"cat <<EOF\n*.go\nEOF",
"*.go\n",
},
{
"mkdir -p a/b a/c; echo ./a/* | sed 's@\\\\@/@g'",
"./a/b ./a/c\n",
},
{
"mkdir -p a/b a/c d; cd d; echo ../a/* | sed 's@\\\\@/@g'",
"../a/b ../a/c\n",
},

// brace expansion; more exhaustive tests in the syntax package
{"echo a}b", "a}b\n"},
Expand Down

0 comments on commit 7d245b2

Please sign in to comment.