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(100, "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(100, "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 Dmsg3(0, "Something is wrong with Delta, skipt it "
228 "fname=%s d1=%d d2=%d\n",
229 row[1], node->delta_seq, delta_seq);
235 * - The first time we see a file (node->inserted==true), we accept it.
236 * - In the same JobId, we accept only the first copy of a
237 * hard linked file (the others are simply pointers).
238 * - In the same JobId, we accept the last copy of any other
239 * file -- in particular directories.
241 * All the code to set ok could be condensed to a single
242 * line, but it would be even harder to read.
245 if (!node->inserted && JobId == node->JobId) {
246 if ((hard_link && FileIndex > node->FileIndex) ||
247 (!hard_link && FileIndex < node->FileIndex)) {
252 node->hard_link = hard_link;
253 node->FileIndex = FileIndex;
256 node->soft_link = S_ISLNK(statp.st_mode) != 0;
257 node->delta_seq = delta_seq;
260 node->extract = true; /* extract all by default */
261 if (type == TN_DIR || type == TN_DIR_NLS) {
262 node->extract_dir = true; /* if dir, extract it */
266 if (node->inserted) {
268 if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
269 tree->ua->send_msg("+");
270 tree->LastCount = tree->FileCount;
279 * Set extract to value passed. We recursively walk
280 * down the tree setting all children if the
281 * node is a directory.
283 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
290 node->extract = extract;
291 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
292 node->extract_dir = extract; /* set/clear dir too */
294 if (node->type != TN_NEWDIR) {
297 /* For a non-file (i.e. directory), we see all the children */
298 if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
299 /* Recursive set children within directory */
300 foreach_child(n, node) {
301 count += set_extract(ua, n, tree, extract);
304 * Walk up tree marking any unextracted parent to be
308 while (node->parent && !node->parent->extract_dir) {
310 node->extract_dir = true;
313 } else if (extract) {
316 * Ordinary file, we get the full path, look up the
317 * attributes, decode them, and if we are hard linked to
318 * a file that was saved, we must load that file too.
320 tree_getpath(node, cwd, sizeof(cwd));
322 fdbr.JobId = node->JobId;
323 if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
325 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
327 * If we point to a hard linked file, traverse the tree to
328 * find that file, and mark it to be restored as well. It
329 * must have the Link we just obtained and the same JobId.
332 for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
333 if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
335 if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
336 n->extract_dir = true;
347 static void strip_trailing_slash(char *arg)
349 int len = strlen(arg);
354 if (arg[len] == '/') { /* strip any trailing slash */
360 * Recursively mark the current directory to be restored as
361 * well as all directories and files below it.
363 static int markcmd(UAContext *ua, TREE_CTX *tree)
369 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
370 ua->send_msg(_("No files marked.\n"));
373 for (int i=1; i < ua->argc; i++) {
374 strip_trailing_slash(ua->argk[i]);
375 foreach_child(node, tree->node) {
376 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
377 count += set_extract(ua, node, tree, true);
382 ua->send_msg(_("No files marked.\n"));
383 } else if (count == 1) {
384 ua->send_msg(_("1 file marked.\n"));
386 ua->send_msg(_("%s files marked.\n"),
387 edit_uint64_with_commas(count, ec1));
392 static int markdircmd(UAContext *ua, TREE_CTX *tree)
398 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
399 ua->send_msg(_("No files marked.\n"));
402 for (int i=1; i < ua->argc; i++) {
403 strip_trailing_slash(ua->argk[i]);
404 foreach_child(node, tree->node) {
405 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
406 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
407 node->extract_dir = true;
414 ua->send_msg(_("No directories marked.\n"));
415 } else if (count == 1) {
416 ua->send_msg(_("1 directory marked.\n"));
418 ua->send_msg(_("%s directories marked.\n"),
419 edit_uint64_with_commas(count, ec1));
425 static int countcmd(UAContext *ua, TREE_CTX *tree)
427 int total, num_extract;
428 char ec1[50], ec2[50];
430 total = num_extract = 0;
431 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
432 if (node->type != TN_NEWDIR) {
434 if (node->extract || node->extract_dir) {
439 ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
440 edit_uint64_with_commas(total, ec1),
441 edit_uint64_with_commas(num_extract, ec2));
445 static int findcmd(UAContext *ua, TREE_CTX *tree)
450 ua->send_msg(_("No file specification given.\n"));
451 return 1; /* make it non-fatal */
454 for (int i=1; i < ua->argc; i++) {
455 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
456 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
458 tree_getpath(node, cwd, sizeof(cwd));
461 } else if (node->extract_dir) {
466 ua->send_msg("%s%s\n", tag, cwd);
473 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
477 if (!tree_node_has_child(tree->node)) {
481 foreach_child(node, tree->node) {
482 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
483 if (tree_node_has_child(node)) {
484 ua->send_msg("%s/\n", node->fname);
492 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
494 for (int i=0; i<comsize; i++) {
495 /* List only non-dot commands */
496 if (commands[i].key[0] != '.') {
497 ua->send_msg("%s\n", commands[i].key);
503 static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
507 if (!tree_node_has_child(tree->node)) {
511 foreach_child(node, tree->node) {
512 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
513 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
520 static int lscmd(UAContext *ua, TREE_CTX *tree)
524 if (!tree_node_has_child(tree->node)) {
527 foreach_child(node, tree->node) {
528 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
532 } else if (node->extract_dir) {
537 ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
544 * Ls command that lists only the marked files
546 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
549 if (!tree_node_has_child(tree->node)) {
552 foreach_child(node, tree->node) {
553 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
554 (node->extract || node->extract_dir)) {
555 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
562 * This recursive ls command that lists only the marked files
564 static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
567 const int max_level = 100;
568 char indent[max_level*2+1];
570 if (!tree_node_has_child(tnode)) {
573 level = MIN(level, max_level);
575 for (i=0; i<level; i++) {
580 foreach_child(node, tnode) {
581 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
582 (node->extract || node->extract_dir)) {
586 } else if (node->extract_dir) {
591 ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
592 if (tree_node_has_child(node)) {
593 rlsmark(ua, node, level+1);
599 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
601 rlsmark(ua, tree->node, 0);
606 * This is actually the long form used for "dir"
608 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
609 struct stat *statp, bool dot_cmd)
614 char en1[30], en2[30];
618 p = encode_mode(statp->st_mode, buf);
621 n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
623 n = sprintf(p, "%s,%s,",
624 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
625 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
627 n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
629 p = encode_time(statp->st_mtime, p);
634 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
636 n = sprintf(p, "%-8.8s %-8.8s",
637 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
638 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
640 n = sprintf(p, "%12.12s ", edit_int64(statp->st_size, ec1));
642 if (statp->st_ctime > statp->st_mtime) {
643 time = statp->st_ctime;
645 time = statp->st_mtime;
647 /* Display most recent time */
648 p = encode_time(time, p);
652 for (f=fname; *f; ) {
659 * Like ls command, but give more detail on each file
661 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
667 char cwd[1100], *pcwd;
670 if (!tree_node_has_child(tree->node)) {
671 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
675 guid = new_guid_list();
676 foreach_child(node, tree->node) {
678 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
681 } else if (node->extract_dir) {
686 tree_getpath(node, cwd, sizeof(cwd));
688 fdbr.JobId = node->JobId;
690 * Strip / from soft links to directories.
691 * This is because soft links to files have a trailing slash
692 * when returned from tree_getpath, but db_get_file_attr...
693 * treats soft links as files, so they do not have a trailing
694 * slash like directory names.
696 if (node->type == TN_FILE && tree_node_has_child(node)) {
697 bstrncpy(buf, cwd, sizeof(buf));
699 int len = strlen(buf);
701 buf[len-1] = 0; /* strip trailing / */
706 if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
708 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
710 /* Something went wrong getting attributes -- print name */
711 memset(&statp, 0, sizeof(statp));
713 ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
714 ua->send_msg("%s\n", buf);
717 free_guid_list(guid);
721 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
723 return do_dircmd(ua, tree, true/*dot command*/);
726 static int dircmd(UAContext *ua, TREE_CTX *tree)
728 return do_dircmd(ua, tree, false/*not dot command*/);
732 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
734 int total, num_extract;
735 uint64_t total_bytes = 0;
741 total = num_extract = 0;
742 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
743 if (node->type != TN_NEWDIR) {
745 /* If regular file, get size */
746 if (node->extract && node->type == TN_FILE) {
748 tree_getpath(node, cwd, sizeof(cwd));
750 fdbr.JobId = node->JobId;
751 if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
753 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
754 if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
755 total_bytes += statp.st_size;
758 /* Directory, count only */
759 } else if (node->extract || node->extract_dir) {
764 ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
765 total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
771 static int helpcmd(UAContext *ua, TREE_CTX *tree)
775 ua->send_msg(_(" Command Description\n ======= ===========\n"));
776 for (i=0; i<comsize; i++) {
777 /* List only non-dot commands */
778 if (commands[i].key[0] != '.') {
779 ua->send_msg(" %-10s %s\n", _(commands[i].key), _(commands[i].help));
787 * Change directories. Note, if the user specifies x: and it fails,
788 * we assume it is a Win32 absolute cd rather than relative and
789 * try a second time with /x: ... Win32 kludge.
791 static int cdcmd(UAContext *ua, TREE_CTX *tree)
798 ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
801 if (!tree_node_has_child(tree->node)) {
802 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
806 node = tree_cwd(ua->argk[1], tree->root, tree->node);
808 /* Try once more if Win32 drive -- make absolute */
809 if (ua->argk[1][1] == ':') { /* win32 drive */
810 bstrncpy(cwd, "/", sizeof(cwd));
811 bstrncat(cwd, ua->argk[1], sizeof(cwd));
812 node = tree_cwd(cwd, tree->root, tree->node);
815 ua->warning_msg(_("Invalid path given.\n"));
822 return pwdcmd(ua, tree);
825 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
828 tree_getpath(tree->node, cwd, sizeof(cwd));
830 ua->send_msg("%s", cwd);
832 ua->send_msg(_("cwd is: %s\n"), cwd);
837 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
840 tree_getpath(tree->node, cwd, sizeof(cwd));
841 ua->send_msg("%s", cwd);
845 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
850 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
851 ua->send_msg(_("No files unmarked.\n"));
854 for (int i=1; i < ua->argc; i++) {
855 strip_trailing_slash(ua->argk[i]);
856 foreach_child(node, tree->node) {
857 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
858 count += set_extract(ua, node, tree, false);
863 ua->send_msg(_("No files unmarked.\n"));
864 } else if (count == 1) {
865 ua->send_msg(_("1 file unmarked.\n"));
868 ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
873 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
878 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
879 ua->send_msg(_("No directories unmarked.\n"));
883 for (int i=1; i < ua->argc; i++) {
884 strip_trailing_slash(ua->argk[i]);
885 foreach_child(node, tree->node) {
886 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
887 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
888 node->extract_dir = false;
896 ua->send_msg(_("No directories unmarked.\n"));
897 } else if (count == 1) {
898 ua->send_msg(_("1 directory unmarked.\n"));
900 ua->send_msg(_("%d directories unmarked.\n"), count);
906 static int donecmd(UAContext *ua, TREE_CTX *tree)
911 static int quitcmd(UAContext *ua, TREE_CTX *tree)