Skip to content

feat: add "jbang.source" and "jbang.dir" - #2643

Open
jmini wants to merge 1 commit into
jbangdev:mainfrom
jmini:issue-2642
Open

feat: add "jbang.source" and "jbang.dir"#2643
jmini wants to merge 1 commit into
jbangdev:mainfrom
jmini:issue-2642

Conversation

@jmini

@jmini jmini commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #2642


NOTE for the reviewer: I changed one existing test:
testProperties() used to split the generated command line on example.java and expect exactly 2 parts;
-Djbang.source legitimately puts the script path on the command line a second time, so the split now uses fakemain, the main class — the actual boundary between JVM options and program arguments, which appears exactly once.

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • ai-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f60956f-bd56-4021-a191-fb5de1313b33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maxandersen

Copy link
Copy Markdown
Collaborator

Thanks for working on this — the motivation is spot on, and the prior-art table in the issue makes a compelling case. Happy to see this landing.

That said, looking at the implementation there are a few gaps versus what the issue proposes and what I think we need for consistency. Before iterating on the code I would like your input on the direction.

Gaps / open questions

Invocation Current PR Issue proposes Gap
jbang script.java -Djbang.source=/abs/script.java ✅ same
jbang https://…/x.java ❌ not set URI as value PR diverges from issue
jbang -c "code" / stdin ❌ not set (caught by accident via RuntimeException catch-all) ❌ not set No explicit guard — works by accident, needs a test or an instanceof LiteralResourceRef check
GAV (group:artifact:version) ❌ not set ❌ not set
.jar file ❌ not set ❌ not set
jshell (jbang --jsh script.jsh) ❌ not set (only JarCmdGenerator is patched) should be set JshCmdGenerator is missed
native image (--native) ❌ not set should be set (issue explicitly mentions it) NativeCmdGenerator is missed
Doc xref anchor #script-location-properties AsciiDoc auto-generates _script_location_properties (underscores), so the FAQ/organizing cross-refs will 404. Need explicit [[script-location-properties]] anchor.

System property vs env var

The issue proposes both -Djbang.source and JBANG_SOURCE env var. The PR only does the system property. I think we need the env var too because:

  • env vars are inherited by child processes (a script that shells out passes the info for free)
  • env vars reach native-image builds where there is no JVM to hold -D properties
  • our existing runtime info (JBANG_RUNTIME_SHELL, JBANG_LAUNCH_CMD, JBANG_STDIN_NOTTY) already uses env vars as the convention

If we set the env var in BaseCmdGenerator rather than each individual generator, jshell and native get it for free.

Naming

JBANG_SOURCE feels ambiguous — the value could be a file path, a URL, a GAV, depending on invocation. Something like JBANG_SCRIPT_REF (and correspondingly jbang.script.ref / jbang.script.dir for the properties) might better convey that it is "whatever reference the user passed" rather than implying it is always a source file. Open to other ideas here — what do you think?

Would love your thoughts on these before the next iteration. 🙏

@maxandersen

Copy link
Copy Markdown
Collaborator

i see in the issue you suggest we only test this for local source ....but that means the behaviour now depends on how this is called?

how about aliases? what will they use as source location?

@jmini

jmini commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @max

I looked your points and with :spark I have tried to move the discussion forward.

From your findings:

  • Two are real gaps
  • two I think are false positives
  • and on the naming, I'd like to counter-propose on.

In addition: implementation of --native and the env variable variant might need some pre-requisites clarifications and/or fixes (maybe in a dedicated issue/pr)

Real gaps, agreed

jshell. You're right, JshCmdGenerator is missed. Note it needs a different mechanism:
it propagates properties by writing System.setProperty(...) into the jshell startup file
(generateArgs, JshCmdGenerator.java:94), not via -D. The ref is already in hand there.
Easy to add.

native. Also missed — but the gap is wider than this feature. NativeCmdGenerator emits
no -D properties at all: addPropertyFlags is only called from JarCmdGenerator:115, so a
--native run silently drops the user's own -Dfoo=bar too. I'd rather fix that as its own
issue ("--native drops all -D properties") and let jbang.source come along for free,
than special-case one property into native.

Two I think are false positives

-c / stdin are explicitly guarded, not accidental. --code goes through
LiteralScriptResourceResolver.stringToResourceRef(null, …) (Run.java:88), so
getOriginalResource() is null and the original == null guard catches it; stdin passes
"-" and hits ref.isStdin(). The RuntimeException catch is only for a ref that cannot
produce a file at all. Both guards are load-bearing rather than defensive: --code and stdin
do have a real file, in ~/.jbang/cache/stdins/<hash>/, so without them we would report a
cache path — the exact thing this feature exists to avoid. Happy to add explicit tests for
both so it is pinned rather than argued.

Doc anchor. Antora sets idprefix: '' and idseparator: '-' by default, so
== Script Location Properties becomes script-location-properties. The repo already relies
on this everywhere — there is not a single explicit [[…]] anchor under
docs/modules/ROOT/pages/, and e.g. script-directives.adoc:281 links to
running.adoc#setting-main-class. If the site playbook overrides those attributes I will add
the explicit anchor, but then a lot of existing xrefs are broken too.

Aliases

These already work, and I should have said so. AliasedResourceRef extends WrappedResourceRef, which delegates getOriginalResource() and getFile(), so an alias
resolving to a local file yields that file's absolute path and its directory. An alias
resolving to a URL is skipped like any other URL. That matches the issue's table
("the resolved path or URI, whichever it came from").

"The behaviour depends on how it's called"

Yes — and I'd argue that is correct rather than a defect, because it is what every runtime we
compared against does: python -c has no __file__, bash -c has no $0 pointing at a
file. There is no honest value for "where does this script live" when it does not live
anywhere. What we can fix is making the mode explicit instead of leaving a script to infer
it from a null, which leads me to naming.

Naming — counter-proposal

I take the point that "source" over-promises if the value can be a URL or a GAV. But
jbang.script.ref + jbang.script.dir has the opposite problem: .dir sits next to a value
that may be a URL, where a directory is undefined — and .dir is the one people actually
use (${BASH_SOURCE[0]%/*}, os.path.dirname(__file__)).

So rather than one overloaded name, how about splitting the two questions?

Property Always set? Value
jbang.ref yes, except --code the reference as the user typed it: path, URL, GAV, alias name, - for stdin
jbang.source only when the primary source is a real local file absolute, normalised path
jbang.dir exactly when jbang.source is its parent directory

jbang.ref answers "how was I invoked" and is always there, so the remote case stops being a
hole — a script can see it is running from a URL. jbang.source/jbang.dir keep a single
narrow meaning: there is a file, here it is. Each property then has one type, and "is there a
local file?" is one != null check instead of a URL-vs-path sniff at the call site.

On remote sources and the env var

Two constraints I ran into, which is why the PR is scoped the way it is.

Setting the URI for remote sources contradicts two existing assertions:
TestRun.testURLPrepare asserts a URL-sourced script puts no file: on the command line, and
testJarViaHttps asserts no https. I did not want to relax deliberate assertions inside a
feature PR. If jbang.ref is the right shape, that is the change those tests need to allow,
and it should be a conscious decision — remote refs can carry credentials and query strings,
so it is worth deciding on purpose that they belong on the generated command line.

For the env var: the obstacle is bigger than CommandBuffer lacking a method. One correction
first — JarCmdGenerator:308 overrides generateCommandLineString, so a change in
BaseCmdGenerator would reach the jshell and native paths but not the ordinary jar path.

The real problem is that JBang prints the command and the launcher executes it, and each
launcher has its own idiom:

  • src/main/scripts/jbang:167 does eval "exec $output", so a VAR=value prefix lands
    after exec and is taken as the name of the program to run — exec: VAR=value: not found,
    exit 127. The POSIX form has to be env VAR=value …, which makes CommandBuffer own
    argument 0 and collides with usingArgsFile(), which assumes argument 0 is the executable and
    would emit env @/tmp/jbang.args. That path is live for the run command
    (JarCmdGenerator:308-313 calls applyWindowsMaxCliLimit()).
  • jbang.cmd is the easy case: it opens with SETLOCAL, so a set is scoped to the launcher
    and still inherited by the child process. Its only real constraint is that for /f reads a
    single line.
  • jbang.ps1:120 runs Invoke-Expression "& $output". The & call operator wants a command
    as the first token rather than an assignment, and PowerShell has no SETLOCAL — which is why
    that script saves and restores its own three variables by hand (:103 and :125). A single
    generated line cannot restore anything after the child exits, so the naive version would leave
    the variable set in the user's interactive session, pointing at whichever script ran last.

So it is closer to a launcher-protocol change than a CommandBuffer feature, which is why I
left it out of this PR rather than half-doing it.

One nuance on the convention argument: JBANG_RUNTIME_SHELL, JBANG_LAUNCH_CMD and
JBANG_STDIN_NOTTY are exported by the launcher scripts into jbang
(src/main/scripts/jbang:147-150, jbang.ps1:103-104) and reach the script only by process
inheritance — so they are precedent for "the launcher exports", not yet for "jbang emits".
That does not make the env var a bad idea, it just means it is new ground rather than an
existing pattern.

Proposed plan

  1. This PR, adjusted: add jsh, add explicit tests for --code/stdin/alias, rename to
    jbang.ref + jbang.source + jbang.dir if you like that shape.

  2. Native — this one I would like your call on. Should --native set these properties?

    If yes, it needs a prior fix rather than a special case. NativeCmdGenerator.generate()
    emits [--enable-preview] <image> <args> and nothing else, and createNativeCmdGenerator
    (CmdGeneratorBuilder.java:142-144) never passes runtimeOptions either — so today
    jbang --native -Dfoo=bar x.java silently drops foo, and //JAVA_OPTIONS with it, while
    the properties are still applied at build time for directive substitution and dependency
    resolution. That is a pre-existing bug rather than something this feature introduces, and
    GraalVM native executables do accept -D at run time, so it is fixable rather than
    inherent.

    If native is in scope, I think it is worth doing that first, as its own issue and PR, and
    rebasing this one on top. It is a bug with its own decisions — does a program argument that
    starts with -D get consumed by the image runtime, do runtime options come along, and it
    really wants a native-image check in CI — and it deserves a title that says so rather than
    riding along under "add jbang.source". Once it lands, this feature covers native for free
    instead of special-casing one property into a generator that passes none. Happy to open that
    issue and take it on if you agree.

    Either way I will fix something in this PR: the docs note lists stdin, --code, URLs, jars
    and GAVs as the cases with no properties, and says nothing about --native or --jsh, so a
    native user would read it and expect them to be set. That omission is mine and this PR
    created the asymmetry, so it should name it — plus a test pinning whatever we agree the
    native behaviour is.

  3. The env var: I would rather agree on whether it is needed before building it, for the
    launcher reasons above. If you have a case that the property plus one line in the script
    (pb.environment().put("JBANG_SOURCE", System.getProperty("jbang.source"))) does not cover,
    I will file an issue and work through it.

Tell me which of those you want in this PR and I will reshape it. Also happy to be wrong on
the anchor if the playbook overrides the id attributes.

@maxandersen

Copy link
Copy Markdown
Collaborator

can you get your bot to summarize that text ? :)

imo properties just isn't the way as native can't reliably get it same as jshell.

For aliases - I don't grok why you are ok use the absolute file for aliases, but not for URLs?

Fundementally though the big problem here is that this mechanism of getting which folder something is running from is NOT reliably the same as the folder of where the source was listed. It just isn't.

for a source file, sure - you will want local dir of the source file.
but a .jar also have a local dir
GAV local dir is in your ~/.m2 somewhere
a URL is downlaoded to disk so the local dir does not make sense.

Maybe we should have two env vars set:

JBANG_RUN_SOURCE which is the actual thing being requests, alias, jar, file, url etc
JBANG_RUN_LOCAL which is set only if there is a meaning ful local folder the script is running from (which means URL will be empty, but .jar, .java, GAV's and aliases will have a local folder which is the thing resolved just before building/running it)

@quintesse

Copy link
Copy Markdown
Contributor

for a source file, sure - you will want local dir of the source file.

Not even that is 100% true in all cases. Remember that sometimes we change the name of the file and we can even edit the code. In those cases the source file gets copied to the cache folder and it's compiled from that location. Now, I guess you'd normally would want the original path, but it won't be the actual source file being compiled/executed.

@jmini

jmini commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

can you get your bot to summarize that text ? :)

Yes sorry about that... I made him even expand some places that I considered as not clear enough... Not easy to find the right tradeoff here…

@jmini

jmini commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Maybe trying to contribute, assisted from ✨ isn't such a great idea... Still I hope it is OK for you to try to continue

Point by point an answer on the different points discussed here.

@quintesse is right, and it is a bug in this PR

Verified against a build of this branch:

Invocation jbang.source today
jbang /p/Loc.java /p/Loc.java ok
jbang /p/my-tool — extensionless, shebang ~/.jbang/cache/scripts/04182c…/MyTool.java wrong
jbang /p/my-project/ — a directory, so main.java inside it ~/.jbang/cache/scripts/29f64a…/main.java wrong
alias → local .java resolved absolute path ok
local .jar not set ok

RenamingScriptResourceResolver copies any local file without a known source extension into the
script cache and returns forResolvedResource(yourPath, cachePath). I read getFile(), so I get the
copy. Both broken rows are the common cases — the extensionless shebang script and jbang <dir>
and both hand the script a cache directory, which is the exact thing #2642 exists to prevent.

getOriginalResource() still holds what the user typed, so it is fixable. But it means my guard was
the wrong question: not "which invocations do we skip" but "which accessor tells the truth".

Which is your point, and I think it is the decision

There are three things here, not two:

Question For jbang my-project/ Defined for
A what was requested my-project — a directory all but --code
B where the user's own file lives my-project/main.java local files only
C where the running artifact sits ~/.jbang/cache/scripts/29f6… everything

JBANG_RUN_SOURCE is A — agreed, no argument from me. JBANG_RUN_LOCAL is C. My PR meant B and
implemented C, which is the bug above.

My hesitation about C standing in for B: it is defined more often but useful less often. #2642 is
about "a script checked into a project needs its project root", and for that
~/.m2/repository/com/foo/1.0/ is a confidently wrong answer where an empty value would have been a
checkable one. B being absent is what makes it trustworthy when present.

One thing in your rule I do not follow: a URL is empty "because it is downloaded to disk", but a GAV
gets ~/.m2. Both are caches. Either both are C and both get filled, or the criterion is really "a
folder the user owns" — which is B, and then GAV and .jar are empty too. Which did you mean?

Aliases

It was never an alias-vs-URL rule. AliasedResourceRef extends WrappedResourceRef and forwards both
accessors, so an alias to a local file behaves as that file and an alias to a URL behaves as a URL —
no exemption. (For A, note getOriginalResource() on an alias gives the resolved target rather than
the alias name; that needs getAlias().)

Env vars — agreed, and here is what it costs

Worth knowing before we commit to it, because it is not a CommandBuffer method.

JBang prints the command and the launcher executes it, and each launcher has its own idiom:

  • src/main/scripts/jbang:167 does eval "exec $output", so a VAR=value prefix is taken as the
    name of the program to run — exec: VAR=value: not found, exit 127. It has to become
    env VAR=value …, which collides with usingArgsFile() assuming argument 0 is the executable.
  • jbang.ps1:120 runs Invoke-Expression "& $output", where & wants a command rather than an
    assignment — and PowerShell has no SETLOCAL, so the variable stays set in the user's session
    afterwards. That script hand-restores its own three variables at :103/:125 for that reason; a
    generated one-liner cannot.
  • jbang.cmd is fine: SETLOCAL scopes it and the child still inherits. Only constraint is that
    for /f reads one line.

So it is a launcher change more than a generator one. Do you want it inside this PR, or as a
prerequisite PR adding "environment for the launched process" that this one then uses?

What I would do next

  1. Fix the cache-path bug here, plus jsh.
  2. Settle A/B/C and the names — that decides everything else.
  3. Env var work, wherever you want it to land.

Separately, related to this topic and kind of a pre-requisite for the --native discussion here:
I opened #2649: NativeCmdGenerator passes no properties or runtime options at
all today, so jbang --native -Dfoo=bar x.java drops foo, and the script's own
//RUNTIME_OPTIONS with it. Pre-existing rather than caused by this PR, and less urgent if env
vars are the transport since a native binary inherits those — but it should not stay silent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: tell the running script where its source file is

3 participants