389

Suppose I have the string 1:2:3:4:5 and I want to get its last field (5 in this case). How do I do that using Bash? I tried cut, but I don't know how to specify the last field with -f.

JonyD
  • 1,237
  • 3
  • 21
  • 34
cd1
  • 15,908
  • 12
  • 46
  • 47

17 Answers17

574

You can use string operators:

$ foo=1:2:3:4:5
$ echo ${foo##*:}
5

This trims everything from the front until a ':', greedily.

${foo  <-- from variable foo
  ##   <-- greedy front trim
  *    <-- matches anything
  :    <-- until the last ':'
 }
Stephen
  • 47,994
  • 7
  • 61
  • 70
  • 8
    While this is working for the given problem, the answer of William below (http://stackoverflow.com/a/3163857/520162) also returns `5` if the string is `1:2:3:4:5:` (while using the string operators yields an empty result). This is especially handy when parsing paths that could contain (or not) a finishing `/` character. – eckes Jan 23 '13 at 15:23
  • 12
    How would you then do the opposite of this? to echo out '1:2:3:4:'? – Dobz Jun 25 '14 at 11:44
  • 20
    And how does one keep the part before the last separator? Apparently by using `${foo%:*}`. `#` - from beginning; `%` - from end. `#`, `%` - shortest match; `##`, `%%` - longest match. – Mihai Danila Jul 09 '14 at 14:07
  • 1
    If i want to get the last element from path, how should I use it? `echo ${pwd##*/}` does not work. – Putnik Feb 11 '16 at 22:33
  • 2
    @Putnik that command sees `pwd` as a variable. Try `dir=$(pwd); echo ${dir##*/}`. Works for me! – Stan Strum Dec 17 '17 at 04:22
  • @Stephen How do I use dot (.) as a delimiter? – Manoj Jun 27 '18 at 05:58
  • 1
    @Stan even shorter is `echo ${$(pwd)##*/}` – Cadoiz Jun 15 '21 at 22:37
  • This worked well for getting the name of the file at the end of a URL for me, by replacing `:` with `/`. – Matt Welke Jul 06 '22 at 05:03
409

Another way is to reverse before and after cut:

$ echo ab:cd:ef | rev | cut -d: -f1 | rev
ef

This makes it very easy to get the last but one field, or any range of fields numbered from the end.

a3nm
  • 8,717
  • 6
  • 31
  • 39
  • 21
    This answer is nice because it uses 'cut', which the author is (presumably) already familiar. Plus, I like this answer because *I* am using 'cut' and had this exact question, hence finding this thread via search. – Dannid Jan 14 '13 at 20:50
  • 6
    Some cut-and-paste fodder for people using spaces as delimiters: `echo "1 2 3 4" | rev | cut -d " " -f1 | rev` – funroll Aug 12 '13 at 19:51
  • 2
    the rev | cut -d -f1 | rev is so clever! Thanks! Helped me a bunch (my use case was rev | -d ' ' -f 2- | rev – EdgeCaseBerg Sep 08 '13 at 05:01
  • 1
    I always forget about `rev`, was just what I needed! `cut -b20- | rev | cut -b10- | rev` – shearn89 Aug 17 '17 at 09:27
  • 1
    I ended up with this solution, my attempt o cut file paths with "awk -F "/" '{print $NF}' " broke somewhat for me, as file names including white spaces got also cut apart – THX Feb 26 '18 at 14:34
  • Beware: `rev` is not safe with multi-byte Unicode characters! Therefore some corner cases might not work with `rev`. – t0r0X Mar 01 '18 at 23:11
  • @t0r0X: Are you sure? On my machine, with `LC_ALL=en_US.utf8`, running `echo 'hé' | rev` correctly returns `éh`. I have to run `echo 'hé' | LC_ALL=C rev` to get an error: `rev: stdin: Invalid or incomplete multibyte or wide character`. – a3nm Mar 02 '18 at 00:55
  • except that: `-sh: rev: command not found` on my NAS, seems like rev is not so common, otherwise I agree it better answers a question about cut – papo Nov 19 '18 at 10:29
  • Fantastic! I wanted to grab only the top-level and second-level domains from a domain name. With `cut`, I can turn things like www.google.com into google.com! – b_laoshi Nov 29 '18 at 07:54
  • If you want to extract the TLD out of a list of domains: `cat domains.txt | rev | cut -d. -f2 | rev | sort | uniq -c | sort -rn` – gies0r Apr 30 '20 at 13:27
  • This is by far the best answer if you are processing a whole file using cat + grep, thanks a lot for coming up with it!! – mrArias Nov 30 '21 at 14:39
111

It's difficult to get the last field using cut, but here are some solutions in awk and perl

echo 1:2:3:4:5 | awk -F: '{print $NF}'
echo 1:2:3:4:5 | perl -F: -wane 'print $F[-1]'
William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • 7
    great advantage of this solution over the accepted answer: it also matches paths that contain or do not contain a finishing `/` character: `/a/b/c/d` and `/a/b/c/d/` yield the same result (`d`) when processing `pwd | awk -F/ '{print $NF}'`. The accepted answer results in an empty result in the case of `/a/b/c/d/` – eckes Jan 23 '13 at 15:20
  • @eckes In case of AWK solution, on GNU bash, version 4.3.48(1)-release that's not true, as it matters whenever you have trailing slash or not. Simply put AWK will use `/` as delimiter, and if your path is `/my/path/dir/` it will use value after last delimiter, which is simply an empty string. So it's best to avoid trailing slash if you need to do such a thing like I do. – stamster May 21 '18 at 11:52
  • How would I get the substring UNTIL the last field? – blackjacx Jun 09 '20 at 16:28
  • 1
    @blackjacx There are some quirks, but something like `awk '{$NF=""; print $0}' FS=: OFS=:` often works well enough. – William Pursell Jun 09 '20 at 16:50
39

Assuming fairly simple usage (no escaping of the delimiter, for example), you can use grep:

$ echo "1:2:3:4:5" | grep -oE "[^:]+$"
5

Breakdown - find all the characters not the delimiter ([^:]) at the end of the line ($). -o only prints the matching part.

Nicholas M T Elliott
  • 3,643
  • 2
  • 19
  • 15
  • 1
    -E means using extended syntax; [^...] means anything but the listed char(s); + one or more such hits (will take the maximum possible length for the pattern; this item is a gnu extension) - for the example the separating char(s) are the colon. – Alexander Stohr Oct 17 '19 at 11:36
28

You could try something like this if you want to use cut:

echo "1:2:3:4:5" | cut -d ":" -f5

You can also use grep try like this :

echo " 1:2:3:4:5" | grep -o '[^:]*$'
Abdallah_98
  • 1,331
  • 7
  • 17
  • Your second command was useful to me. Would you break it down so I can understand it better? Thank you. – John Mar 02 '21 at 15:09
20

One way:

var1="1:2:3:4:5"
var2=${var1##*:}

Another, using an array:

var1="1:2:3:4:5"
saveIFS=$IFS
IFS=":"
var2=($var1)
IFS=$saveIFS
var2=${var2[@]: -1}

Yet another with an array:

var1="1:2:3:4:5"
saveIFS=$IFS
IFS=":"
var2=($var1)
IFS=$saveIFS
count=${#var2[@]}
var2=${var2[$count-1]}

Using Bash (version >= 3.2) regular expressions:

var1="1:2:3:4:5"
[[ $var1 =~ :([^:]*)$ ]]
var2=${BASH_REMATCH[1]}
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
17
$ echo "a b c d e" | tr ' ' '\n' | tail -1
e

Simply translate the delimiter into a newline and choose the last entry with tail -1.

Mateusz Piotrowski
  • 8,029
  • 10
  • 53
  • 79
user3133260
  • 189
  • 1
  • 2
  • 3
    It will fail if the last item contains a `\n`, but for most cases is the most readable solution. – Yajo Jul 30 '14 at 10:13
7

Using sed:

$ echo '1:2:3:4:5' | sed 's/.*://' # => 5

$ echo '' | sed 's/.*://' # => (empty)

$ echo ':' | sed 's/.*://' # => (empty)
$ echo ':b' | sed 's/.*://' # => b
$ echo '::c' | sed 's/.*://' # => c

$ echo 'a' | sed 's/.*://' # => a
$ echo 'a:' | sed 's/.*://' # => (empty)
$ echo 'a:b' | sed 's/.*://' # => b
$ echo 'a::c' | sed 's/.*://' # => c
Rafael
  • 1,126
  • 11
  • 16
  • given the output of many utilities is in the form of the original file name followed by colon (:) followed by the utility's output (${path}:${output}), this is incredibly useful for adding your own control character like TAB $'\t' or unit separator $'\037' etc. after that final colon. example for adding a TAB at the final colon of file output: file ~/yourPath/* | sed "s/\(.*:\)\(.*\)/\1"$'\t'"\2/" – spioter Sep 03 '20 at 13:48
4

There are many good answers here, but still I want to share this one using basename :

 basename $(echo "a:b:c:d:e" | tr ':' '/')

However it will fail if there are already some '/' in your string. If slash / is your delimiter then you just have to (and should) use basename.

It's not the best answer but it just shows how you can be creative using bash commands.

021
  • 323
  • 2
  • 17
3

If your last field is a single character, you could do this:

a="1:2:3:4:5"

echo ${a: -1}
echo ${a:(-1)}

Check string manipulation in bash.

codeforester
  • 39,467
  • 16
  • 112
  • 140
Ab Irato
  • 94
  • 2
2

Using Bash.

$ var1="1:2:3:4:0"
$ IFS=":"
$ set -- $var1
$ eval echo  \$${#}
0
Rafa Viotti
  • 9,998
  • 4
  • 42
  • 62
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
2

Regex matching in sed is greedy (always goes to the last occurrence), which you can use to your advantage here:

$ foo=1:2:3:4:5
$ echo ${foo} | sed "s/.*://"
5
slushy
  • 3,277
  • 1
  • 18
  • 24
1
echo "a:b:c:d:e"|xargs -d : -n1|tail -1

First use xargs split it using ":",-n1 means every line only have one part.Then,pring the last part.

Crytis
  • 300
  • 3
  • 10
1

A solution using the read builtin:

IFS=':' read -a fields <<< "1:2:3:4:5"
echo "${fields[4]}"

Or, to make it more generic:

echo "${fields[-1]}" # prints the last item
codeforester
  • 39,467
  • 16
  • 112
  • 140
baz
  • 1,317
  • 15
  • 10
0
for x in `echo $str | tr ";" "\n"`; do echo $x; done
sth
  • 222,467
  • 53
  • 283
  • 367
  • 2
    This runs into problems if there is whitespace in any of the fields. Also, it does not directly address the question of retrieving the *last* field. – chepner Jun 22 '12 at 12:58
0

improving from @mateusz-piotrowski and @user3133260 answer,

echo "a:b:c:d::e:: ::" | tr ':' ' ' |  xargs | tr ' ' '\n' | tail -1

first, tr ':' ' ' -> replace ':' with whitespace

then, trim with xargs

after that, tr ' ' '\n' -> replace remained whitespace to newline

lastly, tail -1 -> get the last string

Yunnosch
  • 26,130
  • 9
  • 42
  • 54
  • 1
    If you want to contribute an answer according to [answer] please [edit] to make that more obvious. You can provide an answer based on an existing answer; giving credit to the exsting answers author is then very appropriate, thanks for doing that. I do however have trouble identifying the answer you are reffering to. Please use the link you get from the "Share" link beneath that answer to add an unambigous link here. – Yunnosch Nov 19 '22 at 06:25
  • 1
    You know about the commenting privilege which you do not have, so well that you can even put it into words. You are aware of the rule https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead . In that situation please do not decide to misuse a different mechanism (an answer) for something it is not meant for and which you are not allowed yet to do. – Yunnosch Nov 19 '22 at 06:25
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 22 '22 at 12:09
  • Thanks for the reminder @Yunnosch, as i am not a native english speaker, i may make mistakes, for all this time using SO just seeking/reading for answers of questions and not answering questions. – user8582041 Feb 13 '23 at 16:15
  • You only describe the code behaviour on a level which (with some experiece) is clear by the code itself. Please explain what it avhieves, i.e. wha the improvement is. – Yunnosch Feb 13 '23 at 16:21
  • sorry, as i am a rather forgetful person. so, the improvement to what i last remember is, to avoid failing if it has empty value between colon (:) or whitespaces. – user8582041 Feb 16 '23 at 14:03
-2

For those that comfortable with Python, https://github.com/Russell91/pythonpy is a nice choice to solve this problem.

$ echo "a:b:c:d:e" | py -x 'x.split(":")[-1]'

From the pythonpy help: -x treat each row of stdin as x.

With that tool, it is easy to write python code that gets applied to the input.

Edit (Dec 2020): Pythonpy is no longer online. Here is an alternative:

$ echo "a:b:c:d:e" | python -c 'import sys; sys.stdout.write(sys.stdin.read().split(":")[-1])'

it contains more boilerplate code (i.e. sys.stdout.read/write) but requires only std libraries from python.