Skip to content

Commit c36b3c5

Browse files
Improvements for standalone script support (#6015)
- Remove hard requirement for root build file. Now that Mill can support ad-hoc scripts, we do not want to force people to create a root build file just to run scripts. - Cache instantiated script modules to prevent duplicate evaluation - Gracefully handle `ScriptModule#parseHeaderData` when script file is renamed or deleted - Properly quote main class name in launcher scripts to make top-level methods wrapped in classes named `Foo$package` work - Bump MainArgs to `0.7.7` to take advantage of simpler `mainargs.Parser` syntax - Add `example.{javalib,kotlinlib}.basic[2-script-crawler]` examples, mirroring the `example.scalalib.basic[2-script-builtins]` but using explicit instead of bundled libraries --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 2b141ad commit c36b3c5

File tree

65 files changed

+367
-210
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+367
-210
lines changed

core/internal/cli/src/mill/internal/MillCliConfig.scala

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,6 @@ case class MillCliConfig(
169169
).count(_.value)
170170
}
171171

172-
import mainargs.ParserForClass
173-
174172
// We want this in a separate source file, but to avoid stale --help output due
175173
// to under-compilation, we have it in this file
176174
// see https://github.com/com-lihaoyi/mill/issues/2315
@@ -213,21 +211,22 @@ Advanced and internal command-line flags not intended for common usage. Use at y
213211
Advanced Options:
214212
"""
215213

216-
lazy val parser: ParserForClass[MillCliConfig] = mainargs.ParserForClass[MillCliConfig]
214+
lazy val parser: mainargs.ParserForClass[MillCliConfig] = mainargs.Parser[MillCliConfig]
217215

218-
private lazy val helpAdvancedParser: ParserForClass[MillCliConfig] = new ParserForClass(
219-
parser.main.copy(argSigs0 = parser.main.argSigs0.collect {
220-
case a if !isUnsupported(a) && a.hidden =>
221-
a.copy(
222-
hidden = false,
223-
// Hack to work around `a.copy` not propagating the name mapping correctly, so we have
224-
// to manually map the name ourselves. Doesn't affect runtime behavior since this is
225-
// just used for --help-advanced printing and not for argument parsing
226-
unMappedName = a.mappedName(mainargs.Util.kebabCaseNameMapper)
227-
)
228-
}),
229-
parser.companion
230-
)
216+
private lazy val helpAdvancedParser: mainargs.ParserForClass[MillCliConfig] =
217+
new mainargs.ParserForClass(
218+
parser.main.copy(argSigs0 = parser.main.argSigs0.collect {
219+
case a if !isUnsupported(a) && a.hidden =>
220+
a.copy(
221+
hidden = false,
222+
// Hack to work around `a.copy` not propagating the name mapping correctly, so we have
223+
// to manually map the name ourselves. Doesn't affect runtime behavior since this is
224+
// just used for --help-advanced printing and not for argument parsing
225+
unMappedName = a.mappedName(mainargs.Util.kebabCaseNameMapper)
226+
)
227+
}),
228+
parser.companion
229+
)
231230

232231
lazy val shortUsageText: String =
233232
"Please specify a task to evaluate\n" +
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//| mvnDeps:
2+
//| - info.picocli:picocli:4.7.6
3+
//| - com.squareup.okhttp3:okhttp:4.12.0
4+
//| - com.fasterxml.jackson.core:jackson-databind:2.17.2
5+
6+
import com.fasterxml.jackson.databind.*;
7+
import com.fasterxml.jackson.core.util.DefaultIndenter;
8+
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
9+
import okhttp3.*;
10+
import picocli.CommandLine;
11+
import java.io.*;
12+
import java.nio.file.*;
13+
import java.util.*;
14+
import java.util.concurrent.Callable;
15+
16+
@CommandLine.Command(name = "Crawler", mixinStandardHelpOptions = true)
17+
public class Crawler implements Callable<Integer> {
18+
19+
@CommandLine.Option(
20+
names = {"--start-article"},
21+
required = true,
22+
description = "Starting article title"
23+
)
24+
private String startArticle;
25+
26+
@CommandLine.Option(
27+
names = {"--depth"},
28+
required = true,
29+
description = "Depth of crawl"
30+
)
31+
private int depth;
32+
33+
private static final OkHttpClient client = new OkHttpClient();
34+
private static final ObjectMapper mapper = new ObjectMapper();
35+
36+
public static List<String> fetchLinks(String title) throws IOException {
37+
var url = new HttpUrl.Builder()
38+
.scheme("https")
39+
.host("en.wikipedia.org")
40+
.addPathSegments("w/api.php")
41+
.addQueryParameter("action", "query")
42+
.addQueryParameter("titles", title)
43+
.addQueryParameter("prop", "links")
44+
.addQueryParameter("format", "json")
45+
.build();
46+
47+
var request = new Request.Builder()
48+
.url(url)
49+
.header("User-Agent", "WikiFetcherBot/1.0 (https://example.com; [email protected])")
50+
.build();
51+
try (Response response = client.newCall(request).execute()) {
52+
if (!response.isSuccessful())
53+
throw new IOException("Unexpected code " + response);
54+
55+
JsonNode root = mapper.readTree(response.body().byteStream());
56+
JsonNode pages = root.path("query").path("pages");
57+
List<String> links = new ArrayList<>();
58+
59+
for (Iterator<JsonNode> it = pages.elements(); it.hasNext();) {
60+
JsonNode linkArr = it.next().get("links");
61+
if (linkArr != null && linkArr.isArray()) {
62+
for (JsonNode link : linkArr) {
63+
JsonNode titleNode = link.get("title");
64+
if (titleNode != null) links.add(titleNode.asText());
65+
}
66+
}
67+
}
68+
return links;
69+
}
70+
}
71+
72+
@Override
73+
public Integer call() throws Exception {
74+
Set<String> seen = new HashSet<>();
75+
Set<String> current = new HashSet<>();
76+
seen.add(startArticle);
77+
current.add(startArticle);
78+
79+
for (int i = 0; i < depth; i++) {
80+
Set<String> next = new HashSet<>();
81+
for (String article : current) {
82+
for (String link : fetchLinks(article)) {
83+
if (!seen.contains(link)) next.add(link);
84+
}
85+
}
86+
seen.addAll(next);
87+
current = next;
88+
}
89+
90+
Path output = Paths.get("fetched.json");
91+
try (Writer w = Files.newBufferedWriter(output)) {
92+
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
93+
printer.indentArraysWith(new DefaultIndenter(" ", "\n"));
94+
printer.indentObjectsWith(new DefaultIndenter(" ", "\n"));
95+
mapper.writer(printer).writeValue(w, seen);
96+
}
97+
return 0;
98+
}
99+
100+
public static void main(String[] args) {
101+
int exitCode = new CommandLine(new Crawler()).execute(args);
102+
System.exit(exitCode);
103+
}
104+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Mill Single-file Java programs can make it more convenient to script simple command-line
2+
// workflows interacting with files, subprocesses, and HTTP endpoints. For example, below is
3+
// a simple script using these libraries to crawl wikipedia and save the crawl results to a file:
4+
5+
//// SNIPPET:FILE
6+
/** See Also: Crawler.java */
7+
8+
/** Usage
9+
> ./mill Crawler.java --start-article=singapore --depth=2
10+
11+
> cat fetched.json
12+
[
13+
"Calling code",
14+
"+65",
15+
"British Empire",
16+
"1st Parliament of Singapore",
17+
...
18+
]
19+
20+
*/
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//| mvnDeps:
2+
//| - org.jetbrains.kotlinx:kotlinx-cli:0.3.6
3+
//| - com.squareup.okhttp3:okhttp:4.12.0
4+
//| - org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3
5+
6+
import kotlinx.cli.*
7+
import kotlinx.serialization.json.*
8+
import okhttp3.*
9+
import java.nio.file.*
10+
11+
fun fetchLinks(title: String): List<String> {
12+
val client = OkHttpClient()
13+
val url = HttpUrl.Builder()
14+
.scheme("https")
15+
.host("en.wikipedia.org")
16+
.addPathSegments("w/api.php")
17+
.addQueryParameter("action", "query")
18+
.addQueryParameter("titles", title)
19+
.addQueryParameter("prop", "links")
20+
.addQueryParameter("format", "json")
21+
.build()
22+
23+
val request = Request.Builder()
24+
.url(url)
25+
.header("User-Agent", "WikiFetcherBot/1.0 (https://example.com; [email protected])")
26+
.build()
27+
28+
client.newCall(request).execute().use { resp ->
29+
val body = resp.body?.string() ?: return emptyList()
30+
val json = Json.parseToJsonElement(body).jsonObject
31+
val pages = json["query"]?.jsonObject?.get("pages")?.jsonObject ?: return emptyList()
32+
return pages.values.flatMap { page ->
33+
page.jsonObject["links"]
34+
?.jsonArray
35+
?.mapNotNull { it.jsonObject["title"]?.jsonPrimitive?.content }
36+
?: emptyList()
37+
}
38+
}
39+
}
40+
41+
fun main(args: Array<String>) {
42+
val parser = ArgParser("wiki-fetcher")
43+
val startArticle by parser.option(ArgType.String, description = "Starting Wikipedia article").required()
44+
val depth by parser.option(ArgType.Int, description = "Depth of link traversal").required()
45+
parser.parse(args)
46+
47+
var seen = mutableSetOf(startArticle)
48+
var current = mutableSetOf(startArticle)
49+
50+
repeat(depth) {
51+
val next = current.flatMap { fetchLinks(it) }.toSet()
52+
current = (next - seen).toMutableSet()
53+
seen += current
54+
}
55+
56+
val jsonOut = Json { prettyPrint = true }
57+
.encodeToString(JsonElement.serializer(), JsonArray(seen.map { JsonPrimitive(it) }))
58+
Files.writeString(Paths.get("fetched.json"), jsonOut)
59+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Mill Kotlin scripts can make it more convenient to script simple command-line workflows
2+
// interacting with files, subprocesses, and HTTP endpoints. For example, below is a simple
3+
// script using these libraries to crawl wikipedia and save the crawl results to a file:
4+
5+
//// SNIPPET:FILE
6+
/** See Also: Crawler.kt */
7+
8+
/** Usage
9+
> ./mill Crawler.kt --startArticle singapore --depth 2
10+
11+
> cat fetched.json
12+
[
13+
"Calling code",
14+
"+65",
15+
"British Empire",
16+
"1st Parliament of Singapore",
17+
...
18+
]
19+
20+
*/

example/large/multifile/10-multi-file-builds/foo/package.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import mill.*, scalalib.*
33

44
object `package` extends build.MyModule {
55
def moduleDeps = Seq(build.bar.qux.mymodule)
6-
def mvnDeps = Seq(mvn"com.lihaoyi::mainargs:0.4.0")
6+
def mvnDeps = Seq(mvn"com.lihaoyi::mainargs:0.7.7")
77
}

example/large/multifile/10-multi-file-builds/foo/src/Foo.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package foo
2-
import mainargs.{main, ParserForMethods, arg}
2+
import mainargs.{main, Parser, arg}
33
object Foo {
44
val value = "hello"
55

@@ -12,5 +12,5 @@ object Foo {
1212
bar.qux.BarQux.printText(barQuxText)
1313
}
1414

15-
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
15+
def main(args: Array[String]): Unit = Parser(this).runOrExit(args)
1616
}
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
//| jvmId: 11.0.28
22
//| mvnDeps:
33
//| - "com.lihaoyi::scalatags:0.13.1"
4-
//| - "com.lihaoyi::mainargs:0.7.6"
4+
//| - "com.lihaoyi::mainargs:0.7.7"
55
import scalatags.Text.all.*
6-
import mainargs.{main, ParserForMethods}
6+
import mainargs.{main, Parser}
77

8-
object Foo {
9-
def generateHtml(text: String) = {
10-
h1(text).toString
11-
}
12-
13-
@main
14-
def main(text: String) = {
15-
println("Jvm Version: " + System.getProperty("java.version"))
16-
println(generateHtml(text))
17-
}
8+
def generateHtml(text: String) = {
9+
h1(text).toString
10+
}
1811

19-
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
12+
@main
13+
def main(text: String) = {
14+
println("Jvm Version: " + System.getProperty("java.version"))
15+
println(generateHtml(text))
2016
}
17+
18+
def main(args: Array[String]): Unit = Parser(this).runOrExit(args)

example/scalalib/basic/1-script/FooTests.scala

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
//| mvnDeps:
33
//| - "com.google.guava:guava:33.3.0-jre"
44
import com.google.common.html.HtmlEscapers.htmlEscaper
5-
object FooTests {
6-
def main(args: Array[String]): Unit = {
7-
val result = Foo.generateHtml("hello")
8-
assert(result == "<h1>hello</h1>")
9-
println(result)
10-
val result2 = Foo.generateHtml("<hello>")
11-
val expected2 = "<h1>" + htmlEscaper().escape("<hello>") + "</h1>"
12-
assert(result2 == expected2)
13-
println(result2)
14-
}
5+
def main(args: Array[String]): Unit = {
6+
val result = generateHtml("hello")
7+
assert(result == "<h1>hello</h1>")
8+
println(result)
9+
val result2 = generateHtml("<hello>")
10+
val expected2 = "<h1>" + htmlEscaper().escape("<hello>") + "</h1>"
11+
assert(result2 == expected2)
12+
println(result2)
1513
}

example/scalalib/basic/2-script-builtins/Foo.scala renamed to example/scalalib/basic/2-script-builtins/Crawler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ def main(startArticle: String, depth: Int) = {
2828
os.write(os.pwd / "fetched.json", upickle.stream(seen, indent = 4))
2929
}
3030

31-
def main(args: Array[String]): Unit = mainargs.ParserForMethods(this).runOrExit(args)
31+
def main(args: Array[String]): Unit = mainargs.Parser(this).runOrExit(args)

0 commit comments

Comments
 (0)