Line data Source code
1 : // Copyright (c) 2012-2022 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/eventdispatcher
4 : // contact@m2osw.com
5 : //
6 : // This program is free software; you can redistribute it and/or modify
7 : // it under the terms of the GNU General Public License as published by
8 : // the Free Software Foundation; either version 2 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program is distributed in the hope that it will be useful,
12 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : // GNU General Public License for more details.
15 : //
16 : // You should have received a copy of the GNU General Public License along
17 : // with this program; if not, write to the Free Software Foundation, Inc.,
18 : // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 : #pragma once
20 :
21 : /** \file
22 : * \brief Event dispatch class.
23 : *
24 : * Class used to handle events.
25 : */
26 :
27 :
28 : // self
29 : //
30 : #include "eventdispatcher/dispatcher_base.h"
31 : #include "eventdispatcher/communicator.h"
32 : #include "eventdispatcher/utils.h"
33 :
34 :
35 : // snaplogger lib
36 : //
37 : #include <snaplogger/message.h>
38 :
39 :
40 :
41 : namespace ed
42 : {
43 :
44 :
45 :
46 : /** \brief A template to create a list of messages to dispatch on receival.
47 : *
48 : * Whenever you receive messages, they can automatically get dispatched to
49 : * various functions using the dispatcher.
50 : *
51 : * You define a dispatcher_match array and then add a dispatcher to
52 : * your connection object.
53 : *
54 : * \code
55 : * ed::dispatcher<my_connection>::dispatcher_match const my_messages[] =
56 : * {
57 : * {
58 : * "HELP"
59 : * , &my_connection::msg_help
60 : * //, &dispatcher<my_connection>::dispatcher_match::one_to_one_match -- use default
61 : * },
62 : * {
63 : * "STATUS"
64 : * , &my_connection::msg_status
65 : * //, &dispatcher<my_connection>::dispatcher_match::one_to_one_match -- use default
66 : * },
67 : * ... // other messages
68 : *
69 : * // if you'd like you can end your list with a catch all which
70 : * // generate the UNKNOWN message with the following (not required)
71 : * // if you have that entry, your own process_message() function
72 : * // will not get called
73 : * {
74 : * nullptr
75 : * , &dispatcher<my_connection>::dispatcher_match::msg_reply_with_unknown
76 : * , &dispatcher<my_connection>::dispatcher_match::always_match
77 : * },
78 : * };
79 : * \endcode
80 : *
81 : * In most cases you do not need to specify the matching function. It will
82 : * use the default which is a one to one match. So in the example above,
83 : * for "HELP", only a message with the command set to "HELP" will match.
84 : * When a match is found, the corresponding function (msg_help() here)
85 : * gets called.
86 : *
87 : * Note that "functions" are actually offsets. You will get `this` defined
88 : * as expected when your function gets called. The one drawback is that
89 : * only a function of the connection you attach the dispatcher to can
90 : * be called from the dispatcher. This is because we want to have a
91 : * static table instead of a dynamic one created each time we start
92 : * a process (in a website server, many processes get started again and
93 : * again.)
94 : *
95 : * \note
96 : * The T parameter of this template is often referenced as a "connection"
97 : * because it is expected to be a connection. There is actually no such
98 : * constraint on that object. It just needs to understand the dispatcher
99 : * usage which is to call the dispatch() function whenever a message is
100 : * received. Also it needs to implement any f_execute() function as
101 : * defined in its dispatch_match vector.
102 : *
103 : * \note
104 : * This is documented here because it is a template and we cannot do
105 : * that in the .cpp (at least older versions of doxygen could not.)
106 : *
107 : * \todo
108 : * Transform the dispatcher_match with classes so we can build the
109 : * array safely.
110 : *
111 : * \tparam T The connection class to be used with this dispathcer.
112 : */
113 : template<typename T>
114 8 : class dispatcher
115 : : public dispatcher_base
116 : {
117 : public:
118 : /** \brief A smart pointer of the dispatcher.
119 : *
120 : * Although we expect the array of `dispatcher_match` to be
121 : * statically defined, the `dispatcher`, on the other hand,
122 : * is quite dynamic and needs to be allocated in a smart
123 : * pointer then added to your connection.
124 : */
125 : typedef std::shared_ptr<dispatcher> pointer_t;
126 :
127 : /** \brief This structure is used to define the list of supported messages.
128 : *
129 : * Whenever you create an array of messages, you use this structure.
130 : *
131 : * The structure takes a few parameters as follow:
132 : *
133 : * \li f_expr -- the "expression" to be matched to the command name
134 : * for example "HELP".
135 : * \li f_execute -- the function offset to execute on a match.
136 : * \li f_match -- the function to check whether the expression is a match;
137 : * it has a default of one_to_one_match() which means the
138 : * f_expr string is viewed as a plain string defining the
139 : * message name as is.
140 : *
141 : * The command name is called "f_expr" but some matching functions may
142 : * make use of the "f_expr" parameter as an expression such as a
143 : * regular expression. Such functions will be added here with time, you
144 : * may also have your own, of course. The match function is expected to
145 : * be a static or standalone function.
146 : */
147 : struct dispatcher_match
148 : {
149 : /** \brief Define a vector of dispatcher_match objects.
150 : *
151 : * This function includes an array of dispatcher_match objects.
152 : * Whenever you define dispatcher_match objects, you want to
153 : * use the C++11 syntax to create a vector.
154 : *
155 : * \attention
156 : * We are NOT using a match because the matching may make use
157 : * of complex functions that support things such as complex as
158 : * regular expressions. In other words, the name of the message
159 : * may not just be a simple string.
160 : */
161 : typedef std::vector<dispatcher_match> vector_t;
162 :
163 : /** \brief The execution function.
164 : *
165 : * This type defines the execution function. We give it the message
166 : * on a match. If the command name is not a match, it is ignored.
167 : */
168 : typedef void (T::*execute_func_t)(message & msg);
169 :
170 : /** \brief The match function return types.
171 : *
172 : * Whenever a match function is called, it may return one of:
173 : *
174 : * \li MATCH_FALSE
175 : *
176 : * The function did not match anything. Ignore the corresponding
177 : * function.
178 : *
179 : * \li MATCH_TRUE
180 : *
181 : * This is a match, execute the function. We are done with this list
182 : * of matches.
183 : *
184 : * \li MATCH_CALLBACK
185 : *
186 : * The function is a callback, it gets called and the process
187 : * continues. Since the message parameter is read/write, it is
188 : * a way to tweak the message before other functions get it.
189 : */
190 : enum class match_t
191 : {
192 : MATCH_FALSE,
193 : MATCH_TRUE,
194 : MATCH_CALLBACK
195 : };
196 :
197 : /** \brief The match function.
198 : *
199 : * This type defines the match function. We give it the message
200 : * which has the command name, although specialized matching
201 : * function could test other parameters from the message such
202 : * as the origination of the message.
203 : */
204 : typedef match_t (*match_func_t)(std::string const & expr, message & msg);
205 :
206 : /** \brief The default matching function.
207 : *
208 : * This function checks the command one to one to the expression.
209 : * The word in the expression is compared as is to the command
210 : * name:
211 : *
212 : * \code
213 : * return expr == msg.get_command();
214 : * \endcode
215 : *
216 : * We will add other matching functions with time
217 : * (start_with_match(), regex_match(), etc.)
218 : *
219 : * \note
220 : * It is permissible to use a match function to modify the
221 : * message in some way, however, it is not recommended.
222 : *
223 : * \param[in] expr The expression to compare the command against.
224 : * \param[in] msg The message to match against this expression.
225 : *
226 : * \return MATCH_TRUE if it is a match, MATCH_FALSE otherwise.
227 : */
228 8 : static match_t one_to_one_match(std::string const & expr, message & msg)
229 : {
230 8 : return expr == msg.get_command()
231 8 : ? match_t::MATCH_TRUE
232 8 : : match_t::MATCH_FALSE;
233 : }
234 :
235 : /** \brief Always returns true.
236 : *
237 : * This function always returns true. This is practical to close
238 : * your list of messages and return a specific message. In most
239 : * cases this is used to reply with the UNKNOWN message.
240 : *
241 : * \param[in] expr The expression to compare the command against.
242 : * \param[in] msg The message to match against this expression.
243 : *
244 : * \return Always returns MATCH_TRUE.
245 : */
246 0 : static match_t always_match(std::string const & expr, message & msg)
247 : {
248 0 : snapdev::NOT_USED(expr, msg);
249 0 : return match_t::MATCH_TRUE;
250 : }
251 :
252 : /** \brief Always returns true.
253 : *
254 : * This function always returns true. This is practical to close
255 : * your list of messages and return a specific message. In most
256 : * cases this is used to reply with the UNKNOWN message.
257 : *
258 : * \param[in] expr The expression to compare the command against.
259 : * \param[in] msg The message to match against this expression.
260 : *
261 : * \return Always returns MATCH_CALLBACK.
262 : */
263 0 : static match_t callback_match(std::string const & expr, message & msg)
264 : {
265 0 : snapdev::NOT_USED(expr, msg);
266 0 : return match_t::MATCH_CALLBACK;
267 : }
268 :
269 : /** \brief The expression to compare against.
270 : *
271 : * The expression is most often going to be the exact command name
272 : * which will be matched with the one_to_one_match() function.
273 : *
274 : * For other match functions, this would be whatever type of
275 : * expression supported by those other functions.
276 : *
277 : * \note
278 : * Effective C++ doesn't like bare pointers, but there is no real
279 : * reason for us to waste time and memory by having an std::string
280 : * here. It's going to always be a constant pointer anyway.
281 : */
282 : char const * f_expr = nullptr;
283 :
284 : /** \brief The execute function.
285 : *
286 : * This is an offset in your connection class. We do not allow
287 : * std::bind() because we do not want the array of messages to be
288 : * dynamic (that way it is created at compile time and loaded as
289 : * ready/prepared data on load.)
290 : *
291 : * The functions called have `this` defined so you can access
292 : * your connection data and other functions. It requires the
293 : * `&` and the class name to define the pointer, like this:
294 : *
295 : * \code
296 : * &MyClass::my_message_function
297 : * \endcode
298 : *
299 : * The execution is started by calling the run() function.
300 : */
301 : execute_func_t f_execute = nullptr;
302 :
303 : /** \brief The match function.
304 : *
305 : * The match function is used to know whether that command
306 : * dispatch function was found.
307 : *
308 : * By default this parameter is set to one_to_one_match().
309 : * This means the command has to be one to one equal to
310 : * the f_expr string.
311 : *
312 : * The matching is done in the match() function.
313 : */
314 : match_func_t f_match = &::ed::dispatcher<T>::dispatcher_match::one_to_one_match;
315 :
316 : /** \brief Run the execution function if this is a match.
317 : *
318 : * First this function checks whether the command of the message
319 : * in \p msg matches this `dispatcher_match` expression. In
320 : * most cases the match function is going to be
321 : * one_on_one_match() which means it has to be exactly equal.
322 : *
323 : * If it is a match, this function runs your \p connection execution
324 : * function (i.e. the message gets dispatched) and then it returns
325 : * true.
326 : *
327 : * If the message is not a match, then the function returns false
328 : * and only the matching function was called. In this case the
329 : * \p connection does not get used.
330 : *
331 : * When this function returns true, you should not call the
332 : * process_message() function since that was already taken care
333 : * of. The process_message() function should only be called
334 : * if the message was not yet dispatched. When the list of
335 : * matches includes a catch all at the end, the process_message()
336 : * will never be called.
337 : *
338 : * \note
339 : * Note that we do not offer two functions, one to run the match
340 : * function and one to execute the match because you could otherwise
341 : * end up calling the execute function (dispatch) on any
342 : * `dispatcher_match` entry and we did not want that to happen.
343 : *
344 : * \param[in] connection The connection attached to that
345 : * `dispatcher_match`.
346 : * \param[in] msg The message that matched.
347 : *
348 : * \return true if the connection execute function was called.
349 : */
350 8 : bool execute(T * connection, ::ed::message & msg) const
351 : {
352 8 : match_t m(f_match(f_expr == nullptr ? std::string() : f_expr, msg));
353 8 : if(m == match_t::MATCH_TRUE
354 2 : || m == match_t::MATCH_CALLBACK)
355 : {
356 6 : (connection->*f_execute)(msg);
357 6 : if(m == match_t::MATCH_TRUE)
358 : {
359 6 : return true;
360 : }
361 : }
362 :
363 2 : return false;
364 : }
365 :
366 : /** \brief Check whether f_match is one_to_one_match().
367 : *
368 : * This function checks whether the f_match function was defined
369 : * to one_to_one_match()--the default--and if so returns true.
370 : *
371 : * \return true if f_match is the one_to_one_match() function.
372 : */
373 0 : bool match_is_one_to_one_match() const
374 : {
375 0 : return f_match == &ed::dispatcher<T>::dispatcher_match::one_to_one_match;
376 : }
377 :
378 : /** \brief Check whether f_match is always_match().
379 : *
380 : * This function checks whether the f_match function was defined
381 : * to always_match() and if so returns true.
382 : *
383 : * \return true if f_match is the always_match() function.
384 : */
385 0 : bool match_is_always_match() const
386 : {
387 0 : return f_match == &ed::dispatcher<T>::dispatcher_match::always_match;
388 : }
389 :
390 : /** \brief Check whether f_match is callback_match().
391 : *
392 : * This function checks whether the f_match function was defined
393 : * to callback_match() and if so returns true.
394 : *
395 : * \return true if f_match is the callback_match() function.
396 : */
397 0 : bool match_is_callback_match() const
398 : {
399 0 : return f_match == &ed::dispatcher<T>::dispatcher_match::callback_match;
400 : }
401 : };
402 :
403 : private:
404 : /** \brief The connection pointer.
405 : *
406 : * This parameter is set by the constructor. It represents the
407 : * connection this dispatcher was added to (a form of parent of
408 : * this dispatcher object.)
409 : */
410 : T * f_connection = nullptr;
411 :
412 : /** \brief The array of possible matches.
413 : *
414 : * This is the vector of your messages with the corresponding
415 : * match and execute functions. This is used to go through
416 : * the matches and execute (dispatch) as required.
417 : */
418 : typename ed::dispatcher<T>::dispatcher_match::vector_t f_matches = {};
419 :
420 : /** \brief Tell whether messages should be traced or not.
421 : *
422 : * Because your service may accept and send many messages a full
423 : * trace on all of them can really be resource intensive. By default
424 : * the system will not trace anything. By setting this parameter to
425 : * true (call set_trace() for that) you request the SNAP_LOG_TRACE()
426 : * to run on each message received by this dispatcher. This is done
427 : * on entry so whether the message is processed by the dispatcher
428 : * or your own send_message() function, it will trace that message.
429 : */
430 : bool f_trace = false;
431 :
432 : public:
433 :
434 : /** \brief Initialize the dispatcher with your connection and messages.
435 : *
436 : * This function takes a pointer to your connection and an array
437 : * of matches.
438 : *
439 : * Whenever a message is received by one of your connections, the
440 : * dispatch() function gets called which checks the message against
441 : * each entry in this array of \p matches.
442 : *
443 : * \param[in] connection The connection for which this dispatcher is
444 : * created.
445 : * \param[in] matches The array of dispatch keywords and functions.
446 : */
447 4 : dispatcher<T>(T * connection, typename ed::dispatcher<T>::dispatcher_match::vector_t matches)
448 : : f_connection(connection)
449 4 : , f_matches(matches)
450 : {
451 4 : }
452 :
453 : // prevent copies
454 : dispatcher<T>(dispatcher<T> const &) = delete;
455 : dispatcher<T> & operator = (dispatcher<T> const &) = delete;
456 :
457 : /** \brief Add a default array of possible matches.
458 : *
459 : * In Snap! a certain number of messages are always exactly the same
460 : * and these can be implemented internally so each daemon doesn't have
461 : * to duplicate that work over and over again. These are there in part
462 : * because the snapcommunicator expects those messages there.
463 : *
464 : * IMPORTANT NOTE: If you add your own version in your dispatcher_match
465 : * vector, then these will be ignored since your version will match first
466 : * and the dispatcher uses the first function only.
467 : *
468 : * This array currently includes:
469 : *
470 : * \li HELP -- msg_help() -- returns the list of all the messages
471 : * \li LOG -- msg_log() -- reconfigure() the logger
472 : * \li QUITTING -- msg_quitting() -- calls stop(true);
473 : * \li READY -- msg_ready() -- calls ready() -- snapcommunicator always
474 : * sends that message so it has to be supported
475 : * \li RESTART -- msg_restart() -- calls restart() -- it is triggered
476 : * when a restart is required (i.e. the library was
477 : * upgraded, a configuration file was updated, etc.)
478 : * \li STOP -- msg_stop() -- calls stop(false);
479 : * \li UNKNOWN -- msg_log_unknown() -- in case we receive a message we
480 : * don't understand
481 : * \li * -- msg_reply_with_unknown() -- the last entry will be a grab
482 : * all pattern which returns the UNKNOWN message automatically
483 : * for you
484 : *
485 : * The msg_...() functions must be declared in your class T. If you
486 : * use the system connection_with_send_message class then they're
487 : * already defined there.
488 : *
489 : * The HELP response is automatically built from the f_matches.f_expr
490 : * strings. However, if the function used to match the expression is
491 : * not one_to_one_match(), then that string doesn't get used.
492 : *
493 : * If any message can't be determine (i.e. the function is not the
494 : * one_to_one_match()) then the user help() function gets called and
495 : * we expect that function to add any dynamic message the daemon
496 : * understands.
497 : *
498 : * The LOG message reconfigures the logger if the is_configure() function
499 : * says it is configured. In any other circumstances, nothing happens.
500 : *
501 : * Note that the UNKNOWN message is understood and just logs the message
502 : * received. This allows us to see that WE sent a message that the receiver
503 : * (not us) does not understand and adjust our code accordingly (i.e. add
504 : * support for that message in that receiver or maybe fix the spelling.)
505 : */
506 0 : void add_communicator_commands()
507 : {
508 : // avoid more than one realloc()
509 : //
510 0 : f_matches.reserve(f_matches.size() + 10);
511 :
512 : {
513 0 : typename ed::dispatcher<T>::dispatcher_match m;
514 0 : m.f_expr = "ALIVE";
515 0 : m.f_execute = &T::msg_alive;
516 : //m.f_match = <default>;
517 0 : f_matches.push_back(m);
518 : }
519 : {
520 0 : typename ed::dispatcher<T>::dispatcher_match m;
521 0 : m.f_expr = "HELP";
522 0 : m.f_execute = &T::msg_help;
523 : //m.f_match = <default>;
524 0 : f_matches.push_back(m);
525 : }
526 : {
527 0 : typename ed::dispatcher<T>::dispatcher_match m;
528 0 : m.f_expr = "LEAK";
529 0 : m.f_execute = &T::msg_leak;
530 : //m.f_match = <default>;
531 0 : f_matches.push_back(m);
532 : }
533 : {
534 0 : typename ed::dispatcher<T>::dispatcher_match m;
535 0 : m.f_expr = "LOG";
536 0 : m.f_execute = &T::msg_log;
537 : //m.f_match = <default>;
538 0 : f_matches.push_back(m);
539 : }
540 : {
541 0 : typename ed::dispatcher<T>::dispatcher_match m;
542 0 : m.f_expr = "QUITTING";
543 0 : m.f_execute = &T::msg_quitting;
544 : //m.f_match = <default>;
545 0 : f_matches.push_back(m);
546 : }
547 : {
548 0 : typename ed::dispatcher<T>::dispatcher_match m;
549 0 : m.f_expr = "READY";
550 0 : m.f_execute = &T::msg_ready;
551 : //m.f_match = <default>;
552 0 : f_matches.push_back(m);
553 : }
554 : {
555 0 : typename ed::dispatcher<T>::dispatcher_match m;
556 0 : m.f_expr = "RESTART";
557 0 : m.f_execute = &T::msg_restart;
558 : //m.f_match = <default>;
559 0 : f_matches.push_back(m);
560 : }
561 : {
562 0 : typename ed::dispatcher<T>::dispatcher_match m;
563 0 : m.f_expr = "STOP";
564 0 : m.f_execute = &T::msg_stop;
565 : //m.f_match = <default>;
566 0 : f_matches.push_back(m);
567 : }
568 : {
569 0 : typename ed::dispatcher<T>::dispatcher_match m;
570 0 : m.f_expr = "UNKNOWN";
571 0 : m.f_execute = &T::msg_log_unknown;
572 : //m.f_match = <default>;
573 0 : f_matches.push_back(m);
574 : }
575 : {
576 0 : typename ed::dispatcher<T>::dispatcher_match m;
577 0 : m.f_expr = nullptr; // any other message
578 0 : m.f_execute = &T::msg_reply_with_unknown;
579 0 : m.f_match = &ed::dispatcher<T>::dispatcher_match::always_match;
580 0 : f_matches.push_back(m);
581 : }
582 0 : }
583 :
584 : typename ed::dispatcher<T>::dispatcher_match::vector_t const & get_matches() const
585 : {
586 : return f_matches;
587 : }
588 :
589 : /** \brief The dispatch function.
590 : *
591 : * This is the function your message system will call whenever
592 : * the system receives a message.
593 : *
594 : * The function returns true if the message was dispatched.
595 : * When that happen, the process_message() function of the
596 : * connection should not be called.
597 : *
598 : * You may not include a message in the array of `dispatch_match`
599 : * if it is too complicated to match or too many variables are
600 : * necessary then you will probably want to use your
601 : * process_message().
602 : *
603 : * By adding a catch-all at the end of your list of matches, you
604 : * can easily have one function called for any message. By default
605 : * the dispatcher environment offers such a match function and
606 : * it also includes a function that sends the UNKNOWN message as
607 : * an immediate reply to a received message.
608 : *
609 : * \param[in] msg The message to be dispatched.
610 : *
611 : * \return true if the message was dispatched, false otherwise.
612 : */
613 6 : virtual bool dispatch(::ed::message & msg) override
614 : {
615 6 : if(f_trace)
616 : {
617 6 : SNAP_LOG_TRACE
618 : << "dispatch message \""
619 : << msg.to_message()
620 : << "\"."
621 : << SNAP_LOG_SEND;
622 : }
623 :
624 : // go in order to execute matches
625 : //
626 : // remember that a dispatcher with just a set of well defined command
627 : // names is a special case (albeit frequent) and we can't process
628 : // using a map (a.k.a. fast binary search) as a consequence
629 : //
630 8 : for(auto const & m : f_matches)
631 : {
632 8 : if(m.execute(f_connection, msg))
633 : {
634 6 : return true;
635 : }
636 : }
637 :
638 0 : return false;
639 : }
640 :
641 : /** \brief Set whether the dispatcher should trace your messages or not.
642 : *
643 : * By default, the f_trace flag is set to false. You can change it to
644 : * true while debugging. You should remember to turn it back off once
645 : * you make an official version of your service to avoid the possibly
646 : * huge overhead of sending all those log messages. One way to do so
647 : * is to place the code within \#ifdef/\#endif as in:
648 : *
649 : * \code
650 : * #ifdef _DEBUG
651 : * my_dispatcher->set_trace();
652 : * #endif
653 : * \endcode
654 : *
655 : * \param[in] trace Set to true to get SNAP_LOG_TRACE() of each message.
656 : */
657 4 : void set_trace(bool trace = true)
658 : {
659 4 : f_trace = trace;
660 4 : }
661 :
662 : /** \brief Retrieve the list of commands.
663 : *
664 : * This function transforms the vector of f_matches in a list of
665 : * commands in a string_list_t.
666 : *
667 : * \param[in,out] commands The place where the list of commands is saved.
668 : *
669 : * \return false if the commands were all determined, true if some need
670 : * help from the user of this dispatcher.
671 : */
672 0 : virtual bool get_commands(string_list_t & commands) override
673 : {
674 0 : bool need_user_help(false);
675 0 : for(auto const & m : f_matches)
676 : {
677 0 : if(m.f_expr == nullptr)
678 : {
679 0 : if(!m.match_is_always_match()
680 0 : && !m.match_is_callback_match())
681 : {
682 : // this is a "special case" where the user has
683 : // a magical function which does not require an
684 : // expression at all (i.e. "hard coded" in a
685 : // function)
686 : //
687 0 : need_user_help = true;
688 : }
689 : //else -- always match is the last entry and that just
690 : // means we can return UNKNOWN on an unknown message
691 : }
692 0 : else if(m.match_is_one_to_one_match())
693 : {
694 : // add the f_expr as is since it represents a command
695 : // as is
696 : //
697 0 : commands.push_back(m.f_expr);
698 : }
699 : else
700 : {
701 : // this is not a one to one match, so possibly a
702 : // full regex or similar
703 : //
704 0 : need_user_help = true;
705 : }
706 : }
707 0 : return need_user_help;
708 : }
709 : };
710 :
711 :
712 :
713 : } // namespace ed
714 : // vim: ts=4 sw=4 et
|