馃寪 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

馃悰 fix(csh): preserve literals across quoting modes - #3414

Merged
gaborbernat merged 11 commits into
pypa:mainfrom
gaborbernat:fix/cshell-quote-modes
Oct 17, 2026
Merged

gaborbernat merged 11 commits into
pypa:mainfrom
gaborbernat:fix/cshell-quote-modes

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

With tcsh's backslash_quote option set, activation stored VIRTUAL_ENV_PROMPT and VIRTUAL_ENV with fewer backslashes than the values given, and a value ending in a backslash stopped the script with Unmatched '''. On bsd-csh, the csh of Debian and Ubuntu, every backslash in the prompt drew twice. 馃悰 After this PR the generated literals mean the same in both quoting modes, and each shell draws one backslash per backslash in the value. This builds on #3404.

Backslashes inside single quotes change meaning with backslash_quote

Without the option, a backslash inside single quotes is a literal character (Lexical structure). With backslash_quote set, a backslash always quotes \, ' and ", inside single quotes too (tcsh manual, backslash_quote). quote() left the value's backslashes inside the quotes shlex.quote wrote (old lines, kept by #3404). On tcsh 6.24, 'a\\b' is read as a\\b in one mode and as a\b in the other, and 'a\' has no closing quote once \' quotes the apostrophe, so the shell stops sourcing with Unmatched ''' after VIRTUAL_ENV and PATH are already changed. quote() now moves every backslash outside the quotes as '\\' (new lines), where a backslash followed by a backslash gives one backslash in both modes. _prompt_literal has kept its backslashes outside the quotes since #3404; its apostrophe now uses the '"'"' form shlex.quote emits, so both helpers write the same source for the same character (new lines). A test sources the script in tcsh with and without set backslash_quote for a backslash, a trailing backslash, an apostrophe and a protected history character, and compares VIRTUAL_ENV, VIRTUAL_ENV_PROMPT and prompt (test).

flowchart LR
    V["value a\\b"] --> O["old source 'a\\b'"]
    O -- "backslash_quote unset" --> O1["a\\b"]
    O -- "backslash_quote set" --> O2["a\b"]
    V --> N["new source 'a'\\'\\'b'"]
    N -- "either mode" --> N1["a\\b"]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class O2 bad;
    class N1 ok;
Loading

Prompt text is assigned before the conditional

The prompt block from #1454, with the tcsh arm added in #3263, embedded the display literal in both arms of the $?tcsh selection (old lines). Whichever arm the shell took, the other one still carried caller-controlled quoting through the shell's line scanner, and the whole block was scanned without being run when VIRTUAL_ENV_DISABLE_PROMPT was set. Both display strings are now assigned once, before do_prompt is tested, to _virtualenv_prompt_tcsh and _virtualenv_prompt_plain (new lines). The arms reference them as "$_virtualenv_prompt_tcsh:q" and "$_virtualenv_prompt_plain:q" (new lines), and the two variables are unset after the block (new lines). No text from the caller remains inside a skipped arm. A test sources the script in csh with VIRTUAL_ENV_DISABLE_PROMPT set and a prompt containing an apostrophe, and checks that the prompt is untouched (test).

flowchart LR
    A["set _virtualenv_prompt_tcsh = '...'<br>set _virtualenv_prompt_plain = '...'"] --> B{"do_prompt == 1"}
    B -- no --> X["block scanned, not run"]
    B -- yes --> C{"$?tcsh"}
    C -- yes --> T["set prompt = '('#quot;$_virtualenv_prompt_tcsh:q#quot;') '"]
    C -- no --> P["set prompt = '('#quot;$_virtualenv_prompt_plain:q#quot;') '"]
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class A ok;
Loading

bsd-csh draws the prompt without decoding backslashes

tcsh parses \c when it draws the prompt, so a literal backslash has to reach prompt doubled (tcsh manual, prompt). bsd-csh prints the string as stored and replaces only ! (csh manual, prompt). Since #3263 _prompt_literal doubled the backslash for both shells (old lines), so bsd-csh drew (a\\b) for a prompt of a\b. The plain literal now writes one backslash per backslash and the tcsh literal keeps two (new lines). A test that skips unless csh is bsd-csh sources the script interactively and checks the drawn prompt (test).

flowchart LR
    V["prompt a\b"] --> T["tcsh literal: prompt holds a\\b"]
    T -- "\c decoded on draw" --> T1["(a\b)"]
    V --> O["old plain literal: prompt holds a\\b"]
    O -- "printed as stored" --> O1["(a\\b)"]
    V --> N["new plain literal: prompt holds a\b"]
    N -- "printed as stored" --> N1["(a\b)"]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class O1 bad;
    class T1,N1 ok;
Loading

Behavior changes

With backslash_quote set, VIRTUAL_ENV, VIRTUAL_ENV_PROMPT and prompt hold the same values as without it, and a value that ends in a backslash activates. bsd-csh draws one backslash per backslash in the prompt; tcsh draws the same prompt as before. Activation sets two shell variables, _virtualenv_prompt_tcsh and _virtualenv_prompt_plain, and unsets them before it returns. An apostrophe in a generated prompt literal is written as '"'"' rather than '\''.

Reported by Anthropic's OSS Scanner: ANT-2026-I4L9S9FD, ANT-2026-YYQQ0SWO.

@gaborbernat
gaborbernat force-pushed the fix/cshell-quote-modes branch 2 times, most recently from 3cca319 to 5cd8d80 Compare October 16, 2026 14:25
gaborbernat and others added 11 commits October 17, 2026 02:00
Reported by Anthropic's OSS Scanner: ANT-2026-I4L9S9FD, ANT-2026-YYQQ0SWO.
Reported by Anthropic's OSS Scanner: ANT-2026-ZZZSMHXA, ANT-2026-BZW697LP.
Linux CI had no tcsh and no GBK locale, so every tcsh test in this
module was skipped there; install both as the parent PR does. Fold the
percent, caret and GBK prompt tests into one interactive rendering
test, since the caret escape was only checked through the generated
text and the GBK caret case passes on unfixed code. Build the
disabled-prompt and bsd-csh environments through the shared fixture.
@gaborbernat
gaborbernat force-pushed the fix/cshell-quote-modes branch from 836b407 to 0129973 Compare October 17, 2026 09:01
@gaborbernat
gaborbernat merged commit 57dec08 into pypa:main Oct 17, 2026
74 checks passed
@gaborbernat
gaborbernat deleted the fix/cshell-quote-modes branch October 17, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant