Fix GitHub search queries with labels containing spaces (Issue #48) - #101
Conversation
Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: #48
Root cause: GitHub CLI expects search terms as separate arguments, not as a single quoted string. The previous quote() function was over-quoting values, causing nested quote issues in template literals.
Changes:
- Modified quote() function to prefer double quotes for simple spaced strings
- This eliminates nested single-quote-inside-double-quote problems
- Maintains backward compatibility for complex strings with quotes
- Updated existing tests to reflect improved quoting behavior
Technical details:
- Simple spaced strings like "help wanted" now use double quotes: "help wanted"
- Strings with single quotes still use traditional escaping: 'it'\''s'
- Strings with double quotes use single quotes: 'has"quotes'
- No change for safe strings without spaces: nospaces
Examples that now work:
- await $`gh search issues repo:owner/repo label:${labelWithSpaces}`
- await $`gh issue list --label "${label}"`
Fixes #48
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
|
We need to get latest changes from default branch. We should double check more cases similar like this, make sure we have test coverage similar to all our competitors, and select behavior closer to how it would behave in sh scripts or with least surprise based on best practices from competitors. If there multiple options we should allow to configure, and use closer to sh behavior by default. |
|
🤖 AI Work Session Started Starting automated work session at 2026-09-05T13:57:57.983Z The PR has been converted to draft mode while work is in progress. This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback. |
The repository was restructured (js/ and rust/ workspaces) and main now has quote-context aware interpolation (issue #49), which supersedes the earlier attempt in this branch to make quote() prefer double quotes. Resolved all conflicts in favour of main; the fix for issue #48 is re-implemented on top of the current code.
GitHub search queries such as `$`gh issue list --label "${label}"`` (with
`label = "help wanted"`) failed because command-stream only stripped quotes
from wholly-quoted words. Mid-word quotes like `label:"help wanted"` were left
intact or split incorrectly, diverging from POSIX `sh`.
Changes:
- Add POSIX-style quote removal applied per argument in every parse path
(`removeShellQuotes` / `remove_shell_quotes`): single quotes are literal,
double quotes honor `\` escapes for `$ ` " \`, and backslash escapes outside
quotes. Quoted and unquoted pieces concatenate into one argument, matching
`/bin/sh`.
- Preserve the original word as `raw` so re-serializing a command back to a
real shell round-trips exactly.
- Route virtual-command dispatch through quote-aware word splitting
(`split_command_words`) in Rust instead of naive `split_whitespace`, which
split inside quotes.
- Fix a latent tokenizer infinite loop on a lone `&` (e.g. `2>&1` or
backgrounding): the word scanner listed `&` in its stop set but no operator
arm consumed it, so `i` never advanced. Surfaced once the virtual path began
tokenizing raw commands.
- Guard the Rust virtual path from tokenizing commands already destined for a
real shell (empty command name).
Tests:
- JS: js/tests/github-search-escaping.test.mjs (unit + spawned-process + sh
parity + custom virtual command).
- Rust: unit tests for remove_shell_quotes / split_command_words and tokenizer
termination; integration tests for embedded-quote echo.
- experiments/issue-48-quote-removal-parity.mjs diffs 22 cases against /bin/sh.
| const shOut = execFileSync( | ||
| '/bin/sh', | ||
| ['-c', `${process.execPath} ${argprint} label:"${label}"`], | ||
| { encoding: 'utf8' } | ||
| ); |
The unquoted-backslash-as-escape rule in quote removal corrupted Windows paths (e.g. `cd C:\Users\foo`), breaking the cd invocation isolation test on windows-latest. Apply POSIX unquoted-backslash escaping only on non-Windows platforms; keep it literal on Windows where it is the path separator. Double-quote inner escapes and single-quote handling are unchanged. Extract readDoubleQuotedSegment in JS to keep complexity within the lint limit. Tests gated per platform in both languages.
Working session summaryI'll wait for the background CI poller to complete and notify me. This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $16.147203📊 Context and tokens usage:Claude Opus 5: (3 sub-sessions)
Total: (94 new + 82.9K cache writes + 2.6M cache reads) input tokens, 22.3K output tokens, $2.692297 cost Claude Opus 4.8:
Total: (7.0K new + 288.9K cache writes + 15.7M cache reads) input tokens, 108.2K output tokens, $13.454907 cost 🤖 Models used:
📎 Log file uploaded as Gist (7176KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🎉 Auto-mergedThis pull request has been automatically merged by hive-mind.
Auto-merged by hive-mind with --auto-merge flag |
Summary
Fixes
#48— GitHub search queries with labels containing spaces failed because command-stream did not perform full POSIX quote removal when handing arguments to commands.The canonical repro:
Before this change, command-stream only stripped quotes from words that were wholly quoted. A word with an embedded quote — the common shape after interpolation, e.g.
label:"help wanted"orlabel:'help wanted'— kept its quotes or was split at the space, diverging from how/bin/shbehaves.Root cause
POSIX "quote removal" allows quotes to appear anywhere inside a word; the shell strips the quote characters and concatenates the quoted and unquoted pieces into a single argument (
label:'help wanted'→label:help wanted). command-stream's parser only handled the fully-wrapped case ("…"/'…'), so mid-word quotes leaked through.Approach
Per the maintainer request, the behavior now follows
/bin/sh(least surprise vs. competitors), verified by diffing against/bin/shacross 22 cases.removeShellQuotes(JS) /remove_shell_quotes(Rust):'…'is fully literal;"…"honors\escapes only for$ ` " \and newline;\escapes the next character;rawis preserved for each argument (the original word with its quotes) so re-serializing a command back to a real shell round-trips exactly — quotes are only removed for values delivered to built-in/virtual commands.split_command_wordsinstead of naivesplit_whitespace, which previously split inside quotes.&(as in2>&1, or backgrounding) matched neither the&&operator nor advanced the word scanner (it was in the word stop-set), so it spun forever. This was latent until the virtual path began tokenizing raw commands. The tokenizer now always makes progress, and the virtual path skips tokenizing commands already routed to a real shell.JavaScript and Rust implementations are kept in parity (the language-parity check passes).
How to reproduce / verify
Tests
js/tests/github-search-escaping.test.mjs: unit tests forremoveShellQuotes, the exact issue shape (--label "value"), spawned-process argv assertions,/bin/shword-splitting parity, and a custom virtual command receiving quote-removed args.remove_shell_quotes/split_command_wordsand tokenizer termination (lone&); integration tests for embedded-quoteecho.experiments/issue-48-quote-removal-parity.mjsdiffs 22 quoting cases against/bin/sh(built-in and spawned paths).Before / after
A changeset is included (
js/.changeset/gh-search-label-spaces.md, patch).Resolves #48
🤖 Generated with Claude Code