Skip to content

Commit

Permalink
feat: Added --code option for running literal scripts (quarkusio#1243)
Browse files Browse the repository at this point in the history
* feat: Added `--code` option for running literal scripts

The `--code` (or `-c`) option either takes a String argument (using `=`)
that will be taken as a literal script or it can appear without argument
in which case the `scriptOrFile` argument will be assumed to contain a
literal script. In both cases the literal script will be executed in
the same way as if the code was passed on the stdin (that is, the code
will be assumed to be Jshell code unless it obviously is plain Java).
Also allows the `--interactive` option to be used without any script
reference at all.
The shortcut `-i` was added for `--interactive`.

Fixes quarkusio#1242

* docs: Added docs for `--code` code_option

See quarkusio#1242

* test: Added tests for `--code` option

See quarkusio#1242

* fix: fixes handling of `//SOURCES` in ephemeral scripts

This is because Jbang tries to look up those paths relative to the
original script location which in the case of scripts that come from
stdin or from the `--code` option is not available. In those cases we
fall back to using the current working directory.

See quarkusio#1242

* feat: added `--sources` (`-s`) option for adding additional sources

* test: added test for `--sources` option
  • Loading branch information
quintesse committed Feb 18, 2022
1 parent afd7d51 commit 9c0b0ca
Show file tree
Hide file tree
Showing 27 changed files with 192 additions and 43 deletions.
6 changes: 6 additions & 0 deletions docs/modules/ROOT/pages/usage.adoc
Expand Up @@ -230,6 +230,12 @@ jbang init -t hello.kt hello.kt
Hello World
----

== Running script passed as argument

jbang can run scripts that are passed directly on the command line using the `--code` option:

`jbang --code System.out.println("Hello World!")`

== Running script from standard input

jbang can run scripts directly from standard input using `-` or `/dev/stdin` as input.
Expand Down
6 changes: 6 additions & 0 deletions itests/bar/Bar.java
@@ -0,0 +1,6 @@
import static java.lang.System.*;
public class Bar {
public static void main(String... args) {
out.println("Hello World");
}
}
7 changes: 7 additions & 0 deletions itests/foo.java
@@ -0,0 +1,7 @@
import static java.lang.System.*;

public class foo {
public static void main(String... args) {
out.println(Bar.class.getName());
}
}
4 changes: 4 additions & 0 deletions itests/jsh.feature
Expand Up @@ -43,3 +43,7 @@ Scenario: jsh with deps 1
Then match err !contains ".NoClassDef"
Then match out contains "Fake output:"

Scenario: as code option
* command('jbang --code "System.out.println(\\\"Hello\\\")" jbangtest')
* match out == "Hello\n"

14 changes: 14 additions & 0 deletions itests/run-nix.feature
@@ -0,0 +1,14 @@
Feature: run on non-windows

Background:
* if (windows) karate.abort()

Scenario: as code option 2
* command('jbang --code "$(cat helloworld.java)" jbangtest')
* match err == "[jbang] Building jar...\n"
* match out == "Hello jbangtest\n"

Scenario: as code option 3
* command('jbang "--code=$(cat helloworld.java)" jbangtest')
* match err == "[jbang] Building jar...\n"
* match out == "Hello jbangtest\n"
4 changes: 4 additions & 0 deletions itests/run.feature
Expand Up @@ -28,6 +28,10 @@ Scenario: java run multiple matching sources
Then match out contains "NestedOne"
Then match out contains "NestedTwo"

Scenario: java run multiple sources via cli
When command('jbang -s bar/Bar.java foo.java')
Then match out contains "Bar"

Scenario: java run multiple files
When command('jbang res/resource.java')
Then match out contains "hello properties"
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/dev/jbang/Main.java
Expand Up @@ -31,7 +31,8 @@ private static String[] handleDefaultRun(CommandLine.Model.CommandSpec spec, Str
}
// Check if we have a parameter and it's not the same as any of the subcommand
// names
if (!remainingArgs.isEmpty() && !spec.subcommands().containsKey(remainingArgs.get(0))) {
if (!remainingArgs.isEmpty() && !spec.subcommands().containsKey(remainingArgs.get(0))
|| hasRunOpts(leadingOpts)) {
List<String> result = new ArrayList<>();
result.add("run");
result.addAll(leadingOpts);
Expand All @@ -40,4 +41,13 @@ private static String[] handleDefaultRun(CommandLine.Model.CommandSpec spec, Str
}
return args;
}

private static boolean hasRunOpts(List<String> opts) {
boolean res = opts.contains("-i") || opts.contains("--interactive")
|| opts.contains("-c") || opts.contains("--code");
res = res || opts .stream()
.anyMatch(o -> o.startsWith("-i=") || o.startsWith("--interactive=")
|| o.startsWith("-c=") || o.startsWith("--code="));
return res;
}
}
9 changes: 7 additions & 2 deletions src/main/java/dev/jbang/catalog/Alias.java
Expand Up @@ -20,6 +20,7 @@ public class Alias extends CatalogItem {
public final List<String> arguments;
@SerializedName(value = "java-options")
public final List<String> javaOptions;
public final List<String> sources;
public final List<String> dependencies;
public final List<String> repositories;
public final List<String> classpaths;
Expand All @@ -30,13 +31,14 @@ public class Alias extends CatalogItem {
public final String mainClass;

private Alias() {
this(null, null, null, null, null, null, null, null, null, null, null);
this(null, null, null, null, null, null, null, null, null, null, null, null);
}

public Alias(String scriptRef,
String description,
List<String> arguments,
List<String> javaOptions,
List<String> sources,
List<String> dependencies,
List<String> repositories,
List<String> classpaths,
Expand All @@ -49,6 +51,7 @@ public Alias(String scriptRef,
this.description = description;
this.arguments = arguments;
this.javaOptions = javaOptions;
this.sources = sources;
this.dependencies = dependencies;
this.repositories = repositories;
this.classpaths = classpaths;
Expand Down Expand Up @@ -118,6 +121,8 @@ private static Alias merge(Alias a1, String name, Function<String, Alias> findUn
List<String> args = a1.arguments != null && !a1.arguments.isEmpty() ? a1.arguments : a2.arguments;
List<String> opts = a1.javaOptions != null && !a1.javaOptions.isEmpty() ? a1.javaOptions
: a2.javaOptions;
List<String> srcs = a1.sources != null && !a1.sources.isEmpty() ? a1.sources
: a2.sources;
List<String> deps = a1.dependencies != null && !a1.dependencies.isEmpty() ? a1.dependencies
: a2.dependencies;
List<String> repos = a1.repositories != null && !a1.repositories.isEmpty() ? a1.repositories
Expand All @@ -129,7 +134,7 @@ private static Alias merge(Alias a1, String name, Function<String, Alias> findUn
String javaVersion = a1.javaVersion != null ? a1.javaVersion : a2.javaVersion;
String mainClass = a1.mainClass != null ? a1.mainClass : a2.mainClass;
Catalog catalog = a2.catalog != null ? a2.catalog : a1.catalog;
return new Alias(a2.scriptRef, desc, args, opts, deps, repos, cpaths, props, javaVersion, mainClass,
return new Alias(a2.scriptRef, desc, args, opts, srcs, deps, repos, cpaths, props, javaVersion, mainClass,
catalog);
} else {
return a1;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/jbang/catalog/Catalog.java
Expand Up @@ -56,8 +56,8 @@ public Catalog(String baseRef, String description, ResourceRef catalogRef, Map<S
catalogs.forEach((key, c) -> this.catalogs.put(key,
new CatalogRef(c.catalogRef, c.description, this)));
aliases.forEach((key, a) -> this.aliases.put(key,
new Alias(a.scriptRef, a.description, a.arguments, a.javaOptions, a.dependencies, a.repositories,
a.classpaths, a.properties, a.javaVersion, a.mainClass, this)));
new Alias(a.scriptRef, a.description, a.arguments, a.javaOptions, a.sources, a.dependencies,
a.repositories, a.classpaths, a.properties, a.javaVersion, a.mainClass, this)));
templates.forEach((key, t) -> this.templates.put(key,
new Template(t.fileRefs, t.description, t.properties, this)));
}
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/dev/jbang/catalog/CatalogUtil.java
Expand Up @@ -35,6 +35,7 @@ public static Path addNearestAlias(String name,
String scriptRef,
String description,
List<String> arguments,
List<String> sources,
List<String> dependencies,
List<String> repositories,
List<String> classPaths,
Expand All @@ -43,8 +44,8 @@ public static Path addNearestAlias(String name,
String javaVersion,
String mainClass) {
Path catalogFile = Catalog.getCatalogFile(null);
addAlias(catalogFile, name, scriptRef, description, arguments, javaRuntimeOptions, dependencies, repositories,
classPaths, properties, javaVersion, mainClass);
addAlias(catalogFile, name, scriptRef, description, arguments, javaRuntimeOptions, sources, dependencies,
repositories, classPaths, properties, javaVersion, mainClass);
return catalogFile;
}

Expand All @@ -60,6 +61,7 @@ public static Alias addAlias(Path catalogFile,
String description,
List<String> arguments,
List<String> javaRuntimeOptions,
List<String> sources,
List<String> dependencies,
List<String> repositories,
List<String> classPaths,
Expand All @@ -70,7 +72,8 @@ public static Alias addAlias(Path catalogFile,
catalogFile = cwd.resolve(catalogFile);
Catalog catalog = Catalog.get(catalogFile);
scriptRef = catalog.relativize(scriptRef);
Alias alias = new Alias(scriptRef, description, arguments, javaRuntimeOptions, dependencies, repositories,
Alias alias = new Alias(scriptRef, description, arguments, javaRuntimeOptions, sources, dependencies,
repositories,
classPaths, properties, javaVersion, mainClass, catalog);
catalog.aliases.put(name, alias);
try {
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/dev/jbang/cli/Alias.java
Expand Up @@ -61,6 +61,9 @@ class AliasAdd extends BaseAliasCommand {
@CommandLine.Mixin
DependencyInfoMixin dependencyInfoMixin;

@CommandLine.Option(names = { "-s", "--sources" }, description = "Add additional sources.")
List<String> sources;

@CommandLine.Option(names = { "--description",
"-d" }, description = "A description for the alias")
String description;
Expand Down Expand Up @@ -102,11 +105,11 @@ public Integer doCall() {

Path catFile = getCatalog(false);
if (catFile != null) {
CatalogUtil.addAlias(catFile, name, scriptOrFile, desc, userParams, javaRuntimeOptions,
CatalogUtil.addAlias(catFile, name, scriptOrFile, desc, userParams, javaRuntimeOptions, sources,
dependencyInfoMixin.getDependencies(), dependencyInfoMixin.getRepositories(),
dependencyInfoMixin.getClasspaths(), dependencyInfoMixin.getProperties(), javaVersion, mainClass);
} else {
catFile = CatalogUtil.addNearestAlias(name, scriptOrFile, desc, userParams, javaRuntimeOptions,
catFile = CatalogUtil.addNearestAlias(name, scriptOrFile, desc, userParams, javaRuntimeOptions, sources,
dependencyInfoMixin.getDependencies(), dependencyInfoMixin.getRepositories(),
dependencyInfoMixin.getClasspaths(), dependencyInfoMixin.getProperties(), javaVersion, mainClass);
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/dev/jbang/cli/BaseBuildCommand.java
Expand Up @@ -59,6 +59,9 @@ public abstract class BaseBuildCommand extends BaseScriptCommand {
@CommandLine.Mixin
DependencyInfoMixin dependencyInfoMixin;

@CommandLine.Option(names = { "-s", "--sources" }, description = "Add additional sources.")
List<String> sources;

@CommandLine.Option(names = { "-m",
"--main" }, description = "Main class to use when running. Used primarily for running jar's.")
String main;
Expand Down Expand Up @@ -179,7 +182,7 @@ public static IntegrationResult buildJar(ScriptSource src, RunContext ctx, File

// add source files to compile
optionList.add(src.getResourceRef().getFile().getPath());
optionList.addAll(src .getAllSources()
optionList.addAll(ctx .getAllSources(src)
.stream()
.map(x -> x.getResourceRef().getFile().getPath())
.collect(Collectors.toList()));
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/dev/jbang/cli/BaseScriptCommand.java
Expand Up @@ -24,9 +24,15 @@ public abstract class BaseScriptCommand extends BaseCommand {
@CommandLine.Option(names = { "--jsh" }, description = "Force input to be interpreted with jsh/jshell")
boolean forcejsh = false;

@CommandLine.Parameters(index = "0", arity = "1", description = "A file with java code or if named .jsh will be run with jshell")
@CommandLine.Parameters(index = "0", arity = "0..1", description = "A reference to a source file")
String scriptOrFile;

protected void requireScriptArgument() {
if (scriptOrFile == null) {
throw new IllegalArgumentException("Missing required parameter: '<scriptOrFile>'");
}
}

static protected boolean needsJar(Source source, RunContext context) {
// anything but .jar and .jsh files needs jar
return !(source.isJar() || context.isForceJsh() || source.isJShell());
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/jbang/cli/Build.java
Expand Up @@ -12,6 +12,7 @@ public class Build extends BaseBuildCommand {

@Override
public Integer doCall() throws IOException {
requireScriptArgument();
if (insecure) {
enableInsecure();
}
Expand All @@ -35,6 +36,7 @@ RunContext getRunContext() {
ctx.setMainClass(main);
ctx.setNativeImage(nativeImage);
ctx.setCatalog(catalog);
ctx.setAdditionalSources(sources);
return ctx;
}
}
4 changes: 2 additions & 2 deletions src/main/java/dev/jbang/cli/Edit.java
Expand Up @@ -58,7 +58,7 @@ public class Edit extends BaseScriptCommand {

@Override
public Integer doCall() throws IOException {

requireScriptArgument();
if (insecure) {
enableInsecure();
}
Expand Down Expand Up @@ -258,7 +258,7 @@ File createProjectForEdit(ScriptSource src, RunContext ctx, boolean reload) thro
Path srcFile = srcDir.toPath().resolve(name);
Util.createLink(srcFile, originalFile.toPath());

for (ScriptSource source : src.getAllSources()) {
for (ScriptSource source : ctx.getAllSources(src)) {
File sfile = null;
if (source.getJavaPackage().isPresent()) {
File packageDir = new File(srcDir, source.getJavaPackage().get().replace(".", File.separator));
Expand Down
1 change: 1 addition & 0 deletions src/main/java/dev/jbang/cli/Info.java
Expand Up @@ -138,6 +138,7 @@ public ScriptInfo(Source src, RunContext ctx) {
private static Set<String> scripts;

ScriptInfo getInfo() {
requireScriptArgument();
if (insecure) {
enableInsecure();
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/dev/jbang/cli/Init.java
Expand Up @@ -46,6 +46,7 @@ public class Init extends BaseScriptCommand {

@Override
public Integer doCall() throws IOException {
requireScriptArgument();
dev.jbang.catalog.Template tpl = dev.jbang.catalog.Template.get(initTemplate);
if (tpl == null) {
throw new ExitException(BaseCommand.EXIT_INVALID_INPUT,
Expand Down
38 changes: 35 additions & 3 deletions src/main/java/dev/jbang/cli/Run.java
Expand Up @@ -18,6 +18,7 @@
import dev.jbang.source.RunContext;
import dev.jbang.source.ScriptSource;
import dev.jbang.source.Source;
import dev.jbang.source.resolvers.LiteralScriptResourceResolver;
import dev.jbang.util.JavaUtil;
import dev.jbang.util.Util;

Expand Down Expand Up @@ -60,20 +61,50 @@ boolean debug() {
@CommandLine.Option(names = { "--javaagent" }, parameterConsumer = KeyOptionalValueConsumer.class)
public Map<String, Optional<String>> javaAgentSlots;

@CommandLine.Option(names = { "--interactive" }, description = "activate interactive mode")
@CommandLine.Option(names = { "-i", "--interactive" }, description = "Activate interactive mode")
public boolean interactive;

@CommandLine.Option(names = { "-c",
"--code" }, arity = "0..1", description = "Run the given string as code", preprocessor = StrictParameterPreprocessor.class)
public Optional<String> literalScript;

@CommandLine.Parameters(index = "1..*", arity = "0..*", description = "Parameters to pass on to the script")
public List<String> userParams = new ArrayList<>();

@Override
public Integer doCall() throws IOException {
if (scriptOrFile == null && !interactive && !literalScript.isPresent()) {
throw new IllegalArgumentException("Missing required parameter: '<scriptOrFile>'");
}

if (insecure) {
enableInsecure();
}

RunContext ctx = getRunContext();
Source src = ctx.forResource(scriptOrFile);
Source src;
if (literalScript.isPresent()) {
String script;
if (!literalScript.get().isEmpty()) {
script = literalScript.get();
if (scriptOrFile != null) {
userParams.add(0, scriptOrFile);
scriptOrFile = null;
}
} else {
script = scriptOrFile;
}
src = ctx.forResourceRef(LiteralScriptResourceResolver.stringToResourceRef(null, script));
} else {
if (scriptOrFile != null) {
src = ctx.forResource(scriptOrFile);
} else {
// HACK it's a crappy way to work around the fact that in the case of
// interactive we might not have a file to reference but all the code
// expects one to exist
src = ctx.forResourceRef(LiteralScriptResourceResolver.stringToResourceRef(null, ""));
}
}
src = prepareArtifacts(src, ctx);

String cmdline = generateOSCommandLine(src, ctx);
Expand All @@ -94,6 +125,7 @@ RunContext getRunContext() {
ctx.setMainClass(main);
ctx.setNativeImage(nativeImage);
ctx.setCatalog(catalog);
ctx.setAdditionalSources(sources);
return ctx;
}

Expand Down Expand Up @@ -322,7 +354,7 @@ List<String> generateCommandLineList(Source src, RunContext ctx) throws IOExcept
} else {
if (ctx.isForceJsh() || src.isJShell()) {
if (src instanceof ScriptSource) {
for (Source s : ((ScriptSource) src).getAllSources()) {
for (Source s : ctx.getAllSources((ScriptSource) src)) {
fullArgs.add(s.getResourceRef().getFile().toString());
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/jbang/source/MarkdownScriptSource.java
Expand Up @@ -8,7 +8,7 @@

import dev.jbang.cli.BaseCommand;
import dev.jbang.cli.ExitException;
import dev.jbang.source.resolvers.StdinScriptResourceResolver;
import dev.jbang.source.resolvers.LiteralScriptResourceResolver;

public class MarkdownScriptSource extends ScriptSource {

Expand All @@ -22,7 +22,7 @@ public static ScriptSource create(ResourceRef resourceRef, Function<String, Stri
// this will cache the content in stdin cache which is not optimal but needed to
// have the transformed script stored
// seperately from the possibly originally cached file.
resourceRef = StdinScriptResourceResolver.stringToResourceRef(resourceRef.getOriginalResource(),
resourceRef = LiteralScriptResourceResolver.stringToResourceRef(resourceRef.getOriginalResource(),
scriptText);
} catch (IOException e) {
throw new ExitException(BaseCommand.EXIT_UNEXPECTED_STATE,
Expand Down

0 comments on commit 9c0b0ca

Please sign in to comment.