Moving Multiple Files in Bash
It seems like the shorter the code is the longer it takes to write. I'm trying to rename a group of similarly-name directories using Bash and for no good reason I decided I'd like to do it all on one line. There are a bunch of ways to do it, not all fit on a line and some are more efficient than others. There's apparently a package out there called mmv that I haven't looked for which does stuff like this. There's also sed. With sed I got something close: ls -1 | sed -n "s/\(.*\)\(\.com\)\($\|.*\)/mv \1\2\3 \1.design\3/e" The .com is a common part of the directory names. My directory is something like this:
  • www
    • rrc.ca
    • gsc.com
    • lnpc.net.cms
    • obc.com
And I'd like to replace the ca, com, and net parts of the folder names with the word "desgin". Yes, this has to do with a development server I work on but the principle's just about manipulating files from the command line. So the sed script above would probably get the .com names but not the .ca or .net. It could also mess up if the text ".com" showed up somewhere besides the end of the name. I've only tried it with a /p in place of the /e. In the GNU sed manual you can see the /p prints whereas the /e is a GNU sed extension that executes the command in the pattern space. Use that script at your own risk but definitely test with that 'p' command to see what will happen first. I also looked at some possibilities with a for loop to go through each of ca, com and net but it seemed to be getting too complex. I talked to Jeff about it and he came up with a little snippet (untested): for f in `ls -1` do name=`echo $f | cut -f1 -d"."` mv $f $name.design done This just gets everything after the last . if I read it right, but I hadn't told him some of the folders have a .cms at the end. I was about to give up since I've wasted too much time on it but then a quick Googling turned up a little gem I'd totally forgotten about: shell parameter expansion. The example doesn't suit my needs exactly but there's a lot more bash can do with shell parameters. In my case I want to do something like for f in `ls -1`; do mv "$f" "${f/.com/.design}"; done I tested it with echo instead of mv and it's pretty close to what I want. What this one-liner does is easy to explain if I break it down. `ls -1` This can be thought of as saying execute ls -1 and put the results of that command here. This will make a list of the contents of this directory without the extra info ls usually outputs. The for command knows how to process lists like this. mv "$f" "${f/.com/.design}" The for loop puts the current item being processed, that is, one of the lines from the output of ls -1, into the shell variable $f. That tells us what the first parameter to mv is. The ${f/.com/.design} part of this is parsed by Bash using the Shell Parameter Expansion rules. Specifically I'm using the ${parameter/pattern/string} syntax at the end of that page I linked to. We're replacing the .com with .design in the parameter f and the result goes inside the quotes. So this looks promising. It still has the problem that it can match filenames that have another .com in them somewhere, but that's not a problem I want to fix today. What I looked at next was how to get that one-liner to do the .ca and .net folders as well. Interestingly, there is a syntax that will handle this for me. The pattern part of ${parameter/pattern/string} is described in more detail in Ramey and Fox's book in this section. First I found this:
@(pattern-list) Matches one of the given patterns.
It looked perfect but when I tried to use it I didn't get what I hoped for. Serves me right for skimming. The @(pattern-list) only applies with a special shell option turned on.
If the extglob shell option is enabled using the shopt builtin, several extended pattern matching operators are recognized. In the following description, a pattern-list is a list of one or more patterns separated by a ‘|’. Composite patterns may be formed using one or more of the following sub-patterns: ...
Luckily shopt is a simple built-in Bash command. In my OpenSuse 10.3 install, the extglob option is turned off by default it seems. I found this out by just typing shopt at the command line then I turned the extglob option on. rob4@copper:~> shopt | grep ext extdebug off extglob off extquote on rob4@copper:~> shopt -s extglob rob4@copper:~> shopt | grep ext extdebug off extglob on extquote on So after this I can use the @(pattern) syntax I wanted to try: for f in `ls -1`; do mv "$f" "${f/@(.com|.ca|.net)/.design}"; done This will still do work for names that don't match the pattern at all though. So we can just refine the list of stuff that goes in. for f in `ls -1d *.com* *.ca* *.net*`; do mv "$f" "${f/@(.com|.ca|.net)/.design}"; done So about 3 hours of work to save me typing in mv over again about ten times with a list of directory names. Sounds about par for the course ;-).
3.666665
Your rating: None Average: 3.7 (3 votes)

My example gets everything BEFORE the first period (-f1 is the first field in the cut command). I think mine is easier to read personally, and of course you can collapse mine down to a one-line too if you really want. Anyway, parm expansion does look cool and useful...