提交 9ee96846 编写于 作者: K Kohsuke Kawaguchi

Merge pull request #973 from olivergondza/cli-help

[JENKINS-20023] Make CLI interface help more accesible
/*
* The MIT License
*
* Copyright (c) 2013 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.cli;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import hudson.Extension;
import hudson.model.FullDuplexHttpChannel;
import hudson.model.RootAction;
import hudson.remoting.Channel;
/**
* @author ogondza
*/
@Extension
@Restricted(NoExternalUse.class)
public class CLIAction implements RootAction {
private transient final Map<UUID,FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();
public String getIconFileName() {
return null;
}
public String getDisplayName() {
return "Jenkins CLI";
}
public String getUrlName() {
return "/cli";
}
public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
final Jenkins jenkins = Jenkins.getInstance();
jenkins.checkPermission(Jenkins.READ);
// Strip trailing slash
final String commandName = req.getRestOfPath().substring(1);
CLICommand command = CLICommand.clone(commandName);
if (command == null) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "No such command " + commandName);
return;
}
req.setAttribute("command", command);
req.getView(this, "command.jelly").forward(req, rsp);
}
/**
* Handles HTTP requests for duplex channels for CLI.
*/
public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
final Jenkins jenkins = Jenkins.getInstance();
if (!"POST".equals(req.getMethod())) {
// for GET request, serve _cli.jelly, assuming this is a browser
jenkins.checkPermission(Jenkins.READ);
req.setAttribute("command", CLICommand.clone("help"));
req.getView(this,"index.jelly").forward(req,rsp);
return;
}
// do not require any permission to establish a CLI connection
// the actual authentication for the connecting Channel is done by CLICommand
UUID uuid = UUID.fromString(req.getHeader("Session"));
rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know
FullDuplexHttpChannel server;
if(req.getHeader("Side").equals("download")) {
duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !jenkins.hasPermission(Jenkins.ADMINISTER)) {
@Override
protected void main(Channel channel) throws IOException, InterruptedException {
// capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
}
});
try {
server.download(req,rsp);
} finally {
duplexChannels.remove(uuid);
}
} else {
duplexChannels.get(uuid).upload(req,rsp);
}
}
}
......@@ -46,12 +46,15 @@ import org.apache.commons.discovery.resource.classes.DiscoverClasses;
import org.apache.commons.discovery.resource.names.DiscoverServiceNames;
import org.jvnet.hudson.annotation_indexer.Index;
import org.jvnet.tiger_types.Types;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.args4j.ClassParser;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.spi.OptionHandler;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
......@@ -208,7 +211,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
this.stderr = stderr;
this.locale = locale;
registerOptionHandlers();
CmdLineParser p = new CmdLineParser(this);
CmdLineParser p = getCmdLineParser();
// add options from the authenticator
SecurityContext sc = SecurityContextHolder.getContext();
......@@ -242,6 +245,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
sc.setAuthentication(old); // restore
}
}
/**
* Get parser for this command.
*
* Exposed to be overridden by {@link CLIRegisterer}.
* @since TODO
*/
protected CmdLineParser getCmdLineParser() {
return new CmdLineParser(this);
}
public Channel checkChannel() throws AbortException {
if (channel==null)
......@@ -329,11 +342,46 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
protected abstract int run() throws Exception;
protected void printUsage(PrintStream stderr, CmdLineParser p) {
stderr.println("java -jar jenkins-cli.jar "+getName()+" args...");
stderr.print("java -jar jenkins-cli.jar " + getName());
p.printSingleLineUsage(stderr);
stderr.println();
printUsageSummary(stderr);
p.printUsage(stderr);
}
/**
* Get single line summary as a string.
*/
@Restricted(NoExternalUse.class)
public final String getSingleLineSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getCmdLineParser().printSingleLineUsage(out);
return out.toString();
}
/**
* Get usage as a string.
*/
@Restricted(NoExternalUse.class)
public final String getUsage() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getCmdLineParser().printUsage(out);
return out.toString();
}
/**
* Get long description as a string.
*/
@Restricted(NoExternalUse.class)
public final String getLongDescription() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(out);
printUsageSummary(ps);
ps.close();
return out.toString();
}
/**
* Called while producing usage. This is a good method to override
* to render the general description of the command that goes beyond
......
......@@ -66,7 +66,7 @@ public class GroovyCommand extends CLICommand {
/**
* Remaining arguments.
*/
@Argument(index=1)
@Argument(metaVar="ARGUMENTS", index=1, usage="Command line arguments to pass into script.")
public List<String> remaining = new ArrayList<String>();
protected int run() throws Exception {
......
......@@ -29,6 +29,8 @@ import jenkins.model.Jenkins;
import java.util.Map;
import java.util.TreeMap;
import org.kohsuke.args4j.Argument;
/**
* Show the list of all commands.
*
......@@ -36,18 +38,32 @@ import java.util.TreeMap;
*/
@Extension
public class HelpCommand extends CLICommand {
@Argument(metaVar="COMMAND", usage="Name of the command")
public String command;
@Override
public String getShortDescription() {
return Messages.HelpCommand_ShortDescription();
}
@Override
protected int run() {
if (!Jenkins.getInstance().hasPermission(Jenkins.READ)) {
stderr.println("You must authenticate to access this Jenkins.\n"
+ "Use --username/--password/--password-file parameters or login command.");
return 0;
return -1;
}
if (command != null)
return showCommandDetails();
showAllCommands();
return 0;
}
private int showAllCommands() {
Map<String,CLICommand> commands = new TreeMap<String,CLICommand>();
for (CLICommand c : CLICommand.all())
commands.put(c.getName(),c);
......@@ -56,6 +72,19 @@ public class HelpCommand extends CLICommand {
stderr.println(" "+c.getName());
stderr.println(" "+c.getShortDescription());
}
return 0;
}
private int showCommandDetails() {
CLICommand command = CLICommand.clone(this.command);
if (command == null) {
stderr.format("No such command %s. Awailable commands are: ", this.command);
showAllCommands();
return -1;
}
command.printUsage(stderr, command.getCmdLineParser());
return 0;
}
......
......@@ -18,10 +18,10 @@ import java.util.Collections;
*/
@Extension
public class SetBuildParameterCommand extends CommandDuringBuild {
@Argument(index=0, required=true, usage="Name of the build variable")
@Argument(index=0, metaVar="NAME", required=true, usage="Name of the build variable")
public String name;
@Argument(index=1,required=true, usage="Value of the build variable")
@Argument(index=1, metaVar="VALUE", required=true, usage="Value of the build variable")
public String value;
@Override
......
......@@ -110,48 +110,65 @@ public class CLIRegisterer extends ExtensionFinder {
return name;
}
@Override
public String getShortDescription() {
// format by using the right locale
return res.format("CLI."+name+".shortDescription");
}
@Override
protected CmdLineParser getCmdLineParser() {
return bindMethod(new ArrayList<MethodBinder>());
}
private CmdLineParser bindMethod(List<MethodBinder> binders) {
registerOptionHandlers();
CmdLineParser parser = new CmdLineParser(null);
// build up the call sequence
Stack<Method> chains = new Stack<Method>();
Method method = m;
while (true) {
chains.push(method);
if (Modifier.isStatic(method.getModifiers()))
break; // the chain is complete.
// the method in question is an instance method, so we need to resolve the instance by using another resolver
Class<?> type = method.getDeclaringClass();
try {
method = findResolver(type);
} catch (IOException ex) {
throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type, ex);
}
if (method==null) {
throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type);
}
}
while (!chains.isEmpty())
binders.add(new MethodBinder(chains.pop(),this,parser));
new ClassParser().parse(Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this), parser);
return parser;
}
@Override
public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) {
this.stdout = stdout;
this.stderr = stderr;
this.locale = locale;
registerOptionHandlers();
CmdLineParser parser = new CmdLineParser(null);
List<MethodBinder> binders = new ArrayList<MethodBinder>();
CmdLineParser parser = bindMethod(binders);
try {
SecurityContext sc = SecurityContextHolder.getContext();
Authentication old = sc.getAuthentication();
try {
// build up the call sequence
Stack<Method> chains = new Stack<Method>();
Method method = m;
while (true) {
chains.push(method);
if (Modifier.isStatic(method.getModifiers()))
break; // the chain is complete.
// the method in question is an instance method, so we need to resolve the instance by using another resolver
Class<?> type = method.getDeclaringClass();
method = findResolver(type);
if (method==null) {
stderr.println("Unable to find the resolver method annotated with @CLIResolver for "+type);
return 1;
}
}
List<MethodBinder> binders = new ArrayList<MethodBinder>();
while (!chains.isEmpty())
binders.add(new MethodBinder(chains.pop(),this,parser));
// authentication
CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this);
new ClassParser().parse(authenticator,parser);
// fill up all the binders
parser.parseArgument(args);
......
......@@ -3183,44 +3183,6 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
args.add(new byte[1024*1024]);
}
private transient final Map<UUID,FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();
/**
* Handles HTTP requests for duplex channels for CLI.
*/
public void doCli(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
if (!"POST".equals(req.getMethod())) {
// for GET request, serve _cli.jelly, assuming this is a browser
checkPermission(READ);
req.getView(this,"_cli.jelly").forward(req,rsp);
return;
}
// do not require any permission to establish a CLI connection
// the actual authentication for the connecting Channel is done by CLICommand
UUID uuid = UUID.fromString(req.getHeader("Session"));
rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know
FullDuplexHttpChannel server;
if(req.getHeader("Side").equals("download")) {
duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !hasPermission(ADMINISTER)) {
protected void main(Channel channel) throws IOException, InterruptedException {
// capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION,getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel));
}
});
try {
server.download(req,rsp);
} finally {
duplexChannels.remove(uuid);
}
} else {
duplexChannels.get(uuid).upload(req,rsp);
}
}
/**
* Binds /userContent/... to $JENKINS_HOME/userContent.
*/
......
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout norefresh="true">
<st:include page="sidepanel.jelly" it="${app}"/>
<l:main-panel>
<h1>
<img src="${imagesURL}/48x48/terminal.png" alt="" height="48" width="48"/>
Command ${command.name}
</h1>
<j:set var="commandArgs" value="${command.name}${command.singleLineSummary}"/>
<st:include page="example.jelly"/>
<p>${command.longDescription}</p>
<j:set var="usage" value="${command.usage}"/>
<j:if test="${!empty usage}">
<pre>${command.usage}</pre>
</j:if>
</l:main-panel>
</l:layout>
</j:jelly>
<!--
The MIT License
Copyright (c) 2013 Red Hat, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<st:once>
<style type="text/css">
pre {
color: white;
background-color: black;
padding: 1em;
font-weight: bold;
}
</style>
</st:once>
<pre id="example">java -jar <a href="${baseUrl}/jnlpJars/jenkins-cli.jar" style="color: white">jenkins-cli.jar</a> -s ${h.inferHudsonURL(request)} ${commandArgs}</pre>
</j:jelly>
......@@ -25,7 +25,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout norefresh="true">
<st:include page="sidepanel.jelly" />
<st:include page="sidepanel.jelly" it="${app}"/>
<l:main-panel>
<h1>
<img src="${imagesURL}/48x48/terminal.png" alt="" height="48" width="48"/>
......@@ -34,15 +34,18 @@ THE SOFTWARE.
<p>
${%blurb(rootURL)}
</p>
<pre style="color: white; background-color:black; padding:1em; font-weight: bold">java -jar <a
style="color: white"
href="jnlpJars/jenkins-cli.jar">jenkins-cli.jar</a> -s ${h.inferHudsonURL(request)} help</pre>
<h2>${%Available Commands}</h2>
<ul>
<j:forEach items="${h.getCLICommands()}" var="command">
<li><b>${command.getName()}</b>: ${command.getShortDescription()}</li>
</j:forEach>
</ul>
<j:set var="commandArgs" value="help"/>
<st:include page="example.jelly"/>
<h2>${%Available Commands}</h2>
<table>
<j:forEach items="${h.getCLICommands()}" var="command">
<tr>
<td><a href="command/${command.name}" style="color: black; font-weight: bold">${command.name}</a></td>
<td>${command.shortDescription}</td>
</tr>
</j:forEach>
</table>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -21,7 +21,7 @@ GroovyCommand.ShortDescription=\
GroovyshCommand.ShortDescription=\
Runs an interactive groovy shell.
HelpCommand.ShortDescription=\
Lists all the available commands.
Lists all the available commands or a detailed description of single command.
InstallPluginCommand.ShortDescription=\
Installs a plugin either from a file, an URL, or from update center.
InstallToolCommand.ShortDescription=\
......
/*
* The MIT License
*
* Copyright 2013 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.cli;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.text.IsEmptyString.isEmptyString;
import static org.hamcrest.text.StringContainsInOrder.stringContainsInOrder;
import java.io.PrintStream;
import java.util.Arrays;
import hudson.cli.CLICommandInvoker.Result;
import hudson.model.AbstractProject;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
public class HelpCommandTest {
@Rule public JenkinsRule j = new JenkinsRule();
@Test public void getHelpRunningCommand() {
CLICommandInvoker command = new CLICommandInvoker(j, new HelpCommand());
String generalHelp = command.invoke().stderr();
assertContainsOverviewOfClassCommand(generalHelp);
assertContainsOverviewOfMethodCommand(generalHelp);
Result result = command.invokeWithArgs(ClassCommand.NAME);
assertThat(result.returnCode(), equalTo(0));
assertThat(result.stdout(), isEmptyString());
assertContainsUsageOfClassCommand(result.stderr());
result = command.invokeWithArgs("offline-node");
assertThat(result.returnCode(), equalTo(0));
assertThat(result.stdout(), isEmptyString());
assertContainsUsageOfMethodCommand(result.stderr());
}
@Test public void getHelpUsingJenkinsUI() throws Exception {
WebClient wc = j.createWebClient();
String generalHelp = wc.goTo("cli").asText();
assertContainsOverviewOfClassCommand(generalHelp);
assertContainsOverviewOfMethodCommand(generalHelp);
assertContainsUsageOfClassCommand(wc.goTo("cli/command/class-command").asText());
assertContainsUsageOfMethodCommand(wc.goTo("cli/command/offline-node").asText());
}
private void assertContainsOverviewOfClassCommand(String text) {
assertThat(text, containsString(ClassCommand.NAME));
assertThat(text, containsString(ClassCommand.SHORT_DESCRIPTION));
}
private void assertContainsUsageOfClassCommand(String text) {
assertThat(text, containsString("class-command [JOB] [-b BUILD]"));
assertThat(text, containsString(ClassCommand.LONG_DESCRIPTION));
assertThat(text, containsStrings("JOB", "Job arg"));
assertThat(text, containsStrings("BUILD", "Build opt"));
}
private void assertContainsOverviewOfMethodCommand(String text) {
assertThat(text, containsString("offline-node"));
}
private void assertContainsUsageOfMethodCommand(String text) {
assertThat(text, containsString("offline-node NAME [-m VAL]"));
assertThat(text, containsStrings("NAME", "Slave name, or empty string for master"));
assertThat(text, containsStrings("-m VAL", "Record the note about why you are disconnecting this node"));
}
private static Matcher<String> containsStrings(String... strings) {
return stringContainsInOrder(Arrays.asList(strings));
}
@TestExtension
public static class ClassCommand extends CLICommand {
private static final String SHORT_DESCRIPTION = "Short description of class-command";
private static final String LONG_DESCRIPTION = "Long description of class-command";
private static final String NAME = "class-command";
@Argument(usage="Job arg")
public AbstractProject<?, ?> job;
@Option(name="-b", metaVar="BUILD", usage="Build opt")
public String build;
@Override
public String getName() {
return NAME;
}
@Override
public String getShortDescription() {
return SHORT_DESCRIPTION;
}
@Override
protected void printUsageSummary(PrintStream stderr) {
stderr.println(LONG_DESCRIPTION);
}
@Override
protected int run() throws Exception {
throw new UnsupportedOperationException();
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册