From 97b2a0bb958549e298cd137778053210a05d85d8 Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) \"Kassimo\" Qian" Date: Thu, 30 Jan 2020 18:02:14 -0800 Subject: [PATCH] bring back std/examples/xeval.ts (#3822) --- std/examples/tests/xeval_test.ts | 48 ++++++++ std/examples/xeval.ts | 181 +++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 std/examples/tests/xeval_test.ts create mode 100644 std/examples/xeval.ts diff --git a/std/examples/tests/xeval_test.ts b/std/examples/tests/xeval_test.ts new file mode 100644 index 0000000000..db9cbceee3 --- /dev/null +++ b/std/examples/tests/xeval_test.ts @@ -0,0 +1,48 @@ +import { xeval } from "../xeval.ts"; +import { stringsReader } from "../../io/util.ts"; +import { decode, encode } from "../../strings/mod.ts"; +import { assertEquals, assertStrContains } from "../../testing/asserts.ts"; +import { test } from "../../testing/mod.ts"; +const { execPath, run } = Deno; + +test(async function xevalSuccess(): Promise { + const chunks: string[] = []; + await xeval(stringsReader("a\nb\nc"), ($): number => chunks.push($)); + assertEquals(chunks, ["a", "b", "c"]); +}); + +test(async function xevalDelimiter(): Promise { + const chunks: string[] = []; + await xeval(stringsReader("!MADMADAMADAM!"), ($): number => chunks.push($), { + delimiter: "MADAM" + }); + assertEquals(chunks, ["!MAD", "ADAM!"]); +}); + +// https://github.com/denoland/deno/issues/2861 +const xevalPath = "examples/xeval.ts"; + +test(async function xevalCliReplvar(): Promise { + const p = run({ + args: [execPath(), xevalPath, "--", "--replvar=abc", "console.log(abc)"], + stdin: "piped", + stdout: "piped", + stderr: "null" + }); + await p.stdin!.write(encode("hello")); + await p.stdin!.close(); + assertEquals(await p.status(), { code: 0, success: true }); + assertEquals(decode(await p.output()).trimEnd(), "hello"); +}); + +test(async function xevalCliSyntaxError(): Promise { + const p = run({ + args: [execPath(), xevalPath, "--", "("], + stdin: "null", + stdout: "piped", + stderr: "piped" + }); + assertEquals(await p.status(), { code: 1, success: false }); + assertEquals(decode(await p.output()), ""); + assertStrContains(decode(await p.stderrOutput()), "Uncaught SyntaxError"); +}); diff --git a/std/examples/xeval.ts b/std/examples/xeval.ts new file mode 100644 index 0000000000..75c864745b --- /dev/null +++ b/std/examples/xeval.ts @@ -0,0 +1,181 @@ +import { parse } from "../flags/mod.ts"; +const { Buffer, EOF, args, exit, stdin, writeAll } = Deno; +type Reader = Deno.Reader; + +/* eslint-disable-next-line max-len */ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction. +const AsyncFunction = Object.getPrototypeOf(async function(): Promise {}) + .constructor; + +/* eslint-disable max-len */ +const HELP_MSG = `xeval + +Eval a script on lines from stdin. +Read from standard input and eval code on each whitespace-delimited +string chunks. + +Print all the usernames in /etc/passwd: + cat /etc/passwd | deno -A https://deno.land/std/examples/xeval.ts -- "a = $.split(':'); if (a) console.log(a[0])" + +A complicated way to print the current git branch: + git branch | deno -A https://deno.land/std/examples/xeval.ts -- -I 'line' "if (line.startsWith('*')) console.log(line.slice(2))" + +Demonstrates breaking the input up by space delimiter instead of by lines: + cat LICENSE | deno -A https://deno.land/std/examples/xeval.ts -- -d " " "if ($ === 'MIT') console.log('MIT licensed')", + +USAGE: + deno -A https://deno.land/std/examples/xeval.ts [OPTIONS] +OPTIONS: + -d, --delim Set delimiter, defaults to newline + -I, --replvar Set variable name to be used in eval, defaults to $ +ARGS: + `; +/* eslint-enable max-len */ + +export type XevalFunc = (v: string) => void; + +export interface XevalOptions { + delimiter?: string; +} + +const DEFAULT_DELIMITER = "\n"; + +// Generate longest proper prefix which is also suffix array. +function createLPS(pat: Uint8Array): Uint8Array { + const lps = new Uint8Array(pat.length); + lps[0] = 0; + let prefixEnd = 0; + let i = 1; + while (i < lps.length) { + if (pat[i] == pat[prefixEnd]) { + prefixEnd++; + lps[i] = prefixEnd; + i++; + } else if (prefixEnd === 0) { + lps[i] = 0; + i++; + } else { + prefixEnd = pat[prefixEnd - 1]; + } + } + return lps; +} + +// Read from reader until EOF and emit string chunks separated +// by the given delimiter. +async function* chunks( + reader: Reader, + delim: string +): AsyncIterableIterator { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + // Avoid unicode problems + const delimArr = encoder.encode(delim); + const delimLen = delimArr.length; + const delimLPS = createLPS(delimArr); + + let inputBuffer = new Buffer(); + const inspectArr = new Uint8Array(Math.max(1024, delimLen + 1)); + + // Modified KMP + let inspectIndex = 0; + let matchIndex = 0; + while (true) { + const result = await reader.read(inspectArr); + if (result === EOF) { + // Yield last chunk. + const lastChunk = inputBuffer.toString(); + yield lastChunk; + return; + } + if ((result as number) < 0) { + // Discard all remaining and silently fail. + return; + } + const sliceRead = inspectArr.subarray(0, result as number); + await writeAll(inputBuffer, sliceRead); + + let sliceToProcess = inputBuffer.bytes(); + while (inspectIndex < sliceToProcess.length) { + if (sliceToProcess[inspectIndex] === delimArr[matchIndex]) { + inspectIndex++; + matchIndex++; + if (matchIndex === delimLen) { + // Full match + const matchEnd = inspectIndex - delimLen; + const readyBytes = sliceToProcess.subarray(0, matchEnd); + // Copy + const pendingBytes = sliceToProcess.slice(inspectIndex); + const readyChunk = decoder.decode(readyBytes); + yield readyChunk; + // Reset match, different from KMP. + sliceToProcess = pendingBytes; + inspectIndex = 0; + matchIndex = 0; + } + } else { + if (matchIndex === 0) { + inspectIndex++; + } else { + matchIndex = delimLPS[matchIndex - 1]; + } + } + } + // Keep inspectIndex and matchIndex. + inputBuffer = new Buffer(sliceToProcess); + } +} + +export async function xeval( + reader: Reader, + xevalFunc: XevalFunc, + { delimiter = DEFAULT_DELIMITER }: XevalOptions = {} +): Promise { + for await (const chunk of chunks(reader, delimiter)) { + // Ignore empty chunks. + if (chunk.length > 0) { + await xevalFunc(chunk); + } + } +} + +async function main(): Promise { + const parsedArgs = parse(args.slice(1), { + boolean: ["help"], + string: ["delim", "replvar"], + alias: { + delim: ["d"], + replvar: ["I"], + help: ["h"] + }, + default: { + delim: DEFAULT_DELIMITER, + replvar: "$" + } + }); + if (parsedArgs._.length != 1) { + console.error(HELP_MSG); + exit(1); + } + if (parsedArgs.help) { + return console.log(HELP_MSG); + } + + const delimiter = parsedArgs.delim; + const replVar = parsedArgs.replvar; + const code = parsedArgs._[0]; + + // new AsyncFunction()'s error message for this particular case isn't great. + if (!replVar.match(/^[_$A-z][_$A-z0-9]*$/)) { + console.error(`Bad replvar identifier: "${replVar}"`); + exit(1); + } + + const xEvalFunc = new AsyncFunction(replVar, code); + + await xeval(stdin, xEvalFunc, { delimiter }); +} + +if (import.meta.main) { + main(); +}