2 Bacula(R) - The Network Backup Solution
4 Copyright (C) 2000-2015 Kern Sibbald
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many 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 This notice must be preserved when any source code is
15 conveyed and/or propagated.
17 Bacula(R) is a registered trademark of Kern Sibbald.
21 * Bacula Director -- User Agent Database File tree for Restore
22 * command. This file interacts with the user implementing the
25 * Kern Sibbald, July MMII
34 #include "lib/fnmatch.h"
36 #include "findlib/find.h"
39 /* Forward referenced commands */
41 static int markcmd(UAContext *ua, TREE_CTX *tree);
42 static int markdircmd(UAContext *ua, TREE_CTX *tree);
43 static int countcmd(UAContext *ua, TREE_CTX *tree);
44 static int findcmd(UAContext *ua, TREE_CTX *tree);
45 static int lscmd(UAContext *ua, TREE_CTX *tree);
46 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
47 static int dircmd(UAContext *ua, TREE_CTX *tree);
48 static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
49 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
50 static int helpcmd(UAContext *ua, TREE_CTX *tree);
51 static int cdcmd(UAContext *ua, TREE_CTX *tree);
52 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
53 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
54 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
55 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
56 static int quitcmd(UAContext *ua, TREE_CTX *tree);
57 static int donecmd(UAContext *ua, TREE_CTX *tree);
58 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree);
59 static int dot_lscmd(UAContext *ua, TREE_CTX *tree);
60 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree);
61 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree);
63 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
64 static struct cmdstruct commands[] = {
65 { NT_("add"), markcmd, _("add dir/file to be restored recursively, wildcards allowed")},
66 { NT_("cd"), cdcmd, _("change current directory")},
67 { NT_("count"), countcmd, _("count marked files in and below the cd")},
68 { NT_("delete"), unmarkcmd, _("delete dir/file to be restored recursively in dir")},
69 { NT_("dir"), dircmd, _("long list current directory, wildcards allowed")},
70 { NT_(".dir"), dot_dircmd, _("long list current directory, wildcards allowed")},
71 { NT_("done"), donecmd, _("leave file selection mode")},
72 { NT_("estimate"), estimatecmd, _("estimate restore size")},
73 { NT_("exit"), donecmd, _("same as done command")},
74 { NT_("find"), findcmd, _("find files, wildcards allowed")},
75 { NT_("help"), helpcmd, _("print help")},
76 { NT_("ls"), lscmd, _("list current directory, wildcards allowed")},
77 { NT_(".ls"), dot_lscmd, _("list current directory, wildcards allowed")},
78 { NT_(".lsdir"), dot_lsdircmd, _("list subdir in current directory, wildcards allowed")},
79 { NT_("lsmark"), lsmarkcmd, _("list the marked files in and below the cd")},
80 { NT_(".lsmark"), dot_lsmarkcmd,_("list the marked files in")},
81 { NT_("mark"), markcmd, _("mark dir/file to be restored recursively, wildcards allowed")},
82 { NT_("markdir"), markdircmd, _("mark directory name to be restored (no files)")},
83 { NT_("pwd"), pwdcmd, _("print current working directory")},
84 { NT_(".pwd"), dot_pwdcmd, _("print current working directory")},
85 { NT_("unmark"), unmarkcmd, _("unmark dir/file to be restored recursively in dir")},
86 { NT_("unmarkdir"), unmarkdircmd, _("unmark directory name only no recursion")},
87 { NT_("quit"), quitcmd, _("quit and do not do restore")},
88 { NT_(".help"), dot_helpcmd, _("print help")},
89 { NT_("?"), helpcmd, _("print help")},
91 #define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
94 * Enter a prompt mode where the user can select/deselect
95 * files to be restored. This is sort of like a mini-shell
96 * that allows "cd", "pwd", "add", "rm", ...
98 bool user_select_files_from_tree(TREE_CTX *tree)
102 /* Get a new context so we don't destroy restore command args */
103 UAContext *ua = new_ua_context(tree->ua->jcr);
104 ua->UA_sock = tree->ua->UA_sock; /* patch in UA socket */
105 ua->api = tree->ua->api; /* keep API flag too */
106 BSOCK *user = ua->UA_sock;
109 "\nYou are now entering file selection mode where you add (mark) and\n"
110 "remove (unmark) files to be restored. No files are initially added, unless\n"
111 "you used the \"all\" keyword on the command line.\n"
112 "Enter \"done\" to leave this mode.\n\n"));
113 if (ua->api) user->signal(BNET_START_RTREE);
115 * Enter interactive command handler allowing selection
116 * of individual files.
118 tree->node = (TREE_NODE *)tree->root;
119 tree_getpath(tree->node, cwd, sizeof(cwd));
120 ua->send_msg(_("cwd is: %s\n"), cwd);
123 if (!get_cmd(ua, "$ ", true)) {
126 if (ua->api) user->signal(BNET_CMD_BEGIN);
127 parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
129 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
130 if (ua->api) user->signal(BNET_CMD_FAILED);
134 len = strlen(ua->argk[0]);
137 for (i=0; i<comsize; i++) /* search for command */
138 if (strncasecmp(ua->argk[0], commands[i].key, len) == 0) {
139 stat = (*commands[i].func)(ua, tree); /* go execute command */
144 if (*ua->argk[0] == '.') {
145 /* Some unknow dot command -- probably .messages, ignore it */
148 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
149 if (ua->api) user->signal(BNET_CMD_FAILED);
152 if (ua->api) user->signal(BNET_CMD_OK);
157 if (ua->api) user->signal(BNET_END_RTREE);
158 ua->UA_sock = NULL; /* don't release restore socket */
161 free_ua_context(ua); /* get rid of temp UA context */
166 * This callback routine is responsible for inserting the
167 * items it gets into the directory tree. For each JobId selected
168 * this routine is called once for each file. We do not allow
169 * duplicate filenames, but instead keep the info from the most
170 * recent file entered (i.e. the JobIds are assumed to be sorted)
172 * See uar_sel_files in sql_cmds.c for query that calls us.
173 * row[0]=Path, row[1]=Filename, row[2]=FileIndex
174 * row[3]=JobId row[4]=LStat row[5]=DeltaSeq
176 int insert_tree_handler(void *ctx, int num_fields, char **row)
179 TREE_CTX *tree = (TREE_CTX *)ctx;
186 HL_ENTRY *entry = NULL;
189 Dmsg4(150, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
191 if (*row[1] == 0) { /* no filename => directory */
192 if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
200 decode_stat(row[4], &statp, sizeof(statp), &LinkFI);
201 hard_link = (LinkFI != 0);
202 node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
203 JobId = str_to_int64(row[3]);
204 FileIndex = str_to_int64(row[2]);
205 delta_seq = str_to_int64(row[5]);
206 Dmsg6(150, "node=0x%p JobId=%s FileIndex=%s Delta=%s node.delta=%d LinkFI=%d\n",
207 node, row[3], row[2], row[5], node->delta_seq, LinkFI);
209 /* TODO: check with hardlinks */
211 if (delta_seq == (node->delta_seq + 1)) {
212 tree_add_delta_part(tree->root, node, node->JobId, node->FileIndex);
215 /* File looks to be deleted */
216 if (node->delta_seq == -1) { /* just created */
217 tree_remove_node(tree->root, node);
220 tree->ua->warning_msg(_("Something is wrong with the Delta sequence of %s, "
221 "skiping new parts. Current sequence is %d\n"),
222 row[1], node->delta_seq);
224 Dmsg3(0, "Something is wrong with Delta, skip it "
225 "fname=%s d1=%d d2=%d\n", row[1], node->delta_seq, delta_seq);
231 * - The first time we see a file (node->inserted==true), we accept it.
232 * - In the same JobId, we accept only the first copy of a
233 * hard linked file (the others are simply pointers).
234 * - In the same JobId, we accept the last copy of any other
235 * file -- in particular directories.
237 * All the code to set ok could be condensed to a single
238 * line, but it would be even harder to read.
241 if (!node->inserted && JobId == node->JobId) {
242 if ((hard_link && FileIndex > node->FileIndex) ||
243 (!hard_link && FileIndex < node->FileIndex)) {
248 node->hard_link = hard_link;
249 node->FileIndex = FileIndex;
252 node->soft_link = S_ISLNK(statp.st_mode) != 0;
253 node->delta_seq = delta_seq;
256 node->extract = true; /* extract all by default */
257 if (type == TN_DIR || type == TN_DIR_NLS) {
258 node->extract_dir = true; /* if dir, extract it */
261 /* insert file having hardlinks into hardlink hashtable */
262 if (statp.st_nlink > 1 && type != TN_DIR && type != TN_DIR_NLS) {
264 /* first occurrence - file hardlinked to */
265 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
266 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
268 tree->root->hardlinks.insert(entry->key, entry);
269 } else if (tree->hardlinks_in_mem) {
270 /* hardlink to known file index: lookup original file */
271 uint64_t file_key = (((uint64_t) JobId) << 32) + LinkFI;
272 HL_ENTRY *first_hl = (HL_ENTRY *) tree->root->hardlinks.lookup(file_key);
273 if (first_hl && first_hl->node) {
274 /* then add hardlink entry to linked node*/
275 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
276 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
277 entry->node = first_hl->node;
278 tree->root->hardlinks.insert(entry->key, entry);
283 if (node->inserted) {
285 if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
286 tree->ua->send_msg("+");
287 tree->LastCount = tree->FileCount;
295 * Set extract to value passed. We recursively walk
296 * down the tree setting all children if the
297 * node is a directory.
299 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
304 node->extract = extract;
305 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
306 node->extract_dir = extract; /* set/clear dir too */
308 if (node->type != TN_NEWDIR) {
311 /* For a non-file (i.e. directory), we see all the children */
312 if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
313 /* Recursive set children within directory */
314 foreach_child(n, node) {
315 count += set_extract(ua, n, tree, extract);
318 * Walk up tree marking any unextracted parent to be
322 while (node->parent && !node->parent->extract_dir) {
324 node->extract_dir = true;
327 } else if (extract) {
329 if (tree->hardlinks_in_mem) {
330 if (node->hard_link) {
331 key = (((uint64_t) node->JobId) << 32) + node->FileIndex; /* every hardlink is in hashtable, and it points to linked file */
334 /* Get the hard link if it exists */
339 * Ordinary file, we get the full path, look up the
340 * attributes, decode them, and if we are hard linked to
341 * a file that was saved, we must load that file too.
343 tree_getpath(node, cwd, sizeof(cwd));
345 fdbr.JobId = node->JobId;
346 if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
348 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
349 key = (((uint64_t) node->JobId) << 32) + LinkFI; /* lookup by linked file's fileindex */
352 /* If file hard linked and we have a key */
353 if (node->hard_link && key != 0) {
355 * If we point to a hard linked file, find that file in
356 * hardlinks hashmap, and mark it to be restored as well.
358 HL_ENTRY *entry = (HL_ENTRY *)tree->root->hardlinks.lookup(key);
359 if (entry && entry->node) {
362 n->extract_dir = (n->type == TN_DIR || n->type == TN_DIR_NLS);
369 static void strip_trailing_slash(char *arg)
371 int len = strlen(arg);
376 if (arg[len] == '/') { /* strip any trailing slash */
382 * Recursively mark the current directory to be restored as
383 * well as all directories and files below it.
385 static int markcmd(UAContext *ua, TREE_CTX *tree)
391 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
392 ua->send_msg(_("No files marked.\n"));
395 for (int i=1; i < ua->argc; i++) {
396 strip_trailing_slash(ua->argk[i]);
397 foreach_child(node, tree->node) {
398 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
399 count += set_extract(ua, node, tree, true);
404 ua->send_msg(_("No files marked.\n"));
405 } else if (count == 1) {
406 ua->send_msg(_("1 file marked.\n"));
408 ua->send_msg(_("%s files marked.\n"),
409 edit_uint64_with_commas(count, ec1));
414 static int markdircmd(UAContext *ua, TREE_CTX *tree)
420 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
421 ua->send_msg(_("No files marked.\n"));
424 for (int i=1; i < ua->argc; i++) {
425 strip_trailing_slash(ua->argk[i]);
426 foreach_child(node, tree->node) {
427 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
428 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
429 node->extract_dir = true;
436 ua->send_msg(_("No directories marked.\n"));
437 } else if (count == 1) {
438 ua->send_msg(_("1 directory marked.\n"));
440 ua->send_msg(_("%s directories marked.\n"),
441 edit_uint64_with_commas(count, ec1));
447 static int countcmd(UAContext *ua, TREE_CTX *tree)
449 int total, num_extract;
450 char ec1[50], ec2[50];
452 total = num_extract = 0;
453 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
454 if (node->type != TN_NEWDIR) {
456 if (node->extract || node->extract_dir) {
461 ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
462 edit_uint64_with_commas(total, ec1),
463 edit_uint64_with_commas(num_extract, ec2));
467 static int findcmd(UAContext *ua, TREE_CTX *tree)
472 ua->send_msg(_("No file specification given.\n"));
473 return 1; /* make it non-fatal */
476 for (int i=1; i < ua->argc; i++) {
477 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
478 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
480 tree_getpath(node, cwd, sizeof(cwd));
483 } else if (node->extract_dir) {
488 ua->send_msg("%s%s\n", tag, cwd);
495 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
499 if (!tree_node_has_child(tree->node)) {
503 foreach_child(node, tree->node) {
504 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
505 if (tree_node_has_child(node)) {
506 ua->send_msg("%s/\n", node->fname);
514 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
516 for (int i=0; i<comsize; i++) {
517 /* List only non-dot commands */
518 if (commands[i].key[0] != '.') {
519 ua->send_msg("%s\n", commands[i].key);
525 static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
529 if (!tree_node_has_child(tree->node)) {
533 foreach_child(node, tree->node) {
534 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
535 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
542 static int lscmd(UAContext *ua, TREE_CTX *tree)
546 if (!tree_node_has_child(tree->node)) {
549 foreach_child(node, tree->node) {
550 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
554 } else if (node->extract_dir) {
559 ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
566 * Ls command that lists only the marked files
568 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
571 if (!tree_node_has_child(tree->node)) {
574 foreach_child(node, tree->node) {
575 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
576 (node->extract || node->extract_dir)) {
577 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
584 * This recursive ls command that lists only the marked files
586 static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
589 const int max_level = 100;
590 char indent[max_level*2+1];
592 if (!tree_node_has_child(tnode)) {
595 level = MIN(level, max_level);
597 for (i=0; i<level; i++) {
602 foreach_child(node, tnode) {
603 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
604 (node->extract || node->extract_dir)) {
608 } else if (node->extract_dir) {
613 ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
614 if (tree_node_has_child(node)) {
615 rlsmark(ua, node, level+1);
621 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
623 rlsmark(ua, tree->node, 0);
628 * This is actually the long form used for "dir"
630 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
631 struct stat *statp, bool dot_cmd)
636 char en1[30], en2[30];
640 p = encode_mode(statp->st_mode, buf);
643 n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
645 n = sprintf(p, "%s,%s,",
646 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
647 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
649 n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
651 p = encode_time(statp->st_mtime, p);
656 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
658 n = sprintf(p, "%-8.8s %-8.8s",
659 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
660 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
662 n = sprintf(p, "%12.12s ", edit_int64(statp->st_size, ec1));
664 if (statp->st_ctime > statp->st_mtime) {
665 time = statp->st_ctime;
667 time = statp->st_mtime;
669 /* Display most recent time */
670 p = encode_time(time, p);
674 for (f=fname; *f; ) {
681 * Like ls command, but give more detail on each file
683 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
689 char cwd[1100], *pcwd;
692 if (!tree_node_has_child(tree->node)) {
693 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
697 guid = new_guid_list();
698 foreach_child(node, tree->node) {
700 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
703 } else if (node->extract_dir) {
708 tree_getpath(node, cwd, sizeof(cwd));
710 fdbr.JobId = node->JobId;
712 * Strip / from soft links to directories.
713 * This is because soft links to files have a trailing slash
714 * when returned from tree_getpath, but db_get_file_attr...
715 * treats soft links as files, so they do not have a trailing
716 * slash like directory names.
718 if (node->type == TN_FILE && tree_node_has_child(node)) {
719 bstrncpy(buf, cwd, sizeof(buf));
721 int len = strlen(buf);
723 buf[len-1] = 0; /* strip trailing / */
728 if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
730 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
732 /* Something went wrong getting attributes -- print name */
733 memset(&statp, 0, sizeof(statp));
735 ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
736 ua->send_msg("%s\n", buf);
739 free_guid_list(guid);
743 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
745 return do_dircmd(ua, tree, true/*dot command*/);
748 static int dircmd(UAContext *ua, TREE_CTX *tree)
750 return do_dircmd(ua, tree, false/*not dot command*/);
754 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
756 int total, num_extract;
757 uint64_t total_bytes = 0;
763 total = num_extract = 0;
764 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
765 if (node->type != TN_NEWDIR) {
767 /* If regular file, get size */
768 if (node->extract && node->type == TN_FILE) {
770 tree_getpath(node, cwd, sizeof(cwd));
772 fdbr.JobId = node->JobId;
773 if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
775 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
776 if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
777 total_bytes += statp.st_size;
780 /* Directory, count only */
781 } else if (node->extract || node->extract_dir) {
786 ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
787 total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
793 static int helpcmd(UAContext *ua, TREE_CTX *tree)
797 ua->send_msg(_(" Command Description\n ======= ===========\n"));
798 for (i=0; i<comsize; i++) {
799 /* List only non-dot commands */
800 if (commands[i].key[0] != '.') {
801 ua->send_msg(" %-10s %s\n", _(commands[i].key), _(commands[i].help));
809 * Change directories. Note, if the user specifies x: and it fails,
810 * we assume it is a Win32 absolute cd rather than relative and
811 * try a second time with /x: ... Win32 kludge.
813 static int cdcmd(UAContext *ua, TREE_CTX *tree)
820 ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
824 node = tree_cwd(ua->argk[1], tree->root, tree->node);
826 /* Try once more if Win32 drive -- make absolute */
827 if (ua->argk[1][1] == ':') { /* win32 drive */
828 bstrncpy(cwd, "/", sizeof(cwd));
829 bstrncat(cwd, ua->argk[1], sizeof(cwd));
830 node = tree_cwd(cwd, tree->root, tree->node);
833 ua->warning_msg(_("Invalid path given.\n"));
840 return pwdcmd(ua, tree);
843 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
846 tree_getpath(tree->node, cwd, sizeof(cwd));
848 ua->send_msg("%s", cwd);
850 ua->send_msg(_("cwd is: %s\n"), cwd);
855 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
858 tree_getpath(tree->node, cwd, sizeof(cwd));
859 ua->send_msg("%s", cwd);
863 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
868 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
869 ua->send_msg(_("No files unmarked.\n"));
872 for (int i=1; i < ua->argc; i++) {
873 strip_trailing_slash(ua->argk[i]);
874 foreach_child(node, tree->node) {
875 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
876 count += set_extract(ua, node, tree, false);
881 ua->send_msg(_("No files unmarked.\n"));
882 } else if (count == 1) {
883 ua->send_msg(_("1 file unmarked.\n"));
886 ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
891 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
896 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
897 ua->send_msg(_("No directories unmarked.\n"));
901 for (int i=1; i < ua->argc; i++) {
902 strip_trailing_slash(ua->argk[i]);
903 foreach_child(node, tree->node) {
904 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
905 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
906 node->extract_dir = false;
914 ua->send_msg(_("No directories unmarked.\n"));
915 } else if (count == 1) {
916 ua->send_msg(_("1 directory unmarked.\n"));
918 ua->send_msg(_("%d directories unmarked.\n"), count);
924 static int donecmd(UAContext *ua, TREE_CTX *tree)
929 static int quitcmd(UAContext *ua, TREE_CTX *tree)