10

I was trying to remove all the lines of a file except the last line but the following command did not work, although file.txt is not empty.

$cat file.txt |tail -1 > file.txt

$cat file.txt

Why is it so?

Mark Biek
  • 146,731
  • 54
  • 156
  • 201
  • 1
    Does this answer your question? [Why doesnt "tail" work to truncate log files?](https://stackoverflow.com/questions/1759448/why-doesnt-tail-work-to-truncate-log-files) – user202729 Oct 19 '21 at 15:34

11 Answers11

19

Redirecting from a file through a pipeline back to the same file is unsafe; if file.txt is overwritten by the shell when setting up the last stage of the pipeline before tail starts reading off the first stage, you end up with empty output.

Do the following instead:

tail -1 file.txt >file.txt.new && mv file.txt.new file.txt

...well, actually, don't do that in production code; particularly if you're in a security-sensitive environment and running as root, the following is more appropriate:

tempfile="$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt

Another approach (avoiding temporary files, unless <<< implicitly creates them on your platform) is the following:

lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"

(The above implementation is bash-specific, but works in cases where echo does not -- such as when the last line contains "--version", for instance).

Finally, one can use sponge from moreutils:

tail -1 file.txt | sponge file.txt
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Note that tail accepts a filename as an argument: "tail -1 file.txt > file.txt.new && mv file.txt.new file.txt" – Marcel Levy Sep 23 '08 at 19:35
  • @Marcel Levy - quite right, and it has the potential to run more efficiently that way; updated. – Charles Duffy Sep 23 '08 at 20:47
  • Dear @CharlesDuffy, I'd like to note that your "secure" variant requires that user doing that will be root. As for how it is secure, I can say that on access to "file.txt" its not enough to apply its permissions and owner. Consider that "file.txt" have permissions 0666 and is placed inside of "~/.ssh" folder which have permissions 0700. In this case my solution will be more secure since it will not expose that file to the world. Should I downvote your answer because of that? ;) – ony May 14 '13 at 13:02
  • @CharlesDuffy, Oh... I'm totally forgot that you may loose data if someone will add one line to that file because you expose it during the whole time of `tail -1 file.txt`. As result you may overwrite existing `file.txt` with old contents. Should I downvote your answer because of that? ;) – ony May 14 '13 at 13:15
  • @ony The use of `chown` is potentially useful here if the file is owned by a secondary group, and does _not_ require privileges in that case. – Charles Duffy May 14 '13 at 13:18
  • @ony As for the race condition -- if it were avoidable, sure, downvote me for it. I'd love to hear just how that avoidance would be done (particularly given as `sed -i`, `ed`, `ex` and kin aren't atomic either). – Charles Duffy May 14 '13 at 13:19
  • @CharlesDuffy, on file `~/.ssh/file.txt` there is severals levels of permissions - first `~` (and all its parent folders), then `.ssh` (which usually have 0700) and then `file.txt`. You copy only permissions for last item so you potentially decrease level of security. In my solution this is avoided by using the same file (without moving/copying/overwriting). So this issue is avoidable - this means that I can downvote? Of course my solution have other issue - size of file can diverge with its content, but this is other issue. In my original answer I avoid issue with permissions as well. – ony May 14 '13 at 13:33
  • @ony That's a compelling issue -- the temporary file should be created in the final location rather than using TEMPDIR. Making that change; thank you for the pointer. – Charles Duffy May 14 '13 at 13:38
  • i think TEMPFILE=$(tempfile) is much easier... of course (foot in mouth) not every distro has this still? why if it's "safer" i wonder.. – osirisgothra Jul 16 '14 at 15:12
  • @osirisgothra, indeed, `tempfile` is distro-specific. Not available on OS X, not available on the very latest Arch Linux, &c. Beyond that, it's helpful for a user to be able to know software what a given temporary file or directory is associated with, making the template parameter a valuable feature. – Charles Duffy Jul 16 '14 at 15:57
5

You can use sed to delete all lines but the last from a file:

sed -i '$!d' file
  • -i tells sed to replace the file in place; otherwise, the result would write to STDOUT.
  • $ is the address that matches the last line of the file.
  • d is the delete command. In this case, it is negated by !, so all lines not matching the address will be deleted.
Chris
  • 425
  • 3
  • 10
  • The only unfortunate thing about this is that `sed -i` isn't POSIX-compliant but is a GNU extension. Using `ed` or `ex` to edit in-place would avoid those caveats. – Charles Duffy May 14 '13 at 13:16
3

Before 'cat' gets executed, Bash has already opened 'file.txt' for writing, clearing out its contents.

In general, don't write to files you're reading from in the same statement. This can be worked around by writing to a different file, as above:

$cat file.txt | tail -1 >anotherfile.txt
$mv anotherfile.txt file.txt
or by using a utility like sponge from moreutils:
$cat file.txt | tail -1 | sponge file.txt
This works because sponge waits until its input stream has ended before opening its output file.
ephemient
  • 198,619
  • 38
  • 280
  • 391
  • Does `cat` give you any value over `tail -1 file.txt`? It certainly makes things **far** less efficient if your `file.txt` is large -- if `tail` has a direct seekable file descriptor, it can jump straight to the last kb of the file, read only that, and try to find the last line, backing up if the last line is larger than 1kb; if all it has is a pipe from `cat`, it has to read the file from the beginning -- no matter how large it is -- to get to the end. – Charles Duffy Jul 21 '16 at 14:42
2

When you submit your command string to bash, it does the following:

  1. Creates an I/O pipe.
  2. Starts "/usr/bin/tail -1", reading from the pipe, and writing to file.txt.
  3. Starts "/usr/bin/cat file.txt", writing to the pipe.

By the time 'cat' starts reading, 'file.txt' has already been truncated by 'tail'.

That's all part of the design of Unix and the shell environment, and goes back all the way to the original Bourne shell. 'Tis a feature, not a bug.

Craig Trader
  • 15,507
  • 6
  • 37
  • 55
  • Is the ordering between (2) and (3) specified? My impression is that they're happening concurrently, making which occurs effectively a race -- though `file.txt` is opened for write before `/usr/bin/tail` is exec'd, making the truncation highly likely to win the race (since `/usr/bin/cat` requires an exec, with all the linker/loader performance impact that implies). – Charles Duffy Jul 21 '16 at 14:46
2

tmp=$(tail -1 file.txt); echo $tmp > file.txt;

Ken
  • 77,016
  • 30
  • 84
  • 101
2

This works nicely in a Linux shell:

replace_with_filter() {
  local filename="$1"; shift
  local dd_output byte_count filter_status dd_status
  dd_output=$("$@" <"$filename" | dd conv=notrunc of="$filename" 2>&1; echo "${PIPESTATUS[@]}")
  { read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output"
  (( filter_status > 0 )) && return "$filter_status"
  (( dd_status > 0 )) && return "$dd_status"
  dd bs=1 seek="$byte_count" if=/dev/null of="$filename"
}

replace_with_filter file.txt tail -1

dd's "notrunc" option is used to write the filtered contents back, in place, while dd is needed again (with a byte count) to actually truncate the file. If the new file size is greater or equal to the old file size, the second dd invocation is not necessary.

The advantages of this over a file copy method are: 1) no additional disk space necessary, 2) faster performance on large files, and 3) pure shell (other than dd).

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
m104
  • 1,118
  • 7
  • 6
  • I like that -- it's an innovative idea. I'm only not upvoting it at this time because the use of `$FILTER` runs afoul of http://mywiki.wooledge.org/BashFAQ/050 (relying on string-splitting to correctly form a command is extremely error-prone). – Charles Duffy May 13 '13 at 14:14
  • I hope you don't mind my heavy-handed edits; this solution should now be considerably more robust. – Charles Duffy May 14 '13 at 13:14
  • I would not call it `replace_with_filter`. This will work only for filters that with each next chunk produce data no more than original chunk. Otherwise information read by filter might be overwritten by its output. Note that instead of last `dd` you can use `truncate -s $byte_count "$filename"`. – ony May 14 '13 at 16:00
  • @ony `truncate` is not a POSIX-standard tool -- it's found in modern coreutils, but there are plenty of platforms where bash are available but it isn't. – Charles Duffy May 14 '13 at 17:23
1

As Lewis Baumstark says, it doesn't like it that you're writing to the same filename.

This is because the shell opens up "file.txt" and truncates it to do the redirection before "cat file.txt" is run. So, you have to

tail -1 file.txt > file2.txt; mv file2.txt file.txt
wnoise
  • 9,764
  • 37
  • 47
1

Just for this case it's possible to use

cat < file.txt | (rm file.txt; tail -1 > file.txt)
That will open "file.txt" just before connection "cat" with subshell in "(...)". "rm file.txt" will remove reference from disk before subshell will open it for write for "tail", but contents will be still available through opened descriptor which is passed to "cat" until it will close stdin. So you'd better be sure that this command will finish or contents of "file.txt" will be lost
ony
  • 12,457
  • 1
  • 33
  • 41
  • Anonymous downvoters should be punished. This answer works and have logic in it (i.e. remove file after opening it for reading to keep access to contents). – ony Apr 22 '13 at 09:54
  • I have trouble disagreeing with the (presumptive) proposition that a solution in which failure cases lead to data loss is to be avoided, even when that caveat is clearly labeled; an unnecessary subshell is icing on the cake. And "punished"? Really? – Charles Duffy May 13 '13 at 14:12
  • @CharlesDuffy, "shoud be punished" is for anonymous. Because having downvote without any reasoning is irritating. You don't think so?.. As for correctness, are you sure that high security and reliability - is required for correct answer? Sometimes you may sacrifice something for archiving something else. Thus from different views you can treat solutions differently. If you project my answer to your view you'll find that precondition for this answer can't be met so that's wrong logic to say that it is wrong. – ony May 14 '13 at 12:57
  • ...by the way, to be clear, I wasn't the anonymous downvote; I merely consider their actions defensible. – Charles Duffy May 14 '13 at 13:21
  • @CharlesDuffy, ok, that's your assumption about downvoter. But actual reason might be different. And that's the problem. – ony May 14 '13 at 15:34
  • To misquote a bit: "If builders built houses the way developers build software, the first woodpecker to come along would destroy civilization". Suggesting that people use unnecessarily fragile approaches as a matter of course (absent a specific reason to do otherwise) leads to software and systems riddled with unexpected and undocumented failure modes and thereby perpetuates the problem. – Charles Duffy May 14 '13 at 15:51
  • @CharlesDuffy, The fact of replacing file contents with its last line is already a wrong path. This should be either two separate files (if someone needs whole contents) or it should be just one line in that file (i.e. one line produced rather than whole contents) so there will be no need to do `tail -1` on it. And I'm pretty sure the variety of variants here comes from the fact that this looks more like a brain teaser. – ony May 14 '13 at 17:00
  • Inline edits are a thing that people do. `tail -1` may not be a particularly good use case (indeed, it certainly isn't), but anybody with a career in system administration is going to be using practices similar to those found here in real, production environments and scripts with nontrivial frequency. As such, it behooves us -- "brainteaser" or no -- to encourage the adoption of good practices and discourage the adoption of bad ones. – Charles Duffy May 14 '13 at 17:28
1
echo "$(tail -1 file.txt)" > file.txt
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • What happens if the last line contains only `-n`? What if it contains backslash literals and your `echo` is compliant with the XSI POSIX extensions (which call for any escapes those literals may form to be honored even without anything akin to `-e`)? It would be safer to use `printf '%s\n' "$(tail -1 file.txt)"` rather than relying on echo – Charles Duffy Jul 21 '16 at 14:47
0

It seems to not like the fact you're writing it back to the same filename. If you do the following it works:

$cat file.txt | tail -1 > anotherfile.txt
  • There's no need for "seems to" speculation: There's no guarantee of ordering between pipeline components' startup, and redirections (thus, the open of `anotherfile.txt` in truncating mode) happen *before* execution (in this case, of `tail`; whether it happens before execution of `cat` is undefined, but since a program such as `cat` requires time to start up, it's very much likely that the truncation of `anotherfile.txt` will have already happened before `cat` is finished loading and ready to open its argument for input). – Charles Duffy Mar 10 '16 at 19:13
0

tail -1 > file.txt will overwrite your file, causing cat to read an empty file because the re-write will happen before any of the commands in your pipeline are executed.

dsm
  • 10,263
  • 1
  • 38
  • 72