On GNU/Linux: sed -i '1s/^/new first line\n/' file.txt. On macOS or any BSD, the same command fails until you add an explicit empty suffix: sed -i '' '1s/^/new first line\n/' file.txt. Neither one does anything at all to an empty file, and neither is safe on a symlink or a hard-linked file. The form that behaves identically in all four of those cases is ed:
printf '0a\nnew first line\n.\nw\n' | ed -s file.txt
Below is what each method actually did on this machine, measured rather than described — including two commands that either destroy file permissions or do not run. Which spelling of sed -i works depends on the machine, so add-string-beginning-of-file.sh detects whether yours is GNU or BSD and asserts the matching behaviour for the empty file, the 0600 mode, the inode, hard links, symlinks, the unescaped &, the single-quoted $(date), strftime and the missing trailing newline; it only touches files it creates.
Table of Contents
The -i suffix argument is the real portability problem
sed -i '1s/^/…/' filename does not work on BSD or macOS. BSD sed reads the word after -i as the backup extension, so it takes your script as the suffix and your filename as the script:
$ sed -i '1s/^/[INFO] Log Entry - $(date)\n/' t1.txt
sed: 2: "t1.txt
": undefined label '1.txt'
rc=1
The file is untouched and no backup is written. GNU sed documents the opposite convention — the suffix is attached to the option, not passed as a separate word: “-i[SUFFIX], –in-place[=SUFFIX] … edit files in place (makes backup if SUFFIX supplied)”, so there it is -i for no backup and -i.bak for one. There is no single spelling that satisfies both; either add the '' and accept the command is macOS/BSD-only, or use ed. (I have no Linux machine here, so the GNU half of that sentence is quoted from GNU sed(1), not run.)
Two smaller points while I am here. A replacement string wrapped in single quotes inserts $(date) literally, not expanded — you get the seven characters $(date) in your log header. And an unescaped & in a sed replacement means “the text that matched”, which here is the empty string:
$ sed -i '' '1s/^/Tom & Jerry\n/' f3.txt
Tom Jerry
$ sed -i '' '1s/^/Tom \& Jerry\n/' f2.txt
Tom & Jerry
Silent data loss in a header line. cat and ed have no such problem because neither treats the text as a replacement expression.
An empty file defeats sed completely
sed is line-oriented and an empty file has no line 1, so the 1s/// address never matches. It exits 0 and reports nothing. Same input, five methods, byte counts after:
method size after
sed -i '' '1s/^/HDR\n/' 0 <-- did nothing, rc=0
printf | cat - f > t 4
awk 'BEGIN{print}{print}' 4
ed -s (0a … . w) 4
If you prepend headers to log files that may not exist yet, this is the bug you will ship. The other edge case runs the other way: on a 19-byte file whose last line has no trailing newline, prepending a 4-byte header gives 23 bytes with sed and with cat, and 24 bytes with awk — awk silently adds the missing newline. Both are defensible; only one of them is what you asked for.
The cat-and-mv command makes a 0600 file world-readable
This is the finding worth leading with if you keep anything private in the file you are editing. The command in question is echo "…" | cat - filename > temp && mv temp filename. mv installs a new file created under your umask, so the original mode is gone. Measured on files that all started at mode 600:
inode mode
before sed 381591304 600
after sed 381591309 600
before cat+mv 381591306 600
after cat+mv 381591312 644 <-- permissions lost
before awk+mv 381591308 600
after awk+mv 381591314 644 <-- permissions lost
before ed 381591305 600
after ed 381591305 600 <-- same inode
Run that against ~/.ssh/config, a .env, or a credentials file and you have just published it to every account on the box. The fix costs one word — write back through the existing file instead of replacing it, which keeps both the inode and the mode:
printf '%s\n' 'HDR' | cat - file.txt > file.tmp && cat file.tmp > file.txt && rm file.tmp
The same distinction decides what happens to links. cat - link > tmp && mv tmp link replaced the symlink with an ordinary file and left the target unmodified; the cat file.tmp > file.txt form followed the link and updated the target, which is almost certainly what you meant. BSD sed -i refuses outright — sed: s/link.txt: in-place editing only works for regular files — while GNU sed documents needing --follow-symlinks to do it. And on a hard-linked file, sed -i quietly breaks the link:
before: links=2 inode one=381591319 two=381591319
after : links=1 inode one=381591322 two=381591319
one.txt=[HDR|body|] two.txt=[body|]
ed edits in place. sed -i writes a new file and renames it over the old one.
The awk example does not run
Run as written, on this machine:
$ awk 'BEGIN {print "Timestamp: " strftime("%Y-%m-%d %H:%M:%S")} {print}' d.txt > temp && mv temp d.txt
awk: calling undefined function strftime
source line number 1
rc=2
strftime is a gawk function — it appears in the GNU awk manual’s “Time Functions”, and the awk on this machine (awk version 20200816, the one-true-awk that ships with macOS) does not define it. I have not tested mawk. Pass the timestamp in from the shell instead and the dependency disappears:
awk -v ts="$(date '+%Y-%m-%d %H:%M:%S')" 'BEGIN{print "Timestamp: " ts} {print}' d.txt
For appending rather than prepending, see appending strings to a file in bash and appending to a file with cat.