tools: Allow batch command processing by nfdc
Change-Id: Ia6f70fed88f2d4c918e2ca2b786222840dbd9076
Refs: #5169
diff --git a/docs/manpages/nfdc.rst b/docs/manpages/nfdc.rst
index d80d6dd..1e77830 100644
--- a/docs/manpages/nfdc.rst
+++ b/docs/manpages/nfdc.rst
@@ -7,6 +7,7 @@
| nfdc help [COMMAND]
| nfdc [-h|--help]
| nfdc -V|--version
+| nfdc -f|--batch BATCH-FILE
DESCRIPTION
-----------
@@ -31,6 +32,19 @@
``-V`` or ``--version``
Show version information and exit.
+``-f BATCH-FILE`` or ``--batch BATCH-FILE``
+ Process arguments specified in the ``BATCH-FILE`` as if they would have appeared
+ in the command line (but without ``nfdc``). When necessary, arguments should be
+ escaped using backslash ``\``, single ``'``, or double quotes ``"``. If any of
+ the command fails, the processing will be stopped at that command with error
+ code 2. Empty lines and lines that start with ``#`` character will be ignored.
+ Note that the batch file does not support empty string arguments
+ (``""`` or ``''``), even if they are supported by the regular command line ``nfdc``.
+
+ When running a sequence of commands in rapid succession from a script, this
+ option ensures that the commands are properly timestamped and can therefore
+ be accepted by NFD.
+
EXAMPLES
--------
nfdc
diff --git a/tests/tools/nfdc/README.md b/tests/tools/nfdc/README.md
new file mode 100644
index 0000000..647a012
--- /dev/null
+++ b/tests/tools/nfdc/README.md
@@ -0,0 +1,21 @@
+# Manual test for nfdc batch mode
+
+To run the test from this folder
+
+ nfdc -f nfdc-batch.t.txt
+
+ nfdc --batch nfdc-batch.t.txt
+
+If everything works, it should execute 3 commands with example output like this
+in both cases (can be different depending on the NFD runtime):
+
+ face-exists id=263 local=udp4://192.168.100.240:6363 remote=udp4://192.0.2.1:6363 persistency=persistent reliability=off congestion-marking=on congestion-marking-interval=100ms default-congestion-threshold=65536B mtu=8800
+ route-add-accepted prefix=/ nexthop=264 origin=static cost=0 flags=child-inherit expires=never
+ route-add-accepted prefix=/test2/foo%20bar nexthop=265 origin=static cost=0 flags=child-inherit expires=never
+ CS information:
+ capacity=65536
+ admit=on
+ serve=on
+ nEntries=14
+ nHits=0
+ nMisses=53
diff --git a/tests/tools/nfdc/help.t.cpp b/tests/tools/nfdc/help.t.cpp
index 6e899ba..c5d5012 100644
--- a/tests/tools/nfdc/help.t.cpp
+++ b/tests/tools/nfdc/help.t.cpp
@@ -1,6 +1,6 @@
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
- * Copyright (c) 2014-2019, Regents of the University of California,
+ * Copyright (c) 2014-2021, Regents of the University of California,
* Arizona Board of Regents,
* Colorado State University,
* University Pierre & Marie Curie, Sorbonne University,
@@ -47,7 +47,7 @@
ExecuteCommand dummyExecute = [] (ExecuteContext&) { BOOST_ERROR("should not be called"); };
boost::test_tools::output_test_stream out;
- const std::string header("nfdc [-h|--help] [-V|--version] <command> [<args>]\n\n");
+ const std::string header("nfdc [-h|--help] [-V|--version] [-f|--batch <batch-file>] [<command> [<args>]]\n\n");
const std::string trailer("\nSee 'nfdc help <command>' to read about a specific subcommand.\n");
helpList(out, parser);
diff --git a/tests/tools/nfdc/nfdc-batch.t.txt b/tests/tools/nfdc/nfdc-batch.t.txt
new file mode 100644
index 0000000..22fc042
--- /dev/null
+++ b/tests/tools/nfdc/nfdc-batch.t.txt
@@ -0,0 +1,14 @@
+ "face" create "udp://192.0.2.1"
+
+route add / udp://192.0.2.2
+
+route \
+ add \
+ "/test2/foo bar" \
+ 'udp://192.0.2.3'
+
+# route test
+ ##### route test
+ # route test
+
+cs info
diff --git a/tools/nfdc/help.cpp b/tools/nfdc/help.cpp
index 88ebff2..3642a91 100644
--- a/tools/nfdc/help.cpp
+++ b/tools/nfdc/help.cpp
@@ -1,6 +1,6 @@
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
- * Copyright (c) 2014-2018, Regents of the University of California,
+ * Copyright (c) 2014-2021, Regents of the University of California,
* Arizona Board of Regents,
* Colorado State University,
* University Pierre & Marie Curie, Sorbonne University,
@@ -43,7 +43,7 @@
void
helpList(std::ostream& os, const CommandParser& parser, ParseMode mode, const std::string& noun)
{
- os << "nfdc [-h|--help] [-V|--version] <command> [<args>]\n\n";
+ os << "nfdc [-h|--help] [-V|--version] [-f|--batch <batch-file>] [<command> [<args>]]\n\n";
if (noun.empty()) {
os << "All subcommands:\n";
}
diff --git a/tools/nfdc/main.cpp b/tools/nfdc/main.cpp
index 5a7ce13..ebe9d3c 100644
--- a/tools/nfdc/main.cpp
+++ b/tools/nfdc/main.cpp
@@ -1,6 +1,6 @@
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
- * Copyright (c) 2014-2018, Regents of the University of California,
+ * Copyright (c) 2014-2021, Regents of the University of California,
* Arizona Board of Regents,
* Colorado State University,
* University Pierre & Marie Curie, Sorbonne University,
@@ -24,9 +24,11 @@
*/
#include "available-commands.hpp"
-#include "help.hpp"
#include "core/version.hpp"
+#include "help.hpp"
+#include <boost/tokenizer.hpp>
+#include <fstream>
#include <iostream>
namespace nfd {
@@ -51,26 +53,132 @@
return 0;
}
- std::string noun, verb;
- CommandArguments ca;
- ExecuteCommand execute;
- try {
- std::tie(noun, verb, ca, execute) = parser.parse(args, ParseMode::ONE_SHOT);
+ struct Command
+ {
+ std::string noun, verb;
+ CommandArguments ca;
+ ExecuteCommand execute;
+ };
+
+ auto processLine = [&parser] (const std::vector<std::string>& line) -> Command {
+ try {
+ Command cmd;
+ std::tie(cmd.noun, cmd.verb, cmd.ca, cmd.execute) = parser.parse(line, ParseMode::ONE_SHOT);
+ return cmd;
+ }
+ catch (const std::invalid_argument& e) {
+ int ret = help(std::cout, parser, line);
+ if (ret == 2)
+ std::cerr << e.what() << std::endl;
+ return {"", "", {}, nullptr};
+ }
+ };
+
+ std::list<Command> commands;
+
+ if (args[0] == "-f" || args[0] == "--batch") {
+ if (args.size() != 2) {
+ std::cerr << "ERROR: Invalid command line arguments: " << args[0] << " should follow with batch-file."
+ << " Use -h for more detail." << std::endl;
+ return 2;
+ }
+
+ auto processIstream = [&commands,&processLine] (std::istream& is, const std::string& inputFile) {
+ std::string line;
+ size_t lineCounter = 0;
+ while (std::getline(is, line)) {
+ ++lineCounter;
+
+ auto hasEscapeSlash = [] (const std::string& str) {
+ auto count = std::count(str.rbegin(), str.rend(), '\\');
+ return (count % 2) == 1;
+ };
+ while (!line.empty() && hasEscapeSlash(line)) {
+ std::string extraLine;
+ const auto& hasMore = std::getline(is, extraLine);
+ ++lineCounter;
+ line = line.substr(0, line.size() - 1) + extraLine;
+ if (!hasMore) {
+ break;
+ }
+ }
+ boost::tokenizer<boost::escaped_list_separator<char>> tokenizer(
+ line,
+ boost::escaped_list_separator<char>("\\", " \t", "\"'"));
+
+ auto firstNonEmptyToken = tokenizer.begin();
+ while (firstNonEmptyToken != tokenizer.end() && firstNonEmptyToken->empty()) {
+ ++firstNonEmptyToken;
+ }
+
+ // Ignore empty lines (or lines with just spaces) and lines that start with #
+ // Non empty lines with trailing comment are not allowed and may trigger syntax error
+ if (firstNonEmptyToken == tokenizer.end() || (*firstNonEmptyToken)[0] == '#') {
+ continue;
+ }
+
+ std::vector<std::string> lineArgs;
+ std::copy_if(firstNonEmptyToken, tokenizer.end(),
+ std::back_inserter<std::vector<std::string>>(lineArgs),
+ [] (const std::string& t) { return !t.empty(); });
+
+ auto cmd = processLine(lineArgs);
+ if (cmd.noun.empty()) {
+ std::cerr << " >> Syntax error on line " << lineCounter << " of the batch in "
+ << inputFile << std::endl;
+ return 2; // not exactly correct, but should be indication of an error, which already shown
+ }
+ commands.push_back(std::move(cmd));
+ }
+ return 0;
+ };
+
+ if (args[1] == "-") {
+ auto retval = processIstream(std::cin, "standard input");
+ if (retval != 0) {
+ return retval;
+ }
+ }
+ else {
+ std::ifstream iff(args[1]);
+ if (!iff) {
+ std::cerr << "ERROR: Could not open `" << args[1] << "` batch file "
+ << "(" << strerror(errno) << ")" << std::endl;
+ return 2;
+ }
+ auto retval = processIstream(iff, args[1]);
+ if (retval != 0) {
+ return retval;
+ }
+ }
}
- catch (const std::invalid_argument& e) {
- int ret = help(std::cout, parser, std::move(args));
- if (ret == 2)
- std::cerr << e.what() << std::endl;
- return ret;
+ else {
+ commands.push_back(processLine(args));
}
try {
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;
+ size_t commandCounter = 0;
+ for (auto& command : commands) {
+ ++commandCounter;
+ ExecuteContext ctx{command.noun, command.verb, command.ca, 0,
+ std::cout, std::cerr, face, keyChain, controller};
+ command.execute(ctx);
+
+ if (ctx.exitCode != 0) {
+ if (commands.size() > 1) {
+ std::cerr << " >> Failed to execute command on line " << commandCounter
+ << " of the batch file " << args[1] << std::endl;
+ std::cerr << " Note that nfdc has executed all commands on previous lines and "
+ << "stopped processing at this line" << std::endl;
+ }
+
+ return ctx.exitCode;
+ }
+ }
+ return 0;
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;