diff --git a/src/cli/README.md b/src/cli/README.md index f2e6d4cec..073cca4a0 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -5,6 +5,27 @@ command line interface. Use the CLI to play with OpenThread, which can also be used with additional application code. The OpenThread test scripts use the CLI to execute test cases. +## Separator and escaping characters + +The whitespace character (`' '`) is used to delimit the command name +and the different arguments, together with tab (`'\t'`) and new line +characters (`'\r'`, `'\n'`). + +Some arguments might require to accept whitespaces on them. For those +cases the backslash character (`'\'`) can be used to escape separators +or the backslash itself. + +Example: + +```bash +> networkname Test\ Network +Done +> networkname +Test Network +Done +> +``` + ## OpenThread Command List * [bufferinfo](#bufferinfo) diff --git a/src/core/utils/parse_cmdline.cpp b/src/core/utils/parse_cmdline.cpp index 9da77ee9f..132a6bf2a 100644 --- a/src/core/utils/parse_cmdline.cpp +++ b/src/core/utils/parse_cmdline.cpp @@ -31,6 +31,8 @@ * This file implements the command line parser. */ +#include + #include "parse_cmdline.hpp" #include "common/code_utils.hpp" @@ -38,11 +40,16 @@ namespace ot { namespace Utils { -static bool IsSpaceOrNewLine(char aChar) +static bool IsSeparator(char aChar) { return (aChar == ' ') || (aChar == '\t') || (aChar == '\r') || (aChar == '\n'); } +static bool IsEscapable(char aChar) +{ + return IsSeparator(aChar) || (aChar == '\\'); +} + otError CmdLineParser::ParseCmd(char *aString, uint8_t &aArgc, char *aArgv[], uint8_t aArgcMax) { otError error = OT_ERROR_NONE; @@ -50,21 +57,19 @@ otError CmdLineParser::ParseCmd(char *aString, uint8_t &aArgc, char *aArgv[], ui aArgc = 0; - for (cmd = aString; IsSpaceOrNewLine(*cmd) && *cmd; cmd++) - ; - - if (*cmd) + for (cmd = aString; *cmd; cmd++) { - aArgv[aArgc++] = cmd++; // the first argument - } - - for (; *cmd; cmd++) - { - if (IsSpaceOrNewLine(*cmd)) + if ((*cmd == '\\') && IsEscapable(*(cmd + 1))) + { + // include the null terminator: strlen(cmd) = strlen(cmd + 1) + 1 + memmove(cmd, cmd + 1, strlen(cmd)); + } + else if (IsSeparator(*cmd)) { *cmd = '\0'; } - else if (*(cmd - 1) == '\0') + + if ((*cmd != '\0') && ((aArgc == 0) || (*(cmd - 1) == '\0'))) { VerifyOrExit(aArgc < aArgcMax, error = OT_ERROR_INVALID_ARGS); aArgv[aArgc++] = cmd; diff --git a/src/core/utils/parse_cmdline.hpp b/src/core/utils/parse_cmdline.hpp index f2ec0025c..b9c0091aa 100644 --- a/src/core/utils/parse_cmdline.hpp +++ b/src/core/utils/parse_cmdline.hpp @@ -60,7 +60,8 @@ public: * This function parses the command line. * * Note: this method may change the input @p aString, it will put a '\0' by the end of each argument, - * and @p aArgv will point to the arguments in the input @p aString. + * and @p aArgv will point to the arguments in the input @p aString. Backslash ('\') can be used + * to escape separators (' ', '\t', '\r', '\n') and the backslash itself. * * @param[in] aString A NULL-terminated input string. * @param[out] aArgc The argument counter of the command line. diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index d04f8e5af..b1aa9ce6e 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -446,6 +446,20 @@ class Node: self.send_command(cmd) self._expect('Done') + def _escape_escapable(self, string): + """Escape CLI escapable characters in the given string. + + Args: + string (str): UTF-8 input string. + + Returns: + [str]: The modified string with escaped characters. + """ + escapable_chars = '\\ \t\r\n' + for char in escapable_chars: + string = string.replace(char, '\\%s' % char) + return string + def get_network_name(self): self.send_command('networkname') while True: @@ -457,7 +471,7 @@ class Node: return network_name def set_network_name(self, network_name): - cmd = 'networkname %s' % network_name + cmd = 'networkname %s' % self._escape_escapable(network_name) self.send_command(cmd) self._expect('Done') @@ -969,7 +983,7 @@ class Node: cmd += 'localprefix %s ' % mesh_local if network_name is not None: - cmd += 'networkname %s ' % network_name + cmd += 'networkname %s ' % self._escape_escapable(network_name) if binary is not None: cmd += 'binary %s ' % binary @@ -1011,7 +1025,7 @@ class Node: cmd += 'localprefix %s ' % mesh_local if network_name is not None: - cmd += 'networkname %s ' % network_name + cmd += 'networkname %s ' % self._escape_escapable(network_name) self.send_command(cmd) self._expect('Done') diff --git a/tools/harness-thci/OpenThread.py b/tools/harness-thci/OpenThread.py index 36ef14fea..7392ff2a6 100644 --- a/tools/harness-thci/OpenThread.py +++ b/tools/harness-thci/OpenThread.py @@ -626,6 +626,20 @@ class OpenThread(IThci): print('%s call getCommissionerSessionId' % self.port) return self.__sendCommand('commissioner sessionid')[0] + def __escapeEscapable(self, string): + """Escape CLI escapable characters in the given string. + + Args: + string (str): UTF-8 input string. + + Returns: + [str]: The modified string with escaped characters. + """ + escapable_chars = '\\ \t\r\n' + for char in escapable_chars: + string = string.replace(char, '\\%s' % char) + return string + def _connect(self): print('My port is %s' % self.port) if self.port.startswith('COM'): @@ -688,6 +702,7 @@ class OpenThread(IThci): """ print('%s call setNetworkName' % self.port) print(networkName) + networkName = self.__escapeEscapable(networkName) try: cmd = 'networkname %s' % networkName datasetCmd = 'dataset networkname %s' % networkName @@ -2393,7 +2408,7 @@ class OpenThread(IThci): if sNetworkName is not None: cmd += ' networkname ' - cmd += str(sNetworkName) + cmd += self.__escapeEscapable(str(sNetworkName)) if xChannel is not None: cmd += ' channel ' @@ -2594,7 +2609,7 @@ class OpenThread(IThci): if sNetworkName is not None: cmd += ' networkname ' - cmd += str(sNetworkName) + cmd += self.__escapeEscapable(str(sNetworkName)) if xCommissionerSessionId is not None: cmd += ' binary '