#!/usr/bin/env bash # verify-add-string-beginning-of-file.sh # Falsifies the claims on https://heatware.net/linux/add-string-beginning-of-file/ # # Self-contained. Builds its own scratch files, removes them via an EXIT trap, and exits 0 # when run alone in an empty directory. Anything untestable here is SKIP, never FAIL. # Executed checks are counted in PASS/FAIL; greps of the article are counted in CONTENT. set -u PASS=0; FAIL=0; SKIP=0; CONTENT=0 pass() { echo "PASS $*"; PASS=$((PASS+1)); } fail() { echo "FAIL $*"; FAIL=$((FAIL+1)); } skip() { echo "SKIP $*"; SKIP=$((SKIP+1)); } cpass() { echo "CONTENT-OK $*"; CONTENT=$((CONTENT+1)); } need() { command -v "$1" >/dev/null 2>&1; } ORIG_PWD=$(pwd) SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || SCRIPT_DIR=$ORIG_PWD WORK=$(mktemp -d "${TMPDIR:-/tmp}/hw_w14_prepend.XXXXXX") || exit 1 trap 'chmod -R u+rwX "$WORK" 2>/dev/null; rm -rf "$WORK"' EXIT cd "$WORK" || exit 1 # Which sed flavour is this? BSD needs an explicit suffix argument, GNU must not have one. SEDFLAVOUR=unknown if need sed; then printf 'x\n' > .probe if sed -i '' 's/x/y/' .probe >/dev/null 2>&1 && [ "$(cat .probe)" = "y" ]; then SEDFLAVOUR=bsd else printf 'x\n' > .probe if sed -i 's/x/y/' .probe >/dev/null 2>&1 && [ "$(cat .probe)" = "y" ]; then SEDFLAVOUR=gnu fi fi rm -f .probe .probe'' fi echo "# environment" echo "# uname: $(uname -s) $(uname -m)" echo "# sed flavour: $SEDFLAVOUR" echo "# awk: $(awk --version 2>&1 | head -1)" echo "# ed: $(command -v ed || echo 'not installed')" echo # Run sed -i portably for the checks that only need the edit to happen. sedi() { if [ "$SEDFLAVOUR" = bsd ]; then sed -i '' "$@"; else sed -i "$@"; fi; } # ------------------------------------------------ 1. the -i suffix argument differs if [ "$SEDFLAVOUR" = bsd ]; then printf 'body\n' > t1.txt out=$(sed -i '1s/^/HDR\n/' t1.txt 2>&1); rc=$? if [ $rc -ne 0 ] && [ "$(cat t1.txt)" = "body" ]; then pass "BSD sed: 'sed -i SCRIPT file' fails (rc=$rc) and leaves the file unchanged" else fail "expected BSD sed -i without a suffix to fail; rc=$rc file=[$(cat t1.txt)]" fi elif [ "$SEDFLAVOUR" = gnu ]; then printf 'body\n' > t1.txt out=$(sed -i '' '1s/^/HDR\n/' t1.txt 2>&1); rc=$? if [ $rc -ne 0 ]; then pass "GNU sed: the BSD spelling 'sed -i \"\" SCRIPT file' fails here (rc=$rc) - the two are not interchangeable" else fail "expected GNU sed to reject the BSD -i '' spelling; rc=$rc" fi else skip "sed -i suffix check (could not identify the sed flavour)" fi # ---------------------------------------------- 2. sed does nothing to an empty file if [ "$SEDFLAVOUR" != unknown ]; then : > empty.txt sedi '1s/^/HDR\n/' empty.txt >/dev/null 2>&1; rc=$? sz=$(wc -c < empty.txt | tr -d ' ') if [ "$sz" -eq 0 ]; then pass "sed -i on an empty file exited $rc and wrote 0 bytes - the 1s/^/ address never matches" else fail "sed on an empty file produced $sz bytes; the article says 0" fi else skip "empty-file sed check (no usable sed)" fi : > empty2.txt printf 'HDR\n' | cat - empty2.txt > e2.tmp && cat e2.tmp > empty2.txt sz2=$(wc -c < empty2.txt | tr -d ' ') if [ "$sz2" -eq 4 ]; then pass "the cat form wrote $sz2 bytes to the same empty file, where sed wrote 0" else fail "cat form on an empty file produced $sz2 bytes, expected 4" fi if need ed; then : > empty3.txt printf '0a\nHDR\n.\nw\n' | ed -s empty3.txt >/dev/null 2>&1 sz3=$(wc -c < empty3.txt | tr -d ' ') if [ "$sz3" -eq 4 ]; then pass "ed wrote $sz3 bytes to an empty file - it is not line-address limited the way sed is" else fail "ed on an empty file produced $sz3 bytes, expected 4" fi else skip "ed empty-file check (ed not installed)" fi # --------------------------------- 3. DUTY OF CARE: cat+mv resets the file's mode printf 'secret\n' > cred.txt chmod 600 cred.txt before=$(ls -l cred.txt | cut -c1-10) printf 'HDR\n' | cat - cred.txt > c.tmp && mv c.tmp cred.txt after=$(ls -l cred.txt | cut -c1-10) printf 'secret\n' > cred2.txt chmod 600 cred2.txt printf 'HDR\n' | cat - cred2.txt > c2.tmp && cat c2.tmp > cred2.txt && rm -f c2.tmp after2=$(ls -l cred2.txt | cut -c1-10) if [ "$before" = "-rw-------" ] && [ "$after" != "$before" ]; then pass "'cat - file > tmp && mv tmp file' changed a 0600 file to $after - the old page's command leaks permissions" elif [ "$before" = "-rw-------" ] && [ "$after" = "$before" ]; then skip "cat+mv permission check (umask $(umask) happens to reproduce 0600 here, so the leak cannot be shown)" else fail "could not set up the 0600 case: before=$before" fi if [ "$after2" = "-rw-------" ]; then pass "'cat tmp > file' kept the mode at $after2 - writing back through the file is the fix" else fail "the in-place cat form gave $after2, expected -rw-------" fi # ----------------------------------------------- 4. which methods keep the inode inode() { ls -di "$1" 2>/dev/null | awk '{print $1}'; } if need ed; then printf 'body\n' > ino_ed.txt i1=$(inode ino_ed.txt) printf '0a\nHDR\n.\nw\n' | ed -s ino_ed.txt >/dev/null 2>&1 i2=$(inode ino_ed.txt) if [ -n "$i1" ] && [ "$i1" = "$i2" ]; then pass "ed kept the same inode ($i2) - it really edits in place" else fail "ed changed the inode: $i1 -> $i2" fi else skip "ed inode check (ed not installed)" fi if [ "$SEDFLAVOUR" != unknown ]; then printf 'body\n' > ino_sed.txt j1=$(inode ino_sed.txt) sedi '1s/^/HDR\n/' ino_sed.txt >/dev/null 2>&1 j2=$(inode ino_sed.txt) if [ -n "$j1" ] && [ "$j1" != "$j2" ]; then pass "sed -i replaced the inode ($j1 -> $j2) - it writes a new file and renames it over the old one" else fail "expected sed -i to change the inode; got $j1 -> $j2" fi else skip "sed inode check (no usable sed)" fi # ------------------------------------------- 5. sed -i breaks hard links printf 'body\n' > h_one.txt if ln h_one.txt h_two.txt 2>/dev/null && [ "$SEDFLAVOUR" != unknown ]; then sedi '1s/^/HDR\n/' h_one.txt >/dev/null 2>&1 one=$(tr '\n' '|' < h_one.txt); two=$(tr '\n' '|' < h_two.txt) if [ "$one" != "$two" ]; then pass "sed -i broke the hard link: one=[$one] two=[$two]" else fail "hard link survived sed -i: one=[$one] two=[$two]" fi else skip "hard-link check (ln failed or no usable sed on this filesystem)" fi # --------------------------------------------------- 6. symlink handling mkdir -p s && printf 'body\n' > s/real.txt if ln -s real.txt s/link.txt 2>/dev/null; then printf 'HDR\n' | cat - s/link.txt > s/t.tmp && mv s/t.tmp s/link.txt if [ ! -L s/link.txt ] && [ "$(cat s/real.txt)" = "body" ]; then pass "cat+mv replaced the symlink with a regular file and left the target untouched" else fail "expected cat+mv to destroy the symlink; link is still a symlink or the target changed" fi mkdir -p s2 && printf 'body\n' > s2/real.txt && ln -s real.txt s2/link.txt printf 'HDR\n' | cat - s2/link.txt > s2/t.tmp && cat s2/t.tmp > s2/link.txt && rm -f s2/t.tmp if [ -L s2/link.txt ] && [ "$(head -1 s2/real.txt)" = "HDR" ]; then pass "the in-place cat form followed the symlink and updated the target, keeping the link" else fail "in-place cat through a symlink did not update the target" fi else skip "symlink checks (this filesystem would not create a symlink)" fi # ------------------------------------------ 7. an unescaped & in a sed replacement if [ "$SEDFLAVOUR" != unknown ]; then printf 'body\n' > amp.txt sedi '1s/^/Tom & Jerry\n/' amp.txt >/dev/null 2>&1 got=$(head -1 amp.txt) printf 'body\n' > amp2.txt sedi '1s/^/Tom \& Jerry\n/' amp2.txt >/dev/null 2>&1 got2=$(head -1 amp2.txt) if [ "$got" != "Tom & Jerry" ] && [ "$got2" = "Tom & Jerry" ]; then pass "unescaped & became [$got]; escaped \\& became [$got2]" else fail "& handling: unescaped=[$got] escaped=[$got2]" fi else skip "sed & escaping check (no usable sed)" fi # ------------------------------------------- 8. single quotes do not expand \$(date) if [ "$SEDFLAVOUR" != unknown ]; then printf 'body\n' > dq.txt sedi '1s/^/stamp $(date)\n/' dq.txt >/dev/null 2>&1 if head -1 dq.txt | grep -q '[$](date)'; then pass "inside single quotes the shell left \$(date) literal: [$(head -1 dq.txt)]" else fail "expected a literal \$(date) in the header; got [$(head -1 dq.txt)]" fi else skip "quoting check (no usable sed)" fi # -------------------------------------------------- 9. awk strftime availability if need awk; then out=$(awk 'BEGIN {print strftime("%Y")}' 2>&1); rc=$? if [ $rc -ne 0 ]; then pass "this awk has no strftime (rc=$rc): $(printf '%s' "$out" | head -1)" else skip "awk strftime is available here ($(awk --version 2>&1 | head -1)) - the article's failure is specific to awks without it" fi ts=$(awk -v ts="fixed" 'BEGIN{print "Timestamp: " ts}' 2>/dev/null) if [ "$ts" = "Timestamp: fixed" ]; then pass "the portable form (awk -v ts=...) works regardless of strftime" else fail "awk -v form produced [$ts]" fi else skip "awk checks (awk not installed)" fi # ------------------------------------- 10. trailing-newline behaviour differs printf 'no-final-newline' > nn1.txt printf 'no-final-newline' > nn2.txt if [ "$SEDFLAVOUR" != unknown ]; then sedi '1s/^/HDR\n/' nn1.txt >/dev/null 2>&1 a=$(wc -c < nn1.txt | tr -d ' ') else a="" fi awk 'BEGIN{print "HDR"}{print}' nn2.txt > nn2.out 2>/dev/null && mv nn2.out nn2.txt b=$(wc -c < nn2.txt | tr -d ' ') if [ -n "$a" ] && [ -n "$b" ] && [ "$b" -eq $((a+1)) ]; then pass "awk added a final newline the input did not have ($a bytes vs $b); sed preserved its absence" elif [ -z "$a" ]; then skip "trailing-newline comparison (no usable sed)" else fail "trailing-newline comparison: sed=$a bytes, awk=$b bytes; article says awk is one byte larger" fi # ------------------------------------------------------ article content guards ART="" for cand in "$SCRIPT_DIR/add-string-beginning-of-file.html" "$ORIG_PWD/add-string-beginning-of-file.html"; do [ -f "$cand" ] && { ART=$cand; break; } done if [ -n "$ART" ]; then grep -q "in-place editing only works for regular files" "$ART" && cpass "article keeps the BSD symlink refusal" \ || { echo "FAIL BSD symlink refusal missing from the article"; FAIL=$((FAIL+1)); } grep -q "undefined label" "$ART" && cpass "article shows the real BSD sed -i error" \ || { echo "FAIL BSD sed -i error text missing"; FAIL=$((FAIL+1)); } grep -q "permissions lost" "$ART" && cpass "article keeps the cat+mv permission finding" \ || { echo "FAIL cat+mv permission finding missing"; FAIL=$((FAIL+1)); } grep -q "calling undefined function strftime" "$ART" && cpass "article keeps the awk strftime failure" \ || { echo "FAIL awk strftime failure missing"; FAIL=$((FAIL+1)); } grep -q "/linux/append-strings-to-file-bash/" "$ART" && cpass "article links to the append-strings page" \ || { echo "FAIL internal link to append-strings-to-file-bash missing"; FAIL=$((FAIL+1)); } else skip "article self-checks (add-string-beginning-of-file.html not next to this script)" fi echo echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP CONTENT=$CONTENT" [ "$FAIL" -eq 0 ] || exit 1 exit 0