diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -135,6 +135,7 @@
     'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
     'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
     'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
+    'PhutilConsoleTable' => 'console/PhutilConsoleTable.php',
     'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
     'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
     'PhutilDaemon' => 'daemon/PhutilDaemon.php',
@@ -550,6 +551,7 @@
     'PhutilConsoleProgressBar' => 'Phobject',
     'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
     'PhutilConsoleStdinNotInteractiveException' => 'Exception',
+    'PhutilConsoleTable' => 'Phobject',
     'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
     'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
diff --git a/src/console/PhutilConsoleTable.php b/src/console/PhutilConsoleTable.php
new file mode 100644
--- /dev/null
+++ b/src/console/PhutilConsoleTable.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Show a table in the console.
+ */
+final class PhutilConsoleTable extends Phobject {
+
+  private $header = array();
+  private $data = array();
+  private $widths = array();
+  private $console;
+
+  public function setConsole(PhutilConsole $console) {
+    $this->console = $console;
+    return $this;
+  }
+
+  private function getConsole() {
+    if ($this->console) {
+      return $this->console;
+    }
+    return PhutilConsole::getConsole();
+  }
+
+  public function setHeader(array $header) {
+    $this->header = $header;
+  }
+
+  public function addData(array $data) {
+    $this->data[] = $data;
+
+    foreach ($data as $key => $value) {
+      $this->widths[$key] = max(idx($this->widths, $key, 0), strlen($value));
+    }
+  }
+
+  public function draw() {
+    $console = $this->getConsole();
+
+    $console->writeOut($this->getHeader());
+    $console->writeOut($this->getBody());
+    $console->writeOut($this->getFooter());
+  }
+
+
+
+  private function getHeader() {
+    $argv = array_merge(
+      array($this->getFormatString()),
+      $this->header);
+
+    return call_user_func_array('sprintf', $argv);
+  }
+
+  private function getBody() {
+    $format = $this->getFormatString();
+    $output = '';
+
+    foreach ($this->data as $row) {
+      $argv = array($format);
+
+      foreach ($this->header as $column) {
+        $argv[] = idx($row, $column, '');
+      }
+
+      $output .= call_user_func_array('sprintf', $argv);
+    }
+
+    return $output;
+  }
+
+  private function getFooter() {
+    return '';
+  }
+
+  private function getFormatString() {
+    $format = '';
+
+    foreach ($this->header as $column) {
+      if ($format !== '') {
+        $format .= "\t";
+      }
+
+      $format .= sprintf('%%%us', idx($this->widths, $column));
+    }
+
+    return $format."\n";
+  }
+
+}