2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2011 Free Software Foundation Europe e.V.
6 The main author of Bacula is Kern Sibbald, with contributions from
7 many others, a complete list can be found in the file AUTHORS.
8 This program is Free Software; you can redistribute it and/or
9 modify it under the terms of version three of the GNU Affero General Public
10 License as published by the Free Software Foundation and included
13 This program is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
18 You should have received a copy of the GNU Affero General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
23 Bacula® is a registered trademark of Kern Sibbald.
24 The licensor of Bacula is the Free Software Foundation Europe
25 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26 Switzerland, email:ftf@fsfeurope.org.
30 * Bacula Director -- User Agent Database File tree for Restore
31 * command. This file interacts with the user implementing the
34 * Kern Sibbald, July MMII
43 #include "lib/fnmatch.h"
45 #include "findlib/find.h"
48 /* Forward referenced commands */
50 static int markcmd(UAContext *ua, TREE_CTX *tree);
51 static int markdircmd(UAContext *ua, TREE_CTX *tree);
52 static int countcmd(UAContext *ua, TREE_CTX *tree);
53 static int findcmd(UAContext *ua, TREE_CTX *tree);
54 static int lscmd(UAContext *ua, TREE_CTX *tree);
55 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
56 static int dircmd(UAContext *ua, TREE_CTX *tree);
57 static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
58 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
59 static int helpcmd(UAContext *ua, TREE_CTX *tree);
60 static int cdcmd(UAContext *ua, TREE_CTX *tree);
61 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
62 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
63 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
64 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
65 static int quitcmd(UAContext *ua, TREE_CTX *tree);
66 static int donecmd(UAContext *ua, TREE_CTX *tree);
67 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree);
68 static int dot_lscmd(UAContext *ua, TREE_CTX *tree);
69 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree);
70 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree);
72 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
73 static struct cmdstruct commands[] = {
74 { NT_("add"), markcmd, _("add dir/file to be restored recursively, wildcards allowed")},
75 { NT_("cd"), cdcmd, _("change current directory")},
76 { NT_("count"), countcmd, _("count marked files in and below the cd")},
77 { NT_("delete"), unmarkcmd, _("delete dir/file to be restored recursively in dir")},
78 { NT_("dir"), dircmd, _("long list current directory, wildcards allowed")},
79 { NT_(".dir"), dot_dircmd, _("long list current directory, wildcards allowed")},
80 { NT_("done"), donecmd, _("leave file selection mode")},
81 { NT_("estimate"), estimatecmd, _("estimate restore size")},
82 { NT_("exit"), donecmd, _("same as done command")},
83 { NT_("find"), findcmd, _("find files, wildcards allowed")},
84 { NT_("help"), helpcmd, _("print help")},
85 { NT_("ls"), lscmd, _("list current directory, wildcards allowed")},
86 { NT_(".ls"), dot_lscmd, _("list current directory, wildcards allowed")},
87 { NT_(".lsdir"), dot_lsdircmd, _("list subdir in current directory, wildcards allowed")},
88 { NT_("lsmark"), lsmarkcmd, _("list the marked files in and below the cd")},
89 { NT_(".lsmark"), dot_lsmarkcmd,_("list the marked files in")},
90 { NT_("mark"), markcmd, _("mark dir/file to be restored recursively, wildcards allowed")},
91 { NT_("markdir"), markdircmd, _("mark directory name to be restored (no files)")},
92 { NT_("pwd"), pwdcmd, _("print current working directory")},
93 { NT_(".pwd"), dot_pwdcmd, _("print current working directory")},
94 { NT_("unmark"), unmarkcmd, _("unmark dir/file to be restored recursively in dir")},
95 { NT_("unmarkdir"), unmarkdircmd, _("unmark directory name only no recursion")},
96 { NT_("quit"), quitcmd, _("quit and do not do restore")},
97 { NT_(".help"), dot_helpcmd, _("print help")},
98 { NT_("?"), helpcmd, _("print help")},
100 #define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
103 * Enter a prompt mode where the user can select/deselect
104 * files to be restored. This is sort of like a mini-shell
105 * that allows "cd", "pwd", "add", "rm", ...
107 bool user_select_files_from_tree(TREE_CTX *tree)
111 /* Get a new context so we don't destroy restore command args */
112 UAContext *ua = new_ua_context(tree->ua->jcr);
113 ua->UA_sock = tree->ua->UA_sock; /* patch in UA socket */
114 ua->api = tree->ua->api; /* keep API flag too */
115 BSOCK *user = ua->UA_sock;
118 "\nYou are now entering file selection mode where you add (mark) and\n"
119 "remove (unmark) files to be restored. No files are initially added, unless\n"
120 "you used the \"all\" keyword on the command line.\n"
121 "Enter \"done\" to leave this mode.\n\n"));
122 if (ua->api) user->signal(BNET_START_RTREE);
124 * Enter interactive command handler allowing selection
125 * of individual files.
127 tree->node = (TREE_NODE *)tree->root;
128 tree_getpath(tree->node, cwd, sizeof(cwd));
129 ua->send_msg(_("cwd is: %s\n"), cwd);
132 if (!get_cmd(ua, "$ ", true)) {
135 if (ua->api) user->signal(BNET_CMD_BEGIN);
136 parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
138 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
139 if (ua->api) user->signal(BNET_CMD_FAILED);
143 len = strlen(ua->argk[0]);
146 for (i=0; i<comsize; i++) /* search for command */
147 if (strncasecmp(ua->argk[0], commands[i].key, len) == 0) {
148 stat = (*commands[i].func)(ua, tree); /* go execute command */
153 if (*ua->argk[0] == '.') {
154 /* Some unknow dot command -- probably .messages, ignore it */
157 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
158 if (ua->api) user->signal(BNET_CMD_FAILED);
161 if (ua->api) user->signal(BNET_CMD_OK);
166 if (ua->api) user->signal(BNET_END_RTREE);
167 ua->UA_sock = NULL; /* don't release restore socket */
170 free_ua_context(ua); /* get rid of temp UA context */
176 * This callback routine is responsible for inserting the
177 * items it gets into the directory tree. For each JobId selected
178 * this routine is called once for each file. We do not allow
179 * duplicate filenames, but instead keep the info from the most
180 * recent file entered (i.e. the JobIds are assumed to be sorted)
182 * See uar_sel_files in sql_cmds.c for query that calls us.
183 * row[0]=Path, row[1]=Filename, row[2]=FileIndex
184 * row[3]=JobId row[4]=LStat row[5]=DeltaSeq
186 int insert_tree_handler(void *ctx, int num_fields, char **row)
189 TREE_CTX *tree = (TREE_CTX *)ctx;
197 Dmsg4(150, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
199 if (*row[1] == 0) { /* no filename => directory */
200 if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
208 hard_link = (decode_LinkFI(row[4], &statp, sizeof(statp)) != 0);
209 node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
210 JobId = str_to_int64(row[3]);
211 FileIndex = str_to_int64(row[2]);
212 delta_seq = str_to_int64(row[5]);
213 Dmsg5(150, "node=0x%p JobId=%s FileIndex=%s Delta=%s node.delta=%d\n",
214 node, row[3], row[2], row[5], node->delta_seq);
216 /* TODO: check with hardlinks */
218 if (delta_seq == (node->delta_seq + 1)) {
219 tree_add_delta_part(tree->root, node, node->JobId, node->FileIndex);
222 /* File looks to be deleted */
223 if (node->delta_seq == -1) { /* just created */
224 tree_remove_node(tree->root, node);
227 tree->ua->warning_msg(_("Something is wrong with the Delta sequence of %s, "
228 "skiping new parts. Current sequence is %d\n"),
229 row[1], node->delta_seq);
231 Dmsg3(0, "Something is wrong with Delta, skip it "
232 "fname=%s d1=%d d2=%d\n", row[1], node->delta_seq, delta_seq);
238 * - The first time we see a file (node->inserted==true), we accept it.
239 * - In the same JobId, we accept only the first copy of a
240 * hard linked file (the others are simply pointers).
241 * - In the same JobId, we accept the last copy of any other
242 * file -- in particular directories.
244 * All the code to set ok could be condensed to a single
245 * line, but it would be even harder to read.
248 if (!node->inserted && JobId == node->JobId) {
249 if ((hard_link && FileIndex > node->FileIndex) ||
250 (!hard_link && FileIndex < node->FileIndex)) {
255 node->hard_link = hard_link;
256 node->FileIndex = FileIndex;
259 node->soft_link = S_ISLNK(statp.st_mode) != 0;
260 node->delta_seq = delta_seq;
263 node->extract = true; /* extract all by default */
264 if (type == TN_DIR || type == TN_DIR_NLS) {
265 node->extract_dir = true; /* if dir, extract it */
269 if (node->inserted) {
271 if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
272 tree->ua->send_msg("+");
273 tree->LastCount = tree->FileCount;
282 * Set extract to value passed. We recursively walk
283 * down the tree setting all children if the
284 * node is a directory.
286 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
293 node->extract = extract;
294 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
295 node->extract_dir = extract; /* set/clear dir too */
297 if (node->type != TN_NEWDIR) {
300 /* For a non-file (i.e. directory), we see all the children */
301 if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
302 /* Recursive set children within directory */
303 foreach_child(n, node) {
304 count += set_extract(ua, n, tree, extract);
307 * Walk up tree marking any unextracted parent to be
311 while (node->parent && !node->parent->extract_dir) {
313 node->extract_dir = true;
316 } else if (extract) {
319 * Ordinary file, we get the full path, look up the
320 * attributes, decode them, and if we are hard linked to
321 * a file that was saved, we must load that file too.
323 tree_getpath(node, cwd, sizeof(cwd));
325 fdbr.JobId = node->JobId;
326 if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
328 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
330 * If we point to a hard linked file, traverse the tree to
331 * find that file, and mark it to be restored as well. It
332 * must have the Link we just obtained and the same JobId.
335 for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
336 if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
338 if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
339 n->extract_dir = true;
350 static void strip_trailing_slash(char *arg)
352 int len = strlen(arg);
357 if (arg[len] == '/') { /* strip any trailing slash */
363 * Recursively mark the current directory to be restored as
364 * well as all directories and files below it.
366 static int markcmd(UAContext *ua, TREE_CTX *tree)
372 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
373 ua->send_msg(_("No files marked.\n"));
376 for (int i=1; i < ua->argc; i++) {
377 strip_trailing_slash(ua->argk[i]);
378 foreach_child(node, tree->node) {
379 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
380 count += set_extract(ua, node, tree, true);
385 ua->send_msg(_("No files marked.\n"));
386 } else if (count == 1) {
387 ua->send_msg(_("1 file marked.\n"));
389 ua->send_msg(_("%s files marked.\n"),
390 edit_uint64_with_commas(count, ec1));
395 static int markdircmd(UAContext *ua, TREE_CTX *tree)
401 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
402 ua->send_msg(_("No files marked.\n"));
405 for (int i=1; i < ua->argc; i++) {
406 strip_trailing_slash(ua->argk[i]);
407 foreach_child(node, tree->node) {
408 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
409 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
410 node->extract_dir = true;
417 ua->send_msg(_("No directories marked.\n"));
418 } else if (count == 1) {
419 ua->send_msg(_("1 directory marked.\n"));
421 ua->send_msg(_("%s directories marked.\n"),
422 edit_uint64_with_commas(count, ec1));
428 static int countcmd(UAContext *ua, TREE_CTX *tree)
430 int total, num_extract;
431 char ec1[50], ec2[50];
433 total = num_extract = 0;
434 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
435 if (node->type != TN_NEWDIR) {
437 if (node->extract || node->extract_dir) {
442 ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
443 edit_uint64_with_commas(total, ec1),
444 edit_uint64_with_commas(num_extract, ec2));
448 static int findcmd(UAContext *ua, TREE_CTX *tree)
453 ua->send_msg(_("No file specification given.\n"));
454 return 1; /* make it non-fatal */
457 for (int i=1; i < ua->argc; i++) {
458 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
459 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
461 tree_getpath(node, cwd, sizeof(cwd));
464 } else if (node->extract_dir) {
469 ua->send_msg("%s%s\n", tag, cwd);
476 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
480 if (!tree_node_has_child(tree->node)) {
484 foreach_child(node, tree->node) {
485 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
486 if (tree_node_has_child(node)) {
487 ua->send_msg("%s/\n", node->fname);
495 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
497 for (int i=0; i<comsize; i++) {
498 /* List only non-dot commands */
499 if (commands[i].key[0] != '.') {
500 ua->send_msg("%s\n", commands[i].key);
506 static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
510 if (!tree_node_has_child(tree->node)) {
514 foreach_child(node, tree->node) {
515 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
516 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
523 static int lscmd(UAContext *ua, TREE_CTX *tree)
527 if (!tree_node_has_child(tree->node)) {
530 foreach_child(node, tree->node) {
531 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
535 } else if (node->extract_dir) {
540 ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
547 * Ls command that lists only the marked files
549 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
552 if (!tree_node_has_child(tree->node)) {
555 foreach_child(node, tree->node) {
556 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
557 (node->extract || node->extract_dir)) {
558 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
565 * This recursive ls command that lists only the marked files
567 static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
570 const int max_level = 100;
571 char indent[max_level*2+1];
573 if (!tree_node_has_child(tnode)) {
576 level = MIN(level, max_level);
578 for (i=0; i<level; i++) {
583 foreach_child(node, tnode) {
584 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
585 (node->extract || node->extract_dir)) {
589 } else if (node->extract_dir) {
594 ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
595 if (tree_node_has_child(node)) {
596 rlsmark(ua, node, level+1);
602 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
604 rlsmark(ua, tree->node, 0);
609 * This is actually the long form used for "dir"
611 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
612 struct stat *statp, bool dot_cmd)
617 char en1[30], en2[30];
621 p = encode_mode(statp->st_mode, buf);
624 n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
626 n = sprintf(p, "%s,%s,",
627 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
628 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
630 n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
632 p = encode_time(statp->st_mtime, p);
637 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
639 n = sprintf(p, "%-8.8s %-8.8s",
640 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
641 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
643 n = sprintf(p, "%12.12s ", edit_int64(statp->st_size, ec1));
645 if (statp->st_ctime > statp->st_mtime) {
646 time = statp->st_ctime;
648 time = statp->st_mtime;
650 /* Display most recent time */
651 p = encode_time(time, p);
655 for (f=fname; *f; ) {
662 * Like ls command, but give more detail on each file
664 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
670 char cwd[1100], *pcwd;
673 if (!tree_node_has_child(tree->node)) {
674 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
678 guid = new_guid_list();
679 foreach_child(node, tree->node) {
681 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
684 } else if (node->extract_dir) {
689 tree_getpath(node, cwd, sizeof(cwd));
691 fdbr.JobId = node->JobId;
693 * Strip / from soft links to directories.
694 * This is because soft links to files have a trailing slash
695 * when returned from tree_getpath, but db_get_file_attr...
696 * treats soft links as files, so they do not have a trailing
697 * slash like directory names.
699 if (node->type == TN_FILE && tree_node_has_child(node)) {
700 bstrncpy(buf, cwd, sizeof(buf));
702 int len = strlen(buf);
704 buf[len-1] = 0; /* strip trailing / */
709 if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
711 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
713 /* Something went wrong getting attributes -- print name */
714 memset(&statp, 0, sizeof(statp));
716 ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
717 ua->send_msg("%s\n", buf);
720 free_guid_list(guid);
724 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
726 return do_dircmd(ua, tree, true/*dot command*/);
729 static int dircmd(UAContext *ua, TREE_CTX *tree)
731 return do_dircmd(ua, tree, false/*not dot command*/);
735 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
737 int total, num_extract;
738 uint64_t total_bytes = 0;
744 total = num_extract = 0;
745 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
746 if (node->type != TN_NEWDIR) {
748 /* If regular file, get size */
749 if (node->extract && node->type == TN_FILE) {
751 tree_getpath(node, cwd, sizeof(cwd));
753 fdbr.JobId = node->JobId;
754 if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
756 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
757 if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
758 total_bytes += statp.st_size;
761 /* Directory, count only */
762 } else if (node->extract || node->extract_dir) {
767 ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
768 total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
774 static int helpcmd(UAContext *ua, TREE_CTX *tree)
778 ua->send_msg(_(" Command Description\n ======= ===========\n"));
779 for (i=0; i<comsize; i++) {
780 /* List only non-dot commands */
781 if (commands[i].key[0] != '.') {
782 ua->send_msg(" %-10s %s\n", _(commands[i].key), _(commands[i].help));
790 * Change directories. Note, if the user specifies x: and it fails,
791 * we assume it is a Win32 absolute cd rather than relative and
792 * try a second time with /x: ... Win32 kludge.
794 static int cdcmd(UAContext *ua, TREE_CTX *tree)
801 ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
805 node = tree_cwd(ua->argk[1], tree->root, tree->node);
807 /* Try once more if Win32 drive -- make absolute */
808 if (ua->argk[1][1] == ':') { /* win32 drive */
809 bstrncpy(cwd, "/", sizeof(cwd));
810 bstrncat(cwd, ua->argk[1], sizeof(cwd));
811 node = tree_cwd(cwd, tree->root, tree->node);
814 ua->warning_msg(_("Invalid path given.\n"));
821 return pwdcmd(ua, tree);
824 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
827 tree_getpath(tree->node, cwd, sizeof(cwd));
829 ua->send_msg("%s", cwd);
831 ua->send_msg(_("cwd is: %s\n"), cwd);
836 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
839 tree_getpath(tree->node, cwd, sizeof(cwd));
840 ua->send_msg("%s", cwd);
844 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
849 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
850 ua->send_msg(_("No files unmarked.\n"));
853 for (int i=1; i < ua->argc; i++) {
854 strip_trailing_slash(ua->argk[i]);
855 foreach_child(node, tree->node) {
856 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
857 count += set_extract(ua, node, tree, false);
862 ua->send_msg(_("No files unmarked.\n"));
863 } else if (count == 1) {
864 ua->send_msg(_("1 file unmarked.\n"));
867 ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
872 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
877 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
878 ua->send_msg(_("No directories unmarked.\n"));
882 for (int i=1; i < ua->argc; i++) {
883 strip_trailing_slash(ua->argk[i]);
884 foreach_child(node, tree->node) {
885 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
886 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
887 node->extract_dir = false;
895 ua->send_msg(_("No directories unmarked.\n"));
896 } else if (count == 1) {
897 ua->send_msg(_("1 directory unmarked.\n"));
899 ua->send_msg(_("%d directories unmarked.\n"), count);
905 static int donecmd(UAContext *ua, TREE_CTX *tree)
910 static int quitcmd(UAContext *ua, TREE_CTX *tree)