-
Notifications
You must be signed in to change notification settings - Fork 5.2k
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
Changes from all commits
01370f4
7a14643
7740efc
ecf0942
6332fbd
d4ef1b1
28c520b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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") | ||
.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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooopsy, it should be newline (though it is funny to see that 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 | ||
|
@@ -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 | ||
|
@@ -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, _, _) = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the clone necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
.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() | ||
}, | ||
); | ||
|
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++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you abstract out a function called for await (let line of lines(stdin)) {
// ...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did not find this utility in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made a try in #2101 but we didn't land it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ry I instead abstracted out |
||
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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
A | ||
B | ||
C |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
A | ||
B | ||
C |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
A | ||
B | ||
C |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is long necessary?
There was a problem hiding this comment.
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 indeno -h
. Withlong_about
it will show up in `deno help xevalSorry, you meant other
Arg.long
, then it's not needed.There was a problem hiding this comment.
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
There was a problem hiding this comment.
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