Index: commands/pm/pm.drush.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/pm/pm.drush.inc,v
retrieving revision 1.36
diff -u -r1.36 pm.drush.inc
--- commands/pm/pm.drush.inc	21 May 2009 06:04:42 -0000	1.36
+++ commands/pm/pm.drush.inc	25 May 2009 08:00:08 -0000
@@ -290,7 +290,7 @@
     $info = $form['validation_modules']['#value'][$module]->info;
     $rows[] = array($info['name'] . ' (' . $module . ')', $enabled, truncate_utf8($info['description'], 60, FALSE, TRUE));
   }
-  drush_print_table($rows, 2, TRUE);
+  drush_print_table($rows, TRUE);
   
   // Space delimited list for use by other scripts. Set the --pipe option.
   drush_print_pipe(implode(' ', $pipe));
@@ -416,7 +416,7 @@
     return drush_set_error('DRUSH_PM_PROJECT_NOT_FOUND', dt('No information available.'));
   }
   else {
-    return drush_print_table($rows, FALSE, TRUE);
+    return drush_print_table($rows, TRUE);
   }
 }
 
Index: commands/pm/updatecode.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/pm/updatecode.inc,v
retrieving revision 1.10
diff -u -r1.10 updatecode.inc
--- commands/pm/updatecode.inc	20 May 2009 04:28:18 -0000	1.10
+++ commands/pm/updatecode.inc	25 May 2009 08:00:08 -0000
@@ -63,7 +63,7 @@
   drush_print(dt('Update information last refreshed: ') . ($last  ? format_date($last) : dt('Never')));
   drush_print();
   drush_print(dt("Update status information on all installed and enabled Drupal modules:"));
-  drush_print_table($rows, 2, TRUE);
+  drush_print_table($rows, TRUE);
   drush_print();
 
   // If specific project updates were requested then remove releases for all others
Index: includes/command.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/includes/command.inc,v
retrieving revision 1.31
diff -u -r1.31 command.inc
--- includes/command.inc	11 May 2009 14:43:34 -0000	1.31
+++ includes/command.inc	25 May 2009 08:00:08 -0000
@@ -413,7 +413,15 @@
         $files = drush_scan_directory($path, '\.drush\.inc$');
         foreach ($files as $filename => $info) {
           require_once($filename);
-          $list[basename($filename, '.drush.inc')] = $filename;
+          $base = basename($filename, '.drush.inc');
+          $info = array('status' => 1);
+          $func = $base . '_drush_info';
+          if (function_exists($func)) {
+            $info = $func();
+          }
+          if ($info['status']) {
+            $list[$base] = $filename;
+          }
         }
       }
     }
Index: includes/drush.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/includes/drush.inc,v
retrieving revision 1.39
diff -u -r1.39 drush.inc
--- includes/drush.inc	20 May 2009 20:58:37 -0000	1.39
+++ includes/drush.inc	25 May 2009 08:00:08 -0000
@@ -345,7 +345,7 @@
                 // '[command] is a token representing the current command. @see pm_drush_engine_version_control().
                 $rows[] = array(str_replace('[command]', $commandstring, $name), dt($description));
               }
-              drush_print_table($rows, 2);
+              drush_print_table($rows, false, array(40));
               unset($rows);
               drush_print();
             }
@@ -484,66 +484,116 @@
 
 /**
  * Print a formatted table.
+ * 
  * @param $rows
- *   The rows to print
- * @param $indent
- *   Indentation for the whole table
+ *   The rows to print.
  * @param $header
- *   If TRUE, the first line will be treated as table
- *   header and therefore be underlined.
- */
-function drush_print_table($rows, $indent = 0, $header = FALSE) {
-  if (count($rows) == 0) {
-    // Nothing to output.
-    return;
-  }
-
-  $indent = str_repeat(' ', $indent);
-  $format = _drush_get_table_row_format($rows);
-
-  $header_printed = FALSE;
-  foreach ($rows as $cols) {
-    // Print the current line.
-    print $indent . vsprintf($format, $cols) . "\n";
-    // Underline the first row if $header is set to true.
-    if (!$header_printed && $header) {
-      $headers = array();
-      foreach ($cols as $col) {
-        $headers[] = str_repeat('-', strlen($col));
+ *   If TRUE, the first line will be treated as table header and therefore be
+ *   underlined.
+ * @param $widths
+ *   The widths of each column (in characters) to use - if not specified this
+ *   will be determined automatically, based on a "best fit" algorithm.
+ */
+function drush_print_table($rows, $header = FALSE, $widths = array()) {
+  $tbl = new Console_Table(CONSOLE_TABLE_ALIGN_LEFT , '');
+
+  $auto_widths = drush_table_column_autowidth($rows, $widths);
+
+  // Do wordwrap on all cells.
+  foreach ($rows as $rowkey => $row) {
+    foreach ($row as $col_num => $cell) {
+      $newrows[$rowkey][$col_num] = wordwrap($cell, $auto_widths[$col_num], "\n", TRUE);
+      if (isset($widths[$col_num])) {
+        $newrows[$rowkey][$col_num] = str_pad($newrows[$rowkey][$col_num], $widths[$col_num]);
       }
-      print $indent . trim(vsprintf($format, $headers)) . "\n";
-      $header_printed = TRUE;
     }
   }
+  if ($header) {
+    $headers = array_shift($newrows);
+    $tbl->setHeaders($headers);
+  }
+
+  $tbl->addData($newrows);
+  print $tbl->getTable();
 }
 
 /**
- * Create the table row format string to be used in vsprintf().
+ * Determine the best fit for column widths.
+ * 
+ * @param $rows
+ *   The rows to use for calculations.
+ * @param $widths
+ *   Manually specified widths of each column (in characters) - these will be
+ *   left as is.
  */
-function _drush_get_table_row_format($table) {
-  $widths = _drush_get_table_column_widths($table);
-  foreach ($widths as $col_width) {
-    $col_formats[] = "%-{$col_width}s";
+function drush_table_column_autowidth($rows, $widths) {
+  $auto_widths = $widths;
+  
+  // First we determine the distribution of row lengths in each column.
+  // This is an array of descending character length keys (i.e. starting at
+  // the rightmost character column), with the value indicating the number
+  // of rows where that character column is present.
+  $col_dist = array();
+  foreach ($rows as $rowkey => $row) {
+    foreach ($row as $col_num => $cell) {
+      if (empty($widths[$col_num])) {
+        if (!isset($col_dist[$col_num])) {
+          $col_dist[$col_num] = array();
+        }
+        $length = strlen($cell);
+        while ($length > 0) {
+          $col_dist[$col_num][$length]++;
+          $length--;
+        }
+      }
+    }
+  }
+  foreach ($col_dist as $col_num => $count) {
+    // Sort the distribution in decending key order.
+    krsort($col_dist[$col_num]);
+    // Initially we set all columns to their "ideal" longest width
+    // - i.e. the width of their longest column.
+    $auto_widths[$col_num] = max(array_keys($col_dist[$col_num]));
   }
-  $format = implode("\t", $col_formats);
-  return $format;
-}
 
-/**
- * Calculate table column widths.
- */
-function _drush_get_table_column_widths($table) {
-  $widths = array();
-  foreach ($table as $row => $cols) {
-    foreach ($cols as $col => $value) {
-      $old_width = isset($widths[$col]) ? $widths[$col] : 0;
-      $widths[$col] = max($old_width, strlen((string)$value));
+  // We determine what width we have available to use, and what width the
+  // above "ideal" columns take up.
+  $available_width = drush_get_context('DRUSH_COLUMNS', 80) - count($auto_widths) - 2;
+  $auto_width_current = array_sum($auto_widths);
+  
+  // If we need to reduce a column so that we can fit the space we use this
+  // loop to figure out which column will cause the "least wrapping",
+  // (relative to the other columns) and reduce the width of that column.
+  while ($auto_width_current > $available_width) {
+    $count = 0;
+    foreach ($col_dist as $col_num => $counts) {
+      // If we are just starting our, select the first column.
+      if ($count == 0 ||
+         // OR: if this column would cause less wrapping than the currently
+         // selected column, then select it.
+         (current($counts) < $count) ||
+         // OR: if this column would cause the same amount of wrapping, but is
+         // longer, then we choose to wrap the longer column (proportionally
+         // less wrapping, and helps avoid triple line wraps).
+         (current($counts) == $count && key($counts) > $width)) {
+        // Select the column number, and record the count and current width
+        // for later comparisons.
+        $column = $col_num;
+        $count = current($counts);
+        $width = key($counts);
+      }
     }
+    // Reduce the width of the selected column.
+    $auto_widths[$column]--;
+    // Reduce our overall table width counter.
+    $auto_width_current--;
+    // Remove the corresponding data from the disctribution, so next time
+    // around we use the data for the row to the left.
+    unset($col_dist[$column][$width]);
   }
-  return $widths;
+  return $auto_widths;
 }
 
-
 /**
  * @defgroup logging Logging information to be provided as output.
  * @{
Index: commands/core/drupal/update_5.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/core/drupal/update_5.inc,v
retrieving revision 1.4
diff -u -r1.4 update_5.inc
--- commands/core/drupal/update_5.inc	15 May 2009 19:02:20 -0000	1.4
+++ commands/core/drupal/update_5.inc	25 May 2009 08:00:08 -0000
@@ -66,7 +66,7 @@
         drush_print(dt('The following updates are pending:'));
         drush_print();
         array_unshift($pending, array($module . ' module'));
-        drush_print_table($pending, 0, TRUE);
+        drush_print_table($pending, TRUE);
         drush_print();
         if (!drush_confirm(dt('Do you wish to run all pending updates?'))) {
           drush_die('Aborting.');
Index: commands/core/drupal/update_6.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/core/drupal/update_6.inc,v
retrieving revision 1.6
diff -u -r1.6 update_6.inc
--- commands/core/drupal/update_6.inc	20 May 2009 21:12:37 -0000	1.6
+++ commands/core/drupal/update_6.inc	25 May 2009 08:00:08 -0000
@@ -377,7 +377,7 @@
     drush_print(dt('The following updates are pending:'));
     drush_print();
     array_unshift($pending, array($module . ' module'));
-    drush_print_table($pending, 0, TRUE);
+    drush_print_table($pending, TRUE);
     drush_print();
     if (!drush_confirm(dt('Do you wish to run all pending updates?'))) {
       drush_die('Aborting.');
Index: commands/core/drupal/update_7.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/core/drupal/update_7.inc,v
retrieving revision 1.6
diff -u -r1.6 update_7.inc
--- commands/core/drupal/update_7.inc	21 May 2009 01:20:07 -0000	1.6
+++ commands/core/drupal/update_7.inc	25 May 2009 08:00:08 -0000
@@ -514,7 +514,7 @@
     drush_print(dt('The following updates are pending:'));
     drush_print();
     array_unshift($pending, array($module . ' module'));
-    drush_print_table($pending, 0, TRUE);
+    drush_print_table($pending, TRUE);
     drush_print();
     if (!drush_confirm(dt('Do you wish to run all pending updates?'))) {
       drush_die('Aborting.');
Index: commands/core/core.drush.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/commands/core/core.drush.inc,v
retrieving revision 1.27
diff -u -r1.27 core.drush.inc
--- commands/core/core.drush.inc	8 May 2009 03:46:15 -0000	1.27
+++ commands/core/core.drush.inc	25 May 2009 08:00:08 -0000
@@ -154,11 +154,11 @@
         $rows = array();
         foreach($commands as $key => $command) {
           if (!array_key_exists($key, $printed_rows)) {
-            $rows[$key] = array(sprintf("%-20s", $key), $commands[$key]['description']);
+            $rows[$key] = array($key, $commands[$key]['description']);
             $pipe[] = "\"$key\"";
           }
         }
-        drush_print_table($rows, 2);
+        drush_print_table($rows, FALSE, array(0 => 20));
         $printed_rows = array_merge($printed_rows, $rows);
       }
       else {
@@ -397,7 +397,7 @@
     drush_log(dt('Last !count watchdog log messages:', array('!count' => $limit)));
 
     array_unshift($rows, array(dt('Date'), dt('Severity'), dt('Type'), dt('Message'), dt('User')));
-    drush_print_table($rows, 2, TRUE);
+    drush_print_table($rows, TRUE);
   }
 }
 
Index: drush.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/drush/drush.php,v
retrieving revision 1.65
diff -u -r1.65 drush.php
--- drush.php	20 May 2009 20:58:37 -0000	1.65
+++ drush.php	25 May 2009 08:00:07 -0000
@@ -23,6 +23,7 @@
 require_once DRUSH_BASE_PATH . '/includes/drush.inc';
 require_once DRUSH_BASE_PATH . '/includes/backend.inc';
 require_once DRUSH_BASE_PATH . '/includes/context.inc';
+require_once DRUSH_BASE_PATH . '/includes/table.inc';
 
 drush_set_context('argc', $GLOBALS['argc']);
 drush_set_context('argv', $GLOBALS['argv']);
Index: includes/table.inc
===================================================================
RCS file: includes/table.inc
diff -N includes/table.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/table.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,893 @@
+<?php
+/**
+ * Utility for printing tables from commandline scripts.
+ *
+ * PHP versions 4 and 5
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Console
+ * @package   Console_Table
+ * @author    Richard Heyes <richard@phpguru.org>
+ * @author    Jan Schneider <jan@horde.org>
+ * @copyright 2002-2005 Richard Heyes
+ * @copyright 2006-2008 Jan Schneider
+ * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
+ * @version   CVS: $Id: Table.php,v 1.27 2008/10/20 22:22:13 yunosh Exp $
+ * @link      http://pear.php.net/package/Console_Table
+ */
+
+define('CONSOLE_TABLE_HORIZONTAL_RULE', 1);
+define('CONSOLE_TABLE_ALIGN_LEFT', -1);
+define('CONSOLE_TABLE_ALIGN_CENTER', 0);
+define('CONSOLE_TABLE_ALIGN_RIGHT', 1);
+define('CONSOLE_TABLE_BORDER_ASCII', -1);
+
+/**
+ * The main class.
+ *
+ * @category Console
+ * @package  Console_Table
+ * @author   Jan Schneider <jan@horde.org>
+ * @license  http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
+ * @link     http://pear.php.net/package/Console_Table
+ */
+class Console_Table
+{
+    /**
+     * The table headers.
+     *
+     * @var array
+     */
+    var $_headers = array();
+
+    /**
+     * The data of the table.
+     *
+     * @var array
+     */
+    var $_data = array();
+
+    /**
+     * The maximum number of columns in a row.
+     *
+     * @var integer
+     */
+    var $_max_cols = 0;
+
+    /**
+     * The maximum number of rows in the table.
+     *
+     * @var integer
+     */
+    var $_max_rows = 0;
+
+    /**
+     * Lengths of the columns, calculated when rows are added to the table.
+     *
+     * @var array
+     */
+    var $_cell_lengths = array();
+
+    /**
+     * Heights of the rows.
+     *
+     * @var array
+     */
+    var $_row_heights = array();
+
+    /**
+     * How many spaces to use to pad the table.
+     *
+     * @var integer
+     */
+    var $_padding = 1;
+
+    /**
+     * Column filters.
+     *
+     * @var array
+     */
+    var $_filters = array();
+
+    /**
+     * Columns to calculate totals for.
+     *
+     * @var array
+     */
+    var $_calculateTotals;
+
+    /**
+     * Alignment of the columns.
+     *
+     * @var array
+     */
+    var $_col_align = array();
+
+    /**
+     * Default alignment of columns.
+     *
+     * @var integer
+     */
+    var $_defaultAlign;
+
+    /**
+     * Character set of the data.
+     *
+     * @var string
+     */
+    var $_charset = 'utf-8';
+
+    /**
+     * Border character.
+     *
+     * @var string
+     */
+    var $_border = CONSOLE_TABLE_BORDER_ASCII;
+
+    /**
+     * Whether the data has ANSI colors.
+     *
+     * @var boolean
+     */
+    var $_ansiColor = false;
+
+    /**
+     * Constructor.
+     *
+     * @param integer $align   Default alignment. One of
+     *                         CONSOLE_TABLE_ALIGN_LEFT,
+     *                         CONSOLE_TABLE_ALIGN_CENTER or
+     *                         CONSOLE_TABLE_ALIGN_RIGHT.
+     * @param string  $border  The character used for table borders or
+     *                         CONSOLE_TABLE_BORDER_ASCII.
+     * @param integer $padding How many spaces to use to pad the table.
+     * @param string  $charset A charset supported by the mbstring PHP
+     *                         extension.
+     * @param boolean $color   Whether the data contains ansi color codes.
+     */
+    function Console_Table($align = CONSOLE_TABLE_ALIGN_LEFT,
+                           $border = CONSOLE_TABLE_BORDER_ASCII, $padding = 1,
+                           $charset = null, $color = false)
+    {
+        $this->_defaultAlign = $align;
+        $this->_border       = $border;
+        $this->_padding      = $padding;
+        $this->_ansiColor    = $color;
+        if ($this->_ansiColor) {
+            include_once 'Console/Color.php';
+        }
+        if (!empty($charset)) {
+            $this->setCharset($charset);
+        }
+    }
+
+    /**
+     * Converts an array to a table.
+     *
+     * @param array   $headers      Headers for the table.
+     * @param array   $data         A two dimensional array with the table
+     *                              data.
+     * @param boolean $returnObject Whether to return the Console_Table object
+     *                              instead of the rendered table.
+     *
+     * @static
+     *
+     * @return Console_Table|string  A Console_Table object or the generated
+     *                               table.
+     */
+    function fromArray($headers, $data, $returnObject = false)
+    {
+        if (!is_array($headers) || !is_array($data)) {
+            return false;
+        }
+
+        $table = new Console_Table();
+        $table->setHeaders($headers);
+
+        foreach ($data as $row) {
+            $table->addRow($row);
+        }
+
+        return $returnObject ? $table : $table->getTable();
+    }
+
+    /**
+     * Adds a filter to a column.
+     *
+     * Filters are standard PHP callbacks which are run on the data before
+     * table generation is performed. Filters are applied in the order they
+     * are added. The callback function must accept a single argument, which
+     * is a single table cell.
+     *
+     * @param integer $col       Column to apply filter to.
+     * @param mixed   &$callback PHP callback to apply.
+     *
+     * @return void
+     */
+    function addFilter($col, &$callback)
+    {
+        $this->_filters[] = array($col, &$callback);
+    }
+
+    /**
+     * Sets the charset of the provided table data.
+     *
+     * @param string $charset A charset supported by the mbstring PHP
+     *                        extension.
+     *
+     * @return void
+     */
+    function setCharset($charset)
+    {
+        $locale = setlocale(LC_CTYPE, 0);
+        setlocale(LC_CTYPE, 'en_US');
+        $this->_charset = strtolower($charset);
+        setlocale(LC_CTYPE, $locale);
+    }
+
+    /**
+     * Sets the alignment for the columns.
+     *
+     * @param integer $col_id The column number.
+     * @param integer $align  Alignment to set for this column. One of
+     *                        CONSOLE_TABLE_ALIGN_LEFT
+     *                        CONSOLE_TABLE_ALIGN_CENTER
+     *                        CONSOLE_TABLE_ALIGN_RIGHT.
+     *
+     * @return void
+     */
+    function setAlign($col_id, $align = CONSOLE_TABLE_ALIGN_LEFT)
+    {
+        switch ($align) {
+        case CONSOLE_TABLE_ALIGN_CENTER:
+            $pad = STR_PAD_BOTH;
+            break;
+        case CONSOLE_TABLE_ALIGN_RIGHT:
+            $pad = STR_PAD_LEFT;
+            break;
+        default:
+            $pad = STR_PAD_RIGHT;
+            break;
+        }
+        $this->_col_align[$col_id] = $pad;
+    }
+
+    /**
+     * Specifies which columns are to have totals calculated for them and
+     * added as a new row at the bottom.
+     *
+     * @param array $cols Array of column numbers (starting with 0).
+     *
+     * @return void
+     */
+    function calculateTotalsFor($cols)
+    {
+        $this->_calculateTotals = $cols;
+    }
+
+    /**
+     * Sets the headers for the columns.
+     *
+     * @param array $headers The column headers.
+     *
+     * @return void
+     */
+    function setHeaders($headers)
+    {
+        $this->_headers = array(array_values($headers));
+        $this->_updateRowsCols($headers);
+    }
+
+    /**
+     * Adds a row to the table.
+     *
+     * @param array   $row    The row data to add.
+     * @param boolean $append Whether to append or prepend the row.
+     *
+     * @return void
+     */
+    function addRow($row, $append = true)
+    {
+        if ($append) {
+            $this->_data[] = array_values($row);
+        } else {
+            array_unshift($this->_data, array_values($row));
+        }
+
+        $this->_updateRowsCols($row);
+    }
+
+    /**
+     * Inserts a row after a given row number in the table.
+     *
+     * If $row_id is not given it will prepend the row.
+     *
+     * @param array   $row    The data to insert.
+     * @param integer $row_id Row number to insert before.
+     *
+     * @return void
+     */
+    function insertRow($row, $row_id = 0)
+    {
+        array_splice($this->_data, $row_id, 0, array($row));
+
+        $this->_updateRowsCols($row);
+    }
+
+    /**
+     * Adds a column to the table.
+     *
+     * @param array   $col_data The data of the column.
+     * @param integer $col_id   The column index to populate.
+     * @param integer $row_id   If starting row is not zero, specify it here.
+     *
+     * @return void
+     */
+    function addCol($col_data, $col_id = 0, $row_id = 0)
+    {
+        foreach ($col_data as $col_cell) {
+            $this->_data[$row_id++][$col_id] = $col_cell;
+        }
+
+        $this->_updateRowsCols();
+        $this->_max_cols = max($this->_max_cols, $col_id + 1);
+    }
+
+    /**
+     * Adds data to the table.
+     *
+     * @param array   $data   A two dimensional array with the table data.
+     * @param integer $col_id Starting column number.
+     * @param integer $row_id Starting row number.
+     *
+     * @return void
+     */
+    function addData($data, $col_id = 0, $row_id = 0)
+    {
+        foreach ($data as $row) {
+            if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
+                $this->_data[$row_id] = CONSOLE_TABLE_HORIZONTAL_RULE;
+                $row_id++;
+                continue;
+            }
+            $starting_col = $col_id;
+            foreach ($row as $cell) {
+                $this->_data[$row_id][$starting_col++] = $cell;
+            }
+            $this->_updateRowsCols();
+            $this->_max_cols = max($this->_max_cols, $starting_col);
+            $row_id++;
+        }
+    }
+
+    /**
+     * Adds a horizontal seperator to the table.
+     *
+     * @return void
+     */
+    function addSeparator()
+    {
+        $this->_data[] = CONSOLE_TABLE_HORIZONTAL_RULE;
+    }
+
+    /**
+     * Returns the generated table.
+     *
+     * @return string  The generated table.
+     */
+    function getTable()
+    {
+        $this->_applyFilters();
+        $this->_calculateTotals();
+        $this->_validateTable();
+
+        return $this->_buildTable();
+    }
+
+    /**
+     * Calculates totals for columns.
+     *
+     * @return void
+     */
+    function _calculateTotals()
+    {
+        if (empty($this->_calculateTotals)) {
+            return;
+        }
+
+        $this->addSeparator();
+
+        $totals = array();
+        foreach ($this->_data as $row) {
+            if (is_array($row)) {
+                foreach ($this->_calculateTotals as $columnID) {
+                    $totals[$columnID] += $row[$columnID];
+                }
+            }
+        }
+
+        $this->_data[] = $totals;
+        $this->_updateRowsCols();
+    }
+
+    /**
+     * Applies any column filters to the data.
+     *
+     * @return void
+     */
+    function _applyFilters()
+    {
+        if (empty($this->_filters)) {
+            return;
+        }
+
+        foreach ($this->_filters as $filter) {
+            $column   = $filter[0];
+            $callback = $filter[1];
+
+            foreach ($this->_data as $row_id => $row_data) {
+                if ($row_data !== CONSOLE_TABLE_HORIZONTAL_RULE) {
+                    $this->_data[$row_id][$column] =
+                        call_user_func($callback, $row_data[$column]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Ensures that column and row counts are correct.
+     *
+     * @return void
+     */
+    function _validateTable()
+    {
+        if (!empty($this->_headers)) {
+            $this->_calculateRowHeight(-1, $this->_headers[0]);
+        }
+
+        for ($i = 0; $i < $this->_max_rows; $i++) {
+            for ($j = 0; $j < $this->_max_cols; $j++) {
+                if (!isset($this->_data[$i][$j]) &&
+                    (!isset($this->_data[$i]) ||
+                     $this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE)) {
+                    $this->_data[$i][$j] = '';
+                }
+
+            }
+            $this->_calculateRowHeight($i, $this->_data[$i]);
+
+            if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
+                 ksort($this->_data[$i]);
+            }
+
+        }
+
+        $this->_splitMultilineRows();
+
+        // Update cell lengths.
+        for ($i = 0; $i < count($this->_headers); $i++) {
+            $this->_calculateCellLengths($this->_headers[$i]);
+        }
+        for ($i = 0; $i < $this->_max_rows; $i++) {
+            $this->_calculateCellLengths($this->_data[$i]);
+        }
+
+        ksort($this->_data);
+    }
+
+    /**
+     * Splits multiline rows into many smaller one-line rows.
+     *
+     * @return void
+     */
+    function _splitMultilineRows()
+    {
+        ksort($this->_data);
+        $sections          = array(&$this->_headers, &$this->_data);
+        $max_rows          = array(count($this->_headers), $this->_max_rows);
+        $row_height_offset = array(-1, 0);
+
+        for ($s = 0; $s <= 1; $s++) {
+            $inserted = 0;
+            $new_data = $sections[$s];
+
+            for ($i = 0; $i < $max_rows[$s]; $i++) {
+                // Process only rows that have many lines.
+                $height = $this->_row_heights[$i + $row_height_offset[$s]];
+                if ($height > 1) {
+                    // Split column data into one-liners.
+                    $split = array();
+                    for ($j = 0; $j < $this->_max_cols; $j++) {
+                        $split[$j] = preg_split('/\r?\n|\r/',
+                                                $sections[$s][$i][$j]);
+                    }
+
+                    $new_rows = array();
+                    // Construct new 'virtual' rows - insert empty strings for
+                    // columns that have less lines that the highest one.
+                    for ($i2 = 0; $i2 < $height; $i2++) {
+                        for ($j = 0; $j < $this->_max_cols; $j++) {
+                            $new_rows[$i2][$j] = !isset($split[$j][$i2])
+                                ? ''
+                                : $split[$j][$i2];
+                        }
+                    }
+
+                    // Replace current row with smaller rows.  $inserted is
+                    // used to take account of bigger array because of already
+                    // inserted rows.
+                    array_splice($new_data, $i + $inserted, 1, $new_rows);
+                    $inserted += count($new_rows) - 1;
+                }
+            }
+
+            // Has the data been modified?
+            if ($inserted > 0) {
+                $sections[$s] = $new_data;
+                $this->_updateRowsCols();
+            }
+        }
+    }
+
+    /**
+     * Builds the table.
+     *
+     * @return string  The generated table string.
+     */
+    function _buildTable()
+    {
+        if (!count($this->_data)) {
+            return '';
+        }
+
+        $rule      = $this->_border == CONSOLE_TABLE_BORDER_ASCII
+            ? '|'
+            : $this->_border;
+        $separator = $this->_getSeparator();
+
+        $return = array();
+        for ($i = 0; $i < count($this->_data); $i++) {
+            for ($j = 0; $j < count($this->_data[$i]); $j++) {
+                if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE &&
+                    $this->_strlen($this->_data[$i][$j]) <
+                    $this->_cell_lengths[$j]) {
+                    $this->_data[$i][$j] = $this->_strpad($this->_data[$i][$j],
+                                                          $this->_cell_lengths[$j],
+                                                          ' ',
+                                                          $this->_col_align[$j]);
+                }
+            }
+
+            if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
+                $row_begin    = $rule . str_repeat(' ', $this->_padding);
+                $row_end      = str_repeat(' ', $this->_padding) . $rule;
+                $implode_char = str_repeat(' ', $this->_padding) . $rule
+                    . str_repeat(' ', $this->_padding);
+                $return[]     = $row_begin
+                    . implode($implode_char, $this->_data[$i]) . $row_end;
+            } elseif (!empty($separator)) {
+                $return[] = $separator;
+            }
+
+        }
+
+        $return = implode("\r\n", $return);
+        if (!empty($separator)) {
+            $return = $separator . "\r\n" . $return . "\r\n" . $separator;
+        }
+        $return .= "\r\n";
+
+        if (!empty($this->_headers)) {
+            $return = $this->_getHeaderLine() .  "\r\n" . $return;
+        }
+
+        return $return;
+    }
+
+    /**
+     * Creates a horizontal separator for header separation and table
+     * start/end etc.
+     *
+     * @return string  The horizontal separator.
+     */
+    function _getSeparator()
+    {
+        if (!$this->_border) {
+            return;
+        }
+
+        if ($this->_border == CONSOLE_TABLE_BORDER_ASCII) {
+            $rule = '-';
+            $sect = '+';
+        } else {
+            $rule = $sect = $this->_border;
+        }
+
+        $return = array();
+        foreach ($this->_cell_lengths as $cl) {
+            $return[] = str_repeat($rule, $cl);
+        }
+
+        $row_begin    = $sect . str_repeat($rule, $this->_padding);
+        $row_end      = str_repeat($rule, $this->_padding) . $sect;
+        $implode_char = str_repeat($rule, $this->_padding) . $sect
+            . str_repeat($rule, $this->_padding);
+
+        return $row_begin . implode($implode_char, $return) . $row_end;
+    }
+
+    /**
+     * Returns the header line for the table.
+     *
+     * @return string  The header line of the table.
+     */
+    function _getHeaderLine()
+    {
+        // Make sure column count is correct
+        for ($j = 0; $j < count($this->_headers); $j++) {
+            for ($i = 0; $i < $this->_max_cols; $i++) {
+                if (!isset($this->_headers[$j][$i])) {
+                    $this->_headers[$j][$i] = '';
+                }
+            }
+        }
+
+        for ($j = 0; $j < count($this->_headers); $j++) {
+            for ($i = 0; $i < count($this->_headers[$j]); $i++) {
+                if ($this->_strlen($this->_headers[$j][$i]) <
+                    $this->_cell_lengths[$i]) {
+                    $this->_headers[$j][$i] =
+                        $this->_strpad($this->_headers[$j][$i],
+                                       $this->_cell_lengths[$i],
+                                       ' ',
+                                       $this->_col_align[$i]);
+                }
+            }
+        }
+
+        $rule         = $this->_border == CONSOLE_TABLE_BORDER_ASCII
+            ? '|'
+            : $this->_border;
+        $row_begin    = $rule . str_repeat(' ', $this->_padding);
+        $row_end      = str_repeat(' ', $this->_padding) . $rule;
+        $implode_char = str_repeat(' ', $this->_padding) . $rule
+            . str_repeat(' ', $this->_padding);
+
+        $separator = $this->_getSeparator();
+        if (!empty($separator)) {
+            $return[] = $separator;
+        }
+        for ($j = 0; $j < count($this->_headers); $j++) {
+            $return[] = $row_begin
+                . implode($implode_char, $this->_headers[$j]) . $row_end;
+        }
+
+        return implode("\r\n", $return);
+    }
+
+    /**
+     * Updates values for maximum columns and rows.
+     *
+     * @param array $rowdata Data array of a single row.
+     *
+     * @return void
+     */
+    function _updateRowsCols($rowdata = null)
+    {
+        // Update maximum columns.
+        $this->_max_cols = max($this->_max_cols, count($rowdata));
+
+        // Update maximum rows.
+        ksort($this->_data);
+        $keys            = array_keys($this->_data);
+        $this->_max_rows = end($keys) + 1;
+
+        switch ($this->_defaultAlign) {
+        case CONSOLE_TABLE_ALIGN_CENTER:
+            $pad = STR_PAD_BOTH;
+            break;
+        case CONSOLE_TABLE_ALIGN_RIGHT:
+            $pad = STR_PAD_LEFT;
+            break;
+        default:
+            $pad = STR_PAD_RIGHT;
+            break;
+        }
+
+        // Set default column alignments
+        for ($i = count($this->_col_align); $i < $this->_max_cols; $i++) {
+            $this->_col_align[$i] = $pad;
+        }
+    }
+
+    /**
+     * Calculates the maximum length for each column of a row.
+     *
+     * @param array $row The row data.
+     *
+     * @return void
+     */
+    function _calculateCellLengths($row)
+    {
+        for ($i = 0; $i < count($row); $i++) {
+            if (!isset($this->_cell_lengths[$i])) {
+                $this->_cell_lengths[$i] = 0;
+            }
+            $this->_cell_lengths[$i] = max($this->_cell_lengths[$i],
+                                           $this->_strlen($row[$i]));
+        }
+    }
+
+    /**
+     * Calculates the maximum height for all columns of a row.
+     *
+     * @param integer $row_number The row number.
+     * @param array   $row        The row data.
+     *
+     * @return void
+     */
+    function _calculateRowHeight($row_number, $row)
+    {
+        if (!isset($this->_row_heights[$row_number])) {
+            $this->_row_heights[$row_number] = 1;
+        }
+
+        // Do not process horizontal rule rows.
+        if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
+            return;
+        }
+
+        for ($i = 0, $c = count($row); $i < $c; ++$i) {
+            $lines                           = preg_split('/\r?\n|\r/', $row[$i]);
+            $this->_row_heights[$row_number] = max($this->_row_heights[$row_number],
+                                                   count($lines));
+        }
+    }
+
+    /**
+     * Returns the character length of a string.
+     *
+     * @param string $str A multibyte or singlebyte string.
+     *
+     * @return integer  The string length.
+     */
+    function _strlen($str)
+    {
+        static $mbstring, $utf8;
+
+        // Strip ANSI color codes if requested.
+        if ($this->_ansiColor) {
+            $str = Console_Color::strip($str);
+        }
+
+        // Cache expensive function_exists() calls.
+        if (!isset($mbstring)) {
+            $mbstring = function_exists('mb_strlen');
+        }
+        if (!isset($utf8)) {
+            $utf8 = function_exists('utf8_decode');
+        }
+
+        if ($utf8 &&
+            ($this->_charset == strtolower('utf-8') ||
+             $this->_charset == strtolower('utf8'))) {
+            return strlen(utf8_decode($str));
+        }
+        if ($mbstring) {
+            return mb_strlen($str, $this->_charset);
+        }
+
+        return strlen($str);
+    }
+
+    /**
+     * Returns part of a string.
+     *
+     * @param string  $string The string to be converted.
+     * @param integer $start  The part's start position, zero based.
+     * @param integer $length The part's length.
+     *
+     * @return string  The string's part.
+     */
+    function _substr($string, $start, $length = null)
+    {
+        static $mbstring;
+
+        // Cache expensive function_exists() calls.
+        if (!isset($mbstring)) {
+            $mbstring = function_exists('mb_substr');
+        }
+
+        if (is_null($length)) {
+            $length = $this->_strlen($string);
+        }
+        if ($mbstring) {
+            $ret = @mb_substr($string, $start, $length, $this->_charset);
+            if (!empty($ret)) {
+                return $ret;
+            }
+        }
+        return substr($string, $start, $length);
+    }
+
+    /**
+     * Returns a string padded to a certain length with another string.
+     *
+     * This method behaves exactly like str_pad but is multibyte safe.
+     *
+     * @param string  $input  The string to be padded.
+     * @param integer $length The length of the resulting string.
+     * @param string  $pad    The string to pad the input string with. Must
+     *                        be in the same charset like the input string.
+     * @param const   $type   The padding type. One of STR_PAD_LEFT,
+     *                        STR_PAD_RIGHT, or STR_PAD_BOTH.
+     *
+     * @return string  The padded string.
+     */
+    function _strpad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT)
+    {
+        $mb_length  = $this->_strlen($input);
+        $sb_length  = strlen($input);
+        $pad_length = $this->_strlen($pad);
+
+        /* Return if we already have the length. */
+        if ($mb_length >= $length) {
+            return $input;
+        }
+
+        /* Shortcut for single byte strings. */
+        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
+            return str_pad($input, $length, $pad, $type);
+        }
+
+        switch ($type) {
+        case STR_PAD_LEFT:
+            $left   = $length - $mb_length;
+            $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
+                                     0, $left, $this->_charset) . $input;
+            break;
+        case STR_PAD_BOTH:
+            $left   = floor(($length - $mb_length) / 2);
+            $right  = ceil(($length - $mb_length) / 2);
+            $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
+                                     0, $left, $this->_charset) .
+                $input .
+                $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
+                               0, $right, $this->_charset);
+            break;
+        case STR_PAD_RIGHT:
+            $right  = $length - $mb_length;
+            $output = $input .
+                $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
+                               0, $right, $this->_charset);
+            break;
+        }
+
+        return $output;
+    }
+
+}
