diff --git a/changelog.html b/changelog.html
index b56e0f7cfee5caba6cfc07836854027c013908da..8ab44b45cb2cda64989c9c4486711907d9a4e6ec 100644
--- a/changelog.html
+++ b/changelog.html
@@ -68,6 +68,8 @@ Upcoming changes
Display class loading statistics in /computer/name/systemInfo
.
Added list-plugins CLI command.
+
+ Added console CLI command that dumps console output from a build.
diff --git a/core/src/main/java/hudson/cli/ConsoleCommand.java b/core/src/main/java/hudson/cli/ConsoleCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6edd0be80f6f4ffa931658bfdfaae7ea76e0a6e
--- /dev/null
+++ b/core/src/main/java/hudson/cli/ConsoleCommand.java
@@ -0,0 +1,147 @@
+package hudson.cli;
+
+import hudson.Extension;
+import hudson.console.AnnotatedLargeText;
+import hudson.model.AbstractBuild;
+import hudson.model.AbstractProject;
+import hudson.model.Item;
+import hudson.model.PermalinkProjectAction.Permalink;
+import hudson.util.IOUtils;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+
+/**
+ * cat/tail/head of the console output.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@Extension
+public class ConsoleCommand extends CLICommand {
+ @Override
+ public String getShortDescription() {
+ return "Retrieves console output of a build";
+ }
+
+ @Argument(metaVar="JOB",usage="Name of the job",required=true)
+ public AbstractProject,?> job;
+
+ @Argument(metaVar="BUILD",usage="Build number or permalink to point to the build. Defaults to the last build",required=false,index=1)
+ public String build="lastBuild";
+
+ @Option(name="-f",usage="If the build is in progress, stay around and append console output as it comes, like 'tail -f'")
+ public boolean follow = false;
+
+ @Option(name="-n",metaVar="N",usage="Display the last N lines")
+ public int n = -1;
+
+ protected int run() throws Exception {
+ job.checkPermission(Item.BUILD);
+
+ AbstractBuild,?> run;
+
+ try {
+ int n = Integer.parseInt(build);
+ run = job.getBuildByNumber(n);
+ if (run==null)
+ throw new CmdLineException("No such build #"+n);
+ } catch (NumberFormatException e) {
+ // maybe a permalink?
+ Permalink p = job.getPermalinks().get(build);
+ if (p!=null) {
+ run = (AbstractBuild)p.resolve(job);
+ if (run==null)
+ throw new CmdLineException("Permalink "+build+" produced no build");
+ } else {
+ Permalink nearest = job.getPermalinks().findNearest(build);
+ throw new CmdLineException(String.format("Not sure what you meant by \"%s\". Did you mean \"%s\"?", build, nearest.getId()));
+ }
+ }
+
+ OutputStreamWriter w = new OutputStreamWriter(stdout, getClientCharset());
+ try {
+ long pos = n>=0 ? seek(run) : 0;
+
+ if (follow) {
+ AnnotatedLargeText logText;
+ do {
+ logText = run.getLogText();
+ pos = logText.writeLogTo(pos, w);
+ } while (!logText.isComplete());
+ } else {
+ InputStream in = run.getLogInputStream();
+ IOUtils.skip(in,pos);
+ IOUtils.copy(new InputStreamReader(in,run.getCharset()),w);
+ }
+ } finally {
+ w.flush(); // this pointless flush needed to work around SSHD-154
+ w.close();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Find the byte offset in the log input stream that marks "last N lines".
+ */
+ private long seek(AbstractBuild, ?> run) throws IOException {
+ class RingBuffer {
+ long[] lastNlines = new long[n];
+ int ptr=0;
+
+ RingBuffer() {
+ for (int i=0; i=0) {
+ for (int i=0; i