diff --git a/changelog/rdmd-writeln.dd b/changelog/rdmd-writeln.dd new file mode 100644 index 0000000000..6a0badc1c7 --- /dev/null +++ b/changelog/rdmd-writeln.dd @@ -0,0 +1,46 @@ +In rdmd `--eval` and `--loop` the last statement without effect will be printed if prefixed with `@` + +`--eval` and `--loop` are two convenience features of `rdmd` that allow quick experimentation with D code. + +$(CONSOLE +rdmd --eval='"hello"' +$(RED Error): Error: found } when expecting ; following statement +rdmd --eval='"hello";' +$(RED Error): "hello" has no effect +rdmd --eval='@"hello"' +hello +) + +A few more examples of `rdmd` in action: + +$(H4 Use `rdmd` as your calculator) + +$(CONSOLE +rdmd --eval="@2 + 2" +4 +) + +$(CONSOLE +rdmd --eval="@2.pow(4)" +16 +) + +$(CONSOLE +rdmd --eval="auto a = 10; auto b = [a, a + 1]; @b" +[10, 11] +) + +$(H4 Select the first column in a CSV file) + +$(CONSOLE +rdmd --loop='@line.splitter(",").take(1)' +16 +) + +$(H4 Use `rdmd` as drop-in for Unix tools) + +$(CONSOLE +seq 5 | rdmd --loop='@stdin.byLine.drop(2).joiner(newline)' +4 +5 +) diff --git a/rdmd.d b/rdmd.d index 4ca39b380f..430a0b93a4 100755 --- a/rdmd.d +++ b/rdmd.d @@ -828,30 +828,48 @@ Returns: */ string innerEvalCode(string[] eval) { + import std.conv : text; import std.string : join, stripRight; // assumeSafeAppend just to avoid unnecessary reallocation string code = eval.join("\n").stripRight.assumeSafeAppend; - if (code.length > 0 && code[$ - 1] != ';') - code ~= ';'; + size_t lastSemicolon; + if (code.empty || code[$ - 1] == ';') + goto Lret; + // TODO: phobos should have findSplit!(reverse.Flag = reverse.No); + lastSemicolon = code.lastIndexOf("@"); + if(lastSemicolon==-1) + goto Lret; // probably a user error + code = text(code[0..lastSemicolon], "writeln(", code[lastSemicolon+1..$], ");"); + Lret: return code; } unittest { - assert(innerEvalCode([`writeln("Hello!")`]) == `writeln("Hello!");`); + assert(innerEvalCode([`writeln("Hello!")`]) == `writeln("Hello!")`); assert(innerEvalCode([`writeln("Hello!");`]) == `writeln("Hello!");`); // test with trailing whitespace - assert(innerEvalCode([`writeln("Hello!") `]) == `writeln("Hello!");`); + assert(innerEvalCode([`writeln("Hello!") `]) == `writeln("Hello!")`); assert(innerEvalCode([`writeln("Hello!"); `]) == `writeln("Hello!");`); // test with multiple entries assert(innerEvalCode([`writeln("Hello!"); `, `writeln("You!") `]) - == "writeln(\"Hello!\"); \nwriteln(\"You!\");"); + == "writeln(\"Hello!\"); \nwriteln(\"You!\")"); assert(innerEvalCode([`writeln("Hello!"); `, `writeln("You!"); `]) == "writeln(\"Hello!\"); \nwriteln(\"You!\");"); } +unittest +{ + assert(innerEvalCode(["@2"]) == "writeln(2);"); + assert(innerEvalCode(["@2 + 2"]) == "writeln(2 + 2);"); + assert(innerEvalCode(["@2 + 2; "]) == "@2 + 2;"); + assert(innerEvalCode(["2 + 2; @2 + 2"]) == "2 + 2; writeln(2 + 2);"); + assert(innerEvalCode(["@2.pow(4)"]) == "writeln(2.pow(4));"); + assert(innerEvalCode(["if(0) {} @2 + 2"]) == "if(0) {} writeln(2 + 2);"); +} + /** Formats the code provided via `--eval` or `--loop` flags into a string of complete program code that can be written to a file @@ -891,7 +909,7 @@ unittest // innerEvalCode already tests the cases for different // contents in `eval` array, so let's focus on testing // the difference based on the `loop` flag - assert(makeEvalCode([`writeln("Hello!") `], No.loop) == + assert(makeEvalCode([`writeln("Hello!"); `], No.loop) == importWorld ~ "void main(char[][] args) {\n" ~ "writeln(\"Hello!\");\n}"); diff --git a/rdmd_test.d b/rdmd_test.d index 924b9eea52..03666b8611 100755 --- a/rdmd_test.d +++ b/rdmd_test.d @@ -183,6 +183,19 @@ void runTests(string rdmdApp, string compiler, string model) assert(res.status == 0, res.output); assert(res.output.canFind("eval_works")); // there could be a "DMD v2.xxx header in the output" + // Test automatic .writeln for --eval + import std.conv : text; + import std.typecons : tuple; + foreach (t; [tuple(`@"eval_works"`, "eval_works"), + tuple("@2 + 2", "4"), + tuple("2.write; @2 + 2", "24")]) + { + res = execute(rdmdArgs ~ ["--force", "-de", text("--eval=", t[0])]); + assert(res.status == 0, res.output); + // there could be a "DMD v2.xxx header in the output" (NB: only seems to be the case for GDC) + assert(res.output.canFind(t[1]), text("got:", res.output, " expected:", t[1])); + } + // compiler flags res = execute(rdmdArgs ~ ["--force", "-debug", "--eval=debug {} else assert(false);"]); @@ -256,22 +269,24 @@ void runTests(string rdmdApp, string compiler, string model) { auto testLines = "foo\nbar\ndoo".split("\n"); - auto pipes = pipeProcess(rdmdArgs ~ ["--force", "--loop=writeln(line);"], Redirect.stdin | Redirect.stdout); - foreach (input; testLines) - pipes.stdin.writeln(input); - pipes.stdin.close(); - - while (!testLines.empty) + // Test --loop with automatic writeln + foreach (loopArg; ["--loop=writeln(line);", "--loop=@line"]) { - auto line = pipes.stdout.readln.strip; - if (line.empty || line.startsWith("DMD v")) continue; // git-head header - assert(line == testLines.front, "Expected %s, got %s".format(testLines.front, line)); - testLines.popFront; - } - auto status = pipes.pid.wait(); - assert(status == 0); - } + auto pipes = pipeProcess(rdmdArgs ~ ["--force", loopArg], Redirect.stdin | Redirect.stdout); + foreach (input; testLines) + pipes.stdin.writeln(input); + pipes.stdin.close(); + while (!testLines.empty) + { + auto line = pipes.stdout.readln.strip; + if (line.empty || line.startsWith("DMD v")) continue; // git-head header + assert(line == testLines.front, "Expected %s, got %s".format(testLines.front, line)); + testLines.popFront; + } + auto status = pipes.pid.wait(); + assert(status == 0); + }} // vs program file res = execute(rdmdArgs ~ ["--force", "--loop=assert(true);", voidMain]);