Update from Google.

--
MOE_MIGRATED_REVID=85702957
diff --git a/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
new file mode 100644
index 0000000..30562c6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command's execution terminates abnormally -- for example,
+ * if it is killed, or if it terminates with a non-zero exit status.
+ */
+public class AbnormalTerminationException extends CommandException {
+
+  private final CommandResult result;
+
+  public AbnormalTerminationException(final Command command,
+                                      final CommandResult result,
+                                      final String message) {
+    super(command, message);
+    this.result = result;
+  }
+
+  public AbnormalTerminationException(final Command command,
+                                      final CommandResult result,
+                                      final Throwable cause) {
+    super(command, cause);
+    this.result = result;
+  }
+
+  public AbnormalTerminationException(final Command command,
+                                      final CommandResult result,
+                                      final String message,
+                                      final Throwable cause) {
+    super(command, message, cause);
+    this.result = result;
+  }
+
+  public CommandResult getResult() {
+    return result;
+  }
+
+  private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
new file mode 100644
index 0000000..324007a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command's execution terminates with a non-zero exit status.
+ */
+public final class BadExitStatusException extends AbnormalTerminationException {
+
+  public BadExitStatusException(final Command command,
+                                final CommandResult result,
+                                final String message) {
+    super(command, result, message);
+  }
+
+  public BadExitStatusException(final Command command,
+                                final CommandResult result,
+                                final String message,
+                                final Throwable cause) {
+    super(command, result, message, cause);
+  }
+
+  private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Command.java b/src/main/java/com/google/devtools/build/lib/shell/Command.java
new file mode 100644
index 0000000..ab4a7fc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Command.java
@@ -0,0 +1,960 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an executable command, including its arguments and
+ * runtime environment (environment variables, working directory). This class
+ * lets a caller execute a command, get its results, and optionally try to kill
+ * the task during execution.</p>
+ *
+ * <p>The use of "shell" in the full name of this class is a misnomer.  In
+ * terms of the way its arguments are interpreted, this class is closer to
+ * {@code execve(2)} than to {@code system(3)}.  No Bourne shell is executed.
+ *
+ * <p>The most basic use-case for this class is as follows:
+ * <pre>
+ *   String[] args = { "/bin/du", "-s", directory };
+ *   CommandResult result = new Command(args).execute();
+ *   String output = new String(result.getStdout());
+ * </pre>
+ * which writes the output of the {@code du(1)} command into {@code output}.
+ * More complex cases might inspect the stderr stream, kill the subprocess
+ * asynchronously, feed input to its standard input, handle the exceptions
+ * thrown if the command fails, or print the termination status (exit code or
+ * signal name).
+ *
+ * <h4>Invoking the Bourne shell</h4>
+ *
+ * <p>Perhaps the most common command invoked programmatically is the UNIX
+ * shell, {@code /bin/sh}.  Because the shell is a general-purpose programming
+ * language, care must be taken to ensure that variable parts of the shell
+ * command (e.g. strings entered by the user) do not contain shell
+ * metacharacters, as this poses a correctness and/or security risk.
+ *
+ * <p>To execute a shell command directly, use the following pattern:
+ * <pre>
+ *   String[] args = { "/bin/sh", "-c", shellCommand };
+ *   CommandResult result = new Command(args).execute();
+ * </pre>
+ * {@code shellCommand} is a complete Bourne shell program, possibly containing
+ * all kinds of unescaped metacharacters.  For example, here's a shell command
+ * that enumerates the working directories of all processes named "foo":
+ * <pre>ps auxx | grep foo | awk '{print $1}' |
+ *      while read pid; do readlink /proc/$pid/cwd; done</pre>
+ * It is the responsibility of the caller to ensure that this string means what
+ * they intend.
+ *
+ * <p>Consider the risk posed by allowing the "foo" part of the previous
+ * command to be some arbitrary (untrusted) string called {@code processName}:
+ * <pre>
+ *  // WARNING: unsafe!
+ *  String shellCommand = "ps auxx | grep " + processName + " | awk '{print $1}' | "
+ *  + "while read pid; do readlink /proc/$pid/cwd; done";</pre>
+ * </pre>
+ * Passing this string to {@link Command} is unsafe because if the string
+ * {@processName} contains shell metacharacters, the meaning of the command can
+ * be arbitrarily changed;  consider:
+ * <pre>String processName = ". ; rm -fr $HOME & ";</pre>
+ *
+ * <p>To defend against this possibility, it is essential to properly quote the
+ * variable portions of the shell command so that shell metacharacters are
+ * escaped.  Use {@link ShellUtils#shellEscape} for this purpose:
+ * <pre>
+ *  // Safe.
+ *  String shellCommand = "ps auxx | grep " + ShellUtils.shellEscape(processName)
+ *      + " | awk '{print $1}' | while read pid; do readlink /proc/$pid/cwd; done";
+ * </pre>
+ *
+ * <p>Tip: if you are only invoking a single known command, and no shell
+ * features (e.g. $PATH lookup, output redirection, pipelines, etc) are needed,
+ * call it directly without using a shell, as in the {@code du(1)} example
+ * above.
+ *
+ * <h4>Other features</h4>
+ *
+ * <p>A caller can optionally specify bytes to be written to the process's
+ * "stdin". The returned {@link CommandResult} object gives the caller access to
+ * the exit status, as well as output from "stdout" and "stderr". To use
+ * this class with processes that generate very large amounts of input/output,
+ * consider
+ * {@link #execute(InputStream, KillableObserver, OutputStream, OutputStream)}
+ * and
+ * {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}.
+ * </p>
+ *
+ * <p>This class ensures that stdout and stderr streams are read promptly,
+ * avoiding potential deadlock if the output is large. See <a
+ * href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html"> When
+ * <code>Runtime.exec()</code> won't</a>.</p>
+ *
+ * <p>This class is immutable and therefore thread-safe.</p>
+ */
+public final class Command {
+
+  private static final Logger log =
+    Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+  /**
+   * Pass this value to {@link #execute(byte[])} to indicate that no input
+   * should be written to stdin.
+   */
+  public static final byte[] NO_INPUT = new byte[0];
+
+  private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+  /**
+   * Pass this to {@link #execute(byte[], KillableObserver, boolean)} to
+   * indicate that you do not wish to observe / kill the underlying
+   * process.
+   */
+  public static final KillableObserver NO_OBSERVER = new KillableObserver() {
+    @Override
+    public void startObserving(final Killable killable) {
+      // do nothing
+    }
+    @Override
+    public void stopObserving(final Killable killable) {
+      // do nothing
+    }
+  };
+
+  private final ProcessBuilder processBuilder;
+
+  // Start of public API -----------------------------------------------------
+
+  /**
+   * Creates a new {@link Command} that will execute a command line that
+   * is described by a {@link ProcessBuilder}. Command line elements,
+   * environment, and working directory are taken from this object. The
+   * command line is executed exactly as given, without a shell.
+   *
+   * @param processBuilder {@link ProcessBuilder} describing command line
+   *  to execute
+   */
+  public Command(final ProcessBuilder processBuilder) {
+    this(processBuilder.command().toArray(EMPTY_STRING_ARRAY),
+         processBuilder.environment(),
+         processBuilder.directory());
+  }
+
+  /**
+   * Creates a new {@link Command} for the given command line elements. The
+   * command line is executed exactly as given, without a shell.
+   * Subsequent calls to {@link #execute()} will use the JVM's working
+   * directory and environment.
+   *
+   * @param commandLineElements elements of raw command line to execute
+   * @throws IllegalArgumentException if commandLine is null or empty
+   */
+  /* TODO(bazel-team): Use varargs here
+   */
+  public Command(final String[] commandLineElements) {
+    this(commandLineElements, null, null);
+  }
+
+  /**
+   * <p>Creates a new {@link Command} for the given command line elements.
+   * Subsequent calls to {@link #execute()} will use the JVM's working
+   * directory and environment.</p>
+   *
+   * <p>Note: be careful when setting useShell to <code>true</code>; you
+   * may inadvertently expose a security hole. See
+   * {@link #Command(String, Map, File)}.</p>
+   *
+   * @param commandLineElements elements of raw command line to execute
+   * @param useShell if true, command is executed using a shell interpreter
+   *  (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+   *  exactly as given
+   * @throws IllegalArgumentException if commandLine is null or empty
+   */
+  public Command(final String[] commandLineElements, final boolean useShell) {
+    this(commandLineElements, useShell, null, null);
+  }
+
+  /**
+   * Creates a new {@link Command} for the given command line elements. The
+   * command line is executed exactly as given, without a shell. The given
+   * environment variables and working directory are used in subsequent
+   * calls to {@link #execute()}.
+   *
+   * @param commandLineElements elements of raw command line to execute
+   * @param environmentVariables environment variables to replace JVM's
+   *  environment variables; may be null
+   * @param workingDirectory working directory for execution; if null, current
+   * working directory is used
+   * @throws IllegalArgumentException if commandLine is null or empty
+   */
+  public Command(final String[] commandLineElements,
+                 final Map<String, String> environmentVariables,
+                 final File workingDirectory) {
+    this(commandLineElements, false, environmentVariables, workingDirectory);
+  }
+
+  /**
+   * <p>Creates a new {@link Command} for the given command line elements. The
+   * given environment variables and working directory are used in subsequent
+   * calls to {@link #execute()}.</p>
+   *
+   * <p>Note: be careful when setting useShell to <code>true</code>; you
+   * may inadvertently expose a security hole. See
+   * {@link #Command(String, Map, File)}.</p>
+   *
+   * @param commandLineElements elements of raw command line to execute
+   * @param useShell if true, command is executed using a shell interpreter
+   *  (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+   *  exactly as given
+   * @param environmentVariables environment variables to replace JVM's
+   *  environment variables; may be null
+   * @param workingDirectory working directory for execution; if null, current
+   * working directory is used
+   * @throws IllegalArgumentException if commandLine is null or empty
+   */
+  public Command(final String[] commandLineElements,
+                 final boolean useShell,
+                 final Map<String, String> environmentVariables,
+                 final File workingDirectory) {
+    if (commandLineElements == null || commandLineElements.length == 0) {
+      throw new IllegalArgumentException("command line is null or empty");
+    }
+    this.processBuilder =
+      new ProcessBuilder(maybeAddShell(commandLineElements, useShell));
+    if (environmentVariables != null) {
+      // TODO(bazel-team) remove next line eventually; it is here to mimic old
+      // Runtime.exec() behavior
+      this.processBuilder.environment().clear();
+      this.processBuilder.environment().putAll(environmentVariables);
+    }
+    this.processBuilder.directory(workingDirectory);
+  }
+
+  private static String[] maybeAddShell(final String[] commandLineElements,
+                                        final boolean useShell) {
+    if (useShell) {
+      final StringBuilder builder = new StringBuilder();
+      for (final String element : commandLineElements) {
+        if (builder.length() > 0) {
+          builder.append(' ');
+        }
+        builder.append(element);
+      }
+      return Shell.getPlatformShell().shellify(builder.toString());
+    } else {
+      return commandLineElements;
+    }
+  }
+
+  /**
+   * @return raw command line elements to be executed
+   */
+  public String[] getCommandLineElements() {
+    final List<String> elements = processBuilder.command();
+    return elements.toArray(new String[elements.size()]);
+  }
+
+  /**
+   * @return (unmodifiable) {@link Map} view of command's environment variables
+   */
+  public Map<String, String> getEnvironmentVariables() {
+    return Collections.unmodifiableMap(processBuilder.environment());
+  }
+
+  /**
+   * @return working directory used for execution, or null if the current
+   *         working directory is used
+   */
+  public File getWorkingDirectory() {
+    return processBuilder.directory();
+  }
+
+  /**
+   * Execute this command with no input to stdin. This call will block until the
+   * process completes or an error occurs.
+   *
+   * @return {@link CommandResult} representing result of the execution
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if an {@link IOException} is
+   *  encountered while reading from the process, or the process was terminated
+   *  due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   */
+  public CommandResult execute() throws CommandException {
+    return execute(NO_INPUT);
+  }
+
+  /**
+   * Execute this command with given input to stdin. This call will block until
+   * the process completes or an error occurs.
+   *
+   * @param stdinInput bytes to be written to process's stdin
+   * @return {@link CommandResult} representing result of the execution
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if an {@link IOException} is
+   *  encountered while reading from the process, or the process was terminated
+   *  due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if stdin is null
+   */
+  public CommandResult execute(final byte[] stdinInput)
+    throws CommandException {
+    nullCheck(stdinInput, "stdinInput");
+    return doExecute(new ByteArrayInputSource(stdinInput),
+                     NO_OBSERVER,
+                     Consumers.createAccumulatingConsumers(),
+                     /*killSubprocess=*/false, /*closeOutput=*/false).get();
+  }
+
+  /**
+   * <p>Execute this command with given input to stdin. This call will block
+   * until the process completes or an error occurs. Caller may specify
+   * whether the method should ignore stdout/stderr output. If the
+   * given number of milliseconds elapses before the command has
+   * completed, this method will attempt to kill the command.</p>
+   *
+   * @param stdinInput bytes to be written to process's stdin, or
+   * {@link #NO_INPUT} if no bytes should be written
+   * @param timeout number of milliseconds to wait for command completion
+   *  before attempting to kill the command
+   * @param ignoreOutput if true, method will ignore stdout/stderr output
+   *  and return value will not contain this data
+   * @return {@link CommandResult} representing result of the execution
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if an {@link IOException} is
+   *  encountered while reading from the process, or the process was terminated
+   *  due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if stdin is null
+   */
+  public CommandResult execute(final byte[] stdinInput,
+                               final long timeout,
+                               final boolean ignoreOutput)
+    throws CommandException {
+    return execute(stdinInput,
+                   new TimeoutKillableObserver(timeout),
+                   ignoreOutput);
+  }
+
+  /**
+   * <p>Execute this command with given input to stdin. This call will block
+   * until the process completes or an error occurs. Caller may specify
+   * whether the method should ignore stdout/stderr output. The given {@link
+   * KillableObserver} may also terminate the process early while running.</p>
+   *
+   * @param stdinInput bytes to be written to process's stdin, or
+   *  {@link #NO_INPUT} if no bytes should be written
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill
+   *  the process
+   * @param ignoreOutput if true, method will ignore stdout/stderr output
+   *  and return value will not contain this data
+   * @return {@link CommandResult} representing result of the execution
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if the process is interrupted (or
+   *  killed) before completion, if an {@link IOException} is encountered while
+   *  reading from the process, or the process was terminated due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if stdin is null
+   */
+  public CommandResult execute(final byte[] stdinInput,
+                               final KillableObserver observer,
+                               final boolean ignoreOutput)
+    throws CommandException {
+    // supporting "null" here for backwards compatibility
+    final KillableObserver theObserver =
+      observer == null ? NO_OBSERVER : observer;
+    return doExecute(new ByteArrayInputSource(stdinInput),
+                     theObserver,
+                     ignoreOutput ? Consumers.createDiscardingConsumers()
+                                  : Consumers.createAccumulatingConsumers(),
+                     /*killSubprocess=*/false, /*closeOutput=*/false).get();
+  }
+
+  /**
+   * <p>Execute this command with given input to stdin. This call blocks
+   * until the process completes or an error occurs. The caller provides
+   * {@link OutputStream} instances into which the process writes its
+   * stdout/stderr output; these streams are <em>not</em> closed when the
+   * process terminates. The given {@link KillableObserver} may also
+   * terminate the process early while running.</p>
+   *
+   * <p>Note that stdout and stderr are written concurrently. If these are
+   * aliased to each other, it is the caller's duty to ensure thread safety.
+   * </p>
+   *
+   * @param stdinInput bytes to be written to process's stdin, or
+   * {@link #NO_INPUT} if no bytes should be written
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+   *  process
+   * @param stdOut the process will write its standard output into this stream.
+   *  E.g., you could pass {@link System#out} as <code>stdOut</code>.
+   * @param stdErr the process will write its standard error into this stream.
+   *  E.g., you could pass {@link System#err} as <code>stdErr</code>.
+   * @return {@link CommandResult} representing result of the execution. Note
+   *  that {@link CommandResult#getStdout()} and
+   *  {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+   *  in this case, as the output is written to <code>stdOut/stdErr</code>
+   *  instead.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if the process is interrupted (or
+   *  killed) before completion, if an {@link IOException} is encountered while
+   *  reading from the process, or the process was terminated due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if any argument is null.
+   */
+  public CommandResult execute(final byte[] stdinInput,
+                               final KillableObserver observer,
+                               final OutputStream stdOut,
+                               final OutputStream stdErr)
+    throws CommandException {
+    return execute(stdinInput, observer, stdOut, stdErr, false);
+  }
+
+  /**
+   * Like {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}
+   * but enables setting of the killSubprocessOnInterrupt attribute.
+   *
+   * @param killSubprocessOnInterrupt if set to true, the execution of
+   * this command is <i>interruptible</i>: in other words, if this thread is
+   * interrupted during a call to execute, the subprocess will be terminated
+   * and the call will return in a timely manner.  If false, the subprocess
+   * will run to completion; this is the default value use by all other
+   * constructors.  The thread's interrupted status is preserved in all cases,
+   * however.
+   */
+  public CommandResult execute(final byte[] stdinInput,
+                               final KillableObserver observer,
+                               final OutputStream stdOut,
+                               final OutputStream stdErr,
+                               final boolean killSubprocessOnInterrupt)
+    throws CommandException {
+    nullCheck(stdinInput, "stdinInput");
+    nullCheck(observer, "observer");
+    nullCheck(stdOut, "stdOut");
+    nullCheck(stdErr, "stdErr");
+    return doExecute(new ByteArrayInputSource(stdinInput),
+                     observer,
+                     Consumers.createStreamingConsumers(stdOut, stdErr),
+                     killSubprocessOnInterrupt, false).get();
+  }
+
+  /**
+   * <p>Execute this command with given input to stdin; this stream is closed
+   * when the process terminates, and exceptions raised when closing this
+   * stream are ignored. This call blocks
+   * until the process completes or an error occurs. The caller provides
+   * {@link OutputStream} instances into which the process writes its
+   * stdout/stderr output; these streams are <em>not</em> closed when the
+   * process terminates. The given {@link KillableObserver} may also
+   * terminate the process early while running.</p>
+   *
+   * @param stdinInput The input to this process's stdin
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+   *  process
+   * @param stdOut the process will write its standard output into this stream.
+   *  E.g., you could pass {@link System#out} as <code>stdOut</code>.
+   * @param stdErr the process will write its standard error into this stream.
+   *  E.g., you could pass {@link System#err} as <code>stdErr</code>.
+   * @return {@link CommandResult} representing result of the execution. Note
+   *  that {@link CommandResult#getStdout()} and
+   *  {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+   *  in this case, as the output is written to <code>stdOut/stdErr</code>
+   *  instead.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if the process is interrupted (or
+   *  killed) before completion, if an {@link IOException} is encountered while
+   *  reading from the process, or the process was terminated due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if any argument is null.
+   */
+  public CommandResult execute(final InputStream stdinInput,
+                               final KillableObserver observer,
+                               final OutputStream stdOut,
+                               final OutputStream stdErr)
+    throws CommandException {
+    nullCheck(stdinInput, "stdinInput");
+    nullCheck(observer, "observer");
+    nullCheck(stdOut, "stdOut");
+    nullCheck(stdErr, "stdErr");
+    return doExecute(new InputStreamInputSource(stdinInput),
+                     observer,
+                     Consumers.createStreamingConsumers(stdOut, stdErr),
+                     /*killSubprocess=*/false, /*closeOutput=*/false).get();
+  }
+
+  /**
+   * <p>Execute this command with given input to stdin; this stream is closed
+   * when the process terminates, and exceptions raised when closing this
+   * stream are ignored. This call blocks
+   * until the process completes or an error occurs. The caller provides
+   * {@link OutputStream} instances into which the process writes its
+   * stdout/stderr output; these streams are closed when the process terminates
+   * if closeOut is set. The given {@link KillableObserver} may also
+   * terminate the process early while running.</p>
+   *
+   * @param stdinInput The input to this process's stdin
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+   *  process
+   * @param stdOut the process will write its standard output into this stream.
+   *  E.g., you could pass {@link System#out} as <code>stdOut</code>.
+   * @param stdErr the process will write its standard error into this stream.
+   *  E.g., you could pass {@link System#err} as <code>stdErr</code>.
+   * @param closeOut whether to close the output streams when the subprocess
+   *  terminates.
+   * @return {@link CommandResult} representing result of the execution. Note
+   *  that {@link CommandResult#getStdout()} and
+   *  {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+   *  in this case, as the output is written to <code>stdOut/stdErr</code>
+   *  instead.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws AbnormalTerminationException if the process is interrupted (or
+   *  killed) before completion, if an {@link IOException} is encountered while
+   *  reading from the process, or the process was terminated due to a signal.
+   * @throws BadExitStatusException if the process exits with a
+   *  non-zero status
+   * @throws NullPointerException if any argument is null.
+   */
+  public CommandResult execute(final InputStream stdinInput,
+      final KillableObserver observer,
+      final OutputStream stdOut,
+      final OutputStream stdErr,
+      boolean closeOut)
+      throws CommandException {
+    nullCheck(stdinInput, "stdinInput");
+    nullCheck(observer, "observer");
+    nullCheck(stdOut, "stdOut");
+    nullCheck(stdErr, "stdErr");
+    return doExecute(new InputStreamInputSource(stdinInput),
+        observer,
+        Consumers.createStreamingConsumers(stdOut, stdErr),
+        false, closeOut).get();
+  }
+
+  /**
+   * <p>Executes this command with the given stdinInput, but does not
+   * wait for it to complete. The caller may choose to observe the status
+   * of the launched process by calling methods on the returned object.
+   *
+   * @param stdinInput bytes to be written to process's stdin, or
+   * {@link #NO_INPUT} if no bytes should be written
+   * @return An object that can be used to check if the process terminated and
+   *  obtain the process results.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws NullPointerException if stdin is null
+   */
+  public FutureCommandResult executeAsynchronously(final byte[] stdinInput)
+      throws CommandException {
+    return executeAsynchronously(stdinInput, NO_OBSERVER);
+  }
+
+  /**
+   * <p>Executes this command with the given input to stdin, but does
+   * not wait for it to complete. The caller may choose to observe the
+   * status of the launched process by calling methods on the returned
+   * object.  This method performs the minimum cleanup after the
+   * process terminates: It closes the input stream, and it ignores
+   * exceptions that result from closing it. The given {@link
+   * KillableObserver} may also terminate the process early while
+   * running.</p>
+   *
+   * <p>Note that in this case the {@link KillableObserver} will be assigned
+   * to start observing the process via
+   * {@link KillableObserver#startObserving(Killable)} but will only be
+   * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+   * {@link FutureCommandResult#get()} is called. If the
+   * {@link KillableObserver} implementation used with this method will
+   * not work correctly without calls to
+   * {@link KillableObserver#stopObserving(Killable)} then a new instance
+   * should be used for each call to this method.</p>
+   *
+   * @param stdinInput bytes to be written to process's stdin, or
+   * {@link #NO_INPUT} if no bytes should be written
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill
+   *  the process
+   * @return An object that can be used to check if the process terminated and
+   *  obtain the process results.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws NullPointerException if stdin is null
+   */
+  public FutureCommandResult executeAsynchronously(final byte[] stdinInput,
+                                    final KillableObserver observer)
+    throws CommandException {
+    // supporting "null" here for backwards compatibility
+    final KillableObserver theObserver =
+      observer == null ? NO_OBSERVER : observer;
+    nullCheck(stdinInput, "stdinInput");
+    return doExecute(new ByteArrayInputSource(stdinInput),
+        theObserver,
+        Consumers.createDiscardingConsumers(),
+        /*killSubprocess=*/false, /*closeOutput=*/false);
+  }
+
+  /**
+   * <p>Executes this command with the given input to stdin, but does
+   * not wait for it to complete. The caller may choose to observe the
+   * status of the launched process by calling methods on the returned
+   * object.  This method performs the minimum cleanup after the
+   * process terminates: It closes the input stream, and it ignores
+   * exceptions that result from closing it. The caller provides
+   * {@link OutputStream} instances into which the process writes its
+   * stdout/stderr output; these streams are <em>not</em> closed when
+   * the process terminates. The given {@link KillableObserver} may
+   * also terminate the process early while running.</p>
+   *
+   * <p>Note that stdout and stderr are written concurrently. If these are
+   * aliased to each other, or if the caller continues to write to these
+   * streams, it is the caller's duty to ensure thread safety.
+   * </p>
+   *
+   * <p>Note that in this case the {@link KillableObserver} will be assigned
+   * to start observing the process via
+   * {@link KillableObserver#startObserving(Killable)} but will only be
+   * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+   * {@link FutureCommandResult#get()} is called. If the
+   * {@link KillableObserver} implementation used with this method will
+   * not work correctly without calls to
+   * {@link KillableObserver#stopObserving(Killable)} then a new instance
+   * should be used for each call to this method.</p>
+   *
+   * @param stdinInput The input to this process's stdin
+   * @param observer {@link KillableObserver} that should observe the running
+   *  process, or {@link #NO_OBSERVER} if caller does not wish to kill
+   *  the process
+   * @param stdOut the process will write its standard output into this stream.
+   *  E.g., you could pass {@link System#out} as <code>stdOut</code>.
+   * @param stdErr the process will write its standard error into this stream.
+   *  E.g., you could pass {@link System#err} as <code>stdErr</code>.
+   * @return An object that can be used to check if the process terminated and
+   *  obtain the process results.
+   * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+   *  reason
+   * @throws NullPointerException if stdin is null
+   */
+  public FutureCommandResult executeAsynchronously(final InputStream stdinInput,
+                                    final KillableObserver observer,
+                                    final OutputStream stdOut,
+                                    final OutputStream stdErr)
+      throws CommandException {
+    // supporting "null" here for backwards compatibility
+    final KillableObserver theObserver =
+        observer == null ? NO_OBSERVER : observer;
+    nullCheck(stdinInput, "stdinInput");
+    return doExecute(new InputStreamInputSource(stdinInput),
+        theObserver,
+        Consumers.createStreamingConsumers(stdOut, stdErr),
+        /*killSubprocess=*/false, /*closeOutput=*/false);
+  }
+
+  // End of public API -------------------------------------------------------
+
+  private void nullCheck(Object argument, String argumentName) {
+    if (argument == null) {
+      String message = argumentName + " argument must not be null.";
+      throw new NullPointerException(message);
+    }
+  }
+
+  private FutureCommandResult doExecute(final InputSource stdinInput,
+      final KillableObserver observer,
+      final Consumers.OutErrConsumers outErrConsumers,
+      final boolean killSubprocessOnInterrupt,
+      final boolean closeOutputStreams)
+    throws CommandException {
+
+    logCommand();
+
+    final Process process = startProcess();
+
+    outErrConsumers.logConsumptionStrategy();
+
+    outErrConsumers.registerInputs(process.getInputStream(),
+                                   process.getErrorStream(),
+                                   closeOutputStreams);
+
+    processInput(stdinInput, process);
+
+    // TODO(bazel-team): if the input stream is unbounded, observers will not get start
+    // notification in a timely manner!
+    final Killable processKillable = observeProcess(process, observer);
+
+    return new FutureCommandResult() {
+      @Override
+      public CommandResult get() throws AbnormalTerminationException {
+        return waitForProcessToComplete(process,
+            observer,
+            processKillable,
+            outErrConsumers,
+            killSubprocessOnInterrupt);
+      }
+
+      @Override
+      public boolean isDone() {
+        try {
+          // exitValue seems to be the only non-blocking call for
+          // checking process liveness.
+          process.exitValue();
+          return true;
+        } catch (IllegalThreadStateException e) {
+          return false;
+        }
+      }
+    };
+  }
+
+  private Process startProcess()
+    throws ExecFailedException {
+    try {
+      return processBuilder.start();
+    } catch (IOException ioe) {
+      throw new ExecFailedException(this, ioe);
+    }
+  }
+
+  private static interface InputSource {
+    void copyTo(OutputStream out) throws IOException;
+    boolean isEmpty();
+    String toLogString(String sourceName);
+  }
+
+  private static class ByteArrayInputSource implements InputSource {
+    private byte[] bytes;
+    ByteArrayInputSource(byte[] bytes){
+      this.bytes = bytes;
+    }
+    @Override
+    public void copyTo(OutputStream out) throws IOException {
+      out.write(bytes);
+      out.flush();
+    }
+    @Override
+    public boolean isEmpty() {
+      return bytes.length == 0;
+    }
+    @Override
+    public String toLogString(String sourceName) {
+      if (isEmpty()) {
+        return "No input to " + sourceName;
+      } else {
+        return "Input to " + sourceName + ": " +
+            LogUtil.toTruncatedString(bytes);
+      }
+    }
+  }
+
+  private static class InputStreamInputSource implements InputSource {
+    private InputStream inputStream;
+    InputStreamInputSource(InputStream inputStream){
+      this.inputStream = inputStream;
+    }
+    @Override
+    public void copyTo(OutputStream out) throws IOException {
+      byte[] buf = new byte[4096];
+      int r;
+      while ((r = inputStream.read(buf)) != -1) {
+        out.write(buf, 0, r);
+        out.flush();
+      }
+    }
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+    @Override
+    public String toLogString(String sourceName) {
+      return "Input to " + sourceName + " is a stream.";
+    }
+  }
+
+  private static void processInput(final InputSource stdinInput,
+                                   final Process process) {
+    if (log.isLoggable(Level.FINER)) {
+      log.finer(stdinInput.toLogString("stdin"));
+    }
+    try {
+      if (stdinInput.isEmpty()) {
+        return;
+      }
+      stdinInput.copyTo(process.getOutputStream());
+    } catch (IOException ioe) {
+      // Note: this is not an error!  Perhaps the command just isn't hungry for
+      // our input and exited with success.  Process.waitFor (later) will tell
+      // us.
+      //
+      // (Unlike out/err streams, which are read asynchronously, the input stream is written
+      // synchronously, in its entirety, before processInput returns.  If the input is
+      // infinite, and is passed through e.g. "cat" subprocess and back into the
+      // ByteArrayOutputStream, that will eventually run out of memory, causing the output stream
+      // to be closed, "cat" to terminate with SIGPIPE, and processInput to receive an IOException.
+    } finally {
+      // if this statement is ever deleted, the process's outputStream
+      // must be closed elsewhere -- it is not closed automatically
+      Command.silentClose(process.getOutputStream());
+    }
+  }
+
+  private static Killable observeProcess(final Process process,
+                                         final KillableObserver observer) {
+    final Killable processKillable = new ProcessKillable(process);
+    observer.startObserving(processKillable);
+    return processKillable;
+  }
+
+  private CommandResult waitForProcessToComplete(
+    final Process process,
+    final KillableObserver observer,
+    final Killable processKillable,
+    final Consumers.OutErrConsumers outErr,
+    final boolean killSubprocessOnInterrupt)
+    throws AbnormalTerminationException {
+
+    log.finer("Waiting for process...");
+
+    TerminationStatus status =
+        waitForProcess(process, killSubprocessOnInterrupt);
+
+    observer.stopObserving(processKillable);
+
+    log.finer(status.toString());
+
+    try {
+      outErr.waitForCompletion();
+    } catch (IOException ioe) {
+      CommandResult noOutputResult =
+        new CommandResult(CommandResult.EMPTY_OUTPUT,
+                          CommandResult.EMPTY_OUTPUT,
+                          status);
+      if (status.success()) {
+        // If command was otherwise successful, throw an exception about this
+        throw new AbnormalTerminationException(this, noOutputResult, ioe);
+      } else {
+        // Otherwise, throw the more important exception -- command
+        // was not successful
+        String message = status
+          + "; also encountered an error while attempting to retrieve output";
+        throw status.exited()
+          ? new BadExitStatusException(this, noOutputResult, message, ioe)
+          : new AbnormalTerminationException(this,
+              noOutputResult, message, ioe);
+      }
+    }
+
+    CommandResult result = new CommandResult(outErr.getAccumulatedOut(),
+                                             outErr.getAccumulatedErr(),
+                                             status);
+    result.logThis();
+    if (status.success()) {
+      return result;
+    } else if (status.exited()) {
+      throw new BadExitStatusException(this, result, status.toString());
+    } else {
+      throw new AbnormalTerminationException(this, result, status.toString());
+    }
+  }
+
+  private static TerminationStatus waitForProcess(Process process,
+                                       boolean killSubprocessOnInterrupt) {
+    boolean wasInterrupted = false;
+    try {
+      while (true) {
+        try {
+          return new TerminationStatus(process.waitFor());
+        } catch (InterruptedException ie) {
+          wasInterrupted = true;
+          if (killSubprocessOnInterrupt) {
+            process.destroy();
+          }
+        }
+      }
+    } finally {
+      // Read this for detailed explanation:
+      // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+      if (wasInterrupted) {
+        Thread.currentThread().interrupt(); // preserve interrupted status
+      }
+    }
+  }
+
+  private void logCommand() {
+    if (!log.isLoggable(Level.FINE)) {
+      return;
+    }
+    log.fine(toDebugString());
+  }
+
+  /**
+   * A string representation of this command object which includes
+   * the arguments, the environment, and the working directory. Avoid
+   * relying on the specifics of this format. Note that the size
+   * of the result string will reflect the size of the command.
+   */
+  public String toDebugString() {
+    StringBuilder message = new StringBuilder(128);
+    message.append("Executing (without brackets):");
+    for (final String arg : processBuilder.command()) {
+      message.append(" [");
+      message.append(arg);
+      message.append(']');
+    }
+    message.append("; environment: ");
+    message.append(processBuilder.environment().toString());
+    final File workingDirectory = processBuilder.directory();
+    message.append("; working dir: ");
+    message.append(workingDirectory == null ?
+                   "(current)" :
+                   workingDirectory.toString());
+    return message.toString();
+  }
+
+  /**
+   * Close the <code>out</code> stream and log a warning if anything happens.
+   */
+  private static void silentClose(final OutputStream out) {
+    try {
+      out.close();
+    } catch (IOException ioe) {
+      String message = "Unexpected exception while closing output stream";
+      log.log(Level.WARNING, message, ioe);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandException.java b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
new file mode 100644
index 0000000..a11be97
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Superclass of all exceptions that may be thrown during command execution.
+ * It exists to unify them.  It also provides access to the command name
+ * and arguments for the failing command.
+ */
+public class CommandException extends Exception {
+
+  private final Command command;
+
+  /** Returns the command that failed. */
+  public Command getCommand() {
+    return command;
+  }
+
+  public CommandException(Command command, final String message) {
+    super(message);
+    this.command = command;
+  }
+
+  public CommandException(Command command, final Throwable cause) {
+    super(cause);
+    this.command = command;
+  }
+
+  public CommandException(Command command, final String message,
+      final Throwable cause) {
+    super(message, cause);
+    this.command = command;
+  }
+
+  private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
new file mode 100644
index 0000000..185f91d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
@@ -0,0 +1,116 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Encapsulates the results of a command execution, including exit status
+ * and output to stdout and stderr.
+ */
+public final class CommandResult {
+
+  private static final Logger log =
+    Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+  private static final byte[] NO_BYTES = new byte[0];
+
+  static final ByteArrayOutputStream EMPTY_OUTPUT =
+    new ByteArrayOutputStream() {
+
+      @Override
+      public byte[] toByteArray() {
+        return NO_BYTES;
+      }
+  };
+
+  static final ByteArrayOutputStream NO_OUTPUT_COLLECTED =
+    new ByteArrayOutputStream(){
+
+      @Override
+      public byte[] toByteArray() {
+        throw new IllegalStateException("Output was not collected");
+      }
+  };
+
+  private final ByteArrayOutputStream stdout;
+  private final ByteArrayOutputStream stderr;
+  private final TerminationStatus terminationStatus;
+
+  CommandResult(final ByteArrayOutputStream stdout,
+                final ByteArrayOutputStream stderr,
+                final TerminationStatus terminationStatus) {
+    checkNotNull(stdout);
+    checkNotNull(stderr);
+    checkNotNull(terminationStatus);
+    this.stdout = stdout;
+    this.stderr = stderr;
+    this.terminationStatus = terminationStatus;
+  }
+
+  /**
+   * @return raw bytes that were written to stdout by the command, or
+   *  null if caller did chose to ignore output
+   * @throws IllegalStateException if output was not collected
+   */
+  public byte[] getStdout() {
+    return stdout.toByteArray();
+  }
+
+  /**
+   * @return raw bytes that were written to stderr by the command, or
+   *  null if caller did chose to ignore output
+   * @throws IllegalStateException if output was not collected
+   */
+  public byte[] getStderr() {
+    return stderr.toByteArray();
+  }
+
+  /**
+   * @return the result of Process.waitFor for the subprocess.
+   * @deprecated this returns the result of Process.waitFor, which is not
+   *   precisely defined, and is not to be confused with the value passed to
+   *   exit(2) by the subprocess.  Use getTerminationStatus() instead.
+   */
+  @Deprecated
+  public int getExitStatus() {
+    return terminationStatus.getRawResult();
+  }
+
+  /**
+   * @return the termination status of the subprocess.
+   */
+  public TerminationStatus getTerminationStatus() {
+    return terminationStatus;
+  }
+
+  void logThis() {
+    if (!log.isLoggable(Level.FINER)) {
+      return;
+    }
+    log.finer(terminationStatus.toString());
+
+    if (stdout == NO_OUTPUT_COLLECTED) {
+      return;
+    }
+    log.finer("Stdout: " + LogUtil.toTruncatedString(stdout.toByteArray()));
+    log.finer("Stderr: " + LogUtil.toTruncatedString(stderr.toByteArray()));
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Consumers.java b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
new file mode 100644
index 0000000..3ed5b7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
@@ -0,0 +1,359 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class provides convenience methods for consuming (actively reading)
+ * output and error streams with different consumption policies:
+ * discarding ({@link #createDiscardingConsumers()},
+ * accumulating ({@link #createAccumulatingConsumers()},
+ * and streaming ({@link #createStreamingConsumers(OutputStream, OutputStream)}).
+ */
+class Consumers {
+
+  private static final Logger log =
+    Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+  private Consumers() {}
+
+  private static final ExecutorService pool =
+    Executors.newCachedThreadPool(new AccumulatorThreadFactory());
+
+  static OutErrConsumers createDiscardingConsumers() {
+    return new OutErrConsumers(new DiscardingConsumer(),
+                               new DiscardingConsumer());
+  }
+
+  static OutErrConsumers createAccumulatingConsumers() {
+    return new OutErrConsumers(new AccumulatingConsumer(),
+                               new AccumulatingConsumer());
+  }
+
+  static OutErrConsumers createStreamingConsumers(OutputStream out,
+                                                  OutputStream err) {
+    return new OutErrConsumers(new StreamingConsumer(out),
+                               new StreamingConsumer(err));
+  }
+
+  static class OutErrConsumers {
+
+    private final OutputConsumer out;
+    private final OutputConsumer err;
+
+    private OutErrConsumers(final OutputConsumer out, final OutputConsumer err){
+      this.out = out;
+      this.err = err;
+    }
+
+    void registerInputs(InputStream outInput, InputStream errInput, boolean closeStreams){
+      out.registerInput(outInput, closeStreams);
+      err.registerInput(errInput, closeStreams);
+    }
+
+    void cancel() {
+      out.cancel();
+      err.cancel();
+    }
+
+    void waitForCompletion() throws IOException {
+      out.waitForCompletion();
+      err.waitForCompletion();
+    }
+
+    ByteArrayOutputStream getAccumulatedOut(){
+      return out.getAccumulatedOut();
+    }
+
+    ByteArrayOutputStream getAccumulatedErr() {
+      return err.getAccumulatedOut();
+    }
+
+    void logConsumptionStrategy() {
+      // The creation methods guarantee that the consumption strategy is
+      // the same for out and err - doesn't matter whether we call out or err,
+      // let's pick out.
+      out.logConsumptionStrategy();
+    }
+
+  }
+
+  /**
+   * This interface describes just one consumer, which consumes the
+   * InputStream provided by {@link #registerInput(InputStream, boolean)}.
+   * Implementations implement different consumption strategies.
+   */
+  private static interface OutputConsumer {
+    /**
+     * Returns whatever the consumer accumulated internally, or
+     * {@link CommandResult#NO_OUTPUT_COLLECTED} if it doesn't accumulate
+     * any output.
+     *
+     * @see AccumulatingConsumer
+     */
+    ByteArrayOutputStream getAccumulatedOut();
+
+    void logConsumptionStrategy();
+
+    void registerInput(InputStream in, boolean closeConsumer);
+
+    void cancel();
+
+    void waitForCompletion() throws IOException;
+  }
+
+  /**
+   * This consumer sends the input to a stream while consuming it.
+   */
+  private static class StreamingConsumer extends FutureConsumption
+                                         implements OutputConsumer {
+    private OutputStream out;
+
+    StreamingConsumer(OutputStream out) {
+      this.out = out;
+    }
+
+    @Override
+    public ByteArrayOutputStream getAccumulatedOut() {
+      return CommandResult.NO_OUTPUT_COLLECTED;
+    }
+
+    @Override
+    public void logConsumptionStrategy() {
+      log.finer("Output will be sent to streams provided by client");
+    }
+
+    @Override protected Runnable createConsumingAndClosingSink(InputStream in,
+                                                               boolean closeConsumer) {
+      return new ClosingSink(in, out, closeConsumer);
+    }
+  }
+
+  /**
+   * This consumer sends the input to a {@link ByteArrayOutputStream}
+   * while consuming it. This accumulated stream can be obtained by
+   * calling {@link #getAccumulatedOut()}.
+   */
+  private static class AccumulatingConsumer extends FutureConsumption
+                                            implements OutputConsumer {
+    private ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+    @Override
+    public ByteArrayOutputStream getAccumulatedOut() {
+      return out;
+    }
+
+    @Override
+    public void logConsumptionStrategy() {
+      log.finer("Output will be accumulated (promptly read off) and returned");
+    }
+
+    @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+      return new ClosingSink(in, out);
+    }
+  }
+
+  /**
+   * This consumer just discards whatever it reads.
+   */
+  private static class DiscardingConsumer extends FutureConsumption
+                                          implements OutputConsumer {
+    private DiscardingConsumer() {
+    }
+
+    @Override
+    public ByteArrayOutputStream getAccumulatedOut() {
+      return CommandResult.NO_OUTPUT_COLLECTED;
+    }
+
+    @Override
+    public void logConsumptionStrategy() {
+      log.finer("Output will be ignored");
+    }
+
+    @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+      return new ClosingSink(in);
+    }
+  }
+
+  /**
+   * A mixin that makes consumers active - this is where we kick of
+   * multithreading ({@link #registerInput(InputStream, boolean)}), cancel actions
+   * and wait for the consumers to complete.
+   */
+  private abstract static class FutureConsumption implements OutputConsumer {
+
+    private Future<?> future;
+
+    @Override
+    public void registerInput(InputStream in, boolean closeConsumer){
+      Runnable sink = createConsumingAndClosingSink(in, closeConsumer);
+      future = pool.submit(sink);
+    }
+
+    protected abstract Runnable createConsumingAndClosingSink(InputStream in, boolean close);
+
+    @Override
+    public void cancel() {
+      future.cancel(true);
+    }
+
+    @Override
+    public void waitForCompletion() throws IOException {
+      boolean wasInterrupted = false;
+      try {
+        while (true) {
+          try {
+            future.get();
+            break;
+          } catch (InterruptedException ie) {
+            wasInterrupted = true;
+            // continue waiting
+          } catch (ExecutionException ee) {
+            // Runnable threw a RuntimeException
+            Throwable nested = ee.getCause();
+            if (nested instanceof RuntimeException) {
+              final RuntimeException re = (RuntimeException) nested;
+              // The stream sink classes, unfortunately, tunnel IOExceptions
+              // out of run() in a RuntimeException. If that's the case,
+              // unpack and re-throw the IOException. Otherwise, re-throw
+              // this unexpected RuntimeException
+              final Throwable cause = re.getCause();
+              if (cause instanceof IOException) {
+                throw (IOException) cause;
+              } else {
+                throw re;
+              }
+            } else if (nested instanceof OutOfMemoryError) {
+              // OutOfMemoryError does not support exception chaining.
+              throw (OutOfMemoryError) nested;
+            } else if (nested instanceof Error) {
+              throw new Error("unhandled Error in worker thread", ee);
+            } else {
+              throw new RuntimeException("unknown execution problem", ee);
+            }
+          }
+        }
+      } finally {
+        // Read this for detailed explanation:
+        // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+        if (wasInterrupted) {
+          Thread.currentThread().interrupt(); // preserve interrupted status
+        }
+      }
+    }
+  }
+
+  /**
+   * Factory which produces threads with a 32K stack size.
+   */
+  private static class AccumulatorThreadFactory implements ThreadFactory {
+
+    private static final int THREAD_STACK_SIZE = 32 * 1024;
+
+    private static int threadInitNumber;
+
+    private static synchronized int nextThreadNum() {
+      return threadInitNumber++;
+    }
+
+    @Override
+    public Thread newThread(final Runnable runnable) {
+      final Thread t =
+        new Thread(null,
+                   runnable,
+                   "Command-Accumulator-Thread-" + nextThreadNum(),
+                   THREAD_STACK_SIZE);
+      // Don't let this thread hold up JVM exit
+      t.setDaemon(true);
+      return t;
+    }
+
+  }
+
+  /**
+   * A sink that closes its input stream once its done.
+   */
+  private static class ClosingSink implements Runnable {
+
+    private final InputStream in;
+    private final OutputStream out;
+    private final Runnable sink;
+    private final boolean close;
+
+    /**
+     * Creates a sink that will pump InputStream <code>in</code>
+     * into OutputStream <code>out</code>.
+     */
+    ClosingSink(final InputStream in, OutputStream out) {
+      this(in, out, false);
+    }
+
+    /**
+     * Creates a sink that will read <code>in</code> and discard it.
+     */
+    ClosingSink(final InputStream in) {
+      this.sink = InputStreamSink.newRunnableSink(in);
+      this.in = in;
+      this.close = false;
+      this.out = null;
+    }
+
+    ClosingSink(final InputStream in, OutputStream out, boolean close){
+      this.sink = InputStreamSink.newRunnableSink(in, out);
+      this.in = in;
+      this.out = out;
+      this.close = close;
+    }
+
+
+    @Override
+    public void run() {
+      try {
+        sink.run();
+      } finally {
+        silentClose(in);
+        if (close && out != null) {
+          silentClose(out);
+        }
+      }
+    }
+
+  }
+
+  /**
+   * Close the <code>in</code> stream and log a warning if anything happens.
+   */
+  private static void silentClose(final Closeable closeable) {
+    try {
+      closeable.close();
+    } catch (IOException ioe) {
+      String message = "Unexpected exception while closing input stream";
+      log.log(Level.WARNING, message, ioe);
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
new file mode 100644
index 0000000..24f42a6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command could not even be executed by the JVM --
+ * in particular, when {@link Runtime#exec(String[])} fails.
+ */
+public final class ExecFailedException extends CommandException {
+
+  public ExecFailedException(Command command, final Throwable cause) {
+    super(command, cause);
+  }
+
+  private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
new file mode 100644
index 0000000..3e1f5c9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Supplier of the command result which additionally allows to check if
+ * the command already terminated. Implementing full fledged Future would
+ * be a much harder undertaking, so a bare minimum that makes this class still
+ * useful for asynchronous command execution is implemented.
+ */
+public interface FutureCommandResult {
+  /**
+   * Returns the result of command execution. If the process is not finished
+   * yet (as reported by {@link #isDone()}, the call will block until that
+   * process terminates.
+   *
+   * @return non-null result of command execution
+   * @throws AbnormalTerminationException if command execution failed
+   */
+  CommandResult get() throws AbnormalTerminationException;
+
+  /**
+   * Returns true if the process terminated, the command result is available
+   * and the call to {@link #get()} will not block.
+   *
+   * @return true if the process terminated
+   */
+  boolean isDone();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
new file mode 100644
index 0000000..c35552b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Provides sinks for input streams.  Continuously read an input stream
+ * until the end-of-file is encountered.  The stream may be redirected to
+ * an {@link OutputStream}, or discarded.
+ * <p>
+ * This class is useful for handing the {@code stdout} and {@code stderr}
+ * streams from a {@link Process} started with {@link Runtime#exec(String)}.
+ * If these streams are not consumed, the Process may block resulting in a
+ * deadlock.
+ *
+ * @see <a href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html">
+ *      JavaWorld: When Runtime.exec() won&apos;t</a>
+ */
+public final class InputStreamSink {
+
+  /**
+   * Black hole into which bytes are sometimes discarded by {@link NullSink}.
+   * It is shared by all threads since the actual contents of the buffer
+   * are irrelevant.
+   */
+  private static final byte[] DISCARD = new byte[4096];
+
+  // Supresses default constructor; ensures non-instantiability
+  private InputStreamSink() {
+  }
+
+  /**
+   * A {@link Thread} which reads and discards data from an
+   * {@link InputStream}.
+   */
+  private static class NullSink implements Runnable {
+    private final InputStream in;
+
+    public NullSink(InputStream in) {
+      this.in = in;
+    }
+
+    @Override
+    public void run() {
+      try {
+        try {
+          // Attempt to just skip all input
+          do {
+            in.skip(Integer.MAX_VALUE);
+          } while (in.read() != -1); // Need to test for EOF
+        } catch (IOException ioe) {
+          // Some streams throw IOException when skip() is called;
+          // resort to reading off all input with read():
+          while (in.read(DISCARD) != -1) {
+            // no loop body
+          }
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  /**
+   * A {@link Thread} which reads data from an {@link InputStream},
+   * and translates it into an {@link OutputStream}.
+   */
+  private static class CopySink implements Runnable {
+
+    private final InputStream in;
+    private final OutputStream out;
+
+    public CopySink(InputStream in, OutputStream out) {
+      this.in = in;
+      this.out = out;
+    }
+
+    @Override
+    public void run() {
+      try {
+        byte[] buffer = new byte[2048];
+        int bytesRead;
+        while ((bytesRead = in.read(buffer)) >= 0) {
+          out.write(buffer, 0, bytesRead);
+          out.flush();
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  /**
+   * Creates a {@link Runnable} which consumes the provided
+   * {@link InputStream} 'in', discarding its contents.
+   */
+  public static Runnable newRunnableSink(InputStream in) {
+    if (in == null) {
+      throw new NullPointerException("in");
+    }
+    return new NullSink(in);
+  }
+
+  /**
+   * Creates a {@link Runnable} which copies everything from 'in'
+   * to 'out'. 'out' will be written to and flushed after each
+   * read from 'in'. However, 'out' will not be closed.
+   */
+  public static Runnable newRunnableSink(InputStream in, OutputStream out) {
+    if (in == null) {
+      throw new NullPointerException("in");
+    }
+    if (out == null) {
+      throw new NullPointerException("out");
+    }
+    return new CopySink(in, out);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Killable.java b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
new file mode 100644
index 0000000..66d1146
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Implementations encapsulate a running process that can be killed.
+ * In particular, here, it is used to wrap up a {@link Process} object
+ * and expose it to a {@link KillableObserver}. It is wrapped in this way
+ * so that the actual {@link Process} object can't be altered by
+ * a {@link KillableObserver}.
+ */
+public interface Killable {
+
+  /**
+   * Kill this killable instance.
+   */
+  void kill();
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
new file mode 100644
index 0000000..62d9aa0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Implementations of this interface observe, and potentially kill,
+ * a {@link Killable} object. This is the mechanism by which "kill"
+ * functionality is exposed to callers in the
+ * {@link Command#execute(byte[], KillableObserver, boolean)} method.
+ * 
+ */
+public interface KillableObserver {
+
+  /**
+   * <p>Begin observing the given {@link Killable}. This method must return
+   * promptly; until it returns, {@link Command#execute()} cannot complete.
+   * Implementations may wish to start a new {@link Thread} here to handle
+   * kill logic, and to interrupt or otherwise ask the thread to stop in the
+   * {@link #stopObserving(Killable)} method. See
+   * <a href="http://builder.com.com/5100-6370-5144546.html">
+   * Interrupting Java threads</a> for notes on how to implement this
+   * correctly.</p>
+   *
+   * <p>Implementations may or may not be able to observe more than
+   * one {@link Killable} at a time; see javadoc for details.</p>
+   *
+   * @param killable killable to observer
+   */
+  void startObserving(Killable killable);
+
+  /**
+   * Stop observing the given {@link Killable}, since it is
+   * no longer active.
+   */
+  void stopObserving(Killable killable);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
new file mode 100644
index 0000000..ab646f6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Utilities for logging.
+ */
+class LogUtil {
+
+  private LogUtil() {}
+
+  private final static int TRUNCATE_STRINGS_AT = 150;
+
+  /**
+   * Make a string out of a byte array, and truncate it to a reasonable length.
+   * Useful for preventing logs from becoming excessively large.
+   */
+  static String toTruncatedString(final byte[] bytes) {
+    if(bytes == null || bytes.length == 0) {
+      return "";
+    }
+    /*
+     * Yes, we'll use the platform encoding here, and this is one of the rare
+     * cases where it makes sense. You want the logs to be encoded so that
+     * your platform tools (vi, emacs, cat) can render them, don't you?
+     * In practice, this means ISO-8859-1 or UTF-8, I guess.
+     */
+    try {
+      if (bytes.length > TRUNCATE_STRINGS_AT) {
+        return new String(bytes, 0, TRUNCATE_STRINGS_AT)
+          + "[... truncated. original size was " + bytes.length + " bytes.]";
+      }
+      return new String(bytes);
+    } catch (Exception e) {
+      /*
+       * In case encoding a binary string doesn't work for some reason, we
+       * don't want to bring a logging server down - do we? So we're paranoid.
+       */
+      return "IOUtil.toTruncatedString: " + e.getMessage();
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
new file mode 100644
index 0000000..5d0cb8f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * {@link Killable} implementation which simply wraps a
+ * {@link Process} instance.
+ */
+final class ProcessKillable implements Killable {
+
+  private final Process process;
+
+  ProcessKillable(final Process process) {
+    this.process = process;
+  }
+
+  /**
+   * Calls {@link Process#destroy()}.
+   */
+  @Override
+  public void kill() {
+    process.destroy();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Shell.java b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
new file mode 100644
index 0000000..2cae24e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an OS shell, such as "cmd" on Windows or "sh" on Unix-like
+ * platforms. Currently, Linux and Windows XP are supported.</p>
+ *
+ * <p>This class encapsulates shell-specific logic, like how to
+ * create a command line that uses the shell to invoke another command.
+ */
+public abstract class Shell {
+
+  private static final Logger log =
+    Logger.getLogger("com.google.devtools.build.lib.shell.Shell");
+  
+  private static final Shell platformShell;
+
+  static {
+    final String osName = System.getProperty("os.name");
+    if ("Linux".equals(osName)) {
+      platformShell = new SHShell();
+    } else if ("Windows XP".equals(osName)) {
+      platformShell = new WindowsCMDShell();
+    } else {
+      log.severe("OS not supported; will not be able to execute commands");
+      platformShell = null;
+    }
+    log.config("Loaded shell support '" + platformShell +
+               "' for OS '" + osName + "'");
+  }
+
+  private Shell() {
+    // do nothing
+  }
+
+  /**
+   * @return {@link Shell} subclass appropriate for the current platform
+   * @throws UnsupportedOperationException if no such subclass exists
+   */
+  public static Shell getPlatformShell() {
+    if (platformShell == null) {
+      throw new UnsupportedOperationException("OS is not supported");
+    }
+    return platformShell;
+  }
+
+  /**
+   * Creates a command line suitable for execution by
+   * {@link Runtime#exec(String[])} from the given command string,
+   * a command line which uses a shell appropriate for a particular
+   * platform to execute the command (e.g. "/bin/sh" on Linux).
+   *
+   * @param command command for which to create a command line
+   * @return String[] suitable for execution by
+   *  {@link Runtime#exec(String[])}
+   */
+  public abstract String[] shellify(final String command);
+
+
+  /**
+   * Represents the <code>sh</code> shell commonly found on Unix-like
+   * operating systems, including Linux.
+   */
+  private static final class SHShell extends Shell {
+
+    /**
+     * <p>Returns a command line which uses <code>cmd</code> to execute
+     * the {@link Command}. Given the command <code>foo bar baz</code>,
+     * for example, this will return a String array corresponding
+     * to the command line:</p>
+     *
+     * <p><code>/bin/sh -c "foo bar baz"</code></p>
+     *
+     * <p>That is, it always returns a 3-element array.</p>
+     *
+     * @param command command for which to create a command line
+     * @return String[] suitable for execution by
+     *  {@link Runtime#exec(String[])}
+     */
+    @Override public String[] shellify(final String command) {
+      if (command == null || command.length() == 0) {
+        throw new IllegalArgumentException("command is null or empty");
+      }
+      return new String[] { "/bin/sh", "-c", command };
+    }
+
+  }
+
+  /**
+   * Represents the Windows command shell <code>cmd</code>.
+   */
+  private static final class WindowsCMDShell extends Shell {
+
+    /**
+     * <p>Returns a command line which uses <code>cmd</code> to execute
+     * the {@link Command}. Given the command <code>foo bar baz</code>,
+     * for example, this will return a String array corresponding
+     * to the command line:</p>
+     *
+     * <p><code>cmd /S /C "foo bar baz"</code></p>
+     *
+     * <p>That is, it always returns a 4-element array.</p>
+     *
+     * @param command command for which to create a command line
+     * @return String[] suitable for execution by
+     *  {@link Runtime#exec(String[])}
+     */
+    @Override public String[] shellify(final String command) {
+      if (command == null || command.length() == 0) {
+        throw new IllegalArgumentException("command is null or empty");
+      }
+      return new String[] { "cmd", "/S", "/C", command };
+    }
+
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
new file mode 100644
index 0000000..5157f34
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.List;
+
+/**
+ * Utility functions for Bourne shell commands, including escaping and
+ * tokenizing.
+ */
+public abstract class ShellUtils {
+
+  private ShellUtils() {}
+
+  /**
+   * Characters that have no special meaning to the shell.
+   */
+  private static final String SAFE_PUNCTUATION = "@%-_+:,./";
+
+  /**
+   * Quotes a word so that it can be used, without further quoting,
+   * as an argument (or part of an argument) in a shell command.
+   */
+  public static String shellEscape(String word) {
+    int len = word.length();
+    if (len == 0) {
+      // Empty string is a special case: needs to be quoted to ensure that it gets
+      // treated as a separate argument.
+      return "''";
+    }
+    for (int ii = 0; ii < len; ii++) {
+      char c = word.charAt(ii);
+      // We do this positively so as to be sure we don't inadvertently forget
+      // any unsafe characters.
+      if (!Character.isLetterOrDigit(c) && SAFE_PUNCTUATION.indexOf(c) == -1) {
+        // replace() actually means "replace all".
+        return "'" + word.replace("'", "'\\''") + "'";
+      }
+    }
+    return word;
+  }
+
+  /**
+   * Given an argv array such as might be passed to execve(2), returns a string
+   * that can be copied and pasted into a Bourne shell for a similar effect.
+   */
+  public static String prettyPrintArgv(List<String> argv) {
+    StringBuilder buf = new StringBuilder();
+    for (String arg: argv) {
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append(shellEscape(arg));
+    }
+    return buf.toString();
+  }
+
+
+  /**
+   * Thrown by tokenize method if there is an error
+   */
+  public static class TokenizationException extends Exception {
+    TokenizationException(String message) {
+      super(message);
+    }
+  }
+
+  /**
+   * Populates the passed list of command-line options extracted from {@code
+   * optionString}, which is a string containing multiple options, delimited in
+   * a Bourne shell-like manner.
+   *
+   * @param options the list to be populated with tokens.
+   * @param optionString the string to be tokenized.
+   * @throws TokenizationException if there was an error (such as an
+   * unterminated quotation).
+   */
+  public static void tokenize(List<String> options, String optionString)
+      throws TokenizationException {
+    // See test suite for examples.
+    //
+    // Note: backslash escapes the following character, except within a
+    // single-quoted region where it is literal.
+
+    StringBuilder token = new StringBuilder();
+    boolean forceToken = false;
+    char quotation = '\0'; // NUL, '\'' or '"'
+    for (int ii = 0, len = optionString.length(); ii < len; ii++) {
+      char c = optionString.charAt(ii);
+      if (quotation != '\0') { // in quotation
+        if (c == quotation) { // end of quotation
+          quotation = '\0';
+        } else if (c == '\\' && quotation == '"') { // backslash in "-quotation
+          if (++ii == len) {
+            throw new TokenizationException("backslash at end of string");
+          }
+          c = optionString.charAt(ii);
+          if (c != '\\' && c != '"') {
+            token.append('\\');
+          }
+          token.append(c);
+        } else { // regular char, in quotation
+          token.append(c);
+        }
+      } else { // not in quotation
+        if (c == '\'' || c == '"') { // begin single/double quotation
+          quotation = c;
+          forceToken = true;
+        } else if (c == ' ' || c == '\t') { // space, not quoted
+          if (forceToken || token.length() > 0) {
+            options.add(token.toString());
+            token = new StringBuilder();
+            forceToken = false;
+          }
+        } else if (c == '\\') { // backslash, not quoted
+          if (++ii == len) {
+            throw new TokenizationException("backslash at end of string");
+          }
+          token.append(optionString.charAt(ii));
+        } else { // regular char, not quoted
+          token.append(c);
+        }
+      }
+    }
+    if (quotation != '\0') {
+      throw new TokenizationException("unterminated quotation");
+    }
+    if (forceToken || token.length() > 0) {
+      options.add(token.toString());
+    }
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
new file mode 100644
index 0000000..85794b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * <p>A simple implementation of {@link KillableObserver} which can be told
+ * explicitly to kill its {@link Killable} by calling {@link #kill()}. This
+ * is the sort of functionality that callers might expect to find available
+ * on the {@link Command} class.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class SimpleKillableObserver implements KillableObserver {
+
+  private Killable killable;
+
+  /**
+   * Does nothing except store a reference to the given {@link Killable}.
+   *
+   * @param killable {@link Killable} to kill
+   */
+  public synchronized void startObserving(final Killable killable) {
+    this.killable = killable;
+  }
+
+  /**
+   * Forgets reference to {@link Killable} provided to
+   * {@link #startObserving(Killable)}
+   */
+  public synchronized void stopObserving(final Killable killable) {
+    if (!this.killable.equals(killable)) {
+      throw new IllegalStateException("start/stopObservering called with " +
+                                      "different Killables");
+    }
+    this.killable = null;
+  }
+
+  /**
+   * Calls {@link Killable#kill()} on the saved {@link Killable}.
+   */
+  public synchronized void kill() {
+    if (killable != null) {
+      killable.kill();
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
new file mode 100644
index 0000000..73616c4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
@@ -0,0 +1,162 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Represents the termination status of a command.  {@link Process#waitFor} is
+ * not very precisely specified, so this class encapsulates the interpretation
+ * of values returned by it.
+ *
+ * Caveat: due to the lossy encoding, it's not always possible to accurately
+ * distinguish signal and exit cases.  In particular, processes that exit with
+ * a value within the interval [129, 191] will be mistaken for having been
+ * terminated by a signal.
+ *
+ * Instances are immutable.
+ */
+public final class TerminationStatus {
+
+  private final int waitResult;
+
+  /**
+   * Values taken from the glibc strsignal(3) function.
+   */
+  private static final String[] SIGNAL_STRINGS = {
+    null,
+    "Hangup",
+    "Interrupt",
+    "Quit",
+    "Illegal instruction",
+    "Trace/breakpoint trap",
+    "Aborted",
+    "Bus error",
+    "Floating point exception",
+    "Killed",
+    "User defined signal 1",
+    "Segmentation fault",
+    "User defined signal 2",
+    "Broken pipe",
+    "Alarm clock",
+    "Terminated",
+    "Stack fault",
+    "Child exited",
+    "Continued",
+    "Stopped (signal)",
+    "Stopped",
+    "Stopped (tty input)",
+    "Stopped (tty output)",
+    "Urgent I/O condition",
+    "CPU time limit exceeded",
+    "File size limit exceeded",
+    "Virtual timer expired",
+    "Profiling timer expired",
+    "Window changed",
+    "I/O possible",
+    "Power failure",
+    "Bad system call",
+  };
+
+  private static String getSignalString(int signum) {
+    return signum > 0 && signum < SIGNAL_STRINGS.length
+        ? SIGNAL_STRINGS[signum]
+        : "Signal " + signum;
+  }
+
+  /**
+   * Construct a TerminationStatus instance from a Process waitFor code.
+   *
+   * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
+   */
+  public TerminationStatus(int waitResult) {
+    this.waitResult = waitResult;
+  }
+
+  /**
+   * Returns the "raw" result returned by Process.waitFor.
+   */
+  int getRawResult() {
+    return waitResult;
+  }
+
+  /**
+   * Returns true iff the process exited with code 0.
+   */
+  public boolean success() {
+    return exited() && getExitCode() == 0;
+  }
+
+  // We're relying on undocumented behaviour of Process.waitFor, specifically
+  // that waitResult is the exit status when the process returns normally, or
+  // 128+signalnumber when the process is terminated by a signal.  We further
+  // assume that value signal numbers fall in the interval [1, 63].
+  private static final int SIGNAL_1  = 128 + 1;
+  private static final int SIGNAL_63 = 128 + 63;
+
+  /**
+   * Returns true iff the process exited normally.
+   */
+  public boolean exited() {
+    return waitResult < SIGNAL_1 || waitResult > SIGNAL_63;
+  }
+
+  /**
+   * Returns the exit code of the subprocess.  Undefined if exited() is false.
+   */
+  public int getExitCode() {
+    if (!exited()) {
+      throw new IllegalStateException("getExitCode() not defined");
+    }
+    return waitResult;
+  }
+
+  /**
+   * Returns the number of the signal that terminated the process.  Undefined
+   * if exited() returns true.
+   */
+  public int getTerminatingSignal() {
+    if (exited()) {
+      throw new IllegalStateException("getTerminatingSignal() not defined");
+    }
+    return waitResult - SIGNAL_1 + 1;
+  }
+
+  /**
+   * Returns a short string describing the termination status.
+   * e.g. "Exit 1" or "Hangup".
+   */
+  public String toShortString() {
+    return exited()
+      ? ("Exit " + getExitCode())
+      : (getSignalString(getTerminatingSignal()));
+  }
+
+  @Override
+  public String toString() {
+    return exited()
+      ? ("Process exited with status " + getExitCode())
+      : ("Process terminated by signal " + getTerminatingSignal());
+  }
+
+  @Override
+  public int hashCode() {
+    return waitResult;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return other instanceof TerminationStatus &&
+      ((TerminationStatus) other).waitResult == this.waitResult;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
new file mode 100644
index 0000000..c2ed033
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
@@ -0,0 +1,102 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>{@link KillableObserver} implementation which will kill its observed
+ * {@link Killable} if it is still being observed after a given amount
+ * of time has elapsed.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class TimeoutKillableObserver implements KillableObserver {
+
+  private static final Logger log =
+      Logger.getLogger(TimeoutKillableObserver.class.getCanonicalName());
+
+  private final long timeoutMS;
+  private Killable killable;
+  private SleeperThread sleeperThread;
+  private boolean timedOut;
+
+  // TODO(bazel-team): I'd like to use ThreadPool2, but it doesn't currently
+  // provide a way to interrupt a thread
+
+  public TimeoutKillableObserver(final long timeoutMS) {
+    this.timeoutMS = timeoutMS;
+  }
+
+  /**
+   * Starts a new {@link Thread} to wait for the timeout period. This is
+   * interrupted by the {@link #stopObserving(Killable)} method.
+   *
+   * @param killable killable to kill when the timeout period expires
+   */
+  @Override
+  public synchronized void startObserving(final Killable killable) {
+    this.timedOut = false;
+    this.killable = killable;
+    this.sleeperThread = new SleeperThread();
+    this.sleeperThread.start();
+  }
+
+  @Override
+  public synchronized void stopObserving(final Killable killable) {
+    if (!this.killable.equals(killable)) {
+      throw new IllegalStateException("start/stopObservering called with " +
+                                      "different Killables");
+    }
+    if (sleeperThread.isAlive()) {
+      sleeperThread.interrupt();
+    }
+    this.killable = null;
+    sleeperThread = null;
+  }
+
+  private final class SleeperThread extends Thread {
+    @Override public void run() {
+      try {
+        if (log.isLoggable(Level.FINE)) {
+          log.fine("Waiting for " + timeoutMS + "ms to kill process");
+        }
+        Thread.sleep(timeoutMS);
+        // timeout expired; kill it
+        synchronized (TimeoutKillableObserver.this) {
+          if (killable != null) {
+            log.fine("Killing process");
+            killable.kill();
+            timedOut = true;
+          }
+        }
+      } catch (InterruptedException ie) {
+        // continue -- process finished before timeout
+        log.fine("Wait interrupted since process finished; continuing...");
+      }
+    }
+  }
+
+  /**
+   * Returns true if the observed process was killed by this observer.
+   */
+  public synchronized boolean hasTimedOut() {
+    // synchronized needed for memory model visibility.
+    return timedOut;
+  }
+}