Skip to content

fix(cli): install bash completions without hiding ~/.profile - #27

Merged
kalaspuff merged 1 commit into
mainfrom
fctl/3-when-i-installed-fctl-it-added-a-bash-pr
Oct 18, 2026
Merged

kalaspuff merged 1 commit into
mainfrom
fctl/3-when-i-installed-fctl-it-added-a-bash-pr

Conversation

@fctl-sh

@fctl-sh fctl-sh Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

On macOS, fctl completions --install (the installer runs it) always added its block to ~/.bash_profile and created that file if it was missing. Bash login shells, which Terminal and iTerm2 start, read only the first of ~/.bash_profile, ~/.bash_login and ~/.profile that exists. If someone's settings were in ~/.profile, the new ~/.bash_profile stopped bash from reading them, and their shell lost its PATH and the rest until they deleted the file.

What changed

  • Which file. On macOS the bash block now goes at the end of the file bash already reads at login: the first of ~/.bash_profile, ~/.bash_login and ~/.profile that exists. If none of them exists, it creates ~/.profile. That file comes last in bash's order, so creating it can't hide anything. zsh, fish and bash on Linux (~/.bashrc) behave as before.
  • Only bash loads the script. sh, dash and ksh login shells also read ~/.profile, and the generated script is bash syntax. When dash sources it, it stops with a syntax error, and dash -l -c … exits without running its command. The block now checks $BASH_VERSION. It uses if … fi rather than an && chain, so . ~/.profile && cmd still succeeds in other shells. Blocks that are already installed keep working, and the marker still stops a second copy.
  • Startup files are appended to, never rewritten. The old code read the file with read_to_string(..).unwrap_or_default() and wrote it back with fs::write. If a file wasn't valid UTF-8 (one Latin-1 byte in a comment is enough), it was read as empty and replaced with the fctl block alone. Now it reads bytes, appends, and returns an error rather than writing over a file it can't read. This change points it at people's long-lived ~/.profile, so I fixed this in the same pull request.
  • docs/cli.md now says where the block goes.

Not in this change

  • People who already have the stray file. Anyone who installed 0.1.2 on macOS with bash, and had a ~/.profile (or ~/.bash_login) but no ~/.bash_profile, still has the ~/.bash_profile it created. This change doesn't touch it: --install finds the marker there and does nothing. The fix is to delete ~/.bash_profile if it holds only the fctl block, then run fctl completions --install again. 0.1.2 shipped on 2026-10-17, so few people can have the stray file, and I didn't add code that deletes files in people's home directories. fctl self update could do it if we want that.
  • The installer, in hyperstructure/fctl-site. Its header comment still says the block goes in ~/.zshrc or ~/.bashrc. Its PATH hint for bash appends to ~/.profile, which bash login shells skip when ~/.bash_profile exists (on macOS, that's every new terminal). I couldn't reach that repository from this session.

How I verified it

  • New unit tests in crates/fctl/src/cmd/completions.rs. They fail on the old code and pass now. They cover the reported case (an existing ~/.profile gets the block at its end and no ~/.bash_profile appears) and bash's order (.bash_profile, then .bash_login, then .profile). They also cover a new ~/.profile when there is none, the guarded block, and a Latin-1 ~/.bashrc keeping its bytes. They run install against a temporary home directory with macos set either way. I had no Mac, so the macOS file choice is tested only there.
  • I ran the old binary against a throwaway HOME with a Latin-1 ~/.bashrc, and its contents were replaced by the block. The new binary appends and leaves the original bytes as they were. A second run adds nothing.
  • I put the exact block the binary writes at the end of a ~/.profile and tried real shells. bash -l reads the profile and registers complete -F _fctl fctl. dash -l reads the profile without errors, and sh -c 'set -eu; . ~/.profile && …' succeeds. With the old block in the same place, dash -l died on a syntax error. An interactive bash -i loads the completions from ~/.bashrc.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings, the wasm clippy for fctl-control and cargo test --workspace all pass. cargo xtask check --quick stopped at npm ci, because this sandbox can't fetch the private @hyperstructure/design package. The web app is unchanged.

Session: https://app.fctl.sh/s/ses_01M56488710JJ92CYMX0N7GPMB

🤖 Generated with Claude Code

On macOS, `fctl completions --install` (which the installer runs) always
added its block to ~/.bash_profile, creating the file when it was
missing. Bash login shells read only the first of ~/.bash_profile,
~/.bash_login and ~/.profile that exists, so a new ~/.bash_profile
quietly stopped bash from reading the ~/.profile people already had.

The block now goes at the end of the file bash reads at login, and a new
~/.profile when there is none, since that hides nothing. sh, dash and ksh
read ~/.profile too and stop at bash syntax, so the block loads the
script only under bash, in an `if` that leaves `. ~/.profile && ...`
working in other shells.

Writing the block also replaced any startup file it could not read as
UTF-8 (a Latin-1 comment was enough) with the block alone. It now reads
bytes, appends, and stops with an error if it cannot read the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kalaspuff
kalaspuff merged commit 53f1619 into main Oct 18, 2026
5 checks passed
@kalaspuff
kalaspuff deleted the fctl/3-when-i-installed-fctl-it-added-a-bash-pr branch October 18, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant