29

I know that the cut command can print the first n characters of a string but how to select the last n characters?

If I have a string with a variable number of characters, how can I print only the last three characters of the string. eg.

"unlimited" output needed is "ted"
"987654" output needed is "654"
"123456789" output needed is "789"

10 Answers 10

50

Why has nobody given the obvious answer?

sed 's/.*\(...\)/\1/'

… or the slightly less obvious

grep -o '...$'

Admittedly, the second one has the drawback that lines with fewer than three characters vanish; but the question didn’t explicitly define the behavior for this case.

  • 5
    or grep -o '.\{3\}$' – Avinash Raj Oct 22 '14 at 0:36
  • 3
    or echo "unlimited" | python -c "print raw_input()[-3:]" – Kiro Oct 22 '14 at 7:29
  • 7
    @Kiro or "echo unlimited" | java -jar EnterpriseWordTrimmer.jar, but I don't think it's really necessary to bring in a heavier language for character manipulation. – wchargin Oct 22 '14 at 20:53
  • 11
    @WChargin you forgot java -server -Xms300M -Xmx3G -XX:+UseParallelGC -cp /path/to/all/the/jars/ -Dinput.interactive=false -Dinput.pipe=true -Dconfig.file=/path/to/config/last-three-letters.cfg -jar ... – h.j.k. Oct 23 '14 at 3:56
  • 3
    grep -o -P '.{0,3}$' will print last 3 characters even if the line has fewer than 3 characters. -P avoids having to escape the braces. – Raghu Dodda Dec 11 '16 at 19:41
41

Keeping it simple - tail

We should not need a regular expression, or more than one process, just to count characters.
The command tail, often used to show the last lines of a file, has an option -c (--bytes), which seems to be just the right tool for this:

$ printf 123456789 | tail -c 3
789

(When you are in a shell, it makes sense to use a method like in the answer of mikeserv, because it saves starting the process for tail.)

Real Unicode characters?

Now, you ask for the last three characters; That's not what this answer gives you: it outputs the last three bytes!

As long as each character is one byte, tail -c just works. So it can be used if the character set is ASCII, ISO 8859-1 or a variant.

If you have Unicode input, like in the common UTF-8 format, the result is wrong:

$ printf 123αβγ | tail -c 3
�γ

In this example, using UTF-8, the greek characters alpha, beta and gamma are two bytes long:

$ printf 123αβγ | wc -c  
9

The option -m can at least count the real unicode characters:

printf 123αβγ | wc -m
6

Ok, so the last 6 bytes will give us the last 3 characters:

$ printf 123αβγ | tail -c 6
αβγ

So, tail does not support handling general characters, and it does not even try (see below): It handles variable size lines, but no variable size characters.

Let's put it this way: tail is just right for the structure of the problem to solve, but wrong for the kind of data.

GNU coreutils

Looking further, it turns out that thee GNU coreutils, the collection of basic tools like sed, ls, tail and cut, is not yet fully internationalized. Which is mainly about supporting Unicode.
For example, cut would be a good candidate to use instead of tail here for character support; It does have options for working on bytes or chars, -c (--bytes) and -m (--chars);

Only that -m/--chars is, as of version
cut (GNU coreutils) 8.21, 2013,
not implemented!

From info cut:

`-c CHARACTER-LIST'
`--characters=CHARACTER-LIST'
     Select for printing only the characters in positions listed in CHARACTER-LIST.  
     The same as `-b' for now, but internationalization will change that.


See also this answer to Can not use `cut -c` (`--characters`) with UTF-8?.

  • 2
    Actually, most of the other answers seem to handle Unicode just fine, as long as the current locale specifies UTF-8 encoding. Only yours and glenn jackman's cut-based solution don't seem to. – Ilmari Karonen Oct 22 '14 at 6:32
  • @IlmariKaronen True, thanks for the hint. I have edited, with some additional detail. – Volker Siegel Oct 23 '14 at 10:12
  • 1
    Note that POSIX explicitly specifies that tail should deal with bytes, and not characters. I once made a patch to add a new option to also select characters, but I believe that never got merged :-/ – Martin Tournoij Jul 10 '15 at 10:59
  • Doesn't work in file-mode, like tail -c3 -n10 /var/log/syslog – Suncatcher May 13 '18 at 8:39
  • @Suncatcher I tried, and it worked. What is the problem you see? Your command tail -c3 -n10 /var/log/syslog asks for the last 10 lines, and that works for me. You use the option -c3, and after that the conflicting option -n10. The later option takes priority. – Volker Siegel May 13 '18 at 13:11
35

If your text is in an environment variable called STRING, you can do this in a bash shell:

echo "${STRING:(-3)}"
  • 1
    What is that kind of syntax/operation called so I can search for more informarion? – Tulains Córdova Apr 15 '15 at 12:14
  • 5
    It's called Substring Expansion.  It's a kind of Parameter Expansion.  The general form is ${parameter:offset:length}, but the length field is optional  (and, as you can see, it has been omitted in the answer above).  DopeGhoti could also have written ${STRING:(-3):3} (specifying the length field), ${STRING: -3} (with a space between the : and the -), or ${STRING: -3:3}. – G-Man Apr 15 '15 at 19:17
  • In this case, specifying the length of 3 is somewhat moot as that's asking for "the three characters from the third from the last character, inclusive" which happens to be an identical operation in practical terms to "All characters onward from the third from the last, inclusive". – DopeGhoti May 3 '18 at 22:24
12

Using awk:

awk '{ print substr( $0, length($0) - 2, length($0) ) }' file
ted
654
789
10

If the string is in a variable you can do:

printf %s\\n "${var#"${var%???}"}"

That strips the last three characters from the value of $var like:

${var%???}

...and then strips from the head of $var everything but what was just stripped like:

${var#"${var%???}"}

This method has its upsides and downsides. On the bright side it is fully POSIX-portable and should work in any modern shell. Also, if $var does not contain at least three characters nothing but the trailing \newline is printed. Then again, if you want it printed in that case, you need an additional step like:

last3=${var#"${var%???}"}
printf %s\\n "${last3:-$var}"

In that way $last3 is only ever empty if $var contains 3 or fewer bytes. And $var is only ever substituted for $last3 if $last3 is empty or unset - and we know it is not unset because we just set it.

  • That's pretty tidy +1. Aside: any reason you don't quote your printf format strings? – jasonwryan Oct 21 '14 at 23:30
  • Why not just use ${VARNAME:(-3)} (presuming bash)? – DopeGhoti Oct 21 '14 at 23:34
  • @jasonwryan - only because %s\\n is shorter to write than is '%s\n'. I do of course quote them when they contain expansions or characters the shell might misinterpret - like when I use math expansions for string truncation - but simple ones like this I just quote what is necessary - which is to say, I quote the backslash. – mikeserv Oct 21 '14 at 23:36
  • 1
    @DopeGhoti - simply because that is an assumption I almost never make. This works as well in bash as in any other shell claiming POSIX comapibility. – mikeserv Oct 21 '14 at 23:39
  • 2
    @odyssey - The problem is csh is not among the modern, POSIX-compatible shells I mention here, unfortunately. The POSIX-shell spec is modeled after ksh, which modeled itself after a combination of both csh and the traditional Bourne-style shells. ksh incorporated both csh's excellent job-control functionality and the old Bourne-styles' i/o redirection. It also added some things - such as the string manipulation concepts I demonstrate above. This will not likely work in any traditional csh as far as I know, I am sorry to say. – mikeserv Oct 22 '14 at 1:07
7

You can do this, but this is a little ... excessive:

for s in unlimited 987654 123456789; do
    rev <<< $s | cut -c 1-3 | rev
done 
ted
654
789
3

The bulletproof solution for utf-8 strings:

utf8_str=$'\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82' # привет

last_three_chars=$(perl -CAO -e 'print substr($ARGV[0], -3)' "$utf8_str")

Or use:

last_three_chars=$(perl -MEncode -CO -e '
  print substr(decode("UTF-8", $ARGV[0], Encode::FB_CROAK), -3)
' "$utf8_str")

to prevent the malformed data handling.

Example:

perl -MEncode -CO -e '
  print substr(decode("UTF-8", $ARGV[0], Encode::FB_CROAK), -3)
' $'\xd0\xd2\xc9\xd7\xc5\xd4' # koi8-r привет

Outputs something like this:

utf8 "\xD0" does not map to Unicode at /usr/lib/x86_64-linux-gnu/perl/5.20/Encode.pm line 175.

Doesn't depend on locale settings (i.e. works with LC_ALL=C). Bash, sed, grep, awk, rev require something like this: LC_ALL=en_US.UTF-8

Common solution:

  • Receive bytes
  • Detect encoding
  • Decode bytes to characters
  • Extract charaсters
  • Encode character to bytes

You can detect encoding with uchardet. See also related projects.

You can decode/encode with Encode in Perl, codecs in Python 2.7

Example:

Extract last three characters from utf-16le string and convert these characters to utf-8

utf16_le_str=$'\xff\xfe\x3f\x04\x40\x04\x38\x04\x32\x04\x35\x04\x42\x04' # привет

chardet <<<"$utf16_le_str"  # outputs <stdin>: UTF-16LE with confidence 1.0

last_three_utf8_chars=$(perl -MEncode -e '
    my $chars = decode("utf-16le", $ARGV[0]);
    my $last_three_chars = substr($chars, -3);
    my $bytes = encode("utf-8", $last_three_chars);
    print $bytes;
  ' "$utf16_le_str"
)

See also: perlunitut, Python 2 Unicode HOWTO

  • echo is your bulletproof source? – mikeserv Jul 10 '15 at 2:17
  • @mikeserv, decode/encode is my bulletproof source. Cleaned up my answer. – Evgeny Vereshchagin Jul 10 '15 at 9:44
  • This also depends on locale settings to guarantee that it works correctly, since a set of bytes may reflect different characters in different charsets. It "works" for LC_ALL=C because that's a very "dumb" setting, but it may break when you try to pass an UTF-8 string to SHIFT-5, or a SHIFT-5 string to KOI8, etc. – Martin Tournoij Jul 10 '15 at 11:02
  • @Carpetsmoker, thanks. Could you explain your comment? I suppose that perl -CAO -e 'print substr($ARGV[0], -3)' works fine. A the @ARGV elements are expected to be strings encoded in UTF-8, O STDOUT will be in UTF-8. – Evgeny Vereshchagin Jul 10 '15 at 11:11
  • looks like you tell about assignment to utf8_str – Evgeny Vereshchagin Jul 10 '15 at 11:18
1

What about using "expr" or "rev" ?

An answer similar the one provided by @G-Man : expr "$yourstring" : '.*\(...\)$' It has the same drawback than the grep solution.

A well known trick is to combine "cut" with "rev" : echo "$yourstring" | rev | cut -n 1-3 | rev

0

Get size of the string with:

size=${#STRING}

Then get substring of last n character:

echo ${STRING:size-n:size}

For example:

STRING=123456789
n=3
size=${#STRING}
echo ${STRING:size-n:size}

would give:

789
0

printf will not work if string has spaces in it.

Below code for string with space

str="Welcome to Linux"
echo -n $str | tail -c 3

nux

  • Um, if printf doesn't work, then you are doing something very wrong. – Kusalananda Mar 10 '18 at 10:42
  • @Kusalananda: Based on the command that Saurabh shows, they tried printf $str (rather than printf "$str" or printf '%s' "$str").  And, yes, printf $str is very wrong. (echo -n $str isn’t much better.) – G-Man May 24 at 21:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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