In Unix, using a simple command like sed, is there a way to print the last character of a file?
7 Answers
Try this cat filename | tail -c -1
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 dotail -c9fo this tasktr -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
- 21
- 3
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
bashversion 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.
- 1,246
This should work, provided that last line is not empty:
sed -n '$s/.*\(.\)$/\1/p' file
- 3,088