UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

185 lines • 7.69 kB
import { ByteSpool, IO_BUFFER_SIZE } from "../../../io/spool.js"; // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { stdinStat, stdinStream, streamOutput } from "../utils/stream.js"; import { splitReadable } from "../utils/operands.js"; import { IOResult } from "../../../io/types.js"; import { closeQuietly } from "../../../io/stream.js"; import { LineFacts } from "../../../types.js"; import { wordRefusalError } from "../../spec/usage.js"; import { matchWord } from "../../spec/word_choice.js"; import { OPERAND } from "../../spec/constants.js"; import { FlagView } from "../../spec/flag_view.js"; import { specOf } from "../../spec/builtins.js"; import { resolveSource } from "../utils/stream.js"; import { StreamRefusal } from "../../../rs/errors.js"; import { refused } from "../../../rs/scan.js"; import { openStream, openStreamSync } from "../../../rs/stream.js"; import { byteView, encodeText } from "../../../shell/bytes.js"; // The --all-repeated and --group words, in the order GNU lists each back. No // aliases in either, so one candidate per line. const ALL_REPEATED_ARGS = ['none', 'prepend', 'separate']; const GROUP_ARGS = ['prepend', 'append', 'separate', 'both']; function methodWord(value, defaultValue, allowed, option) { if (value === undefined || value === false) return null; const normalized = value === true ? defaultValue : value; const word = String(normalized); // A non-string bag value (a number, a list) is not a candidate word at // all, so it never reaches the matcher and reads as invalid. const match = typeof normalized === 'string' ? matchWord(word, allowed) : null; if (match?.matched !== true) { const kind = match === null ? 'invalid' : match.kind; throw wordRefusalError('uniq', `--${option}`, word, allowed, undefined, kind); } return match.word; } function parseFlags(bag, operands) { const fl = new FlagView(bag, specOf('uniq')); const typed = fl.texts(OPERAND, 'skip_fields', 'skip_chars', 'check_chars'); if (typed.every(([name]) => name !== OPERAND)) { typed.push(...operands.map((word) => [OPERAND, word])); } const count = fl.asBool('count'); const duplicatesOnly = fl.asBool('repeated'); const uniqueOnly = fl.asBool('unique'); const allRepeated = methodWord(fl.asBool('D') ? true : fl.raw('all_repeated'), 'none', ALL_REPEATED_ARGS, 'all-repeated'); const group = methodWord(fl.raw('group'), 'separate', GROUP_ARGS, 'group'); return { count, duplicatesOnly, uniqueOnly, typed, ignoreCase: fl.asBool('ignore_case'), allRepeated, group, zeroTerminated: fl.asBool('zero_terminated'), }; } /** The options GNU uniq refuses together once it has read them all. Mirrors * Python's `_conflict`. */ function conflict(parsed) { if (parsed.group !== null && (parsed.count || parsed.duplicatesOnly || parsed.uniqueOnly || parsed.allRepeated !== null)) { return 'uniq: --group is mutually exclusive with -c/-d/-D/-u'; } if (parsed.count && parsed.allRepeated !== null) { return 'uniq: printing all duplicated lines and repeat counts is meaningless'; } return null; } /** * uniq's options, its counts and operands refused by the engine in the order * typed, before any operand is read: the flags, or uniq's refusal. The flags * carry each operand's glob spread over its matches. Mirrors Python's * `uniq_flags`. */ export function uniqFlags(bag, operands = []) { let parsed; try { parsed = parseFlags(bag, operands); } catch (err) { const message = err instanceof Error ? err.message : String(err); return new IOResult({ exitCode: 1, stderr: encodeText(`${message}\n`) }); } try { openStreamSync('uniq', engineConfig(parsed)).close(); } catch (err) { if (!(err instanceof StreamRefusal)) throw err; return refused(err); } const refusal = conflict(parsed); if (refusal !== null) return new IOResult({ exitCode: 1, stderr: encodeText(`${refusal}\n`) }); return parsed; } /** The uniq engine's config, the counts and operands as typed. Mirrors Python's * `engine_config`. */ export function engineConfig(parsed) { return { count: parsed.count, repeated: parsed.duplicatesOnly, unique: parsed.uniqueOnly, typed: parsed.typed.map(([name, text]) => [name, byteView(text)]), ignore_case: parsed.ignoreCase, all_repeated: parsed.allRepeated, group: parsed.group, zero_terminated: parsed.zeroTerminated, }; } export async function uniqGeneric(parsed, paths, ctx, stream, write, stat) { stream = stdinStream(stream, ctx.stdin); if (paths.length > 0 && stat !== undefined) { // The input is stat'ed before the lazy stream starts, so a missing or // unreadable one is reported in uniq's own words rather than // surfacing mid-drain. const [, err] = await splitReadable(paths.slice(0, 1), stdinStat(stat), 'uniq'); if (err !== '') return [null, new IOResult({ exitCode: 1, stderr: encodeText(err) })]; } let source; if (paths.length > 0) { const input = paths[0]; if (input === undefined) return [null, new IOResult()]; source = stream(input); } else { try { source = resolveSource(ctx.stdin); } catch (err) { const message = err instanceof Error ? err.message : String(err); return [null, new IOResult({ exitCode: 1, stderr: encodeText(`${message}\n`) })]; } } const output = streamOutput(await openStream('uniq', engineConfig(parsed)), source, ctx.signal); const outputPath = paths[1]; if (outputPath !== undefined && outputPath.rawPath !== '-') { if (write === undefined) { await closeQuietly(output); return [ null, new IOResult({ exitCode: 1, stderr: encodeText('uniq: output is not writable\n') }), ]; } // Consume before writing: Mirage preserves a shared INPUT/OUTPUT; // GNU truncates it when opening the output. const stored = new ByteSpool(); try { await stored.fill(output); await write(outputPath, stored.stream(0, undefined, IO_BUFFER_SIZE)); } finally { await stored.close(); } return [new Uint8Array(), new IOResult()]; } return [output, new IOResult()]; } function namesOutput(operands) { return operands.length > 1 || operands.some((w) => typeof w !== 'string' && w.pattern !== null); } /** * A uniq line writes its second operand, which a pattern the backend * expands may name too; a quoted name is no pattern. Mirrors Python's * `facts`. */ export function facts(fl, operands) { return new LineFacts({ writes: namesOutput(operands) }); } //# sourceMappingURL=uniq.js.map