4

I'm trying to manually pull the digests off of Rails assets (don't ask). I was directed to ZMV for easy regex-based find/replace. But the normal {32} syntax for specifying an amount of repetition does not work:

$ zmv -n '(**/)(*)' '$1${2//-[A-Za-z0-9]\{32\}/}'

I've tried some other formats. This, for example, works, but is too greedy (it will turn image-3.png into image.png, for example):

$ zmv -n '(**/)(*)' '$1${2//-[A-Za-z0-9]##\./.}'

That double-hash syntax only showed up after a lot of Googling (I'd have expected +). But I cannot for the life of me find how to make {32} work. I tried #32#? Which appeared to work, but that's because it was reading it as (in my eyes) ?32? and that means it met anything that had a three in the digest or last character.

How do I signify character repetition in zmv?

EDIT:

Apparently it would help some to view filenames I'm trying to match? To be clear: my question is "how do I signify character repetition in zmv" not "how do I match these filenames" (a question I know the answer to in standard RegEx format). If it helps, here is my intended before and after:

directory/asset-jej4jtifne9bjkkeuwr09rewrewlur23.css
another-directory/style-748reiodlpqwerntaerwerwerexfzsdf.js.gz
directory/subdirectory/this-is-a-thing-qwertyuiopasdfghjklzxcvbnm123456.js
third-directory/should-not-match-3.css

Should become:

directory/asset.css
another-directory/style.js.gz
directory/subdirectory/this-is-a-thing.js
third-directory/should-not-match-3.css

SECOND EDIT:

Because I needed to do this yesterday, I did it the long way and (as expected) it worked. I'd still like to know how to avoid it in the future. Here's the command I ended up using (I repeated my character matcher 32 times explicitly):

$ zmv '(***/)(*)' '$1${2//-[A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9][A-Za-z0-9]/}'

THIRD EDIT:

For the record, I'm using zsh on OS X. I'd imagine zmv is the same across platforms, but I couldn't say for sure.

4
  • Please edit your question and provide a list of input files and your desired output. You need to explain what you are trying to match, not just give a regex that may or may not do what you need.
    – terdon
    Mar 27, 2015 at 12:51
  • @terdon I think that clouds the question, as my RegEx already matches the filenames as expected on sites like rubular.com or in Ruby, JS, etc., and my question is how do I do this in zmv, but I've added them if it helps. Mar 27, 2015 at 14:35
  • Fair enough. I just wanted to check because i) most users don't actually know much about regexes and ii) so I could have something to play around with and see if I could figure it out. By the way, while this is perfectly on topic here, in the future, you might want to consider asking such questions on Unix & Linux instead. There's a much higher concentration of CLI junkies there, including quite a few zsh experts.
    – terdon
    Mar 27, 2015 at 15:50
  • Good to know. I wasn't sure - I spend pretty much all my time on StackOverflow but this didn't seem right there. Mar 27, 2015 at 15:52

2 Answers 2

7

Shells mostly don't provide the usual regexp syntax, but wildcard “glob” patterns. Basic shell wildcards aren't as powerful as regular expressions; for example, the regexp .* (any sequence of characters) is equivalent to the glob pattern * but the regexp a* (any sequence of a's) has no glob pattern equivalent in plain sh. See Why does my regular expression work in X but not in Y? for an overview of the main different regexp/pattern syntaxes.

Zsh has zsh extended glob patterns which provides the same expressive power as regular expressions but with different syntax. These patterns are automatically enabled in zmv and in completion functions, but elsewhere in zsh they need to be enabled explicitly with setopt extended_glob (put that in your .zshrc — the only reason it isn't the default is backward compatibility with ancient versions of zsh).

There is a repeat-N-times syntax, but it's a bit hidden, listed under globbing flags rather than under the list of operators. It's the c flag, which must be used alone, followed by the number of repetition (or two comma-separated numbers to give a range).

zmv -n '(**/)(*)' '$1${2//-[A-Za-z0-9](#c32)/}'
3
  • (#c32) is what I was looking for! Awesome! Thanks so much. I really need to read up on globs in general. Mar 27, 2015 at 17:10
  • PS I think the postfix # wouldn't be what I was looking for because it would match the should-not-match-3.css file, and remove -3, which I didn't want. Mar 27, 2015 at 17:10
  • Last comment: "Why does my regular expression work in X but not Y" is an amazing resource that I've been seeking for years. Thank you! Mar 27, 2015 at 17:12
1

I can't get it to work with zmv either. There must be a way but it escapes me. However, zmv is not the only tool that can do something like that. You can also use rename.

  • If you're using zsh

    $ rename -n 's/-[A-Za-z0-9]{32}//' **/* 
    another-directory/style-748reiodlpqwerntaerwerwerexfzsdf.js.gz renamed as another-directory/style.js.gz
    directory/asset-jej4jtifne9bjkkeuwr09rewrewlur23.css renamed as directory/asset.css
    directory/subdirectory/this-is-a-thing-qwertyuiopasdfghjklzxcvbnm123456.js renamed as directory/subdirectory/this-is-a-thing.js
    
  • If you're using bash

    $ shopt -s globstar 
    $ rename -n 's/-[A-Za-z0-9]{32}//' **/* 
    another-directory/style-748reiodlpqwerntaerwerwerexfzsdf.js.gz renamed as another-directory/style.js.gz
    directory/asset-jej4jtifne9bjkkeuwr09rewrewlur23.css renamed as directory/asset.css
    directory/subdirectory/this-is-a-thing-qwertyuiopasdfghjklzxcvbnm123456.js renamed as directory/subdirectory/this-is-a-thing.js
    

Note that there are two rename commands in the Linux world. The examples above use the Perl one which is the default on Debian-based distributions.


The reason you couldn't get this to work with zmv is that i) it's not zmv that interprets the expression, that's a shell feature and therefore ii) that's not a regular expression at all, it's a glob.

When you run the command in your question, zmv sets $2 to each of the file names and then it's the shell that runs the replacement (${2//...). Once the variable has been expanded by the shell, it is returned to zmv which attempts the rename operation.

Like the korn shell and bash, zsh supports the ${foo//bar} format which will remove all matches of the glob bar from the variable $foo (contrast it with ${foo/bar} which will only remove the first match). It works like this:

% foo="Xababab"
% echo ${foo//ab}
X
% echo ${foo//a*b}
X

As you can see above, the patterns are globs and not regular expressions. The glob a*b means "match a, then 0 or more characters and then b". It is the equivalent of this regular expression: a.*b. Globs, unlike regexes, don't support repetition (Apparently, zsh's globs do, see Gilles's answer). The x{n} syntax will not match n repetitions of x. This, therefore is why your regex failed: it wasn't being interpreted as a regex at all!

8
  • In my googling I saw suggestions to use rename but zsh doesn't recognize that for me – probably because I'm in OS X, not Linux. I'll add that detail above. Some more googling tells me I can just brew install rename, which would've saved me all this trouble. Upvoting because this solves my actual problem. I'd still like to know if there's a way to do this in zmv. Mar 27, 2015 at 16:16
  • 1
    @BenSaufley yeah, me too now. It's strange that none of the obvious solutions work. I asked one of our resident zsh experts on U&L in chat and gave the link to this Q. We'll see if he responds. Seriously though, next time you have a zsh related question, do ask on U&L. We have a couple of zsh fanatics there who'll always answer zsh questions and who are ridiculously knowledgeable about it.
    – terdon
    Mar 27, 2015 at 16:18
  • Thanks for the tip; I definitely will. There's so much to learn! Mar 27, 2015 at 16:20
  • @BenSaufley I figured it out. I should have realized sooner, that's not a regex, it's a glob and globs don't do repetitions. See updated answer.
    – terdon
    Mar 27, 2015 at 16:44
  • 1
    @BenSaufley the expert I called on responded. Please feel free to accept his answer since he does provide the way to do it using the tools you wanted.
    – terdon
    Mar 27, 2015 at 17:09

You must log in to answer this question.

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