tags:

views:

1956

answers:

5

I've got this little script in sh (Mac OSX 10.6) to look through an array of files. Google has stopped being helpful at this point:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

So far (obviously, to you shell gurus) $name merely holds 0, 1 or 2, depending on if grep found that the filename matched the matter provided. What I'd like is to capture what's inside the parens ([a-z]+) and store that to a variable.

I'd like to use grep only, if possible. If not, please no Python or Perl, etc. sed or something like it – I'm new to shell and would like to attack this from the *nix purist angle.

Also, as a super-cool bonus, I'm curious as to how I can concatenate string in shell? Is the group I captured was the string "somename" stored in $name, and I wanted to add the string ".jpg" to the end of it, could I cat $name '.jpg'?

Thanks! And please explain what's going on, if you've got the time.

A: 

Not possible in just grep I believe

for sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

I'll take a stab at the bonus though:

echo "$name.jpg"
cobbal
Ah, of course, thanks for that haha.
Isaac Hodes
Unfortunately, that `sed` solution doesn't work. It simply prints out everything in my directory.
Isaac Hodes
updated, will output a blank line if there isn't a match, so be sure to check for that
cobbal
It now outputs only blank lines!
Isaac Hodes
this sed has a problem. The first group of capturing parenthesis encompass everything. Of course \2 will have nothing.
it worked for some simple test cases... \2 gets the inner group
cobbal
+1  A: 

A suggestion for you - you can use parameter expansion to remove the part of the name from the last underscore onwards, and similarly at the start:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Then name will have the value abc.

See Apple developer docs, search forward for 'Parameter Expansion'.

martin clayton
Ah, now this does work. But is it *unix-y* enough? Hmm...
Isaac Hodes
this will not check for ([a-z]+).
@levislevis - that's true, but, as commented by the OP, it does do what was needed.
martin clayton
+2  A: 

This isn't really possible with pure grep, at least not generally.

But if your pattern is suitable, you may be able to use grep multiple times within a pipeline to first reduce your line to a known format, and then to extract just the bit you want. (Although tools like cut and sed are far better at this).

Suppose for the sake of argument that your pattern was a bit simpler: [0-9]+_([a-z]+)_ You could extract this like so:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

The first grep would remove any lines that didn't match your overall patern, the second grep (which has --only-matching specified) would display the alpha portion of the name. This only works because the pattern is suitable: "alpha portion" is specific enough to pull out what you want.

(Aside: Personally I'd use grep + cut to achieve what you are after: echo $name | grep {pattern} | cut -d _ -f 2. This gets cut to parse the line into fields by splitting on the delimiter _, and returns just field 2 (field numbers start at 1)).

Unix philosophy is to have tools which do one thing, and do it well, and combine them to achieve non-trivial tasks, so I'd argue that grep + sed etc is a more Unixy way of doing things :-)

RobM
Very interesting. I hadn't even heard of `cut`! How might I store the output of that to a variable, though? Does `cut` return the string it's just operated on, unlike `greg`?
Isaac Hodes
`for f in $files; do name=`echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'| cut -d _ -f 2`;` Aha!
Isaac Hodes
using shell, no need for grep + cut. wasting overheads if OP has lots of files..
i disagree with that "philosophy". if you can use the shell's in built capabilities without calling external commands, then your script will be a lot faster in performance. there are some tools that overlap in function. eg grep and sed and awk. all of them does string manipulations, but awk stands out above them all because it can do a lot more. Practically, all those chaining of commands, like the above double greps or grep+sed can be shortened by doing them with one awk process.
ghostdog74
@ghostdog74: No argument here that chaining lots of tiny operations together is generally less efficient than doing it all in one place, but I stand by my assertion that the Unix philosophy is lots of tools working together. For instance, tar just archives files, it doesn't compress them, and because it outputs to STDOUT by default you can pipe it across the network with netcat, or compress it with bzip2, etc. Which to my mind reinforces the convention and general ethos that Unix tools should be able to work together in pipes.
RobM
+4  A: 

If you're using Bash, you don't even have to use grep:

files="*.jpg"
for f in $files
do
    [[ $f =~ [0-9]+_([a-z]+)_[0-9a-z]* ]]
    name="${BASH_REMATCH[1]}"
    echo "${name}.jpg"    # concatenate strings
    name="${name}.jpg"    # same thing stored in a variable
done

This uses =~ which is Bash's regex match operator. The results of the match are saved to an array called $BASH_REMATCH. The first capture group is stored in index 1, the second (if any) in index 2, etc. Index zero is the full match.

You should be aware that without anchors, this regex (and the one using grep) will match any of the following examples and more, which may not be what you're looking for:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

To eliminate the second and fourth examples, make your regex like this:

^[0-9]+_([a-z]+)_[0-9a-z]*

which says the string must start with one or more digits. The carat represents the beginning of the string. If you add a dollar sign at the end of the regex, like this:

^[0-9]+_([a-z]+)_[0-9a-z]*$

then the third example will also be eliminated since the dot is not among the characters in the regex and the dollar sign represents the end of the string. Note that the fourth example fails this match as well.

Dennis Williamson
Thanks Dennis! I appreciate the detailed help – I had completely forgot about the `=~` operator (very new the Bash scripting, so I've seen it maybe once or twice). I've **never** seen `${BASH_REMATCH[n]}`! That would have saved me ages. Thanks so much! (Aside: the regex I made doesn't handle cases like the ones described very well, but it handled the large number of .jpg's i wanted to rename. I appreciate the extra RegEx explanations, too, though.) Cheers!
Isaac Hodes
+1  A: 

if you have bash, you can use extended globbing

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

or

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done
That looks intriguing. Could you perhaps append a little explanation to it? Or, if you're so inclined, link to a particularly insightful resource that explains it? Thanks!
Isaac Hodes
bash reference manual - 3.5.8.1 Pattern Matching
forgot the link: here it is http://www.gnu.org/software/bash/manual/bashref.html