From b8d5a71ae48dcbb5bec7f343da1c4afab79b16d4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 24 Sep 2026 10:36:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?docs(PLAN70):=20=E4=BD=9C=E3=82=8A=E7=9B=B4?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=82=82=E6=AE=8B=E3=82=8B=E3=82=B7=E3=82=A7?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=AE=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E8=BE=BC=E3=81=BF=E5=85=88=E3=81=AE=E8=A6=81=E6=B1=82=E3=81=A8?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=20(#253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ~/.shellrc.d をアカウントグループのボリュームへ張り、~/.bashrc が起動定義の後で その中の *.sh を読む。置き場所のパスはイメージの ENV の DEVBASE_SHELLRC_DIR で示す。 要求と受け入れ条件、設計(名前・読み込み器・zsh を含めないこと等の決定)だけを載せる。 Refs #253 Co-Authored-By: Claude Opus 5.5 (1M context) --- issues/PLAN70_shellrc-dir-design.md | 279 ++++++++++++++++++++++++++++ issues/PLAN70_shellrc-dir.md | 210 +++++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 issues/PLAN70_shellrc-dir-design.md create mode 100644 issues/PLAN70_shellrc-dir.md diff --git a/issues/PLAN70_shellrc-dir-design.md b/issues/PLAN70_shellrc-dir-design.md new file mode 100644 index 0000000..2ebca6f --- /dev/null +++ b/issues/PLAN70_shellrc-dir-design.md @@ -0,0 +1,279 @@ +# PLAN70: コンテナを作り直しても残るシェルの設定の読み込み先 の設計 + +要求と受け入れ条件は [PLAN70_shellrc-dir.md](PLAN70_shellrc-dir.md) にある。 +この文書は「どう作るか」だけを扱う。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 置き場所 `~/.shellrc.d/` を、アカウントグループのボリュームへ張って作り直しでも残す | コンテナの利用者と、設定を足すツール(最初は ai-plugins の中継。ai-plugins#928) | +| F2 | 対話シェルの起動時に、置き場所の `*.sh` を名前の順に読む | 同上 | +| F3 | 置き場所のパスを、非対話の処理にも見える環境変数 `DEVBASE_SHELLRC_DIR` で示す | 置き場所へファイルを置くツール | +| F4 | 上の 3 つを回帰テストで固定し、利用者向け文書と CHANGELOG を合わせる | devbase の開発者・利用者 | + +## 構成要素 + +| 要素 | 変更 | 責務 | +| --- | --- | --- | +| 永続化のエントリの一覧(`containers/base/entrypoint.sh` の `DEVBASE_GROUP_SETTINGS`) | 変える | 末尾へ `".shellrc.d"` を足す。既存の `devbase_link_setting` が、グループ側にディレクトリを作り、`~/.shellrc.d` をそこへの symlink にする。関数は変えない(決定 6・7) | +| 読み込み器(`containers/base/shellrc-dir.sh`、新設) | 足す | `/etc/devbase/shellrc-dir.sh` として置く。置き場所の `*.sh` を名前の順に読み、使った変数を消す。副作用はそれだけにする(決定 3・4) | +| 読み込み器の配置(`containers/base/Dockerfile` の `COPY` 群) | 変える | `ai-cli-aliases.sh` の `COPY` の次へ、`COPY --chmod=0644 shellrc-dir.sh /etc/devbase/shellrc-dir.sh` を足す。`/etc/devbase` は既存の `install -d -m 0755` が先に作る | +| `~/.bashrc` への 1 行(同じ Dockerfile の `RUN`) | 変える | `. /etc/devbase/ai-cli-aliases.sh` の行の**次**へ `. /etc/devbase/shellrc-dir.sh` を足す(決定 3) | +| 環境変数(同じ Dockerfile の `ENV`) | 足す | `ENV DEVBASE_SHELLRC_DIR=/home/${USERNAME}/.shellrc.d` を読み込み器の `COPY` の直前に置く(決定 2) | +| `tests/containers/test_shellrc_dir.py`(新設) | 足す | 読み込み器を一時ディレクトリで source し、読む順・対象・失敗の扱い・変数の後始末を固定する。Dockerfile の文字列で配置と順序を固定する | +| `tests/containers/test_entrypoint_ai_settings.py` | 変える | 分類 B の張り先の検査(`test_group_entries_point_at_the_group_volume`)の一覧へ `.shellrc.d` を足す | +| `docs/user/container-operations.md` | 変える | 「AI 設定の永続化」の分類 B の表へ行を足す。新しい小節「作り直しても残るシェルの設定」を「AI CLI の起動定義」の後に立てる | +| `CHANGELOG.md` | 変える | `[Unreleased]` の `### Added` に 1 項目足す | + +次のものは変えない。 + +- `entrypoint.sh` の関数(`devbase_link_setting` / `devbase_seed_group_settings` など) +- `/etc/devbase/ai-cli-aliases.sh` の中身 +- `~/.zshrc`(決定 5) +- ホスト側(`lib/devbase/`)。生成する compose に環境変数を足さない(決定 2) +- 派生イメージの Dockerfile(base を継ぐ 7 つはすべて `FROM devbase-base:latest`)と + `containers/lfm`(決定 8) + +### 文脈 + +```mermaid +graph LR + U[利用者] -->|手で置く| B[devbase の base イメージと
そのコンテナ] + T[設定を足すツール
ai-plugins の中継] -->|DEVBASE_SHELLRC_DIR を読み
1 ファイル置く| B + B -->|置き場所の実体| V[アカウントグループの
ボリューム] +``` + +ai-plugins は devbase の外にあり、この変更では変えない。devbase が約束するのは置き場所と +環境変数の 2 つだけである(「入出力の契約」)。 + +### 構成要素と配置 + +```mermaid +graph TD + subgraph イメージ + E[ENV DEVBASE_SHELLRC_DIR] + L[読み込み器
/etc/devbase/shellrc-dir.sh] + A[AI CLI の起動定義
/etc/devbase/ai-cli-aliases.sh] + R[~/.bashrc の末尾] + EP[entrypoint の
分類 B の一覧] + end + subgraph コンテナの起動時 + S["~/.shellrc.d → /persistent/group/.shellrc.d"] + end + subgraph アカウントグループのボリューム + D["/persistent/group/.shellrc.d/*.sh"] + end + EP -->|symlink を張る| S + S --> D + R -->|1. 読む| A + R -->|2. 読む| L + L -->|パスを読む| E + L -->|名前の順に読む| D +``` + +図にはイメージとコンテナの中で動くものだけを描く。テスト・利用者向け文書・CHANGELOG は +描かない。 + +## 入出力の契約 + +devbase が外へ約束するのは次の 2 つである。ai-plugins#928 はこの名前を使う。 + +| 項目 | 約束 | +| --- | --- | +| 名前 | 環境変数 `DEVBASE_SHELLRC_DIR` と、その既定値の置き場所 `~/.shellrc.d/` | +| 値 | `/home/ubuntu/.shellrc.d`(絶対パス。イメージの `ENV` が決める) | +| 存在 | entrypoint が終わった後(`/tmp/entrypoint-ready` がある時点)、値のパスは開発ユーザーが書けるディレクトリで、実体はアカウントグループのボリュームにある | +| 読まれるもの | 置き場所の直下の、名前が `.sh` で終わる通常ファイルで、読めるもの。サブディレクトリの中・`.` で始まる名前・他の拡張子は読まない | +| 読む順 | グロブの展開順(ファイル名の昇順)。`ai-cli-aliases.sh` の**後**なので、同じ名前の alias は置き場所の定義が勝つ | +| 読まれる時点 | 対話の bash の起動時だけ(`devbase login` / tmux の窓 / VS Code の端末)。置いたファイルは次に開くシェルから効く。非対話の処理には読まれない | +| 失敗の形 | 1 つのファイルの誤りは、そのファイルの誤りとして標準エラーに出て、次のファイルへ進む。devbase は誤りを握りつぶさず、起動も止めない | +| 互換性 | 変数が無い(古いイメージ)ときは置き場所も無い。ツールは変数の有無で判定し、無ければ `~/.bashrc` を書き換えずに、イメージの建て直しを案内する。これは ai-plugins 側の扱いで、devbase は約束しない | + +**置き場所へ置くファイルの作法**(利用者向け文書に書く): + +- 1 つのツール・1 つの用途につき 1 ファイルにし、`<名前>.sh` とする(例: `ndf-relay.sh`)。 + 順序を指定したいときは `10-` のような数字の接頭辞を付ける +- 何度読まれても同じ結果になるように書く。`exit` を書かない(対話シェルが終わる)。 + 標準出力へ何も出さない +- devbase は置き場所へ何も書かない。置いたものを消すのは置いた側である + +## 処理の流れ + +```mermaid +sequenceDiagram + participant EP as entrypoint + participant V as グループのボリューム + participant SH as 対話の bash + participant L as 読み込み器 + EP->>V: .shellrc.d が無ければ作る + EP->>EP: ~/.shellrc.d を symlink にする + Note over EP: /tmp/entrypoint-ready + SH->>SH: ~/.bashrc(非対話ならここで帰る) + SH->>SH: . /etc/devbase/ai-cli-aliases.sh + SH->>L: . /etc/devbase/shellrc-dir.sh + L->>L: dir = DEVBASE_SHELLRC_DIR か ~/.shellrc.d + alt dir がディレクトリでない + L-->>SH: 何もしない + else ディレクトリである + loop dir/*.sh を名前の順に + L->>L: 通常ファイルで読めるなら source + end + L->>L: 使った変数を unset + end +``` + +読み込み器の中身は次の形にする。**外部コマンドを起動しない**(非機能の条件)。 + +```bash +# 置き場所の *.sh を名前の順に読む。対話シェルの ~/.bashrc から読まれる。 +__devbase_shellrc_dir="${DEVBASE_SHELLRC_DIR:-$HOME/.shellrc.d}" +if [ -d "$__devbase_shellrc_dir" ]; then + for __devbase_shellrc_file in "$__devbase_shellrc_dir"/*.sh; do + if [ -f "$__devbase_shellrc_file" ] && [ -r "$__devbase_shellrc_file" ]; then + . "$__devbase_shellrc_file" + fi + done +fi +unset __devbase_shellrc_dir __devbase_shellrc_file +``` + +- 一致が無いとき bash のグロブは文字列のまま残る。`-f` の判定で落ちるので、`nullglob` を + 切り替えずに済む。利用者のシェルの設定を読み込み器が変えない +- `if` で包むのは、最後のファイルが読めないときに `&&` の連なりが非 0 を残さないためである。 + 末尾の `unset` で終了状態は 0 になる(受け入れ条件 6) +- 変数名を `__devbase_` で始めるのは、利用者の変数(`f` など)を上書きしないためである + (受け入れ条件 9)。置き場所のファイルが同じ名前の変数を使う場合までは守らない + +実測(2026-09-24、手元の `devbase-base:latest` の bash で上の形を source した): + +| 置いたもの | 結果 | +| --- | --- | +| `10-a.sh` と `20-b.sh` が同じ alias を定義 | `20-b.sh` の定義が残った | +| 構文の誤りを持つ `15-bad.sh` | 標準エラーに誤りを出し、次のファイルへ進んだ | +| `x.txt` | 読まれなかった | +| 置き場所が無い / 空 | 何も出さず、終了状態 0 | + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | 実体を `~/.claude` と同じ `/persistent/group` に置き、所有者は既存の `devbase_ensure_entry` と同じく開発ユーザーにする。イメージと entrypoint は置き場所へファイルを書かない | 受け入れ条件 1 の実機で所有者を見る。テストで、entrypoint の後の置き場所が空であることを見る | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器を組み込みの `[`・`for`・`.`・`unset` だけで書く | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | + +## 決定の記録 + +### 決定 1: 置き場所の名前は `~/.shellrc.d` にする + +特定のシェルの名前を含まず、後で zsh を入れたときも同じ置き場所を読ませられる。`.d` の +接尾辞は「中のファイルを全部読む」ディレクトリの慣例で、名前だけで使い方が伝わる。ホームの +直下に置くのは、分類 B の既存のエントリ(`.claude` / `.gemini`)と同じ並びにするためである。 +ai-plugins#928 の設計もこの名前で進んでいる。 + +`~/.bashrc.d` は bash 専用に読め、zsh を足すときに名前が実態と食い違う。 +`~/.config/devbase/shellrc.d` は `~/.config` が永続化されない場所で、symlink の親だけが +揮発する構成になり、見つけにくい。 + +### 決定 2: `DEVBASE_SHELLRC_DIR` はイメージの `ENV` で定める + +置き場所のパスはイメージの開発ユーザーのホームで決まるので、イメージが定めるのが筋である。 +`ENV` は `docker exec` の非対話の処理にも、派生イメージにも届く。名前は `DEVBASE_` で始め、 +ディレクトリを指すので `_DIR` で終える。コンテナの中で読む既存の変数 +(`DEVBASE_ACCOUNT_GROUP` / `DEVBASE_PRIMARY_DIR` / `DEVBASE_WORK_ROOT`)と同じ形である。 + +ホストが生成する compose で渡す形は採らない。古いイメージのコンテナへも、存在しない置き場所を +指す変数が渡ってしまう。entrypoint の `export` は PID 1 の子にしか効かず `docker exec` の +シェルに届かない(`entrypoint.sh` の PLAN39 の注記と同じ理由)。`~/.bashrc` の `export` は +非対話の処理から見えない。 + +### 決定 3: 読み込みはファイルにして、`~/.bashrc` からは 1 行で読む + +ループを `~/.bashrc` へ直接書くと、Docker を起動しないテストで振る舞いを確かめられない。 +`ai-cli-aliases.sh` と同じく(PLAN50)`/etc/devbase/` にファイルを置き、テストはそのファイルを +source する。読み込みの中身を直すときも `~/.bashrc` の行は変わらない。 + +#253 の提案の `for f in ...; unset f` は採らない。利用者が先に置いた `f` を消す。 + +### 決定 4: 読み込み器は `DEVBASE_SHELLRC_DIR` を読み、空なら `~/.shellrc.d` にする + +ツールがファイルを置く先と、読み込み器が読む先を、同じ 1 つの値から決める。利用者が変数を +別の場所へ向ければ、置く側と読む側がそろってそちらへ移る。変数が空になる経路(`env -i` で +開いたシェルなど)でも、既定の置き場所は読まれる。 + +読み込み器がパスを固定で持つ形は採らない。変数と読む先が別々に決まり、食い違いうる。 +なお、変数を既定から変えた先は永続化の対象ではない。文書にそう書く。 + +### 決定 5: zsh は対象にしない + +base に zsh は入っておらず、`~/.zshrc` を読むシェルがいない(要求の前提 1)。devbase 自身の +起動定義も bash だけに効いている。確かめようのない行を `~/.zshrc` へ足すと、壊れていても +誰も気づかない。zsh を入れる変更のときに、起動定義と一緒に読み込みを足す。置き場所の名前は +そのとき変えずに済む(決定 1)。 + +### 決定 6: 置き場所は分類 B(アカウントグループ単位)に置く + +最初の使い手である中継の本体は `~/.claude/ndf/`(分類 B)にある。置き場所を分類 A(全 +コンテナ共通)にすると、中継の本体が無い別のグループのコンテナでも中継を読む 1 行が効き、 +存在しないファイルを読みに行く。認証情報のように、alias の中身がグループの契約に紐づく +場合もある。 + +グループをまたいで効かせたい設定の置き場所は作らない。必要になったら分類 A の置き場所を +別の名前で足す。 + +### 決定 7: `default` グループの初回シードの試行は特別扱いしない + +`DEVBASE_GROUP_SETTINGS` へ足すと、`devbase_seed_group_settings` が +`/persistent/ai/.shellrc.d` からのコピーを試みる。シード元が無いので `skip (シード元なし)` の +行を出す。この行が出るのはグループ側に +`.shellrc.d` がまだ無い最初の起動の 1 回だけで、次からはシードが何もしない。除外の一覧を +新しく持つほどの費用に見合わない。 + +### 決定 8: `containers/lfm` には入れない + +lfm は base を継がず、base から `entrypoint.sh` をコピーするだけである。lfm のコンテナでも +`~/.shellrc.d` の symlink は張られるが、`~/.bashrc` と `ENV` は lfm 自身の Dockerfile が +持つので読まれない。lfm は起動定義も base と別に `~/.bashrc` へ直書きしており、base に +そろえる変更は別の課題になる(PLAN67 の決定 5 と同じ扱い)。 + +## テスト設計 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| 1. symlink と所有者 | 関数: `test_group_entries_point_at_the_group_volume` の一覧に `.shellrc.d`。実機: 建て直したイメージのコンテナで `readlink ~/.shellrc.d` と `stat -c %U` | +| 2. 作り直しで残る | 実機: `devbase down` → `devbase up` → `devbase login` で `plan70probe` | +| 3. 同じグループの別のコンテナ・別のグループ | 関数: グループの根を 2 つ用意し、片方に置いたファイルがもう片方の置き場所から見えない。実機: `--index=2` の対話シェル | +| 4. 名前の昇順 | 読み込み器: `10-a.sh` と `20-b.sh` が同じ alias を定義し、`20-b.sh` の定義が残る | +| 5. `*.sh` 以外を読まない | 読み込み器: `x.txt` / `README` / `sub.sh/`(ディレクトリ)/ `.hidden.sh` が読まれない | +| 6. 無い・空・一致なし | 読み込み器: 3 通りで、標準出力と標準エラーが空、終了状態 0 | +| 7. 誤りの後も続く | 読み込み器: 構文の誤りを持つ `15-bad.sh` の後ろの `20-b.sh` が読まれる | +| 8. 起動定義より勝つ | 読み込み器: `ai-cli-aliases.sh` を source した後に読み込み器を source し、置き場所の `alias claude` が残る。Dockerfile: `. /etc/devbase/shellrc-dir.sh` の行が `. /etc/devbase/ai-cli-aliases.sh` の行より後 | +| 9. 変数を残さない | 読み込み器: source の前に `f=keep` を置き、後で `f` が `keep`、`__devbase_` で始まる変数が無い | +| 10. 変数と既定 | 読み込み器: 変数で指した場所を読む。変数が空のとき `$HOME/.shellrc.d` を読む | +| 11. 非対話で見える | Dockerfile: `ENV DEVBASE_SHELLRC_DIR=/home/${USERNAME}/.shellrc.d`。実機: `docker exec printenv DEVBASE_SHELLRC_DIR` | +| 12. 起動定義が変わらない | 既存の `tests/containers/test_ai_cli_aliases.py` | +| 13. 既存のエントリとシード | 既存の `tests/containers/test_entrypoint_ai_settings.py` | +| 14. `~/.zshrc` が変わらない | Dockerfile: `.zshrc` へ書き込む行が無い。実機: 建て直した前後の `cat ~/.zshrc` | +| 15. 全体テスト | `uv run --locked pytest tests/ -q` | +| 16. 建つ | `devbase build base --no-cache`(arm64) | +| 17. 文書 | 目視(3 か所) | + +読み込み器のテストは `bash --norc -i` ではなく `bash -c` で `shopt -s expand_aliases` を +付けて source する(`test_ai_cli_aliases.py` と同じ方式)。非機能の性能の条件は、`PATH=` +を空にして source し、`command not found` が出ないことで確かめる。 + +## 並行する変更との重なり + +#234(tmux の名指し。別の設計で進行中)は `containers/base/` の tmux の部分と Dockerfile を +触る見込みである。この変更が Dockerfile で触るのは、`ai-cli-aliases.sh` の `COPY` と +`~/.bashrc` へ書く `RUN` である(2026-09-24 の main で 233〜244 行目)。その直後が tmux の +`COPY` 群(246 行目以降)である。**同じ行は触らないが、隣り合う。** 後からマージする側で +差分の位置がずれることがあるので、実装の持ち場は先にマージされた側の上へ載せ直してから +テストを通す。 + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| amd64 での建て直し | 手元は arm64 のみ。変更はシェルの断片と symlink の一覧・`ENV` で、アーキテクチャに依存しない | +| Claude Code の Bash からの見え方 | 環境変数(受け入れ条件 11)までを確かめる。Claude Code が対話シェルの設定を取り込むかは Claude Code 側の振る舞いで、範囲外 | +| 名前の連絡 | ai-plugins#928 へ決まった名前を知らせるのは実装の持ち場以降 | diff --git a/issues/PLAN70_shellrc-dir.md b/issues/PLAN70_shellrc-dir.md new file mode 100644 index 0000000..eedbafd --- /dev/null +++ b/issues/PLAN70_shellrc-dir.md @@ -0,0 +1,210 @@ +# PLAN70: コンテナを作り直しても残るシェルの設定の読み込み先を用意する + +対象 issue: devbasex/devbase#253 + +- ワークフローモード: `standard` + - 根拠: base イメージの本番の振る舞い(対話シェルの初期化)と、entrypoint が張る永続化の + symlink を変える。base はすべての派生イメージとプロジェクトの土台で、変更は建て直した + 全員に届く(前例: PLAN67 / #249) +- ベースブランチ: `main` + +## 目的 + +- **利用者やツールが足したシェルの設定が、コンテナの作り直しで消えない。** 置き場所の + ディレクトリへ `*.sh` を 1 つ置けば、以後に開く対話シェルで読み込まれる。対話シェルは + `devbase login` / tmux の窓 / VS Code の端末である。`devbase down` → `devbase up` の後も + 読み込まれ続ける +- **ツールが置き場所を見つけられる。** 対話シェルでない処理(Claude Code の中の Bash など) + からも、環境変数で置き場所のパスを知れる。ツールは `~/.bashrc` を書き換えなくてよい +- 最初の使い手は ai-plugins の中継(devbasex/ai-plugins#928)で、決まった名前をその実装が使う + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | **増える。** コンテナの環境変数 `DEVBASE_SHELLRC_DIR` と、置き場所 `~/.shellrc.d/`。ai-plugins#928 がこの 2 つを使う。名前の決定は設計の決定 1・2 | +| データ | **増える。** アカウントグループのボリューム(`/persistent/group`)に `.shellrc.d/` ができる。既存のエントリは変わらない | +| 既存の振る舞い | **変わる。** base とその派生イメージの `~/.bashrc` が、末尾で置き場所の `*.sh` を読む。置き場所が空なら見かけの振る舞いは変わらない | +| 利用者の操作 | **`devbase build base --no-cache` が要る。`devbase up` だけでは反映されない**(`entrypoint.sh` と `~/.bashrc` はイメージの中にある)。派生イメージを使うプロジェクトはその派生イメージも建て直し、稼働中のコンテナは `devbase down` → `devbase up` で作り直す | + +## 前提 + +- **前提 1: 対話シェルは bash だけである。** base に zsh は入っていない(2026-09-24 の + `devbase-base:latest` で `command -v zsh` が空)。`~/.zshrc` はインストーラ(uv / agy)が + PATH の行を書くために作ったもので、読むシェルがいない。devbase 自身の起動定義 + (`/etc/devbase/ai-cli-aliases.sh`)も `~/.bashrc` からしか読まれていない +- **前提 2: `devbase login` は `docker compose exec ... bash` で、対話の非ログインシェルを + 開く。** `~/.bashrc` が読まれる。tmux の窓はログインシェルで、`~/.profile` が `~/.bashrc` を読む +- **前提 3: `~/.bashrc` は先頭で非対話シェルを帰す**(Ubuntu の既定の `case $- in *i*)`)。 + 置き場所の `*.sh` が読まれるのは対話シェルだけである。非対話の処理へ届けるのは環境変数 + だけにする +- **前提 4: 対象は `FROM devbase-base:latest` の派生イメージに限る。** `containers/lfm` は + base の `entrypoint.sh` をコピーするだけで `~/.bashrc` も `ENV` も継がない(PLAN67 の + 前提 3 と同じ) +- **前提 5: CI はイメージを建てない。** イメージの中でしか確かめられない条件の証跡は、手元で + 建てたイメージから採って Pull Request 本文へ載せる + +## 対象範囲 + +含む: + +- 置き場所のディレクトリを、分類 B(アカウントグループ単位)の永続化のエントリに加えること +- 置き場所の `*.sh` を読む処理(読み込み器)を base イメージへ置き、`~/.bashrc` の末尾から + `ai-cli-aliases.sh` の**後に**読むこと +- 置き場所のパスを示すコンテナの環境変数 +- 回帰テスト(`tests/containers/`)、利用者向け文書、CHANGELOG + +含まない: + +- `~/.zshrc` への読み込みの追加(前提 1。設計の決定 5) +- ai-plugins 側の実装(中継の本体・`~/.shellrc.d/` へ置くファイル。ai-plugins#928)と、中継の + alias が `claude --dangerously-skip-permissions` を上書きする件(ai-plugins#936) +- ai-plugins#928 への名前の連絡(実装の持ち場以降で行う) +- `containers/lfm` と `containers/snapshot`(前提 4) +- 分類 A(全コンテナ共通)の置き場所。グループをまたいで効かせたい設定の置き場所は作らない +- 置き場所へ最初から入れておくファイル(雛形・例) + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| 置き場所 | 読み込み用のディレクトリ。コンテナの中では `~/.shellrc.d/`、実体は `/persistent/group/.shellrc.d/` | +| 読み込み器 | 置き場所の `*.sh` を名前の順に読むシェルの断片。base イメージが `/etc/devbase/` に置く | +| 分類 B | `entrypoint.sh` の `DEVBASE_GROUP_SETTINGS`。アカウントグループ単位で `/persistent/group` へ symlink するエントリ | + +## 受け入れ条件 + +**実測の基準**: 以下の「現状」は 2026-09-24 に手元の `devbase-base:latest`(arm64、作成 +2026-09-24T00:46Z)で採った。 + +### 残ること(#253 の中心) + +- [ ] 1. 建て直したイメージで作ったコンテナで、`~/.shellrc.d` が `/persistent/group/.shellrc.d` + を指す symlink である。その先は開発ユーザー(`ubuntu`)の所有のディレクトリである。 + 現状は `~/.shellrc.d` が無い +- [ ] 2. **前提**: コンテナの `~/.shellrc.d/` に `alias plan70probe='echo kept'` を書いた + `plan70.sh` がある + **操作**: `devbase down` → `devbase up` で作り直し、`devbase login` で入る + **結果**: `plan70probe` が `kept` を出す +- [ ] 3. 同じアカウントグループの別のコンテナ(`--index=2` など)の対話シェルでも、2 の + `plan70probe` が効く。別のアカウントグループのコンテナでは効かない + 検証: entrypoint の関数のテストで、置き場所がグループの根の下へ張られることを固定する。 + 実機ではグループを 1 つ確かめる + +### 読み込み + +- [ ] 4. 置き場所の `*.sh` は、ファイル名の昇順で全部読まれる。`10-a.sh` と `20-b.sh` が同じ + alias を定義すると、`20-b.sh` の定義が残る +- [ ] 5. 名前が `*.sh` でないファイル(`x.txt` / `README`)、`.` で始まる名前のファイル、ディレクトリは読まれない +- [ ] 6. 置き場所が無い・空・`*.sh` が 1 つも無いとき、対話シェルの起動は何も出力せず、 + 直後の `$?` が 0 である +- [ ] 7. 1 つのファイルが構文の誤りで失敗しても、名前の順で後ろのファイルは読まれる + (誤りの行は標準エラーに出てよい) +- [ ] 8. 置き場所のファイルで `alias claude=...` を定義すると、`/etc/devbase/ai-cli-aliases.sh` + の定義より勝つ(読む順が `ai-cli-aliases.sh` の後である) +- [ ] 9. 読み込みの後、読み込み器が使った変数がシェルに残らず、利用者が先に置いた同じ名前でない + 変数(たとえば `f`)の値も変わらない +- [ ] 10. 置き場所のパスは `DEVBASE_SHELLRC_DIR` が指すディレクトリで、変数が空か未設定なら + `$HOME/.shellrc.d` である + +### 見つけ方 + +- [ ] 11. 建て直したイメージで作ったコンテナで、**非対話**の次のコマンドが + `/home/ubuntu/.shellrc.d` を出す。現状は何も出さず終了コード 1 + `docker exec printenv DEVBASE_SHELLRC_DIR` + +### 退行しないこと + +- [ ] 12. `/etc/devbase/ai-cli-aliases.sh` の定義と起動オプションは変わらない(既存の + `tests/containers/test_ai_cli_aliases.py` が通る) +- [ ] 13. 分類 A・B の既存のエントリの張り先と、`default` グループの初回シードの結果は変わらない + (既存の `tests/containers/test_entrypoint_ai_settings.py` が通る) +- [ ] 14. `~/.zshrc` は変わらない +- [ ] 15. `uv run --locked pytest tests/ -q` が終了コード 0 +- [ ] 16. `devbase build base --no-cache` が arm64 で成功する +- [ ] 17. 次の 3 か所に置き場所と `DEVBASE_SHELLRC_DIR` がある。いずれも**反映に + `devbase build base --no-cache` が要る**ことを書いている(表の行を除く) + - `docs/user/container-operations.md` の「AI 設定の永続化」の分類 B の表の行 + - 同じ文書の新しい小節(置き場所の使い方・読む順・対話シェルだけで読まれること) + - `CHANGELOG.md` の `[Unreleased]` の `### Added` + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --locked pytest tests/ -q`(受け入れ条件 4〜10・12〜15 と、1・3 の関数の部分) | +| イメージの中 | `devbase build base --no-cache` の後、`docker run --rm --entrypoint /bin/bash devbase-base:latest -c '...'` と `docker exec`(受け入れ条件 1・11・14)。出力を Pull Request 本文へ貼る | +| 実機の手順 | 受け入れ条件 2・3 は、実際のプロジェクトで `devbase down` → `devbase up` を挟んで確かめる | +| CI | **イメージを建てない**(前提 5)。CI が緑でも受け入れ条件 1・2・3・11・16 は確かめていない | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | base イメージの構成物は `containers/base/` の直下に置く。永続化のエントリは `entrypoint.sh` の一覧で持つ(PLAN39) | +| コーディング規約 | `containers/base/Dockerfile` と `entrypoint.sh` の既存の書き方に合わせる。理由を直前のコメントに書く | +| テスト戦略 | Docker に依存しない検査を既定にする。読み込み器は `test_ai_cli_aliases.py` と同じく一時ディレクトリで source して確かめ、entrypoint は `DEVBASE_ENTRYPOINT_LIB_ONLY=1` で関数を呼ぶ(`test_entrypoint_ai_settings.py`) | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 手元で全体テスト、`devbase build base --no-cache` と、作り直したコンテナでの確認 | +| 確認してから行う | 名前(`~/.shellrc.d`・`DEVBASE_SHELLRC_DIR`)と、zsh を含めないこと。設計 Pull Request の承認で確かめる | +| 行わない | ai-plugins 側の実装、`~/.zshrc` の変更、lfm への導入、置き場所への既定ファイルの配置 | + +## 実装計画 + +設計は [PLAN70_shellrc-dir-design.md](PLAN70_shellrc-dir-design.md)。 +**タスクへの分解は実装の持ち場で `/ndf:implementation-plan` が行う。** + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| amd64 での建て直し | 手元は arm64 のみ。変更はシェルの断片と symlink の一覧で、アーキテクチャに依存しない | +| Claude Code の Bash からの見え方 | 環境変数(受け入れ条件 11)までを確かめる。Claude Code が対話シェルの設定を取り込むかは Claude Code 側の振る舞いで、この変更の範囲外 | + +## 依頼(原文) + +#253: + +> ## 何をしたいか +> +> コンテナを作り直しても残るシェルの設定の置き場所(読み込み用のディレクトリ)を 1 つ用意し、`~/.bashrc` と `~/.zshrc` がその中の `*.sh` を読み込むようにしてほしい。 +> +> 今は `~/.bashrc` と `~/.zshrc` がイメージの層にあり、コンテナを作り直すと Dockerfile が書いた中身へ戻る。利用者やツールが足した alias は作り直しで消える。 +> +> ## 例: ai-plugins の中継(devbasex/ai-plugins#928) +> +> ai-plugins の ndf プラグインは、`claude` を中継(`relay.py`)で包む alias を、利用者の明示の操作(`/ndf:install-wrapper`)で入れる。中継の本体は `~/.claude/ndf/relay.py` に置く(`~/.claude` は `/persistent/group/.claude` への symlink で、作り直しでも残る)。alias の行は `~/.claude/ndf/shellrc` に書く。 +> +> 足りないのは、この `shellrc` を読み込む 1 行を、作り直しで消えない形で置く場所である。`~/.bashrc` に書くと作り直しで消える。 +> +> ## 提案 +> +> | 項目 | 提案 | +> | --- | --- | +> | 置き場所 | 分類 B(アカウントグループ単位)の新しい項目として `~/.shellrc.d/` を `/persistent/group/.shellrc.d/` へ張る(`entrypoint.sh` の `DEVBASE_GROUP_SETTINGS` に足す)。ndf の中継の本体が `~/.claude`(分類 B)にあるので、同じグループの単位にそろえる | +> | 読み込み | Dockerfile が `~/.bashrc` と `~/.zshrc` の末尾へ、`. /etc/devbase/ai-cli-aliases.sh` の**後に**次を足す: `for f in "$HOME"/.shellrc.d/*.sh; do [ -r "$f" ] && . "$f"; done; unset f`(zsh では一致が無いときの誤りを避けるため `setopt local_options null_glob` 相当の扱いが要る) | +> | 見つけ方 | 読み込むディレクトリのパスを、コンテナの環境変数 `DEVBASE_SHELLRC_DIR` で示す(対話シェルでない処理、たとえば Claude Code の中の Bash からも見えるように、rc ではなくコンテナの環境に置く)。ツールはこの変数があればそこへ 1 ファイルを置き、`~/.bashrc` を書き換えない | +> +> **汎用にする理由:** 読み込むのは ndf 固有のファイルではなく、ディレクトリの中の `*.sh` 全部にする。devbase は 4 つの AI CLI と利用者自身の設定を載せるため、ndf のパス(`~/.claude/ndf/shellrc`)を devbase に書くと、ndf の置き場所が変わるたびに devbase を直すことになる。ndf 側はこのディレクトリへ `ndf-relay.sh`(`shellrc` を読む 1 行)を置く。 +> +> 名前(`~/.shellrc.d`・`DEVBASE_SHELLRC_DIR`)は案で、devbase の命名に合わせて決めてよい。決まった名前を ai-plugins#928 の実装が使う。 +> +> ## 気をつけること +> +> - **読み込みの順序:** `ai-cli-aliases.sh` の後に読むと、利用者の定義が devbase の定義より勝つ。ndf の中継の alias が `claude --dangerously-skip-permissions` を上書きする件は ai-plugins 側の不具合として devbasex/ai-plugins#936 で扱う +> - 分類 B なので、同じアカウントグループの全コンテナで同じ設定が効く +> +> ## 由来 +> +> devbasex/ai-plugins#928(設計 PR devbasex/ai-plugins#932 の関門 1 で、利用者が「中継の alias を永続化される rc ファイルへ書き、devbase がそれを読み込む」と決めた。2026-09-23) From 72dd4e7857aafe8a1873ccb867b39b615478979f Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 24 Sep 2026 11:00:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs(PLAN70):=20failglob=20=E3=81=AE?= =?UTF-8?q?=E6=89=B1=E3=81=84=E3=83=BB=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97?= =?UTF-8?q?=E5=88=86=E9=9B=A2=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E5=85=88?= =?UTF-8?q?=E3=83=BB=E3=83=9C=E3=83=AA=E3=83=A5=E3=83=BC=E3=83=A0=E6=A7=8B?= =?UTF-8?q?=E9=80=A0=E3=81=AE=E8=A1=A8=E3=82=92=E8=A8=AD=E8=A8=88=E3=81=B8?= =?UTF-8?q?=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #253 Co-Authored-By: Claude Opus 5.5 (1M context) --- issues/PLAN70_shellrc-dir-design.md | 28 +++++++++++++++++++--------- issues/PLAN70_shellrc-dir.md | 11 +++++++---- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/issues/PLAN70_shellrc-dir-design.md b/issues/PLAN70_shellrc-dir-design.md index 2ebca6f..dca9176 100644 --- a/issues/PLAN70_shellrc-dir-design.md +++ b/issues/PLAN70_shellrc-dir-design.md @@ -22,8 +22,8 @@ | `~/.bashrc` への 1 行(同じ Dockerfile の `RUN`) | 変える | `. /etc/devbase/ai-cli-aliases.sh` の行の**次**へ `. /etc/devbase/shellrc-dir.sh` を足す(決定 3) | | 環境変数(同じ Dockerfile の `ENV`) | 足す | `ENV DEVBASE_SHELLRC_DIR=/home/${USERNAME}/.shellrc.d` を読み込み器の `COPY` の直前に置く(決定 2) | | `tests/containers/test_shellrc_dir.py`(新設) | 足す | 読み込み器を一時ディレクトリで source し、読む順・対象・失敗の扱い・変数の後始末を固定する。Dockerfile の文字列で配置と順序を固定する | -| `tests/containers/test_entrypoint_ai_settings.py` | 変える | 分類 B の張り先の検査(`test_group_entries_point_at_the_group_volume`)の一覧へ `.shellrc.d` を足す | -| `docs/user/container-operations.md` | 変える | 「AI 設定の永続化」の分類 B の表へ行を足す。新しい小節「作り直しても残るシェルの設定」を「AI CLI の起動定義」の後に立てる | +| `tests/containers/test_entrypoint_ai_settings.py` | 変える | 分類 B の張り先の検査(`test_group_entries_point_at_the_group_volume`)の一覧へ `.shellrc.d` を足す。グループの分離の検査(`test_two_groups_share_assets_but_not_credentials`)へ、片方のグループの置き場所に置いたファイルがもう片方から見えないことを足す | +| `docs/user/container-operations.md` | 変える | 「ボリューム構造」の表の `devbase_home_{group}` の行の用途へ置き場所を足す。「AI 設定の永続化」の分類 B の表へ行を足す。新しい小節「作り直しても残るシェルの設定」を「AI CLI の起動定義」の後に立てる | | `CHANGELOG.md` | 変える | `[Unreleased]` の `### Added` に 1 項目足す | 次のものは変えない。 @@ -116,11 +116,13 @@ sequenceDiagram alt dir がディレクトリでない L-->>SH: 何もしない else ディレクトリである + L->>L: failglob の状態を控えて切る loop dir/*.sh を名前の順に L->>L: 通常ファイルで読めるなら source end - L->>L: 使った変数を unset + L->>L: failglob を控えた状態へ戻す end + L->>L: 使った変数を unset ``` 読み込み器の中身は次の形にする。**外部コマンドを起動しない**(非機能の条件)。 @@ -129,17 +131,24 @@ sequenceDiagram # 置き場所の *.sh を名前の順に読む。対話シェルの ~/.bashrc から読まれる。 __devbase_shellrc_dir="${DEVBASE_SHELLRC_DIR:-$HOME/.shellrc.d}" if [ -d "$__devbase_shellrc_dir" ]; then + __devbase_shellrc_glob="$(shopt -p failglob)" + shopt -u failglob for __devbase_shellrc_file in "$__devbase_shellrc_dir"/*.sh; do if [ -f "$__devbase_shellrc_file" ] && [ -r "$__devbase_shellrc_file" ]; then . "$__devbase_shellrc_file" fi done + eval "$__devbase_shellrc_glob" fi -unset __devbase_shellrc_dir __devbase_shellrc_file +unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_glob ``` - 一致が無いとき bash のグロブは文字列のまま残る。`-f` の判定で落ちるので、`nullglob` を - 切り替えずに済む。利用者のシェルの設定を読み込み器が変えない + 切り替えずに済む。`nullglob` が有効なら繰り返しが 0 回になるだけで、結果は同じである +- `failglob` だけは控えて切る。有効なまま一致が無いと、bash は `no match` を標準エラーへ出して + `for` を実行しない(受け入れ条件 6)。控えた `shopt -p` の出力を `eval` して元の状態へ戻すので、 + 利用者のシェルの設定は読み込みの前後で変わらない。置き場所のファイルは `failglob` が切れた + 状態で読まれる - `if` で包むのは、最後のファイルが読めないときに `&&` の連なりが非 0 を残さないためである。 末尾の `unset` で終了状態は 0 になる(受け入れ条件 6) - 変数名を `__devbase_` で始めるのは、利用者の変数(`f` など)を上書きしないためである @@ -153,13 +162,14 @@ unset __devbase_shellrc_dir __devbase_shellrc_file | 構文の誤りを持つ `15-bad.sh` | 標準エラーに誤りを出し、次のファイルへ進んだ | | `x.txt` | 読まれなかった | | 置き場所が無い / 空 | 何も出さず、終了状態 0 | +| 空の置き場所を `shopt -s failglob` の下で読む | 何も出さず終了状態 0。読んだ後も `failglob` は `on`。控えて切る処理が無い形では `no match` を出し終了状態 1 だった | ## 非機能の実現方式 | 大項目 | 要求の条件 | 実現方式 | 確かめ方 | | --- | --- | --- | --- | | セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | 実体を `~/.claude` と同じ `/persistent/group` に置き、所有者は既存の `devbase_ensure_entry` と同じく開発ユーザーにする。イメージと entrypoint は置き場所へファイルを書かない | 受け入れ条件 1 の実機で所有者を見る。テストで、entrypoint の後の置き場所が空であることを見る | -| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器を組み込みの `[`・`for`・`.`・`unset` だけで書く | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器をシェルの組み込み(`[`・`for`・`.`・`shopt`・`eval`・`unset`)だけで書く。`failglob` を控える `$(shopt -p failglob)` はサブシェルを作るが、外部コマンドは起動しない | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | ## 決定の記録 @@ -241,10 +251,10 @@ lfm は base を継がず、base から `entrypoint.sh` をコピーするだけ | --- | --- | | 1. symlink と所有者 | 関数: `test_group_entries_point_at_the_group_volume` の一覧に `.shellrc.d`。実機: 建て直したイメージのコンテナで `readlink ~/.shellrc.d` と `stat -c %U` | | 2. 作り直しで残る | 実機: `devbase down` → `devbase up` → `devbase login` で `plan70probe` | -| 3. 同じグループの別のコンテナ・別のグループ | 関数: グループの根を 2 つ用意し、片方に置いたファイルがもう片方の置き場所から見えない。実機: `--index=2` の対話シェル | +| 3. 同じグループの別のコンテナ・別のグループ | 関数: `test_two_groups_share_assets_but_not_credentials` で、片方のグループの置き場所に置いたファイルがもう片方の置き場所から見えない。実機: `--index=2` の対話シェル | | 4. 名前の昇順 | 読み込み器: `10-a.sh` と `20-b.sh` が同じ alias を定義し、`20-b.sh` の定義が残る | | 5. `*.sh` 以外を読まない | 読み込み器: `x.txt` / `README` / `sub.sh/`(ディレクトリ)/ `.hidden.sh` が読まれない | -| 6. 無い・空・一致なし | 読み込み器: 3 通りで、標準出力と標準エラーが空、終了状態 0 | +| 6. 無い・空・一致なし | 読み込み器: 3 通りで、標準出力と標準エラーが空、終了状態 0。`shopt -s failglob` の下で空の置き場所を読んでも同じで、読んだ後の `shopt failglob` が `on` | | 7. 誤りの後も続く | 読み込み器: 構文の誤りを持つ `15-bad.sh` の後ろの `20-b.sh` が読まれる | | 8. 起動定義より勝つ | 読み込み器: `ai-cli-aliases.sh` を source した後に読み込み器を source し、置き場所の `alias claude` が残る。Dockerfile: `. /etc/devbase/shellrc-dir.sh` の行が `. /etc/devbase/ai-cli-aliases.sh` の行より後 | | 9. 変数を残さない | 読み込み器: source の前に `f=keep` を置き、後で `f` が `keep`、`__devbase_` で始まる変数が無い | @@ -255,7 +265,7 @@ lfm は base を継がず、base から `entrypoint.sh` をコピーするだけ | 14. `~/.zshrc` が変わらない | Dockerfile: `.zshrc` へ書き込む行が無い。実機: 建て直した前後の `cat ~/.zshrc` | | 15. 全体テスト | `uv run --locked pytest tests/ -q` | | 16. 建つ | `devbase build base --no-cache`(arm64) | -| 17. 文書 | 目視(3 か所) | +| 17. 文書 | 目視(4 か所) | 読み込み器のテストは `bash --norc -i` ではなく `bash -c` で `shopt -s expand_aliases` を 付けて source する(`test_ai_cli_aliases.py` と同じ方式)。非機能の性能の条件は、`PATH=` diff --git a/issues/PLAN70_shellrc-dir.md b/issues/PLAN70_shellrc-dir.md index eedbafd..c549ebd 100644 --- a/issues/PLAN70_shellrc-dir.md +++ b/issues/PLAN70_shellrc-dir.md @@ -97,7 +97,8 @@ alias を定義すると、`20-b.sh` の定義が残る - [ ] 5. 名前が `*.sh` でないファイル(`x.txt` / `README`)、`.` で始まる名前のファイル、ディレクトリは読まれない - [ ] 6. 置き場所が無い・空・`*.sh` が 1 つも無いとき、対話シェルの起動は何も出力せず、 - 直後の `$?` が 0 である + 直後の `$?` が 0 である。読み込みの前に `shopt -s failglob` が有効でも同じである。 + 読み込みの後、`failglob` は読み込みの前の状態に戻っている - [ ] 7. 1 つのファイルが構文の誤りで失敗しても、名前の順で後ろのファイルは読まれる (誤りの行は標準エラーに出てよい) - [ ] 8. 置き場所のファイルで `alias claude=...` を定義すると、`/etc/devbase/ai-cli-aliases.sh` @@ -122,9 +123,11 @@ - [ ] 14. `~/.zshrc` は変わらない - [ ] 15. `uv run --locked pytest tests/ -q` が終了コード 0 - [ ] 16. `devbase build base --no-cache` が arm64 で成功する -- [ ] 17. 次の 3 か所に置き場所と `DEVBASE_SHELLRC_DIR` がある。いずれも**反映に - `devbase build base --no-cache` が要る**ことを書いている(表の行を除く) - - `docs/user/container-operations.md` の「AI 設定の永続化」の分類 B の表の行 +- [ ] 17. 次の 4 か所に置き場所がある。表の 2 か所を除く 2 か所は、`DEVBASE_SHELLRC_DIR` と、 + **反映に `devbase build base --no-cache` が要る**ことを書いている + - `docs/user/container-operations.md` の「ボリューム構造」の表の `devbase_home_{group}` の行 + (用途の列) + - 同じ文書の「AI 設定の永続化」の分類 B の表の行 - 同じ文書の新しい小節(置き場所の使い方・読む順・対話シェルだけで読まれること) - `CHANGELOG.md` の `[Unreleased]` の `### Added` From a1f30d92c65ca2ce5e80fe79216daa126d630a8e Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 24 Sep 2026 11:13:47 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs(PLAN70):=20=E3=82=B0=E3=83=AD=E3=83=96?= =?UTF-8?q?=E3=81=AE=E5=B1=95=E9=96=8B=E3=81=AE=E9=96=93=E3=81=A0=E3=81=91?= =?UTF-8?q?=20failglob=20/=20dotglob=20=E3=82=92=E5=88=87=E3=82=8A?= =?UTF-8?q?=E3=80=81=E8=AA=AD=E3=82=80=E5=89=8D=E3=81=AB=E6=88=BB=E3=81=99?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #253 Co-Authored-By: Claude Opus 5.5 (1M context) --- issues/PLAN70_shellrc-dir-design.md | 39 ++++++++++++++++++----------- issues/PLAN70_shellrc-dir.md | 8 +++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/issues/PLAN70_shellrc-dir-design.md b/issues/PLAN70_shellrc-dir-design.md index dca9176..824f224 100644 --- a/issues/PLAN70_shellrc-dir-design.md +++ b/issues/PLAN70_shellrc-dir-design.md @@ -116,11 +116,12 @@ sequenceDiagram alt dir がディレクトリでない L-->>SH: 何もしない else ディレクトリである - L->>L: failglob の状態を控えて切る - loop dir/*.sh を名前の順に + L->>L: failglob と dotglob を控えて切る + L->>L: dir/*.sh を配列へ展開する + L->>L: failglob と dotglob を控えた状態へ戻す + loop 配列を名前の順に L->>L: 通常ファイルで読めるなら source end - L->>L: failglob を控えた状態へ戻す end L->>L: 使った変数を unset ``` @@ -131,24 +132,27 @@ sequenceDiagram # 置き場所の *.sh を名前の順に読む。対話シェルの ~/.bashrc から読まれる。 __devbase_shellrc_dir="${DEVBASE_SHELLRC_DIR:-$HOME/.shellrc.d}" if [ -d "$__devbase_shellrc_dir" ]; then - __devbase_shellrc_glob="$(shopt -p failglob)" - shopt -u failglob - for __devbase_shellrc_file in "$__devbase_shellrc_dir"/*.sh; do + __devbase_shellrc_opts="$(shopt -p failglob dotglob)" + shopt -u failglob dotglob + __devbase_shellrc_files=("$__devbase_shellrc_dir"/*.sh) + eval "$__devbase_shellrc_opts" + for __devbase_shellrc_file in "${__devbase_shellrc_files[@]}"; do if [ -f "$__devbase_shellrc_file" ] && [ -r "$__devbase_shellrc_file" ]; then . "$__devbase_shellrc_file" fi done - eval "$__devbase_shellrc_glob" fi -unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_glob +unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_files __devbase_shellrc_opts ``` - 一致が無いとき bash のグロブは文字列のまま残る。`-f` の判定で落ちるので、`nullglob` を 切り替えずに済む。`nullglob` が有効なら繰り返しが 0 回になるだけで、結果は同じである -- `failglob` だけは控えて切る。有効なまま一致が無いと、bash は `no match` を標準エラーへ出して - `for` を実行しない(受け入れ条件 6)。控えた `shopt -p` の出力を `eval` して元の状態へ戻すので、 - 利用者のシェルの設定は読み込みの前後で変わらない。置き場所のファイルは `failglob` が切れた - 状態で読まれる +- グロブの展開の間だけ `failglob` と `dotglob` を切る。`failglob` が有効なまま一致が無いと、 + bash は `no match` を標準エラーへ出して展開した文を実行しない(受け入れ条件 6)。`dotglob` が + 有効だと `*.sh` が `.` で始まる名前にも一致する(受け入れ条件 5) +- 展開の結果を配列へ移し、**読む前に**控えた `shopt -p` の出力を `eval` して元へ戻す。置き場所の + ファイルは利用者の設定のまま読まれ、ファイルの中で変えた設定は読み込みの後も残る + (受け入れ条件 6a) - `if` で包むのは、最後のファイルが読めないときに `&&` の連なりが非 0 を残さないためである。 末尾の `unset` で終了状態は 0 になる(受け入れ条件 6) - 変数名を `__devbase_` で始めるのは、利用者の変数(`f` など)を上書きしないためである @@ -163,13 +167,17 @@ unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_glob | `x.txt` | 読まれなかった | | 置き場所が無い / 空 | 何も出さず、終了状態 0 | | 空の置き場所を `shopt -s failglob` の下で読む | 何も出さず終了状態 0。読んだ後も `failglob` は `on`。控えて切る処理が無い形では `no match` を出し終了状態 1 だった | +| `.hidden.sh` を `shopt -s dotglob` の下で読む | 読まれなかった。読んだ後も `dotglob` は `on` | +| `failglob` が切れた状態で、`shopt -s failglob` を実行する `10-set.sh` と、状態を出す `20-show.sh` | `20-show.sh` は `on` を出し、読んだ後も `on` | + +同じ 3 行は macOS の bash 3.2 でも同じ結果だった(テストはホストの bash でも走る)。 ## 非機能の実現方式 | 大項目 | 要求の条件 | 実現方式 | 確かめ方 | | --- | --- | --- | --- | | セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | 実体を `~/.claude` と同じ `/persistent/group` に置き、所有者は既存の `devbase_ensure_entry` と同じく開発ユーザーにする。イメージと entrypoint は置き場所へファイルを書かない | 受け入れ条件 1 の実機で所有者を見る。テストで、entrypoint の後の置き場所が空であることを見る | -| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器をシェルの組み込み(`[`・`for`・`.`・`shopt`・`eval`・`unset`)だけで書く。`failglob` を控える `$(shopt -p failglob)` はサブシェルを作るが、外部コマンドは起動しない | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器をシェルの組み込み(`[`・`for`・`.`・`shopt`・`eval`・`unset`)だけで書く。設定を控える `$(shopt -p failglob dotglob)` はサブシェルを作るが、外部コマンドは起動しない | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | ## 決定の記録 @@ -253,8 +261,9 @@ lfm は base を継がず、base から `entrypoint.sh` をコピーするだけ | 2. 作り直しで残る | 実機: `devbase down` → `devbase up` → `devbase login` で `plan70probe` | | 3. 同じグループの別のコンテナ・別のグループ | 関数: `test_two_groups_share_assets_but_not_credentials` で、片方のグループの置き場所に置いたファイルがもう片方の置き場所から見えない。実機: `--index=2` の対話シェル | | 4. 名前の昇順 | 読み込み器: `10-a.sh` と `20-b.sh` が同じ alias を定義し、`20-b.sh` の定義が残る | -| 5. `*.sh` 以外を読まない | 読み込み器: `x.txt` / `README` / `sub.sh/`(ディレクトリ)/ `.hidden.sh` が読まれない | -| 6. 無い・空・一致なし | 読み込み器: 3 通りで、標準出力と標準エラーが空、終了状態 0。`shopt -s failglob` の下で空の置き場所を読んでも同じで、読んだ後の `shopt failglob` が `on` | +| 5. `*.sh` 以外を読まない | 読み込み器: `x.txt` / `README` / `sub.sh/`(ディレクトリ)/ `.hidden.sh` が読まれない。`shopt -s dotglob` の下でも `.hidden.sh` が読まれない | +| 6. 無い・空・一致なし | 読み込み器: 3 通りで、標準出力と標準エラーが空、終了状態 0。`shopt -s failglob` の下で空の置き場所を読んでも同じ | +| 6a. 利用者の設定のまま読む | 読み込み器: `failglob` と `dotglob` を有効にして読み、読んだ後も両方 `on`。`failglob` が切れた状態で `shopt -s failglob` を実行するファイルを読み、後ろのファイルと読んだ後の両方で `on` | | 7. 誤りの後も続く | 読み込み器: 構文の誤りを持つ `15-bad.sh` の後ろの `20-b.sh` が読まれる | | 8. 起動定義より勝つ | 読み込み器: `ai-cli-aliases.sh` を source した後に読み込み器を source し、置き場所の `alias claude` が残る。Dockerfile: `. /etc/devbase/shellrc-dir.sh` の行が `. /etc/devbase/ai-cli-aliases.sh` の行より後 | | 9. 変数を残さない | 読み込み器: source の前に `f=keep` を置き、後で `f` が `keep`、`__devbase_` で始まる変数が無い | diff --git a/issues/PLAN70_shellrc-dir.md b/issues/PLAN70_shellrc-dir.md index c549ebd..3afebc4 100644 --- a/issues/PLAN70_shellrc-dir.md +++ b/issues/PLAN70_shellrc-dir.md @@ -95,10 +95,12 @@ - [ ] 4. 置き場所の `*.sh` は、ファイル名の昇順で全部読まれる。`10-a.sh` と `20-b.sh` が同じ alias を定義すると、`20-b.sh` の定義が残る -- [ ] 5. 名前が `*.sh` でないファイル(`x.txt` / `README`)、`.` で始まる名前のファイル、ディレクトリは読まれない +- [ ] 5. 名前が `*.sh` でないファイル(`x.txt` / `README`)、`.` で始まる名前のファイル、ディレクトリは読まれない。 + 読み込みの前に `shopt -s dotglob` が有効でも、`.` で始まる名前のファイルは読まれない - [ ] 6. 置き場所が無い・空・`*.sh` が 1 つも無いとき、対話シェルの起動は何も出力せず、 - 直後の `$?` が 0 である。読み込みの前に `shopt -s failglob` が有効でも同じである。 - 読み込みの後、`failglob` は読み込みの前の状態に戻っている + 直後の `$?` が 0 である。読み込みの前に `shopt -s failglob` が有効でも同じである +- [ ] 6a. 置き場所のファイルは、読み込みの前の `failglob` / `dotglob` の状態のまま読まれる。 + 置き場所のファイルが `shopt -s failglob` を実行すると、読み込みの後も `failglob` は有効である - [ ] 7. 1 つのファイルが構文の誤りで失敗しても、名前の順で後ろのファイルは読まれる (誤りの行は標準エラーに出てよい) - [ ] 8. 置き場所のファイルで `alias claude=...` を定義すると、`/etc/devbase/ai-cli-aliases.sh` From 8cfde1b106cf1596f45c19f5e20a23bc2c01ae00 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 24 Sep 2026 11:24:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(PLAN70):=20=E3=82=B0=E3=83=AD=E3=83=96?= =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=AE=E6=8E=A7=E3=81=88=E3=82=92?= =?UTF-8?q?=E3=82=B5=E3=83=96=E3=82=B7=E3=82=A7=E3=83=AB=E7=84=A1=E3=81=97?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=80=81=E6=80=A7=E8=83=BD=E3=81=AE=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E3=81=A8=E5=AE=9F=E6=B8=AC=E3=81=AE=E7=AF=84=E5=9B=B2?= =?UTF-8?q?=E3=82=92=E3=81=9D=E3=82=8D=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #253 Co-Authored-By: Claude Opus 5.5 (1M context) --- issues/PLAN70_shellrc-dir-design.md | 23 +++++++++++++++-------- issues/PLAN70_shellrc-dir.md | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/issues/PLAN70_shellrc-dir-design.md b/issues/PLAN70_shellrc-dir-design.md index 824f224..b168804 100644 --- a/issues/PLAN70_shellrc-dir-design.md +++ b/issues/PLAN70_shellrc-dir-design.md @@ -126,16 +126,21 @@ sequenceDiagram L->>L: 使った変数を unset ``` -読み込み器の中身は次の形にする。**外部コマンドを起動しない**(非機能の条件)。 +読み込み器の中身は次の形にする。**外部コマンドもサブシェルも起動しない**(非機能の条件)。 ```bash # 置き場所の *.sh を名前の順に読む。対話シェルの ~/.bashrc から読まれる。 __devbase_shellrc_dir="${DEVBASE_SHELLRC_DIR:-$HOME/.shellrc.d}" if [ -d "$__devbase_shellrc_dir" ]; then - __devbase_shellrc_opts="$(shopt -p failglob dotglob)" + __devbase_shellrc_opts= + shopt -q failglob && __devbase_shellrc_opts="$__devbase_shellrc_opts failglob" + shopt -q dotglob && __devbase_shellrc_opts="$__devbase_shellrc_opts dotglob" shopt -u failglob dotglob __devbase_shellrc_files=("$__devbase_shellrc_dir"/*.sh) - eval "$__devbase_shellrc_opts" + if [ -n "$__devbase_shellrc_opts" ]; then + # 名前ごとに分けて渡すため、引用符で囲まない + shopt -s $__devbase_shellrc_opts + fi for __devbase_shellrc_file in "${__devbase_shellrc_files[@]}"; do if [ -f "$__devbase_shellrc_file" ] && [ -r "$__devbase_shellrc_file" ]; then . "$__devbase_shellrc_file" @@ -150,9 +155,10 @@ unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_files __dev - グロブの展開の間だけ `failglob` と `dotglob` を切る。`failglob` が有効なまま一致が無いと、 bash は `no match` を標準エラーへ出して展開した文を実行しない(受け入れ条件 6)。`dotglob` が 有効だと `*.sh` が `.` で始まる名前にも一致する(受け入れ条件 5) -- 展開の結果を配列へ移し、**読む前に**控えた `shopt -p` の出力を `eval` して元へ戻す。置き場所の - ファイルは利用者の設定のまま読まれ、ファイルの中で変えた設定は読み込みの後も残る - (受け入れ条件 6a) +- 有効だった設定の名前を `shopt -q` で控え、展開の結果を配列へ移し、**読む前に** `shopt -s` で + 戻す。置き場所のファイルは利用者の設定のまま読まれ、ファイルの中で変えた設定は読み込みの + 後も残る(受け入れ条件 6a)。控えに `$(shopt -p ...)` と `eval` を使わないのは、サブシェルを + 作らないためである(非機能の条件) - `if` で包むのは、最後のファイルが読めないときに `&&` の連なりが非 0 を残さないためである。 末尾の `unset` で終了状態は 0 になる(受け入れ条件 6) - 変数名を `__devbase_` で始めるのは、利用者の変数(`f` など)を上書きしないためである @@ -170,14 +176,15 @@ unset __devbase_shellrc_dir __devbase_shellrc_file __devbase_shellrc_files __dev | `.hidden.sh` を `shopt -s dotglob` の下で読む | 読まれなかった。読んだ後も `dotglob` は `on` | | `failglob` が切れた状態で、`shopt -s failglob` を実行する `10-set.sh` と、状態を出す `20-show.sh` | `20-show.sh` は `on` を出し、読んだ後も `on` | -同じ 3 行は macOS の bash 3.2 でも同じ結果だった(テストはホストの bash でも走る)。 +表のすべての行は、macOS の bash 3.2(`/bin/bash`)でも同じ結果だった。テストはホストの bash +でも走る。`PATH` を空にして空の置き場所を読んでも、両方の bash で何も出さず終了状態 0 だった。 ## 非機能の実現方式 | 大項目 | 要求の条件 | 実現方式 | 確かめ方 | | --- | --- | --- | --- | | セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | 実体を `~/.claude` と同じ `/persistent/group` に置き、所有者は既存の `devbase_ensure_entry` と同じく開発ユーザーにする。イメージと entrypoint は置き場所へファイルを書かない | 受け入れ条件 1 の実機で所有者を見る。テストで、entrypoint の後の置き場所が空であることを見る | -| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | 読み込み器をシェルの組み込み(`[`・`for`・`.`・`shopt`・`eval`・`unset`)だけで書く。設定を控える `$(shopt -p failglob dotglob)` はサブシェルを作るが、外部コマンドは起動しない | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定、グロブの設定 2 つ(`failglob` / `dotglob`)の控えと戻し、1 回のグロブで終わる(外部コマンドもサブシェルも起動しない) | 読み込み器をシェルの組み込み(`[`・`for`・`.`・`shopt`・`unset`)と代入だけで書く。コマンド置換(`$(...)`)とパイプを使わない | 読み込み器のテストで、`PATH` を空にして source しても誤りが出ないことを見る。読み込み器の文字列に `$(`・`` ` ``・`|` が無いことを見る | ## 決定の記録 diff --git a/issues/PLAN70_shellrc-dir.md b/issues/PLAN70_shellrc-dir.md index 3afebc4..18ed497 100644 --- a/issues/PLAN70_shellrc-dir.md +++ b/issues/PLAN70_shellrc-dir.md @@ -138,7 +138,7 @@ | 大項目 | 条件 | | --- | --- | | セキュリティ | 置き場所に書けるのは、同じアカウントグループのボリュームに書ける者だけである。既に `~/.claude`(hooks を含む)へ書ける者と同じ範囲で、新しい書き手を増やさない。devbase は置き場所へ何も書かない | -| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定と 1 回のグロブで終わる(外部コマンドを起動しない) | +| 性能・拡張性 | 置き場所が空のとき、対話シェルの起動にかかる追加の処理は、ディレクトリの有無の判定、グロブの設定 2 つ(`failglob` / `dotglob`)の控えと戻し、1 回のグロブで終わる(外部コマンドもサブシェルも起動しない) | ## 検証手段