How to rename files using Zsh shell and `sed` command on Mac OSX
Asked Answered
R

3

7

I have a folder on my desktop called "Images", inside that folder are 3 images...

File-A-B.gif
File-C-D.gif
File-E-F.gif

...I want to rename the files on the command line so the second hyphen/dash is removed. So the result would be files named like so...

File-AB.gif
File-CD.gif
File-EF.gif

...I'm using a combination of commands piped through to each other but not having much luck in getting them to work.

Initially I had ls File-* | sed 's/\(File-[^-]\)-\(.+\)/mv & \1\2/' | sh

The break-down of this command is...

  • list out any files that begin File-
  • pipe the results through to sed
  • we'll use sed to capture different parts of the name
  • looking at the first file we'll capture the File-A and the B.gif
  • with sed we then replace the matched file name with a move command and effectively tell it to rename the original file to a file made up of the two pieces of data we've captured
  • we then pipe that command through to sh

The initial error I was getting was a permissions error. I'm not sure why as I had 'read and write' access to the files.

To work around this I chmod 777 each of the files.

This then resulted in a new error which was cannot execute binary file.

To work around that error I saw this post: http://blog.mpdaugherty.com/2010/05/27/difference-with-sed-in-place-editing-on-mac-os-x-vs-linux/ which suggests adding an extra pair of single quotes. The reason this works (apparently) is because sed will error because it doesn't know what file extension to use, but putting an empty String neutralises that error (on a side note: my understanding was that you also need to use the -i flag - which I'm not using - but doing that caused a different error so I scrapped that and just used the single quotes to avoid the binary file error I was getting).

So now I have ls File-* | sed '' 's/\(File-[^-]\)-\(.+\)/mv & \1\2/' | sh

But this errors with No such file or directory?

I tried to be explicit with the location of the file, so for example...

ls File-* | sed '' 's/\(File-[^-]\)-\(.+\)/mv ~\/Desktop\/Images\/& ~/Desktop\/Images\/\1\2/' | sh

...notice I've had to escape the forward slashes ~\/Desktop\/Images\/ but this still didn't work.

I also installed the GNU version of sed using Homebrew but it also errored in a similar way with gsed: can't read s/\(Vim-[^-]\)-\(.+\)/mv & \1\2/: No such file or directory

I'm a bit at a loss with how I can get this to work.

I don't want to have to resort to writing a bash/zsh script or a Ruby script.

Any help you can give me would be greatly appreciated.

Radarman answered 27/9, 2013 at 13:31 Comment(0)
W
3

The following should work for you:

ls File-* | sed 's/\(.*\)-\(.*\)/mv & \1\2/' | sh

What you tried

ls File-* | sed '' 's/\(File-[^-]\)-\(.+\)/mv & \1\2/' | sh

was pretty close. Saying:

ls File-* | sed '' 's/\(File-[^-]*\)-\(.*\)/mv & \1\2/' | sh

instead would have worked.

Walkin answered 27/9, 2013 at 13:41 Comment(5)
Thanks! It seems the first option you mentioned worked for me but it did produce an error in output (even though it correctly renamed the files)? The error was preexec:4: command not found: ^A^B do you have any idea what that means? Side note, your last example which you say was what I almost had, didn't work. But I can see why the first version works (I think), we're interpolating the other commands using combination of echo and $(). I'm not 100% sure what $() is normally used for though? Could you add extra explanation to your answer to help me (and others) understand better :)Radarman
$() is process substitution. I guess since we're anyways piping to sh it's not required. See edit above.Walkin
Just to be clear for others reading this: I made two mistakes in my original version. The biggest mistake was that I'd used + (which typical in regexp means "one or more") but that's not supported, so I needed * (which means "zero or more"). The other mistake which didn't actually effect anything but might cause someone else confusion is I missed an *. So (File-[^-]\) passed File-A-B.gif but would have failed File-AB-C.gif so it should have been (File-[^-]*\) instead (i.e. [^-]* means "match anything that isn't a - "zero or more times").Radarman
btw I was interested to see how it worked using $() but ls File-* | sed 's/\(.*\)-\(.*\)/echo $(mv & \1\2)/' generated an error for me so I guess the echo $() section doesn't work by itself?Radarman
@Radarman Can't say what's the possible issue since I'm not on a Mac, but it should have also worked.Walkin
A
14

If you're using zsh, you don't need to use sed: there's a zsh module called zmv that'll achieve this pretty simply:

$ ls -F -G
File-A-B.gif  File-C-D.gif  File-E-F.gif

$ zmv -n 'File-(*)-(*).gif' 'File-${1}${2}.gif'
mv -- File-A-B.gif File-AB.gif
mv -- File-C-D.gif File-CD.gif
mv -- File-E-F.gif File-EF.gif

Note: Omit -n to get it to actually run.

It works by matching things inside brackets. In this case: File-(*)-(*).gif will both match File-A-B.gif and also save A and B so we can refer to them later. Then, we move the file we've just matched to a new filename, omitting the hyphen between the two letters and inserting the letters using references: File-${1}${2}.gif.

It's quite a powerful little module, provided you can give it the two regexes: one to match files to rename, and the second to match the renamed-file name.

Abstracted answered 29/9, 2013 at 10:3 Comment(1)
In case of zsh: command not found: zmv load it with autoload -U zmv. Details about this function man zshcontribGreenebaum
W
3

The following should work for you:

ls File-* | sed 's/\(.*\)-\(.*\)/mv & \1\2/' | sh

What you tried

ls File-* | sed '' 's/\(File-[^-]\)-\(.+\)/mv & \1\2/' | sh

was pretty close. Saying:

ls File-* | sed '' 's/\(File-[^-]*\)-\(.*\)/mv & \1\2/' | sh

instead would have worked.

Walkin answered 27/9, 2013 at 13:41 Comment(5)
Thanks! It seems the first option you mentioned worked for me but it did produce an error in output (even though it correctly renamed the files)? The error was preexec:4: command not found: ^A^B do you have any idea what that means? Side note, your last example which you say was what I almost had, didn't work. But I can see why the first version works (I think), we're interpolating the other commands using combination of echo and $(). I'm not 100% sure what $() is normally used for though? Could you add extra explanation to your answer to help me (and others) understand better :)Radarman
$() is process substitution. I guess since we're anyways piping to sh it's not required. See edit above.Walkin
Just to be clear for others reading this: I made two mistakes in my original version. The biggest mistake was that I'd used + (which typical in regexp means "one or more") but that's not supported, so I needed * (which means "zero or more"). The other mistake which didn't actually effect anything but might cause someone else confusion is I missed an *. So (File-[^-]\) passed File-A-B.gif but would have failed File-AB-C.gif so it should have been (File-[^-]*\) instead (i.e. [^-]* means "match anything that isn't a - "zero or more times").Radarman
btw I was interested to see how it worked using $() but ls File-* | sed 's/\(.*\)-\(.*\)/echo $(mv & \1\2)/' generated an error for me so I guess the echo $() section doesn't work by itself?Radarman
@Radarman Can't say what's the possible issue since I'm not on a Mac, but it should have also worked.Walkin
G
0

Another way this can be done is with mv, for example:

mv <current-filename> <new-filename> 
Gallstone answered 7/11, 2023 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.