1

This:

echo 'some text' > file.txt
cat file.txt > file.txt

Obviously gives me an empty file.

But to my surprise also:

cat2 () {
    cat "$@" > tmp.txt
    cat tmp.txt
    return $?
}

echo 'some text' > file.txt cat2 file.txt > file.txt

it gives me an empty file, even though the file tmp.txt has been cated, different from file.txt.

This method doesn't work either!:

echo 'some text' > file.txt
cat file.txt | cat2 > file.txt

Why is the file emptied anyway?

Daniel Walker
  • 801
  • 1
  • 9
  • 35
Mario Palumbo
  • 233
  • 1
  • 14

2 Answers2

6

Your command

cat2 file.txt > file.txt

will create a new empty file.txt before it calls the cat2 function.

So by the time cat2 is run the file is empty and so cating it will result in empty output.

If it helps break down the order of operation, what you've done is

  • create a file file.txt with content
  • create a new blank file file.txt
  • run cat2 with output to file.txt

This is because the redirect operator occurs before the cat2 execution.

  • "will create a new empty file.txt" – New, only if the file does not exist yet. If file.txt exists (and this is the case here) then it will be truncated to zero size. The inode number will stay. Open file descriptions (if any) associated with the "old" file will see the "new" file because it's the same file. – Kamil Maciorowski Apr 27 '23 at 04:18
1

This construct does work (it uses tr as a sample operation):

{ 'rm' -f File.txt && tr '[:lower:]' '[:upper:]' > File.txt; } < File.txt

The input redirection < File.txt assigns the file as stdin to the statements in braces first, before executing anything within the block.

Removing the file inside the block does not remove the contents, because the file has an open file descriptor. Note the rm is quoted to avoid the case where it might be aliased, e.g. with the -i option.

The output redirection creates a new output file (inode) with the same name as the original. When the tr terminates, the new file is closed.

Finally, the input redirection completes, it now has no connections via directories or file descriptors, and its resources are freed up.

Edit based on Stéphane Chazelas' excellent comments:

(a) The rm should have the -f option, to avoid all prompts and errors.

(b) The tr should use the Locale-independent character classes.

(c) The new File.txt is independent from the old one, with different inode number, birth time and possible ownership, permissions, acl or other extended attributes. It also replaces a symlink with a plain file and would detach a hard link, leaving the original data under a different path name.

Paul_Pedant
  • 8,679
  • 1
    rm may still prompt the user without -i like when asked to remove a non-writable file. 'rm' will bypass aliases (unless there's also an alias for 'rm' of course), but not functions (like a rm() { if ...; then rm -i "$@"; ...; }). Better to use { rm -f File.txt && tr '[:lower:]' '[:upper:]'; } < File.txt here – Stéphane Chazelas Apr 27 '23 at 10:11
  • 1
    Would be worth noting that you get a brand new File.txt independent from the old one, with different inode number, birth time and possible ownership, permissions, acl or other extended attributes. It also replaces a symlink with a plain file and would detach a hard link – Stéphane Chazelas Apr 27 '23 at 10:13
  • 1
    Note that the file must be read via a shell redirection outside of the {} for this to work reliably. If you use something like cat File.txt | { 'rm' -f File.txt && ..., the rm command may happen to execute first and delete the file before cat (or whatever) can open it. The example in this answer does not have this problem, but you need to be careful not to change it too much... – Gordon Davisson May 02 '23 at 00:36