sh: add POSIX-like -c support and complete shell subset gaps

Implement sh -c COMMAND [NAME [ARG...]] in /bin/sh, add shell function
declarations/invocation support, expand export semantics (NAME, NAME=value, -p),
and make until/loop-control behavior available by default. Add focused shell
tests and update sh man-page option docs.
This commit is contained in:
Raven Scott
2026-04-26 15:28:29 -04:00
parent 9471788f6b
commit 4cdd1569af
12 changed files with 490 additions and 38 deletions
+36 -3
View File
@@ -88,9 +88,9 @@ function bareOsEmitRaw(ctx, chunk) {
}
async function run(ctx, argv) {
const script = argv[1]
if (script == null) {
ctx.console.error('usage: sh SCRIPT')
const mode = argv[1]
if (mode == null) {
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
@@ -99,6 +99,39 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
if (mode === '-c') {
const command = argv[2]
if (command == null) {
ctx.console.error('sh: option requires an argument -- c')
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
const env = ctx.vfs?.env
/** @type {Record<string, string | undefined> | null} */
const saved = env && typeof env === 'object' ? {} : null
if (saved && env) {
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
saved[k] = env[k]
}
env['0'] = argv[3] ?? 'sh'
const args = argv.slice(4)
for (let i = 1; i <= 9; i++) env[String(i)] = args[i - 1] ?? ''
env['#'] = String(args.length)
}
try {
await ctx.execLine(command)
} finally {
if (saved && env) {
for (const [k, v] of Object.entries(saved)) {
if (v == null) delete env[k]
else env[k] = v
}
}
}
return
}
const script = mode
let buf
try {
buf = await ctx.vfs.readFile(script)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-04-26T19:04:18.264Z",
"generatedAt": "2026-04-26T19:19:08.534Z",
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
"commandIndex": [
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1777230258263,
"atMs": 1777231148534,
"commands": [
"agent",
"appctl",
+8 -3
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"generatedAt": "2026-04-26T19:04:18.469Z",
"generatedAt": "2026-04-26T19:19:08.715Z",
"pages": [
{
"name": "agent",
@@ -3211,8 +3211,13 @@
"synopsis": [
"sh [OPTION]... [OPERAND]..."
],
"description": "Runs a shell script file line-by-line via ctx.execLine (same language as the interactive booter shell). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [],
"description": "Runs shell commands via ctx.execLine (same language as the interactive booter shell). Supports command-string mode (`-c`) and script-file mode (`sh SCRIPT`). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [
{
"flag": "-c COMMAND [NAME [ARG ...]]",
"meaning": "Execute command string and set positional parameters for that command context."
}
],
"keywords": [
"sh",
"bare-os",
+259 -17
View File
@@ -633,11 +633,21 @@ export function tokenize(line) {
i++
continue
}
if (c === '{') {
tokens.push({ type: 'op', value: '{' })
i++
continue
}
if (c === ')') {
tokens.push({ type: 'op', value: ')' })
i++
continue
}
if (c === '}') {
tokens.push({ type: 'op', value: '}' })
i++
continue
}
if (c === '<') {
if (line[i + 1] === '<' && line[i + 2] === '<') {
tokens.push({ type: 'op', value: '<<<' })
@@ -705,7 +715,9 @@ export function tokenize(line) {
ch === ';' ||
ch === '&' ||
ch === '(' ||
ch === ')'
ch === ')' ||
ch === '{' ||
ch === '}'
)
break
cur.t += ch
@@ -1186,8 +1198,38 @@ export function splitTokensBySemicolon(tokens) {
const lists = []
/** @type {Token[]} */
let cur = []
let kwDepth = 0
let parenDepth = 0
let braceDepth = 0
for (const t of tokens) {
if (t.type === 'op' && t.value === ';') {
if (t.type === 'word') {
if (
t.value === 'if' ||
t.value === 'while' ||
t.value === 'until' ||
t.value === 'for' ||
t.value === 'case'
)
kwDepth++
else if (
t.value === 'fi' ||
t.value === 'done' ||
t.value === 'esac'
)
kwDepth = Math.max(0, kwDepth - 1)
} else if (t.type === 'op') {
if (t.value === '(') parenDepth++
else if (t.value === ')') parenDepth = Math.max(0, parenDepth - 1)
else if (t.value === '{') braceDepth++
else if (t.value === '}') braceDepth = Math.max(0, braceDepth - 1)
}
if (
t.type === 'op' &&
t.value === ';' &&
kwDepth === 0 &&
parenDepth === 0 &&
braceDepth === 0
) {
lists.push(cur)
cur = []
} else {
@@ -1210,8 +1252,38 @@ export function splitTokensByAndOr(tokens) {
const ops = []
/** @type {Token[]} */
let cur = []
let kwDepth = 0
let parenDepth = 0
let braceDepth = 0
for (const t of tokens) {
if (t.type === 'op' && (t.value === '&&' || t.value === '||')) {
if (t.type === 'word') {
if (
t.value === 'if' ||
t.value === 'while' ||
t.value === 'until' ||
t.value === 'for' ||
t.value === 'case'
)
kwDepth++
else if (
t.value === 'fi' ||
t.value === 'done' ||
t.value === 'esac'
)
kwDepth = Math.max(0, kwDepth - 1)
} else if (t.type === 'op') {
if (t.value === '(') parenDepth++
else if (t.value === ')') parenDepth = Math.max(0, parenDepth - 1)
else if (t.value === '{') braceDepth++
else if (t.value === '}') braceDepth = Math.max(0, braceDepth - 1)
}
if (
t.type === 'op' &&
(t.value === '&&' || t.value === '||') &&
kwDepth === 0 &&
parenDepth === 0 &&
braceDepth === 0
) {
segments.push(cur)
ops.push(t.value)
cur = []
@@ -1553,6 +1625,52 @@ async function execParsedPipeline(ctx, pipeline) {
let code = 'ok'
try {
if (
ctx.shellFunctions &&
typeof ctx.shellFunctions === 'object' &&
Object.prototype.hasOwnProperty.call(ctx.shellFunctions, name)
) {
const fn = ctx.shellFunctions[name]
const maxDepth = Number.parseInt(
String(env.BARE_OS_SHELL_FUNCTION_MAX_DEPTH || '32'),
10
)
const cap =
Number.isFinite(maxDepth) && maxDepth > 0 ? Math.min(maxDepth, 128) : 32
const curDepth =
typeof ctx.shellFunctionDepth === 'number' ? ctx.shellFunctionDepth : 0
if (curDepth >= cap) {
origErr.call(
ctx.console,
`shell: function recursion too deep (max ${cap})`
)
ctx.exitCode = 1
continue
}
const prevEnv = vfs.env
const fnEnv = env
const prevPositional = {}
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
prevPositional[k] = fnEnv[k]
}
assignShellFunctionPositionalEnv(fnEnv, argv)
vfs.env = fnEnv
ctx.env = fnEnv
ctx.shellFunctionDepth = curDepth + 1
try {
const fnResult = await execSemicolonLists(ctx, fn.body)
if (fnResult === 'exit') code = 'exit'
} finally {
for (const [k, v] of Object.entries(prevPositional)) {
if (v == null) delete fnEnv[k]
else fnEnv[k] = v
}
ctx.shellFunctionDepth = curDepth
vfs.env = prevEnv
ctx.env = prevEnv
}
continue
}
if (isShellBuiltin(name, env) && isExecLineBuiltinDenied(name, env)) {
origErr.call(
ctx.console,
@@ -1610,6 +1728,7 @@ async function execParsedPipeline(ctx, pipeline) {
origLog.call(ctx.console, 'export ' + k + '=' + q)
}
} else {
let sawErr = false
for (const a of args) {
if (a === '-p') {
origErr.call(
@@ -1617,22 +1736,45 @@ async function execParsedPipeline(ctx, pipeline) {
'export: -p must be the only argument'
)
ctx.exitCode = 2
continue
sawErr = true
break
}
if (a.startsWith('-')) {
origErr.call(ctx.console, 'export: unsupported option: ' + a)
ctx.exitCode = 2
sawErr = true
break
}
const eq = a.indexOf('=')
if (eq > 0) {
const k = a.slice(0, eq)
if (!isValidShellIdentifier(k)) {
origErr.call(ctx.console, `export: not an identifier: ${k}`)
ctx.exitCode = 1
sawErr = true
continue
}
if (
ctx.shellReadonlyVars instanceof Set &&
ctx.shellReadonlyVars.has(k)
) {
origErr.call(ctx.console, k + ': readonly variable')
ctx.exitCode = 1
sawErr = true
continue
}
env[k] = expandWord(a.slice(eq + 1), env)
} else {
if (!isValidShellIdentifier(a)) {
origErr.call(ctx.console, `export: not an identifier: ${a}`)
ctx.exitCode = 1
sawErr = true
continue
}
if (!Object.prototype.hasOwnProperty.call(env, a)) env[a] = ''
}
}
if (!sawErr) ctx.exitCode = 0
}
} else if (name === 'unset') {
if (!ctx.shellReadonlyVars) ctx.shellReadonlyVars = new Set()
@@ -2155,6 +2297,8 @@ function splitTopLevelStatements(tokens) {
/** @type {Token[]} */
let cur = []
let depth = 0
let parenDepth = 0
let braceDepth = 0
for (const t of tokens) {
if (t.type === 'word') {
if (t.value === 'if') depth++
@@ -2168,8 +2312,19 @@ function splitTopLevelStatements(tokens) {
else if (t.value === 'done') depth = Math.max(0, depth - 1)
else if (t.value === 'case') depth++
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
} else if (t.type === 'op') {
if (t.value === '(') parenDepth++
else if (t.value === ')') parenDepth = Math.max(0, parenDepth - 1)
else if (t.value === '{') braceDepth++
else if (t.value === '}') braceDepth = Math.max(0, braceDepth - 1)
}
if (t.type === 'op' && t.value === ';' && depth === 0) {
if (
t.type === 'op' &&
t.value === ';' &&
depth === 0 &&
parenDepth === 0 &&
braceDepth === 0
) {
if (cur.length) out.push(cur)
cur = []
} else {
@@ -2191,6 +2346,8 @@ function splitTopLevelByAmpersand(tokens) {
/** @type {Token[]} */
let cur = []
let depth = 0
let parenDepth = 0
let braceDepth = 0
for (const t of tokens) {
if (t.type === 'word') {
if (t.value === 'if') depth++
@@ -2204,8 +2361,19 @@ function splitTopLevelByAmpersand(tokens) {
else if (t.value === 'done') depth = Math.max(0, depth - 1)
else if (t.value === 'case') depth++
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
} else if (t.type === 'op') {
if (t.value === '(') parenDepth++
else if (t.value === ')') parenDepth = Math.max(0, parenDepth - 1)
else if (t.value === '{') braceDepth++
else if (t.value === '}') braceDepth = Math.max(0, braceDepth - 1)
}
if (t.type === 'op' && t.value === '&' && depth === 0) {
if (
t.type === 'op' &&
t.value === '&' &&
depth === 0 &&
parenDepth === 0 &&
braceDepth === 0
) {
out.push(cur)
cur = []
} else {
@@ -2408,13 +2576,6 @@ async function execSemicolonLists(ctx, toks, opts) {
* @returns {Promise<'ok' | 'exit' | 'break' | 'continue'>}
*/
async function execLoopBody(ctx, bodyToks) {
const loopCtl =
ctx.vfs?.env?.BARE_OS_SHELL_LOOP_CONTROL === '1' ||
ctx.vfs?.env?.BARE_OS_SHELL_LOOP_CONTROL === 'true'
if (!loopCtl) {
const r = await execSemicolonLists(ctx, bodyToks)
return r === 'exit' ? 'exit' : 'ok'
}
const lists = splitTokensBySemicolon(bodyToks)
for (const list of lists) {
if (!list.length) continue
@@ -2433,6 +2594,70 @@ async function execLoopBody(ctx, bodyToks) {
return 'ok'
}
/** @param {string} name */
function isValidShellIdentifier(name) {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
}
/**
* @param {Token[]} stmt
* @returns {{ name: string, body: Token[] } | null}
*/
function parseShellFunctionDeclaration(stmt) {
const n = stmt.length
if (n < 5) return null
let name = ''
let bodyStart = -1
if (
stmt[0]?.type === 'word' &&
stmt[0].value === 'function' &&
stmt[1]?.type === 'word'
) {
name = stmt[1].value
if (
stmt[2]?.type === 'op' &&
stmt[2].value === '(' &&
stmt[3]?.type === 'op' &&
stmt[3].value === ')'
) {
if (stmt[4]?.type === 'op' && stmt[4].value === '{') bodyStart = 5
} else if (stmt[2]?.type === 'op' && stmt[2].value === '{') {
bodyStart = 3
}
} else if (
stmt[0]?.type === 'word' &&
stmt[1]?.type === 'op' &&
stmt[1].value === '(' &&
stmt[2]?.type === 'op' &&
stmt[2].value === ')' &&
stmt[3]?.type === 'op' &&
stmt[3].value === '{'
) {
name = stmt[0].value
bodyStart = 4
}
if (!name || bodyStart < 0 || !isValidShellIdentifier(name)) return null
let depth = 1
let close = -1
for (let i = bodyStart; i < stmt.length; i++) {
const t = stmt[i]
if (t.type === 'op' && t.value === '{') depth++
else if (t.type === 'op' && t.value === '}') {
depth--
if (depth === 0) {
close = i
break
}
}
}
if (close < 0 || close !== stmt.length - 1) return null
const body = stmt.slice(bodyStart, close)
if (body.length && body[body.length - 1]?.type === 'op' && body[body.length - 1].value === ';') {
body.pop()
}
return { name, body }
}
/**
* @param {Token[]} tokens
* @returns {number}
@@ -2842,12 +3067,18 @@ async function dispatchShellStatement(ctx, stmt) {
const dr = tryExecShellDeclareBuiltin(ctx, stmt.slice(1))
if (dr != null) return dr
}
const fnDecl = parseShellFunctionDeclaration(stmt)
if (fnDecl) {
if (!ctx.shellFunctions || typeof ctx.shellFunctions !== 'object') {
ctx.shellFunctions = Object.create(null)
}
ctx.shellFunctions[fnDecl.name] = { body: fnDecl.body }
ctx.exitCode = 0
return 'ok'
}
if (head?.type === 'word' && head.value === 'if')
return execIfConstruct(ctx, stmt)
const untilOn =
shEnv &&
(shEnv.BARE_OS_SHELL_UNTIL === '1' || shEnv.BARE_OS_SHELL_UNTIL === 'true')
if (untilOn && head?.type === 'word' && head.value === 'until')
if (head?.type === 'word' && head.value === 'until')
return execUntilConstruct(ctx, stmt)
if (head?.type === 'word' && head.value === 'while')
return execWhileConstruct(ctx, stmt)
@@ -2954,6 +3185,17 @@ async function execAndOrList(ctx, tokens) {
return 'ok'
}
/**
* @param {Record<string, string>} env
* @param {string[]} argv
*/
function assignShellFunctionPositionalEnv(env, argv) {
env['0'] = argv[0] || ''
const fnArgs = argv.slice(1)
for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? ''
env['#'] = String(fnArgs.length)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} line
+96
View File
@@ -6215,6 +6215,102 @@ test('tier-1 sh shebang strip and symlink script path', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 sh -c executes command and sets positional args', async (t) => {
const dir = testCorestoreDir('shdashc')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pshc'))
await drive.ready()
await personal.ready()
await drive.put('/bin/sh', b4a.from(await readBuiltBin('sh')))
await drive.put(
'/bin/echo',
b4a.from(`async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`)
)
await drive.put(
'/bin/false',
b4a.from(`async function run(ctx) {
ctx.exitCode = 1
}
`)
)
const lines = []
const ctx = testCtx(drive, personal)
ctx.runBinCommand = function (argv, ro) {
return runBinCommand(this, argv, ro)
}
ctx.execLine = (line) => execShellLine(ctx, line)
ctx.console = {
log(s) {
lines.push(String(s))
},
error(s) {
lines.push('e:' + String(s))
}
}
ctx.exitCode = 0
await runBinCommand(ctx, ['sh', '-c', 'echo $0 $1 $2 $#', 'myshell', 'aa', 'bb'])
t.is(ctx.exitCode, 0)
t.ok(lines.some((l) => l.includes('myshell aa bb 2')))
ctx.exitCode = 0
await runBinCommand(ctx, ['sh', '-c', 'false'])
t.is(ctx.exitCode, 1)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine supports shell functions, export NAME, and until by default', async (t) => {
const dir = testCorestoreDir('shfunc')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pshf'))
await drive.ready()
await personal.ready()
await drive.put(
'/bin/echo',
b4a.from(`async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`)
)
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
log(s) {
lines.push(String(s))
},
error(s) {
lines.push('e:' + String(s))
}
}
ctx.exitCode = 0
await execShellLine(ctx, 'hello() { echo $1; }; hello world')
t.is(ctx.exitCode, 0)
t.ok(lines.includes('world'))
ctx.exitCode = 0
await execShellLine(ctx, 'export DEMO')
t.is(ctx.exitCode, 0)
t.is(ctx.vfs.env.DEMO, '')
ctx.exitCode = 0
await execShellLine(ctx, 'export BAD-NAME=1')
t.is(ctx.exitCode, 1)
ctx.exitCode = 0
await execShellLine(ctx, 'until false; do break; done')
t.is(ctx.exitCode, 0)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('tier-2 cat env touch mkdir', async (t) => {
const dir = testCorestoreDir('tier2tem')
const store = new Corestore(dir)
+7 -2
View File
@@ -5,8 +5,13 @@
"synopsis": [
"sh [OPTION]... [OPERAND]..."
],
"description": "Runs a shell script file line-by-line via ctx.execLine (same language as the interactive booter shell). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [],
"description": "Runs shell commands via ctx.execLine (same language as the interactive booter shell). Supports command-string mode (`-c`) and script-file mode (`sh SCRIPT`). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [
{
"flag": "-c COMMAND [NAME [ARG ...]]",
"meaning": "Execute command string and set positional parameters for that command context."
}
],
"keywords": [
"sh",
"bare-os",
+36 -3
View File
@@ -1,7 +1,7 @@
async function run(ctx, argv) {
const script = argv[1]
if (script == null) {
ctx.console.error('usage: sh SCRIPT')
const mode = argv[1]
if (mode == null) {
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
@@ -10,6 +10,39 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
if (mode === '-c') {
const command = argv[2]
if (command == null) {
ctx.console.error('sh: option requires an argument -- c')
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
const env = ctx.vfs?.env
/** @type {Record<string, string | undefined> | null} */
const saved = env && typeof env === 'object' ? {} : null
if (saved && env) {
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
saved[k] = env[k]
}
env['0'] = argv[3] ?? 'sh'
const args = argv.slice(4)
for (let i = 1; i <= 9; i++) env[String(i)] = args[i - 1] ?? ''
env['#'] = String(args.length)
}
try {
await ctx.execLine(command)
} finally {
if (saved && env) {
for (const [k, v] of Object.entries(saved)) {
if (v == null) delete env[k]
else env[k] = v
}
}
}
return
}
const script = mode
let buf
try {
buf = await ctx.vfs.readFile(script)
+36 -3
View File
@@ -88,9 +88,9 @@ function bareOsEmitRaw(ctx, chunk) {
}
async function run(ctx, argv) {
const script = argv[1]
if (script == null) {
ctx.console.error('usage: sh SCRIPT')
const mode = argv[1]
if (mode == null) {
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
@@ -99,6 +99,39 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
if (mode === '-c') {
const command = argv[2]
if (command == null) {
ctx.console.error('sh: option requires an argument -- c')
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
ctx.exitCode = 2
return
}
const env = ctx.vfs?.env
/** @type {Record<string, string | undefined> | null} */
const saved = env && typeof env === 'object' ? {} : null
if (saved && env) {
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
saved[k] = env[k]
}
env['0'] = argv[3] ?? 'sh'
const args = argv.slice(4)
for (let i = 1; i <= 9; i++) env[String(i)] = args[i - 1] ?? ''
env['#'] = String(args.length)
}
try {
await ctx.execLine(command)
} finally {
if (saved && env) {
for (const [k, v] of Object.entries(saved)) {
if (v == null) delete env[k]
else env[k] = v
}
}
}
return
}
const script = mode
let buf
try {
buf = await ctx.vfs.readFile(script)
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-04-26T19:04:18.264Z",
"generatedAt": "2026-04-26T19:19:08.534Z",
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
"commandIndex": [
{
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1777230258263,
"atMs": 1777231148534,
"commands": [
"agent",
"appctl",
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"generatedAt": "2026-04-26T19:04:18.469Z",
"generatedAt": "2026-04-26T19:19:08.715Z",
"pages": [
{
"name": "agent",
@@ -3211,8 +3211,13 @@
"synopsis": [
"sh [OPTION]... [OPERAND]..."
],
"description": "Runs a shell script file line-by-line via ctx.execLine (same language as the interactive booter shell). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [],
"description": "Runs shell commands via ctx.execLine (same language as the interactive booter shell). Supports command-string mode (`-c`) and script-file mode (`sh SCRIPT`). This is not the interactive REPL; see man bare-os-shell for session builtins, pipelines, and jobs.",
"options": [
{
"flag": "-c COMMAND [NAME [ARG ...]]",
"meaning": "Execute command string and set positional parameters for that command context."
}
],
"keywords": [
"sh",
"bare-os",