148

I'm currently trying to rexp a string into multiple variables. Example string:

ryan_string = "RyanOnRails: This is a test"

I've matched it with this regexp, with 3 groups:

ryan_group = ryan_string.scan(/(^.*)(:)(.*)/i)

Now to access each group I have to do something like this:

ryan_group[0][0] (first group) RyanOnRails
ryan_group[0][1] (second group) :
ryan_group[0][2] (third group) This is a test

This seems pretty ridiculous and it feels like I'm doing something wrong. I would be expect to be able to do something like this:

g1, g2, g3 = ryan_string.scan(/(^.*)(:)(.*)/i)

Is this possible? Or is there a better way than how I'm doing it?

5 Answers 5

236

You don't want scan for this, as it makes little sense. You can use String#match which will return a MatchData object, you can then call #captures to return an Array of captures. Something like this:

#!/usr/bin/env ruby

string = "RyanOnRails: This is a test"
one, two, three = string.match(/(^.*)(:)(.*)/i).captures

p one   #=> "RyanOnRails"
p two   #=> ":"
p three #=> " This is a test"

Be aware that if no match is found, String#match will return nil, so something like this might work better:

if match = string.match(/(^.*)(:)(.*)/i)
  one, two, three = match.captures
end

Although scan does make little sense for this. It does still do the job, you just need to flatten the returned Array first. one, two, three = string.scan(/(^.*)(:)(.*)/i).flatten

Sign up to request clarification or add additional context in comments.

3 Comments

Beware that if no matches are found, match returns nil and you get a NilError. If you are in Rails, I suggest you to change: one, two, three = string.match(/(^.*)(:)(.*)/i).captures into: one, two, three = string.match(/(^.*)(:)(.*)/i).try(:captures)
@AndreaSalicetti I've edited my post, I'm not adding Rails-specific code to it so I have altered it with a version for handling the returned nil object
You could also the new &. operator to get it back on a line and even using it twice when there is only one capture group. Eg.., string.match(regex)&.captures&.first
51

You could use Match or =~ instead which would give you a single match and you could either access the match data the same way or just use the special match variables $1, $2, $3

Something like:

if ryan_string =~ /(^.*)(:)(.*)/i
   first = $1
   third = $3
end

1 Comment

@Gaston that's actually the original regexp syntax originating from Perl :)
37

You can name your captured matches

string = "RyanOnRails: This is a test"
/(?<one>^.*)(?<two>:)(?<three>.*)/i =~ string
puts one, two, three

It doesn't work if you reverse the order of string and the regex.

1 Comment

This should be the accepted answer -- I've been writing the equivalent of one, two, three = (md = /(?<one>^.*)(?<two>:)(?<three>.*)/i.match(string)) && [md[:one], md[:two], md[:three]] for literally YEARS for not knowing this
7

You have to decide whether it is a good idea, but ruby regexp can (automagically) define local variables for you!

I am not yet sure whether this feature is awesome or just totally crazy, but your regex can define local variables.

ryan_string = "RyanOnRails: This is a test"
/^(?<webframework>.*)(?<colon>:)(?<rest>)/ =~ ryan_string
# This defined three variables for you. Crazy, but true.
webframework # => "RyanOnRails"
puts "W: #{webframework} , C: #{colon}, R: #{rest}"

(Take a look at http://ruby-doc.org/core-2.1.1/Regexp.html , search for "local variable").

Note: As pointed out in a comment, I see that there is a similar and earlier answer to this question by @toonsend (https://stackoverflow.com/a/21412455). I do not think I was "stealing", but if you want to be fair with praises and honor the first answer, feel free :) I hope no animals were harmed.

2 Comments

This answer looks remarkably similar to stackoverflow.com/a/21412455/525478, which is over a year older...
@BradWerth I guess I just did not see that. But I updated my answer to include your concerns.
6

scan() will find all non-overlapping matches of the regex in your string, so instead of returning an array of your groups like you seem to be expecting, it is returning an array of arrays.

You are probably better off using match(), and then getting the array of captures using MatchData#captures:

g1, g2, g3 = ryan_string.match(/(^.*)(:)(.*)/i).captures

However you could also do this with scan() if you wanted to:

g1, g2, g3 = ryan_string.scan(/(^.*)(:)(.*)/i)[0]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.