Search code examples
shellvalidationzshglob

How to check whether a string is a valid zsh glob pattern?


I'm writing a zsh program that takes glob patterns as command-line arguments:

tss files --tags 'a*'  # lists files bearing a tag that starts with an 'a'

In order to validate input, is there a way to check whether a string is a syntactically valid glob pattern?

Ideally I would be able to validate both extended and non-extended patterns, leaving to the user the choice to use either, but I'd appreciate a solution that works with only one of the two.


Solution

  • One possibility is to attempt to use the glob pattern with a ${~...} expansion. This can be wrapped in a try-block to prevent the shell from exiting if there is an error:

    checkGlob() {
      setopt nullglob
      { : ${~1} } always { TRY_BLOCK_ERROR=0 }
    }
    
    checkGlob 'aa*'; print $?
    # => 0
    checkGlob 'aa['; print $?
    # => 1
    

    Some of the pieces

    • setopt nullglob - no error if there isn't a match for the glob pattern.
    • { <try-block> } always { <always-block> } - even if there's an error in the try-block, the code in the always-block will always be executed.
    • : - no-op. The expansion that follows will be attempted, but no command will be executed.
    • ${~1} - expands the first positional parameter with GLOB_SUBST set, so glob patterns in the value will be processed.
    • TRY_BLOCK_ERROR=0 - resets the error status. Normally a runtime error like an incorrect glob pattern results in the shell exiting. This prevents that.
    • The return code from the function is from the last command in the try-block, i.e. the ~ expansion.

    The extended glob setting will determine which glob patterns are validated. This version of the function sets that option:

    checkGlob() {
      setopt nullglob extendedglob localoptions
      pat=APREFIX${1:?}
      {
        : ${~pat} 
      } always {
        TRY_BLOCK_ERROR=0
      } &> /dev/null
    }
    

    This also has:

    • localoptions - changes from setopt will only apply to this function.
    • pat=APREFIX... - the ~ expansion is going to try to find actual files that match the pattern in the current directory - this is one way to make that list shorter. Another option is to change into an empty directory created with mktemp.
    • &> /dev/null - block any output from the expansion.

    You could run a similar check with extended glob turned off to try with both types, but note that many zsh utilities, such as zmv, simply turn extended globs on by default and call it a day.

    Testing:

    for gl in '*' 'a*' 'a[bc]' 'a[' 'a(.)' 'a(' 'a(#z)' '/tmp'; do
      if checkGlob $gl; then
          print "valid:   $gl"
      else
          print "invalid: $gl"
      fi
    done
    
    # => valid:   *
    # => valid:   a*
    # => valid:   a[bc]
    # => invalid: a[
    # => valid:   a(.)
    # => invalid: a(
    # => invalid: a(#z)
    # => valid:   /tmp