2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2014 Free Software Foundation Europe e.V.
6 The main author of Bacula is Kern Sibbald, with contributions from many
7 others, a complete list can be found in the file AUTHORS.
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
14 Bacula® is a registered trademark of Kern Sibbald.
18 * Bacula Director -- User Agent Database File tree for Restore
19 * command. This file interacts with the user implementing the
22 * Kern Sibbald, July MMII
31 #include "lib/fnmatch.h"
33 #include "findlib/find.h"
36 /* Forward referenced commands */
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 dot_dircmd(UAContext *ua, TREE_CTX *tree);
46 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
47 static int helpcmd(UAContext *ua, TREE_CTX *tree);
48 static int cdcmd(UAContext *ua, TREE_CTX *tree);
49 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
50 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
51 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
52 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
53 static int quitcmd(UAContext *ua, TREE_CTX *tree);
54 static int donecmd(UAContext *ua, TREE_CTX *tree);
55 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree);
56 static int dot_lscmd(UAContext *ua, TREE_CTX *tree);
57 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree);
58 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree);
60 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
61 static struct cmdstruct commands[] = {
62 { NT_("add"), markcmd, _("add dir/file to be restored recursively, wildcards allowed")},
63 { NT_("cd"), cdcmd, _("change current directory")},
64 { NT_("count"), countcmd, _("count marked files in and below the cd")},
65 { NT_("delete"), unmarkcmd, _("delete dir/file to be restored recursively in dir")},
66 { NT_("dir"), dircmd, _("long list current directory, wildcards allowed")},
67 { NT_(".dir"), dot_dircmd, _("long list current directory, wildcards allowed")},
68 { NT_("done"), donecmd, _("leave file selection mode")},
69 { NT_("estimate"), estimatecmd, _("estimate restore size")},
70 { NT_("exit"), donecmd, _("same as done command")},
71 { NT_("find"), findcmd, _("find files, wildcards allowed")},
72 { NT_("help"), helpcmd, _("print help")},
73 { NT_("ls"), lscmd, _("list current directory, wildcards allowed")},
74 { NT_(".ls"), dot_lscmd, _("list current directory, wildcards allowed")},
75 { NT_(".lsdir"), dot_lsdircmd, _("list subdir in current directory, wildcards allowed")},
76 { NT_("lsmark"), lsmarkcmd, _("list the marked files in and below the cd")},
77 { NT_(".lsmark"), dot_lsmarkcmd,_("list the marked files in")},
78 { NT_("mark"), markcmd, _("mark dir/file to be restored recursively, wildcards allowed")},
79 { NT_("markdir"), markdircmd, _("mark directory name to be restored (no files)")},
80 { NT_("pwd"), pwdcmd, _("print current working directory")},
81 { NT_(".pwd"), dot_pwdcmd, _("print current working directory")},
82 { NT_("unmark"), unmarkcmd, _("unmark dir/file to be restored recursively in dir")},
83 { NT_("unmarkdir"), unmarkdircmd, _("unmark directory name only no recursion")},
84 { NT_("quit"), quitcmd, _("quit and do not do restore")},
85 { NT_(".help"), dot_helpcmd, _("print help")},
86 { NT_("?"), helpcmd, _("print help")},
88 #define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
91 * Enter a prompt mode where the user can select/deselect
92 * files to be restored. This is sort of like a mini-shell
93 * that allows "cd", "pwd", "add", "rm", ...
95 bool user_select_files_from_tree(TREE_CTX *tree)
99 /* Get a new context so we don't destroy restore command args */
100 UAContext *ua = new_ua_context(tree->ua->jcr);
101 ua->UA_sock = tree->ua->UA_sock; /* patch in UA socket */
102 ua->api = tree->ua->api; /* keep API flag too */
103 BSOCK *user = ua->UA_sock;
106 "\nYou are now entering file selection mode where you add (mark) and\n"
107 "remove (unmark) files to be restored. No files are initially added, unless\n"
108 "you used the \"all\" keyword on the command line.\n"
109 "Enter \"done\" to leave this mode.\n\n"));
110 if (ua->api) user->signal(BNET_START_RTREE);
112 * Enter interactive command handler allowing selection
113 * of individual files.
115 tree->node = (TREE_NODE *)tree->root;
116 tree_getpath(tree->node, cwd, sizeof(cwd));
117 ua->send_msg(_("cwd is: %s\n"), cwd);
120 if (!get_cmd(ua, "$ ", true)) {
123 if (ua->api) user->signal(BNET_CMD_BEGIN);
124 parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
126 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
127 if (ua->api) user->signal(BNET_CMD_FAILED);
131 len = strlen(ua->argk[0]);
134 for (i=0; i<comsize; i++) /* search for command */
135 if (strncasecmp(ua->argk[0], commands[i].key, len) == 0) {
136 stat = (*commands[i].func)(ua, tree); /* go execute command */
141 if (*ua->argk[0] == '.') {
142 /* Some unknow dot command -- probably .messages, ignore it */
145 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
146 if (ua->api) user->signal(BNET_CMD_FAILED);
149 if (ua->api) user->signal(BNET_CMD_OK);
154 if (ua->api) user->signal(BNET_END_RTREE);
155 ua->UA_sock = NULL; /* don't release restore socket */
158 free_ua_context(ua); /* get rid of temp UA context */
163 * This callback routine is responsible for inserting the
164 * items it gets into the directory tree. For each JobId selected
165 * this routine is called once for each file. We do not allow
166 * duplicate filenames, but instead keep the info from the most
167 * recent file entered (i.e. the JobIds are assumed to be sorted)
169 * See uar_sel_files in sql_cmds.c for query that calls us.
170 * row[0]=Path, row[1]=Filename, row[2]=FileIndex
171 * row[3]=JobId row[4]=LStat row[5]=DeltaSeq
173 int insert_tree_handler(void *ctx, int num_fields, char **row)
176 TREE_CTX *tree = (TREE_CTX *)ctx;
183 HL_ENTRY *entry = NULL;
186 Dmsg4(150, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
188 if (*row[1] == 0) { /* no filename => directory */
189 if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
197 decode_stat(row[4], &statp, sizeof(statp), &LinkFI);
198 hard_link = (LinkFI != 0);
199 node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
200 JobId = str_to_int64(row[3]);
201 FileIndex = str_to_int64(row[2]);
202 delta_seq = str_to_int64(row[5]);
203 Dmsg6(150, "node=0x%p JobId=%s FileIndex=%s Delta=%s node.delta=%d LinkFI=%d\n",
204 node, row[3], row[2], row[5], node->delta_seq, LinkFI);
206 /* TODO: check with hardlinks */
208 if (delta_seq == (node->delta_seq + 1)) {
209 tree_add_delta_part(tree->root, node, node->JobId, node->FileIndex);
212 /* File looks to be deleted */
213 if (node->delta_seq == -1) { /* just created */
214 tree_remove_node(tree->root, node);
217 tree->ua->warning_msg(_("Something is wrong with the Delta sequence of %s, "
218 "skiping new parts. Current sequence is %d\n"),
219 row[1], node->delta_seq);
221 Dmsg3(0, "Something is wrong with Delta, skip it "
222 "fname=%s d1=%d d2=%d\n", row[1], node->delta_seq, delta_seq);
228 * - The first time we see a file (node->inserted==true), we accept it.
229 * - In the same JobId, we accept only the first copy of a
230 * hard linked file (the others are simply pointers).
231 * - In the same JobId, we accept the last copy of any other
232 * file -- in particular directories.
234 * All the code to set ok could be condensed to a single
235 * line, but it would be even harder to read.
238 if (!node->inserted && JobId == node->JobId) {
239 if ((hard_link && FileIndex > node->FileIndex) ||
240 (!hard_link && FileIndex < node->FileIndex)) {
245 node->hard_link = hard_link;
246 node->FileIndex = FileIndex;
249 node->soft_link = S_ISLNK(statp.st_mode) != 0;
250 node->delta_seq = delta_seq;
253 node->extract = true; /* extract all by default */
254 if (type == TN_DIR || type == TN_DIR_NLS) {
255 node->extract_dir = true; /* if dir, extract it */
258 /* insert file having hardlinks into hardlink hashtable */
259 if (statp.st_nlink > 1 && type != TN_DIR && type != TN_DIR_NLS) {
261 /* first occurrence - file hardlinked to */
262 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
263 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
265 tree->root->hardlinks.insert(entry->key, entry);
266 } else if (tree->hardlinks_in_mem) {
267 /* hardlink to known file index: lookup original file */
268 uint64_t file_key = (((uint64_t) JobId) << 32) + LinkFI;
269 HL_ENTRY *first_hl = (HL_ENTRY *) tree->root->hardlinks.lookup(file_key);
270 if (first_hl && first_hl->node) {
271 /* then add hardlink entry to linked node*/
272 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
273 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
274 entry->node = first_hl->node;
275 tree->root->hardlinks.insert(entry->key, entry);
280 if (node->inserted) {
282 if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
283 tree->ua->send_msg("+");
284 tree->LastCount = tree->FileCount;
292 * Set extract to value passed. We recursively walk
293 * down the tree setting all children if the
294 * node is a directory.
296 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
301 node->extract = extract;
302 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
303 node->extract_dir = extract; /* set/clear dir too */
305 if (node->type != TN_NEWDIR) {
308 /* For a non-file (i.e. directory), we see all the children */
309 if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
310 /* Recursive set children within directory */
311 foreach_child(n, node) {
312 count += set_extract(ua, n, tree, extract);
315 * Walk up tree marking any unextracted parent to be
319 while (node->parent && !node->parent->extract_dir) {
321 node->extract_dir = true;
324 } else if (extract) {
326 if (tree->hardlinks_in_mem) {
327 if (node->hard_link) {
328 key = (((uint64_t) node->JobId) << 32) + node->FileIndex; /* every hardlink is in hashtable, and it points to linked file */
331 /* Get the hard link if it exists */
336 * Ordinary file, we get the full path, look up the
337 * attributes, decode them, and if we are hard linked to
338 * a file that was saved, we must load that file too.
340 tree_getpath(node, cwd, sizeof(cwd));
342 fdbr.JobId = node->JobId;
343 if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
345 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
346 key = (((uint64_t) node->JobId) << 32) + LinkFI; /* lookup by linked file's fileindex */
349 /* If file hard linked and we have a key */
350 if (node->hard_link && key != 0) {
352 * If we point to a hard linked file, find that file in
353 * hardlinks hashmap, and mark it to be restored as well.
355 HL_ENTRY *entry = (HL_ENTRY *)tree->root->hardlinks.lookup(key);
356 if (entry && entry->node) {
359 n->extract_dir = (n->type == TN_DIR || n->type == TN_DIR_NLS);
366 static void strip_trailing_slash(char *arg)
368 int len = strlen(arg);
373 if (arg[len] == '/') { /* strip any trailing slash */
379 * Recursively mark the current directory to be restored as
380 * well as all directories and files below it.
382 static int markcmd(UAContext *ua, TREE_CTX *tree)
388 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
389 ua->send_msg(_("No files marked.\n"));
392 for (int i=1; i < ua->argc; i++) {
393 strip_trailing_slash(ua->argk[i]);
394 foreach_child(node, tree->node) {
395 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
396 count += set_extract(ua, node, tree, true);
401 ua->send_msg(_("No files marked.\n"));
402 } else if (count == 1) {
403 ua->send_msg(_("1 file marked.\n"));
405 ua->send_msg(_("%s files marked.\n"),
406 edit_uint64_with_commas(count, ec1));
411 static int markdircmd(UAContext *ua, TREE_CTX *tree)
417 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
418 ua->send_msg(_("No files marked.\n"));
421 for (int i=1; i < ua->argc; i++) {
422 strip_trailing_slash(ua->argk[i]);
423 foreach_child(node, tree->node) {
424 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
425 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
426 node->extract_dir = true;
433 ua->send_msg(_("No directories marked.\n"));
434 } else if (count == 1) {
435 ua->send_msg(_("1 directory marked.\n"));
437 ua->send_msg(_("%s directories marked.\n"),
438 edit_uint64_with_commas(count, ec1));
444 static int countcmd(UAContext *ua, TREE_CTX *tree)
446 int total, num_extract;
447 char ec1[50], ec2[50];
449 total = num_extract = 0;
450 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
451 if (node->type != TN_NEWDIR) {
453 if (node->extract || node->extract_dir) {
458 ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
459 edit_uint64_with_commas(total, ec1),
460 edit_uint64_with_commas(num_extract, ec2));
464 static int findcmd(UAContext *ua, TREE_CTX *tree)
469 ua->send_msg(_("No file specification given.\n"));
470 return 1; /* make it non-fatal */
473 for (int i=1; i < ua->argc; i++) {
474 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
475 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
477 tree_getpath(node, cwd, sizeof(cwd));
480 } else if (node->extract_dir) {
485 ua->send_msg("%s%s\n", tag, cwd);
492 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
496 if (!tree_node_has_child(tree->node)) {
500 foreach_child(node, tree->node) {
501 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
502 if (tree_node_has_child(node)) {
503 ua->send_msg("%s/\n", node->fname);
511 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
513 for (int i=0; i<comsize; i++) {
514 /* List only non-dot commands */
515 if (commands[i].key[0] != '.') {
516 ua->send_msg("%s\n", commands[i].key);
522 static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
526 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) {
532 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
539 static int lscmd(UAContext *ua, TREE_CTX *tree)
543 if (!tree_node_has_child(tree->node)) {
546 foreach_child(node, tree->node) {
547 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
551 } else if (node->extract_dir) {
556 ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
563 * Ls command that lists only the marked files
565 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
568 if (!tree_node_has_child(tree->node)) {
571 foreach_child(node, tree->node) {
572 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
573 (node->extract || node->extract_dir)) {
574 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
581 * This recursive ls command that lists only the marked files
583 static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
586 const int max_level = 100;
587 char indent[max_level*2+1];
589 if (!tree_node_has_child(tnode)) {
592 level = MIN(level, max_level);
594 for (i=0; i<level; i++) {
599 foreach_child(node, tnode) {
600 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
601 (node->extract || node->extract_dir)) {
605 } else if (node->extract_dir) {
610 ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
611 if (tree_node_has_child(node)) {
612 rlsmark(ua, node, level+1);
618 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
620 rlsmark(ua, tree->node, 0);
625 * This is actually the long form used for "dir"
627 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
628 struct stat *statp, bool dot_cmd)
633 char en1[30], en2[30];
637 p = encode_mode(statp->st_mode, buf);
640 n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
642 n = sprintf(p, "%s,%s,",
643 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
644 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
646 n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
648 p = encode_time(statp->st_mtime, p);
653 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
655 n = sprintf(p, "%-8.8s %-8.8s",
656 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
657 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
659 n = sprintf(p, "%12.12s ", edit_int64(statp->st_size, ec1));
661 if (statp->st_ctime > statp->st_mtime) {
662 time = statp->st_ctime;
664 time = statp->st_mtime;
666 /* Display most recent time */
667 p = encode_time(time, p);
671 for (f=fname; *f; ) {
678 * Like ls command, but give more detail on each file
680 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
686 char cwd[1100], *pcwd;
689 if (!tree_node_has_child(tree->node)) {
690 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
694 guid = new_guid_list();
695 foreach_child(node, tree->node) {
697 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
700 } else if (node->extract_dir) {
705 tree_getpath(node, cwd, sizeof(cwd));
707 fdbr.JobId = node->JobId;
709 * Strip / from soft links to directories.
710 * This is because soft links to files have a trailing slash
711 * when returned from tree_getpath, but db_get_file_attr...
712 * treats soft links as files, so they do not have a trailing
713 * slash like directory names.
715 if (node->type == TN_FILE && tree_node_has_child(node)) {
716 bstrncpy(buf, cwd, sizeof(buf));
718 int len = strlen(buf);
720 buf[len-1] = 0; /* strip trailing / */
725 if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
727 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
729 /* Something went wrong getting attributes -- print name */
730 memset(&statp, 0, sizeof(statp));
732 ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
733 ua->send_msg("%s\n", buf);
736 free_guid_list(guid);
740 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
742 return do_dircmd(ua, tree, true/*dot command*/);
745 static int dircmd(UAContext *ua, TREE_CTX *tree)
747 return do_dircmd(ua, tree, false/*not dot command*/);
751 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
753 int total, num_extract;
754 uint64_t total_bytes = 0;
760 total = num_extract = 0;
761 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
762 if (node->type != TN_NEWDIR) {
764 /* If regular file, get size */
765 if (node->extract && node->type == TN_FILE) {
767 tree_getpath(node, cwd, sizeof(cwd));
769 fdbr.JobId = node->JobId;
770 if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
772 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
773 if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
774 total_bytes += statp.st_size;
777 /* Directory, count only */
778 } else if (node->extract || node->extract_dir) {
783 ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
784 total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
790 static int helpcmd(UAContext *ua, TREE_CTX *tree)
794 ua->send_msg(_(" Command Description\n ======= ===========\n"));
795 for (i=0; i<comsize; i++) {
796 /* List only non-dot commands */
797 if (commands[i].key[0] != '.') {
798 ua->send_msg(" %-10s %s\n", _(commands[i].key), _(commands[i].help));
806 * Change directories. Note, if the user specifies x: and it fails,
807 * we assume it is a Win32 absolute cd rather than relative and
808 * try a second time with /x: ... Win32 kludge.
810 static int cdcmd(UAContext *ua, TREE_CTX *tree)
817 ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
821 node = tree_cwd(ua->argk[1], tree->root, tree->node);
823 /* Try once more if Win32 drive -- make absolute */
824 if (ua->argk[1][1] == ':') { /* win32 drive */
825 bstrncpy(cwd, "/", sizeof(cwd));
826 bstrncat(cwd, ua->argk[1], sizeof(cwd));
827 node = tree_cwd(cwd, tree->root, tree->node);
830 ua->warning_msg(_("Invalid path given.\n"));
837 return pwdcmd(ua, tree);
840 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
843 tree_getpath(tree->node, cwd, sizeof(cwd));
845 ua->send_msg("%s", cwd);
847 ua->send_msg(_("cwd is: %s\n"), cwd);
852 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
855 tree_getpath(tree->node, cwd, sizeof(cwd));
856 ua->send_msg("%s", cwd);
860 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
865 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
866 ua->send_msg(_("No files unmarked.\n"));
869 for (int i=1; i < ua->argc; i++) {
870 strip_trailing_slash(ua->argk[i]);
871 foreach_child(node, tree->node) {
872 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
873 count += set_extract(ua, node, tree, false);
878 ua->send_msg(_("No files unmarked.\n"));
879 } else if (count == 1) {
880 ua->send_msg(_("1 file unmarked.\n"));
883 ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
888 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
893 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
894 ua->send_msg(_("No directories unmarked.\n"));
898 for (int i=1; i < ua->argc; i++) {
899 strip_trailing_slash(ua->argk[i]);
900 foreach_child(node, tree->node) {
901 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
902 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
903 node->extract_dir = false;
911 ua->send_msg(_("No directories unmarked.\n"));
912 } else if (count == 1) {
913 ua->send_msg(_("1 directory unmarked.\n"));
915 ua->send_msg(_("%d directories unmarked.\n"), count);
921 static int donecmd(UAContext *ua, TREE_CTX *tree)
926 static int quitcmd(UAContext *ua, TREE_CTX *tree)