Search code examples
unixscriptingzsh

Using pseudo-array $@ for dynamic commands results in errors if the command includes globs


I have the following script:

#!/bin/zsh

old_IFS="$IFS"
IFS='|'

files_to_exclude="$*"

IFS="$old_IFS"

set -- -f --
set -- "$@" ./^("${files_to_exclude}")

rm "$@"

and I expect the following command to be executed (if the script was invoked with arguments test.txt, cover.jpg and 01.\ Filename.flac):

rm -f -- ./^(test.txt|cover.jpg|01.\ Filename.flac)

Instead, when executing the script I get the following error on line 11: number expected.

I presume that the reason for it is the glob character ^ which refuses to be parsed somehow. I have used this resource to write the script.

I have also tried eval but to no avail either.

Is there a way to get this working?

P.S. I have just tried doing this using zsh's function alias and it is not working either:

rm-excluding() {
    rm -f -- ./^($*)
}

Invoking the above with rm-excluding a b results in:

rm-excluding:1: bad pattern: ./^(a


Solution

  • I think there's a bit of an XY problem going on here; you're not explaining the what, you're focusing on a rather convoluted how instead. It looks like you're trying to build up a glob pattern that's supposed to expand to all but a list of files known at runtime.

    One way, using zsh's more advanced parameter expansion features to first join the positional parameters together with | characters while building the pattern, and then to force filename expansion on it (Which can also be enabled globally with the GLOB_SUBST option I prefer to do it case-by-case):

    #!/usr/bin/env zsh
    
    # Just in case it's turned off somehow by your setup
    setopt EXTENDED_GLOB
    
    # Build the pattern by joining position parameters with pipes
    excluded="^(${(j:|:)argv})"
    
    # And expand with zsh's ${~spec} form of parameter expansion
    # to force the expanded value to undergo filename expansion
    print -l -- ${~excluded} # Replace with rm when you're sure it's working right
    

    Note the use of a variable named argv; it's an array version of the positional parameters that can be used instead of $* or $@ in zsh that often ends up being simpler to work with.

    You could also avoid a complicated glob completely by using array difference expansion (:|):

    # Make an array with all matching files and remove the ones in $argv
    typeset -a allfiles=(*)
    print -l -- ${allfiles:|argv}
    

    which is the option I'd prefer; it's simpler with fewer moving parts for unexpected behavior.