This is zmv command to non-recursively replace any number of spaces with a single underscore (thanks to Stéphane Chazelas, who helped me with it):

zmv -- '* *' '${f// ##/_}'

And here is a command from the internet, that seems to do the same thing, but recursively:

zmv -- '(**/)(* *)' '$1${2//( #-## #| ##)/_}'

Could someone explain how does it work? Specifically, how the ( #-## #| ##) part works?

  • 1
    ## (same as (#c1,)) is the equivalent of extended regexp + (same as {1,}) as mentioned in the first Q&A you're referring to while # (same as (#c,) or (#c0,)) is the equivalent of * (same as {0,}). See the documentation about glob operators (link already given in the first Q&A you're referring to). Commented May 18 at 9:14
  • @StéphaneChazelas Many thanks, Stéphane. So, ## is for + (one or more), # is for * (zero or more), but what is for ? (one or none), is there a replacement for it? I have read the docs, but haven't found easy-to-grasp answer there.
    – jsx97
    Commented May 19 at 7:43
  • 1
    ERE (pattern)? would be (pattern|) (pattern or nothing) or (pattern)(#c0,1) (a count of 0 to 1 patterns) with extendedglob (like ERE (pattern){0,1}) in zsh. Commented May 19 at 8:20

1 Answer 1


There are two differences:

  • The first command replaces spaces in the whole path, but only considers files in the current directory because the pattern doesn't use recursivve globbing. The second command looks for files in subdirectories recursively, and only replaces spaces in the base name (some directory/some filesome directory/some_file).
  • The first command only replaces sequences of spaces. The second command also replaces sequences of dashes with optional surrounding spaces. For example, foo bar, foo - bar, foo ---bar all map to foo_bar. foo-bar qux maps to foo_bar_qux. foo-bar would also map to foo_bar, but it isn't matched by the pattern.

In the second command, (**/)(* *) matches all paths under the current directory where the file's base name contains at least one space. The parentheses create named groups, which zmv places in the positional parameters, so the directory part (equivalent to $f:h/) is available as $1 in the replacement text and the base name (equivalent to $f:t) is available as $2. Note that the (**/) group is an exception: normally parentheses turn off the recursive behavior of **, but zmv treats (**/) specially.

In the replacement, $1$2 would be the original name (the trailing / is part of $1). ${2//…/_} replaces every occurence of the pattern … by an underscore. The pattern ( #-## #| ##) uses the “or” operator: a match is either a match of #-## # (zero or more spaces, one or more dashes, zero or more spaces) or a match of ## (one or more spaces).

  • Many thanks. That is, # means zero-or-more occurrences of the previous character (as ? in PERL-compatible regular expressions) and ## means one-or-more occurrences of the previous character (as + in PERL-compatible regular expressions), correct?
    – jsx97
    Commented May 18 at 4:54
  • @jsx97: ? means 0 or 1 in PCRE (and I believe every other regex engines). * would mean 0, 1 or more. Commented May 18 at 6:19
  • @OlivierDulac Ah, yes, sorry. Just in case, I really like regexr.com cheat sheet.
    – jsx97
    Commented May 18 at 7:15
  • 1
    @jsx97 Yes, those are glob operators available when extended_glob is active. Commented May 18 at 9:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .