Github #525: Simple Command Line Interface
diff --git a/jacoco/assembly.xml b/jacoco/assembly.xml
index b25483f..81a3938 100644
--- a/jacoco/assembly.xml
+++ b/jacoco/assembly.xml
@@ -65,6 +65,14 @@
     </dependencySet>
     <dependencySet>
       <outputDirectory>lib</outputDirectory>
+      <outputFileNameMapping>jacococli.jar</outputFileNameMapping>
+      <useProjectArtifact>false</useProjectArtifact>
+      <includes>
+        <include>${project.groupId}:org.jacoco.cli:jar:nodeps</include>
+      </includes>
+    </dependencySet>
+    <dependencySet>
+      <outputDirectory>lib</outputDirectory>
       <outputFileNameMapping>jacocoagent.jar</outputFileNameMapping>
       <useProjectArtifact>false</useProjectArtifact>
       <includes>
diff --git a/jacoco/pom.xml b/jacoco/pom.xml
index 5a1875d..0e0787a 100644
--- a/jacoco/pom.xml
+++ b/jacoco/pom.xml
@@ -55,6 +55,12 @@
     </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.cli</artifactId>
+      <version>${project.version}</version>
+      <classifier>nodeps</classifier>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
       <artifactId>org.jacoco.examples</artifactId>
       <version>${project.version}</version>
       <type>zip</type>
@@ -104,8 +110,8 @@
             <configuration>
               <rules>
                 <requireFilesSize>
-                  <maxsize>3500000</maxsize>
-                  <minsize>2500000</minsize>
+                  <maxsize>3600000</maxsize>
+                  <minsize>3000000</minsize>
                   <files>
                     <file>${project.build.directory}/jacoco-${qualified.bundle.version}.zip</file>
                   </files>
diff --git a/org.jacoco.build/pom.xml b/org.jacoco.build/pom.xml
index e746d6a..655b8db 100644
--- a/org.jacoco.build/pom.xml
+++ b/org.jacoco.build/pom.xml
@@ -94,12 +94,12 @@
     <module>../org.jacoco.agent.rt</module>
     <module>../org.jacoco.agent</module>
     <module>../org.jacoco.ant</module>
-
+    <module>../org.jacoco.cli</module>
+    <module>../org.jacoco.examples</module>
     <module>../jacoco-maven-plugin</module>
 
     <module>../org.jacoco.tests</module>
 
-    <module>../org.jacoco.examples</module>
     <module>../org.jacoco.doc</module>
     <module>../jacoco</module>
   </modules>
@@ -143,6 +143,7 @@
     <!-- Dependencies versions -->
     <asm.version>5.2</asm.version>
     <ant.version>1.7.1</ant.version>
+    <args4j.version>2.0.28</args4j.version>
     <junit.version>4.8.2</junit.version>
 
     <!-- ================== -->
@@ -198,6 +199,11 @@
       </dependency>
       <dependency>
         <groupId>${project.groupId}</groupId>
+        <artifactId>org.jacoco.cli</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>${project.groupId}</groupId>
         <artifactId>org.jacoco.examples</artifactId>
         <version>${project.version}</version>
       </dependency>
@@ -223,6 +229,11 @@
         <version>1.2</version>
       </dependency>
       <dependency>
+        <groupId>args4j</groupId>
+        <artifactId>args4j</artifactId>
+        <version>${args4j.version}</version>
+      </dependency>
+      <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>${junit.version}</version>
@@ -394,6 +405,11 @@
           <artifactId>xml-maven-plugin</artifactId>
           <version>1.0</version>
         </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>exec-maven-plugin</artifactId>
+          <version>1.6.0</version>
+        </plugin>
         <!-- Third-party plugins -->
         <plugin>
           <groupId>org.codehaus.groovy.maven</groupId>
diff --git a/org.jacoco.cli.test/.classpath b/org.jacoco.cli.test/.classpath
new file mode 100644
index 0000000..0ed344a
--- /dev/null
+++ b/org.jacoco.cli.test/.classpath
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry including="**/*.java" kind="src" output="target/classes" path="src">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/org.jacoco.cli.test/.project b/org.jacoco.cli.test/.project
new file mode 100644
index 0000000..fbd366b
--- /dev/null
+++ b/org.jacoco.cli.test/.project
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>org.jacoco.cli.test</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+	<linkedResources>
+		<link>
+			<name>.settings</name>
+			<type>2</type>
+			<locationURI>PARENT-1-PROJECT_LOC/org.jacoco.core.test/.settings</locationURI>
+		</link>
+	</linkedResources>
+</projectDescription>
diff --git a/org.jacoco.cli.test/about.html b/org.jacoco.cli.test/about.html
new file mode 100644
index 0000000..d31112d
--- /dev/null
+++ b/org.jacoco.cli.test/about.html
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<title>About</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>
+  @build.date@
+</p>
+
+<h3>License</h3>
+
+<p>
+  All Content in this plug-in is made available by Mountainminds GmbH &amp; Co.
+  KG, Munich. Unless otherwise indicated below, the Content is provided to you
+  under the terms and conditions of the Eclipse Public License Version 1.0
+  (&quot;EPL&quot;). A copy of the EPL is available at
+  <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+  For purposes of the EPL, "Program" will mean the Content.
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/org.jacoco.cli.test/pom.xml b/org.jacoco.cli.test/pom.xml
new file mode 100644
index 0000000..b0a5b61
--- /dev/null
+++ b/org.jacoco.cli.test/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+   All rights reserved. This program and the accompanying materials
+   are made available under the terms of the Eclipse Public License v1.0
+   which accompanies this distribution, and is available at
+   http://www.eclipse.org/legal/epl-v10.html
+
+   Contributors:
+      Evgeny Mandrikov - initial API and implementation
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jacoco</groupId>
+    <artifactId>org.jacoco.tests</artifactId>
+    <version>0.7.10-SNAPSHOT</version>
+    <relativePath>../org.jacoco.tests</relativePath>
+  </parent>
+
+  <artifactId>org.jacoco.cli.test</artifactId>
+
+  <name>JaCoCo :: Test :: Command Line Interface</name>
+
+  <properties>
+    <jacoco.includes>org.jacoco.cli.*</jacoco.includes>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.cli</artifactId>
+      <!-- 
+      <version>${project.version}</version> 
+      <classifier>nodeps</classifier>
+      <exclusions>
+        <exclusion>
+          <groupId>*</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+      </exclusions>
+       -->
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/CommandTestBase.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/CommandTestBase.java
new file mode 100644
index 0000000..4d12a37
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/CommandTestBase.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import org.junit.Before;
+
+/**
+ * Base class for command tests.
+ */
+public abstract class CommandTestBase {
+
+	protected StringWriter out;
+	protected StringWriter err;
+	protected int result;
+
+	@Before
+	public void before() {
+		out = new StringWriter();
+		err = new StringWriter();
+	}
+
+	protected int execute(String... args) throws Exception {
+		result = new Main(args).execute(new PrintWriter(out),
+				new PrintWriter(err));
+		return result;
+	}
+
+	protected void assertOk() {
+		assertEquals(err.toString(), 0, result);
+	}
+
+	protected void assertFailure() {
+		assertEquals(-1, result);
+	}
+
+	protected void assertNoOutput(StringWriter buffer) {
+		assertEquals("", buffer.toString());
+	}
+
+	protected void assertContains(String expected, StringWriter buffer) {
+		final String content = buffer.toString();
+		assertTrue(content, content.contains(expected));
+	}
+
+	protected String getClassPath() {
+		final String name = getClass().getName();
+		final String res = "/" + name.replace('.', '/') + ".class";
+		String loc = getClass().getResource(res).getFile();
+		try {
+			loc = URLDecoder.decode(loc, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+		}
+		return loc.substring(0, loc.length() - res.length());
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/MainTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/MainTest.java
new file mode 100644
index 0000000..7a79d1e
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/MainTest.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link Main}.
+ */
+public class MainTest extends CommandTestBase {
+
+	@Test
+	public void shouldPrintUsage_whenNoArgumentsGiven() throws Exception {
+		execute();
+
+		assertFailure();
+		assertNoOutput(out);
+		assertContains("Argument \"<command>\" is required", err);
+		assertContains("Usage: java -jar jacococli.jar -help | <command>", err);
+		assertContains("Command line interface for JaCoCo.", err);
+	}
+
+	@Test
+	public void shouldPrintErrorMessage_whenInvalidCommandIsGiven()
+			throws Exception {
+		execute("foo");
+
+		assertFailure();
+		assertNoOutput(out);
+		assertContains("\"foo\" is not a valid value for \"<command>\"", err);
+		assertContains("Usage: java -jar jacococli.jar -help | <command>", err);
+	}
+
+	@Test
+	public void shouldPrintGeneralHelp_whenHelpOptionIsGiven()
+			throws Exception {
+		execute("-help");
+
+		assertOk();
+		assertNoOutput(err);
+		assertContains("Usage: java -jar jacococli.jar -help | <command>", out);
+		assertContains("<command> : dump|instrument|merge|report", out);
+	}
+
+	@Test
+	public void shouldPrintCommandHelp_whenHelpOptionIsGiven()
+			throws Exception {
+		execute("dump", "-help");
+
+		assertOk();
+		assertNoOutput(err);
+		assertContains("Usage: java -jar jacococli.jar dump", out);
+		assertContains(
+				"Request execution data from a JaCoCo agent running in 'tcpserver' output mode.",
+				out);
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ClassInfoTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ClassInfoTest.java
new file mode 100644
index 0000000..d812d0c
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ClassInfoTest.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link ExecInfo}.
+ */
+public class ClassInfoTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenInvalidArgumentIsGiven() throws Exception {
+		execute("classinfo", "-invalid");
+
+		assertFailure();
+		assertContains("\"-invalid\" is not a valid option", err);
+		assertContains(
+				"java -jar jacococli.jar classinfo [<classlocations> ...]",
+				err);
+	}
+
+	@Test
+	public void shouldProvideClassInfoInfo() throws Exception {
+		execute("classinfo", getClassPath());
+
+		assertOk();
+		assertContains(
+				"class name:   org/jacoco/cli/internal/commands/ClassInfoTest",
+				out);
+		assertContains("methods:      3", out);
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/DumpTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/DumpTest.java
new file mode 100644
index 0000000..6f7616e
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/DumpTest.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.jacoco.core.runtime.IRemoteCommandVisitor;
+import org.jacoco.core.runtime.RemoteControlReader;
+import org.jacoco.core.runtime.RemoteControlWriter;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link Dump}.
+ */
+public class DumpTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenNoArgumentsGiven() throws Exception {
+		execute("dump");
+		assertFailure();
+		assertContains("Option \"-destfile\" is required", err);
+		assertContains("java -jar jacococli.jar dump [-address <address>]",
+				err);
+	}
+
+	@Test
+	public void shouldWriteDump() throws Exception {
+
+		File execfile = new File(tmp.getRoot(), "jacoco.exec");
+		int port = startMockServer();
+
+		execute("dump", "-destfile", execfile.getAbsolutePath(), "-port",
+				String.valueOf(port));
+
+		assertOk();
+		assertContains("[INFO] Connecting to ", out);
+		assertContains("[INFO] Writing execution data to "
+				+ execfile.getAbsolutePath(), out);
+		assertTrue(execfile.exists());
+	}
+
+	@Test
+	public void shouldLogConnectionError() throws Exception {
+
+		File execfile = new File(tmp.getRoot(), "jacoco.exec");
+		int port = unusedPort();
+
+		try {
+			execute("dump", "-destfile", execfile.getAbsolutePath(), "-port",
+					String.valueOf(port), "-retry", "1");
+			fail("IOException expected");
+		} catch (IOException ignore) {
+		}
+
+		assertContains("[WARN] Connection refused", err);
+	}
+
+	private int startMockServer() throws IOException {
+		final ServerSocket serverSocket = new ServerSocket(0, 0,
+				InetAddress.getByName(null));
+		new Thread() {
+			@Override
+			public void run() {
+				try {
+					serveRequest(serverSocket.accept());
+				} catch (IOException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		}.start();
+		return serverSocket.getLocalPort();
+	}
+
+	private void serveRequest(Socket socket) throws IOException {
+		final RemoteControlWriter writer = new RemoteControlWriter(
+				socket.getOutputStream());
+		final RemoteControlReader reader = new RemoteControlReader(
+				socket.getInputStream());
+		reader.setRemoteCommandVisitor(new IRemoteCommandVisitor() {
+
+			public void visitDumpCommand(boolean dump, boolean reset)
+					throws IOException {
+				writer.sendCmdOk();
+			}
+		});
+		while (reader.read()) {
+		}
+	}
+
+	private int unusedPort() throws IOException {
+		final ServerSocket serverSocket = new ServerSocket(0, 0,
+				InetAddress.getByName(null));
+		final int port = serverSocket.getLocalPort();
+		serverSocket.close();
+		return port;
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ExecInfoTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ExecInfoTest.java
new file mode 100644
index 0000000..15a1b99
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ExecInfoTest.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.ExecutionDataWriter;
+import org.jacoco.core.data.SessionInfo;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link ExecInfo}.
+ */
+public class ExecInfoTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenInvalidArgumentIsGiven() throws Exception {
+		execute("execinfo", "-invalid");
+
+		assertFailure();
+		assertContains("\"-invalid\" is not a valid option", err);
+		assertContains("java -jar jacococli.jar execinfo [<execfiles> ...]",
+				err);
+	}
+
+	@Test
+	public void shouldProvideExecutionDataInfo() throws Exception {
+		File execfile = createExecFile();
+
+		execute("execinfo", execfile.getAbsolutePath());
+
+		assertOk();
+		assertContains("[INFO] Loading exec file " + execfile.getAbsolutePath(),
+				out);
+		assertContains("CLASS ID         HITS/PROBES   CLASS NAME", out);
+		assertContains("Session \"testid\":", out);
+		assertContains("0000000000001234    2 of   3   foo/MyClass", out);
+	}
+
+	private File createExecFile() throws IOException {
+		File f = new File(tmp.getRoot(), "test.exec");
+		final FileOutputStream out = new FileOutputStream(f);
+		final ExecutionDataWriter writer = new ExecutionDataWriter(out);
+		writer.visitSessionInfo(new SessionInfo("testid", 1, 2));
+		writer.visitClassExecution(new ExecutionData(0x1234, "foo/MyClass",
+				new boolean[] { false, true, true }));
+		out.close();
+		return f;
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/InstrumentTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/InstrumentTest.java
new file mode 100644
index 0000000..7ea8b5c
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/InstrumentTest.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Unit tests for {@link Instrument}.
+ */
+public class InstrumentTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenNoArgumentsGiven() throws Exception {
+		execute("instrument");
+		assertFailure();
+		assertContains("Option \"-destdir\" is required", err);
+		assertContains(
+				"Usage: java -jar jacococli.jar instrument [<sourcefiles> ...]",
+				err);
+	}
+
+	@Test
+	public void shouldInstrumentClassFiles() throws Exception {
+		File destdir = tmp.getRoot();
+
+		execute("instrument", "-destdir", destdir.getAbsolutePath(),
+				getClassPath());
+
+		assertOk();
+		assertContains("[INFO] 11 classes instrumented to "
+				+ destdir.getAbsolutePath(), out);
+
+		// non class-file resources are copied:
+		assertTrue(new File(destdir, "about.html").isFile());
+
+		assertInstrumented(new File(destdir,
+				"org/jacoco/cli/internal/commands/InstrumentTest.class"));
+	}
+
+	@Test
+	public void shouldNotInstrumentAnything_whenNoSourceIsGiven()
+			throws Exception {
+		File destdir = tmp.getRoot();
+
+		execute("instrument", "-destdir", destdir.getAbsolutePath());
+
+		assertOk();
+		assertArrayEquals(new String[0], destdir.list());
+	}
+
+	@Test
+	public void shouldNotCreateTargetFile_whenSourceClassFileIsBroken()
+			throws Exception {
+		File srcdir = new File(tmp.getRoot(), "src");
+		srcdir.mkdir();
+		File destdir = new File(tmp.getRoot(), "sdest");
+		destdir.mkdir();
+
+		OutputStream out = new FileOutputStream(
+				new File(srcdir, "Broken.class"));
+		out.write((byte) 0xca);
+		out.write((byte) 0xfe);
+		out.write((byte) 0xba);
+		out.write((byte) 0xbe);
+		out.write((byte) 0x00);
+		out.write((byte) 0x00);
+		out.write((byte) 0x00);
+		out.write((byte) 50);
+		out.close();
+
+		try {
+			execute("instrument", "-destdir", destdir.getAbsolutePath(),
+					srcdir.getAbsolutePath());
+			fail("exception expected");
+		} catch (IOException expected) {
+		}
+
+		assertFalse(new File(destdir, "Broken.class").exists());
+	}
+
+	private void assertInstrumented(File classfile) throws IOException {
+		InputStream in = new FileInputStream(classfile);
+		ClassReader reader = new ClassReader(in);
+		in.close();
+		final Set<String> fields = new HashSet<String>();
+		reader.accept(new ClassVisitor(Opcodes.ASM5) {
+			@Override
+			public FieldVisitor visitField(int access, String name, String desc,
+					String signature, Object value) {
+				fields.add(name);
+				return null;
+			}
+		}, 0);
+		assertTrue(fields.contains("$jacocoData"));
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/MergeTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/MergeTest.java
new file mode 100644
index 0000000..b6d3edc
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/MergeTest.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.ExecutionDataWriter;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link Merge}.
+ */
+public class MergeTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenNoArgumentsGiven() throws Exception {
+		execute("merge");
+
+		assertFailure();
+		assertContains("Option \"-destfile\" is required", err);
+		assertContains("java -jar jacococli.jar merge [<execfiles> ...]", err);
+	}
+
+	@Test
+	public void shouldMergeExecFiles() throws Exception {
+		File a = createExecFile("a");
+		File b = createExecFile("b");
+		File c = createExecFile("c");
+		File dest = new File(tmp.getRoot(), "merged.exec");
+
+		execute("merge", "-destfile", dest.getAbsolutePath(),
+				a.getAbsolutePath(), b.getAbsolutePath(), c.getAbsolutePath());
+
+		assertOk();
+		Set<String> names = loadExecFile(dest);
+		assertEquals(new HashSet<String>(Arrays.asList("a", "b", "c")), names);
+	}
+
+	private File createExecFile(String name) throws IOException {
+		File file = new File(tmp.getRoot(), name + ".exec");
+		final FileOutputStream execout = new FileOutputStream(file);
+		ExecutionDataWriter writer = new ExecutionDataWriter(execout);
+		writer.visitClassExecution(new ExecutionData(name.hashCode(), name,
+				new boolean[] { true }));
+		execout.close();
+		return file;
+	}
+
+	private Set<String> loadExecFile(File file) throws IOException {
+		ExecFileLoader loader = new ExecFileLoader();
+		loader.load(file);
+		Set<String> names = new HashSet<String>();
+		for (ExecutionData d : loader.getExecutionDataStore().getContents()) {
+			names.add(d.getName());
+		}
+		return names;
+	}
+
+}
diff --git a/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ReportTest.java b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ReportTest.java
new file mode 100644
index 0000000..1c56088
--- /dev/null
+++ b/org.jacoco.cli.test/src/org/jacoco/cli/internal/commands/ReportTest.java
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import org.jacoco.cli.internal.CommandTestBase;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.ExecutionDataWriter;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link Report}.
+ */
+public class ReportTest extends CommandTestBase {
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	@Test
+	public void shouldPrintUsage_whenNoArgumentsGiven() throws Exception {
+		execute("report");
+
+		assertFailure();
+		assertContains("Option \"-classfiles\" is required", err);
+		assertContains(
+				"Usage: java -jar jacococli.jar report [<execfiles> ...]", err);
+	}
+
+	@Test
+	public void shouldPrintNumberOfAnalyzedClasses() throws Exception {
+		execute("report", "-classfiles", getClassPath());
+
+		assertOk();
+		assertContains("[INFO] Writing report with 11 classes.", out);
+	}
+
+	@Test
+	public void shouldPrintWarning_whenExecDataDoesNotMatch() throws Exception {
+		File exec = new File(tmp.getRoot(), "jacoco.exec");
+		final FileOutputStream execout = new FileOutputStream(exec);
+		ExecutionDataWriter writer = new ExecutionDataWriter(execout);
+		// Add probably invalid id for this test class:
+		writer.visitClassExecution(
+				new ExecutionData(0x123, getClass().getName().replace('.', '/'),
+						new boolean[] { true }));
+		execout.close();
+
+		execute("report", exec.getAbsolutePath(), "-classfiles",
+				getClassPath());
+
+		assertOk();
+		assertContains("[WARN] Some classes do not match with execution data.",
+				out);
+		assertContains(
+				"[WARN] For report generation the same class files must be used as at runtime.",
+				out);
+		assertContains(
+				"[WARN] Execution data for class org/jacoco/cli/internal/commands/ReportTest does not match.",
+				out);
+	}
+
+	@Test
+	public void shouldCreateXmlReport_whenXmlOptionIsProvided()
+			throws Exception {
+		File xml = new File(tmp.getRoot(), "coverage.xml");
+
+		execute("report", "-classfiles", getClassPath(), "-xml",
+				xml.getAbsolutePath());
+
+		assertOk();
+		assertTrue(xml.isFile());
+	}
+
+	@Test
+	public void shouldCreateCsvReport_whenXmlOptionIsProvided()
+			throws Exception {
+		File csv = new File(tmp.getRoot(), "coverage.csv");
+
+		execute("report", "-classfiles", getClassPath(), "-csv",
+				csv.getAbsolutePath());
+
+		assertOk();
+		assertTrue(csv.isFile());
+	}
+
+	@Test
+	public void shouldCreateHtmlReport_whenHtmlOptionIsProvided()
+			throws Exception {
+		File html = new File(tmp.getRoot(), "coverage");
+
+		execute("report", "-classfiles", getClassPath(), "-sourcefiles",
+				"./src", "-html", html.getAbsolutePath());
+
+		assertOk();
+		assertTrue(html.isDirectory());
+		assertTrue(new File(html,
+				"org.jacoco.cli.internal.commands/ReportTest.html").isFile());
+		assertTrue(new File(html,
+				"org.jacoco.cli.internal.commands/ReportTest.java.html")
+						.isFile());
+	}
+
+}
diff --git a/org.jacoco.cli/.classpath b/org.jacoco.cli/.classpath
new file mode 100644
index 0000000..0ed344a
--- /dev/null
+++ b/org.jacoco.cli/.classpath
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry including="**/*.java" kind="src" output="target/classes" path="src">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/org.jacoco.cli/.gitignore b/org.jacoco.cli/.gitignore
new file mode 100644
index 0000000..4dc0091
--- /dev/null
+++ b/org.jacoco.cli/.gitignore
@@ -0,0 +1,2 @@
+/target
+/bin
diff --git a/org.jacoco.cli/.project b/org.jacoco.cli/.project
new file mode 100644
index 0000000..828621a
--- /dev/null
+++ b/org.jacoco.cli/.project
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<projectDescription>

+	<name>org.jacoco.cli</name>

+	<comment></comment>

+	<projects>

+	</projects>

+	<buildSpec>

+		<buildCommand>

+			<name>org.eclipse.jdt.core.javabuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+		<buildCommand>

+			<name>org.eclipse.m2e.core.maven2Builder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+	</buildSpec>

+	<natures>

+		<nature>org.eclipse.m2e.core.maven2Nature</nature>

+		<nature>org.eclipse.jdt.core.javanature</nature>

+	</natures>

+	<linkedResources>

+		<link>

+			<name>.settings</name>

+			<type>2</type>

+			<locationURI>PARENT-1-PROJECT_LOC/org.jacoco.core/.settings</locationURI>

+		</link>

+	</linkedResources>

+</projectDescription>

diff --git a/org.jacoco.cli/about.html b/org.jacoco.cli/about.html
new file mode 100644
index 0000000..d31112d
--- /dev/null
+++ b/org.jacoco.cli/about.html
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<title>About</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>
+  @build.date@
+</p>
+
+<h3>License</h3>
+
+<p>
+  All Content in this plug-in is made available by Mountainminds GmbH &amp; Co.
+  KG, Munich. Unless otherwise indicated below, the Content is provided to you
+  under the terms and conditions of the Eclipse Public License Version 1.0
+  (&quot;EPL&quot;). A copy of the EPL is available at
+  <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+  For purposes of the EPL, "Program" will mean the Content.
+</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/org.jacoco.cli/pom.xml b/org.jacoco.cli/pom.xml
new file mode 100644
index 0000000..553472e
--- /dev/null
+++ b/org.jacoco.cli/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+   All rights reserved. This program and the accompanying materials
+   are made available under the terms of the Eclipse Public License v1.0
+   which accompanies this distribution, and is available at
+   http://www.eclipse.org/legal/epl-v10.html
+
+   Contributors:
+      Marc R. Hoffmann - initial API and implementation
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.jacoco</groupId>
+    <artifactId>org.jacoco.build</artifactId>
+    <version>0.7.10-SNAPSHOT</version>
+    <relativePath>../org.jacoco.build</relativePath>
+  </parent>
+
+  <artifactId>org.jacoco.cli</artifactId>
+
+  <name>JaCoCo :: Command Line Interface</name>
+  <description>JaCoCo Command Line Interface</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.report</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>args4j</groupId>
+      <artifactId>args4j</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <sourceDirectory>src</sourceDirectory>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>java</goal>
+            </goals>
+            <configuration>
+              <mainClass>org.jacoco.cli.internal.XmlDocumentation</mainClass>
+              <arguments>
+                <argument>${project.build.directory}/generated-documentation/cli.xml</argument>
+              </arguments>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <shadedArtifactAttached>true</shadedArtifactAttached>
+              <shadedClassifierName>nodeps</shadedClassifierName>
+              <minimizeJar>true</minimizeJar>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <manifestEntries>
+                    <Main-Class>org.jacoco.cli.internal.Main</Main-Class>
+                  </manifestEntries>
+                </transformer>
+              </transformers>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>    
+  </build>
+</project>
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/Command.java b/org.jacoco.cli/src/org/jacoco/cli/internal/Command.java
new file mode 100644
index 0000000..5e83b8b
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/Command.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.kohsuke.args4j.Option;
+
+/**
+ * Common interface for all commands.
+ */
+public abstract class Command {
+
+	/**
+	 * Common command line prefix.
+	 */
+	public static final String JAVACMD = "java -jar jacococli.jar ";
+
+	/**
+	 * Flag whether help should be printed for this command.
+	 */
+	@Option(name = "-help", usage = "show help", help = true)
+	public boolean help = false;
+
+	/**
+	 * @return Short description of the command.
+	 */
+	public abstract String description();
+
+	/**
+	 * @return name of the command
+	 */
+	public String name() {
+		return getClass().getSimpleName().toLowerCase();
+	}
+
+	/**
+	 * @param parser
+	 *            parser for this command
+	 * @return usage string displayed for help
+	 */
+	public String usage(final CommandParser parser) {
+		final StringWriter writer = new StringWriter();
+		parser.printSingleLineUsage(writer, null);
+		return JAVACMD + name() + writer;
+	}
+
+	/**
+	 * Executes the given command.
+	 * 
+	 * @param out
+	 *            std out
+	 * @param err
+	 *            std err
+	 * @return exit code, should be 0 for normal operation
+	 * @throws Exception
+	 *             any exception that my occur during execution
+	 */
+	public abstract int execute(PrintWriter out, PrintWriter err)
+			throws Exception;
+
+	/**
+	 * Prints textual help for this command.
+	 * 
+	 * @param writer
+	 *            output destination
+	 */
+	protected void printHelp(final PrintWriter writer) {
+		final CommandParser parser = new CommandParser(this);
+		writer.println("Usage: " + parser.getCommand().usage(parser));
+		writer.println(description());
+		parser.printUsage(writer, null);
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/CommandHandler.java b/org.jacoco.cli/src/org/jacoco/cli/internal/CommandHandler.java
new file mode 100644
index 0000000..3c6a2a5
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/CommandHandler.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import java.util.AbstractList;
+
+import org.jacoco.cli.internal.commands.AllCommands;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.Messages;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+/**
+ * {@link OptionHandler} which uses {@link CommandParser} internally to provide
+ * help context also for sub-commands.
+ */
+public class CommandHandler extends OptionHandler<Command> {
+
+	/**
+	 * This constructor is required by the args4j framework.
+	 * 
+	 * @param parser
+	 * @param option
+	 * @param setter
+	 */
+	public CommandHandler(final CmdLineParser parser, final OptionDef option,
+			final Setter<Object> setter) {
+		super(parser,
+				new OptionDef(AllCommands.names(), "<command>",
+						option.required(), option.help(), option.hidden(),
+						CommandHandler.class, option.isMultiValued()) {
+				}, setter);
+	}
+
+	@Override
+	public int parseArguments(final Parameters params) throws CmdLineException {
+		final String subCmd = params.getParameter(0);
+
+		for (final Command c : AllCommands.get()) {
+			if (c.name().equals(subCmd)) {
+				parseSubArguments(c, params);
+				setter.addValue(c);
+				return params.size(); // consume all the remaining tokens
+			}
+		}
+
+		throw new CmdLineException(owner,
+				Messages.ILLEGAL_OPERAND.format(option.toString(), subCmd));
+	}
+
+	private void parseSubArguments(final Command c, final Parameters params)
+			throws CmdLineException {
+		final CmdLineParser p = new CommandParser(c);
+		p.parseArgument(new AbstractList<String>() {
+			@Override
+			public String get(final int index) {
+				try {
+					return params.getParameter(index + 1);
+				} catch (final CmdLineException e) {
+					// invalid index was accessed.
+					throw new IndexOutOfBoundsException();
+				}
+			}
+
+			@Override
+			public int size() {
+				return params.size() - 1;
+			}
+		});
+	}
+
+	@Override
+	public String getDefaultMetaVariable() {
+		return "<command>";
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/CommandParser.java b/org.jacoco.cli/src/org/jacoco/cli/internal/CommandParser.java
new file mode 100644
index 0000000..0e31093
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/CommandParser.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import org.kohsuke.args4j.CmdLineParser;
+
+/**
+ * Parser which remembers the parsed command to have additional context
+ * information to produce help output.
+ */
+public class CommandParser extends CmdLineParser {
+
+	private final Command command;
+
+	CommandParser(final Command command) {
+		super(command);
+		this.command = command;
+	}
+
+	Command getCommand() {
+		return command;
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/Main.java b/org.jacoco.cli/src/org/jacoco/cli/internal/Main.java
new file mode 100644
index 0000000..63a464d
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/Main.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import java.io.PrintWriter;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+
+/**
+ * Entry point for all command line operations.
+ */
+public class Main extends Command {
+
+	private final String[] args;
+
+	Main(final String... args) {
+		this.args = args;
+	}
+
+	@Argument(handler = CommandHandler.class, required = true)
+	Command command;
+
+	@Override
+	public String description() {
+		return "Command line interface for JaCoCo.";
+	}
+
+	@Override
+	public String usage(final CommandParser parser) {
+		return JAVACMD + "-help | <command>";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws Exception {
+
+		final CommandParser mainParser = new CommandParser(this);
+		try {
+			mainParser.parseArgument(args);
+		} catch (final CmdLineException e) {
+			err.println(e.getMessage());
+			err.println();
+			((CommandParser) e.getParser()).getCommand().printHelp(err);
+			return -1;
+		}
+
+		if (help) {
+			printHelp(out);
+			return 0;
+		} else if (command.help) {
+			command.printHelp(out);
+			return 0;
+		} else {
+			return command.execute(out, err);
+		}
+	}
+
+	/**
+	 * Main entry point for program invocations.
+	 * 
+	 * @param args
+	 *            program arguments
+	 * @throws Exception
+	 *             All internal exceptions are directly passed on to get printed
+	 *             on the console
+	 */
+	public static void main(final String... args) throws Exception {
+		new Main(args).execute(new PrintWriter(System.out),
+				new PrintWriter(System.err));
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/XmlDocumentation.java b/org.jacoco.cli/src/org/jacoco/cli/internal/XmlDocumentation.java
new file mode 100644
index 0000000..e10e847
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/XmlDocumentation.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.jacoco.cli.internal.commands.AllCommands;
+import org.jacoco.report.internal.xml.XMLDocument;
+import org.jacoco.report.internal.xml.XMLElement;
+import org.kohsuke.args4j.spi.OptionHandler;
+
+/**
+ * Internal utility to dump all command descriptions as XML.
+ */
+public class XmlDocumentation {
+
+	private static void writeCommand(final Command command,
+			final XMLElement parent) throws IOException {
+		final CommandParser parser = new CommandParser(command);
+		final XMLElement element = parent.element("command");
+		element.attr("name", command.name());
+		element.element("usage").text(command.usage(parser));
+		element.element("description").text(command.description());
+		writeOptions(element, parser.getArguments());
+		writeOptions(element, parser.getOptions());
+	}
+
+	private static void writeOptions(final XMLElement parent,
+			@SuppressWarnings("rawtypes") final List<OptionHandler> list)
+			throws IOException {
+		for (final OptionHandler<?> o : list) {
+			final XMLElement optionNode = parent.element("option");
+			optionNode.attr("required", String.valueOf(o.option.required()));
+			optionNode.element("usage").text(o.getNameAndMeta(null));
+			optionNode.element("description").text(o.option.usage());
+		}
+	}
+
+	/**
+	 * Called during the build process.
+	 * 
+	 * @param args
+	 * @throws IOException
+	 */
+	public static void main(final String... args) throws IOException {
+		final File file = new File(args[0]);
+		file.getParentFile().mkdirs();
+
+		final XMLElement root = new XMLDocument("documentation", null, null,
+				"UTF-8", true, new FileOutputStream(file));
+
+		for (final Command c : AllCommands.get()) {
+			writeCommand(c, root);
+		}
+
+		root.close();
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/AllCommands.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/AllCommands.java
new file mode 100644
index 0000000..accc7fb
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/AllCommands.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+
+/**
+ * List of all available commands.
+ */
+public class AllCommands {
+
+	/**
+	 * @return list of new instances of all available commands
+	 */
+	public static List<Command> get() {
+		return Arrays.asList(new Dump(), new Instrument(), new Merge(),
+				new Report(), new ClassInfo(), new ExecInfo());
+	}
+
+	/**
+	 * @return String containing all available command names
+	 */
+	public static String names() {
+		final StringBuilder sb = new StringBuilder();
+		for (final Command c : get()) {
+			if (sb.length() > 0) {
+				sb.append('|');
+			}
+			sb.append(c.name());
+		}
+		return sb.toString();
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ClassInfo.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ClassInfo.java
new file mode 100644
index 0000000..6cf66e1
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ClassInfo.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.analysis.IClassCoverage;
+import org.jacoco.core.analysis.ICoverageVisitor;
+import org.jacoco.core.data.ExecutionDataStore;
+import org.kohsuke.args4j.Argument;
+
+/**
+ * The <code>classinfo</code> command.
+ */
+public class ClassInfo extends Command {
+
+	@Argument(usage = "location of Java class files", metaVar = "<classlocations>")
+	List<File> classfiles = new ArrayList<File>();
+
+	@Override
+	public String description() {
+		return "Print information about Java class files at the provided location.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws IOException {
+		final Analyzer analyzer = new Analyzer(new ExecutionDataStore(),
+				new ICoverageVisitor() {
+					public void visitCoverage(final IClassCoverage coverage) {
+						print(coverage, out);
+					}
+				});
+
+		for (final File file : classfiles) {
+			analyzer.analyzeAll(file);
+		}
+		return 0;
+	}
+
+	private void print(final IClassCoverage coverage, final PrintWriter out) {
+		out.printf("class name:   %s%n", coverage.getName());
+		out.printf("class id:     %016x%n", Long.valueOf(coverage.getId()));
+		out.printf("instructions: %s%n", Integer
+				.valueOf(coverage.getInstructionCounter().getTotalCount()));
+		out.printf("branches:     %s%n",
+				Integer.valueOf(coverage.getBranchCounter().getTotalCount()));
+		out.printf("lines:        %s%n",
+				Integer.valueOf(coverage.getLineCounter().getTotalCount()));
+		out.printf("methods:      %s%n",
+				Integer.valueOf(coverage.getMethodCounter().getTotalCount()));
+		out.printf("complexity:   %s%n%n", Integer
+				.valueOf(coverage.getComplexityCounter().getTotalCount()));
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Dump.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Dump.java
new file mode 100644
index 0000000..d56a2a1
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Dump.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.runtime.AgentOptions;
+import org.jacoco.core.tools.ExecDumpClient;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.kohsuke.args4j.Option;
+
+/**
+ * The <code>dump</code> command.
+ */
+public class Dump extends Command {
+
+	@Option(name = "-address", usage = "host name or ip address to connect to (default localhost)", metaVar = "<address>")
+	String address = AgentOptions.DEFAULT_ADDRESS;
+
+	@Option(name = "-port", usage = "the port to connect to (default 6300)", metaVar = "<port>")
+	int port = AgentOptions.DEFAULT_PORT;
+
+	@Option(name = "-destfile", usage = "file to write execution data to", metaVar = "<path>", required = true)
+	File destfile;
+
+	@Option(name = "-reset", usage = "reset execution data on test target after dump")
+	boolean reset = false;
+
+	@Option(name = "-retry", usage = "number of retries (default 0)", metaVar = "<count>")
+	int retrycount = 0;
+
+	@Override
+	public String description() {
+		return "Request execution data from a JaCoCo agent running in 'tcpserver' output mode.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws Exception {
+		final ExecDumpClient client = new ExecDumpClient() {
+			@Override
+			protected void onConnecting(final InetAddress address,
+					final int port) {
+				out.printf("[INFO] Connecting to %s:%s.%n", address,
+						Integer.valueOf(port));
+			}
+
+			@Override
+			protected void onConnectionFailure(final IOException exception) {
+				err.printf("[WARN] %s.%n", exception.getMessage());
+			}
+		};
+		client.setReset(reset);
+		client.setRetryCount(retrycount);
+
+		final ExecFileLoader loader = client.dump(address, port);
+		out.printf("[INFO] Writing execution data to %s.%n",
+				destfile.getAbsolutePath());
+		loader.save(destfile, true);
+
+		return 0;
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ExecInfo.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ExecInfo.java
new file mode 100644
index 0000000..e798bf6
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/ExecInfo.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.ExecutionDataReader;
+import org.jacoco.core.data.IExecutionDataVisitor;
+import org.jacoco.core.data.ISessionInfoVisitor;
+import org.jacoco.core.data.SessionInfo;
+import org.kohsuke.args4j.Argument;
+
+/**
+ * The <code>execinfo</code> command.
+ */
+public class ExecInfo extends Command {
+
+	@Argument(usage = "list of JaCoCo *.exec files to read", metaVar = "<execfiles>")
+	List<File> execfiles = new ArrayList<File>();
+
+	@Override
+	public String description() {
+		return "Print exec file content in human readable format.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws IOException {
+		for (final File file : execfiles) {
+			dump(file, out);
+		}
+		return 0;
+	}
+
+	private void dump(final File file, final PrintWriter out)
+			throws IOException {
+		out.printf("[INFO] Loading exec file %s.%n", file);
+		out.println("CLASS ID         HITS/PROBES   CLASS NAME");
+
+		final FileInputStream in = new FileInputStream(file);
+		final ExecutionDataReader reader = new ExecutionDataReader(in);
+		reader.setSessionInfoVisitor(new ISessionInfoVisitor() {
+			public void visitSessionInfo(final SessionInfo info) {
+				out.printf("Session \"%s\": %s - %s%n", info.getId(),
+						new Date(info.getStartTimeStamp()),
+						new Date(info.getDumpTimeStamp()));
+			}
+		});
+		reader.setExecutionDataVisitor(new IExecutionDataVisitor() {
+			public void visitClassExecution(final ExecutionData data) {
+				out.printf("%016x  %3d of %3d   %s%n",
+						Long.valueOf(data.getId()),
+						Integer.valueOf(getHitCount(data.getProbes())),
+						Integer.valueOf(data.getProbes().length),
+						data.getName());
+			}
+		});
+		reader.read();
+		in.close();
+		out.println();
+	}
+
+	private int getHitCount(final boolean[] data) {
+		int count = 0;
+		for (final boolean hit : data) {
+			if (hit) {
+				count++;
+			}
+		}
+		return count;
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Instrument.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Instrument.java
new file mode 100644
index 0000000..d098057
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Instrument.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    John Keeping - initial implementation
+ *    Marc R. Hoffmann - rework
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.instr.Instrumenter;
+import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+/**
+ * The <code>instrument</code> command.
+ */
+public class Instrument extends Command {
+
+	@Option(name = "-destdir", usage = "directory to write instrumented Java classes to", metaVar = "<dir>", required = true)
+	File destdir;
+
+	@Argument(usage = "list of folder or files to instrument recusively", metaVar = "<sourcefiles>")
+	List<File> source = new ArrayList<File>();
+
+	private Instrumenter instrumenter;
+
+	@Override
+	public String description() {
+		return "Off-line instrumentation of Java class files and JAR files.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws IOException {
+		instrumenter = new Instrumenter(
+				new OfflineInstrumentationAccessGenerator());
+		int total = 0;
+		for (final File s : source) {
+			total += instrumentRecursive(s, destdir);
+		}
+		out.printf("[INFO] %s classes instrumented to %s.%n",
+				Integer.valueOf(total), destdir.getAbsolutePath());
+		return 0;
+	}
+
+	private int instrumentRecursive(final File src, final File dest)
+			throws IOException {
+		int total = 0;
+		if (src.isDirectory()) {
+			for (final File child : src.listFiles()) {
+				total += instrumentRecursive(child,
+						new File(dest, child.getName()));
+			}
+		} else {
+			total += instrument(src, dest);
+		}
+		return total;
+	}
+
+	private int instrument(final File src, final File dest) throws IOException {
+		dest.getParentFile().mkdirs();
+		final InputStream input = new FileInputStream(src);
+		try {
+			final OutputStream output = new FileOutputStream(dest);
+			try {
+				return instrumenter.instrumentAll(input, output,
+						src.getAbsolutePath());
+			} finally {
+				output.close();
+			}
+		} catch (final IOException e) {
+			dest.delete();
+			throw e;
+		} finally {
+			input.close();
+		}
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Merge.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Merge.java
new file mode 100644
index 0000000..530285c
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Merge.java
@@ -0,0 +1,62 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+/**
+ * The <code>merge</code> command.
+ */
+public class Merge extends Command {
+
+	@Argument(usage = "list of JaCoCo *.exec files to read", metaVar = "<execfiles>")
+	List<File> execfiles = new ArrayList<File>();
+
+	@Option(name = "-destfile", usage = "file to write merged execution data to", metaVar = "<path>", required = true)
+	File destfile;
+
+	@Override
+	public String description() {
+		return "Merges multiple exec files into a new one.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws IOException {
+		final ExecFileLoader loader = loadExecutionData(out);
+		out.printf("[INFO] Writing execution data to %s.%n",
+				destfile.getAbsolutePath());
+		loader.save(destfile, true);
+		return 0;
+	}
+
+	private ExecFileLoader loadExecutionData(final PrintWriter out)
+			throws IOException {
+		final ExecFileLoader loader = new ExecFileLoader();
+		for (final File file : execfiles) {
+			out.printf("[INFO] Loading execution data file %s.%n",
+					file.getAbsolutePath());
+			loader.load(file);
+		}
+		return loader;
+	}
+
+}
diff --git a/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Report.java b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Report.java
new file mode 100644
index 0000000..be2fbfe
--- /dev/null
+++ b/org.jacoco.cli/src/org/jacoco/cli/internal/commands/Report.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    John Keeping - initial implementation
+ *    Marc R. Hoffmann - rework
+ *
+ *******************************************************************************/
+package org.jacoco.cli.internal.commands;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.jacoco.cli.internal.Command;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.analysis.CoverageBuilder;
+import org.jacoco.core.analysis.IBundleCoverage;
+import org.jacoco.core.analysis.IClassCoverage;
+import org.jacoco.core.data.ExecutionDataStore;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.jacoco.report.DirectorySourceFileLocator;
+import org.jacoco.report.FileMultiReportOutput;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.ISourceFileLocator;
+import org.jacoco.report.MultiReportVisitor;
+import org.jacoco.report.MultiSourceFileLocator;
+import org.jacoco.report.csv.CSVFormatter;
+import org.jacoco.report.html.HTMLFormatter;
+import org.jacoco.report.xml.XMLFormatter;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+/**
+ * The <code>report</code> command.
+ */
+public class Report extends Command {
+
+	@Argument(usage = "list of JaCoCo *.exec files to read", metaVar = "<execfiles>")
+	List<File> execfiles = new ArrayList<File>();
+
+	@Option(name = "-classfiles", usage = "location of Java class files", metaVar = "<path>", required = true)
+	List<File> classfiles = new ArrayList<File>();
+
+	@Option(name = "-sourcefiles", usage = "location of the source files", metaVar = "<path>")
+	List<File> sourcefiles = new ArrayList<File>();
+
+	@Option(name = "-tabwith", usage = "tab stop width for the source pages (default 4)", metaVar = "<n>")
+	int tabwidth = 4;
+
+	@Option(name = "-name", usage = "name used for this report", metaVar = "<name>")
+	String name = "JaCoCo Coverage Report";
+
+	@Option(name = "-encoding", usage = "source file encoding (default platform encoding)", metaVar = "<charset>")
+	String encoding;
+
+	@Option(name = "-xml", usage = "output file for the XML report", metaVar = "<file>")
+	File xml;
+
+	@Option(name = "-csv", usage = "output file for the CSV report", metaVar = "<file>")
+	File csv;
+
+	@Option(name = "-html", usage = "output directory for the HTML report", metaVar = "<dir>")
+	File html;
+
+	@Override
+	public String description() {
+		return "Generate reports in different formats by reading exec and Java class files.";
+	}
+
+	@Override
+	public int execute(final PrintWriter out, final PrintWriter err)
+			throws IOException {
+		final ExecFileLoader loader = loadExecutionData(out);
+		final IBundleCoverage bundle = analyze(loader.getExecutionDataStore(),
+				out);
+		writeReports(bundle, loader, out);
+		return 0;
+	}
+
+	private ExecFileLoader loadExecutionData(final PrintWriter out)
+			throws IOException {
+		final ExecFileLoader loader = new ExecFileLoader();
+		for (final File file : execfiles) {
+			out.printf("[INFO] Loading execution data file %s.%n",
+					file.getAbsolutePath());
+			loader.load(file);
+		}
+		return loader;
+	}
+
+	private IBundleCoverage analyze(final ExecutionDataStore data,
+			final PrintWriter out) throws IOException {
+		final CoverageBuilder builder = new CoverageBuilder();
+		final Analyzer analyzer = new Analyzer(data, builder);
+		for (final File f : classfiles) {
+			analyzer.analyzeAll(f);
+		}
+		printNoMatchWarning(builder.getNoMatchClasses(), out);
+		return builder.getBundle(name);
+	}
+
+	private void printNoMatchWarning(final Collection<IClassCoverage> nomatch,
+			final PrintWriter out) {
+		if (!nomatch.isEmpty()) {
+			out.println(
+					"[WARN] Some classes do not match with execution data.");
+			out.println(
+					"[WARN] For report generation the same class files must be used as at runtime.");
+			for (final IClassCoverage c : nomatch) {
+				out.printf(
+						"[WARN] Execution data for class %s does not match.%n",
+						c.getName());
+			}
+		}
+	}
+
+	private void writeReports(final IBundleCoverage bundle,
+			final ExecFileLoader loader, final PrintWriter out)
+			throws IOException {
+		out.printf("[INFO] Writing report with %s classes.%n",
+				Integer.valueOf(bundle.getClassCounter().getTotalCount()));
+		final IReportVisitor visitor = createReportVisitor();
+		visitor.visitInfo(loader.getSessionInfoStore().getInfos(),
+				loader.getExecutionDataStore().getContents());
+		visitor.visitBundle(bundle, getSourceLocator());
+		visitor.visitEnd();
+	}
+
+	private IReportVisitor createReportVisitor()
+			throws IOException, IOException {
+		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();
+
+		if (xml != null) {
+			final XMLFormatter formatter = new XMLFormatter();
+			visitors.add(formatter.createVisitor(new FileOutputStream(xml)));
+		}
+
+		if (csv != null) {
+			final CSVFormatter formatter = new CSVFormatter();
+			visitors.add(formatter.createVisitor(new FileOutputStream(csv)));
+		}
+
+		if (html != null) {
+			final HTMLFormatter formatter = new HTMLFormatter();
+			visitors.add(
+					formatter.createVisitor(new FileMultiReportOutput(html)));
+		}
+
+		return new MultiReportVisitor(visitors);
+	}
+
+	private ISourceFileLocator getSourceLocator() {
+		final MultiSourceFileLocator multi = new MultiSourceFileLocator(
+				tabwidth);
+		for (final File f : sourcefiles) {
+			multi.add(new DirectorySourceFileLocator(f, encoding, tabwidth));
+		}
+		return multi;
+	}
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/tools/ExecDumpClientTest.java b/org.jacoco.core.test/src/org/jacoco/core/tools/ExecDumpClientTest.java
index 3fe1688..8091814 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/tools/ExecDumpClientTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/tools/ExecDumpClientTest.java
@@ -101,7 +101,7 @@
 				// 3. Retry
 				"onConnectionFailure", "onConnecting")
 
-		, callbacks);
+				, callbacks);
 	}
 
 	@Test
@@ -126,6 +126,12 @@
 		assertTrue(resetRequested);
 	}
 
+	@Test(expected = IOException.class)
+	public void testNopServer() throws IOException {
+		int port = createNopServer();
+		client.dump((String) null, port);
+	}
+
 	private int getFreePort() throws IOException {
 		final ServerSocket server = new ServerSocket(0, 0,
 				InetAddress.getByName(null));
@@ -159,11 +165,27 @@
 				dumpRequested = dump;
 				resetRequested = reset;
 				if (dump) {
-					writer.visitSessionInfo(new SessionInfo("TestId", 100, 200));
+					writer.visitSessionInfo(
+							new SessionInfo("TestId", 100, 200));
 				}
 				writer.sendCmdOk();
 			}
 		});
 		reader.read();
 	}
+
+	private int createNopServer() throws IOException {
+		server = new ServerSocket(0, 0, InetAddress.getByName(null));
+		new Thread(new Runnable() {
+			public void run() {
+				try {
+					server.accept().close();
+				} catch (IOException e) {
+					// ignore
+				}
+			}
+		}).start();
+		return server.getLocalPort();
+	}
+
 }
diff --git a/org.jacoco.core/src/org/jacoco/core/tools/ExecDumpClient.java b/org.jacoco.core/src/org/jacoco/core/tools/ExecDumpClient.java
index 39fd8f3..e99204d 100644
--- a/org.jacoco.core/src/org/jacoco/core/tools/ExecDumpClient.java
+++ b/org.jacoco.core/src/org/jacoco/core/tools/ExecDumpClient.java
@@ -123,7 +123,10 @@
 					.setExecutionDataVisitor(loader.getExecutionDataStore());
 
 			remoteWriter.visitDumpCommand(dump, reset);
-			remoteReader.read();
+
+			if (!remoteReader.read()) {
+				throw new IOException("Socket closed unexpectedly.");
+			}
 
 		} finally {
 			socket.close();
diff --git a/org.jacoco.doc/docroot/doc/index.html b/org.jacoco.doc/docroot/doc/index.html
index 4de1a58..e17a349 100644
--- a/org.jacoco.doc/docroot/doc/index.html
+++ b/org.jacoco.doc/docroot/doc/index.html
@@ -44,6 +44,7 @@
   <li><a href="examples/build/pom.xml">Maven Usage Example</a> -
       <a href="examples/build/pom-offline.xml">Offline Example</a></li>
   <li><a href="agent.html">Java Agent</a></li>
+  <li><a href="cli.html">Command Line Interface</a></li>
   <li><a href="classids.html">Class Ids</a></li>
   <li><a href="offline.html">Offline Instrumentation</a></li>
   <li><a href="faq.html">FAQ</a></li>
diff --git a/org.jacoco.doc/docroot/index.html b/org.jacoco.doc/docroot/index.html
index 9a7991a..8f35d33 100644
--- a/org.jacoco.doc/docroot/index.html
+++ b/org.jacoco.doc/docroot/index.html
@@ -68,6 +68,12 @@
       <td>Ant <i>(all other dependencies included)</i></td>
     </tr>
     <tr>
+      <td><span class="el_jar">jacococli.jar</span></td>
+      <td>no</td>
+      <td>JaCoCo Ant tasks</td>
+      <td>- <i>(all dependencies included)</i></td>
+    </tr>
+    <tr>
       <td><span class="el_jar">org.jacoco.agent_@qualified.bundle.version@.jar</span></td>
       <td>yes</td>
       <td>JaCoCo agent</td>
diff --git a/org.jacoco.doc/pom.xml b/org.jacoco.doc/pom.xml
index 505eb10..c43b3ec 100644
--- a/org.jacoco.doc/pom.xml
+++ b/org.jacoco.doc/pom.xml
@@ -82,6 +82,16 @@
     </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.cli</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>org.jacoco.cli.test</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
       <artifactId>org.jacoco.examples</artifactId>
       <version>${project.version}</version>
     </dependency>
@@ -152,6 +162,7 @@
                   <fileset dir="../org.jacoco.agent.test/target" includes="surefire-reports/**/*.xml"/>
                   <fileset dir="../org.jacoco.agent.rt.test/target" includes="surefire-reports/**/*.xml"/>
                   <fileset dir="../org.jacoco.ant.test/target" includes="surefire-reports/**/*.xml"/>
+                  <fileset dir="../org.jacoco.cli.test/target" includes="surefire-reports/**/*.xml"/>
                   <fileset dir="../org.jacoco.core.test/target" includes="surefire-reports/**/*.xml"/>
                   <fileset dir="../org.jacoco.report.test/target" includes="surefire-reports/**/*.xml"/>
                   <fileset dir="../org.jacoco.examples.test/target" includes="surefire-reports/**/*.xml"/>
@@ -230,7 +241,7 @@
         <artifactId>xml-maven-plugin</artifactId>
         <executions>
           <execution>
-            <id>default-transform</id>
+            <id>transform-generated-doc</id>
             <goals>
               <goal>transform</goal>
             </goals>
@@ -262,6 +273,32 @@
                     </parameter>
                   </parameters>
                 </transformationSet>
+                <transformationSet>
+                  <dir>../org.jacoco.cli/target/generated-documentation</dir>
+                  <includes>
+                    <include>*.xml</include>
+                  </includes>
+                  <stylesheet>xsl/cli.xsl</stylesheet>
+                  <fileMappers>
+                    <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.FileExtensionMapper">
+                      <targetExtension>.html</targetExtension>
+                    </fileMapper>
+                  </fileMappers>
+                  <parameters>
+                    <parameter>
+                      <name>qualified.bundle.version</name>
+                      <value>${qualified.bundle.version}</value>
+                    </parameter>
+                    <parameter>
+                      <name>jacoco.home.url</name>
+                      <value>${jacoco.home.url}</value>
+                    </parameter>
+                    <parameter>
+                      <name>copyright.years</name>
+                      <value>${copyright.years}</value>
+                    </parameter>
+                  </parameters>
+                </transformationSet>
               </transformationSets>
             </configuration>
           </execution>
diff --git a/org.jacoco.doc/xsl/cli.xsl b/org.jacoco.doc/xsl/cli.xsl
new file mode 100644
index 0000000..71eb563
--- /dev/null
+++ b/org.jacoco.doc/xsl/cli.xsl
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+
+<!-- 
+   Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors
+   All rights reserved. This program and the accompanying materials
+   are made available under the terms of the Eclipse Public License v1.0
+   which accompanies this distribution, and is available at
+   http://www.eclipse.org/legal/epl-v10.html
+  
+   Contributors:
+      Marc R. Hoffmann - initial API and implementation
+-->
+
+<xsl:stylesheet version="1.0"
+	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+	xmlns="http://www.w3.org/1999/xhtml" exclude-result-prefixes="xdoc">
+
+	<xsl:output method="xml" indent="yes" encoding="UTF-8"
+		doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" />
+
+	<xsl:param name="qualified.bundle.version" />
+	<xsl:param name="jacoco.home.url" />
+	<xsl:param name="copyright.years" />
+
+	<xsl:template match="/">
+		<html>
+			<head>
+				<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+				<link rel="stylesheet" href="resources/doc.css" charset="UTF-8"
+					type="text/css" />
+				<link rel="shortcut icon" href="resources/report.gif" type="image/gif" />
+				<title>
+					JaCoCo - Command Line Interface
+				</title>
+			</head>
+			<body>
+				<div class="breadcrumb">
+					<a href="../index.html" class="el_report">JaCoCo</a> &gt;
+					<a href="index.html" class="el_group">Documentation</a> &gt;
+					<span class="el_source">Command Line Interface</span>
+				</div>
+				<div id="content">
+				
+					<h1>Command Line Interface</h1>
+					
+					<p>
+					  JaCoCo comes with a command line interface to perform
+					  basic operations from the command line. The command line
+					  tools with all dependencies are packaged in
+					  <code>jacococli.jar</code> and are available with the
+					  JaCoCo download. A Java VM with version 1.5 or greater is
+					  required for execution.
+					</p>
+					
+					<p>
+					  For more sophisticated usage especially with larger
+					  projects please use our
+					  <a href="integrations.html">integrations</a> with various
+					  build tools. 
+					</p>
+					<xsl:apply-templates select="documentation" />
+				</div>
+				<div class="footer">
+					<span class="right">
+						<a href="{$jacoco.home.url}">JaCoCo</a>
+						&#160;
+						<xsl:value-of select="$qualified.bundle.version" />
+					</span>
+					<a href="../doc/license.html">Copyright</a>
+					&#169;
+					<xsl:value-of select="$copyright.years" />
+					Mountainminds GmbH &amp; Co. KG and Contributors
+				</div>
+			</body>
+		</html>
+	</xsl:template>
+	
+	<xsl:template match="command">
+		<h2><xsl:value-of select="@name" /></h2>
+		<pre class="source">
+			<xsl:value-of select="usage" />
+		</pre>
+		<p><xsl:value-of select="description" /></p>
+		<table class="coverage">
+			<thead>
+				<tr>
+					<td>Option</td>
+					<td>Description</td>
+					<td>Required</td>
+				</tr>
+			</thead>
+			<tbody>
+				<xsl:for-each select="option">
+					<tr>
+						<td><code><xsl:value-of select="usage" /></code></td>
+						<td><xsl:value-of select="description" /></td>
+						<td><xsl:choose>
+							<xsl:when test="@required = 'true'">
+								yes
+							</xsl:when>
+							<xsl:otherwise>
+								no
+							</xsl:otherwise>
+						</xsl:choose></td>
+					</tr>
+				</xsl:for-each>
+			</tbody>
+		</table>
+	</xsl:template>
+	
+</xsl:stylesheet>
+
diff --git a/org.jacoco.tests/pom.xml b/org.jacoco.tests/pom.xml
index d363212..2e676f9 100644
--- a/org.jacoco.tests/pom.xml
+++ b/org.jacoco.tests/pom.xml
@@ -31,8 +31,9 @@
     <module>../org.jacoco.agent.rt.test</module>
     <module>../org.jacoco.agent.test</module>
     <module>../org.jacoco.ant.test</module>
-    <module>../jacoco-maven-plugin.test</module>
+    <module>../org.jacoco.cli.test</module>
     <module>../org.jacoco.examples.test</module>
+    <module>../jacoco-maven-plugin.test</module>
   </modules>
 
   <properties>