sh2lang v0.1.1
Incremental release adding the sudo(...) builtin, confirm(...) helper, and semicolon statement separators.
Added
sudo(...) builtin
Structured wrapper for sudo command execution with type-safe options:
# Basic usage
sudo("apt-get", "update")
# With user option
sudo("systemctl", "restart", "nginx", user="root")
# With environment preservation
sudo("env", env_keep=["PATH", "HOME"])
# Mixed argument ordering supported
sudo(user="admin", "ls", n=true)
Supported options:
user(string literal) — run as specified user (-u)n(boolean) — non-interactive modek(boolean) — invalidate cached credentialsprompt(string literal) — custom password prompt (-p)E(boolean) — preserve environment (-E)env_keep(list of string literals) — preserve specific variables (--preserve-env=...)allow_fail(boolean, statement-form only) — non-aborting execution
Behavior:
- Generates stable flag ordering with mandatory
--separator before command - Validates option types at compile time (literals only for
user,prompt,env_keep) - Rejects duplicate options with clear diagnostics
- Mixed positional/named argument ordering allowed in all contexts
Diagnostics:
- Unknown options:
"unknown sudo() option 'X'; supported: user, n, k, prompt, E, env_keep, allow_fail" - Duplicate options:
"user specified more than once" - Type errors (per-option literal requirements):
user,prompt:"user must be a string literal"n,k,E,allow_fail:"n must be a boolean literal"env_keep:"env_keep must be a list of string literals"
- Expression-form
allow_fail:"allow_fail is only valid on statement-form sudo(...); use capture(sudo(...), allow_fail=true) to allow failure during capture"
Examples:
Basic sudo:
func main() {
sudo("echo", "hello")
}
With user and options:
func deploy() {
sudo("systemctl", "restart", "app", user="deploy", E=true)
}
Statement-form with allow_fail:
func cleanup() {
sudo("rm", "-rf", "/tmp/cache", allow_fail=true)
if status() != 0 {
print_err("cleanup failed")
}
}
confirm(...) helper
Interactive yes/no prompts with optional defaults:
# Basic confirmation
if confirm("Proceed?") {
run("deploy.sh")
}
# With default value
if confirm("Delete files?", default=false) {
run("rm", "-rf", "data/")
}
Behavior:
- Returns boolean result
- Supports
default=trueordefault=falseparameter - Non-interactive mode: uses default if provided, otherwise fails
- Environment override:
SH2_YES=1orSH2_NO=1
Semicolon statement separators
Optional semicolon separators/terminators are now supported in statement blocks:
func main() {
print("a"); print("b")
}
Semicolons are not allowed inside expressions.
sh2do File Mode
sh2do now supports running .sh2 files directly, in addition to inline snippets.
- Run a file:
sh2do script.sh2 - Emit mode:
sh2do script.sh2 --emit(compiles toscript.shand runs it) - Output selection:
sh2do script.sh2 -o output.sh(compiles tooutput.shand runs it) - Runtime selection:
--target <bash|posix>and--shell <bash|sh> - Safety: Invokes the shell as
<shell> -- <script> <args...>to prevent script paths from being interpreted as flags.
No Implicit Expansion
String literals and variables are never automatically expanded (globbed or split) by the shell.
run("echo", "*")prints*literal.run("echo", my_var)passesmy_varas a single argument, even if it contains spaces.- Tilde (
~) is treated as a literal character in paths unless explicitly handled.
CWD Semantic Restriction
with cwd(...) accepts an expression syntactically, but the compiler enforces a string-literal-only rule.
- Valid:
with cwd("/tmp") { ... } - Invalid:
with cwd(my_var) { ... }-> Compile Error:cwd(…) requires a string literal path. Computed expressions are not allowed. help: if you need a computed cwd, use run(“sh”, “-c”, …) (and cd inside the shell snippet).
Tilde Hint: If a literal cwd starts with ~ (e.g. with cwd("~/foo")) and the directory change fails at runtime (any non-zero exit), a hint is now printed to stderr:
hint: ‘~’ is not expanded; use env.HOME & “/path” or an absolute path.
Parser Hints
- Added error hint for missing whitespace around
&operator (e.g.,let x="a"&"b"orenv.HOME&"/path"):The & operator requires whitespace: env.HOME & “/x”
Comparisons & Breaking Changes
- No Implicit Expansion: sh2lang treats all string literals verbatim.
~/foois a literal path starting with~. Users must useenv.HOME & "/foo"or absolute paths.
Expression Interpolation
- Expression Interpolation (Limited):
$"..."string literals now support evaluating expressions inside{...}holes.- Supported:
$"Sum: {1 + 2}",$"Cwd: {pwd()}",$"User: {name}". - Limitation: String literals inside holes are not yet supported (e.g.,
$"X: { "value" }"will not compile) due to lexer tokenization constraints. Use variables as a workaround:let v = "value"; print($"X: {v}"). - Escape literal braces with
\{and\}(e.g.,$"Set: \{a, b\}"outputsSet: {a, b}). - A future release will address this limitation with lexer redesign to support full expression interpolation.
- Supported:
Pipe Blocks
Support for arbitrary statement blocks in pipelines:
pipe { ... } | { ... }run(...) | { ... }pipe { ... } | run(...)Mixed run/block stages are fully supported, with each stage running in an isolated subshell context.
Pipeline Sudo
Pipelines now accept sudo(...) stages:
run("cmd") | sudo("cmd", n=true)pipe { ... } | sudo(...)sudostages participate in the pipeline with correct pipefail and error handling, using the same options as standalonesudo(...).
Predicates
- Added
starts_with(text, prefix)builtin predicate.
Argument Access
- Added
argv()as an alias forargs()(returns all arguments as a list). - Fixed
arg(n)to avoid generating runtime calls toargvcommand in shell output.
Capture Improvements
- Fixed
capture(..., allow_fail=true)to correctly return captured stdout and updatestatus()without aborting the script on failure. - Added support for nested
allow_failoption directly onruncalls withincapture(e.g.capture(run(..., allow_fail=true))), which is hoisted to the capture behavior. - Fixed bash codegen so
capture(run(...), allow_fail=true)preservesstatus()(non-zero exit codes no longer clobbered). - Clarified that
capture(..., allow_fail=true)is only valid inletassignments. - Implemented Named Argument Policy (Hardening):
name=valuearguments are now strictly limited to builtins (run,sudo,sh,capture,confirm). General function calls are restricted to positional arguments, with clear diagnostics for violations. - Implemented Strict Command Word Model for
$(...): Command substitution now strictly interprets its content as command words. Generic function call shorthand$(func())is preserved via parser-level flattening tofunc, but named options are rejected within$(...). - Sudo Hardening: Refactored
sudo(...)lowering to unconditional inject the--separator before command arguments in all contexts (including command substitution), preventing flag injection attacks. Flattenedsudoarguments in the parser to ensure consistent behavior without double-quoting. - Fixed Bash codegen for
arg(expr)with dynamic indexes to properly quote arguments passed to__sh2_arg_by_indexusing a dedicated helper to ensure safe and deterministic forms. - Hardened
arg(expr)validation: non-integer indices (e.g., strings, nested calls) now produce a compile-time error.
Security & Correctness
- P0 Fix (Breaking Change / Correctness Fix): String literals (
"...") are now strict literals. They do not support implicit variable interpolation or Bash parameter expansion."$foo"and${bar}in string literals are preserved as literal text (e.g.print("$foo")prints$foo).- To use variables, use concatenation (
"Hello " & name) or explicit interpolation ($"Hello {name}"). - This change ensures that strings like
"$5"or"*"are strictly safe and never trigger unintended Bash behavior.
- contains() Type Safety Fix:
contains(haystack, needle)now uses robust static type dispatch instead of brittle runtime probing.- List Membership: Triggered for list literals, list expressions (e.g.
split), and tracked list variables (Bash-only). - Substring Search: Default behavior for strings and untracked variables (Portable).
- Improvements: Removed
declare -pprobing that caused false negatives. Added support forcontains(split(...), ...)via temporary variable materialization.
- List Membership: Triggered for list literals, list expressions (e.g.
- contains_line() Implementation (P0):
contains_line(file, needle)now correctly reads file contents with exact-line matching semantics.- Behavior: Uses
grep -Fqx -e <needle> <file>for exact-line matching within the file. - POSIX Portability: Uses
-eflag (POSIX-compliant) instead of--for robust handling of needles starting with-. - Use case: Ideal for registry trust checks, configuration validation, or any line-oriented file operations.
- Behavior: Uses
- contains() String Substring POSIX Fix (P1): Fixed
contains(string, string)to work correctly in POSIX targets and with needles starting with-.- Bug: Previously used non-POSIX
grep -Fq --which failed on POSIX sh and misinterpreted needles like"-b"as flags. - Fix: Changed to POSIX-compliant
grep -Fq -ematchingcontains_line()implementation. - Impact:
contains("a-b-c", "-b")now works correctly in both Bash and POSIX targets. - Special characters: All special chars (
$,[,],*,\) continue to be treated literally (fixed-string search).
- Bug: Previously used non-POSIX
- Boolean Encoding Standardization: Boolean variables are now consistently stored as
"true"and"false"strings (previously “1”/”0”).- Effect: Implicit print/string conversion for booleans is now supported (e.g.
print(true)outputstrue). - Back-compat: Generated conditions still use standard shell logic (
[ "$v" = "true" ]). Users relying on internal “1”/”0” representation (undocumented) will be affected.
- Effect: Implicit print/string conversion for booleans is now supported (e.g.
Changed
- Parser now allows mixed positional and named arguments for
sudo(...)in all syntactic forms (statement, expression, command substitution) - Diagnostic spans for option errors now highlight the specific option name rather than the entire expression
Fixed
- Corrected
sudo(...)lowering to use precise option-name spans for error messages - Removed legacy “positional arguments cannot follow named options” restriction in command substitution path
Documentation
- Updated
docs/language.mdwithsudo(...)andconfirm(...)sections - Updated
artifacts/grammar/sh2.ebnfto reflect implemented syntax - Updated
README.mdexamples - Updated
editors/vscodeextension for new builtins
Testing
- Added
tests/syntax_sudo.rswith 22 test cases - Added
tests/codegen_sudo.rssnapshot tests - Added
tests/syntax_confirm.rswith interactive and non-interactive fixtures - All tests pass on both bash and POSIX targets
Full changelog: v0.1.0…v0.1.1
👉 https://github.com/siu-mak/sh2lang