How to Insert a String to the Beginning of a File on Linux

bash arguments, pass parameter, pass argument to script

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 the previous version of this page recommended that either destroy file permissions or do not run.

The -i suffix argument is the real portability problem

The previous version gave sed -i '1s/^/…/' filename with no qualification and said the commands “work on all Linux distributions”. 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 corrections while I am here. The old page’s replacement string was wrapped in single quotes, so $(date) is inserted 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 awkawk silently adds the missing newline. Both are defensible; only one of them is what you asked for.

The old page’s cat command makes a 0600 file world-readable

This is the correction worth leading with if you keep anything private in the file you are editing. The recommended command was 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|]

So the old page’s “sed -i and ed are best for modifying files in place” is half right. ed edits in place. sed -i writes a new file and renames it over the old one.

The awk example does not run

Verbatim from the previous version, 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. The script that reproduces every block above builds its own scratch files, prints one line per claim, and cleans up after itself.

Photo of author
Sudhir P. founded HeatWare.com in 1999 and has built and operated it full-stack ever since; it is now used by more than 88,000 people. He writes here about the PostgreSQL, MySQL, Linux and DevOps work that keeps it running. Articles are rewritten only after the commands in them have actually been run, and the verification scripts are published alongside them so anyone can check the claims. Reach him at blog@heatware.net.