bash – Passing Braces as Arguments to Bash Function

argumentsbashfunctionquoting

I love using the following pattern for searching in files:

grep --color=auto -iRnHr --include={*.js,*.html,} --exclude-dir={release,dev,} "span" .

I'd like, however, to have this one wrapped into a bash command like this:

findinfiles {*.js,*.html,} {release,dev,} "span"  // syntax is just a guessing

I cannot solve the problem of passing these kinds of braces into a bash function and using them with $1, $2 and so on. When I use the following:

function findinfiles() {
    echo $1, $2, $3
}

Then:

me@pc ~> findinfiles {*.js,*.html,} {release,dev,} "span"
*.js, *.html, release

Of course, passing arguments to grep won't work this way. It seems that arguments are indexed but not properly grouped.

Can anyone teach me how to deal with these kinds of arguments?

Best Answer

When you run findinfiles {*.js,*.html,} {release,dev,} "span", here is what happens:

  1. Bash parses quotes and splits the command into words: findinfiles, {*.js,*.html,} {release,dev,} span.
  2. Bash expands the braces, resulting in the list of words findinfiles, *.js, *.html, release, dev, span.
  3. Bash expands the wildcard patterns *.js and *.html to the list of matching file names. If no file name matches either pattern, it is left alone.
  4. Bash looks up the name findinfiles, finds out that it's a function, and executes the function with the supplied parameters.

You can prevent the braces and wildcards from being expanded at the function call, but then the braces will appear literally in the arguments, which isn't particularly useful.

I suggest changing the format of your function call. Instead of using braces, use only commas, and manually split the arguments at commas inside the function.

findinfiles () {
  local patterns="$1" excludes="$2" pattern="$3"
  shift 3
  typeset -a cmd dirs
  if [[ $# -eq 0 ]]; then dirs=(.); else dirs=("$@"); fi
  cmd=(grep --color=auto -iRnHr)
  local IFS=,
  for x in $patterns; do
    cmd+=("--include=$x")
  done
  for x in $excludes; do
    cmd+=("--exclude-dir=$x")
  done
  "${cmd[@]}" "${dirs}"
}

Explanations:

  • Store the first three parameters in local variables. Any extra parameters are directories to search in.
  • Set dirs to the list of extra parameters. If there are none, use . (current directory) instead.
  • Set IFS to a comma. When a script contains an unquoted variable expansion like $patterns and $excludes above, the shell performs the following:

    1. Replace the variable by its value.
    2. Split the variable into separate words wherever it contains a character that is present in IFS. By default, IFS contains whitespace characters (space, tab and newline).
    3. Treat each word as a wildcard pattern and expand it to the list of matching files. If there is no matching file, leave the pattern alone.

    (To avoid these expansion steps, use double quotes around the variable substitution, as in patterns="$1" and so on in the script above.)

  • The function builds the command line to execute incrementally in the array variable cmd. At the end, the command is executed.

Alternatively, you could build on the following settings:

shopt -s extglob globstar
fif_excludes='release dev'
alias fif='grep --color=auto -inH $fif_excludes'

Run commands like

fif span **/*.@(js|html)

Explanations:

  • shopt -s extglob activates the @(js|html) form of wildcard pattern, which matches either js or html. (This option activates other pattern forms, see the manual for details.)
  • shopt -s globstar actives the pattern **/ which matches subdirectories at any depth (i.e. it performs a recursive traversal).
  • To change the exclude list (which I expect doesn't happen often), modify the fif_excludes variable.
Related Question