]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_tree.c
28Jul05
[bacula/bacula] / bacula / src / dird / ua_tree.c
1 /*
2  *
3  *   Bacula Director -- User Agent Database File tree for Restore
4  *      command. This file interacts with the user implementing the
5  *      UA tree commands.
6  *
7  *     Kern Sibbald, July MMII
8  *
9  *   Version $Id$
10  */
11 /*
12    Copyright (C) 2002-2005 Kern Sibbald
13
14    This program is free software; you can redistribute it and/or
15    modify it under the terms of the GNU General Public License
16    version 2 as amended with additional clauses defined in the
17    file LICENSE in the main source directory.
18
19    This program is distributed in the hope that it will be useful,
20    but WITHOUT ANY WARRANTY; without even the implied warranty of
21    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
22    the file LICENSE for additional details.
23
24  */
25
26 #include "bacula.h"
27 #include "dird.h"
28 #ifdef HAVE_FNMATCH
29 #include <fnmatch.h>
30 #else
31 #include "lib/fnmatch.h"
32 #endif
33 #include "findlib/find.h"
34
35
36 /* Forward referenced commands */
37
38 static int markcmd(UAContext *ua, TREE_CTX *tree);
39 static int markdircmd(UAContext *ua, TREE_CTX *tree);
40 static int countcmd(UAContext *ua, TREE_CTX *tree);
41 static int findcmd(UAContext *ua, TREE_CTX *tree);
42 static int lscmd(UAContext *ua, TREE_CTX *tree);
43 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
44 static int dircmd(UAContext *ua, TREE_CTX *tree);
45 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
46 static int helpcmd(UAContext *ua, TREE_CTX *tree);
47 static int cdcmd(UAContext *ua, TREE_CTX *tree);
48 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
49 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
50 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
51 static int quitcmd(UAContext *ua, TREE_CTX *tree);
52 static int donecmd(UAContext *ua, TREE_CTX *tree);
53
54
55 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
56 static struct cmdstruct commands[] = {
57  { N_("cd"),         cdcmd,        _("change current directory")},
58  { N_("count"),      countcmd,     _("count marked files in and below the cd")},
59  { N_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
60  { N_("done"),       donecmd,      _("leave file selection mode")},
61  { N_("estimate"),   estimatecmd,  _("estimate restore size")},
62  { N_("exit"),       donecmd,      _("same as done command")},
63  { N_("find"),       findcmd,      _("find files, wildcards allowed")},
64  { N_("help"),       helpcmd,      _("print help")},
65  { N_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
66  { N_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
67  { N_("mark"),       markcmd,      _("mark dir/file to be restored recursively in dirs")},
68  { N_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
69  { N_("pwd"),        pwdcmd,       _("print current working directory")},
70  { N_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
71  { N_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
72  { N_("quit"),       quitcmd,      _("quit and do not do restore")},
73  { N_("?"),          helpcmd,      _("print help")},
74              };
75 #define comsize (sizeof(commands)/sizeof(struct cmdstruct))
76
77
78 /*
79  * Enter a prompt mode where the user can select/deselect
80  *  files to be restored. This is sort of like a mini-shell
81  *  that allows "cd", "pwd", "add", "rm", ...
82  */
83 bool user_select_files_from_tree(TREE_CTX *tree)
84 {
85    char cwd[2000];
86    bool stat;
87    /* Get a new context so we don't destroy restore command args */
88    UAContext *ua = new_ua_context(tree->ua->jcr);
89    ua->UA_sock = tree->ua->UA_sock;   /* patch in UA socket */
90
91    bsendmsg(tree->ua, _(
92       "\nYou are now entering file selection mode where you add (mark) and\n"
93       "remove (unmark) files to be restored. No files are initially added, unless\n"
94       "you used the \"all\" keyword on the command line.\n"
95       "Enter \"done\" to leave this mode.\n\n"));
96    /*
97     * Enter interactive command handler allowing selection
98     *  of individual files.
99     */
100    tree->node = (TREE_NODE *)tree->root;
101    tree_getpath(tree->node, cwd, sizeof(cwd));
102    bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
103    for ( ;; ) {
104       int found, len, i;
105       if (!get_cmd(ua, "$ ")) {
106          break;
107       }
108       parse_ua_args(ua);
109       if (ua->argc == 0) {
110          break;
111       }
112
113       len = strlen(ua->argk[0]);
114       found = 0;
115       stat = false;
116       for (i=0; i<(int)comsize; i++)       /* search for command */
117          if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
118             stat = (*commands[i].func)(ua, tree);   /* go execute command */
119             found = 1;
120             break;
121          }
122       if (!found) {
123          bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
124          continue;
125       }
126       if (!stat) {
127          break;
128       }
129    }
130    ua->UA_sock = NULL;                /* don't release restore socket */
131    stat = !ua->quit;
132    ua->quit = false;
133    free_ua_context(ua);               /* get rid of temp UA context */
134    return stat;
135 }
136
137
138 /*
139  * This callback routine is responsible for inserting the
140  *  items it gets into the directory tree. For each JobId selected
141  *  this routine is called once for each file. We do not allow
142  *  duplicate filenames, but instead keep the info from the most
143  *  recent file entered (i.e. the JobIds are assumed to be sorted)
144  *
145  *   See uar_sel_files in sql_cmds.c for query that calls us.
146  *      row[0]=Path, row[1]=Filename, row[2]=FileIndex
147  *      row[3]=JobId row[4]=LStat
148  */
149 int insert_tree_handler(void *ctx, int num_fields, char **row)
150 {
151    struct stat statp;
152    TREE_CTX *tree = (TREE_CTX *)ctx;
153    TREE_NODE *node;
154    int type;
155    bool hard_link, ok;
156    int FileIndex;
157    JobId_t JobId;
158
159 // Dmsg4(000, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
160 //    row[2], row[3]);
161    if (*row[1] == 0) {                /* no filename => directory */
162       if (*row[0] != '/') {           /* Must be Win32 directory */
163          type = TN_DIR_NLS;
164       } else {
165          type = TN_DIR;
166       }
167    } else {
168       type = TN_FILE;
169    }
170    hard_link = (decode_LinkFI(row[4], &statp) != 0);
171    node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
172    JobId = str_to_int64(row[3]);
173    FileIndex = str_to_int64(row[2]);
174    /*
175     * - The first time we see a file (node->inserted==true), we accept it.
176     * - In the same JobId, we accept only the first copy of a
177     *   hard linked file (the others are simply pointers).
178     * - In the same JobId, we accept the last copy of any other
179     *   file -- in particular directories.
180     *
181     * All the code to set ok could be condensed to a single
182     *  line, but it would be even harder to read.
183     */
184    ok = true;
185    if (!node->inserted && JobId == node->JobId) {
186       if ((hard_link && FileIndex > node->FileIndex) ||
187           (!hard_link && FileIndex < node->FileIndex)) {
188          ok = false;
189       }
190    }
191    if (ok) {
192       node->hard_link = hard_link;
193       node->FileIndex = FileIndex;
194       node->JobId = JobId;
195       node->type = type;
196       node->soft_link = S_ISLNK(statp.st_mode) != 0;
197       if (tree->all) {
198          node->extract = true;          /* extract all by default */
199          if (type == TN_DIR || type == TN_DIR_NLS) {
200             node->extract_dir = true;   /* if dir, extract it */
201          }
202       }
203    }
204    if (node->inserted) {
205       tree->FileCount++;
206       if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
207          bsendmsg(tree->ua, "+");
208          tree->LastCount = tree->FileCount;
209       }
210    }
211    tree->cnt++;
212    return 0;
213 }
214
215
216 /*
217  * Set extract to value passed. We recursively walk
218  *  down the tree setting all children if the
219  *  node is a directory.
220  */
221 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
222 {
223    TREE_NODE *n;
224    FILE_DBR fdbr;
225    struct stat statp;
226    int count = 0;
227
228    node->extract = extract;
229    if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
230       node->extract_dir = extract;    /* set/clear dir too */
231    }
232    if (node->type != TN_NEWDIR) {
233       count++;
234    }
235    /* For a non-file (i.e. directory), we see all the children */
236    if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
237       /* Recursive set children within directory */
238       foreach_child(n, node) {
239          count += set_extract(ua, n, tree, extract);
240       }
241       /*
242        * Walk up tree marking any unextracted parent to be
243        * extracted.
244        */
245       if (extract) {
246          while (node->parent && !node->parent->extract_dir) {
247             node = node->parent;
248             node->extract_dir = true;
249          }
250       }
251    } else if (extract) {
252       char cwd[2000];
253       /*
254        * Ordinary file, we get the full path, look up the
255        * attributes, decode them, and if we are hard linked to
256        * a file that was saved, we must load that file too.
257        */
258       tree_getpath(node, cwd, sizeof(cwd));
259       fdbr.FileId = 0;
260       fdbr.JobId = node->JobId;
261       if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
262          int32_t LinkFI;
263          decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
264          /*
265           * If we point to a hard linked file, traverse the tree to
266           * find that file, and mark it to be restored as well. It
267           * must have the Link we just obtained and the same JobId.
268           */
269          if (LinkFI) {
270             for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
271                if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
272                   n->extract = true;
273                   if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
274                      n->extract_dir = true;
275                   }
276                   break;
277                }
278             }
279          }
280       }
281    }
282    return count;
283 }
284
285 /*
286  * Recursively mark the current directory to be restored as
287  *  well as all directories and files below it.
288  */
289 static int markcmd(UAContext *ua, TREE_CTX *tree)
290 {
291    TREE_NODE *node;
292    int count = 0;
293    char ec1[50];
294
295    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
296       bsendmsg(ua, _("No files marked.\n"));
297       return 1;
298    }
299    for (int i=1; i < ua->argc; i++) {
300       foreach_child(node, tree->node) {
301          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
302             count += set_extract(ua, node, tree, true);
303          }
304       }
305    }
306    if (count == 0) {
307       bsendmsg(ua, _("No files marked.\n"));
308    } else {
309       bsendmsg(ua, _("%s file%s marked.\n"),
310                edit_uint64_with_commas(count, ec1), count==0?"":"s");
311    }
312    return 1;
313 }
314
315 static int markdircmd(UAContext *ua, TREE_CTX *tree)
316 {
317    TREE_NODE *node;
318    int count = 0;
319    char ec1[50];
320
321    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
322       bsendmsg(ua, _("No files marked.\n"));
323       return 1;
324    }
325    for (int i=1; i < ua->argc; i++) {
326       foreach_child(node, tree->node) {
327          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
328             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
329                node->extract_dir = true;
330                count++;
331             }
332          }
333       }
334    }
335    if (count == 0) {
336       bsendmsg(ua, _("No directories marked.\n"));
337    } else {
338       bsendmsg(ua, _("%s director%s marked.\n"),
339                edit_uint64_with_commas(count, ec1), count==1?"y":"ies");
340    }
341    return 1;
342 }
343
344
345 static int countcmd(UAContext *ua, TREE_CTX *tree)
346 {
347    int total, num_extract;
348    char ec1[50], ec2[50];
349
350    total = num_extract = 0;
351    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
352       if (node->type != TN_NEWDIR) {
353          total++;
354          if (node->extract || node->extract_dir) {
355             num_extract++;
356          }
357       }
358    }
359    bsendmsg(ua, "%s total files/dirs. %s marked to be restored.\n",
360             edit_uint64_with_commas(total, ec1),
361             edit_uint64_with_commas(num_extract, ec2));
362    return 1;
363 }
364
365 static int findcmd(UAContext *ua, TREE_CTX *tree)
366 {
367    char cwd[2000];
368
369    if (ua->argc == 1) {
370       bsendmsg(ua, _("No file specification given.\n"));
371       return 0;
372    }
373
374    for (int i=1; i < ua->argc; i++) {
375       for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
376          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
377             const char *tag;
378             tree_getpath(node, cwd, sizeof(cwd));
379             if (node->extract) {
380                tag = "*";
381             } else if (node->extract_dir) {
382                tag = "+";
383             } else {
384                tag = "";
385             }
386             bsendmsg(ua, "%s%s\n", tag, cwd);
387          }
388       }
389    }
390    return 1;
391 }
392
393
394
395 static int lscmd(UAContext *ua, TREE_CTX *tree)
396 {
397    TREE_NODE *node;
398
399    if (!tree_node_has_child(tree->node)) {
400       return 1;
401    }
402    foreach_child(node, tree->node) {
403       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
404          const char *tag;
405          if (node->extract) {
406             tag = "*";
407          } else if (node->extract_dir) {
408             tag = "+";
409          } else {
410             tag = "";
411          }
412          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
413       }
414    }
415    return 1;
416 }
417
418 /*
419  * Ls command that lists only the marked files
420  */
421 static void rlsmark(UAContext *ua, TREE_NODE *tnode)
422 {
423    TREE_NODE *node;
424    if (!tree_node_has_child(tnode)) {
425       return;
426    }
427    foreach_child(node, tnode) {
428       if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
429           (node->extract || node->extract_dir)) {
430          const char *tag;
431          if (node->extract) {
432             tag = "*";
433          } else if (node->extract_dir) {
434             tag = "+";
435          } else {
436             tag = "";
437          }
438          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
439          if (tree_node_has_child(node)) {
440             rlsmark(ua, node);
441          }
442       }
443    }
444 }
445
446 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
447 {
448    rlsmark(ua, tree->node);
449    return 1;
450 }
451
452
453
454 extern char *getuser(uid_t uid, char *name, int len);
455 extern char *getgroup(gid_t gid, char *name, int len);
456
457 /*
458  * This is actually the long form used for "dir"
459  */
460 static void ls_output(char *buf, const char *fname, const char *tag, struct stat *statp)
461 {
462    char *p;
463    const char *f;
464    char ec1[30];
465    char en1[30], en2[30];
466    int n;
467
468    p = encode_mode(statp->st_mode, buf);
469    n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
470    p += n;
471    n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid, en1, sizeof(en1)),
472                getgroup(statp->st_gid, en2, sizeof(en2)));
473    p += n;
474    n = sprintf(p, "%10.10s  ", edit_uint64(statp->st_size, ec1));
475    p += n;
476    p = encode_time(statp->st_ctime, p);
477    *p++ = ' ';
478    *p++ = *tag;
479    for (f=fname; *f; ) {
480       *p++ = *f++;
481    }
482    *p = 0;
483 }
484
485
486 /*
487  * Like ls command, but give more detail on each file
488  */
489 static int dircmd(UAContext *ua, TREE_CTX *tree)
490 {
491    TREE_NODE *node;
492    FILE_DBR fdbr;
493    struct stat statp;
494    char buf[1100];
495    char cwd[1100], *pcwd;
496
497    if (!tree_node_has_child(tree->node)) {
498       bsendmsg(ua, "Node %s has no children.\n", tree->node->fname);
499       return 1;
500    }
501
502    foreach_child(node, tree->node) {
503       const char *tag;
504       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
505          if (node->extract) {
506             tag = "*";
507          } else if (node->extract_dir) {
508             tag = "+";
509          } else {
510             tag = " ";
511          }
512          tree_getpath(node, cwd, sizeof(cwd));
513          fdbr.FileId = 0;
514          fdbr.JobId = node->JobId;
515          /*
516           * Strip / from soft links to directories.
517           *   This is because soft links to files have a trailing slash
518           *   when returned from tree_getpath, but db_get_file_attr...
519           *   treats soft links as files, so they do not have a trailing
520           *   slash like directory names.
521           */
522          if (node->type == TN_FILE && tree_node_has_child(node)) {
523             bstrncpy(buf, cwd, sizeof(buf));
524             pcwd = buf;
525             int len = strlen(buf);
526             if (len > 1) {
527                buf[len-1] = 0;        /* strip trailing / */
528             }
529          } else {
530             pcwd = cwd;
531          }
532          if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
533             int32_t LinkFI;
534             decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
535          } else {
536             /* Something went wrong getting attributes -- print name */
537             memset(&statp, 0, sizeof(statp));
538          }
539          ls_output(buf, cwd, tag, &statp);
540          bsendmsg(ua, "%s\n", buf);
541       }
542    }
543    return 1;
544 }
545
546
547 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
548 {
549    int total, num_extract;
550    uint64_t total_bytes = 0;
551    FILE_DBR fdbr;
552    struct stat statp;
553    char cwd[1100];
554    char ec1[50];
555
556    total = num_extract = 0;
557    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
558       if (node->type != TN_NEWDIR) {
559          total++;
560          /* If regular file, get size */
561          if (node->extract && node->type == TN_FILE) {
562             num_extract++;
563             tree_getpath(node, cwd, sizeof(cwd));
564             fdbr.FileId = 0;
565             fdbr.JobId = node->JobId;
566             if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
567                int32_t LinkFI;
568                decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
569                if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
570                   total_bytes += statp.st_size;
571                }
572             }
573          /* Directory, count only */
574          } else if (node->extract || node->extract_dir) {
575             num_extract++;
576          }
577       }
578    }
579    bsendmsg(ua, "%d total files; %d marked to be restored; %s bytes.\n",
580             total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
581    return 1;
582 }
583
584
585
586 static int helpcmd(UAContext *ua, TREE_CTX *tree)
587 {
588    unsigned int i;
589
590    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
591    for (i=0; i<comsize; i++) {
592       bsendmsg(ua, _("  %-10s %s\n"), _(commands[i].key), _(commands[i].help));
593    }
594    bsendmsg(ua, "\n");
595    return 1;
596 }
597
598 /*
599  * Change directories.  Note, if the user specifies x: and it fails,
600  *   we assume it is a Win32 absolute cd rather than relative and
601  *   try a second time with /x: ...  Win32 kludge.
602  */
603 static int cdcmd(UAContext *ua, TREE_CTX *tree)
604 {
605    TREE_NODE *node;
606    char cwd[2000];
607
608    if (ua->argc != 2) {
609       return 1;
610    }
611    strip_leading_space(ua->argk[1]);
612    node = tree_cwd(ua->argk[1], tree->root, tree->node);
613    if (!node) {
614       /* Try once more if Win32 drive -- make absolute */
615       if (ua->argk[1][1] == ':') {  /* win32 drive */
616          bstrncpy(cwd, "/", sizeof(cwd));
617          bstrncat(cwd, ua->argk[1], sizeof(cwd));
618          node = tree_cwd(cwd, tree->root, tree->node);
619       }
620       if (!node) {
621          bsendmsg(ua, _("Invalid path given.\n"));
622       } else {
623          tree->node = node;
624       }
625    } else {
626       tree->node = node;
627    }
628    tree_getpath(tree->node, cwd, sizeof(cwd));
629    bsendmsg(ua, _("cwd is: %s\n"), cwd);
630    return 1;
631 }
632
633 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
634 {
635    char cwd[2000];
636    tree_getpath(tree->node, cwd, sizeof(cwd));
637    bsendmsg(ua, _("cwd is: %s\n"), cwd);
638    return 1;
639 }
640
641
642 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
643 {
644    TREE_NODE *node;
645    int count = 0;
646
647    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
648       bsendmsg(ua, _("No files unmarked.\n"));
649       return 1;
650    }
651    for (int i=1; i < ua->argc; i++) {
652       foreach_child(node, tree->node) {
653          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
654             count += set_extract(ua, node, tree, false);
655          }
656       }
657    }
658    if (count == 0) {
659       bsendmsg(ua, _("No files unmarked.\n"));
660    } else {
661       bsendmsg(ua, _("%d file%s unmarked.\n"), count, count==0?"":"s");
662    }
663    return 1;
664 }
665
666 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
667 {
668    TREE_NODE *node;
669    int count = 0;
670
671    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
672       bsendmsg(ua, _("No directories unmarked.\n"));
673       return 1;
674    }
675
676    for (int i=1; i < ua->argc; i++) {
677       foreach_child(node, tree->node) {
678          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
679             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
680                node->extract_dir = false;
681                count++;
682             }
683          }
684       }
685    }
686
687    if (count == 0) {
688       bsendmsg(ua, _("No directories unmarked.\n"));
689    } else {
690       bsendmsg(ua, _("%d director%s unmarked.\n"), count, count==1?"y":"ies");
691    }
692    return 1;
693 }
694
695
696 static int donecmd(UAContext *ua, TREE_CTX *tree)
697 {
698    return 0;
699 }
700
701 static int quitcmd(UAContext *ua, TREE_CTX *tree)
702 {
703    ua->quit = true;
704    return 0;
705 }