2 Bacula(R) - The Network Backup Solution
4 Copyright (C) 2000-2017 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.
20 * Bacula Director -- User Agent Database File tree for Restore
21 * command. This file interacts with the user implementing the
24 * Kern Sibbald, July MMII
32 #include "lib/fnmatch.h"
34 #include "findlib/find.h"
37 /* Forward referenced commands */
39 static int markcmd(UAContext *ua, TREE_CTX *tree);
40 static int markdircmd(UAContext *ua, TREE_CTX *tree);
41 static int countcmd(UAContext *ua, TREE_CTX *tree);
42 static int findcmd(UAContext *ua, TREE_CTX *tree);
43 static int lscmd(UAContext *ua, TREE_CTX *tree);
44 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
45 static int dircmd(UAContext *ua, TREE_CTX *tree);
46 static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
47 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
48 static int helpcmd(UAContext *ua, TREE_CTX *tree);
49 static int cdcmd(UAContext *ua, TREE_CTX *tree);
50 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
51 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
52 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
53 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
54 static int quitcmd(UAContext *ua, TREE_CTX *tree);
55 static int donecmd(UAContext *ua, TREE_CTX *tree);
56 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree);
57 static int dot_lscmd(UAContext *ua, TREE_CTX *tree);
58 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree);
59 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree);
61 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
62 static struct cmdstruct commands[] = {
63 { NT_("add"), markcmd, _("add dir/file to be restored recursively, wildcards allowed")},
64 { NT_("cd"), cdcmd, _("change current directory")},
65 { NT_("count"), countcmd, _("count marked files in and below the cd")},
66 { NT_("delete"), unmarkcmd, _("delete dir/file to be restored recursively in dir")},
67 { NT_("dir"), dircmd, _("long list current directory, wildcards allowed")},
68 { NT_(".dir"), dot_dircmd, _("long list current directory, wildcards allowed")},
69 { NT_("done"), donecmd, _("leave file selection mode")},
70 { NT_("estimate"), estimatecmd, _("estimate restore size")},
71 { NT_("exit"), donecmd, _("same as done command")},
72 { NT_("find"), findcmd, _("find files, wildcards allowed")},
73 { NT_("help"), helpcmd, _("print help")},
74 { NT_("ls"), lscmd, _("list current directory, wildcards allowed")},
75 { NT_(".ls"), dot_lscmd, _("list current directory, wildcards allowed")},
76 { NT_(".lsdir"), dot_lsdircmd, _("list subdir in current directory, wildcards allowed")},
77 { NT_("lsmark"), lsmarkcmd, _("list the marked files in and below the cd")},
78 { NT_(".lsmark"), dot_lsmarkcmd,_("list the marked files in")},
79 { NT_("mark"), markcmd, _("mark dir/file to be restored recursively, wildcards allowed")},
80 { NT_("markdir"), markdircmd, _("mark directory name to be restored (no files)")},
81 { NT_("pwd"), pwdcmd, _("print current working directory")},
82 { NT_(".pwd"), dot_pwdcmd, _("print current working directory")},
83 { NT_("unmark"), unmarkcmd, _("unmark dir/file to be restored recursively in dir")},
84 { NT_("unmarkdir"), unmarkdircmd, _("unmark directory name only no recursion")},
85 { NT_("quit"), quitcmd, _("quit and do not do restore")},
86 { NT_(".help"), dot_helpcmd, _("print help")},
87 { NT_("?"), helpcmd, _("print help")},
89 #define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
92 * Enter a prompt mode where the user can select/deselect
93 * files to be restored. This is sort of like a mini-shell
94 * that allows "cd", "pwd", "add", "rm", ...
96 bool user_select_files_from_tree(TREE_CTX *tree)
100 /* Get a new context so we don't destroy restore command args */
101 UAContext *ua = new_ua_context(tree->ua->jcr);
102 ua->UA_sock = tree->ua->UA_sock; /* patch in UA socket */
103 ua->api = tree->ua->api; /* keep API flag too */
104 BSOCK *user = ua->UA_sock;
107 "\nYou are now entering file selection mode where you add (mark) and\n"
108 "remove (unmark) files to be restored. No files are initially added, unless\n"
109 "you used the \"all\" keyword on the command line.\n"
110 "Enter \"done\" to leave this mode.\n\n"));
111 if (ua->api) user->signal(BNET_START_RTREE);
113 * Enter interactive command handler allowing selection
114 * of individual files.
116 tree->node = (TREE_NODE *)tree->root;
117 tree_getpath(tree->node, cwd, sizeof(cwd));
118 ua->send_msg(_("cwd is: %s\n"), cwd);
121 if (!get_cmd(ua, "$ ", true)) {
124 if (ua->api) user->signal(BNET_CMD_BEGIN);
125 parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
127 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
128 if (ua->api) user->signal(BNET_CMD_FAILED);
132 len = strlen(ua->argk[0]);
135 for (i=0; i<comsize; i++) /* search for command */
136 if (strncasecmp(ua->argk[0], commands[i].key, len) == 0) {
137 stat = (*commands[i].func)(ua, tree); /* go execute command */
142 if (*ua->argk[0] == '.') {
143 /* Some unknow dot command -- probably .messages, ignore it */
146 ua->warning_msg(_("Invalid command \"%s\". Enter \"done\" to exit.\n"), ua->cmd);
147 if (ua->api) user->signal(BNET_CMD_FAILED);
150 if (ua->api) user->signal(BNET_CMD_OK);
155 if (ua->api) user->signal(BNET_END_RTREE);
156 ua->UA_sock = NULL; /* don't release restore socket */
159 free_ua_context(ua); /* get rid of temp UA context */
164 * This callback routine is responsible for inserting the
165 * items it gets into the directory tree. For each JobId selected
166 * this routine is called once for each file. We do not allow
167 * duplicate filenames, but instead keep the info from the most
168 * recent file entered (i.e. the JobIds are assumed to be sorted)
170 * See uar_sel_files in sql_cmds.c for query that calls us.
171 * row[0]=Path, row[1]=Filename, row[2]=FileIndex
172 * row[3]=JobId row[4]=LStat row[5]=DeltaSeq
174 int insert_tree_handler(void *ctx, int num_fields, char **row)
177 TREE_CTX *tree = (TREE_CTX *)ctx;
184 HL_ENTRY *entry = NULL;
187 Dmsg4(150, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
189 if (*row[1] == 0) { /* no filename => directory */
190 if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
198 decode_stat(row[4], &statp, sizeof(statp), &LinkFI);
200 hard_link = (LinkFI != 0);
201 node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
202 JobId = str_to_int64(row[3]);
203 FileIndex = str_to_int64(row[2]);
204 delta_seq = str_to_int64(row[5]);
205 Dmsg6(150, "node=0x%p JobId=%s FileIndex=%s Delta=%s node.delta=%d LinkFI=%d\n",
206 node, row[3], row[2], row[5], node->delta_seq, LinkFI);
208 /* TODO: check with hardlinks */
210 if (delta_seq == (node->delta_seq + 1)) {
211 tree_add_delta_part(tree->root, node, node->JobId, node->FileIndex);
214 /* File looks to be deleted */
215 if (node->delta_seq == -1) { /* just created */
216 tree_remove_node(tree->root, node);
219 tree->ua->warning_msg(_("Something is wrong with the Delta sequence of %s, "
220 "skipping new parts. Current sequence is %d\n"),
221 row[1], node->delta_seq);
223 Dmsg3(0, "Something is wrong with Delta, skip it "
224 "fname=%s d1=%d d2=%d\n", row[1], node->delta_seq, delta_seq);
230 * - The first time we see a file (node->inserted==true), we accept it.
231 * - In the same JobId, we accept only the first copy of a
232 * hard linked file (the others are simply pointers).
233 * - In the same JobId, we accept the last copy of any other
234 * file -- in particular directories.
236 * All the code to set ok could be condensed to a single
237 * line, but it would be even harder to read.
240 if (!node->inserted && JobId == node->JobId) {
241 if ((hard_link && FileIndex > node->FileIndex) ||
242 (!hard_link && FileIndex < node->FileIndex)) {
247 node->hard_link = hard_link;
248 node->FileIndex = FileIndex;
251 node->soft_link = S_ISLNK(statp.st_mode) != 0;
252 node->delta_seq = delta_seq;
253 node->can_access = true;
255 node->extract = true; /* extract all by default */
256 if (type == TN_DIR || type == TN_DIR_NLS) {
257 node->extract_dir = true; /* if dir, extract it */
260 /* insert file having hardlinks into hardlink hashtable */
261 if (statp.st_nlink > 1 && type != TN_DIR && type != TN_DIR_NLS) {
263 /* first occurrence - file hardlinked to */
264 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
265 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
267 tree->root->hardlinks.insert(entry->key, entry);
268 } else if (tree->hardlinks_in_mem) {
269 /* hardlink to known file index: lookup original file */
270 uint64_t file_key = (((uint64_t) JobId) << 32) + LinkFI;
271 HL_ENTRY *first_hl = (HL_ENTRY *) tree->root->hardlinks.lookup(file_key);
272 if (first_hl && first_hl->node) {
273 /* then add hardlink entry to linked node*/
274 entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
275 entry->key = (((uint64_t) JobId) << 32) + FileIndex;
276 entry->node = first_hl->node;
277 tree->root->hardlinks.insert(entry->key, entry);
282 if (node->inserted) {
284 if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
285 tree->ua->send_msg("+");
286 tree->LastCount = tree->FileCount;
294 * Set extract to value passed. We recursively walk
295 * down the tree setting all children if the
296 * node is a directory.
298 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
303 node->extract = extract;
304 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
305 node->extract_dir = extract; /* set/clear dir too */
307 if (node->type != TN_NEWDIR) {
310 /* For a non-file (i.e. directory), we see all the children */
311 if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
312 /* Recursive set children within directory */
313 foreach_child(n, node) {
314 count += set_extract(ua, n, tree, extract);
317 * Walk up tree marking any unextracted parent to be
320 if (!tree->no_auto_parent && extract) {
321 while (node->parent && !node->parent->extract_dir) {
323 node->extract_dir = true;
326 } else if (extract) {
328 if (tree->hardlinks_in_mem) {
329 if (node->hard_link) {
330 key = (((uint64_t) node->JobId) << 32) + node->FileIndex; /* every hardlink is in hashtable, and it points to linked file */
333 /* Get the hard link if it exists */
338 * Ordinary file, we get the full path, look up the
339 * attributes, decode them, and if we are hard linked to
340 * a file that was saved, we must load that file too.
342 tree_getpath(node, cwd, sizeof(cwd));
344 fdbr.JobId = node->JobId;
345 if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
347 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
348 key = (((uint64_t) node->JobId) << 32) + LinkFI; /* lookup by linked file's fileindex */
351 /* If file hard linked and we have a key */
352 if (node->hard_link && key != 0) {
354 * If we point to a hard linked file, find that file in
355 * hardlinks hashmap, and mark it to be restored as well.
357 HL_ENTRY *entry = (HL_ENTRY *)tree->root->hardlinks.lookup(key);
358 if (entry && entry->node) {
361 n->extract_dir = (n->type == TN_DIR || n->type == TN_DIR_NLS);
368 static void strip_trailing_slash(char *arg)
370 int len = strlen(arg);
375 if (arg[len] == '/') { /* strip any trailing slash */
381 * Recursively mark the current directory to be restored as
382 * well as all directories and files below it.
384 static int markcmd(UAContext *ua, TREE_CTX *tree)
390 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
391 ua->send_msg(_("No files marked.\n"));
394 for (int i=1; i < ua->argc; i++) {
395 strip_trailing_slash(ua->argk[i]);
396 foreach_child(node, tree->node) {
397 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
398 count += set_extract(ua, node, tree, true);
403 ua->send_msg(_("No files marked.\n"));
404 } else if (count == 1) {
405 ua->send_msg(_("1 file marked.\n"));
407 ua->send_msg(_("%s files marked.\n"),
408 edit_uint64_with_commas(count, ec1));
413 static int markdircmd(UAContext *ua, TREE_CTX *tree)
419 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
420 ua->send_msg(_("No files marked.\n"));
423 for (int i=1; i < ua->argc; i++) {
424 strip_trailing_slash(ua->argk[i]);
425 foreach_child(node, tree->node) {
426 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
427 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
428 node->extract_dir = true;
435 ua->send_msg(_("No directories marked.\n"));
436 } else if (count == 1) {
437 ua->send_msg(_("1 directory marked.\n"));
439 ua->send_msg(_("%s directories marked.\n"),
440 edit_uint64_with_commas(count, ec1));
446 static int countcmd(UAContext *ua, TREE_CTX *tree)
448 int total, num_extract;
449 char ec1[50], ec2[50];
451 total = num_extract = 0;
452 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
453 if (node->type != TN_NEWDIR) {
455 if (node->extract || node->extract_dir) {
460 ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
461 edit_uint64_with_commas(total, ec1),
462 edit_uint64_with_commas(num_extract, ec2));
466 static int findcmd(UAContext *ua, TREE_CTX *tree)
471 ua->send_msg(_("No file specification given.\n"));
472 return 1; /* make it non-fatal */
475 for (int i=1; i < ua->argc; i++) {
476 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
477 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
479 tree_getpath(node, cwd, sizeof(cwd));
482 } else if (node->extract_dir) {
487 ua->send_msg("%s%s\n", tag, cwd);
494 static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
498 if (!tree_node_has_child(tree->node)) {
502 foreach_child(node, tree->node) {
503 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
504 if (tree_node_has_child(node)) {
505 ua->send_msg("%s/\n", node->fname);
513 static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
515 for (int i=0; i<comsize; i++) {
516 /* List only non-dot commands */
517 if (commands[i].key[0] != '.') {
518 ua->send_msg("%s\n", commands[i].key);
524 static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
528 if (!tree_node_has_child(tree->node)) {
532 foreach_child(node, tree->node) {
533 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
534 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
541 static int lscmd(UAContext *ua, TREE_CTX *tree)
545 if (!tree_node_has_child(tree->node)) {
548 foreach_child(node, tree->node) {
549 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
553 } else if (node->extract_dir) {
558 ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
565 * Ls command that lists only the marked files
567 static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
570 if (!tree_node_has_child(tree->node)) {
573 foreach_child(node, tree->node) {
574 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
575 (node->extract || node->extract_dir)) {
576 ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
583 * This recursive ls command that lists only the marked files
585 static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
588 const int max_level = 100;
589 char indent[max_level*2+1];
591 if (!tree_node_has_child(tnode)) {
594 level = MIN(level, max_level);
596 for (i=0; i<level; i++) {
601 foreach_child(node, tnode) {
602 if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
603 (node->extract || node->extract_dir)) {
607 } else if (node->extract_dir) {
612 ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
613 if (tree_node_has_child(node)) {
614 rlsmark(ua, node, level+1);
620 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
622 rlsmark(ua, tree->node, 0);
627 * This is actually the long form used for "dir"
629 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
630 struct stat *statp, bool dot_cmd)
635 char en1[30], en2[30];
639 p = encode_mode(statp->st_mode, buf);
642 n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
644 n = sprintf(p, "%s,%s,",
645 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
646 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
648 n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
650 p = encode_time(statp->st_mtime, p);
655 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
657 n = sprintf(p, "%-8.8s %-8.8s",
658 guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
659 guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
661 n = sprintf(p, "%12.12s ", edit_int64(statp->st_size, ec1));
663 if (statp->st_ctime > statp->st_mtime) {
664 time = statp->st_ctime;
666 time = statp->st_mtime;
668 /* Display most recent time */
669 p = encode_time(time, p);
673 for (f=fname; *f; ) {
680 * Like ls command, but give more detail on each file
682 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
688 char cwd[1100], *pcwd;
691 if (!tree_node_has_child(tree->node)) {
692 ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
696 guid = new_guid_list();
697 foreach_child(node, tree->node) {
699 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
702 } else if (node->extract_dir) {
707 tree_getpath(node, cwd, sizeof(cwd));
709 fdbr.JobId = node->JobId;
711 * Strip / from soft links to directories.
712 * This is because soft links to files have a trailing slash
713 * when returned from tree_getpath, but db_get_file_attr...
714 * treats soft links as files, so they do not have a trailing
715 * slash like directory names.
717 if (node->type == TN_FILE && tree_node_has_child(node)) {
718 bstrncpy(buf, cwd, sizeof(buf));
720 int len = strlen(buf);
722 buf[len-1] = 0; /* strip trailing / */
727 if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
729 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
731 /* Something went wrong getting attributes -- print name */
732 memset(&statp, 0, sizeof(statp));
734 ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
735 ua->send_msg("%s\n", buf);
738 free_guid_list(guid);
742 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
744 return do_dircmd(ua, tree, true/*dot command*/);
747 static int dircmd(UAContext *ua, TREE_CTX *tree)
749 return do_dircmd(ua, tree, false/*not dot command*/);
753 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
755 int total, num_extract;
756 uint64_t total_bytes = 0;
762 total = num_extract = 0;
763 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
764 if (node->type != TN_NEWDIR) {
766 /* If regular file, get size */
767 if (node->extract && node->type == TN_FILE) {
769 tree_getpath(node, cwd, sizeof(cwd));
771 fdbr.JobId = node->JobId;
772 if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
774 decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
775 if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
776 total_bytes += statp.st_size;
779 /* Directory, count only */
780 } else if (node->extract || node->extract_dir) {
785 ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
786 total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
792 static int helpcmd(UAContext *ua, TREE_CTX *tree)
796 ua->send_msg(_(" Command Description\n ======= ===========\n"));
797 for (i=0; i<comsize; i++) {
798 /* List only non-dot commands */
799 if (commands[i].key[0] != '.') {
800 ua->send_msg(" %-10s %s\n", _(commands[i].key), _(commands[i].help));
808 * Change directories. Note, if the user specifies x: and it fails,
809 * we assume it is a Win32 absolute cd rather than relative and
810 * try a second time with /x: ... Win32 kludge.
812 static int cdcmd(UAContext *ua, TREE_CTX *tree)
819 ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
823 node = tree_cwd(ua->argk[1], tree->root, tree->node);
825 /* Try once more if Win32 drive -- make absolute */
826 if (ua->argk[1][1] == ':') { /* win32 drive */
827 bstrncpy(cwd, "/", sizeof(cwd));
828 bstrncat(cwd, ua->argk[1], sizeof(cwd));
829 node = tree_cwd(cwd, tree->root, tree->node);
832 ua->warning_msg(_("Invalid path given.\n"));
836 if (node->can_access) {
839 ua->warning_msg(_("Invalid path given. Permission denied.\n"));
842 return pwdcmd(ua, tree);
845 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
848 tree_getpath(tree->node, cwd, sizeof(cwd));
850 ua->send_msg("%s", cwd);
852 ua->send_msg(_("cwd is: %s\n"), cwd);
857 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
860 tree_getpath(tree->node, cwd, sizeof(cwd));
861 ua->send_msg("%s", cwd);
865 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
870 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
871 ua->send_msg(_("No files unmarked.\n"));
874 for (int i=1; i < ua->argc; i++) {
875 strip_trailing_slash(ua->argk[i]);
876 foreach_child(node, tree->node) {
877 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
878 count += set_extract(ua, node, tree, false);
883 ua->send_msg(_("No files unmarked.\n"));
884 } else if (count == 1) {
885 ua->send_msg(_("1 file unmarked.\n"));
888 ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
893 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
898 if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
899 ua->send_msg(_("No directories unmarked.\n"));
903 for (int i=1; i < ua->argc; i++) {
904 strip_trailing_slash(ua->argk[i]);
905 foreach_child(node, tree->node) {
906 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
907 if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
908 node->extract_dir = false;
916 ua->send_msg(_("No directories unmarked.\n"));
917 } else if (count == 1) {
918 ua->send_msg(_("1 directory unmarked.\n"));
920 ua->send_msg(_("%d directories unmarked.\n"), count);
926 static int donecmd(UAContext *ua, TREE_CTX *tree)
931 static int quitcmd(UAContext *ua, TREE_CTX *tree)