38

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"
0

12 Answers 12

63

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.

13
  • 11
    or grep -o '.\{3\}$' Oct 22, 2014 at 0:36
  • 3
    or echo "unlimited" | python -c "print raw_input()[-3:]"
    – Kiro
    Oct 22, 2014 at 7:29
  • 10
    @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, 2014 at 20:53
  • 13
    @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, 2014 at 3:56
  • 11
    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. Dec 11, 2016 at 19:41
56

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?.

11
  • 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. Oct 22, 2014 at 6:32
  • @IlmariKaronen True, thanks for the hint. I have edited, with some additional detail. Oct 23, 2014 at 10:12
  • 3
    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 :-/ Jul 10, 2015 at 10:59
  • Doesn't work in file-mode, like tail -c3 -n10 /var/log/syslog
    – Suncatcher
    May 13, 2018 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. May 13, 2018 at 13:11
36

If your text is in a shell variable called STRING, you can do this in a bash, zsh, mksh or busybox ash shell:

printf '%s\n' "${STRING:(-3)}"

Or

printf '%s\n' "${STRING: -3}"

which also has the benefit to work with ksh93 where that syntax comes from.

The point is that the : has to be separated from the -, otherwise it becomes the ${var:-default} operator of the Bourne shell.

The equivalent syntax in the zsh or yash shells is:

printf '%s\n' "${STRING[-3,-1]}"
11
  • 2
    What is that kind of syntax/operation called so I can search for more informarion? Apr 15, 2015 at 12:14
  • 7
    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}. Apr 15, 2015 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, 2018 at 22:24
  • Not for multiline var input.
    – done
    Mar 27 at 22:32
  • 1
    @IsaaC: Huh? This answer outputs the last three characters in the string (which is what the question asks for) plus a newline (which is always implied).  The question doesn’t say anything about multi-line strings; this answer handles that unspecified case in a reasonable way. Mar 28 at 21:13
16

Using awk:

awk '{ print substr( $0, length() - 2) }' file
ted
654
789
1
  • Works for (some) utf-8 and for multiline input (+1). Warning: If a glyph (character) comes from a decomposed character then this fail. Make a=$(printf '%b' a \\U301 e \\U301 i \\U301 $'\n') (in bash, but could be easily translated to any shell). And try <<<"$a" awk '{ print "substr( $0," length() "- 2)",$0, substr( $0, length() - 2) }'
    – done
    Mar 27 at 22:31
13

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.

9
  • That's pretty tidy +1. Aside: any reason you don't quote your printf format strings?
    – jasonwryan
    Oct 21, 2014 at 23:30
  • Why not just use ${VARNAME:(-3)} (presuming bash)?
    – DopeGhoti
    Oct 21, 2014 at 23:34
  • 1
    Thanks for clarifying; makes sense, even if it looks (to me) a little odd...
    – jasonwryan
    Oct 21, 2014 at 23:38
  • 2
    @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, 2014 at 23:39
  • 3
    @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, 2014 at 1:07
9

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

words=( unlimited 987654 123456789 )

for s in "${words[@]}"; do
    rev <<< "$s" | cut -c 1-3 | rev
done 
ted
654
789
0
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")

(the -- is critical here, or you'd introduce an arbitary command injection vulnerability).

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

7
  • echo is your bulletproof source?
    – mikeserv
    Jul 10, 2015 at 2:17
  • @mikeserv, decode/encode is my bulletproof source. Cleaned up my answer. Jul 10, 2015 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. Jul 10, 2015 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. Jul 10, 2015 at 11:11
  • 1
    You generally can't pass UTF-16 encoded strings as arguments to commands as the encoding of many characters (including all the ones in ASCII and latin-1) contain NUL bytes. Mar 28 at 8:17
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

11
  • 1
    The rev solution looks a lot like glenn jackman's
    – Jeff Schaller
    Nov 13, 2015 at 15:45
  • You're right @Jeff_Schaller : I missed glenn's one :-(
    – gildux
    Nov 13, 2015 at 17:27
  • The expr one doesn't work for values of $yourstring that start with - or are expr operators (or contain sequences of bytes that don't form valid characters). The echo one doesn't work for values of $yourstring that are valid combinations of echo options or with some echo implementations (including the ones that are UNIX compliant) that contain backslash characters. Mar 28 at 7:38
  • @StéphaneChazelas: (1) In expr version 8.23, I can’t get it to fail for values of $yourstring that start with -. I tried -, -a, --a, -ab, -abc, -1, --1, -12, -123, --help and --version. -- did produce an error, but (2) if it’s an expr operator, it’s fewer than three characters, right? So the correct stdout is null. (OK, yes, it’s unfortunate that it gives an error message.) The expr answer is wrong for the general question, “last n characters” (it would fail for n = 1 or 2) but it seems to work for 3 and above.  … (Cont’d) Mar 28 at 22:17
  • (Cont’d) … (3) And expr "X$yourstring" : 'X.*\(...\)$' would fix all (most?) of the problems, right?  (4) The question says “last n characters”, so it seems unfair to say that the answer would fail if the input isn’t characters.  (5) Also, I feel a little like you’re nit-picking on the echo. One could argue that the second answer is “provide your string as input to rev | cut -n 1-3 | rev”, and that gildux just made the tactical error of illustrating a bad way to pipe a string into a command. Mar 28 at 22:17
1

Using Raku (formerly known as Perl_6)

The three main solutions employed here use either Raku's substr substring routine, or Raku's m/…/ match operator, or Raku's s/…/…/ substitution operator. Since Raku handles Unicode by default, many of the caveats listed within other answers to this question are obviated.

First three code examples return the last 3 characters per line (and a blank line if there are less than 3 characters available):

raku -ne 'put .substr( *-3 ) // "";' 

#OR

raku -ne 'put m{ .**3 $ } // "";'

#OR

raku -ne 'm{  .**3 $ } ?? $/.put !! "".put;' 
  1. Example 1 uses Raku's substr (substring) routine in conjunction with the // "defined-OR" operator. The substr routine takes a $from-to parameter, and *-3 indicates the third character from the end. Note, substr actually takes a Range here, so .substr(*-3,*) also works, with * representing the last (rightmost) character. This answer could also be written in sub form à la Perl5 as: raku -ne 'put substr($_, *-3) // "";'.

  2. The second answer uses Raku's m{ … } operator with user-defined delimiters (curly braces). The way to say three consecutive . characters of any kind is .**3. This is followed by the $ end-of-string zero-width assertion ($$ end-of-line zero-width assertion is also available in Raku). To handle the case where three characters are not available (and the match is therefore False/undefined), Raku's // "defined-OR" operator is employed to return an empty string.

  3. Example 3 uses the m{ … } match operator in conjunction with Raku's <Test> ?? <True> !! <False> ternary operator, to handle cases similar to the first example. The True condition returns $/.put wherein $/ represents Raku's match variable. Currently, $<> can be used interchangeably with $/, for those who have an aversion to slash/backslash characters.


Below, returning last 1 to 3 characters per line, depending on character availability (will return a blank line if given a blank line):

raku -ne 'put m[ .**{0..3} $ ];' 

#OR

raku -pe 's/ .*? (.**3) $ /$0/;'     

#OR

raku -pe 's/ <(.*?)> .**3 $ //;'

Answers 4-through-6 (above) return 0 up to 3 characters, depending on character availability:

  1. Again in example 4, the m[ … ] operator is employed with user defined delimiters (square braces). Here the match asks for .**{0..3} zero-to-three consecutive characters of any kind.
  2. In example 5, the s/…/…/ substitution operator is employed. The first atom .*? non-greedily ("frugally") matches any character zero-or-more times (the trailing ? indicates frugal and changes the default greedy to non-greedy). The second atom matches and captures (.**3) any character zero-or-more times (the parentheses direct capturing). In the replacement half, the $0 capture is returned. Because the first atom is non-greedy, the $0 capture varies in length from 0 up to 3 characters.
  3. Finally, in example 6 the s/…/…/ substitution operator is employed using a method that is somewhat opposite that of example 4. The same atoms as example 4 are employed, however this time the first atom <(.*?)> is captured. In fact, the <( … )> "pointy-curly" capture markers instruct Raku to drop anything outside of these markers. In the replacement half, this capture is deleted, leaving the .**3 $ second atom's characters.

Sample Input (first line is blank):

0
10
210
3210
43210
543210

Sample Output (code examples 1 through 3, three blank lines at top returned):

210
210
210
210

Sample Output (code examples 4 through 6, one blank line returned at top):

0
10
210
210
210
210

https://docs.raku.org/routine/substr
https://docs.raku.org/routine/substr-rw
https://raku.org

0
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
-1

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

2
  • 1
    Um, if printf doesn't work, then you are doing something very wrong.
    – Kusalananda
    Mar 10, 2018 at 10:42
  • 2
    @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.) May 24, 2019 at 21:14
-1

tail -n 1 revisions.log | awk '{ print substr( $0, 0, length($0)-(length($0)-13 )) }'

If you want to print first thirteen characters from the begining

2
  • This is the same as jasonwryan’s answer,  except adapted for a different question. Apr 2, 2020 at 8:29
  • 1
    length($0)-(length($0)-13) is a very convoluted way to write 13. That prints the first 13 characters of the last line of revisions.log which has nothing to do with what is being asked here. Mar 28 at 7:29

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.