1

Given a string s

s="B /home/BL/004_010_0100.0      23      0.031"

How can I grep only the number behind the path, tab and space in a string?

In the string s above, I'd like to extract the number 23.

num=$(echo $s | grep 'B .*\t (\d*)')
1
  • What can be assumed about the substring that looks like a pathname? Can it contain spaces and/or tabs? Are the "fields" in the $s string delimited by spaces or tabs, or by some other character? You mention a tab and a space, is that what's just in front of the number that you're looking for?
    – Kusalananda
    Mar 23, 2021 at 15:45

4 Answers 4

4

Treating the string as a set of whitespace-delimited fields, you want the second to last field:

num=$( awk '{ print $(NF-1) }' <<<"$s" )

or, in shells without here-strings,

num=$( printf '%s\n' "$s" | awk '{ print $(NF-1) }' )

This feeds the string in $s into an awk command. The awk command outputs the second to last whitespace-delimited field. This result is assigned to the num variable.

Testing:

$ s="B /home/BL/004_010_0100.0      23      0.031"
$ num=$( awk '{ print $(NF-1) }' <<<"$s" )
$ printf 'num is "%s"\n' "$num"
num is "23"

If your data in $s comes from a command, then you may feed that into awk directly instead of storing it in an intermediate variable:

num=$( some-command | awk '{ print $(NF-1) }' )

grep is a tool that returns matching lines (ignoring the non-standard -o option available in some implementations of the tool). We can use grep to pick out the number if we first transform the string $s into several lines based on the whitespaces in the string:

$ tr -s '[:blank:]' '[\n*]' <<<"$s" | grep -x '[[:digit:]]\{1,\}'
23

The tr command used here changes the string from

B /home/BL/004_010_0100.0      23      0.031

into

B
/home/BL/004_010_0100.0
23
0.031

and the grep command picks out the line that only consists of digits (the -x option would force the given pattern to match a complete line). This woud obviously only work if the number that you're looking for is a positive integer.

If you knew that you'd be interested in the second to last "field", then you could have used tail and head instead:

$ tr -s '[:blank:]' '[\n*]' <<<"$s" | tail -n 2 | head -n 1
23

... or sed:

$ tr -s '[:blank:]' '[\n*]' <<<"$s" | sed -n -e '${ g; p; }' -e h
23

All the variations above are standard and portable. We can also use cut to extract the second to last field if we use the non-standard rev utility to reverse the line twice:

$ rev <<<"$s" | tr -s '[:blank:]' '[\t*]' | cut -f 2 | rev
23

Here, we also employ tr to replace all whitespace characters by tabs (and squeeze them into single tabs). cut then simply extracts the second field before rev reverses the extracted data again.

4

You can try it with Perl:

echo "$s" | perl -e 'for(<>){/B\s+.*?\s+(\d+)\s+/;print $1}'

Here we find the string with:

  • B character
  • followed by one or more space characters - \s+
  • followed by all lazy characters before the first space character(s) - .*?\s+
  • followed by our desired number - capture it in capture group in parentheses (\d+) - it's saved in $1 special variable
  • followed by one or more space characters - \s+.

This regexp could be refined (e.g. with ^ and $ operators to point out the start and the end of string).

Read more about regex.

0
2

If you have access to GNU grep (the default on Linux systems), this regex will capture digits without any decimal in them.

grep -oP '\b(?<!\.)\d+(?!\.)\b'

regex Explanation:

  • \b matches a word boundary
  • (?<!\.) Negative lookbehind to assert no decimal(.) is behind
  • \d+ matches a digit one or more times
  • (?!\.) Negative lookahead asserts no decimal(.) is ahead
  • \b matches a word boundary
5
  • 3
    @GeoRie But this will also match some unrelated things. Check it with echo "AZAZA LOL 23 WTF" | grep -oP '\b(?<!\.)\d+(?!\.)\b' - it returns 23. So this is a bit inaccurate solution.
    – narotello
    Mar 23, 2021 at 11:52
  • 1
    @narotello isn't it supposed to return 23? Mar 23, 2021 at 11:55
  • 2
    Yep, it's supposed. It is called false-positive error. The one who search the string with specified format (like starting from B and so on) will find the value from wrong string.
    – narotello
    Mar 23, 2021 at 12:03
  • If the pathname in the string contains spaces, this may possibly pick up numbers from it instead of from the end of the string. It also assumes that there are no numbers earlier in the string (we don't know any variations of the data format though).
    – Kusalananda
    Mar 23, 2021 at 15:41
  • This would also extract 23 if the number was actually -23.
    – Kusalananda
    Mar 23, 2021 at 16:07
0

Just changed your grep a bit. If you are using GNU grep (the default on Linux systems), this should work:

s="B /home/BL/004_010_0100.0      23      0.031"
num=$(echo $s | grep -oP '\.*?\s+\d+\s+')
2
  • 2
    This will include the whitespace on either side of the number.
    – terdon
    Mar 23, 2021 at 11:20
  • What good does the \.*? do? If I leave it out, this behaves the same. P.S. You should always use quotes when referencing shell variables; e.g., echo "$s" rather than echo $s. Mar 23, 2021 at 23:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .