Apache Jelly 常用 CLI 命令与工具详解

逐参数实测 Apache Jelly 命令行(jelly 脚本/-script/-o/-D/-awt/-swing/--help 及退出码行为)与 Ant Task 方式调用(JellyTask taskdef),并附 jelly 与 Ant 双向集成的完整命令与回显。

Apache JellyApache JellyJavaUbuntuUbuntu 22.04Linux2026/10/16

Apache Jelly 常用 CLI 命令与工具详解

目标:把 Apache Commons Jelly 1.0.1 自带的命令行工具 jelly 的每个参数逐个实测(含真实回显与退出码),并完整演示「Ant Task 方式调用 Jelly 脚本」与「Jelly 脚本内调用 Ant 任务」的双向集成。全部命令均在 Ubuntu 22.04 + OpenJDK 17 环境实测,示例目录 /root/jelly-demo/。

一、环境信息

  • 操作系统:Ubuntu 22.04(x86_64)
  • JDK:OpenJDK 17.0.20.1(实测可正常运行 Jelly 1.0.1)
  • 实测对象:commons-jelly 1.0.1(Maven Central 坐标 commons-jelly:commons-jelly)
  • Ant:Apache Ant(TM) version 1.10.12(apt 仓库 ant)
  • 运行环境:/opt/jelly/lib/(依赖 jar),启动脚本 /usr/local/bin/jelly
  • 示例目录:/root/jelly-demo/

环境核实回显:

java -version 2>&1 | head -1
openjdk version "17.0.20.1" 2026-08-18
ant -version
Apache Ant(TM) version 1.10.12 compiled on January 17 1970

💡 技术拓展:Java 命令行工具的参数解析库常见三种——

解析库特点典型用户
Apache Commons CLI老牌,POSIX/GNU 风格选项jelly 1.0.1(本次实测)
JCommander注解式声明,现代各类新工具
picocli注解 + 子命令 + 彩色帮助新一代 CLI

jelly 1.0.1 采用 Commons CLI 1.0 的 GnuParser,因此本文所有参数行为均可从 CommandLineParser 源码与其真实回显双向印证。

二、工具概览

Jelly 的「工具面」由两部分组成(均在实测 jar 中确认存在):

工具入口类方向用途
jelly 命令行org.apache.commons.jelly.Jelly#main终端 → Jelly 脚本独立运行 .jelly 脚本
JellyTask(Ant Task)org.apache.commons.jelly.task.JellyTaskAnt → Jelly在 build.xml 中把 Jelly 脚本作为构建步骤
AntTagLibrary(jelly:ant 标签库)org.apache.commons.jelly.tags.ant.AntTagLibraryJelly → Ant在 .jelly 脚本中直接使用 Ant 任务

三者的依赖构件:jelly 只需主 jar 与基础依赖;JellyTask/AntTagLibrary 额外需要 commons-jelly-tags-ant-20040902.071105.jar、commons-grant-1.0-beta-4.jar 及 ant 运行时(ant.jar、ant-launcher.jar、ant-junit.jar、ant-junit4.jar——实测 ant-junit 缺失时抛 NoClassDefFoundError: org/apache/tools/ant/taskdefs/optional/junit/FormatterElement$TypeAttribute)。

三、前置条件

沿用《Apache Jelly 在 Ubuntu 22.04 集成使用指南》组装好的环境(/opt/jelly/lib + /usr/local/bin/jelly)。本文额外安装 Ant 并把 ant 相关 jar 补进 classpath 目录:

apt-get install -y ant
cp /usr/share/ant/lib/ant.jar /usr/share/ant/lib/ant-launcher.jar /opt/jelly/lib/
cp /usr/share/ant/lib/ant-junit.jar /usr/share/ant/lib/ant-junit4.jar /opt/jelly/lib/
curl -fsSL -o /opt/jelly/lib/commons-jelly-tags-ant-20040902.071105.jar \
  https://repo1.maven.org/maven2/commons-jelly/commons-jelly-tags-ant/20040902.071105/commons-jelly-tags-ant-20040902.071105.jar
curl -fsSL -o /opt/jelly/lib/commons-grant-1.0-beta-4.jar \
  https://repo1.maven.org/maven2/commons-grant/commons-grant/1.0-beta-4/commons-grant-1.0-beta-4.jar

验证 Ant 与 jelly 就绪:

ant -version && jelly --help | head -2
Apache Ant(TM) version 1.10.12 compiled on January 17 1970
usage: jelly [scriptFile] [-script scriptFile] [-o outputFile]

四、jelly 命令详解(逐参数实测)

4.1 无参数:短用法提示

cd /root/jelly-demo && jelly
Usage: jelly [scriptFile] [-script scriptFile -o outputFile -Dsysprop=syspropval]

退出码 0。该行提示来自 Jelly.main(源码核实:args.length <= 0 时打印后直接 return)。

4.2 --help / -h:完整用法

jelly --help
usage: jelly [scriptFile] [-script scriptFile] [-o outputFile]
             [-Dsysprop=syspropval] [-awt]
 -swing         Synonym of "-awt".
 -awt           Wether to run in the AWT thread.
 -h,--help      Give this help message
 -o             Output file
 -script        Jelly script to run
 -v,--version   prints Jelly's version and exits

-h 与 --help 等价,实测两者退出码均为 1(源码中打印用法后 System.exit(1))。

4.3 -v / --version:实测崩溃(打包缺陷)

jelly -v
java.lang.NullPointerException
        at java.base/java.io.Reader.<init>(Reader.java:168)
        at java.base/java.io.InputStreamReader.<init>(InputStreamReader.java:97)
        at org.apache.commons.jelly.Jelly.readBuildTimestampResource(Jelly.java:119)
        at org.apache.commons.jelly.Jelly.getJellyVersion(Jelly.java:108)
        at org.apache.commons.jelly.util.CommandLineParser.invokeCommandLineJelly(CommandLineParser.java:82)
        at org.apache.commons.jelly.Jelly.main(Jelly.java:93)
Exception in thread "main" java.lang.IllegalStateException: Resource "jelly-version.txt" not found.

原因(源码 + jar 解包双重核实):Jelly.getJellyVersion() 读取 jar 内资源 jelly-version.txt,而 Central 打包的 1.0.1 jar 只有 org/apache/commons/jelly/jelly.properties、没有该资源文件。这是发布缺陷,任何正常安装都无法用 -v 查版本;版本以 Maven Central 元数据为准(1.0.1)。退出码 1。

4.4 位置参数运行脚本

cd /root/jelly-demo && jelly hello.jelly
Hello, Jelly!
  Hello, Ubuntu 22.04!

退出码 0。注意引擎只认 第一个 位置参数作为脚本(源码:scriptFile = args[0]),多给的文件不会被执行。

4.5 -script:显式指定脚本

jelly -script hello.jelly
Hello, Jelly!
  Hello, Ubuntu 22.04!

与位置参数等价。当 -script 存在时优先于 args[0]。

4.6 -o:输出重定向到文件

cd /root/jelly-demo && jelly hello.jelly -o out.txt
echo EXIT=$?
cat out.txt
EXIT=0
Hello, Jelly!
  Hello, Ubuntu 22.04!

脚本正常输出不再打印到终端,而是写入 out.txt(底层是 XMLOutput.createXMLOutput(new FileWriter(...)))。

⚠️ 注意:-o 放在脚本名 前面 会踩 args[0] 陷阱——实测 jelly -o out.txt hello.jelly 直接报错:

org.apache.commons.jelly.JellyException: null:-1:-1: <null> Script file -o not found

原因:位置参数从原始 args 数组取下标 0,-o 被当成了脚本名。保险写法:脚本名放最前,或一律用 -script 指定脚本。

4.7 -D:注入系统属性(脚本内以变量读取)

cat > /root/jelly-demo/sysprop.jelly <<'EOF'
<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">
  env from -D: ${env} / host: ${hostname}
</j:jelly>
EOF
cd /root/jelly-demo && jelly sysprop.jelly -Denv=production -Dhostname=web01
env from -D: production / host: web01

-Dk=v 由解析器直接写入 JVM 系统属性,引擎运行时把系统属性并入脚本上下文,因此脚本里 ${env}、${hostname} 能直接取到。

位置同样有讲究(实测 jelly -Denv=production sysprop.jelly):

org.apache.commons.jelly.JellyException: null:-1:-1: <null> Script file -Denv=production not found

同样是 args[0] 陷阱——-D 选项要放在脚本名之后。

非法 -D 的真实行为(缺 =,实测):

jelly sysprop.jelly -Dbroken
Invalid system property: "broken".
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: begin 0, end -1, length 6
        at java.base/java.lang.String.substring(String.java:2709)
        at org.apache.commons.jelly.util.CommandLineParser.parseCommandLineOptions(CommandLineParser.java:187)

先打印 Invalid system property,随后解析器自身缺陷(对 indexOf('=')==-1 未短路)导致 StringIndexOutOfBoundsException,退出码 1。

4.8 -awt / -swing:在 AWT(Swing) 线程中执行

两个参数互为同义词(源码:cmdLine.hasOption("awt") || cmdLine.hasOption("swing")),脚本体在 Swing 事件线程中执行(GUI 类标签库需要)。无显示环境的服务器上执行普通脚本实测正常:

cd /root/jelly-demo && jelly showprops.jelly -awt
who = from-jelly.properties / mode = demo

同样受 args[0] 陷阱约束(jelly -awt showprops.jelly 实测报 Script file -awt not found)。

4.9 脚本内可用的内置变量

解析器会把两组运行时信息注入脚本上下文(源码核实 + 实测):

cat > /root/jelly-demo/vars.jelly <<'EOF'
<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">
  args[0] = ${args[0]}
  args[1] = ${args[1]}
  commandLine.hasOption("script") = ${commandLine.hasOption("script")}
</j:jelly>
EOF
cd /root/jelly-demo && jelly vars.jelly A1 A2
args[0] = vars.jelly
  args[1] = A1
  commandLine.hasOption("script") = false
  • args:原始命令行数组(下标 0 是脚本名本身,业务参数从 1 起);
  • commandLine:Commons CLI 的 CommandLine 对象,可在脚本里调用其方法判断选项。

4.10 脚本解析顺序与 jelly.properties 自动注入

jelly 按以下顺序定位脚本(源码核实):classpath 资源 → 文件路径 → URL。变量来源优先级上,jelly.properties(用户主目录 → 当前目录 → classpath)会在编译脚本前注入根上下文,实测:当前目录放 jelly.properties(内容 who=from-jelly.properties、mode=demo)后运行 ${who}/${mode} 输出:

who = from-jelly.properties / mode = demo

4.11 退出码行为(自动化运维必读)

Jelly.main 捕获 JellyException 后只打印堆栈并正常返回(源码核实),因此脚本缺失、解析失败这类错误退出码仍是 0,实测:

cd /root/jelly-demo && jelly no-such.jelly; echo EXIT=$?
org.apache.commons.jelly.JellyException: null:-1:-1: <null> Script file no-such.jelly not found
EXIT=0
场景退出码(实测)
正常运行0
脚本不存在 / 表达式解析失败0(错误只体现在 stderr)
-h/--help1
-v(崩溃)1
非法 -D(解析器抛未捕获异常)1

⚠️ 注意:用 shell 判断 Jelly 是否失败不能只看退出码,须同时检查 stderr(如 jelly a.jelly 2> err.log || true 后再校验 err.log)。

五、Ant Task 方式调用 Jelly(Ant → Jelly)

5.1 JellyTask 的属性

org.apache.commons.jelly.task.JellyTask 继承 org.apache.tools.ant.Task,实测 javap 摘录其属性 setter:

public void setScript(java.lang.String) throws java.net.MalformedURLException;
public void setUrl(java.net.URL);
public void setFile(java.io.File) throws java.net.MalformedURLException;
public void setOutput(java.io.File) throws java.io.IOException;

即 build.xml 中可用属性:script、url、file、output(四选一给脚本位置,output 可选)。

5.2 build.xml 与被调用的 Jelly 脚本

Ant 侧 /root/jelly-demo/build.xml:

<?xml version="1.0"?>
<project name="jelly-demo" default="run" basedir=".">
  <target name="run">
    <taskdef name="jelly" classname="org.apache.commons.jelly.task.JellyTask">
      <classpath>
        <fileset dir="/opt/jelly/lib" includes="*.jar"/>
      </classpath>
    </taskdef>
    <jelly file="/root/jelly-demo/ant-hook.jelly"/>
  </target>
</project>

被调用的 /root/jelly-demo/ant-hook.jelly(演示 Ant Project 自动注入为 project 变量):

<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">
  hello from jelly running inside Ant
  project.name = ${project.name} default = ${project.defaultTarget}
</j:jelly>

5.3 实测运行

cd /root/jelly-demo && ant -f build.xml
Buildfile: /root/jelly-demo/build.xml

run:
    [jelly] Running script: file:/root/jelly-demo/ant-hook.jelly
    [jelly] hello from jelly running inside Ant
    [jelly]   project.name = jelly-demo default = run

BUILD SUCCESSFUL
Total time: 0 seconds

要点:Jelly 的每行输出被 Ant 日志加上 [jelly] 前缀;JellyTask 把 Ant Project 对象注入上下文变量 project,脚本里 ${project.name}、${project.defaultTarget} 实测取到 jelly-demo、run。

六、Jelly 脚本内调用 Ant 任务(Jelly → Ant)

6.1 脚本

/root/jelly-demo/jelly-ant.jelly:

<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core" xmlns:ant="jelly:ant">

  <ant:mkdir dir="/root/jelly-demo/ant-made"/>

  <ant:echo message="echo from ant task inside jelly"/>

  <j:forEach var="i" begin="1" end="3">
    <ant:echo message="loop ${i}: task via ant"/>
  </j:forEach>

</j:jelly>

6.2 实测运行

cd /root/jelly-demo && jelly jelly-ant.jelly
ls -d /root/jelly-demo/ant-made
    [mkdir] Created dir: /root/jelly-demo/ant-made
     [echo] echo from ant task inside jelly
     [echo] loop 1: task via ant
     [echo] loop 2: task via ant
     [echo] loop 3: task via ant
/root/jelly-demo/ant-made

Ant 任务的日志格式([mkdir]/[echo])与 Ant 原生一致,目录真实创建。依赖排障实录(均在实测中复现,按三、前置补齐即可):缺 commons-jelly-tags-ant 时引擎打 SEVERE: Could not find the class: org.apache.commons.jelly.tags.ant.AntTagLibrary 并抛 ClassNotFoundException;缺 ant jar 时报 NoClassDefFoundError: org/apache/tools/ant/BuildLogger;缺 ant-junit 时报 Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/tools/ant/taskdefs/optional/junit/FormatterElement$TypeAttribute。

七、注意事项

  • args[0] 陷阱贯穿所有选项:-o、-D、-awt、-swing 一律写在脚本名之后,或用 -script 显式指定脚本(四、6 实测复现)。
  • 一个命令只跑一个脚本:多个位置参数时只有 args[0] 生效;要跑多个脚本就调用多次或在一个脚本里 include。
  • -v 不可用:Central 1.0.1 jar 缺版本资源,jelly -v 必崩(四、3),版本核对走 Maven Central 元数据。
  • 退出码不可靠:脚本级失败退出码为 0,自动化须检查 stderr(四、11)。
  • Ant 集成的 classpath 要含 ant-junit:AntTagLibrary 静态初始化引用 FormatterElement,缺了直接类加载失败(六、2)。

八、常见问题

Q1:jelly hello.jelly 提示 Script file not found,但文件确实在?

检查三点(均实测定位):① 是否把 -D/-o 等选项放在了脚本名前(args[0] 被选项抢占);② 相对路径是否对应当前工作目录(引擎按 user.dir 解析相对文件);③ 脚本名是否拼错——报错信息会原样带出被当作脚本的字符串,如 Script file -o not found。

Q2:如何在脚本里取到命令行参数?

用内置变量 args(数组,args[0] 是脚本名)与 commandLine(Commons CLI 对象,可调用 hasOption() 等方法),实测见四、9。

Q3:能否把 -D 传进来的属性在脚本里改写?

可以。-D 写的是 JVM 系统属性;脚本里用 <j:set var="env" value="staging"/> 覆盖的是上下文变量,两者独立。若要改系统属性本身,用 j:invokeStatic 调 System.setProperty(core 标签支持,见《Apache Jelly 在 Ubuntu 22.04 集成使用指南》六、2)。

Q4:JellyTask 执行失败时 Ant 构建会失败吗?

会。JellyTask.execute() 把脚本异常包装成 org.apache.tools.ant.BuildException 抛出(javap 字节码可见 new BuildException),Ant 将以 BUILD FAILED 终止并回显脚本错误位置。

九、参考链接