Extract every simple command as a NormalizedLeaf — the operation-level
twin of leafCommands. Same AST-backed structural coverage (a leaf nested
in a pipeline / && / subshell is still found), but each word is quote-unwrapped
and $HOME-canonicalized, the head is reduced to its backslash-stripped basename,
and flags are expanded to short+long canonical forms. A matcher built on this
compares against the OPERATION rather than the surface tokens, so it is robust
to quoting, interpreter path, backslash escaping, flag aliasing, and $HOME/~.
Each leaf also carries its REDIRECTIONS (LeafRedirect), which live on
the enclosing Stmt rather than the CallExpr — walking CallExprs alone
dropped them, hiding the file a command writes. Every CallExpr in the mvdan AST
IS a Stmt.Cmd, so iterating statements finds exactly the same leaves.
Purely additive: reuses the same mvdan-sh parse, changes nothing above. Parse
failure → []. A leaf with a dynamic head is skipped (can't be normalized).
Extract every simple command as a NormalizedLeaf — the operation-level twin of leafCommands. Same AST-backed structural coverage (a leaf nested in a pipeline /
&&/ subshell is still found), but each word is quote-unwrapped and $HOME-canonicalized, the head is reduced to its backslash-stripped basename, and flags are expanded to short+long canonical forms. A matcher built on this compares against the OPERATION rather than the surface tokens, so it is robust to quoting, interpreter path, backslash escaping, flag aliasing, and $HOME/~.Each leaf also carries its REDIRECTIONS (LeafRedirect), which live on the enclosing
Stmtrather than theCallExpr— walking CallExprs alone dropped them, hiding the file a command writes. Every CallExpr in the mvdan AST IS aStmt.Cmd, so iterating statements finds exactly the same leaves.Purely additive: reuses the same mvdan-sh parse, changes nothing above. Parse failure → []. A leaf with a dynamic head is skipped (can't be normalized).