Skip to content

fix: use shellParse for cmd tokenization to match genValidationFile#173

Open
memosr wants to merge 1 commit into
base:mainfrom
memosr:fix/use-shellparse-for-cmd-tokenization
Open

fix: use shellParse for cmd tokenization to match genValidationFile#173
memosr wants to merge 1 commit into
base:mainfrom
memosr:fix/use-shellparse-for-cmd-tokenization

Conversation

@memosr

@memosr memosr commented Jun 6, 2026

Copy link
Copy Markdown

Summary

genValidationFile writes the cmd field using shellParse, but validation-service reads it back with a naive whitespace split. This produces wrong arguments to forge for manually authored configs that follow the README's example syntax.

The drift

Write pathsrc/lib/genValidationFile.ts uses shellParse from shell-quote to tokenize the forge command before storing it. This correctly handles quoted arguments like --sig "run()".

Read pathsrc/lib/validation-service.ts:94 reads the same cmd field back with:

const argv = cfg.cmd.trim().split(/\s+/);

A naive whitespace split. The two paths use different tokenizers for the same string.

Why it appears to work

For auto-generated configs, genValidationFile strips quotes before joining the arguments, so the stored cmd happens to have no quotes — and a naive split works.

Where it breaks

The README's documented example shows manually authored configs with shell-quoted args:

{
  "cmd": "forge script ... --sig \"run()\""
}

Running this through cfg.cmd.trim().split(/\s+/) produces argv tokens including the literal "run()" with quotes preserved. spawn('forge', [..., '--sig', '"run()"', ...]) passes the quoted string to forge, which then can't find a function matching that literal.

The error surfaces as "function not found" — completely opaque, with no hint that the actual problem is in the tokenizer mismatch.

The fix

+ import { parse as shellParse } from 'shell-quote';
  
  // ...
  
- const argv = cfg.cmd.trim().split(/\s+/);
+ const argv = shellParse(cfg.cmd).filter((token): token is string => typeof token === 'string');

Uses shellParse from the same shell-quote package already imported in genValidationFile. The .filter() is needed because shell-quote returns OpEntry objects for shell operators (|, ;, >, etc.) that have no meaning when spawning forge directly — we want only plain string tokens.

Now both the write path and the read path use the same tokenizer. Any cmd string that round-trips through the JSON config is parsed identically on both sides.

Verification

  • ✅ One file modified: src/lib/validation-service.ts
  • ✅ Import added (same package as genValidationFile)
  • .filter() ensures only string tokens reach spawn()
  • ✅ Auto-generated configs continue to work (they have no quotes anyway)
  • ✅ Manually authored configs following the README example now work

src/lib/genValidationFile.ts tokenizes the forge command with shellParse
from shell-quote before storing it in the JSON config — correctly handling
quoted arguments like --sig "run()".

src/lib/validation-service.ts then read that same cmd field back and
re-tokenized with cfg.cmd.trim().split(/\\s+/), a naive whitespace split.
For auto-generated configs this happened to work because genValidationFile
strips quotes before joining. But the README example shows manually
authored configs that include shell-quoted args:

    "cmd": "forge script ... --sig \"run()\""

The naive whitespace split passes \"run()\" (with quotes) as the --sig
value to spawn, not run(). The forge invocation then fails to find any
function matching the literal quoted string, producing an opaque error
unrelated to the actual problem.

Replaced the naive split with shellParse from the same shell-quote
package already imported in genValidationFile. Filtered the result to
plain string tokens, since shell-quote returns OpEntry objects for shell
operators (|, ;, >, etc.) that have no meaning when spawning forge
directly.

Both the write path (genValidationFile) and the read path
(validation-service) now use the same tokenizer, so any cmd string that
round-trips through the JSON config is parsed identically.
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 1
Sum 2

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.

2 participants