Updates to coreutils

This commit is contained in:
Raven Scott
2026-04-03 22:37:04 -04:00
parent 35ac6633ea
commit eeef3da6e1
17 changed files with 883 additions and 120 deletions
@@ -2,4 +2,4 @@
* Semantic version of the booter `ctx` contract for custom kernels.
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
*/
export const BARE_OS_CTX_API_VERSION = '1.7.0'
export const BARE_OS_CTX_API_VERSION = '1.7.1'
+5
View File
@@ -78,6 +78,11 @@ export interface BareOsKernelContext {
execLine(line: string, opts?: BareOsAbortOpts): Promise<string>
readLine(prompt?: string, opts?: BareOsAbortOpts): Promise<string | null>
runBinCommand(argv: string[], opts?: BareOsAbortOpts): Promise<unknown>
/**
* Set by the shell when this **`/bin`** commands stdout is **captured** (pipeline to the next stage, or **`>`** / **`>>`** on the last stage).
* Utilities may emit **one record per line** instead of multi-column text (e.g. **`ls`**).
*/
bareOsStdoutCaptured?: boolean
/** Optional raw output hook for NUL/binary (e.g. **`printenv -0`**, **`find -print0`**) when **`process.stdout.write`** is unavailable. */
bareOsBinWrite?(chunk: Uint8Array | string): void
bareOsIpc?: BareOsIpc
+27 -8
View File
@@ -717,6 +717,21 @@ function segmentHasCommand(seg) {
return cmd.argv.length > 0 || Object.keys(cmd.assign).length > 0
}
/**
* Shallow clone for pipeline **`/bin`** execution: session **`env`**, optional **`shellStdin`**, and
* **`bareOsStdoutCaptured`** when stdout is captured (pipe to next stage or **`>`** redirect) so
* utilities (e.g. **`ls`**) can use one-record-per-line output.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string>} env
* @param {string | null} stdinText
* @param {boolean} bareOsStdoutCaptured
*/
function bareOsPipelineChildCtx(ctx, env, stdinText, bareOsStdoutCaptured) {
const o = Object.assign({}, ctx, { env, bareOsStdoutCaptured })
if (stdinText != null) o.shellStdin = stdinText
return o
}
/**
* @param {Record<string, unknown>} ctx
* @param {SimpleCmd[]} pipeline
@@ -956,10 +971,12 @@ async function execParsedPipeline(ctx, pipeline) {
}
}
} else {
const childCtx =
stdinText != null
? Object.assign({}, ctx, { shellStdin: stdinText, env })
: Object.assign({}, ctx, { env })
const childCtx = bareOsPipelineChildCtx(
ctx,
env,
stdinText,
capOut
)
await runBinCommand(childCtx, cargs)
mergeChildExitCode(ctx, childCtx)
}
@@ -1073,10 +1090,12 @@ async function execParsedPipeline(ctx, pipeline) {
ctx.requestBooterExit(ec)
}
} else {
const childCtx =
stdinText != null
? Object.assign({}, ctx, { shellStdin: stdinText, env })
: Object.assign({}, ctx, { env })
const childCtx = bareOsPipelineChildCtx(
ctx,
env,
stdinText,
capOut
)
try {
await runBinCommand(childCtx, argv)
mergeChildExitCode(ctx, childCtx)
+158
View File
@@ -1657,6 +1657,94 @@ async function run(ctx) {
rmSync(dir, { recursive: true, force: true })
})
test('ls bareOsStdoutCaptured lists one name per line', async (t) => {
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
const lsSrc = await readFile(lsPath, 'utf8')
const dir = testCorestoreDir('lscap')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('plcap'))
await drive.ready()
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (s) => lines.push(String(s)),
error: (...a) => lines.push(a.join(' '))
}
await ctx.vfs.writeFile('aaa', b4a.from(''))
await ctx.vfs.writeFile('lib', b4a.from(''))
await ctx.vfs.writeFile('zzz', b4a.from(''))
await runBinCommand(
Object.assign({}, ctx, { bareOsStdoutCaptured: true }),
['ls']
)
t.is(lines.length, 3)
t.ok(lines.includes('aaa') && lines.includes('lib') && lines.includes('zzz'))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine ls pipe grep prints only matching entry line', async (t) => {
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
const grepPath = path.join(__dirname, '../../kernel/bin/grep')
const lsSrc = await readFile(lsPath, 'utf8')
const grepSrc = await readFile(grepPath, 'utf8')
const dir = testCorestoreDir('lspipegrep')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('plpg'))
await drive.ready()
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
await drive.put('/bin/grep', b4a.from(grepSrc))
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (s) => lines.push(String(s)),
error: (...a) => lines.push(a.join(' '))
}
await ctx.vfs.writeFile('aaa', b4a.from(''))
await ctx.vfs.writeFile('lib', b4a.from(''))
await ctx.vfs.writeFile('zzz', b4a.from(''))
await execShellLine(ctx, 'ls | grep -F lib')
t.is(ctx.exitCode, 0)
t.alike(lines, ['lib'])
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine ls pipe wc -l counts lines', async (t) => {
const lsPath = path.join(__dirname, '../../kernel/bin/ls')
const wcPath = path.join(__dirname, '../../kernel/bin/wc')
const lsSrc = await readFile(lsPath, 'utf8')
const wcSrc = await readFile(wcPath, 'utf8')
const dir = testCorestoreDir('lspipewc')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('plwc'))
await drive.ready()
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
await drive.put('/bin/wc', b4a.from(wcSrc))
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (s) => lines.push(String(s)),
error: (...a) => lines.push(a.join(' '))
}
await ctx.vfs.writeFile('a', b4a.from(''))
await ctx.vfs.writeFile('b', b4a.from(''))
await ctx.vfs.writeFile('c', b4a.from(''))
await execShellLine(ctx, 'ls | wc -l')
t.is(ctx.exitCode, 0)
const out = lines.join('\n').trim()
t.is(out, '3', 'three directory entries => three lines')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine runs cd and external', async (t) => {
const dir = testCorestoreDir('sh')
const store = new Corestore(dir)
@@ -2287,6 +2375,48 @@ test('tier-1 sort and wc flags', async (t) => {
await runBinCommand(ctx, ['wc', '-c', 'nums.txt'])
t.is(lines[0], ' 7 nums.txt')
const errs = []
ctx.console = {
log(s) {
lines.push(String(s))
},
error(...a) {
errs.push(a.join(' '))
}
}
await ctx.vfs.writeFile('ord.txt', b4a.from('a\nb\nc\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sort', '-c', 'ord.txt'])
t.is(ctx.exitCode, 0)
t.is(errs.length, 0)
await ctx.vfs.writeFile('bad.txt', b4a.from('b\na\n'))
lines.length = 0
errs.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sort', '-c', 'bad.txt'])
t.is(ctx.exitCode, 1)
t.ok(errs.some((e) => e.includes('disorder')))
errs.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sort', '-C', 'bad.txt'])
t.is(ctx.exitCode, 1)
t.is(errs.length, 0)
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sort', '-o', 'sorted.txt', '-n', 'nums.txt'])
t.is(ctx.exitCode, 0)
const sortedBuf = await ctx.vfs.readFile('sorted.txt')
t.is(ctx.b4a.toString(sortedBuf), '1\n2\n10\n')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sort', '-s', 'ord.txt'])
t.is(lines.join('\n'), 'a\nb\nc')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
@@ -2334,6 +2464,34 @@ test('tier-1 find du basename options', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine find pipes to wc -l', async (t) => {
const dir = testCorestoreDir('findpipewc')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('fpwc'))
await drive.ready()
await personal.ready()
await drive.put('/bin/find', b4a.from(await readBuiltBin('find')))
await drive.put('/bin/wc', b4a.from(await readBuiltBin('wc')))
const lines = []
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error() {}
}
await ctx.vfs.writeFile('p.txt', b4a.from(''))
await execShellLine(ctx, 'find . -maxdepth 1 | wc -l')
t.is(ctx.exitCode, 0)
const n = Number.parseInt(String(lines[0] || '').trim(), 10)
t.ok(Number.isFinite(n) && n >= 1)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 date format and test numeric', async (t) => {
const dir = testCorestoreDir('date-test')
const store = new Corestore(dir)
+5 -3
View File
@@ -39,13 +39,15 @@ Or `node packages/bare-os-coreutils/build.mjs`.
**`ls`** prepends **[`bare-os-lscolors`](../bare-os-lscolors/bare-os-lscolors.js)** for **`LS_COLORS`** / dircolors parsing. **`dircolors`** and **`theme`** integrate with the booters **`bare-os-theme-presets.js`** (see [docs/themes/README.md](../../docs/themes/README.md)).
**`grep`** uses JavaScript **`RegExp`** (and **`-F`** fixed strings); POSIX/GNU-like **subset** (including **`-x`**, **`-m`**, **`-o`** among common flags).
**`grep`** uses JavaScript **`RegExp`** (and **`-F`** fixed strings); POSIX/GNU-like **subset** (including **`-x`**, **`-m`**, **`-o`** among common flags). It is **not** PCRE- or GNU-bit-identical; use **`-F`** when literals must not be treated as regex.
**`sed`** / **`awk`** use large interpreters in **`lib/sed-engine.js`** and **`lib/awk-engine.js`** — capable, but not guaranteed to match every POSIX or GNU edge case.
**`sed`** / **`awk`** use large interpreters in **`lib/sed-engine.js`** and **`lib/awk-engine.js`** — capable, but **not** guaranteed to match every POSIX or GNU edge case; treat parity as best-effort.
**`mkfifo`** creates simulated named pipes under **`/run/bare-os/ipc/<name>`** (in-memory; see booter VFS). **`getconf`** and **`xargs`** implement a documented Bare-specific subset (see `src/getconf.js`, `src/xargs.js`).
**Recent parity / UX:** **`tail -f`** (watch or poll; **`BARE_OS_TAIL_F_*`** env), **`head`/`tail -c`** and **`+` line/byte offsets**, **`sort -n/-r/-u/-f`**, **`wc -l/-w/-c`**, **`grep -A/-B/-C`** and **`--color`**, **`date +FORMAT`** (strftime-like subset), **`test`** integer compares and **`-h`/`-L`**, **`xargs -I`**, **`find -iname`/`-print0`**, **`du -h`**, **`basename -a`/`-s`**, **`dirname -z`**. Phase 2 adds **`cat -n/-A`**, **`env -i`**, **`touch -a/-m/-d/-r`**, **`mkdir -m`**, **`grep -r`**, **`sort -k/-t`**, **`cp`/`mv` `-L`/`-P`**, **`readlink -f`**, **`rmdir -p`**, **`du -a/-L`**, **`find -mtime`/`-newer`/`-prune`/`-empty`/`-delete`** (gated), **`stat --format`**, **`cut -s`**, **`tr [:class:]`**, richer **`od`/`nl`/`seq`/`pathchk`/`printf`/`time`**, and **`bareOsBinWrite`** for captured NUL output in tests.
**Pipelines:** the shell sets **`ctx.bareOsStdoutCaptured`** when a commands stdout is captured (pipe or **`>`** / **`>>`**). **`ls`** prints **one name per line** in short mode in that case (GNU-like), so **`ls | grep`** / **`sort`** / **`wc`** see one record per line.
**Recent parity / UX:** **`tail -f`** (watch or poll; **`BARE_OS_TAIL_F_*`** env), **`head`/`tail -c`** and **`+` line/byte offsets**, **`sort -n/-r/-u/-f/-k/-t`**, **`sort -c/-C`** (check), **`-s`** (stable), **`-o`**, **`wc -l/-w/-c`**, **`grep -A/-B/-C`** and **`--color`**, **`date +FORMAT`** (strftime-like subset), **`test`** integer compares and **`-h`/`-L`**, **`xargs -I`**, **`find -iname`/`-print0`**, **`du -h`**, **`basename -a`/`-s`**, **`dirname -z`**. Phase 2 adds **`cat -n/-A`**, **`env -i`**, **`touch -a/-m/-d/-r`**, **`mkdir -m`**, **`grep -r`**, **`cp`/`mv` `-L`/`-P`**, **`readlink -f`**, **`rmdir -p`**, **`du -a/-L`**, **`find -mtime`/`-newer`/`-prune`/`-empty`/`-delete`** (gated), **`stat --format`**, **`cut -s`**, **`tr [:class:]`**, richer **`od`/`nl`/`seq`/`pathchk`/`printf`/`time`**, and **`bareOsBinWrite`** for captured NUL output in tests.
See [handbook §6 — Kernel and `/bin`](../../handbook/06-kernel-and-binaries.md), [handbook §9 — POSIX alignment](../../handbook/09-posix-utilities-shell-and-vfs.md), and [handbook §10 — `man` and online help](../../handbook/10-manpages-and-online-help.md).
+4 -4
View File
@@ -2,8 +2,8 @@
"name": "ls",
"section": 1,
"title": "list directory contents",
"synopsis": ["ls [-1al] [--color[=never|auto|always]] [FILE...]"],
"description": "Lists directory contents. Long format shows mode, links, owner, group, size, mtime, and symlink targets. With color (default auto on a TTY), directories, symlinks, executables, and permission bits are highlighted.",
"synopsis": ["ls [-1al] [--color[=never|auto|always]] [--format=single-column] [FILE...]"],
"description": "Lists directory contents. Long format shows mode, links, owner, group, size, mtime, and symlink targets. With color (default auto on a TTY), directories, symlinks, executables, and permission bits are highlighted. When stdout is captured by the shell (pipeline or **>** redirect), short format lists **one name per line** (GNU-like), so tools such as **grep** and **sort** see one entry per line.",
"options": [
{
"flag": "-a",
@@ -14,8 +14,8 @@
"meaning": "Long listing"
},
{
"flag": "-1",
"meaning": "One name per line (short format)"
"flag": "-1, --format=single-column, --format=vertical",
"meaning": "One name per line (short format); same layout when stdout is piped or redirected"
},
{
"flag": "--color[=never|auto|always]",
+16 -4
View File
@@ -2,15 +2,19 @@
"name": "sort",
"section": 1,
"title": "sort lines",
"synopsis": ["sort [OPTION]... [OPERAND]..."],
"description": "Sorts lines from files or stdin. Supports numeric sort, reverse, unique consecutive lines, and case fold for sort keys.",
"synopsis": ["sort [OPTION]... [FILE]..."],
"description": "Sorts lines from files or stdin. Collation follows JavaScript string ordering (and numeric keys when **`-n`**). Stable ordering is only guaranteed when **`-s`** is set.",
"options": [
{ "flag": "-c, --check", "meaning": "Check whether input is already sorted; exit **1** if not; print a **disorder** diagnostic to stderr (GNU-style)" },
{ "flag": "-C, --check=quiet, --check=silent", "meaning": "Like **`-c`** but no stderr message on failure" },
{ "flag": "-o, --output FILE", "meaning": "Write result to **FILE** instead of stdout (place **`-o`** before file operands)" },
{ "flag": "-s, --stable", "meaning": "Stable sort (preserve original order when keys compare equal)" },
{ "flag": "-n, --numeric-sort, -g", "meaning": "Sort by leading numeric prefix" },
{ "flag": "-r, --reverse", "meaning": "Reverse sort order" },
{ "flag": "-u, --unique", "meaning": "Suppress duplicate lines after sorting" },
{ "flag": "-u, --unique", "meaning": "Suppress duplicate lines after sorting; with **`-c`**, require strictly increasing keys (no adjacent duplicates)" },
{ "flag": "-f, --ignore-case", "meaning": "Fold case for ordering" },
{ "flag": "-t, --field-separator SEP", "meaning": "Field delimiter for **`-k`** (use **\\t** for tab)" },
{ "flag": "-k, --key POS", "meaning": "Sort by 1-based field **POS** or **START,END** (bare subset; blank-separated fields when **`-t`** omitted)" },
{ "flag": "-k, --key POS", "meaning": "Sort by 1-based field **POS** or **START,END** (blank-separated fields when **`-t`** omitted)" },
{ "flag": "-", "meaning": "Operand reads stdin" }
],
"keywords": ["sort", "bare-os", "coreutils"],
@@ -26,6 +30,14 @@
{
"caption": "unique",
"code": "sort -u tags.txt"
},
{
"caption": "verify sorted",
"code": "sort -c sorted.txt"
},
{
"caption": "write to file",
"code": "sort -o out.txt -n nums.txt"
}
],
"listCategory": "coreutils"
+30 -2
View File
@@ -2,6 +2,8 @@ async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
let longFmt = false
/** One name per line (GNU **`-1`** / **`--format=single-column`**, or piped/captured stdout). */
let singleColumn = false
/** @type {'never' | 'auto' | 'always'} */
let colorMode = 'auto'
const paths = []
@@ -23,6 +25,10 @@ async function run(ctx, argv) {
else colorMode = 'auto'
continue
}
if (a === '--format=single-column' || a === '--format=vertical') {
singleColumn = true
continue
}
ctx.console.error('ls: unrecognized option ' + a)
ctx.exitCode = 2
return
@@ -32,7 +38,7 @@ async function run(ctx, argv) {
const c = a[j]
if (c === 'a') showAll = true
else if (c === 'l') longFmt = true
else if (c === '1') longFmt = false
else if (c === '1') singleColumn = true
}
continue
}
@@ -40,6 +46,8 @@ async function run(ctx, argv) {
}
const targets = paths.length ? paths : ['.']
const useColor = bareLsUseColor(ctx, colorMode)
const onePerLine =
singleColumn || ctx.bareOsStdoutCaptured === true
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
@@ -61,7 +69,27 @@ async function run(ctx, argv) {
}
if (!showAll) names = names.filter((n) => !n.startsWith('.'))
if (!longFmt) {
if (!useColor) {
if (onePerLine) {
for (const n of names) {
if (!useColor) {
ctx.console.log(n)
} else {
const sub =
singleEntryPath != null
? singleEntryPath
: t === '.' || t === './'
? n
: t.replace(/\/$/, '') + '/' + n
let st = null
try {
st = await vfs.lstat(sub)
} catch {
st = null
}
ctx.console.log(bareLsColorWrap(n, st, true, ctx))
}
}
} else if (!useColor) {
ctx.console.log(names.join(' '))
} else {
const parts = []
+188 -28
View File
@@ -21,12 +21,75 @@ function sortKeySlice(line, delim, start1, end1) {
return slice.join(' ')
}
/**
* @param {object} o
* @param {string} o.line
* @param {boolean} o.numeric
* @param {boolean} o.fold
* @param {number | null} o.keyStart
* @param {number | null} o.keyEnd
* @param {string | null} o.dForKey
*/
function sortKeyObj(o) {
const keyText =
o.keyStart != null
? sortKeySlice(o.line, o.dForKey, o.keyStart, o.keyEnd)
: o.line
if (o.numeric) {
const m = String(keyText).match(
/^\s*(-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
)
if (m) return { n: Number(m[1]), raw: keyText }
return { n: Number.POSITIVE_INFINITY, raw: keyText }
}
if (o.fold) return { n: 0, raw: keyText.toLowerCase() }
return { n: 0, raw: keyText }
}
/**
* @param {string} x
* @param {string} y
* @param {object} opts
* @param {boolean} opts.numeric
* @param {boolean} opts.reverse
* @param {boolean} opts.fold
* @param {number | null} opts.keyStart
* @param {number | null} opts.keyEnd
* @param {string | null} opts.dForKey
* @returns {number}
*/
function sortCompareLines(x, y, opts) {
const base = {
numeric: opts.numeric,
fold: opts.fold,
keyStart: opts.keyStart,
keyEnd: opts.keyEnd,
dForKey: opts.dForKey
}
const kx = sortKeyObj({ ...base, line: x })
const ky = sortKeyObj({ ...base, line: y })
if (opts.numeric) {
if (kx.n !== ky.n) {
const ord = kx.n < ky.n ? -1 : 1
return opts.reverse ? -ord : ord
}
}
const cmp =
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
return opts.reverse ? -cmp : cmp
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let numeric = false
let reverse = false
let uniq = false
let fold = false
/** @type {'off' | 'verbose' | 'quiet'} */
let checkMode = 'off'
let stable = false
/** @type {string | null} */
let outFile = null
/** @type {string | undefined} */
let delim
/** @type {number | null} */
@@ -40,6 +103,52 @@ async function run(ctx, argv) {
i++
break
}
if (a === '--check' || a === '--check=diagnose-first') {
checkMode = 'verbose'
i++
continue
}
if (a === '--check=silent' || a === '--check=quiet') {
checkMode = 'quiet'
i++
continue
}
if (a === '-C') {
checkMode = 'quiet'
i++
continue
}
if (a === '-c') {
checkMode = 'verbose'
i++
continue
}
if (a === '-s' || a === '--stable') {
stable = true
i++
continue
}
if (a === '-o' || a === '--output') {
const f = argv[++i]
if (f === undefined) {
ctx.console.error('sort: option requires an argument -- output')
ctx.exitCode = 1
return
}
outFile = f
i++
continue
}
if (a.startsWith('--output=')) {
outFile = a.slice('--output='.length) || null
if (!outFile) {
ctx.console.error('sort: option requires an argument -- output')
ctx.exitCode = 1
return
}
i++
continue
}
if (a === '-n' || a === '--numeric-sort' || a === '-g') {
numeric = true
i++
@@ -130,6 +239,9 @@ async function run(ctx, argv) {
else if (c === 'r') reverse = true
else if (c === 'u') uniq = true
else if (c === 'f') fold = true
else if (c === 'c') checkMode = 'verbose'
else if (c === 'C') checkMode = 'quiet'
else if (c === 's') stable = true
else {
ok = false
break
@@ -172,37 +284,73 @@ async function run(ctx, argv) {
}
const dForKey = delim === undefined ? null : delim
/** @param {string} s */
function sortKey(s) {
const keyText =
keyStart != null
? sortKeySlice(s, dForKey, keyStart, keyEnd)
: s
if (numeric) {
const m = String(keyText).match(
/^\s*(-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
)
if (m) return { n: Number(m[1]), raw: keyText }
return { n: Number.POSITIVE_INFINITY, raw: keyText }
}
if (fold) return { n: 0, raw: keyText.toLowerCase() }
return { n: 0, raw: keyText }
const cmpOpts = {
numeric,
reverse,
fold,
keyStart,
keyEnd,
dForKey
}
lines.sort((x, y) => {
const kx = sortKey(x)
const ky = sortKey(y)
if (numeric) {
if (kx.n !== ky.n) {
const ord = kx.n < ky.n ? -1 : 1
return reverse ? -ord : ord
if (checkMode !== 'off') {
const checkLabel = files.length === 1 ? files[0] : '-'
for (let li = 0; li < lines.length - 1; li++) {
const aLine = lines[li]
const bLine = lines[li + 1]
const c = sortCompareLines(aLine, bLine, cmpOpts)
if (uniq && c === 0) {
ctx.exitCode = 1
if (checkMode === 'verbose') {
ctx.console.error(
'sort: ' +
checkLabel +
':' +
(li + 2) +
': disorder: ' +
bLine
)
}
return
}
if (c > 0) {
ctx.exitCode = 1
if (checkMode === 'verbose') {
ctx.console.error(
'sort: ' +
checkLabel +
':' +
(li + 2) +
': disorder: ' +
bLine
)
}
return
}
}
const cmp =
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
return reverse ? -cmp : cmp
})
ctx.exitCode = 0
return
}
/** @param {string} x
* @param {string} y */
function cmpPair(x, y) {
const primary = sortCompareLines(x, y, cmpOpts)
if (primary !== 0 || !stable) return primary
return 0
}
if (stable) {
const decorated = lines.map((line, idx) => ({ line, idx }))
decorated.sort((a, b) => {
const p = sortCompareLines(a.line, b.line, cmpOpts)
if (p !== 0) return p
return a.idx - b.idx
})
lines = decorated.map((d) => d.line)
} else {
lines.sort(cmpPair)
}
if (uniq) {
const out = []
@@ -214,5 +362,17 @@ async function run(ctx, argv) {
lines = out
}
if (lines.length) ctx.console.log(lines.join('\n'))
const body = lines.length ? lines.join('\n') + '\n' : ''
if (outFile != null) {
try {
await vfs.writeFile(outFile, ctx.b4a.from(body))
} catch (e) {
ctx.console.error('sort: ' + outFile + ': ' + (e.message || e))
ctx.exitCode = 1
return
}
} else if (lines.length) {
ctx.console.log(lines.join('\n'))
}
}
+30 -2
View File
@@ -503,6 +503,8 @@ async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
let longFmt = false
/** One name per line (GNU **`-1`** / **`--format=single-column`**, or piped/captured stdout). */
let singleColumn = false
/** @type {'never' | 'auto' | 'always'} */
let colorMode = 'auto'
const paths = []
@@ -524,6 +526,10 @@ async function run(ctx, argv) {
else colorMode = 'auto'
continue
}
if (a === '--format=single-column' || a === '--format=vertical') {
singleColumn = true
continue
}
ctx.console.error('ls: unrecognized option ' + a)
ctx.exitCode = 2
return
@@ -533,7 +539,7 @@ async function run(ctx, argv) {
const c = a[j]
if (c === 'a') showAll = true
else if (c === 'l') longFmt = true
else if (c === '1') longFmt = false
else if (c === '1') singleColumn = true
}
continue
}
@@ -541,6 +547,8 @@ async function run(ctx, argv) {
}
const targets = paths.length ? paths : ['.']
const useColor = bareLsUseColor(ctx, colorMode)
const onePerLine =
singleColumn || ctx.bareOsStdoutCaptured === true
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
@@ -562,7 +570,27 @@ async function run(ctx, argv) {
}
if (!showAll) names = names.filter((n) => !n.startsWith('.'))
if (!longFmt) {
if (!useColor) {
if (onePerLine) {
for (const n of names) {
if (!useColor) {
ctx.console.log(n)
} else {
const sub =
singleEntryPath != null
? singleEntryPath
: t === '.' || t === './'
? n
: t.replace(/\/$/, '') + '/' + n
let st = null
try {
st = await vfs.lstat(sub)
} catch {
st = null
}
ctx.console.log(bareLsColorWrap(n, st, true, ctx))
}
}
} else if (!useColor) {
ctx.console.log(names.join(' '))
} else {
const parts = []
+188 -28
View File
@@ -110,12 +110,75 @@ function sortKeySlice(line, delim, start1, end1) {
return slice.join(' ')
}
/**
* @param {object} o
* @param {string} o.line
* @param {boolean} o.numeric
* @param {boolean} o.fold
* @param {number | null} o.keyStart
* @param {number | null} o.keyEnd
* @param {string | null} o.dForKey
*/
function sortKeyObj(o) {
const keyText =
o.keyStart != null
? sortKeySlice(o.line, o.dForKey, o.keyStart, o.keyEnd)
: o.line
if (o.numeric) {
const m = String(keyText).match(
/^\s*(-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
)
if (m) return { n: Number(m[1]), raw: keyText }
return { n: Number.POSITIVE_INFINITY, raw: keyText }
}
if (o.fold) return { n: 0, raw: keyText.toLowerCase() }
return { n: 0, raw: keyText }
}
/**
* @param {string} x
* @param {string} y
* @param {object} opts
* @param {boolean} opts.numeric
* @param {boolean} opts.reverse
* @param {boolean} opts.fold
* @param {number | null} opts.keyStart
* @param {number | null} opts.keyEnd
* @param {string | null} opts.dForKey
* @returns {number}
*/
function sortCompareLines(x, y, opts) {
const base = {
numeric: opts.numeric,
fold: opts.fold,
keyStart: opts.keyStart,
keyEnd: opts.keyEnd,
dForKey: opts.dForKey
}
const kx = sortKeyObj({ ...base, line: x })
const ky = sortKeyObj({ ...base, line: y })
if (opts.numeric) {
if (kx.n !== ky.n) {
const ord = kx.n < ky.n ? -1 : 1
return opts.reverse ? -ord : ord
}
}
const cmp =
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
return opts.reverse ? -cmp : cmp
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let numeric = false
let reverse = false
let uniq = false
let fold = false
/** @type {'off' | 'verbose' | 'quiet'} */
let checkMode = 'off'
let stable = false
/** @type {string | null} */
let outFile = null
/** @type {string | undefined} */
let delim
/** @type {number | null} */
@@ -129,6 +192,52 @@ async function run(ctx, argv) {
i++
break
}
if (a === '--check' || a === '--check=diagnose-first') {
checkMode = 'verbose'
i++
continue
}
if (a === '--check=silent' || a === '--check=quiet') {
checkMode = 'quiet'
i++
continue
}
if (a === '-C') {
checkMode = 'quiet'
i++
continue
}
if (a === '-c') {
checkMode = 'verbose'
i++
continue
}
if (a === '-s' || a === '--stable') {
stable = true
i++
continue
}
if (a === '-o' || a === '--output') {
const f = argv[++i]
if (f === undefined) {
ctx.console.error('sort: option requires an argument -- output')
ctx.exitCode = 1
return
}
outFile = f
i++
continue
}
if (a.startsWith('--output=')) {
outFile = a.slice('--output='.length) || null
if (!outFile) {
ctx.console.error('sort: option requires an argument -- output')
ctx.exitCode = 1
return
}
i++
continue
}
if (a === '-n' || a === '--numeric-sort' || a === '-g') {
numeric = true
i++
@@ -219,6 +328,9 @@ async function run(ctx, argv) {
else if (c === 'r') reverse = true
else if (c === 'u') uniq = true
else if (c === 'f') fold = true
else if (c === 'c') checkMode = 'verbose'
else if (c === 'C') checkMode = 'quiet'
else if (c === 's') stable = true
else {
ok = false
break
@@ -261,37 +373,73 @@ async function run(ctx, argv) {
}
const dForKey = delim === undefined ? null : delim
/** @param {string} s */
function sortKey(s) {
const keyText =
keyStart != null
? sortKeySlice(s, dForKey, keyStart, keyEnd)
: s
if (numeric) {
const m = String(keyText).match(
/^\s*(-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
)
if (m) return { n: Number(m[1]), raw: keyText }
return { n: Number.POSITIVE_INFINITY, raw: keyText }
}
if (fold) return { n: 0, raw: keyText.toLowerCase() }
return { n: 0, raw: keyText }
const cmpOpts = {
numeric,
reverse,
fold,
keyStart,
keyEnd,
dForKey
}
lines.sort((x, y) => {
const kx = sortKey(x)
const ky = sortKey(y)
if (numeric) {
if (kx.n !== ky.n) {
const ord = kx.n < ky.n ? -1 : 1
return reverse ? -ord : ord
if (checkMode !== 'off') {
const checkLabel = files.length === 1 ? files[0] : '-'
for (let li = 0; li < lines.length - 1; li++) {
const aLine = lines[li]
const bLine = lines[li + 1]
const c = sortCompareLines(aLine, bLine, cmpOpts)
if (uniq && c === 0) {
ctx.exitCode = 1
if (checkMode === 'verbose') {
ctx.console.error(
'sort: ' +
checkLabel +
':' +
(li + 2) +
': disorder: ' +
bLine
)
}
return
}
if (c > 0) {
ctx.exitCode = 1
if (checkMode === 'verbose') {
ctx.console.error(
'sort: ' +
checkLabel +
':' +
(li + 2) +
': disorder: ' +
bLine
)
}
return
}
}
const cmp =
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
return reverse ? -cmp : cmp
})
ctx.exitCode = 0
return
}
/** @param {string} x
* @param {string} y */
function cmpPair(x, y) {
const primary = sortCompareLines(x, y, cmpOpts)
if (primary !== 0 || !stable) return primary
return 0
}
if (stable) {
const decorated = lines.map((line, idx) => ({ line, idx }))
decorated.sort((a, b) => {
const p = sortCompareLines(a.line, b.line, cmpOpts)
if (p !== 0) return p
return a.idx - b.idx
})
lines = decorated.map((d) => d.line)
} else {
lines.sort(cmpPair)
}
if (uniq) {
const out = []
@@ -303,5 +451,17 @@ async function run(ctx, argv) {
lines = out
}
if (lines.length) ctx.console.log(lines.join('\n'))
const body = lines.length ? lines.join('\n') + '\n' : ''
if (outFile != null) {
try {
await vfs.writeFile(outFile, ctx.b4a.from(body))
} catch (e) {
ctx.console.error('sort: ' + outFile + ': ' + (e.message || e))
ctx.exitCode = 1
return
}
} else if (lines.length) {
ctx.console.log(lines.join('\n'))
}
}
File diff suppressed because one or more lines are too long