In Unix, using a simple command like sed, is there a way to print the last character of a file?

migrated from stackoverflow.com Mar 3 '11 at 14:16

This question came from our site for professional and enthusiast programmers.

  • Do you care if the last character is actually "printable"? i.e. if the last character is a new line, is that ok? – DaveParillo Mar 3 '11 at 16:09

tail is the right tool, not sed.

tail -c 1 filename
  • 3
    +1 - For a text file, if you want the last character before the last newline, just replace 1 by 2 – mouviciel Mar 3 '11 at 13:09

tail -c 2 file should do it. Should be -c 1 in theory but practice proved me wrong.

Edit: If your file has a an end of line character that you want to ignore, it's 2. 1 otherwise.

  • Maybe your test file has a newline at the end? – Delan Azabani Mar 3 '11 at 13:09
  • It indeed had one. – J.N. Mar 3 '11 at 13:10
  • I've never seen a file with an end of life character. – Paul R Mar 3 '11 at 13:49
  • 1
    @PaulR Perhaps it's just a file so long when you arrive at the end you're dead :D – рüффп Jan 19 '17 at 23:20

Try this cat filename | tail -c -1

  • 4
    Heh, first time I've ever seen "Your answer must be at least 30 characters". – irritate Mar 3 '11 at 13:06
  • 3
    misuse of pipes, same pitfall as in cat foo | grep bar – vtest Mar 3 '11 at 15:27
  • @vtest: What are those pitfalls? – Eclipse Mar 3 '11 at 16:35
  • @vtest, maybe "Try this tail -c -1 filename" was too short answer (less than 30 chars), so additional cat | was added to prolong it. ;) – ulidtko Mar 3 '11 at 19:35
  • @ulidtko: I commented on the recipe, not on the answer length – vtest Mar 3 '11 at 23:18

This should work, provided that last line is not empty:

sed -n '$s/.*\(.\)$/\1/p' file

Use: cat test | sed -e "s/^.*\(.\)$/\1/"

where test is the name of your file.


For the benefit of the reader:

The problem

There is some misconception here. While tail -c1 file indeed is the correct command to print the last character of a file, this cannot be used easily to detect if a file ends on NL from shell level, due to how POSIX-shells process command outputs:

if last_character="$(tail -c1 "$file")"; then ..
# WRONG! last_character will be empty on NL, as well as on an empty file

For some unknown reason POSIX defines, that all trailing NLs are stripped if you catch some command's output. Note that nearly all shell scripts out there fall into this trap on even the most simple things like

cd "$(dirname -- "$0")" || exit           # Wrong, see: mkdir $'\n'
base="$(basename -- "$0" .sh)" || exit    # Wrong, see: mv script.sh $'\n.sh'

Attackers might even exploit this POSIX standard using softlinks!

How to solve this .. NOT

Please note that I use bash version 4 syntax here.

You can do things like this:

set -o pipefail
if tail -c1 file | { IFS= read -rd '' var; postprocess "$var"; }; then ..

But this is a PITA due to the subshell. Or

6< <(tail -c1 file) IFS= read -ru6 -d '' var

which is even likewise weird. (How to properly detect failure of tail here, etc.)

How to solve this the right way

But following recipe usually works:

var="$(tail -c1 file && echo x)" && var="${var%x}" || error ..

For commands which always add an extra NL (nearly all commands do this, like basename or dirname), you need to adapt this to:

var="$(basename -- "$file" && echo x)" && var="${var%$'\nx'}" || error ..
#----------------------------------------------------^^^^^^ change

Note that this recipe also works in other normal circumstances:

# This is only correct here because we only expect a single filename
echo wrong > $'file'
echo right > $'file\n'
wrongname="$(fgrep -lx right file*)" || notfound
correctname="$(fgrep -lx right file* && echo x)" && correctname="${correctname%$'\nx'}" || notfound

wrongname will contain file while correctname will contain file\n (where \n stands for NL). As both files exist and contain data, you probably won't spot the error quickly, that you used the wrongname variant with the file with the wrong content.

Yes, a bit finicking, but nevertheless sometimes needed.

Your Answer

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