200

I want to grab the last two numbers (one int, one float; followed by optional whitespace) and print only them.

Example:

foo bar <foo> bla 1 2 3.4

Should print:

2 3.4

So far, I have the following:

sed -n  's/\([0-9][0-9]*[\ \t][0-9.]*[\ \t]*$\)/replacement/p' 

will give me

foo bar <foo> bla 1 replacement

However, if I try to replace it with group 1, the whole line is printed.

sed -n  's/\([0-9][0-9]*[\ \t][0-9.]*[\ \t]*$\)/\1/p' 

How can I print only the section of the line that matches the regex in the group?

2
  • The question asks for more than just printing. A person with the appropriate permissions should modify this question. Commented Sep 2, 2020 at 15:48
  • to avoid false positives (int + ...) and (int + int) use: sed -nr 's/.*([0-9]+[\ \t][0-9]+.[0-9]+[\ \t]*$)/\1/p'
    – ash17
    Commented Oct 29, 2021 at 12:09

5 Answers 5

210

Match the whole line, so add a .* at the beginning of your regex. This causes the entire line to be replaced with the contents of the group

echo "foo bar <foo> bla 1 2 3.4" |
 sed -n  's/.*\([0-9][0-9]*[\ \t][0-9.]*[ \t]*$\)/\1/p'
2 3.4
6
  • 55
    I had to add the -r or ` --regexp-extended` option otherwise I was getting invalid reference \1 on s' command's RHS ` error. Commented Aug 11, 2014 at 16:11
  • 19
    @DanielSokolowski I think you get that error if you use ( and ) instead of \( and \). Commented Jun 24, 2015 at 11:27
  • 4
    Also remember to add .* to the end of the regexp if the string you want to extract is not always at the end of the line. Commented Nov 22, 2017 at 8:54
  • 7
    This won't work for me because .* is greedy and sed doesn't have a non-greedy .*? Commented Oct 3, 2018 at 15:21
  • @DanielDarabos Just mention that ( and ) will not raise error in ubuntu 16.04. So I think this comment is outdated.
    – Li haonan
    Commented Sep 6, 2019 at 20:52
94

grep is the right tool for extracting.

using your example and your regex:

kent$  echo 'foo bar <foo> bla 1 2 3.4'|grep -o '[0-9][0-9]*[\ \t][0-9.]*[\ \t]*$'
2 3.4
5
  • 20
    great for the entire group, though sed is needed for individual groups
    – jozxyqk
    Commented Aug 26, 2014 at 9:04
  • grep -o does not port on systems running msysgit but sed does. Commented Jun 15, 2015 at 3:43
  • See the question linked by @jozxyqk for an answer that uses look-ahead and look-behind to solve this with grep. Commented Feb 22, 2016 at 23:08
  • You can extract a group from a pattern with piped grep -o calls. stackoverflow.com/a/58314379/117471 Commented Oct 10, 2019 at 1:54
  • 1
    Note for more complex regex you need to use `grep -Eo'
    – cdalxndr
    Commented Jan 26, 2021 at 13:57
13

And for yet another option, I'd go with awk!

echo "foo bar <foo> bla 1 2 3.4" | awk '{ print $(NF-1), $NF; }'

This will split the input (I'm using STDIN here, but your input could easily be a file) on spaces, and then print out the last-but-one field, and then the last field. The $NF variables hold the number of fields found after exploding on spaces.

The benefit of this is that it doesn't matter if what precedes the last two fields changes, as long as you only ever want the last two it'll continue to work.

8

The cut command is designed for this exact situation. It will "cut" on any delimiter and then you can specify which chunks should be output.

For instance: echo "foo bar <foo> bla 1 2 3.4" | cut -d " " -f 6-7

Will result in output of: 2 3.4

-d sets the delimiter

-f selects the range of 'fields' to output, in this case, it's the 6th through 7th chunks of the original string. You can also specify the range as a list, such as 6,7.

5
  • To print only certain columns, pipe to awk '{ print $2" "$6 }'
    – nurettin
    Commented Jun 8, 2018 at 10:38
  • @nurettin I think your comment might have been meant for one of the awk answers. Commented Jun 14, 2018 at 21:50
  • I tried cut when I visited this page and realized it's limitations and decided to write a more generalized version in awk instead as a comment to improve the quality of this post.
    – nurettin
    Commented Jun 15, 2018 at 5:33
  • 1
    Yeah, I think that belongs on a different answer involving awk. The cut command to do what you wrote is: cut -d " " -f 2,6 Commented Jun 15, 2018 at 22:09
  • ah, I didn't know that, I thought you could only give ranges. Thanks for that.
    – nurettin
    Commented Jun 16, 2018 at 6:33
8

I agree with @kent that this is well suited for grep -o. If you need to extract a group within a pattern, you can do it with a 2nd grep.

# To extract \1 from /xx([0-9]+)yy/
$ echo "aa678bb xx123yy xx4yy aa42 aa9bb" | grep -Eo 'xx[0-9]+yy' | grep -Eo '[0-9]+'
123
4

# To extract \1 from /a([0-9]+)b/
$ echo "aa678bb xx123yy xx4yy aa42 aa9bb" | grep -Eo 'a[0-9]+b' | grep -Eo '[0-9]+'
678
9

I generally cringe when I see 2 calls to grep/sed/awk piped together, but it's not always wrong. While we should exercise our skills of doing things efficiently, "A foolish consistency is the hobgoblin of little minds", and "Real artists ship".

1
  • 1
    like that you added -E flag that allows more complex regex
    – cdalxndr
    Commented Jan 26, 2021 at 13:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.