Files
bare-operating-system/packages/bare-os-booter/lib/shell/shell-syntax.js
T
2026-08-18 18:11:28 -04:00

270 lines
6.8 KiB
JavaScript

/**
* Pure shell syntax helpers (if/fi, loops, functions, case patterns).
*/
import { bareOsFnmatch } from './shell-glob.js'
/**
* Strip one layer of matching single/double quotes from alias value.
* @param {string} val
*/
export function stripAliasQuotes(val) {
const v = val.trim()
if (v.length >= 2) {
const q = v[0]
if ((q === "'" || q === '"') && v[v.length - 1] === q) {
return v.slice(1, -1)
}
}
return v
}
export function collapseShellLineContinuations(s) {
return String(s || '').replace(/\\\r?\n/g, '')
}
/**
* Split on `;` only at depth 0 (`if` / `fi` nesting).
* @param {Token[]} tokens
* @returns {Token[][]}
*/
export function splitTopLevelStatements(tokens) {
/** @type {Token[][]} */
const out = []
/** @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++
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
else if (
t.value === 'while' ||
t.value === 'until' ||
t.value === 'for' ||
t.value === 'select'
)
depth++
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 &&
parenDepth === 0 &&
braceDepth === 0
) {
if (cur.length) out.push(cur)
cur = []
} else {
cur.push(t)
}
}
if (cur.length) out.push(cur)
return out
}
/**
* Split on `&` at depth 0 (outside `if`/`fi`). Each segment except the last runs in the background.
* @param {Token[]} tokens
* @returns {Token[][]}
*/
export function splitTopLevelByAmpersand(tokens) {
/** @type {Token[][]} */
const out = []
/** @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++
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
else if (
t.value === 'while' ||
t.value === 'until' ||
t.value === 'for' ||
t.value === 'select'
)
depth++
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 &&
parenDepth === 0 &&
braceDepth === 0
) {
out.push(cur)
cur = []
} else {
cur.push(t)
}
}
out.push(cur)
return out
}
/**
* @param {Token[]} tokens
* @param {number} start index after `if`
*/
export function findThenIndex(tokens, start) {
let d = 1
for (let j = start; j < tokens.length; j++) {
const t = tokens[j]
if (t.type !== 'word') continue
if (t.value === 'if') d++
else if (t.value === 'fi') d--
else if (t.value === 'then' && d === 1) return j
}
return -1
}
/**
* After `then`, find `else` at depth 1 or closing `fi` at depth 0.
* @param {Token[]} tokens
* @param {number} start index after `then` token
* @returns {{ kind: 'else' | 'fi', idx: number } | null}
*/
export function findElseOrFiAfterThen(tokens, start) {
let d = 1
for (let j = start; j < tokens.length; j++) {
const t = tokens[j]
if (t.type !== 'word') continue
if (t.value === 'if') d++
else if (t.value === 'fi') {
d--
if (d === 0) return { kind: 'fi', idx: j }
} else if (t.value === 'else' && d === 1) return { kind: 'else', idx: j }
}
return null
}
/**
* @param {Token[]} tokens
* @param {number} start index after `else`
*/
export function findFiAfterElse(tokens, start) {
let d = 1
for (let j = start; j < tokens.length; j++) {
const t = tokens[j]
if (t.type !== 'word') continue
if (t.value === 'if') d++
else if (t.value === 'fi') {
d--
if (d === 0) return j
}
}
return -1
}
/**
* @param {Token[]} tokens
* @returns {number}
*/
export function findWhileDoSplit(tokens) {
for (let j = 1; j < tokens.length - 2; j++) {
const t = tokens[j]
if (t.type === 'op' && t.value === ';') {
const n = tokens[j + 1]
if (n && n.type === 'word' && n.value === 'do') return j
}
}
return -1
}
export function isValidShellIdentifier(name) {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
}
/**
* @param {Token[]} stmt
* @returns {{ name: string, body: Token[] } | null}
*/
export 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 {string} subject
* @param {string} pat
*/
export function casePatternMatches(subject, pat) {
if (pat.includes('|')) {
return pat.split('|').some((p) => casePatternMatches(subject, p.trim()))
}
if (pat === '*') return true
if (!/[*?\[]/.test(pat)) return subject === pat
return bareOsFnmatch(subject, pat)
}