Skip to content

CLI argument parsing: fail loudly on malformed ssh:// URLs; support vim-style +N line args - #2951

Merged
sinelaw merged 3 commits into
masterfrom
claude/fix-cli-args
Aug 10, 2026
Merged

CLI argument parsing: fail loudly on malformed ssh:// URLs; support vim-style +N line args#2951
sinelaw merged 3 commits into
masterfrom
claude/fix-cli-args

Conversation

@sinelaw

@sinelaw sinelaw commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Fixes #2221
Fixes #1926

Two CLI argument-parsing fixes, one commit each (plus a docs note).

Motivation

#2221 — ssh:// silently degrades to a local file. A userless ssh://host/path run without $USER/$USERNAME in the environment (and any other malformed ssh:// argument: missing path, bad port, empty user) fell back to opening a local buffer literally named ssh://.... No error, no SSH attempt — and saving that buffer offered to create a local ssh://host/... directory, a data-loss trap for anyone following the documented URL form.

#1926 — vim-style +N. fresh +50 file.txt opened a stray empty buffer named +50, and in trailing position (fresh file.txt +50) that stray buffer also stole focus from the real file.

The fix

ssh://: parse_location now returns a Result, and the ssh:// scheme is claimed exclusively — anything starting with ssh:// that doesn't parse as a remote spec fails startup with the specific reason, e.g.:

invalid ssh:// URL "ssh://localhost/etc/hosts": no user given and neither $USER nor
$USERNAME is set; specify one explicitly (ssh://user@host/path). To open a local file
with this name, prefix it with ./

The $USER/$USERNAME fallback is injected as a parameter (parse_location_with_default_user) so tests cover the no-$USER case without racing over the process environment. Local files with odd names (ssh:file, ssh:/x, ./ssh://x) are untouched — only the exact scheme prefix is claimed. Note this intentionally changes the previous "malformed ssh:// is a local filename" tests: opening a local file that is literally named ssh://... now requires the ./ prefix (same escape hatch vim uses for +50-style names).

+N: +N arguments are folded into the file list once, right after clap parsing and before any consumer (TUI, GUI, web, attach, nested forward) sees it, by rewriting the target to the exact same file:line syntax path that already existed — fresh +50 file.txt == fresh file.txt:50, including remote specs (fresh +50 user@host:/path). Vim-parallel rules:

  • the line applies to the first file argument (any position, so the trailing-focus-steal case is fixed too);
  • an explicit file:line on the target beats +N;
  • at most one +N is accepted; +N with no file argument is an error with a hint;
  • +0 and beyond-EOF lines clamp exactly like file:0 / file:huge (verified: +99999 on a 100-line file lands on the last line);
  • bare + (vim: "last line") is not claimed — it has no file:line equivalent — and stays an ordinary filename, as do ./+50 and +abc. A real file named +50 is still openable as ./+50 (breaking change relative to bare +50, matching vim's behavior).

--help documents the new form in the file-location syntax section; the new cli.file_syntax.plus_line key is translated in all 14 locales.

Tests

New unit tests (all in crates/fresh-editor/src/main.rs):

  • ssh://: userless URL with no default user → error naming $USER; missing path / bad port / empty user → errors (these previously asserted the silent local fallback — assertions flipped to the correct behavior, so they fail without the fix); userless + injected default user works, incl. port and :line:col; scp-style unaffected by missing $USER; weird local names (ssh:file.txt, ssh:/host/path, sshfile, ./ssh://host/path) stay local.
  • +N: leading/trailing/multi-file placement, +0, saturation of absurdly large numbers, explicit file:line precedence, remote specs, stdin - skipping, bare + / ./+50 / +abc untouched, no-file and multiple-+N errors.

Ran: cargo test -p fresh-editor --bin fresh (70 passed), cargo test -p fresh-editor --lib i18n (locale parity), cargo check -p fresh-editor --all-targets, cargo check -p fresh-editor --all-targets --features gui, cargo fmt, cargo clippy -p fresh-editor (no new warnings).

Manual validation (tmux, debug build, isolated $HOME):

  • env -u USER -u USERNAME fresh --no-restore ssh://localhost/etc/hosts → clean startup error (above), exit 1, no editor, no local buffer.
  • fresh --no-restore ssh://root@localhost/etc/hosts → still prints Connecting via SSH to root@localhost... (control).
  • fresh +50 test.txt and fresh test.txt +50 → single test.txt tab, status bar Ln 50, Col 1, no +50 buffer, no focus steal.
  • fresh +0 test.txtLn 1; fresh +99999 test.txt → last line.
  • fresh +50 → clear error with hint; fresh ./+50 opens the literal file.
  • fresh --help shows +10 file.txt Open at line 10 (vim-style).

Notes for review

  • Startup errors from these paths print through anyhow with RUST_BACKTRACE=1 (force-set in real_main), so a backtrace follows the message — same as existing startup errors (e.g. mixed local/remote); not new to this PR.
  • On support vim +[line_number] [filename] #1926's deferred label: the issue thread contains no argument against the syntax — the owner's only comment was a clarifying question ("from CLI, in addition to filename:line?", answered yes) — so this implements it fully; happy to trim to just the stray-buffer fix if the deferral was intentional.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y


Generated by Claude Code

claude added 3 commits August 10, 2026 15:14
A userless `ssh://host/path` URL run without $USER/$USERNAME in the
environment (and any other malformed ssh:// argument: missing path,
bad port, empty user) silently fell back to opening a LOCAL buffer
literally named `ssh://...`. No SSH connection was attempted and no
error shown; typing into the buffer and saving offered to create a
local `ssh://host/...` directory — a data-loss trap for users
following the documented URL form (#2221).

The `ssh://` scheme is now claimed exclusively by the remote parser:
parse_location returns a Result, and anything starting with `ssh://`
that does not parse as a remote spec fails startup with a clear
reason (e.g. "no user given and neither $USER nor $USERNAME is set").
The default user is injected as a parameter so unit tests cover the
no-$USER case without racing over process environment. Local files
with odd names (ssh:file, ./ssh://x) are untouched — only the exact
scheme prefix is claimed, and the error suggests the ./ escape hatch.

Fixes #2221

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
`fresh +50 file.txt` used to open a stray empty buffer literally
named `+50` next to the real file — and in trailing position
(`fresh file.txt +50`) that stray buffer also stole focus. Users
coming from vi/vim expect +N to open the file at that line (#1926).

`+N` arguments are now folded into the file list before any consumer
(TUI, GUI, web, attach, nested forward) sees it: the line is applied
to the first file argument through the exact same `file:line` syntax
path that already existed, so `fresh +50 file.txt` ==
`fresh file.txt:50`, including for remote specs. Vim-parallel rules:
the line goes to the first file, an explicit `file:line` beats `+N`,
at most one `+N` is accepted, and `+N` with no file is an error with
a hint. Out-of-range lines clamp exactly like `file:huge`. Bare `+`
(vim: last line), `./+50`, and `+abc` remain ordinary filenames.
The --help file-location section documents the new form (new
`cli.file_syntax.plus_line` key translated in all 14 locales).

Fixes #1926

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
Document the contract introduced for #2221: the ssh:// scheme is
claimed exclusively by the remote parser, malformed URLs fail at
startup, and ./ssh://... is the escape hatch for weird local names.

Refs #2221

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
@sinelaw
sinelaw force-pushed the claude/fix-cli-args branch from 400109a to 05e2d39 Compare August 10, 2026 15:18
@sinelaw
sinelaw merged commit 4fe7cc7 into master Aug 10, 2026
17 of 19 checks passed
@sinelaw
sinelaw deleted the claude/fix-cli-args branch August 10, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSH URL-style URI (ssh://host/path) treated as local file path instead of triggering SSH connection support vim +[line_number] [filename]

2 participants