Linuxでファイルの先頭にテキストを追加する手法と応用例

ファイル先頭へのテキスト追加では、元ファイルを直接リダイレクトで開きません。変更前backupをno-clobberで先に固定し、同じディレクトリのpost-imageを検証してからrenameat2(RENAME_EXCHANGE)で一度だけ交換します。これにより、採用の瞬間にも元bytesが別名で残ります。

目次

変更前identityとbackupの不存在を固定する

対象はregular non-symlink fileに限定します。固定backupが既に存在する場合は上書きせず停止します。SHA-256とmode・uid・gidをbaselineとし、4ブロックは同じBashセッションで実行します。

set -Eeuo pipefail
file=/srv/lab/report.txt
backup=/srv/lab/report.txt.before-prepend
header='# generated: 2026-07-17'
parent=$(dirname -- "$file")
test -f "$file" && test ! -L "$file"
test ! -e "$backup" || { printf 'STOP: immutable before-backup already exists\n' >&2; exit 20; }
command -v python3 >/dev/null
before_hash=$(sha256sum -- "$file"); before_hash=${before_hash%% *}; before_hash=${before_hash#\\}
before_meta=$(stat --format='%a:%u:%g' -- "$file")
printf 'before_sha256=%s metadata=%s\n' "$before_hash" "$before_meta"

backupを先に確定しpost-imageを作る

backup tempとpost-imageは対象と同じfilesystemのmktemp directoryへ作ります。backupはRENAME_NOREPLACEで固定名へ採用し、header後の全bytesがbackupと一致すること、mode・uid・gidが意図どおりであること、採用直前の元hashがbaselineのままであることを確認します。

atomic_noreplace() {
  python3 - "$1" "$2" <<'PY'
import ctypes, os, sys
src, dst = map(os.fsencode, sys.argv[1:])
libc = ctypes.CDLL(None, use_errno=True)
try:
    renameat2 = libc.renameat2
except AttributeError:
    raise SystemExit("renameat2 is unavailable; no non-atomic fallback is allowed")
renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
if renameat2(-100, src, -100, dst, 1) != 0:  # RENAME_NOREPLACE
    number = ctypes.get_errno()
    raise OSError(number, os.strerror(number), os.fsdecode(dst))
PY
}
atomic_exchange() {
  python3 - "$1" "$2" <<'PY'
import ctypes, os, sys
left, right = map(os.fsencode, sys.argv[1:])
libc = ctypes.CDLL(None, use_errno=True)
try:
    renameat2 = libc.renameat2
except AttributeError:
    raise SystemExit("renameat2 is unavailable; no non-atomic fallback is allowed")
renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
if renameat2(-100, left, -100, right, 2) != 0:  # RENAME_EXCHANGE
    number = ctypes.get_errno()
    raise OSError(number, os.strerror(number))
PY
}
run_id=$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM
work=$(mktemp -d -p "$parent" ".ittrip-110.$run_id.XXXXXXXX")
printf '%s\n' "$run_id" > "$work/.owner"
backup_stage="$work/before-backup"
cp --preserve=all -- "$file" "$backup_stage"
test "$(sha256sum -- "$file" | sed 's/^\\//;s/ .*//')" = "$before_hash"
atomic_noreplace "$backup_stage" "$backup"
test "$(sha256sum -- "$backup" | sed 's/^\\//;s/ .*//')" = "$before_hash"
stage="$work/postimage"
cp --preserve=all -- "$backup" "$stage"
{ printf '%s\n' "$header"; cat -- "$backup"; } > "$stage"
sync -f "$stage"
printf '%s\n' "$header" > "$work/header"
header_bytes=$(stat --format='%s' -- "$work/header")
dd if="$stage" bs=1 skip="$header_bytes" status=none | cmp --silent - "$backup"
test "$(stat --format='%a:%u:%g' -- "$stage")" = "$before_meta"
after_hash=$(sha256sum -- "$stage"); after_hash=${after_hash%% *}; after_hash=${after_hash#\\}
test "$(sha256sum -- "$file" | sed 's/^\\//;s/ .*//')" = "$before_hash"

atomic exchangeで採用し再検証する

RENAME_EXCHANGE後、stage側へ移った旧fileがbaseline hashと一致しない場合は即座にもう一度exchangeして並行変更を戻します。成功時はheader、残り全bytes、metadata、before/after hashをreceiptへ記録します。

atomic_exchange "$stage" "$file"
swapped_old_hash=$(sha256sum -- "$stage" | sed 's/^\\//;s/ .*//')
installed_hash=$(sha256sum -- "$file" | sed 's/^\\//;s/ .*//')
if test "$swapped_old_hash" != "$before_hash" || test "$installed_hash" != "$after_hash"; then
  atomic_exchange "$stage" "$file"
  printf 'STOP: concurrent change detected; atomic exchange was reversed\n' >&2
  exit 31
fi
test "$(sed -n '1p' -- "$file")" = "$header"
dd if="$file" bs=1 skip="$header_bytes" status=none | cmp --silent - "$backup"
test "$(stat --format='%a:%u:%g' -- "$file")" = "$before_meta"
receipt="$parent/.ittrip-110-$run_id.receipt"
( umask 077; set -o noclobber; printf 'file=%s\nbackup=%s\nbefore=%s\nafter=%s\nrun_id=%s\n' "$file" "$backup" "$before_hash" "$after_hash" "$run_id" > "$receipt" )
printf 'accepted before=%s after=%s backup=%s receipt=%s\n' "$before_hash" "$after_hash" "$backup" "$receipt"

真のbackupからatomic restoreする

復旧前に現在fileがこのrunのafter hash、backupがbefore hashであることを再確認します。backup copyとfileをatomic exchangeし、exact before hashへ戻ったことを確認してから、旧post-imageをno-clobber quarantineへ残します。

# receiptと両hashが一致するときだけ、backupからatomic restoreする。
grep -Fx "file=$file" "$receipt" >/dev/null
grep -Fx "backup=$backup" "$receipt" >/dev/null
grep -Fx "before=$before_hash" "$receipt" >/dev/null
grep -Fx "after=$after_hash" "$receipt" >/dev/null
test "$(sha256sum -- "$file" | sed 's/^\\//;s/ .*//')" = "$after_hash"
test "$(sha256sum -- "$backup" | sed 's/^\\//;s/ .*//')" = "$before_hash"
restore_stage="$work/restore-stage"
cp --preserve=all -- "$backup" "$restore_stage"
atomic_exchange "$restore_stage" "$file"
test "$(sha256sum -- "$file" | sed 's/^\\//;s/ .*//')" = "$before_hash" || { atomic_exchange "$restore_stage" "$file"; exit 41; }
post_quarantine="$file.after-prepend-$run_id"
test ! -e "$post_quarantine"
atomic_noreplace "$restore_stage" "$post_quarantine"
test "$(sha256sum -- "$post_quarantine" | sed 's/^\\//;s/ .*//')" = "$after_hash"
printf 'restored exact before-image; post-image quarantined at %s\n' "$post_quarantine"

受入条件

既存backup sentinelでは変更前に停止し、fileとsentinelのbytes/hashが不変であることを確認します。clean runでは先頭1行が完全一致し、header byte数以降がbackupとbyte完全一致し、mode・uid・gidがbaselineどおりです。restore後はcontent/hashとpreserve=allで保存したmetadataがbackupと一致します。

停止すべきケース

ログ追記中、巨大file、hard linkの意味を維持する必要があるfile、SELinux labelやACLの移送可否を確認できない環境では停止します。同じfileを入力とリダイレクト先へ指定する方法や、fixed .newを上書きする方法は使用しません。

公式情報・参考資料

ファイル先頭へのテキスト追加の構文と制約は、本文末の一次資料と対象環境のlocal helpで照合します。ファイル先頭へのテキスト追加の記事確認日は2026年7月17日で、版が異なる場合はoption、default、終了statusの差を先に確認してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次