How can I pass a complex file glob to a subshell?

Viewed 334

I am trying to use git ls-files with file patterns to get a filtered list of tracked files. (I know I could do the below with grep et. al but I am curious why it doesn't work)

#!/usr/bin/env bash

GIT_ROOT=$(git rev-parse --show-toplevel)
# only include stuff in src and include
SRC_PATTERNS="{include,src}/**/*.{hpp,cpp,tpp,h,c}"

# @brief    Get list of files in git index with names relative to root
# @param    $1  Any of the constraints git-ls-files knows (e.g. --cached)
function git_sources()
{
    FILE_STRING=$(git -C $GIT_ROOT ls-files --full-name $1 $SRC_PATTERNS)
    echo "$FILE_STRING"
}

The glob I am using works when invoking the git ls-files command from the command line

git -C $GIT_ROOT ls-files --full-name --cached {include,src}/**/*.{hpp,cpp,tpp,h,c}

But not when invoking the git_sources command after sourcing the script.

Question: How do I properly quote the glob pattern so it is passed to the subcommand as if invoked directly from the command line?

1 Answers

It is probably because of how shell expands the glob string stored in the variable. No matter which quotes you use the shell does not expand the command containing the glob string into the resulting files on which you can apply the filter on.

The shell will do globbing after expanding an unquoted variable, but because brace expansion is done before variable expansion (ref. Shell Expansions), the glob will not find any matching files and the shell leaves the literal glob string

When you run

git -C $GIT_ROOT ls-files --full-name --cached {include,src}/**/*.{hpp,cpp,tpp,h,c}

The shell expands the glob string after the --cached part to file1..fileN depending on how many files that match your glob definition, i.e. as

git -C $GIT_ROOT ls-files --full-name --cached file1 file2 ... fileN

But the same does not happen when your glob string is quoted (expanded from a variable), it is expanded as

git -C $GIT_ROOT ls-files --full-name --cached '{include,src}/**/*.{hpp,cpp,tpp,h,c}'

which is your glob string left un-expanded.

The recommended way would be to use an array to expand the result of the glob definition to generate the filenames and pass a quoted array expansion as argument. Change your code to below.

SRC_PATTERNS=({include,src}/**/*.{hpp,cpp,tpp,h,c})

Now the array already stores the list of files matching your glob definition and we just have to pass it along to the command as below. The ${arr[@]} is a quoted array expansion to ensure the shell-meta characters in the filenames don't cause the names to be split

git -C "${GIT_ROOT}" ls-files --full-name --cached "${SRC_PATTERNS[@]}"

Always use quotes to expand shell variables (unless you see a good reason not to, which is no in most cases) and use lower case variable names without _ in the file name.

Related