Repository: cli/cli
File: pkg/cmdutil/args.go (Go)

Code before:
	hasValueFlag := false
	cmd.Flags().Visit(func(f *pflag.Flag) {
		if f.Value.Type() != "bool" {
			hasValueFlag = true
		}
	})

	if hasValueFlag {
		errMsg += "; please quote all values that have spaces"
	}

	return FlagErrorf("%s", errMsg)
}

func GlobWindowsPaths(patterns []string) ([]string, error) {
	expansions := []string{}

	for _, pattern := range patterns {
		matches, err := filepath.Glob(pattern)
		if err != nil {
			return nil, fmt.Errorf("%s: %v", pattern, err)
		}
		if len(matches) > 0 {
			expansions = append(expansions, matches...)
		} else {
			expansions = append(expansions, pattern)
		}
	}

	return expansions, nil
}


Review comment (suggestion):
Alright, I talked to @andyfeller about this and we think this should be erroring here. Additionally, we think that ALL systems should rely on this Glob expansion and not just Windows. Here's why:

Currently, the desired behavior is that we error if no matches are found for a pattern. Right now, this is happening [here](https://github.com/cli/cli/blob/537a22228cd6b42b740d7f1c09f47c45bb1dab30/pkg/cmd/release/shared/upload.go#L47-L51) in `AssetsForArgs`. The issue is that its not really the same error for different filesystems. The error ALWAYS happens for Windows, because it doesn't auto-expand glob patterns, and we find that misleading.

Thus, to reflect the behavior of shells, we want the CLI to error when it finds no expansions. That way we can separate out the "this didn't match any files" errors from other errors. If we don't do that, then we are essentially splitting the error handling for no matches between two places that is shell dependent, and we don't really like that pattern.

I'll write a high-level decision of the desired behavior in another comment.

Code after:
	hasValueFlag := false
	cmd.Flags().Visit(func(f *pflag.Flag) {
		if f.Value.Type() != "bool" {
			hasValueFlag = true
		}
	})

	if hasValueFlag {
		errMsg += "; please quote all values that have spaces"
	}

	return FlagErrorf("%s", errMsg)
}

// Partition takes a slice of any type T and separates it into two slices
// of the same type based on the provided predicate function. Any item
// that returns true for the predicate will be included in the first slice
// returned, and any item that returns false for the predicate will be
// included in the second slice returned.
func Partition[T any](slice []T, predicate func(T) bool) ([]T, []T) {
	var matching, nonMatching []T
	for _, item := range slice {
		if predicate(item) {
			matching = append(matching, item)
		} else {
			nonMatching = append(nonMatching, item)
		}
	}
	return matching, nonMatching
}

// GlobPaths expands a list of file patterns into a list of file paths.
// If no files match a pattern, that pattern will return an error.
// If no pattern is passed, this returns an empty list and no error.
//
// For information on supported glob patterns, see
// https://pkg.go.dev/path/filepath#Match
func GlobPaths(patterns []string) ([]string, error) {
	expansions := []string{}

	for _, pattern := range patterns {
		matches, err := filepath.Glob(pattern)
		if err != nil {
			return nil, fmt.Errorf("%s: %v", pattern, err)
		}
		if len(matches) == 0 {
			return []string{}, fmt.Errorf("no matches found for `%s`", pattern)
		}
		expansions = append(expansions, matches...)
	}