summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin (Kun) "Kassimo" Qian <kevinkassimo@gmail.com>2019-05-03 13:24:09 -0700
committerRyan Dahl <ry@tinyclouds.org>2019-05-03 16:24:09 -0400
commit36081171323e266760db8bed2f31a6e3be7d8839 (patch)
tree6f4dc276656c7b119fc5efad5368e847f9cf7f19
parent401a5c021141d4ba5a71078b28f6daefcd1826a6 (diff)
feat(cli cmd): deno xeval (#2260)
-rw-r--r--cli/BUILD.gn1
-rw-r--r--cli/flags.rs64
-rw-r--r--cli/main.rs26
-rw-r--r--cli/msg.fbs1
-rw-r--r--cli/ops.rs7
-rw-r--r--js/main.ts6
-rw-r--r--js/xeval.ts99
-rw-r--r--tests/030_xeval.out3
-rw-r--r--tests/030_xeval.test3
-rw-r--r--tests/031_xeval_replvar.out3
-rw-r--r--tests/031_xeval_replvar.test3
-rw-r--r--tests/032_xeval_delim.out3
-rw-r--r--tests/032_xeval_delim.test3
-rwxr-xr-xtools/integration_tests.py22
14 files changed, 241 insertions, 3 deletions
diff --git a/cli/BUILD.gn b/cli/BUILD.gn
index 0b5fadc68..195bdfd60 100644
--- a/cli/BUILD.gn
+++ b/cli/BUILD.gn
@@ -115,6 +115,7 @@ ts_sources = [
"../js/write_file.ts",
"../js/performance.ts",
"../js/version.ts",
+ "../js/xeval.ts",
"../tsconfig.json",
# Listing package.json and yarn.lock as sources ensures the bundle is rebuilt
diff --git a/cli/flags.rs b/cli/flags.rs
index 380c87b90..f60c1a3fa 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -24,6 +24,8 @@ pub struct DenoFlags {
pub no_prompts: bool,
pub no_fetch: bool,
pub v8_flags: Option<Vec<String>>,
+ pub xeval_replvar: Option<String>,
+ pub xeval_delim: Option<String>,
}
static ENV_VARIABLES_HELP: &str = "ENVIRONMENT VARIABLES:
@@ -194,6 +196,37 @@ Prettier dependencies on first run.
.required(true),
),
).subcommand(
+ SubCommand::with_name("xeval")
+ .setting(AppSettings::DisableVersion)
+ .about("Eval a script on text segments from stdin")
+ .long_about(
+ "
+Eval a script on lines (or chunks split under delimiter) from stdin.
+
+Read from standard input and eval code on each whitespace-delimited
+string chunks.
+
+-I/--replvar optionally set variable name for input to be used in eval.
+Otherwise '$' will be used as default variable name.
+
+ cat /etc/passwd | deno xeval \"a = $.split(':'); if (a) console.log(a[0])\"
+ git branch | deno xeval -I 'line' \"if (line.startsWith('*')) console.log(line.slice(2))\"
+ cat LICENSE | deno xeval -d ' ' \"if ($ === 'MIT') console.log('MIT licensed')\"
+",
+ ).arg(
+ Arg::with_name("replvar")
+ .long("replvar")
+ .short("I")
+ .help("Set variable name to be used in eval, defaults to $")
+ .takes_value(true),
+ ).arg(
+ Arg::with_name("delim")
+ .long("delim")
+ .short("d")
+ .help("Set delimiter, defaults to newline")
+ .takes_value(true),
+ ).arg(Arg::with_name("code").takes_value(true).required(true)),
+ ).subcommand(
// this is a fake subcommand - it's used in conjunction with
// AppSettings:AllowExternalSubcommand to treat it as an
// entry point script
@@ -281,6 +314,7 @@ pub enum DenoSubcommand {
Repl,
Run,
Types,
+ Xeval,
}
pub fn flags_from_vec(
@@ -322,6 +356,17 @@ pub fn flags_from_vec(
DenoSubcommand::Info
}
("types", Some(_)) => DenoSubcommand::Types,
+ ("xeval", Some(eval_match)) => {
+ let code: &str = eval_match.value_of("code").unwrap();
+ flags.xeval_replvar =
+ Some(eval_match.value_of("replvar").unwrap_or("$").to_owned());
+ // Currently clap never escapes string,
+ // So -d "\n" won't expand to newline.
+ // Instead, do -d $'\n'
+ flags.xeval_delim = eval_match.value_of("delim").map(String::from);
+ argv.extend(vec![code.to_string()]);
+ DenoSubcommand::Xeval
+ }
(script, Some(script_match)) => {
argv.extend(vec![script.to_string()]);
// check if there are any extra arguments that should
@@ -570,6 +615,25 @@ mod tests {
}
#[test]
+ fn test_flags_from_vec_15() {
+ let (flags, subcommand, argv) = flags_from_vec(svec![
+ "deno",
+ "xeval",
+ "-I",
+ "val",
+ "-d",
+ " ",
+ "console.log(val)"
+ ]);
+ let mut expected_flags = DenoFlags::default();
+ expected_flags.xeval_replvar = Some("val".to_owned());
+ expected_flags.xeval_delim = Some(" ".to_owned());
+ assert_eq!(flags, expected_flags);
+ assert_eq!(subcommand, DenoSubcommand::Xeval);
+ assert_eq!(argv, svec!["deno", "console.log(val)"]);
+ }
+
+ #[test]
fn test_set_flags_11() {
let (flags, _, _) =
flags_from_vec(svec!["deno", "-c", "tsconfig.json", "script.ts"]);
diff --git a/cli/main.rs b/cli/main.rs
index cff42f5a0..5a6efa073 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -208,6 +208,31 @@ fn eval_command(flags: DenoFlags, argv: Vec<String>) {
tokio_util::run(main_future);
}
+fn xeval_command(flags: DenoFlags, argv: Vec<String>) {
+ let xeval_replvar = flags.xeval_replvar.clone().unwrap();
+ let (mut worker, state) = create_worker_and_state(flags, argv);
+ let xeval_source = format!(
+ "window._xevalWrapper = async function ({}){{
+ {}
+ }}",
+ &xeval_replvar, &state.argv[1]
+ );
+
+ let main_future = lazy(move || {
+ // Setup runtime.
+ js_check(worker.execute(&xeval_source));
+ js_check(worker.execute("denoMain()"));
+ worker
+ .then(|result| {
+ js_check(result);
+ Ok(())
+ }).map_err(|(err, _worker): (RustOrJsError, Worker)| {
+ print_err_and_exit(err)
+ })
+ });
+ tokio_util::run(main_future);
+}
+
fn run_repl(flags: DenoFlags, argv: Vec<String>) {
let (mut worker, _state) = create_worker_and_state(flags, argv);
@@ -275,5 +300,6 @@ fn main() {
DenoSubcommand::Repl => run_repl(flags, argv),
DenoSubcommand::Run => run_script(flags, argv),
DenoSubcommand::Types => types_command(),
+ DenoSubcommand::Xeval => xeval_command(flags, argv),
}
}
diff --git a/cli/msg.fbs b/cli/msg.fbs
index b93fb68a7..fb8fd9c22 100644
--- a/cli/msg.fbs
+++ b/cli/msg.fbs
@@ -175,6 +175,7 @@ table StartRes {
deno_version: string;
v8_version: string;
no_color: bool;
+ xeval_delim: string;
}
table CompilerConfig {
diff --git a/cli/ops.rs b/cli/ops.rs
index b2b9b4245..ab2284110 100644
--- a/cli/ops.rs
+++ b/cli/ops.rs
@@ -341,6 +341,12 @@ fn op_start(
let main_module = state.main_module().map(|m| builder.create_string(&m));
+ let xeval_delim = state
+ .flags
+ .xeval_delim
+ .clone()
+ .map(|m| builder.create_string(&m));
+
let inner = msg::StartRes::create(
&mut builder,
&msg::StartResArgs {
@@ -354,6 +360,7 @@ fn op_start(
deno_version: Some(deno_version_off),
no_color: !ansi::use_color(),
exec_path: Some(exec_path),
+ xeval_delim,
..Default::default()
},
);
diff --git a/js/main.ts b/js/main.ts
index ad9e0e99d..cb27690b5 100644
--- a/js/main.ts
+++ b/js/main.ts
@@ -9,7 +9,9 @@ import { assert, log } from "./util";
import * as os from "./os";
import { args } from "./deno";
import { replLoop } from "./repl";
+import { xevalMain, XevalFunc } from "./xeval";
import { setVersions } from "./version";
+import { window } from "./window";
import { setLocation } from "./location";
// builtin modules
@@ -43,7 +45,9 @@ export default function denoMain(name?: string): void {
log("args", args);
Object.freeze(args);
- if (!mainModule) {
+ if (window["_xevalWrapper"] !== undefined) {
+ xevalMain(window["_xevalWrapper"] as XevalFunc, startResMsg.xevalDelim());
+ } else if (!mainModule) {
replLoop();
}
}
diff --git a/js/xeval.ts b/js/xeval.ts
new file mode 100644
index 000000000..f769a2ead
--- /dev/null
+++ b/js/xeval.ts
@@ -0,0 +1,99 @@
+import { Buffer } from "./buffer";
+import { stdin } from "./files";
+import { TextEncoder, TextDecoder } from "./text_encoding";
+import { Reader } from "./io";
+
+export type XevalFunc = (v: string) => void;
+
+async function writeAll(buffer: Buffer, arr: Uint8Array): Promise<void> {
+ let bytesWritten = 0;
+ while (bytesWritten < arr.length) {
+ try {
+ const nwritten = await buffer.write(arr.subarray(bytesWritten));
+ bytesWritten += nwritten;
+ } catch {
+ return;
+ }
+ }
+}
+
+// TODO(kevinkassimo): Move this utility to deno_std.
+// Import from there once doable.
+// Read from reader until EOF and emit string chunks separated
+// by the given delimiter.
+async function* chunks(
+ reader: Reader,
+ delim: string
+): AsyncIterableIterator<string> {
+ const inputBuffer = new Buffer();
+ const inspectArr = new Uint8Array(1024);
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ // Avoid unicode problems
+ const delimArr = encoder.encode(delim);
+
+ // Record how far we have gone with delimiter matching.
+ let nextMatchIndex = 0;
+ while (true) {
+ const rr = await reader.read(inspectArr);
+ if (rr.nread < 0) {
+ // Silently fail.
+ break;
+ }
+ const sliceRead = inspectArr.subarray(0, rr.nread);
+ // Remember how far we have scanned through inspectArr.
+ let nextSliceStartIndex = 0;
+ for (let i = 0; i < sliceRead.length; i++) {
+ if (sliceRead[i] == delimArr[nextMatchIndex]) {
+ // One byte matches with delimiter, move 1 step forward.
+ nextMatchIndex++;
+ } else {
+ // Match delimiter failed. Start from beginning.
+ nextMatchIndex = 0;
+ }
+ // A complete match is found.
+ if (nextMatchIndex === delimArr.length) {
+ nextMatchIndex = 0; // Reset delim match index.
+ const sliceToJoin = sliceRead.subarray(nextSliceStartIndex, i + 1);
+ // Record where to start next chunk when a subsequent match is found.
+ nextSliceStartIndex = i + 1;
+ // Write slice to buffer before processing, since potentially
+ // part of the delimiter is stored in the buffer.
+ await writeAll(inputBuffer, sliceToJoin);
+
+ let readyBytes = inputBuffer.bytes();
+ inputBuffer.reset();
+ // Remove delimiter from buffer bytes.
+ readyBytes = readyBytes.subarray(
+ 0,
+ readyBytes.length - delimArr.length
+ );
+ let readyChunk = decoder.decode(readyBytes);
+ yield readyChunk;
+ }
+ }
+ // Write all unprocessed chunk to buffer for future inspection.
+ await writeAll(inputBuffer, sliceRead.subarray(nextSliceStartIndex));
+ if (rr.eof) {
+ // Flush the remainder unprocessed chunk.
+ const lastChunk = inputBuffer.toString();
+ yield lastChunk;
+ break;
+ }
+ }
+}
+
+export async function xevalMain(
+ xevalFunc: XevalFunc,
+ delim_: string | null
+): Promise<void> {
+ if (!delim_) {
+ delim_ = "\n";
+ }
+ for await (const chunk of chunks(stdin, delim_)) {
+ // Ignore empty chunks.
+ if (chunk.length > 0) {
+ xevalFunc(chunk);
+ }
+ }
+}
diff --git a/tests/030_xeval.out b/tests/030_xeval.out
new file mode 100644
index 000000000..b1e67221a
--- /dev/null
+++ b/tests/030_xeval.out
@@ -0,0 +1,3 @@
+A
+B
+C
diff --git a/tests/030_xeval.test b/tests/030_xeval.test
new file mode 100644
index 000000000..3ecff4153
--- /dev/null
+++ b/tests/030_xeval.test
@@ -0,0 +1,3 @@
+args: xeval console.log($.toUpperCase())
+input: a\nb\n\nc
+output: tests/030_xeval.out
diff --git a/tests/031_xeval_replvar.out b/tests/031_xeval_replvar.out
new file mode 100644
index 000000000..b1e67221a
--- /dev/null
+++ b/tests/031_xeval_replvar.out
@@ -0,0 +1,3 @@
+A
+B
+C
diff --git a/tests/031_xeval_replvar.test b/tests/031_xeval_replvar.test
new file mode 100644
index 000000000..ebadb6d28
--- /dev/null
+++ b/tests/031_xeval_replvar.test
@@ -0,0 +1,3 @@
+args: xeval -I val console.log(val.toUpperCase());
+input: a\nb\n\nc
+output: tests/031_xeval_replvar.out
diff --git a/tests/032_xeval_delim.out b/tests/032_xeval_delim.out
new file mode 100644
index 000000000..b1e67221a
--- /dev/null
+++ b/tests/032_xeval_delim.out
@@ -0,0 +1,3 @@
+A
+B
+C
diff --git a/tests/032_xeval_delim.test b/tests/032_xeval_delim.test
new file mode 100644
index 000000000..b4d8342fc
--- /dev/null
+++ b/tests/032_xeval_delim.test
@@ -0,0 +1,3 @@
+args: xeval -d DELIM console.log($.toUpperCase());
+input: aDELIMbDELIMDELIMc
+output: tests/032_xeval_delim.out
diff --git a/tools/integration_tests.py b/tools/integration_tests.py
index 5c3b24f75..32a53e3f7 100755
--- a/tools/integration_tests.py
+++ b/tools/integration_tests.py
@@ -65,6 +65,12 @@ def integration_tests(deno_exe, test_filter=None):
stderr = subprocess.STDOUT if check_stderr else open(os.devnull, 'w')
+ stdin_input = (test.get("input",
+ "").strip().decode("string_escape").replace(
+ "\r\n", "\n"))
+
+ has_stdin_input = len(stdin_input) > 0
+
output_abs = os.path.join(root_path, test.get("output", ""))
with open(output_abs, 'r') as f:
expected_out = f.read()
@@ -73,8 +79,20 @@ def integration_tests(deno_exe, test_filter=None):
sys.stdout.flush()
actual_code = 0
try:
- actual_out = subprocess.check_output(
- cmd, universal_newlines=True, stderr=stderr)
+ if has_stdin_input:
+ # Provided stdin
+ proc = subprocess.Popen(
+ cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=stderr)
+ actual_out, _ = proc.communicate(stdin_input)
+ actual_out = actual_out.replace("\r\n", "\n")
+ else:
+ # No stdin sent
+ actual_out = subprocess.check_output(
+ cmd, universal_newlines=True, stderr=stderr)
+
except subprocess.CalledProcessError as e:
actual_code = e.returncode
actual_out = e.output