Releases: mike-north/sh-ast
Release list
sh-ast@0.3.1
Patch Changes
-
#28
7a70a12Thanks @mike-north! - Audited and documented dialect enforcement (see issue #27): for each supported dialect (bash,posix,mksh,bats,zsh), a pinned test matrix (test/dialect-matrix.test.ts) and README table (README.md#dialect-enforcement) now spell out which constructs mvdan/sh's parser rejects vs accepts per dialect — test clauses[[ ]], regex tests=~, array literals, process substitution<(), brace expansion, thefunctionkeyword, C-stylefor, extended globs@(), herestrings<<<, andlet.[[ ... ]]andletparsing without error underdialect: 'posix'is confirmed accepted-by-design, not a bridge bug: mvdan/sh has nocheckLanggate for either keyword, so outside bash-like/mksh/zsh they fall through to ordinary word/command parsing ([[ a == b ]]under posix parses as aCallExprcalling a command literally named[[) — mirroring mvdan/sh's ownLangPOSIXtest fixtures. No runtime parsing behavior changes in this release; this is audit, tests, and docs only.
sh-ast@0.3.0
Minor Changes
-
#24
b93cba8Thanks @mike-north! - Generated node types (node-types.d.ts) now give every scalar leaf field a concrete TypeScript type instead of leaving it asunknown— e.g.ShLitNode.value: string | undefined,ShStmtNode.negated: boolean | undefined,ShRedirectNode.op: number | undefined(see issue #22).tools/gen-visitor-keyswas only classifying node-reference/interface-union child fields; plain scalars fell through to the[field: string]: unknowncatch-all every node still carries.Types are derived from the pinned
mvdan.cc/sh/v3 v3.13.1struct definitions plus the shim's actual runtime serialization:bool/stringfields map straight through, and mvdan/sh'suint32-backed operator-enum fields (RedirOperator,BinAritOperator,CaseOperator, ...) are typednumber— those types implement onlyfmt.Stringer, notjson.Marshaler, so the shim's encoder serializes them as plain JSON numbers, not string tokens.Every generated interface still carries
[field: string]: unknownas an escape hatch for fields this generator doesn't yet classify — this is unchanged and intentional, not a hedge against the newly-typed fields.
sh-ast@0.2.0
Minor Changes
-
#25
ea7dbfbThanks @mike-north! -sh-ast/analyzenow re-exportsShAstError(the shared base class every error the analyze layer throws —ShAnalyzeMaxDepthError,ShAnalyzeInvalidWrapperSpecError— extends) andShNode/Position(the node/position types the subpath's public surface consumes and exposes). A consumer ofsh-ast/analyzecan nowcatch/reference these directly, without also importing from the rootsh-astentry point. This also clears the two pre-existing, acceptedae-forgotten-exportAPI Extractor warnings at the top ofpackages/sh-ast/api-report/sh-ast-analyze.api.md(see #23). -
#16
ed46136Thanks @mike-north! - AddresolveArgv0tosh-ast/analyze: follows aCommandSite's argv0 through zero or more transparent wrappers (env,sudo,nohup,nice,command,exec,time,timeout) to the effective command actually invoked — the one a permission/policy check must judge, since argv0 alone is trivially spoofable through any of these (env FOO=1 rm -rf /,sudo -u x "$prog", …).The default wrapper table (
DEFAULT_TRANSPARENT_WRAPPERS, exported alongside the newWrapperSpectype) is plain data — each entry's flag/operand handling is hand-derived from that wrapper's own manual page — and is fully overridable/extensible viaresolveArgv0'stransparentWrappersoption;xargsis deliberately excluded (its argument-splicing semantics make "the wrapped command" a stdin-dependent, not statically locatable, concept). A statically-unknowable word anywhere in the chain (an expansion, a glob, …) is never guessed through: it becomesArgv0Resolution.effectiveimmediately, andArgv0Resolution.chainstops there.Argv0Resolution.assignmentsSkippedcountsCallExpr.assignsshell-assignment prefixes (FOO=bar rm x), a mechanism distinct from a wrapper's ownVAR=valoperands (env A=1 rm x).Facts only, matching
resolveWord's andenumerateCommands's posture: no safety verdict, no hardcoded "dangerous command" list.Hardened against several ways an unrecognized or unusual invocation could previously misreport the effective command:
- A statically known word shaped like a flag (
--prefixed, not--) that doesn't match any flag/operand shape aWrapperSpecrecognizes now makes the whole resolution unresolvable (Argv0Resolution.effectivebecomes{ static: false, reason: 'unknown-flag' }), instead of being silently treated as the wrapped command — e.g.sudo -D /tmp rm x(-Disn't a modeledsudoflag) no longer reportsrmas effective.Argv0ChainWordand the newArgv0UnresolvedReason/Argv0UnresolvedWordtypes widenArgv0Resolution.chain/.effectiveto carry this. argFlagsnow recognizes every standard getopt short-option form, not just the exact and separate-word forms: attached (-uuser), clustered with preceding no-operand flags (-Eu user/-Euuser), matching real option parsing.env's-S/--split-stringis no longer modeled as an ordinary operand-taking flag: its value splices into the invoked command's own argv (GNU env(1)), so the real command is embedded inside the operand text, not a separate word — it now reportsreason: 'embedded-command'instead of guessing.command -v/-V(which print information about a command name rather than execute it) now correctly stop the chain atcommanditself via the newWrapperSpec.stopsChainFlagsfield, instead of continuing to whatever word follows.WrapperSpec.namesmatching is now documented as exact-name-only (sudonever matches/usr/bin/sudo), andDEFAULT_TRANSPARENT_WRAPPERSis now deep-frozen.- A caller-supplied
transparentWrapperstable with a malformed entry now throws the newShAnalyzeInvalidWrapperSpecErrorat theresolveArgv0boundary, instead of failing confusingly deep inside flag matching.
- A statically known word shaped like a flag (
-
#21
163f842Thanks @mike-north! - Renamed the public error taxonomy exported fromsh-astto drop pre-spinouteslint-sh/@eslint-sh/bridgenaming, now that this package ships standalone. This is a breaking rename with no back-compat aliases — 0.x, so it ships asminorrather thanmajorper this repo's pre-1.0 policy.codestring literals:ESLINT_SH_PARSE_ERROR→SH_AST_PARSE_ERRORESLINT_SH_INVALID_DIALECT→SH_AST_INVALID_DIALECTESLINT_SH_BRIDGE_INTERNAL→SH_AST_INTERNALESLINT_SH_ANALYZE_MAX_DEPTH→SH_AST_ANALYZE_MAX_DEPTHESLINT_SH_PARSE_MAX_DEPTH→SH_AST_PARSE_MAX_DEPTHESLINT_SH_ANALYZE_INVALID_WRAPPER_SPEC→SH_AST_ANALYZE_INVALID_WRAPPER_SPEC
Class renames:
ShBridgeError(abstract base) →ShAstErrorShBridgeInternalError→ShInternalError
All other exported error classes (
ShParseError,ShInvalidDialectError,ShAnalyzeMaxDepthError,ShParseMaxDepthError,ShAnalyzeInvalidWrapperSpecError) are unchanged — they were already product-neutral. No error semantics, messages, or throw sites changed; this is an identifier rename only. -
#18
ef343d7Thanks @mike-north! -parseSyncnow rejects pathologically deep/nested shell source (deeply nested subshells, command substitutions, control-flow bodies, or very long pipeline/list chains) with a typed, catchableShParseMaxDepthError(code: 'SH_AST_PARSE_MAX_DEPTH') instead of letting it reach the WASM parser, where sufficiently deep nesting causes an uncatchable native stack overflow. The guard runs a conservative, single-pass estimate of the input's structural nesting depth before ever invoking the shared WASM instance, so pathological input never risks crashing (or wedging) that instance for subsequent calls. Realistic scripts, including deeply-but-legitimately nested ones, are unaffected.Hardened against a bypass found in review: an unmatched closer (
}with no open{, or a strayfi/done/esacwith no matching opener) previously decremented the depth estimate unconditionally, letting a self-canceling adversarial input (e.g.case x in a})repeated) silently defeat the guard while the real parser still recursed to a genuine, uncatchable stack overflow — every closer now only decrements state when it actually matches an open region. Also fixed:|alternation inside acasearm's pattern list (a|b|c) ...) was incorrectly counted as pipeline depth, falsely rejecting arms with many alternatives — a real pipeline in the arm's action list still counts correctly.
sh-ast@0.1.0
Minor Changes
-
#11
936c601Thanks @mike-north! - AddenumerateCommandstosh-ast/analyze: finds every command invocation (CallExpr) reachable from a parsed tree, including ones hidden inside command/process substitutions nested in arguments, redirection targets, case subjects, loop word lists, assignment values, and test/arithmetic operands. EachCommandSitereports its resolved words (viaresolveWord) and aCommandContextpath describing how it's reached —&&/||/pipeline position, subshell, if/case branch, loop role, function name, background/negated/coproc — in source order. Facts only: no safety verdict, no command/wrapper allowlist or denylist.enumerateCommandstraverses|/|&/&&/||chains of any length iteratively, so a long linear chain never risks a stack overflow. For genuinely nested structure (subshells within subshells, chained command/process substitutions, deeply nestedif/case/loop/function/time/{ }bodies, chainedelif), it now throws the newShAnalyzeMaxDepthErrorrather than crashing or silently returning a truncated result once a defensive nesting-depth guard is exceeded — a fail-closed backstop against pathological/adversarial input. -
#7
d051af4Thanks @mike-north! - Add thesh-ast/analyzesubpath, withresolveWordas its first primitive: determines whether aWordnode is statically a known string (single quoting,$'...'ANSI-C escapes, concatenated literals, andDblQuotedtext with no expansions all count as static) and reports a neutral, extensible reason —expansion,tilde,glob,brace,locale, orunsupported— when it isn't. Bracket expressions ([...]) and$"..."locale-translated strings are recognized as non-static, and an unrepresentable$'\U...'ANSI-C code point reportsunsupportedrather than a false literal. An optional secondoptions.contextparameter ('command-argument'or'assignment-value', default'assignment-value') controls whether an assignment's colon-adjacent tilde (PATH=/foo:~/bar) is detected. Built against the pinned mvdan.cc/sh/v3 v3.13.1 parser; reports syntactic facts only, never a safety verdict. -
#6
6cd00f4Thanks @mike-north! - Fix the normalizer droppingForClause/WhileClauseloop bodies (thedostatement list). Anyfor,while, oruntilloop's body is now reachable on the normalized node'sdofield — previously it was silently omitted entirely.Root cause:
normalize.ts'sPOS_KEYSdenylist listed the bare field nameDo, but mvdan/sh v3.13.1'sForClause/WhileClausestructs useDofor two different fields —DoPos Pos(a position, correctly dropped) andDo []*Stmt(the loop body, a real child) — so the denylist entry discarded the statement list before it could become a normalized child.This changes normalized output for any source containing a
for/while/untilloop, hence the minor bump. -
#12
b8bead0Thanks @mike-north! - Replacenormalize.ts's hand-maintainedPOS_KEYSbare-name denylist with a generated, per-(node type, field name)position-fieldstable (tools/gen-visitor-keys, pinned to mvdan.cc/sh/v3 v3.13.1), so the normalizer never has to guess whether a reused field name is a position from one struct or real data from another.This recovers three fields the old denylist wrongly dropped for every node carrying a same-named field, even though none of them is a
Posin any mvdan/sh v3.13.1 struct:ForClause.Select(bool) — marks aselect ... in ...; do ...; doneloop.WhileClause.Until(bool) — marks anuntil ...; do ...; doneloop.ArithmExp.Unsigned/ArithmCmd.Unsigned(bool) — marks mksh's$((# expr))/((# expr))unsigned arithmetic.
Do(ForClause/WhileClause's loop body, issue #2) andDollar(SglQuoted/DblQuoted's ANSI-C-quoting flag, issue #3) were already fixed by earlier PRs; this generated table is what makes the whole collision class structurally impossible to reintroduce on a future mvdan/sh bump, rather than requiring another hand audit.Normalized output changes for any source containing a
selectloop, anuntilloop, or mksh unsigned arithmetic — previously these booleans were silently omitted — hence the minor bump. -
#9
e6ceb37Thanks @mike-north! - PreserveSglQuoted/DblQuoted'sDollarflag in the normalized AST.normalize.ts'sPOS_KEYSdenylist previously stripped the bare field nameDollarunconditionally — correct forParamExp.Dollar(a position, already dropped by the generic position check), but wrong forSglQuoted.Dollar/DblQuoted.Dollar, which areboolflags marking$'...'(ANSI-C quoting) and$"..."(locale translation) respectively. Without the flag,$'...'was indistinguishable from plain'...'in the normalized tree (and likewise for$"..."vs"...").A normalized
SglQuoted/DblQuotednode now carries adollarboolean field it previously omitted entirely — a consumer-visible addition to the normalized output shape, hence the minor bump.sh-ast/analyze'sresolveWord(see its own changeset) is the first consumer that relies on this flag.