Skip to content

Commit 1586044

Browse files
committed
[Qt] RPC-Console: support nested commands and simple value queries
Commands can be executed with bracket syntax, example: `getwalletinfo()`. Commands can be nested, example: `sendtoaddress(getnewaddress(), 10)`. Simple queries are possible: `listunspent()[0][txid]` Object values are accessed with a non-quoted string, example: [txid]. Fully backward compatible. `generate 101` is identical to `generate(101)` Result value queries indicated with `[]` require the new brackets syntax. Comma as argument separator is now also possible: `sendtoaddress,<address>,<amount>` Space as argument separator works also with the bracket syntax, example: `sendtoaddress(getnewaddress() 10) No dept limitation, complex commands are possible: `decoderawtransaction(getrawtransaction(getblock(getbestblockhash())[tx][0]))[vout][0][value]`
1 parent 41d8e78 commit 1586044

File tree

6 files changed

+300
-71
lines changed

6 files changed

+300
-71
lines changed

src/Makefile.qttest.include

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
bin_PROGRAMS += qt/test/test_bitcoin-qt
22
TESTS += qt/test/test_bitcoin-qt
33

4-
TEST_QT_MOC_CPP = qt/test/moc_uritests.cpp
4+
TEST_QT_MOC_CPP = \
5+
qt/test/moc_rpcnestedtests.cpp \
6+
qt/test/moc_uritests.cpp
57

68
if ENABLE_WALLET
79
TEST_QT_MOC_CPP += qt/test/moc_paymentservertests.cpp
810
endif
911

1012
TEST_QT_H = \
13+
qt/test/rpcnestedtests.h \
1114
qt/test/uritests.h \
1215
qt/test/paymentrequestdata.h \
1316
qt/test/paymentservertests.h
@@ -16,6 +19,7 @@ qt_test_test_bitcoin_qt_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BITCOIN_
1619
$(QT_INCLUDES) $(QT_TEST_INCLUDES) $(PROTOBUF_CFLAGS)
1720

1821
qt_test_test_bitcoin_qt_SOURCES = \
22+
qt/test/rpcnestedtests.cpp \
1923
qt/test/test_main.cpp \
2024
qt/test/uritests.cpp \
2125
$(TEST_QT_H)

src/qt/rpcconsole.cpp

Lines changed: 160 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -113,117 +113,208 @@ class QtRPCTimerInterface: public RPCTimerInterface
113113
#include "rpcconsole.moc"
114114

115115
/**
116-
* Split shell command line into a list of arguments. Aims to emulate \c bash and friends.
116+
* Split shell command line into a list of arguments and execute the command(s).
117+
* Aims to emulate \c bash and friends.
117118
*
118-
* - Arguments are delimited with whitespace
119+
* - Command nesting is possible with brackets [example: validateaddress(getnewaddress())]
120+
* - Arguments are delimited with whitespace or comma
119121
* - Extra whitespace at the beginning and end and between arguments will be ignored
120122
* - Text can be "double" or 'single' quoted
121123
* - The backslash \c \ is used as escape character
122124
* - Outside quotes, any character can be escaped
123125
* - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
124126
* - Within single quotes, no escaping is possible and no special interpretation takes place
125127
*
126-
* @param[out] args Parsed arguments will be appended to this list
128+
* @param[out] result stringified Result from the executed command(chain)
127129
* @param[in] strCommand Command line to split
128130
*/
129-
bool parseCommandLine(std::vector<std::string> &args, const std::string &strCommand)
131+
132+
bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand)
130133
{
134+
std::vector< std::vector<std::string> > stack;
135+
stack.push_back(std::vector<std::string>());
136+
131137
enum CmdParseState
132138
{
133139
STATE_EATING_SPACES,
134140
STATE_ARGUMENT,
135141
STATE_SINGLEQUOTED,
136142
STATE_DOUBLEQUOTED,
137143
STATE_ESCAPE_OUTER,
138-
STATE_ESCAPE_DOUBLEQUOTED
144+
STATE_ESCAPE_DOUBLEQUOTED,
145+
STATE_COMMAND_EXECUTED,
146+
STATE_COMMAND_EXECUTED_INNER
139147
} state = STATE_EATING_SPACES;
140148
std::string curarg;
141-
Q_FOREACH(char ch, strCommand)
149+
UniValue lastResult;
150+
151+
std::string strCommandTerminated = strCommand;
152+
if (strCommandTerminated.back() != '\n')
153+
strCommandTerminated += "\n";
154+
for(char ch: strCommandTerminated)
142155
{
143156
switch(state)
144157
{
145-
case STATE_ARGUMENT: // In or after argument
146-
case STATE_EATING_SPACES: // Handle runs of whitespace
147-
switch(ch)
158+
case STATE_COMMAND_EXECUTED_INNER:
159+
case STATE_COMMAND_EXECUTED:
148160
{
149-
case '"': state = STATE_DOUBLEQUOTED; break;
150-
case '\'': state = STATE_SINGLEQUOTED; break;
151-
case '\\': state = STATE_ESCAPE_OUTER; break;
152-
case ' ': case '\n': case '\t':
153-
if(state == STATE_ARGUMENT) // Space ends argument
161+
bool breakParsing = true;
162+
switch(ch)
154163
{
155-
args.push_back(curarg);
156-
curarg.clear();
164+
case '[': curarg.clear(); state = STATE_COMMAND_EXECUTED_INNER; break;
165+
default:
166+
if (state == STATE_COMMAND_EXECUTED_INNER)
167+
{
168+
if (ch != ']')
169+
{
170+
// append char to the current argument (which is also used for the query command)
171+
curarg += ch;
172+
break;
173+
}
174+
if (curarg.size())
175+
{
176+
// if we have a value query, query arrays with index and objects with a string key
177+
UniValue subelement;
178+
if (lastResult.isArray())
179+
{
180+
for(char argch: curarg)
181+
if (!std::isdigit(argch))
182+
throw std::runtime_error("Invalid result query");
183+
subelement = lastResult[atoi(curarg.c_str())];
184+
}
185+
else if (lastResult.isObject())
186+
subelement = find_value(lastResult, curarg);
187+
else
188+
throw std::runtime_error("Invalid result query"); //no array or object: abort
189+
lastResult = subelement;
190+
}
191+
192+
state = STATE_COMMAND_EXECUTED;
193+
break;
194+
}
195+
// don't break parsing when the char is required for the next argument
196+
breakParsing = false;
197+
198+
// pop the stack and return the result to the current command arguments
199+
stack.pop_back();
200+
201+
// don't stringify the json in case of a string to avoid doublequotes
202+
if (lastResult.isStr())
203+
curarg = lastResult.get_str();
204+
else
205+
curarg = lastResult.write(2);
206+
207+
// if we have a non empty result, use it as stack argument otherwise as general result
208+
if (curarg.size())
209+
{
210+
if (stack.size())
211+
stack.back().push_back(curarg);
212+
else
213+
strResult = curarg;
214+
}
215+
curarg.clear();
216+
// assume eating space state
217+
state = STATE_EATING_SPACES;
157218
}
158-
state = STATE_EATING_SPACES;
159-
break;
160-
default: curarg += ch; state = STATE_ARGUMENT;
219+
if (breakParsing)
220+
break;
161221
}
162-
break;
163-
case STATE_SINGLEQUOTED: // Single-quoted string
164-
switch(ch)
222+
case STATE_ARGUMENT: // In or after argument
223+
case STATE_EATING_SPACES: // Handle runs of whitespace
224+
switch(ch)
165225
{
166-
case '\'': state = STATE_ARGUMENT; break;
167-
default: curarg += ch;
226+
case '"': state = STATE_DOUBLEQUOTED; break;
227+
case '\'': state = STATE_SINGLEQUOTED; break;
228+
case '\\': state = STATE_ESCAPE_OUTER; break;
229+
case '(': case ')': case '\n':
230+
if (state == STATE_ARGUMENT)
231+
{
232+
if (ch == '(' && stack.size() && stack.back().size() > 0)
233+
stack.push_back(std::vector<std::string>());
234+
if (curarg.size())
235+
{
236+
// don't allow commands after executed commands on baselevel
237+
if (!stack.size())
238+
throw std::runtime_error("Invalid Syntax");
239+
stack.back().push_back(curarg);
240+
}
241+
curarg.clear();
242+
state = STATE_EATING_SPACES;
243+
}
244+
if ((ch == ')' || ch == '\n') && stack.size() > 0)
245+
{
246+
std::string strPrint;
247+
// Convert argument list to JSON objects in method-dependent way,
248+
// and pass it along with the method name to the dispatcher.
249+
lastResult = tableRPC.execute(stack.back()[0], RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end())));
250+
251+
state = STATE_COMMAND_EXECUTED;
252+
curarg.clear();
253+
}
254+
break;
255+
case ' ': case ',': case '\t':
256+
if(state == STATE_ARGUMENT) // Space ends argument
257+
{
258+
if (curarg.size())
259+
stack.back().push_back(curarg);
260+
curarg.clear();
261+
}
262+
state = STATE_EATING_SPACES;
263+
break;
264+
default: curarg += ch; state = STATE_ARGUMENT;
168265
}
169-
break;
170-
case STATE_DOUBLEQUOTED: // Double-quoted string
171-
switch(ch)
266+
break;
267+
case STATE_SINGLEQUOTED: // Single-quoted string
268+
switch(ch)
172269
{
173-
case '"': state = STATE_ARGUMENT; break;
174-
case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
175-
default: curarg += ch;
270+
case '\'': state = STATE_ARGUMENT; break;
271+
default: curarg += ch;
176272
}
177-
break;
178-
case STATE_ESCAPE_OUTER: // '\' outside quotes
179-
curarg += ch; state = STATE_ARGUMENT;
180-
break;
181-
case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
182-
if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
183-
curarg += ch; state = STATE_DOUBLEQUOTED;
184-
break;
273+
break;
274+
case STATE_DOUBLEQUOTED: // Double-quoted string
275+
switch(ch)
276+
{
277+
case '"': state = STATE_ARGUMENT; break;
278+
case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
279+
default: curarg += ch;
280+
}
281+
break;
282+
case STATE_ESCAPE_OUTER: // '\' outside quotes
283+
curarg += ch; state = STATE_ARGUMENT;
284+
break;
285+
case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
286+
if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
287+
curarg += ch; state = STATE_DOUBLEQUOTED;
288+
break;
185289
}
186290
}
187291
switch(state) // final state
188292
{
189-
case STATE_EATING_SPACES:
190-
return true;
191-
case STATE_ARGUMENT:
192-
args.push_back(curarg);
193-
return true;
194-
default: // ERROR to end in one of the other states
195-
return false;
293+
case STATE_COMMAND_EXECUTED:
294+
if (lastResult.isStr())
295+
strResult = lastResult.get_str();
296+
else
297+
strResult = lastResult.write(2);
298+
case STATE_ARGUMENT:
299+
case STATE_EATING_SPACES:
300+
return true;
301+
default: // ERROR to end in one of the other states
302+
return false;
196303
}
197304
}
198305

199306
void RPCExecutor::request(const QString &command)
200307
{
201-
std::vector<std::string> args;
202-
if(!parseCommandLine(args, command.toStdString()))
203-
{
204-
Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
205-
return;
206-
}
207-
if(args.empty())
208-
return; // Nothing to do
209308
try
210309
{
211-
std::string strPrint;
212-
// Convert argument list to JSON objects in method-dependent way,
213-
// and pass it along with the method name to the dispatcher.
214-
UniValue result = tableRPC.execute(
215-
args[0],
216-
RPCConvertValues(args[0], std::vector<std::string>(args.begin() + 1, args.end())));
217-
218-
// Format result reply
219-
if (result.isNull())
220-
strPrint = "";
221-
else if (result.isStr())
222-
strPrint = result.get_str();
223-
else
224-
strPrint = result.write(2);
225-
226-
Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(strPrint));
310+
std::string result;
311+
std::string executableCommand = command.toStdString() + "\n";
312+
if(!RPCConsole::RPCExecuteCommandLine(result, executableCommand))
313+
{
314+
Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
315+
return;
316+
}
317+
Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result));
227318
}
228319
catch (UniValue& objError)
229320
{

src/qt/rpcconsole.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class RPCConsole: public QWidget
3535
explicit RPCConsole(const PlatformStyle *platformStyle, QWidget *parent);
3636
~RPCConsole();
3737

38+
static bool RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand);
39+
3840
void setClientModel(ClientModel *model);
3941

4042
enum MessageClass {

0 commit comments

Comments
 (0)