]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_tree.c
Mark translatable strings in all source files.
[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 if (count == 1) {
309       bsendmsg(ua, _("1 file marked.\n"));
310    } else {
311       bsendmsg(ua, _("%s files marked.\n"),
312                edit_uint64_with_commas(count, ec1));
313    }
314    return 1;
315 }
316
317 static int markdircmd(UAContext *ua, TREE_CTX *tree)
318 {
319    TREE_NODE *node;
320    int count = 0;
321    char ec1[50];
322
323    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
324       bsendmsg(ua, _("No files marked.\n"));
325       return 1;
326    }
327    for (int i=1; i < ua->argc; i++) {
328       foreach_child(node, tree->node) {
329          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
330             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
331                node->extract_dir = true;
332                count++;
333             }
334          }
335       }
336    }
337    if (count == 0) {
338       bsendmsg(ua, _("No directories marked.\n"));
339    } else if (count == 1) {
340       bsendmsg(ua, _("1 directory marked.\n"));
341    } else {
342       bsendmsg(ua, _("%s directories marked.\n"),
343                edit_uint64_with_commas(count, ec1));
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, "%10.10s  ", 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    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
595    for (i=0; i<comsize; i++) {
596       bsendmsg(ua, "  %-10s %s\n", _(commands[i].key), _(commands[i].help));
597    }
598    bsendmsg(ua, "\n");
599    return 1;
600 }
601
602 /*
603  * Change directories.  Note, if the user specifies x: and it fails,
604  *   we assume it is a Win32 absolute cd rather than relative and
605  *   try a second time with /x: ...  Win32 kludge.
606  */
607 static int cdcmd(UAContext *ua, TREE_CTX *tree)
608 {
609    TREE_NODE *node;
610    char cwd[2000];
611
612    if (ua->argc != 2) {
613       return 1;
614    }
615    strip_leading_space(ua->argk[1]);
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 if (count == 1) {
665       bsendmsg(ua, _("1 file unmarked.\n"));
666    } else {
667       bsendmsg(ua, _("%d files unmarked.\n"), count);
668    }
669    return 1;
670 }
671
672 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
673 {
674    TREE_NODE *node;
675    int count = 0;
676
677    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
678       bsendmsg(ua, _("No directories unmarked.\n"));
679       return 1;
680    }
681
682    for (int i=1; i < ua->argc; i++) {
683       foreach_child(node, tree->node) {
684          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
685             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
686                node->extract_dir = false;
687                count++;
688             }
689          }
690       }
691    }
692
693    if (count == 0) {
694       bsendmsg(ua, _("No directories unmarked.\n"));
695    } else if (count == 1) {
696       bsendmsg(ua, _("1 directory unmarked.\n"));
697    } else {
698       bsendmsg(ua, _("%d directories unmarked.\n"), count);
699    }
700    return 1;
701 }
702
703
704 static int donecmd(UAContext *ua, TREE_CTX *tree)
705 {
706    return 0;
707 }
708
709 static int quitcmd(UAContext *ua, TREE_CTX *tree)
710 {
711    ua->quit = true;
712    return 0;
713 }