tools: nfdc face show command

refs #3864

Change-Id: Ibee7b082681edc35e8c272b2363436dbc7eaf9c0
diff --git a/docs/manpages/nfdc-face.rst b/docs/manpages/nfdc-face.rst
index 8233364..f015daa 100644
--- a/docs/manpages/nfdc-face.rst
+++ b/docs/manpages/nfdc-face.rst
@@ -4,6 +4,7 @@
 SYNOPSIS
 --------
 | nfdc face [list]
+| nfdc face show <faceId>
 | nfdc channel [list]
 | nfdc create [-P] <faceUri>
 | nfdc destroy <faceId|faceUri>
@@ -17,6 +18,8 @@
 
 The **nfdc face list** command shows a list of faces, their properties, and statistics.
 
+The **nfdc face show** command shows properties and statistics of one specific face.
+
 The **nfdc channel list** command shows a list of channels.
 Channels are listening sockets that can accept incoming connections and create new faces.
 
@@ -33,6 +36,10 @@
     A permanent face is kept alive upon socket errors,
     and is closed only upon **nfdc destroy** command.
 
+<faceId>
+    Numerical identifier of the face.
+    It is displayed in the output of **nfdc face list** and **nfdc create** commands.
+
 <faceUri>
     An URI representing the remote endpoint of a face.
     Its syntax is:
@@ -42,9 +49,16 @@
 
     When a hostname is specified, a DNS query is used to obtain the IP address.
 
-<faceId>
-    Numerical identifier of the face.
-    It is displayed in the output of **nfdc face list** and **nfdc create** commands.
+EXIT CODES
+----------
+
+0: Success
+
+1: An unspecified error occurred
+
+2: Malformed command line
+
+3: Face not found (**nfdc face show** only)
 
 SEE ALSO
 --------
diff --git a/tests/tools/nfdc/channel-module.t.cpp b/tests/tools/nfdc/channel-module.t.cpp
index 8624cd3..1f815ff 100644
--- a/tests/tools/nfdc/channel-module.t.cpp
+++ b/tests/tools/nfdc/channel-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -25,7 +25,7 @@
 
 #include "nfdc/channel-module.hpp"
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -33,7 +33,7 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestChannelModule, ModuleFixture<ChannelModule>)
+BOOST_FIXTURE_TEST_SUITE(TestChannelModule, StatusFixture<ChannelModule>)
 
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <channels>
diff --git a/tests/tools/nfdc/execute-command-fixture.hpp b/tests/tools/nfdc/execute-command-fixture.hpp
new file mode 100644
index 0000000..f25a942
--- /dev/null
+++ b/tests/tools/nfdc/execute-command-fixture.hpp
@@ -0,0 +1,77 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_TESTS_TOOLS_NFDC_EXECUTE_COMMAND_FIXTURE_HPP
+#define NFD_TESTS_TOOLS_NFDC_EXECUTE_COMMAND_FIXTURE_HPP
+
+#include "mock-nfd-mgmt-fixture.hpp"
+#include "nfdc/available-commands.hpp"
+#include <boost/algorithm/string/split.hpp>
+#include <boost/algorithm/string/classification.hpp>
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+namespace tests {
+
+using boost::test_tools::output_test_stream;
+
+/** \brief fixture to test command execution
+ */
+class ExecuteCommandFixture : public MockNfdMgmtFixture
+{
+protected:
+  void
+  execute(const std::string& cmd)
+  {
+    std::vector<std::string> args;
+    boost::split(args, cmd, boost::is_any_of(" "));
+
+    CommandParser parser;
+    registerCommands(parser);
+
+    std::string noun, verb;
+    CommandArguments ca;
+    ExecuteCommand execute;
+    std::tie(noun, verb, ca, execute) = parser.parse(args, ParseMode::ONE_SHOT);
+
+    Controller controller(face, m_keyChain);
+    ExecuteContext ctx{noun, verb, ca, 0, out, err, face, m_keyChain, controller};
+    execute(ctx);
+    exitCode = ctx.exitCode;
+  }
+
+protected:
+  output_test_stream out;
+  output_test_stream err;
+  int exitCode = -1;
+};
+
+} // namespace tests
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
+
+#endif // NFD_TESTS_TOOLS_NFDC_EXECUTE_COMMAND_FIXTURE_HPP
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index faf8ead..0c9d074 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -25,7 +25,8 @@
 
 #include "nfdc/face-module.hpp"
 
-#include "module-fixture.hpp"
+#include "execute-command-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -33,7 +34,76 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestFaceModule, ModuleFixture<FaceModule>)
+BOOST_AUTO_TEST_SUITE(TestFaceModule)
+
+BOOST_FIXTURE_TEST_SUITE(ShowCommand, ExecuteCommandFixture)
+
+using ndn::nfd::FaceQueryFilter;
+
+const std::string NORMAL_OUTPUT = std::string(R"TEXT(
+  faceid=256
+  remote=udp4://84.67.35.111:6363
+   local=udp4://79.91.49.215:6363
+counters={in={28975i 28232d 212n 13307258B} out={19525i 30993d 1038n 6231946B}}
+   flags={non-local on-demand point-to-point}
+)TEXT").substr(1);
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    Name datasetName("/localhost/nfd/faces/query");
+    FaceQueryFilter filter;
+    filter.setFaceId(256);
+    datasetName.append(filter.wireEncode());
+
+    FaceStatus payload;
+    payload.setFaceId(256)
+           .setRemoteUri("udp4://84.67.35.111:6363")
+           .setLocalUri("udp4://79.91.49.215:6363")
+           .setFaceScope(ndn::nfd::FACE_SCOPE_NON_LOCAL)
+           .setFacePersistency(ndn::nfd::FACE_PERSISTENCY_ON_DEMAND)
+           .setLinkType(ndn::nfd::LINK_TYPE_POINT_TO_POINT)
+           .setNInInterests(28975)
+           .setNInDatas(28232)
+           .setNInNacks(212)
+           .setNOutInterests(19525)
+           .setNOutDatas(30993)
+           .setNOutNacks(1038)
+           .setNInBytes(13307258)
+           .setNOutBytes(6231946);
+
+    this->sendDataset(datasetName, payload);
+  };
+
+  this->execute("face show 256");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal(NORMAL_OUTPUT));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(NotFound)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    this->sendEmptyDataset(interest.getName());
+  };
+
+  this->execute("face show 256");
+  BOOST_CHECK_EQUAL(exitCode, 3);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Face 256 not found.\n"));
+}
+
+BOOST_AUTO_TEST_CASE(Error)
+{
+  this->processInterest = nullptr; // no response
+
+  this->execute("face show 256");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when fetching face information: Timeout\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ShowCommand
 
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <faces>
@@ -96,12 +166,12 @@
   "Faces:\n"
   "  faceid=134 remote=udp4://233.252.0.4:6363 local=udp4://192.0.2.1:6363"
     " counters={in={22562i 22031d 63n 2522915B} out={30121i 20940d 1218n 1353592B}}"
-    " non-local permanent multi-access flags={}\n"
+    " flags={non-local permanent multi-access}\n"
   "  faceid=745 remote=fd://75 local=unix:///var/run/nfd.sock"
     " counters={in={18998i 26701d 147n 4672308B} out={34779i 17028d 1176n 8957187B}}"
-    " local on-demand point-to-point flags={local-fields}\n";
+    " flags={local on-demand point-to-point local-fields}\n";
 
-BOOST_AUTO_TEST_CASE(Status)
+BOOST_FIXTURE_TEST_CASE(Status, StatusFixture<FaceModule>)
 {
   this->fetchStatus();
   FaceStatus payload1;
diff --git a/tests/tools/nfdc/fib-module.t.cpp b/tests/tools/nfdc/fib-module.t.cpp
index 3ec3775..812c32c 100644
--- a/tests/tools/nfdc/fib-module.t.cpp
+++ b/tests/tools/nfdc/fib-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -25,7 +25,7 @@
 
 #include "nfdc/fib-module.hpp"
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -33,7 +33,7 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestFibModule, ModuleFixture<FibModule>)
+BOOST_FIXTURE_TEST_SUITE(TestFibModule, StatusFixture<FibModule>)
 
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <fib>
diff --git a/tests/tools/nfdc/forwarder-general-module.t.cpp b/tests/tools/nfdc/forwarder-general-module.t.cpp
index d210dc7..ed026f2 100644
--- a/tests/tools/nfdc/forwarder-general-module.t.cpp
+++ b/tests/tools/nfdc/forwarder-general-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,7 +26,7 @@
 #include "nfdc/forwarder-general-module.hpp"
 #include <ndn-cxx/security/signing-helpers.hpp>
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -34,7 +34,7 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestForwarderGeneralModule, ModuleFixture<ForwarderGeneralModule>)
+BOOST_FIXTURE_TEST_SUITE(TestForwarderGeneralModule, StatusFixture<ForwarderGeneralModule>)
 
 class MakeNfdIdCollector
 {
@@ -46,7 +46,7 @@
   };
 };
 
-class ForwarderGeneralStatusFixture : public ModuleFixture<ForwarderGeneralModule,
+class ForwarderGeneralStatusFixture : public StatusFixture<ForwarderGeneralModule,
                                                            MakeNfdIdCollector>
 {
 protected:
diff --git a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
new file mode 100644
index 0000000..a1dd694
--- /dev/null
+++ b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
@@ -0,0 +1,161 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
+#define NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
+
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+#include "tests/test-common.hpp"
+#include "tests/identity-management-fixture.hpp"
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+namespace tests {
+
+using namespace nfd::tests;
+
+/** \brief fixture to emulate NFD management
+ */
+class MockNfdMgmtFixture : public IdentityManagementTimeFixture
+{
+protected:
+  MockNfdMgmtFixture()
+    : face(g_io, m_keyChain,
+           {true, false, bind(&MockNfdMgmtFixture::processEventsOverride, this, _1)})
+  {
+    face.onSendInterest.connect([=] (const Interest& interest) {
+      g_io.post([=] {
+        if (processInterest != nullptr) {
+          processInterest(interest);
+        }
+      });
+    });
+  }
+
+protected: // status fetching
+  /** \brief send an empty dataset in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  void
+  sendEmptyDataset(const Name& prefix)
+  {
+    this->sendDatasetReply(prefix, nullptr, 0);
+  }
+
+  /** \brief send one WireEncodable in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \param payload payload block
+   *  \note payload must fit in one Data
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  template<typename T>
+  void
+  sendDataset(const Name& prefix, const T& payload)
+  {
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T>));
+
+    this->sendDatasetReply(prefix, payload.wireEncode());
+  }
+
+  /** \brief send two WireEncodables in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \param payload1 first vector item
+   *  \param payload2 second vector item
+   *  \note all payloads must fit in one Data
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  template<typename T1, typename T2>
+  void
+  sendDataset(const Name& prefix, const T1& payload1, const T2& payload2)
+  {
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T1>));
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T2>));
+
+    ndn::encoding::EncodingBuffer buffer;
+    payload2.wireEncode(buffer);
+    payload1.wireEncode(buffer);
+
+    this->sendDatasetReply(prefix, buffer.buf(), buffer.size());
+  }
+
+private:
+  virtual void
+  processEventsOverride(time::milliseconds timeout)
+  {
+    if (timeout <= time::milliseconds::zero()) {
+      // give enough time to finish execution
+      timeout = time::seconds(30);
+    }
+    this->advanceClocks(time::milliseconds(100), timeout);
+  }
+
+  /** \brief send a payload in reply to StatusDataset request
+   *  \param name dataset prefix without version and segment
+   *  \param contentArgs passed to Data::setContent
+   */
+  template<typename ...ContentArgs>
+  void
+  sendDatasetReply(Name name, ContentArgs&&... contentArgs)
+  {
+    name.appendVersion().appendSegment(0);
+
+    // These warnings assist in debugging when nfdc does not receive StatusDataset.
+    // They usually indicate a misspelled prefix or incorrect timing in the test case.
+    if (face.sentInterests.empty()) {
+      BOOST_WARN_MESSAGE(false, "no Interest expressed");
+    }
+    else {
+      BOOST_WARN_MESSAGE(face.sentInterests.back().getName().isPrefixOf(name),
+                         "last Interest " << face.sentInterests.back().getName() <<
+                         " cannot be satisfied by this Data " << name);
+    }
+
+    auto data = make_shared<Data>(name);
+    data->setFinalBlockId(name[-1]);
+    data->setContent(std::forward<ContentArgs>(contentArgs)...);
+    this->signDatasetReply(*data);
+    face.receive(*data);
+  }
+
+  virtual void
+  signDatasetReply(Data& data)
+  {
+    signData(data);
+  }
+
+protected:
+  ndn::util::DummyClientFace face;
+  std::function<void(const Interest&)> processInterest;
+};
+
+} // namespace tests
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
+
+#endif // NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
diff --git a/tests/tools/nfdc/module-fixture.hpp b/tests/tools/nfdc/module-fixture.hpp
deleted file mode 100644
index 61f2295..0000000
--- a/tests/tools/nfdc/module-fixture.hpp
+++ /dev/null
@@ -1,236 +0,0 @@
-/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2014-2016,  Regents of the University of California,
- *                           Arizona Board of Regents,
- *                           Colorado State University,
- *                           University Pierre & Marie Curie, Sorbonne University,
- *                           Washington University in St. Louis,
- *                           Beijing Institute of Technology,
- *                           The University of Memphis.
- *
- * This file is part of NFD (Named Data Networking Forwarding Daemon).
- * See AUTHORS.md for complete list of NFD authors and contributors.
- *
- * NFD is free software: you can redistribute it and/or modify it under the terms
- * of the GNU General Public License as published by the Free Software Foundation,
- * either version 3 of the License, or (at your option) any later version.
- *
- * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- * PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifndef NFD_TESTS_TOOLS_NFD_STATUS_MODULE_FIXTURE_HPP
-#define NFD_TESTS_TOOLS_NFD_STATUS_MODULE_FIXTURE_HPP
-
-#include "nfdc/module.hpp"
-#include <ndn-cxx/security/validator-null.hpp>
-#include <ndn-cxx/util/dummy-client-face.hpp>
-
-#include "tests/test-common.hpp"
-#include "tests/identity-management-fixture.hpp"
-
-namespace nfd {
-namespace tools {
-namespace nfdc {
-namespace tests {
-
-using namespace nfd::tests;
-using ndn::Face;
-using ndn::KeyChain;
-using ndn::Validator;
-using ndn::ValidatorNull;
-using ndn::util::DummyClientFace;
-using boost::test_tools::output_test_stream;
-
-class MakeValidatorNull
-{
-public:
-  unique_ptr<ValidatorNull>
-  operator()(Face&, KeyChain&) const
-  {
-    return make_unique<ValidatorNull>();
-  };
-};
-
-/** \brief fixture to test a \p Module
- *  \tparam MODULE a subclass of \p Module
- *  \tparam MakeValidator a callable to make a Validator for use in \p controller;
- *                        MakeValidator()(Face&, KeyChain&) should return a unique_ptr
- *                        to Validator or its subclass
- */
-template<typename MODULE, typename MakeValidator = MakeValidatorNull>
-class ModuleFixture : public UnitTestTimeFixture
-                    , public IdentityManagementFixture
-{
-protected:
-  typedef typename std::result_of<MakeValidator(Face&, KeyChain&)>::type ValidatorUniquePtr;
-
-  ModuleFixture()
-    : face(g_io, m_keyChain)
-    , validator(MakeValidator()(face, m_keyChain))
-    , controller(face, m_keyChain, *validator)
-    , nFetchStatusSuccess(0)
-  {
-  }
-
-protected: // status fetching
-  /** \brief start fetching status
-   *
-   *  A test case should call \p fetchStatus, \p sendDataset, and \p prepareStatusOutput
-   *  in this order, and then check \p statusXml and \p statusText contain the correct outputs.
-   *  No advanceClocks is needed in between, as they are handled by the fixture.
-   */
-  void
-  fetchStatus()
-  {
-    nFetchStatusSuccess = 0;
-    module.fetchStatus(controller, [this] { ++nFetchStatusSuccess; },
-                       [this] (uint32_t code, const std::string& reason) {
-                         BOOST_FAIL("fetchStatus failure " << code << " " << reason);
-                       },
-                       CommandOptions());
-    this->advanceClocks(time::milliseconds(1));
-  }
-
-  /** \brief send one WireEncodable in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \param payload payload block
-   *  \note payload must fit in one Data
-   *  \pre fetchStatus has been invoked, sendDataset has not been invoked
-   */
-  template<typename T>
-  void
-  sendDataset(const Name& prefix, const T& payload)
-  {
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T>));
-
-    this->sendDatasetReply(prefix, payload.wireEncode());
-  }
-
-  /** \brief send two WireEncodables in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \param payload1 first vector item
-   *  \param payload2 second vector item
-   *  \note all payloads must fit in one Data
-   *  \pre fetchStatus has been invoked, sendDataset has not been invoked
-   */
-  template<typename T1, typename T2>
-  void
-  sendDataset(const Name& prefix, const T1& payload1, const T2& payload2)
-  {
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T1>));
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T2>));
-
-    ndn::encoding::EncodingBuffer buffer;
-    payload2.wireEncode(buffer);
-    payload1.wireEncode(buffer);
-
-    this->sendDatasetReply(prefix, buffer.buf(), buffer.size());
-  }
-
-  /** \brief prepare status output as XML and text
-   *  \pre sendDataset has been invoked
-   */
-  void
-  prepareStatusOutput()
-  {
-    this->advanceClocks(time::milliseconds(1));
-    BOOST_REQUIRE_EQUAL(nFetchStatusSuccess, 1);
-
-    statusXml.str("");
-    module.formatStatusXml(statusXml);
-    statusText.str("");
-    module.formatStatusText(statusText);
-  }
-
-private:
-  /** \brief send a payload in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \param contentArgs passed to Data::setContent
-   */
-  template<typename ...ContentArgs>
-  void
-  sendDatasetReply(const Name& prefix, ContentArgs&&...contentArgs)
-  {
-    Name name = prefix;
-    name.appendVersion().appendSegment(0);
-
-    // These warnings assist in debugging a `nFetchStatusSuccess != 1` check failure.
-    // They usually indicate a misspelled prefix or incorrect timing in the test case.
-    if (face.sentInterests.size() < 1) {
-      BOOST_WARN_MESSAGE(false, "no Interest expressed");
-    }
-    else {
-      BOOST_WARN_MESSAGE(face.sentInterests.back().getName().isPrefixOf(name),
-                         "last Interest " << face.sentInterests.back().getName() <<
-                         " cannot be satisfied by this Data " << name);
-    }
-
-    auto data = make_shared<Data>(name);
-    data->setFinalBlockId(name[-1]);
-    data->setContent(std::forward<ContentArgs>(contentArgs)...);
-    this->signDatasetReply(*data);
-    face.receive(*data);
-  }
-
-  virtual void
-  signDatasetReply(Data& data)
-  {
-    signData(data);
-  }
-
-protected:
-  DummyClientFace face;
-  ValidatorUniquePtr validator;
-  Controller controller;
-
-  MODULE module;
-
-  int nFetchStatusSuccess;
-  output_test_stream statusXml;
-  output_test_stream statusText;
-};
-
-/** \brief strips leading spaces on every line in expected XML
- *
- *  This allows expected XML to be written as:
- *  \code
- *  const std::string STATUS_XML = stripXmlSpaces(R"XML(
- *    <rootElement>
- *      <element>value</element>
- *    </rootElement>
- *  )XML");
- *  \endcode
- *  And \p STATUS_XML would be assigned:
- *  \code
- *  "<rootElement><element>value</element></rootElement>"
- *  \endcode
- */
-inline std::string
-stripXmlSpaces(const std::string& xml)
-{
-  std::string s;
-  bool isSkipping = true;
-  std::copy_if(xml.begin(), xml.end(), std::back_inserter(s),
-               [&isSkipping] (char ch) {
-                 if (ch == '\n') {
-                   isSkipping = true;
-                 }
-                 else if (ch != ' ') {
-                   isSkipping = false;
-                 }
-                 return !isSkipping;
-               });
-  return s;
-}
-
-} // namespace tests
-} // namespace nfdc
-} // namespace tools
-} // namespace nfd
-
-#endif // NFD_TESTS_TOOLS_NFD_STATUS_MODULE_FIXTURE_HPP
diff --git a/tests/tools/nfdc/rib-module.t.cpp b/tests/tools/nfdc/rib-module.t.cpp
index 6088382..9550c5e 100644
--- a/tests/tools/nfdc/rib-module.t.cpp
+++ b/tests/tools/nfdc/rib-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -25,7 +25,7 @@
 
 #include "nfdc/rib-module.hpp"
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -33,7 +33,7 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestRibModule, ModuleFixture<RibModule>)
+BOOST_FIXTURE_TEST_SUITE(TestRibModule, StatusFixture<RibModule>)
 
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <rib>
diff --git a/tests/tools/nfdc/status-fixture.hpp b/tests/tools/nfdc/status-fixture.hpp
new file mode 100644
index 0000000..2d33d37
--- /dev/null
+++ b/tests/tools/nfdc/status-fixture.hpp
@@ -0,0 +1,156 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_TESTS_TOOLS_NFDC_STATUS_FIXTURE_HPP
+#define NFD_TESTS_TOOLS_NFDC_STATUS_FIXTURE_HPP
+
+#include "mock-nfd-mgmt-fixture.hpp"
+#include "nfdc/module.hpp"
+#include <ndn-cxx/security/validator-null.hpp>
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+namespace tests {
+
+using ndn::Face;
+using ndn::KeyChain;
+using ndn::Validator;
+using ndn::ValidatorNull;
+using boost::test_tools::output_test_stream;
+
+class MakeValidatorNull
+{
+public:
+  unique_ptr<ValidatorNull>
+  operator()(Face&, KeyChain&) const
+  {
+    return make_unique<ValidatorNull>();
+  };
+};
+
+/** \brief fixture to test status fetching routines in a \p Module
+ *  \tparam M a subclass of \p Module
+ *  \tparam MakeValidator a callable to make a Validator for use in \p controller;
+ *                        MakeValidator()(Face&, KeyChain&) should return a unique_ptr
+ *                        to Validator or its subclass
+ */
+template<typename M, typename MakeValidator = MakeValidatorNull>
+class StatusFixture : public MockNfdMgmtFixture
+{
+protected:
+  using ValidatorUniquePtr = typename std::result_of<MakeValidator(Face&, KeyChain&)>::type;
+
+  StatusFixture()
+    : validator(MakeValidator()(face, m_keyChain))
+    , controller(face, m_keyChain, *validator)
+    , nFetchStatusSuccess(0)
+  {
+  }
+
+protected: // status fetching
+  /** \brief start fetching status
+   *
+   *  A test case should call \p fetchStatus, \p sendDataset, and \p prepareStatusOutput
+   *  in this order, and then check \p statusXml and \p statusText contain the correct outputs.
+   *  No advanceClocks is needed in between, as they are handled by the fixture.
+   */
+  void
+  fetchStatus()
+  {
+    nFetchStatusSuccess = 0;
+    module.fetchStatus(controller, [this] { ++nFetchStatusSuccess; },
+                       [this] (uint32_t code, const std::string& reason) {
+                         BOOST_FAIL("fetchStatus failure " << code << " " << reason);
+                       },
+                       CommandOptions());
+    this->advanceClocks(time::milliseconds(1));
+  }
+
+  /** \brief prepare status output as XML and text
+   *  \pre sendDataset has been invoked
+   */
+  void
+  prepareStatusOutput()
+  {
+    this->advanceClocks(time::milliseconds(1));
+    BOOST_REQUIRE_EQUAL(nFetchStatusSuccess, 1);
+
+    statusXml.str("");
+    module.formatStatusXml(statusXml);
+    statusText.str("");
+    module.formatStatusText(statusText);
+  }
+
+protected:
+  ValidatorUniquePtr validator;
+  Controller controller;
+
+  M module;
+
+  int nFetchStatusSuccess;
+  output_test_stream statusXml;
+  output_test_stream statusText;
+};
+
+/** \brief strips leading spaces on every line in expected XML
+ *
+ *  This allows expected XML to be written as:
+ *  \code
+ *  const std::string STATUS_XML = stripXmlSpaces(R"XML(
+ *    <rootElement>
+ *      <element>value</element>
+ *    </rootElement>
+ *  )XML");
+ *  \endcode
+ *  And \p STATUS_XML would be assigned:
+ *  \code
+ *  "<rootElement><element>value</element></rootElement>"
+ *  \endcode
+ */
+inline std::string
+stripXmlSpaces(const std::string& xml)
+{
+  std::string s;
+  bool isSkipping = true;
+  std::copy_if(xml.begin(), xml.end(), std::back_inserter(s),
+               [&isSkipping] (char ch) {
+                 if (ch == '\n') {
+                   isSkipping = true;
+                 }
+                 else if (ch != ' ') {
+                   isSkipping = false;
+                 }
+                 return !isSkipping;
+               });
+  return s;
+}
+
+} // namespace tests
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
+
+#endif // NFD_TESTS_TOOLS_NFDC_STATUS_FIXTURE_HPP
diff --git a/tests/tools/nfdc/status-report.t.cpp b/tests/tools/nfdc/status-report.t.cpp
index c3637a3..fbe212c 100644
--- a/tests/tools/nfdc/status-report.t.cpp
+++ b/tests/tools/nfdc/status-report.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,7 +26,7 @@
 #include "nfdc/status-report.hpp"
 #include "core/scheduler.hpp"
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -154,7 +154,7 @@
   }
 
 protected:
-  DummyClientFace face;
+  ndn::util::DummyClientFace face;
   ValidatorNull validator;
   Controller controller;
   StatusReportTester report;
diff --git a/tests/tools/nfdc/strategy-choice-module.t.cpp b/tests/tools/nfdc/strategy-choice-module.t.cpp
index 68dee9f..3ed8c34 100644
--- a/tests/tools/nfdc/strategy-choice-module.t.cpp
+++ b/tests/tools/nfdc/strategy-choice-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -25,7 +25,7 @@
 
 #include "nfdc/strategy-choice-module.hpp"
 
-#include "module-fixture.hpp"
+#include "status-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -33,7 +33,7 @@
 namespace tests {
 
 BOOST_AUTO_TEST_SUITE(Nfdc)
-BOOST_FIXTURE_TEST_SUITE(TestStrategyChoiceModule, ModuleFixture<StrategyChoiceModule>)
+BOOST_FIXTURE_TEST_SUITE(TestStrategyChoiceModule, StatusFixture<StrategyChoiceModule>)
 
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <strategyChoices>
diff --git a/tools/nfdc/available-commands.cpp b/tools/nfdc/available-commands.cpp
index 23d55e3..cc6cecc 100644
--- a/tools/nfdc/available-commands.cpp
+++ b/tools/nfdc/available-commands.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,6 +26,7 @@
 #include "available-commands.hpp"
 #include "help.hpp"
 #include "status.hpp"
+#include "face-module.hpp"
 #include "legacy-status.hpp"
 #include "legacy-nfdc.hpp"
 
@@ -38,6 +39,8 @@
 {
   registerHelpCommand(parser);
   registerStatusCommands(parser);
+  FaceModule::registerCommands(parser);
+
   registerLegacyStatusCommand(parser);
 
   struct LegacyNfdcCommandDefinition
diff --git a/tools/nfdc/execute-command.cpp b/tools/nfdc/execute-command.cpp
new file mode 100644
index 0000000..639fd19
--- /dev/null
+++ b/tools/nfdc/execute-command.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "execute-command.hpp"
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+
+Controller::DatasetFailCallback
+ExecuteContext::makeDatasetFailureHandler(const std::string& datasetName)
+{
+  return [=] (uint32_t code, const std::string& reason) {
+    this->exitCode = 1;
+    this->err << "Error " << code << " when fetching " << datasetName << ": " << reason << '\n';
+  };
+}
+
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
diff --git a/tools/nfdc/execute-command.hpp b/tools/nfdc/execute-command.hpp
index c8ea5d2..644c70b 100644
--- a/tools/nfdc/execute-command.hpp
+++ b/tools/nfdc/execute-command.hpp
@@ -28,6 +28,7 @@
 
 #include "command-arguments.hpp"
 #include <ndn-cxx/face.hpp>
+#include <ndn-cxx/mgmt/nfd/controller.hpp>
 #include <ndn-cxx/security/key-chain.hpp>
 
 namespace nfd {
@@ -36,11 +37,20 @@
 
 using ndn::Face;
 using ndn::KeyChain;
+using ndn::nfd::Controller;
 
 /** \brief context for command execution
  */
-struct ExecuteContext
+class ExecuteContext
 {
+public:
+  /** \brief handler for dataset retrieval failure
+   *  \param datasetName dataset name used in error message
+   */
+  Controller::DatasetFailCallback
+  makeDatasetFailureHandler(const std::string& datasetName);
+
+public:
   const std::string& noun;
   const std::string& verb;
   const CommandArguments& args;
@@ -52,11 +62,12 @@
   Face& face;
   KeyChain& keyChain;
   ///\todo validator
+  Controller& controller;
 };
 
 /** \brief a function to execute a command
  */
-typedef std::function<void(ExecuteContext& ctx)> ExecuteCommand;
+using ExecuteCommand = std::function<void(ExecuteContext& ctx)>;
 
 } // namespace nfdc
 } // namespace tools
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 076b7af..721a56f 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -23,26 +23,6 @@
  * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/** \todo
- *  #2542 plans to merge nfd-status with nfdc.
- *  FaceModule class should be changed as follows:
- *  (1) move into ndn::nfd::nfdc namespace
- *  (2) assuming command syntax is similar to Windows NT \p netsh ,
- *      this class can handle command argument parsing as soon as
- *      'face' sub-command is detected
- *  (3) introduce methods to create and destroy faces, and update face attributes
- *
- *  \todo
- *  #3444 aims at improving output texts of nfdc.
- *  Assuming it's worked with or after #2542:
- *  (1) introduce an \p style parameter on formatItemText method to specify desired text style,
- *      such as human friendly style vs. porcelain style for script consumption
- *  (2) upon successful command execute, convert the command result into FaceStatus type,
- *      and use formatItemText to render the result, so that output from status retrieval
- *      and command execution have consistent styles
- *  }
- */
-
 #include "face-module.hpp"
 #include "format-helpers.hpp"
 
@@ -51,6 +31,38 @@
 namespace nfdc {
 
 void
+FaceModule::registerCommands(CommandParser& parser)
+{
+  CommandDefinition defFaceShow("face", "show");
+  defFaceShow
+    .setTitle("show face information")
+    .addArg("id", ArgValueType::UNSIGNED, Required::YES, Positional::YES);
+  parser.addCommand(defFaceShow, &FaceModule::show);
+}
+
+void
+FaceModule::show(ExecuteContext& ctx)
+{
+  uint64_t faceId = ctx.args.get<uint64_t>("id");
+
+  ndn::nfd::FaceQueryFilter filter;
+  filter.setFaceId(faceId);
+  ctx.controller.fetch<ndn::nfd::FaceQueryDataset>(
+    filter,
+    [faceId, &ctx] (const std::vector<FaceStatus>& result) {
+      if (result.size() != 1) {
+        ctx.exitCode = 3;
+        ctx.err << "Face " << faceId << " not found.\n";
+        return;
+      }
+      formatItemText(ctx.out, result.front(), true);
+    },
+    ctx.makeDatasetFailureHandler("face information"));
+
+  ctx.face.processEvents();
+}
+
+void
 FaceModule::fetchStatus(Controller& controller,
                         const function<void()>& onSuccess,
                         const Controller::DatasetFailCallback& onFailure,
@@ -128,43 +140,48 @@
 {
   os << "Faces:\n";
   for (const FaceStatus& item : m_status) {
-    this->formatItemText(os, item);
+    os << "  ";
+    formatItemText(os, item, false);
+    os << '\n';
   }
 }
 
 void
-FaceModule::formatItemText(std::ostream& os, const FaceStatus& item) const
+FaceModule::formatItemText(std::ostream& os, const FaceStatus& item, bool wantMultiLine)
 {
-  os << "  faceid=" << item.getFaceId();
-  os << " remote=" << item.getRemoteUri();
-  os << " local=" << item.getLocalUri();
+  text::ItemAttributes ia(wantMultiLine, 8);
+
+  os << ia("faceid") << item.getFaceId();
+  os << ia("remote") << item.getRemoteUri();
+  os << ia("local") << item.getLocalUri();
 
   if (item.hasExpirationPeriod()) {
-    os << " expires=" << text::formatDuration(item.getExpirationPeriod());
+    os << ia("expires") << text::formatDuration(item.getExpirationPeriod());
   }
 
-  os << " counters={in={"
+  os << ia("counters")
+     << "{in={"
      << item.getNInInterests() << "i "
      << item.getNInDatas() << "d "
      << item.getNInNacks() << "n "
-     << item.getNInBytes() << "B} ";
-  os << "out={"
+     << item.getNInBytes() << "B} "
+     << "out={"
      << item.getNOutInterests() << "i "
      << item.getNOutDatas() << "d "
      << item.getNOutNacks() << "n "
      << item.getNOutBytes() << "B}}";
 
-  os << " " << item.getFaceScope();
-  os << " " << item.getFacePersistency();
-  os << " " << item.getLinkType();
-
-  os << " flags={";
+  os << ia("flags") << '{';
+  text::Separator flagSep("", " ");
+  os << flagSep << item.getFaceScope();
+  os << flagSep << item.getFacePersistency();
+  os << flagSep << item.getLinkType();
   if (item.getFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED)) {
-    os << "local-fields";
+    os << flagSep << "local-fields";
   }
-  os << "}";
+  os << '}';
 
-  os << "\n";
+  os << ia.end();
 }
 
 } // namespace nfdc
diff --git a/tools/nfdc/face-module.hpp b/tools/nfdc/face-module.hpp
index 27c089b..92c29b4 100644
--- a/tools/nfdc/face-module.hpp
+++ b/tools/nfdc/face-module.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,6 +27,7 @@
 #define NFD_TOOLS_NFDC_FACE_MODULE_HPP
 
 #include "module.hpp"
+#include "command-parser.hpp"
 
 namespace nfd {
 namespace tools {
@@ -40,13 +41,23 @@
 class FaceModule : public Module, noncopyable
 {
 public:
-  virtual void
+  /** \brief register 'face show', 'face create', 'face destroy' commands
+   */
+  static void
+  registerCommands(CommandParser& parser);
+
+  /** \brief the 'face show' command
+   */
+  static void
+  show(ExecuteContext& ctx);
+
+  void
   fetchStatus(Controller& controller,
               const function<void()>& onSuccess,
               const Controller::DatasetFailCallback& onFailure,
               const CommandOptions& options) override;
 
-  virtual void
+  void
   formatStatusXml(std::ostream& os) const override;
 
   /** \brief format a single status item as XML
@@ -56,15 +67,16 @@
   void
   formatItemXml(std::ostream& os, const FaceStatus& item) const;
 
-  virtual void
+  void
   formatStatusText(std::ostream& os) const override;
 
   /** \brief format a single status item as text
    *  \param os output stream
    *  \param item status item
+   *  \param wantMultiLine use multi-line style
    */
-  void
-  formatItemText(std::ostream& os, const FaceStatus& item) const;
+  static void
+  formatItemText(std::ostream& os, const FaceStatus& item, bool wantMultiLine);
 
 private:
   std::vector<FaceStatus> m_status;
diff --git a/tools/nfdc/format-helpers.cpp b/tools/nfdc/format-helpers.cpp
index 942a9f1..b6b8258 100644
--- a/tools/nfdc/format-helpers.cpp
+++ b/tools/nfdc/format-helpers.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -118,6 +118,45 @@
   return os << sep.m_subsequent;
 }
 
+ItemAttributes::ItemAttributes(bool wantMultiLine, int maxAttributeWidth)
+  : m_wantMultiLine(wantMultiLine)
+  , m_maxAttributeWidth(maxAttributeWidth)
+  , m_count(0)
+{
+}
+
+ItemAttributes::Attribute
+ItemAttributes::operator()(const std::string& attribute)
+{
+  ++m_count;
+  if (m_wantMultiLine) {
+    return {m_count > 1,
+            {m_maxAttributeWidth - static_cast<int>(attribute.size())},
+            attribute};
+  }
+  else {
+    return {false,
+            {m_count > 1 ? 1 : 0},
+            attribute};
+  }
+}
+
+std::string
+ItemAttributes::end() const
+{
+  return m_wantMultiLine ? "\n" : "";
+}
+
+std::ostream&
+operator<<(std::ostream& os, const ItemAttributes::Attribute& attr)
+{
+  if (attr.wantNewline) {
+    os << '\n';
+  }
+  return os << attr.spaces << attr.attribute << '=';
+}
+
+
 std::string
 formatSeconds(time::seconds d, bool isLong)
 {
diff --git a/tools/nfdc/format-helpers.hpp b/tools/nfdc/format-helpers.hpp
index 4c7badd..4fd2c38 100644
--- a/tools/nfdc/format-helpers.hpp
+++ b/tools/nfdc/format-helpers.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -87,7 +87,7 @@
  *  // prints: 1,2,3
  *  \endcode
  */
-class Separator
+class Separator : noncopyable
 {
 public:
   Separator(const std::string& first, const std::string& subsequent);
@@ -112,6 +112,54 @@
 std::ostream&
 operator<<(std::ostream& os, Separator& sep);
 
+/** \brief print attributes of an item
+ *
+ *  \code
+ *  ItemAttributes ia(wantMultiLine, 3);
+ *  os << ia("id") << 500
+ *     << ia("uri") << "udp4://192.0.2.1:6363"
+ *     << ia.end();
+ *
+ *  // prints in single-line style (wantMultiLine==false):
+ *  // id=500 uri=udp4://192.0.2.1:6363 [no-newline]
+ *
+ *  // prints in multi-line style (wantMultiLine==true):
+ *  //  id=500
+ *  // uri=udp4://192.0.2.1:6363 [newline]
+ *  \endcode
+ */
+class ItemAttributes : noncopyable
+{
+public:
+  /** \brief constructor
+   *  \param wantMultiLine true to select multi-line style, false to use single-line style
+   *  \param maxAttributeWidth maximum width of attribute names, for alignment in multi-line style
+   */
+  explicit
+  ItemAttributes(bool wantMultiLine = false, int maxAttributeWidth = 0);
+
+  struct Attribute
+  {
+    bool wantNewline;
+    Spaces spaces;
+    std::string attribute;
+  };
+
+  Attribute
+  operator()(const std::string& attribute);
+
+  std::string
+  end() const;
+
+private:
+  bool m_wantMultiLine;
+  int m_maxAttributeWidth;
+  int m_count;
+};
+
+std::ostream&
+operator<<(std::ostream& os, const ItemAttributes::Attribute& attr);
+
 std::string
 formatSeconds(time::seconds d, bool isLong = false);
 
diff --git a/tools/nfdc/main.cpp b/tools/nfdc/main.cpp
index 1f96c83..a7469a9 100644
--- a/tools/nfdc/main.cpp
+++ b/tools/nfdc/main.cpp
@@ -61,9 +61,10 @@
   }
 
   try {
-    ndn::Face face;
-    ndn::KeyChain keyChain;
-    ExecuteContext ctx{noun, verb, ca, 0, std::cout, std::cerr, face, keyChain};
+    Face face;
+    KeyChain keyChain;
+    Controller controller(face, keyChain);
+    ExecuteContext ctx{noun, verb, ca, 0, std::cout, std::cerr, face, keyChain, controller};
     execute(ctx);
     return ctx.exitCode;
   }