2026-05-16_coscliで日付ページタイトル一括変換
プライベートの日記ページの手入れの一環で、coscliを使って日付部分を/区切りから-区切りに変換した
Scrapboxのタイトルをファイル名にすると、わかりやすいのだけど、タイトルの/がファイル区切りと被ってるので扱いにくかった。
このプロジェクト(/taktamur)は、とりあえずこのままのYYYY/MM/DD形式で触らない。
以下🤖まとめ
----
2026-05-16_coscliで日付ページタイトル一括変換
co sync がファイル名に / を含むと生成できない問題への対応として、taktamur-diary の YYYY/MM/DD 系日付ページを YYYY-MM-DD 形式に一括変換した。
対象規模 (計 1043 ページ)
純粋日付 YYYY/MM/DD: 766 件
曜日付き YYYY/MM/DD(曜): 15 件
0埋めなし YYYY/M/D: 172 件
月次 YYYY/MM / YYYY/M: 90 件
実装方針
./rename-page.sh という bash スクリプトを 1 本作成
引数: <old-title> <new-title> --dry-run
処理: persistent ガード → cos search でリンク元検索 → 各リンク元の本文を old → new 置換 → cos page rename
driver は targets.txt をループして呼び出すだけ
cos のソースコードは変更せず、既存コマンドの組み合わせで実現
途中で踏んだバグ 3 つ
バグ1: タイトル累積 cos page text の出力をそのまま cos page edit に流すと、タイトル行がボディに毎回積まれる
原因: cos page edit は lines = 既存title, ...入力ファイル を構築する (edit.ts:112-114, pages.ts:75-85)。入力にタイトル行が含まれると lines1 にも title が入る
対処: cos page text | tail -n +2 で先頭行を除外
遺物: 2018-10 (旧 2018/10) ページ本文先頭にタイトル文字列 3 行残存 (未修復)
バグ2: バックアップ巻き戻し バックアップファイルを再利用していたため、後続処理で前回の置換が巻き戻った
対処: 毎回 cos page text で最新本文を取得し直す形に変更
バグ3: プレースホルダーへの rename persistent: false (Scrapbox のプレースホルダー) に対して rename すると、lines = [新タイトル] だけの空ページが新規作成される事故になる
対処: スクリプト冒頭で旧タイトルの persistent を確認し、false なら skip
注意点: targets.txt 内に既に rename 済みのページが placeholder 化して混入することがある
反映タイミング (Scrapbox 側)
ページ本文・rename 判定: cos page edit / rename は WebSocket commit ack を待って戻るため即時反映
cos search の検索インデックス: 数秒〜数分の遅延あり (連続実行中の誤検出は冪等な本文 sed で吸収)
検証は最後の操作から 5〜10 分待ってから行うのが安全
結果
全 1043 ページ rename OK
リンク元ページの本文置換イベント 累計 1500+ 件
純粋日付形式の残数: 0
完全一致 old リンク残存: 0 (サンプル検証 + 全件チェックで確認)
新規 ERR: 0
残課題
日付プレフィックス + 説明 (例: 2026/01/24_通院メモ) 約 89 件
trash/ プレフィックス 15 件
非日付タイトル (YouTube 等) 79 件
バグ1 の遺物 (2018-10 の重複タイトル) 修復
ソース側改修案: cos page rename にインバウンドリンク追従オプションを足すかどうか
code:rename-page.sh
#!/usr/bin/env bash
# rename-page.sh <old-title> <new-title> --dry-run
# 1ページの rename + 全リンク元の old -> new 置換を行う。
set -uo pipefail
OLD="${1:-}"
NEW="${2:-}"
DRY=""
"${3:-}" = "--dry-run" && DRY="--dry-run"
if -z "$OLD" || -z "$NEW" ; then
echo "usage: $0 <old-title> <new-title> --dry-run" >&2
exit 64
fi
PROJ="${COS_PROJECT:-taktamur-diary}"
WORK_DIR="${WORK_DIR:-./work}"
BACKUP_DIR="$WORK_DIR/backup"
LOG="$WORK_DIR/log.txt"
mkdir -p "$BACKUP_DIR"
ts() { date +"%Y-%m-%dT%H:%M:%S"; }
log() { echo "$(ts) $*" | tee -a "$LOG" >&2; }
safe_name() {
printf '%s' "$1" | tr '/' '_'
}
log "START rename: '$OLD' -> '$NEW' (project=$PROJ dry-run=${DRY:-no})"
# --- Step 0: persistent ガード ---
# 旧タイトルが persistent: false (プレースホルダー) の場合、rename すると
# 空ページ (lines = 新タイトル のみ) が新規作成される事故になる。
# 既に rename 済みの旧タイトルも placeholder 化しているため、ここで skip する。
OLD_PERSISTENT=$(cos page get -p "$PROJ" "$OLD" -J --results-only 2>/dev/null | jq -r '.persistent // false')
if "$OLD_PERSISTENT" != "true" ; then
log "skip placeholder: '$OLD' (persistent=$OLD_PERSISTENT)"
exit 0
fi
# --- Step 1: リンク元検索 ---
SEARCH_JSON=$(cos search -p "$PROJ" "$OLD" -J --results-only --limit 100 2>/dev/null) || {
log "ERR search failed for '$OLD'"
exit 1
}
INBOUND=()
while IFS= read -r line; do
-n "$line" && INBOUND+=("$line")
done < <(echo "$SEARCH_JSON" | jq -r '.pages[].title')
log "found ${#INBOUND@} inbound page(s) for '$OLD'"
# --- Step 2: リンク元ページ本文の置換 ---
EDIT_FAIL=0
for title in ${INBOUND@+"${INBOUND@}"}; do
-z "$title" && continue
safe=$(safe_name "$title")
backup="$BACKUP_DIR/${safe}.txt"
current="$BACKUP_DIR/${safe}.current.txt"
newfile="$BACKUP_DIR/${safe}.new.txt"
# 毎回最新本文を取得 (巻き戻し防止)。tail -n +2 でタイトル行除外
# 理由: cos page edit は lines = title, ...入力ファイル を構築するため
cos page text -p "$PROJ" "$title" -q 2>/dev/null | tail -n +2 > "$current"
if ! -s "$current" && ! cos page text -p "$PROJ" "$title" -q >/dev/null 2>&1; then
log "ERR text fetch failed: '$title'"
EDIT_FAIL=1
continue
fi
# ロールバック用 backup は初回のみ保存
! -f "$backup" && cp "$current" "$backup"
# <old> のみ置換 (素のテキストは触らない)
sed "s|\\${OLD}\\|${NEW}|g" "$current" > "$newfile"
if diff -q "$current" "$newfile" >/dev/null 2>&1; then
log "skip (no diff): '$title'"
rm -f "$newfile" "$current"
continue
fi
if cos page edit -p "$PROJ" "$title" --from-file "$newfile" -q $DRY 2>/dev/null; then
log "edit OK: '$title'"
else
log "ERR edit failed: '$title'"
EDIT_FAIL=1
fi
rm -f "$current"
done
# --- Step 3: タイトル変更 ---
if cos page rename -p "$PROJ" "$OLD" "$NEW" -q $DRY 2>/dev/null; then
log "rename OK: '$OLD' -> '$NEW'"
else
RC=$?
# 既に新タイトルが存在し、旧タイトルが消えていれば「処理済み」として 0
if cos page get -p "$PROJ" "$NEW" -J --results-only 2>/dev/null | jq -e '.persistent == true' >/dev/null 2>&1; then
log "rename skipped (already done): '$OLD' -> '$NEW'"
else
log "ERR rename failed (rc=$RC): '$OLD' -> '$NEW'"
exit 2
fi
fi
if "$EDIT_FAIL" -ne 0 ; then
log "DONE with edit errors: '$OLD' -> '$NEW'"
exit 1
fi
log "DONE: '$OLD' -> '$NEW'"
exit 0