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