From f9107a3267024b8aeebf91e58b6a2e53fadcb12f Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 9 Jun 2026 17:12:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(shell):=20bin/rc=20=E6=96=B0=E8=A8=AD?= =?UTF-8?q?=E3=81=A7=E7=8F=BE=E5=9C=A8=E3=82=B7=E3=82=A7=E3=83=AB=E6=9C=89?= =?UTF-8?q?=E5=8A=B9=E5=8C=96=E3=82=92=E7=B5=B1=E4=B8=80=E3=81=97=20devbas?= =?UTF-8?q?e=20shell-rc=20=E3=82=92=E5=BB=83=E6=AD=A2=20(PLAN31=5F1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 破壊的変更: `devbase shell-rc` サブコマンドを削除。rc パスを print して `source "$(devbase shell-rc)"` する方式 (Python/uv 起動 + コマンド置換) を、 source 用の軽量スクリプト `bin/rc` に置き換える。 - bin/rc: 自身の場所から DEVBASE_ROOT を自己解決し、DEVBASE_ROOT/bin を PATH へ 追加 (冪等) + 補完を読み込む (bash/zsh 対応・init が rc に書く内容と同一)。 `. ~/devbase/bin/rc` で現在のシェルを即時有効化できる。 - bin/devbase / cli.py: shell-rc の dispatch・parser・command list を削除。 shell_rc.py を削除 (get_shell_rc_file は init が使うため utils に残置)。 - install.sh 完了案内・README・getting-started・cli-reference のワンライナー/ 手順を `. bin/rc` に更新。ワンライナーは `curl -fsSL https://dl.basex.jp/i | bash && . ~/devbase/bin/rc`。 - tests/cli/test_bin_rc.py: source で DEVBASE_ROOT/PATH 設定・devbase 解決・ PATH 冪等を検証 (5 件)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 15 +++++++ README.md | 10 ++--- bin/devbase | 4 +- bin/rc | 38 +++++++++++++++++ docs/user/cli-reference.md | 24 ++--------- docs/user/getting-started.md | 16 +++---- install.sh | 4 +- lib/devbase/cli.py | 11 +---- lib/devbase/commands/shell_rc.py | 15 ------- tests/cli/test_bin_rc.py | 73 ++++++++++++++++++++++++++++++++ 10 files changed, 146 insertions(+), 64 deletions(-) create mode 100644 bin/rc delete mode 100644 lib/devbase/commands/shell_rc.py create mode 100644 tests/cli/test_bin_rc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7a5b6..902bf22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ ## [Unreleased] +### Changed +- **シェル有効化を `bin/rc` の source に統一**しました (PLAN31_1)。`devbase init` 後に + いま開いているシェルへ devbase(PATH / 補完)を即時適用するには + `. ~/devbase/bin/rc`(= `source ~/devbase/bin/rc`)を使います。`bin/rc` は自身の + 場所から `DEVBASE_ROOT` を解決するため、Python(uv)起動もコマンド置換 `$(...)` + も不要になり、ワンライナーは + `curl -fsSL https://dl.basex.jp/i | bash && . ~/devbase/bin/rc` で現在のシェルまで + 有効化できます。 + +### Removed +- **`devbase shell-rc` サブコマンドを廃止**しました (PLAN31_1, 破壊的変更)。rc ファイル + パスを print して `source "$(devbase shell-rc)"` する方式は、上記の `. bin/rc` に + 置き換えました。`source "$(devbase shell-rc)"` を使っているスクリプトは + `. /bin/rc` に書き換えてください。 + ### Added - **ワンライナー installer (`install.sh`) を新設**しました (PLAN31_1)。 `curl -fsSL https://dl.basex.jp/i | bash` diff --git a/README.md b/README.md index baa82bf..7b7d009 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,12 @@ devbaseは、Docker Composeを使った再現性の高い開発環境を提供 ### ワンライナーインストール(推奨) ```bash -curl -fsSL https://dl.basex.jp/i | bash +curl -fsSL https://dl.basex.jp/i | bash && . ~/devbase/bin/rc ``` -`~/devbase` に clone(既存なら更新)し、`devbase init` まで自動実行します(uv の自動導入・PATH/補完の登録・`plugins.yml` 生成を含む)。**新しく開くターミナルでは自動で有効**です。 +`~/devbase` に clone(既存なら更新)し `devbase init` まで自動実行(uv の自動導入・PATH/補完の登録・`plugins.yml` 生成を含む)したうえで、末尾の `&& . ~/devbase/bin/rc` で**いま開いている端末**にも devbase を通します(`&&` 以降はパイプのサブシェルではなく呼び出し元シェルで実行されるため、その場で `devbase` が使えます)。新しく開くターミナルは init の rc 追記により自動で有効です。 -**いま開いている端末で即使う**なら、末尾に `&& source "$(~/devbase/bin/devbase shell-rc)"` を付けます(`&&` 以降は呼び出し元シェルで実行されるため、その場で PATH が通ります)。配置先を `DEVBASE_INSTALL_DIR` で変えた場合は同パスに合わせてください。 +> 配置先を `DEVBASE_INSTALL_DIR` で変えた場合は後半の `~/devbase/bin/rc` も同じパスに合わせてください。`. ~/devbase/bin/rc` を省いた `... | bash` だけでも導入は完了します(その場合は完了メッセージの案内に従ってください)。 環境変数で挙動を上書きできます。 @@ -59,7 +59,7 @@ DEVBASE_INSTALL_DIR=~/work/devbase DEVBASE_INSTALL_REF=v1.2.3 \ git clone https://github.com/devbasex/devbase.git cd devbase ./bin/devbase init -source "$(./bin/devbase shell-rc)" # rc ファイル(zsh/bash on Linux/macOS)を自動判定して再読み込み +. ./bin/rc # いまのシェルで devbase を有効化(PATH / 補完) # 2. Pluginのインストール devbase plugin repo add user/repo # リポジトリ登録(init でサンプルレジストリ devbasex/devbase-samples は自動登録済み) @@ -120,7 +120,7 @@ devbaseのコマンドは4つのグループにまとめられています。 - **ショートカット**: `up [name]`, `down [name]`, `login [index]`, `build [image]`, `ps [name]`, `scale [name] `, `list` はトップレベルから直接使用可能(`project` グループへ自動転送。`logs` はシノニムを持ちません)。ただし `build` のみ例外で、`project` グループ(Python 実装)ではなく `bin/devbase` のシェル実装 `cmd_build` へ直接委譲されます(詳細は [CLI リファレンス](docs/user/cli-reference.md#ショートカットコマンド)) - **プレフィックス略記**: `devbase p l` → `devbase plugin list` -- **トップレベルコマンド**: `init`, `status`, `shell-rc` +- **トップレベルコマンド**: `init`, `status` 全コマンドの構文・オプション・使用例は [CLIリファレンス](docs/user/cli-reference.md) を参照してください。 diff --git a/bin/devbase b/bin/devbase index a348391..2cb1b06 100755 --- a/bin/devbase +++ b/bin/devbase @@ -199,7 +199,7 @@ run_python() { # Resolve abbreviated command to full command name via unique prefix matching resolve_command() { local input="$1" - local commands="init status shell-rc project container ct env plugin pl snapshot ss up down login build rebuild ps scale list help" + local commands="init status project container ct env plugin pl snapshot ss up down login build rebuild ps scale list help" local matches=() for cmd in $commands; do [[ "$cmd" == "$input"* ]] && matches+=("$cmd") @@ -355,7 +355,7 @@ case "$_resolved_cmd" in # Python-implemented commands --version|-V) run_python "$@" ;; - init|status|shell-rc|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale|rebuild|list) + init|status|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale|rebuild|list) run_python "${_resolved_cmd}" "${_DEVBASE_ARGS[@]}" ;; # Shell-implemented commands build) cmd_build "${_DEVBASE_ARGS[@]}" ;; diff --git a/bin/rc b/bin/rc new file mode 100644 index 0000000..3ab5235 --- /dev/null +++ b/bin/rc @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# devbase: いま開いているシェルで devbase を有効化する (source して使う)。 +# +# . ~/devbase/bin/rc # bash / zsh 共通 +# +# `devbase init` が rc ファイルに書き込む有効化 (DEVBASE_ROOT / PATH / 補完) を、 +# 現在のシェルへ即時適用する。新しく開くシェルは init が rc に追記したブロックで +# 自動有効化されるため、本ファイルは「今のシェルにも通す」ためのもの。 +# +# 注意: 実行ではなく source して使う。exit せず、シェルのオプションも変更しない。 + +# このファイル自身の場所から DEVBASE_ROOT を解決する。 +# bash は ${BASH_SOURCE[0]}、zsh は $0 (FUNCTION_ARGZERO 既定で source 元のパス)。 +_devbase_rc_src="${BASH_SOURCE[0]:-$0}" +_devbase_bin="$(CDPATH='' cd -- "$(dirname -- "$_devbase_rc_src")" && pwd)" +DEVBASE_ROOT="$(CDPATH='' cd -- "$_devbase_bin/.." && pwd)" +export DEVBASE_ROOT + +# DEVBASE_ROOT/bin を PATH 先頭へ追加 (冪等)。 +case ":$PATH:" in + *":$_devbase_bin:"*) ;; + *) PATH="$_devbase_bin:$PATH"; export PATH ;; +esac + +# シェル補完を現在のシェルへ読み込む (init が rc に書く内容と同一)。 +if [ -n "${ZSH_VERSION:-}" ]; then + # zsh の fpath は配列。$fpath は要素展開させたいので意図的に非クォート。 + # shellcheck disable=SC2206 + fpath=("$DEVBASE_ROOT/etc" $fpath) + autoload -Uz compinit && compinit +elif [ -n "${BASH_VERSION:-}" ]; then + if [ -f "$DEVBASE_ROOT/etc/devbase-completion.bash" ]; then + # shellcheck source=/dev/null + . "$DEVBASE_ROOT/etc/devbase-completion.bash" + fi +fi + +unset _devbase_rc_src _devbase_bin diff --git a/docs/user/cli-reference.md b/docs/user/cli-reference.md index 9a0f388..879f08b 100644 --- a/docs/user/cli-reference.md +++ b/docs/user/cli-reference.md @@ -10,7 +10,6 @@ devbase のコマンドは 4 つのグループとトップレベルコマンド graph TD A[devbase] --> B[init] A --> C[status] - A --> H[shell-rc] A --> D[project] A --> E[env] A --> F[plugin / pl] @@ -103,31 +102,16 @@ devbase status - 環境変数の設定状況 - スナップショットの状態 -### `devbase shell-rc` +### `bin/rc`(いまのシェルで有効化) -`devbase init` が書き込んだシェル設定ファイル(rc ファイル)のフルパスを stdout に 1 行だけ出力します。`source` のコマンド置換と組み合わせ、ユーザーが zsh / bash on Linux / bash on macOS のどれを使っているかを意識せずに rc ファイルを再読み込みするためのユーティリティです。 - -``` -devbase shell-rc -``` - -判定ロジックは `devbase init` と同一なので、書き込み先と完全に一致します。 - -| 環境 | 出力例 | -|------|--------| -| zsh | `/Users//.zshrc` | -| bash on macOS | `/Users//.bash_profile` | -| bash on Linux | `/home//.bashrc` | - -使用例: +`devbase init` 後に **いま開いているシェル**で devbase(PATH / 補完)を即時有効化するための source 用スクリプトです。`devbase` のサブコマンドではなく、`bin/rc` を直接 source して使います。 ```bash -# 初期化直後に rc を再読み込み(環境差を吸収) ./bin/devbase init -source "$(./bin/devbase shell-rc)" +. ./bin/rc # = source ./bin/rc (bash / zsh 共通) ``` -> **⚠ 引用符は必須**: `source $(devbase shell-rc)` のように引用符を省くと、ホームディレクトリ名に空白を含む環境(例: `/Users/foo bar/.zshrc`)で word splitting が起き `source` が失敗します。必ず `source "$(devbase shell-rc)"` の形で書いてください。 +`bin/rc` は自身の場所から `DEVBASE_ROOT` を解決し、`DEVBASE_ROOT/bin` を PATH へ追加(冪等)したうえで、シェル補完を読み込みます(`init` が rc ファイルへ追記する有効化と同じ内容)。新しく開くシェルは init が rc に追記したブロックで自動有効化されるため、この手順は不要です。 ## project グループ diff --git a/docs/user/getting-started.md b/docs/user/getting-started.md index 2de5e78..2c088e7 100644 --- a/docs/user/getting-started.md +++ b/docs/user/getting-started.md @@ -22,16 +22,14 @@ devbase を利用するには、以下のソフトウェアがホストマシン 手順 1〜2(クローンと初期化)を 1 コマンドで自動化できます。`git` と `curl` があれば実行できます。 ```bash -curl -fsSL https://dl.basex.jp/i | bash +curl -fsSL https://dl.basex.jp/i | bash && . ~/devbase/bin/rc ``` このコマンドは次を行います。 1. `~/devbase` に devbase を clone します(既に devbase が clone 済みなら `git pull --ff-only` で更新)。 2. clone 先で `devbase init` を 1 回実行します(uv の自動導入・PATH/補完の登録・`plugins.yml` 生成を含む)。 -3. 完了後、シェル再読み込み(手順 3)以降の次の手順を表示します。**新しく開くターミナルでは自動で有効**です。 - -**いま開いている端末で即使う**なら、末尾に `&& source "$(~/devbase/bin/devbase shell-rc)"` を付けます(`&&` 以降は呼び出し元シェルで実行されるため、その場で PATH が通ります)。 +3. 末尾の `&& . ~/devbase/bin/rc` を**いま開いている端末**で実行し(`&&` 以降はパイプのサブシェルではなく呼び出し元シェルで動くため)、その端末で即座に `devbase`(PATH / 補完)が使える状態にします。新しく開くターミナルは init の rc 追記により自動で有効です。 `env init`(手順 7)は対話が必要なため、ワンライナーでは**実行せず案内のみ**です。完了後に手動で実行してください。配置先を `DEVBASE_INSTALL_DIR` で変えた場合は、`~/devbase/...` を同じパスに合わせてください。 @@ -78,17 +76,15 @@ cd devbase 書き込み先は現在のシェル種別と OS から自動判定されます(zsh → `~/.zshrc`、bash on macOS → `~/.bash_profile`、bash on Linux → `~/.bashrc`)。 -### 3. シェルの再読み込み +### 3. シェルで有効化 ```bash -source "$(./bin/devbase shell-rc)" +. ./bin/rc ``` -`devbase shell-rc` は `init` が書き込んだ rc ファイルのパスを 1 行で出力します。コマンド置換と組み合わせることで、環境差を意識せずに 1 行で再読み込みできます。 - -> **⚠ 引用符は必須**: `source $(./bin/devbase shell-rc)` のように引用符を省くと、ホームディレクトリ名に空白を含む環境(例: `/Users/foo bar/.zshrc`)で word splitting が起き `source` が失敗します。必ず `source "$(...)"` の形で書いてください。 +`bin/rc` を source すると、いま開いているシェルに devbase の PATH と補完がその場で適用されます(`init` が rc ファイルに追記する内容と同じ有効化を、現在のシェルへ即時反映します)。`devbase` 前提シェルである bash / zsh のどちらでも同じく `.`(= `source`)で読み込めます。 -> **Note:** 新しいターミナルを開いても同様に反映されます。 +> **Note:** 新しいターミナルを開いた場合は `init` が rc に追記したブロックで自動的に有効化されるため、この手順は不要です。 ### 4. プラグインリポジトリの登録 diff --git a/install.sh b/install.sh index f1c9fec..ad06074 100755 --- a/install.sh +++ b/install.sh @@ -79,8 +79,8 @@ print_next_steps() { 配置先: ${INSTALL_DIR} ------------------------------------------------------------ 次の手順: - 1. シェルを再読み込み: - source "\$("${INSTALL_DIR}/bin/devbase" shell-rc)" + 1. いまのシェルで有効化: + . "${INSTALL_DIR}/bin/rc" 2. plugin を導入: devbase plugin install 3. プロジェクトへ移動して env を初期化 (対話): diff --git a/lib/devbase/cli.py b/lib/devbase/cli.py index 4b1f624..7886750 100644 --- a/lib/devbase/cli.py +++ b/lib/devbase/cli.py @@ -488,10 +488,6 @@ def _create_parser(): # --- Top-level commands --- subparsers.add_parser('init', help='Initialize devbase environment') subparsers.add_parser('status', help='Show overall status') - subparsers.add_parser( - 'shell-rc', - help='Print shell RC file path (e.g. source "$(devbase shell-rc)")' - ) _add_project_parser(subparsers) _add_container_parser(subparsers) @@ -531,7 +527,7 @@ def _expand_argv(): # `build` はトップレベルショートカットから除外 (SHORTCUTS の注記参照)。 # bin/devbase が build を shell 実装に委譲するため Python 側には top-level # build parser が無い。project build / container build は引き続き利用可能。 - commands = ['init', 'status', 'shell-rc', 'project', 'container', 'ct', 'env', 'plugin', 'pl', + commands = ['init', 'status', 'project', 'container', 'ct', 'env', 'plugin', 'pl', 'snapshot', 'ss', 'up', 'down', 'login', 'ps', 'scale', 'rebuild', 'list', 'help'] repo_subcmds = ['add', 'remove', 'list', 'refresh'] @@ -607,11 +603,6 @@ def _dispatch(cmd, args): from devbase.commands.container import cmd_container return cmd_container(args) - # --- Commands not requiring DEVBASE_ROOT --- - if cmd == 'shell-rc': - from devbase.commands.shell_rc import cmd_shell_rc - return cmd_shell_rc() - # --- Commands requiring DEVBASE_ROOT --- devbase_root = _require_devbase_root() diff --git a/lib/devbase/commands/shell_rc.py b/lib/devbase/commands/shell_rc.py deleted file mode 100644 index 64b25c1..0000000 --- a/lib/devbase/commands/shell_rc.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Shell-related utility commands""" - -from devbase.utils.shell import get_shell_rc_file - - -def cmd_shell_rc() -> int: - """Print the appropriate shell RC file path to stdout (single line). - - Intended for `source "$(devbase shell-rc)"` so users can reload the - file `devbase init` wrote to without needing to know which one - (zsh -> ~/.zshrc, bash on macOS -> ~/.bash_profile, - bash on Linux -> ~/.bashrc). - """ - print(get_shell_rc_file()) - return 0 diff --git a/tests/cli/test_bin_rc.py b/tests/cli/test_bin_rc.py new file mode 100644 index 0000000..7bc3209 --- /dev/null +++ b/tests/cli/test_bin_rc.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""bin/rc(シェル有効化スクリプト)のテスト (PLAN31_1)。 + +`. bin/rc` を source すると、いま開いているシェルへ devbase の有効化 +(`DEVBASE_ROOT` の設定 / `DEVBASE_ROOT/bin` の PATH 追加)が即時適用される +ことを検証する。`devbase shell-rc`(廃止)+ `source "$(...)"` を置き換えた +軽量パス(Python/uv 起動なし・コマンド置換なし)であることが要点。 + +補完の読み込みはシェル種別依存のため、ここでは PATH / DEVBASE_ROOT と冪等性の +みを検証する(補完登録ロジックは init テストの責務)。 +""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +RC = REPO_ROOT / "bin" / "rc" +BIN = REPO_ROOT / "bin" +BASH = shutil.which("bash") or "/bin/bash" + + +def _source_rc(snippet: str) -> subprocess.CompletedProcess: + """クリーンな環境で bin/rc を source し、続けて snippet を実行する。""" + env = {**os.environ} + env.pop("DEVBASE_ROOT", None) + script = f'. "{RC}"\n{snippet}' + return subprocess.run( + [BASH, "-c", script], capture_output=True, text=True, env=env, + ) + + +def test_rc_file_exists(): + assert RC.exists(), "bin/rc が存在すること" + + +def test_sourcing_sets_devbase_root(): + r = _source_rc('printf "ROOT=%s\\n" "$DEVBASE_ROOT"') + assert r.returncode == 0, r.stderr + assert f"ROOT={REPO_ROOT}" in r.stdout, r.stdout + + +def test_sourcing_prepends_bin_to_path(): + r = _source_rc('printf "PATH=%s\\n" "$PATH"') + assert r.returncode == 0, r.stderr + path_value = next( + line[len("PATH="):] for line in r.stdout.splitlines() + if line.startswith("PATH=") + ) + assert f":{BIN}:" in f":{path_value}:", f"{BIN} が PATH に含まれること: {path_value}" + + +def test_devbase_resolves_after_sourcing(): + """source 後に `devbase` 実行ファイルが PATH 経由で解決できること。""" + r = _source_rc('command -v devbase') + assert r.returncode == 0, r.stderr + assert r.stdout.strip() == str(BIN / "devbase"), r.stdout + + +def test_path_addition_is_idempotent(): + """2 回 source しても bin が PATH に重複追加されないこと。""" + r = _source_rc(f'. "{RC}"\nprintf "%s" "$PATH"') + assert r.returncode == 0, r.stderr + count = (":" + r.stdout + ":").count(f":{BIN}:") + assert count == 1, f"{BIN} は PATH に 1 回だけ: count={count}\n{r.stdout}" + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-v"]))