diff --git a/features.drush.inc b/features.drush.inc
index 883540a..e3e2452 100644
--- a/features.drush.inc
+++ b/features.drush.inc
@@ -38,6 +38,21 @@ function features_drush_command() {
     ),
     'aliases' => array('fa'),
   );
+  $items['features-components'] = array(
+    'description' => 'List features components.',
+    'arguments' => array(
+      'patterns' => 'The features components type to list. Omit this argument to list all components.',
+    ),
+    'options' => array(
+      'exported' => array(
+        'description' => 'Show only components that have been exported.',
+      ),
+      'not-exported' => array(
+        'description' => 'Show only components that have not been exported.',
+      ),
+    ),
+    'aliases' => array('fc'),
+  );
   $items['features-update'] = array(
     'description' => "Update a feature module on your site.",
     'arguments' => array(
@@ -355,6 +370,225 @@ function _drush_features_component_list($arg = NULL, $source = NULL, $render = T
 }
 
 /**
+ * List components, with pattern matching.
+ */
+function drush_features_components() {
+  $args = func_get_args();
+  $components = _drush_features_components_list();
+  // If no args supplied, prompt with a list.
+  if (empty($args)) {
+    $types = array_keys($components);
+    array_unshift($types, 'all');
+    $choice = drush_choice($types, 'Enter a number to choose which component type to list.');
+    if ($choice === FALSE) {
+      return;
+    }
+
+    $args = ($choice == 0) ? array('*') : array($types[$choice]);
+  }
+  $options = array(
+    'provided by' => TRUE,
+  );
+  if (drush_get_option(array('exported', 'e'), NULL)) {
+    $options['not exported'] = FALSE;
+  }
+  elseif (drush_get_option(array('not-exported', 'o'), NULL)) {
+    $options['exported'] = FALSE;
+  }
+
+  $filtered_components = _drush_features_components_filter($components, $args, $options);
+  if ($filtered_components) {
+    _drush_features_components_print($filtered_components);
+  }
+}
+
+/**
+ * Returns a listing of all known components, indexed by source.
+ */
+function _drush_features_components_list() {
+  $components = array();
+  foreach (features_get_feature_components() as $source => $info) {
+    if ($options = features_invoke($source, 'features_export_options')) {
+      foreach ($options as $name => $title) {
+        $components[$source][$name] = $title;
+      }
+    }
+  }
+  return $components;
+}
+
+/**
+ * Filters components by patterns.
+ */
+function _drush_features_components_filter($all_components, $patterns = array(), $options = array()) {
+  $options += array(
+    'exported' => TRUE,
+    'not exported' => TRUE,
+    'provided by' => FALSE,
+  );
+  $pool = array();
+  // Maps exported components to feature modules.
+  $components_map = features_get_component_map();
+  // First filter on exported state.
+  foreach ($all_components as $source => $components) {
+    foreach ($components as $name => $title) {
+      $exported = (count($components_map[$source][$name]) > 0);
+      if ($exported) {
+        if ($options['exported']) {
+          $pool[$source][$name] = $title;
+        }
+      }
+      else {
+        if ($options['not exported']) {
+          $pool[$source][$name] = $title;
+        }
+      }
+    }
+  }
+
+  $state_string = '';
+
+  if (!$options['exported']) {
+    $state_string = 'unexported';
+  }
+  elseif (!$options['not exported']) {
+    $state_string = 'exported';
+  }
+
+  $selected = array();
+  foreach ($patterns as $pattern) {
+    // Rewrite * to %. Let users use both as wildcard.
+    $pattern = strtr($pattern, array('*' => '%'));
+    $sources = array();
+    list($source_pattern, $component_pattern) = explode(':', $pattern, 2);
+    // If source is empty, use a pattern.
+    if ($source_pattern == '') {
+      $source_pattern = '%';
+    }
+    if ($component_pattern == '') {
+      $component_pattern = '%';
+    }
+
+    $preg_source_pattern = strtr(preg_quote($source_pattern, '/'), array('%' => '.*'));
+    $preg_component_pattern = strtr(preg_quote($component_pattern, '/'), array('%' => '.*'));
+    // If it isn't a pattern, but a simple string, we don't anchor the
+    // pattern, this allows for abbreviating. Else, we do, as this seems more
+    // natural for patterns.
+    if (strpos($source_pattern, '%') !== FALSE) {
+      $preg_source_pattern = '^' . $preg_source_pattern . '$';
+    }
+    if (strpos($component_pattern, '%') !== FALSE) {
+      $preg_component_pattern = '^' . $preg_component_pattern . '$';
+    }
+    $matches = array();
+
+    // Find the sources.
+    $all_sources = array_keys($pool);
+    $matches = preg_grep('/' . $preg_source_pattern . '/', $all_sources);
+    if (count($matches) > 0) {
+      // If we have multiple matches and the source string wasn't a
+      // pattern, check if one of the matches is equal to the pattern, and
+      // use that, or error out.
+      if (count($matches) > 1 and $preg_source_pattern[0] != '^') {
+        if (in_array($source_pattern, $matches)) {
+          $matches = array($source_pattern);
+        }
+        else {
+          $args = array(
+            '!source' => $source_pattern,
+            '!matches' => implode(', ', $matches),
+          );
+          return drush_set_error('', dt('Ambiguous source "!source", matches !matches', $args));
+        }
+      }
+      // Loose the indexes preg_grep preserved.
+      $sources = array_values($matches);
+    }
+    else {
+      return drush_set_error('', dt('No !state sources match "!source"', array('!state' => $state_string, '!source' => $source_pattern)));
+    }
+
+    // Now find the components.
+    foreach ($sources as $source) {
+      // Find the components.
+      $all_components = array_keys($pool[$source]);
+      // See if there's any matches.
+      $matches = preg_grep('/' . $preg_component_pattern . '/', $all_components);
+      if (count($matches) > 0) {
+        // If we have multiple matches and the components string wasn't a
+        // pattern, check if one of the matches is equal to the pattern, and
+        // use that, or error out.
+        if (count($matches) > 1 and $preg_component_pattern[0] != '^') {
+          if (in_array($component_pattern, $matches)) {
+            $matches = array($component_pattern);
+          }
+          else {
+            $args = array(
+              '!component' => $component_pattern,
+              '!matches' => implode(', ', $matches),
+            );
+            return drush_set_error('', dt('Ambiguous component "!component", matches !matches', $args));
+          }
+        }
+        if (!is_array($selected[$source])) {
+          $selected[$source] = array();
+        }
+        $selected[$source] += array_intersect_key($pool[$source], array_flip($matches));
+      }
+      else {
+        // No matches. If the source was a pattern, just carry on, else
+        // error out. Allows for patterns like :*field*
+        if ($preg_source_pattern[0] != '^') {
+          $args = array(
+            '!state' => $state_string,
+            '!component' => $component_pattern,
+            '!source' => $source,
+          );
+          return drush_set_error('', dt('No !state !source components match "!component"', $args));
+        }
+      }
+    }
+  }
+
+  // Lastly, provide feature module information on the selected components, if
+  // requested.
+  $provided_by = array();
+  if ($options['provided by'] && $options['exported']) {
+    foreach ($selected as $source => $components) {
+      foreach ($components as $name => $title) {
+        $exported = (count($components_map[$source][$name]) > 0);
+        if ($exported) {
+          $provided_by[$source . ':' . $name] = join(', ', $components_map[$source][$name]);
+        }
+      }
+    }
+  }
+
+  return array(
+    'components' => $selected,
+    'sources' => $provided_by,
+  );
+}
+
+/**
+ * Prints a list of filtered components.
+ */
+function _drush_features_components_print($filtered_components) {
+  $rows = array(array(dt('Available sources')));
+  foreach ($filtered_components['components'] as $source => $components) {
+    foreach ($components as $name => $value) {
+      $row = array($source . ':' . $name);
+      if (isset($filtered_components['sources'][$source . ':' . $name])) {
+        $row[] = dt('Provided by') . ': ' . $filtered_components['sources'][$source . ':' . $name];
+      }
+      $rows[] = $row;
+    }
+  }
+
+  drush_print_table($rows, TRUE);
+}
+
+/**
  * Update an existing feature module.
  */
 function drush_features_update() {
diff --git a/features.module b/features.module
index a225d8c..193b48d 100644
--- a/features.module
+++ b/features.module
@@ -784,3 +784,10 @@ function features_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
   _features_form_taxonomy_form_vocabulary_alter($form, $form_state);
 }
 
+/**
+ * Returns components that are offered as an option on feature creation.
+ */
+function features_get_feature_components() {
+  return array_intersect_key(features_get_components(), array_filter(features_get_components(NULL, 'feature_source')));
+}
+
