24

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

slhck
  • 235,242

7 Answers7

45

tail is the right tool, not sed.

tail -c 1 filename
Erik
  • 626
  • 4
  • 5
8

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.

einpoklum
  • 10,666
J.N.
  • 371
4

Try this cat filename | tail -c -1

2

by 'last char' - do you mean last letter or last byte?

Usually a text file is ended with LF (or CRLF in Win), so tail -c1 will return that ending cbyte of the file.

to get last 'letter-char' one can filter special chars first, and then get last char:

tail -1 filename| tr -d "\r\n" | tail -c1
tail -c9 filename| tr -d "\r\n" | tail -c1

where

  • tail -1 - at 1st get last line, actually getting few chars also will do tail -c9 fo this task
  • tr -d "\r\n" - strip EOL special chars if there are,
  • tail -c1 - finally get last byte, which is last letter-char.

P.S. also, if you need to test whether a file is ended with LF, it is easier to do after converting last byte to hex code:

tail -c1 file.csv | xxd -p
0a
Fedor
  • 21
  • 3
0

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.

Tino
  • 1,246
0

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

sed -n '$s/.*\(.\)$/\1/p' file
mouviciel
  • 3,088
0

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

where test is the name of your file.

soandos
  • 24,600
  • 29
  • 105
  • 136
David
  • 101