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

210 lines
6.0 KiB
JavaScript

/**
* Default aliases, alias/unalias builtins, and ~/.barerc loader.
*/
import { stripAliasQuotes } from './shell-syntax.js'
import { tokenize } from './shell-token.js'
import { expandWord } from './shell-expand.js'
import {
applyBareOsThemeFromEnv,
bareOsGetThemePreset
} from './bare-os-theme-presets.js'
/** Max alias indirections (prevents cycles). */
const MAX_ALIAS_DEPTH = 16
/**
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
* @returns {Record<string, string>}
*/
export function defaultShellAliases() {
return {
nano: 'edit',
top: 'baretop',
btop: 'baretop',
ll: 'ls -la',
la: 'ls -A',
l: 'ls',
'..': 'cd ..',
'...': 'cd ../..'
}
}
/**
* Expand first argv[0] through alias chain; append original argv.slice(1).
* @param {string[]} argv
* @param {Record<string, string> | null | undefined} aliases
* @returns {string[]}
*/
export function expandArgvAliases(argv, aliases) {
if (!argv.length) return argv
const map = aliases && typeof aliases === 'object' ? aliases : {}
const out = [...argv]
let depth = 0
while (depth < MAX_ALIAS_DEPTH) {
const first = out[0]
const repl = map[first]
if (repl == null || repl === '') break
const words = tokenize(repl)
.filter((t) => t.type === 'word')
.map((t) => t.value)
if (!words.length) break
out.splice(0, 1, ...words)
depth++
}
if (depth >= MAX_ALIAS_DEPTH && map[out[0]]) {
throw new Error('alias: expansion nested too deeply')
}
return out
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} rest content after `alias ` (name=value...)
*/
export function applyAliasDefinition(ctx, rest) {
const eq = rest.indexOf('=')
if (eq <= 0) return false
const aname = rest.slice(0, eq).trim()
if (!aname) return false
let val = rest.slice(eq + 1).trim()
val = stripAliasQuotes(val)
if (!ctx.shellAliases) ctx.shellAliases = {}
ctx.shellAliases[aname] = val
return true
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv argv for unalias builtin (includes 'unalias')
*/
export function runUnaliasBuiltin(ctx, argv, logError) {
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
const args = argv.slice(1)
if (args.length === 0) {
logError('unalias: missing name')
return
}
if (args.includes('-a')) {
ctx.shellAliases = { ...defaultShellAliases() }
return
}
for (const name of args) {
if (name === '-a') continue
delete ctx.shellAliases[name]
}
}
/**
* Parse one line from ~/.barerc (without leading `unalias`).
*/
export function applyBarercUnalias(ctx, rest) {
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
const parts = rest.split(/\s+/).filter(Boolean)
if (parts.length === 1 && parts[0] === '-a') {
ctx.shellAliases = { ...defaultShellAliases() }
return
}
for (const name of parts) {
if (name === '-a') ctx.shellAliases = { ...defaultShellAliases() }
else delete ctx.shellAliases[name]
}
}
/**
* Comment-only template written on first login when `~/.barerc` is absent
* (`loadBarerc(ctx, { createSkeletonIfMissing: true })`).
*/
export const BARERC_SKELETON = `# Bare OS — ~/.barerc (not full sh; only export, alias, unalias, theme, # comments).
#
# export MY_VAR=value
# theme default
# export BARE_OS_COLOR_DEPTH=truecolor
# export BARE_OS_DIRCOLORS=~/.dir_colors
# export BARE_OS_LS_COLORS_LOCKED=1
# alias gst='git status'
# unalias ll
`
/**
* Load `~/.barerc`: only `export`, `alias`, `unalias`, `theme`, comments, blank lines.
* Resets aliases to defaults first, then applies file.
* @param {Record<string, unknown>} ctx
* @param {{ createSkeletonIfMissing?: boolean }} [opts] If true and the file is missing, write {@link BARERC_SKELETON} (login / unlock only).
*/
export async function loadBarerc(ctx, opts = {}) {
const { createSkeletonIfMissing = false } = opts
const strict = globalThis.process?.env?.BARE_OS_STRICT_BARC === '1'
ctx.shellAliases = { ...defaultShellAliases() }
const vfs = ctx.vfs
const env = vfs.env
let buf = null
try {
buf = await vfs.readFile('~/.barerc')
} catch {
buf = null
}
let text = null
if (!buf && createSkeletonIfMissing) {
try {
await vfs.writeFile('~/.barerc', ctx.b4a.from(BARERC_SKELETON))
text = BARERC_SKELETON
} catch (e) {
ctx.console?.error?.(
'[bare-os] could not create ~/.barerc: ' + ((e && e.message) || e)
)
}
}
if (!text) {
if (!buf) {
await applyBareOsThemeFromEnv(ctx)
return
}
text = ctx.b4a.toString(buf)
}
for (const line of text.split(/\r?\n/)) {
const t = line.trim()
if (!t || t.startsWith('#')) continue
if (t.startsWith('export ')) {
const rest = t.slice(7).trim()
const eq = rest.indexOf('=')
if (eq > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rest.slice(0, eq))) {
env[rest.slice(0, eq)] = expandWord(rest.slice(eq + 1), env)
} else if (strict) {
ctx.console?.error?.('barerc: ignored: ' + t)
}
continue
}
if (t.startsWith('theme ') || t === 'theme') {
const name = t === 'theme' ? '' : t.slice(6).trim()
if (!name) {
if (strict) ctx.console?.error?.('barerc: theme requires a name')
continue
}
if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
if (strict) ctx.console?.error?.('barerc: invalid theme name: ' + name)
continue
}
const norm = name.toLowerCase().replace(/\s+/g, '_')
if (!bareOsGetThemePreset(norm)) {
ctx.console?.error?.('barerc: unknown theme: ' + name)
continue
}
env.BARE_OS_THEME = norm
continue
}
if (t.startsWith('alias ')) {
const ok = applyAliasDefinition(ctx, t.slice(6).trim())
if (!ok && strict) ctx.console?.error?.('barerc: ignored: ' + t)
continue
}
if (t.startsWith('unalias ')) {
applyBarercUnalias(ctx, t.slice(8).trim())
continue
}
if (strict) ctx.console?.error?.('barerc: ignored: ' + t)
}
await applyBareOsThemeFromEnv(ctx)
}