Skip to content

fix(layouts): keep literal paths when shellexpand fails#5386

Open
arimu1 wants to merge 1 commit into
zellij-org:mainfrom
arimu1:fix/5378-literal-dollar-cwd
Open

fix(layouts): keep literal paths when shellexpand fails#5386
arimu1 wants to merge 1 commit into
zellij-org:mainfrom
arimu1:fix/5378-literal-dollar-cwd

Conversation

@arimu1

@arimu1 arimu1 commented Jul 19, 2026

Copy link
Copy Markdown

Problem

Fixes #5378

Layout parsing runs shellexpand::full() on every cwd/command/edit path string (KdlLayoutParser::parse_path in zellij-utils/src/kdl/kdl_layout_parser.rs). If that string contains a literal $ that doesn't happen to name a real environment variable (e.g. a directory called $programId+, as produced by React Router / Remix file-based routing), shellexpand::full returns an Err, which parse_path turns into a hard ConfigError - aborting the whole layout parse.

This is especially bad for resurrection layouts: zellij writes a pane's real, unescaped cwd verbatim into session-layout.kdl. If that cwd contains a literal $, the cache file zellij just wrote becomes unparseable by zellij itself, and zellij attach fails with only a generic error (Failed to parse resurrection layout file ...: KdlDeserialization error: Failed to parse Zellij configuration.), hiding the real cause.

Minimal repro (from the issue):

layout {
    pane command="ls" cwd="/tmp/foo/$programId+" {
        start_suspended true
    }
}
zellij -n layout.kdl -s repro

fails with:

×  Failed to parse Zellij configuration
   pane command="ls" cwd="/tmp/foo/$programId+" {
   error looking key 'programId' up: environment variable not found

Fix

parse_path now falls back to the original, unexpanded string when shellexpand::full errors, instead of propagating a ConfigError. A log::warn! is emitted so the fallback is still visible for debugging. Legitimate expansion (~, real $VARS) is unaffected - only the error case falls back to the raw string.

This mirrors the fallback pattern already used for plugin path expansion in RunPluginLocation::parse (zellij-utils/src/input/layout.rs), so it's consistent with existing conventions in this codebase.

Testing

  • Updated env_var_missing (zellij-utils/src/input/unit/layout_test.rs) to reflect the new behavior: a missing/unresolvable $VAR no longer hard-fails layout parsing, it falls back to the literal string.
  • Added literal_dollar_sign_in_cwd_is_not_a_parse_error, a regression test using the exact repro from the issue (pane command="ls" cwd="/tmp/foo/$programId+"), asserting the layout parses and the pane's cwd is kept as the literal /tmp/foo/$programId+.
  • Ran the full zellij-utils unit test suite:
    cargo test -p zellij-utils --lib -- --skip cli::tests
    
    409 passed; 0 failed; 2 ignored (the cli::tests module was skipped because several of its tests stack-overflow on this machine/toolchain independent of this change - reproduced identically on unmodified main).
  • cargo fmt -p zellij-utils -- --check is clean.

Note

I did not change how resurrection layouts are serialized (e.g. escaping $ as $$ when writing). The parser-side fallback fixes the actual symptom (an unparseable file zellij wrote itself) without needing to touch the writer, and keeps existing hand-written layouts with a genuinely-missing env var loading instead of hard-failing, which seems like the more robust behavior for a value that's just going to become a path string either way.

A cwd/command/edit path containing a literal `$` that isn't a real
environment variable (eg. a directory named `$programId+`) made
shellexpand::full() return an Err, which parse_path() turned into a
hard ConfigError, failing the whole layout parse.

This is especially bad for resurrection layouts: zellij serializes a
pane's real cwd verbatim into session-layout.kdl, so a directory name
with a literal `$` produces a cache file that zellij itself can no
longer parse, and `zellij attach` fails with a generic error.

Fall back to the original, unexpanded string when shellexpand fails
instead of aborting the parse, matching the existing fallback pattern
already used for plugin path expansion in RunPluginLocation::parse.

Fixes zellij-org#5378
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.

Resurrection layout with literal $ in pane cwd is written by zellij but fails to re-parse

1 participant