Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli cmd): deno xeval #2260

Merged
merged 7 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -193,6 +195,37 @@ Prettier dependencies on first run.
.multiple(true)
.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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is long necessary?

Copy link
Member

@bartlomieju bartlomieju May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, otherwise this big message would show up in deno -h. With long_about it will show up in `deno help xeval

Sorry, you meant other Arg.long, then it's not needed.

Copy link
Contributor Author

@kevinkassimo kevinkassimo May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is necessary if you want to support long flags, e.g. --replvar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinkassimo you're right, sorry for noise

.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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it newline or whitespace? The subcommand help says "whitespace-delimited".

(IMO line sep seems more useful as a default... but okay.)

Copy link
Contributor Author

@kevinkassimo kevinkassimo May 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooopsy, it should be newline (though it is funny to see that xargs also claims to be white space delimited while actually working on new lines also)

This would be fixed along with other changes made to this feature as discussed above

.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
Expand Down Expand Up @@ -281,6 +314,7 @@ pub enum DenoSubcommand {
Repl,
Run,
Types,
Xeval,
}

pub fn flags_from_vec(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -569,6 +614,25 @@ mod tests {
assert_eq!(argv, svec!["deno", "script.ts"]);
}

#[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, _, _) =
Expand Down
26 changes: 26 additions & 0 deletions cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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),
}
}
1 change: 1 addition & 0 deletions cli/msg.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ table StartRes {
deno_version: string;
v8_version: string;
no_color: bool;
xeval_delim: string;
}

table CompilerConfig {
Expand Down
7 changes: 7 additions & 0 deletions cli/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the clone necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe yes, we are doing a move later as we initialize the StartResArgs struct (while state.flags.log_debug copies by default)

.map(|m| builder.create_string(&m));

let inner = msg::StartRes::create(
&mut builder,
&msg::StartResArgs {
Expand All @@ -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()
},
);
Expand Down
6 changes: 5 additions & 1 deletion js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
99 changes: 99 additions & 0 deletions js/xeval.ts
Original file line number Diff line number Diff line change
@@ -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++) {
Copy link
Member

@ry ry May 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you abstract out a function called lines() which yields a generator of strings. I believe @bartlomieju might have done a version of this before. Add a TODO above function stating that this functionality exists in deno_std, and that we should dedup the code once we have the ability to import from there.

for await (let line of lines(stdin)) {
  // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not find this utility in deno_std. I'll add a comment nevertheless, saying we might want to port this to deno_std and later import from it once it is doable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a try in #2101 but we didn't land it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ry I instead abstracted out chunks(reader, delim) that supports any string delimiter of any length.

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);
}
}
}
3 changes: 3 additions & 0 deletions tests/030_xeval.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A
B
C
3 changes: 3 additions & 0 deletions tests/030_xeval.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
args: xeval console.log($.toUpperCase())
input: a\nb\n\nc
output: tests/030_xeval.out
3 changes: 3 additions & 0 deletions tests/031_xeval_replvar.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A
B
C
3 changes: 3 additions & 0 deletions tests/031_xeval_replvar.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
args: xeval -I val console.log(val.toUpperCase());
input: a\nb\n\nc
output: tests/031_xeval_replvar.out
3 changes: 3 additions & 0 deletions tests/032_xeval_delim.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A
B
C
3 changes: 3 additions & 0 deletions tests/032_xeval_delim.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
args: xeval -d DELIM console.log($.toUpperCase());
input: aDELIMbDELIMDELIMc
output: tests/032_xeval_delim.out
22 changes: 20 additions & 2 deletions tools/integration_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down