]> git.sur5r.net Git - bacula/bacula/blobdiff - bacula/src/dird/ua_tree.c
This commit was manufactured by cvs2svn to create tag
[bacula/bacula] / bacula / src / dird / ua_tree.c
index c59d1888fae2d69ffcf13c2497e7c2f019a9c2a0..d818494af608de28ca9241ce8d9bd325f5cffb80 100644 (file)
@@ -1,37 +1,35 @@
 /*
  *
  *   Bacula Director -- User Agent Database File tree for Restore
- *     command. This file interacts with the user implementing the
- *     UA tree commands.
+ *      command. This file interacts with the user implementing the
+ *      UA tree commands.
  *
  *     Kern Sibbald, July MMII
  *
  *   Version $Id$
  */
-
 /*
-   Copyright (C) 2002-2004 Kern Sibbald and John Walker
+   Copyright (C) 2002-2005 Kern Sibbald
 
    This program is free software; you can redistribute it and/or
-   modify it under the terms of the GNU General Public License as
-   published by the Free Software Foundation; either version 2 of
-   the License, or (at your option) any later version.
+   modify it under the terms of the GNU General Public License
+   version 2 as amended with additional clauses defined in the
+   file LICENSE in the main source directory.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
-   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-   General Public License for more details.
-
-   You should have received a copy of the GNU General Public
-   License along with this program; if not, write to the Free
-   Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
-   MA 02111-1307, USA.
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
+   the file LICENSE for additional details.
 
  */
 
 #include "bacula.h"
 #include "dird.h"
+#ifdef HAVE_FNMATCH
 #include <fnmatch.h>
+#else
+#include "lib/fnmatch.h"
+#endif
 #include "findlib/find.h"
 
 
@@ -44,6 +42,7 @@ static int findcmd(UAContext *ua, TREE_CTX *tree);
 static int lscmd(UAContext *ua, TREE_CTX *tree);
 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
 static int dircmd(UAContext *ua, TREE_CTX *tree);
+static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
 static int helpcmd(UAContext *ua, TREE_CTX *tree);
 static int cdcmd(UAContext *ua, TREE_CTX *tree);
@@ -54,26 +53,27 @@ static int quitcmd(UAContext *ua, TREE_CTX *tree);
 static int donecmd(UAContext *ua, TREE_CTX *tree);
 
 
-struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; }; 
+struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
 static struct cmdstruct commands[] = {
- { N_("cd"),         cdcmd,        _("change current directory")},
- { N_("count"),      countcmd,     _("count marked files in and below the cd")},
- { N_("dir"),        dircmd,       _("list current directory")},    
- { N_("done"),       donecmd,      _("leave file selection mode")},
- { N_("estimate"),   estimatecmd,  _("estimate restore size")},
- { N_("exit"),       donecmd,      _("exit = done")},
- { N_("find"),       findcmd,      _("find files -- wildcards allowed")},
- { N_("help"),       helpcmd,      _("print help")},
- { N_("ls"),         lscmd,        _("list current directory -- wildcards allowed")},    
- { N_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},    
- { N_("mark"),       markcmd,      _("mark dir/file to be restored -- recursively in dirs")},
- { N_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
- { N_("pwd"),        pwdcmd,       _("print current working directory")},
- { N_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored -- recursively in dir")},
- { N_("unmarkdir"),  unmarkdircmd, _("unmark directory name only -- no recursion")},
- { N_("quit"),       quitcmd,      _("quit")},
- { N_("?"),          helpcmd,      _("print help")},    
-            };
+ { NT_("cd"),         cdcmd,        _("change current directory")},
+ { NT_("count"),      countcmd,     _("count marked files in and below the cd")},
+ { NT_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
+ { NT_(".dir"),       dot_dircmd,   _("long list current directory, wildcards allowed")},
+ { NT_("done"),       donecmd,      _("leave file selection mode")},
+ { NT_("estimate"),   estimatecmd,  _("estimate restore size")},
+ { NT_("exit"),       donecmd,      _("same as done command")},
+ { NT_("find"),       findcmd,      _("find files, wildcards allowed")},
+ { NT_("help"),       helpcmd,      _("print help")},
+ { NT_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
+ { NT_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
+ { NT_("mark"),       markcmd,      _("mark dir/file to be restored recursively, wildcards allowed")},
+ { NT_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
+ { NT_("pwd"),        pwdcmd,       _("print current working directory")},
+ { NT_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
+ { NT_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
+ { NT_("quit"),       quitcmd,      _("quit and do not do restore")},
+ { NT_("?"),          helpcmd,      _("print help")},
+             };
 #define comsize (sizeof(commands)/sizeof(struct cmdstruct))
 
 
@@ -90,9 +90,9 @@ bool user_select_files_from_tree(TREE_CTX *tree)
    UAContext *ua = new_ua_context(tree->ua->jcr);
    ua->UA_sock = tree->ua->UA_sock;   /* patch in UA socket */
 
-   bsendmsg(tree->ua, _( 
-      "\nYou are now entering file selection mode where you add and\n"
-      "remove files to be restored. No files are initially added, unless\n"
+   bsendmsg(tree->ua, _(
+      "\nYou are now entering file selection mode where you add (mark) and\n"
+      "remove (unmark) files to be restored. No files are initially added, unless\n"
       "you used the \"all\" keyword on the command line.\n"
       "Enter \"done\" to leave this mode.\n\n"));
    /*
@@ -102,37 +102,37 @@ bool user_select_files_from_tree(TREE_CTX *tree)
    tree->node = (TREE_NODE *)tree->root;
    tree_getpath(tree->node, cwd, sizeof(cwd));
    bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
-   for ( ;; ) {       
+   for ( ;; ) {
       int found, len, i;
       if (!get_cmd(ua, "$ ")) {
-        break;
+         break;
       }
       parse_ua_args(ua);
       if (ua->argc == 0) {
-        break;
+         break;
       }
 
       len = strlen(ua->argk[0]);
       found = 0;
       stat = false;
-      for (i=0; i<(int)comsize; i++)      /* search for command */
-        if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
-           stat = (*commands[i].func)(ua, tree);   /* go execute command */
-           found = 1;
-           break;
-        }
+      for (i=0; i<(int)comsize; i++)       /* search for command */
+         if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
+            stat = (*commands[i].func)(ua, tree);   /* go execute command */
+            found = 1;
+            break;
+         }
       if (!found) {
          bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
-        continue;
+         continue;
       }
       if (!stat) {
-        break;
+         break;
       }
    }
    ua->UA_sock = NULL;                /* don't release restore socket */
    stat = !ua->quit;
    ua->quit = false;
-   free_ua_context(ua);              /* get rid of temp UA context */
+   free_ua_context(ua);               /* get rid of temp UA context */
    return stat;
 }
 
@@ -145,8 +145,8 @@ bool user_select_files_from_tree(TREE_CTX *tree)
  *  recent file entered (i.e. the JobIds are assumed to be sorted)
  *
  *   See uar_sel_files in sql_cmds.c for query that calls us.
- *     row[0]=Path, row[1]=Filename, row[2]=FileIndex
- *     row[3]=JobId row[4]=LStat
+ *      row[0]=Path, row[1]=Filename, row[2]=FileIndex
+ *      row[3]=JobId row[4]=LStat
  */
 int insert_tree_handler(void *ctx, int num_fields, char **row)
 {
@@ -158,26 +158,27 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
    int FileIndex;
    JobId_t JobId;
 
-   strip_trailing_junk(row[1]);
-   if (*row[1] == 0) {               /* no filename => directory */
+// Dmsg4(000, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
+//    row[2], row[3]);
+   if (*row[1] == 0) {                /* no filename => directory */
       if (*row[0] != '/') {           /* Must be Win32 directory */
-        type = TN_DIR_NLS;
+         type = TN_DIR_NLS;
       } else {
-        type = TN_DIR;
+         type = TN_DIR;
       }
    } else {
       type = TN_FILE;
    }
    hard_link = (decode_LinkFI(row[4], &statp) != 0);
    node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
-   JobId = (JobId_t)str_to_int64(row[3]);
-   FileIndex = atoi(row[2]);
+   JobId = str_to_int64(row[3]);
+   FileIndex = str_to_int64(row[2]);
    /*
     * - The first time we see a file (node->inserted==true), we accept it.
     * - In the same JobId, we accept only the first copy of a
-    *  hard linked file (the others are simply pointers).
+    *   hard linked file (the others are simply pointers).
     * - In the same JobId, we accept the last copy of any other
-    *  file -- in particular directories.
+    *   file -- in particular directories.
     *
     * All the code to set ok could be condensed to a single
     *  line, but it would be even harder to read.
@@ -185,8 +186,8 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
    ok = true;
    if (!node->inserted && JobId == node->JobId) {
       if ((hard_link && FileIndex > node->FileIndex) ||
-         (!hard_link && FileIndex < node->FileIndex)) {
-        ok = false;
+          (!hard_link && FileIndex < node->FileIndex)) {
+         ok = false;
       }
    }
    if (ok) {
@@ -196,17 +197,17 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
       node->type = type;
       node->soft_link = S_ISLNK(statp.st_mode) != 0;
       if (tree->all) {
-        node->extract = true;          /* extract all by default */
-        if (type == TN_DIR || type == TN_DIR_NLS) {
-           node->extract_dir = true;   /* if dir, extract it */
-        }
+         node->extract = true;          /* extract all by default */
+         if (type == TN_DIR || type == TN_DIR_NLS) {
+            node->extract_dir = true;   /* if dir, extract it */
+         }
       }
    }
    if (node->inserted) {
       tree->FileCount++;
       if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
          bsendmsg(tree->ua, "+");
-        tree->LastCount = tree->FileCount;
+         tree->LastCount = tree->FileCount;
       }
    }
    tree->cnt++;
@@ -216,7 +217,7 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
 
 /*
  * Set extract to value passed. We recursively walk
- *  down the tree setting all children if the 
+ *  down the tree setting all children if the
  *  node is a directory.
  */
 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
@@ -237,17 +238,17 @@ static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extr
    if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
       /* Recursive set children within directory */
       foreach_child(n, node) {
-        count += set_extract(ua, n, tree, extract);
+         count += set_extract(ua, n, tree, extract);
       }
       /*
        * Walk up tree marking any unextracted parent to be
        * extracted.
        */
       if (extract) {
-        while (node->parent && !node->parent->extract_dir) {
-           node = node->parent;
-           node->extract_dir = true;
-        }
+         while (node->parent && !node->parent->extract_dir) {
+            node = node->parent;
+            node->extract_dir = true;
+         }
       }
    } else if (extract) {
       char cwd[2000];
@@ -260,31 +261,31 @@ static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extr
       fdbr.FileId = 0;
       fdbr.JobId = node->JobId;
       if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
-        int32_t LinkFI;
-        decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
-        /*
-         * If we point to a hard linked file, traverse the tree to
-         * find that file, and mark it to be restored as well. It
-         * must have the Link we just obtained and the same JobId.
-         */
-        if (LinkFI) {
-           for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
-              if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
-                 n->extract = true;
-                 if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
-                    n->extract_dir = true;
-                 }
-                 break;
-              }
-           }
-        }
+         int32_t LinkFI;
+         decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+         /*
+          * If we point to a hard linked file, traverse the tree to
+          * find that file, and mark it to be restored as well. It
+          * must have the Link we just obtained and the same JobId.
+          */
+         if (LinkFI) {
+            for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
+               if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
+                  n->extract = true;
+                  if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
+                     n->extract_dir = true;
+                  }
+                  break;
+               }
+            }
+         }
       }
    }
    return count;
 }
 
 /*
- * Recursively mark the current directory to be restored as 
+ * Recursively mark the current directory to be restored as
  *  well as all directories and files below it.
  */
 static int markcmd(UAContext *ua, TREE_CTX *tree)
@@ -299,16 +300,18 @@ static int markcmd(UAContext *ua, TREE_CTX *tree)
    }
    for (int i=1; i < ua->argc; i++) {
       foreach_child(node, tree->node) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           count += set_extract(ua, node, tree, true);
-        }
+         if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
+            count += set_extract(ua, node, tree, true);
+         }
       }
    }
    if (count == 0) {
       bsendmsg(ua, _("No files marked.\n"));
+   } else if (count == 1) {
+      bsendmsg(ua, _("1 file marked.\n"));
    } else {
-      bsendmsg(ua, _("%s file%s marked.\n"),        
-               edit_uint64_with_commas(count, ec1), count==0?"":"s");
+      bsendmsg(ua, _("%s files marked.\n"),
+               edit_uint64_with_commas(count, ec1));
    }
    return 1;
 }
@@ -325,19 +328,21 @@ static int markdircmd(UAContext *ua, TREE_CTX *tree)
    }
    for (int i=1; i < ua->argc; i++) {
       foreach_child(node, tree->node) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
-              node->extract_dir = true;
-              count++;
-           }
-        }
+         if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
+            if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
+               node->extract_dir = true;
+               count++;
+            }
+         }
       }
    }
    if (count == 0) {
       bsendmsg(ua, _("No directories marked.\n"));
+   } else if (count == 1) {
+      bsendmsg(ua, _("1 directory marked.\n"));
    } else {
-      bsendmsg(ua, _("%s director%s marked.\n"), 
-               edit_uint64_with_commas(count, ec1), count==1?"y":"ies");
+      bsendmsg(ua, _("%s directories marked.\n"),
+               edit_uint64_with_commas(count, ec1));
    }
    return 1;
 }
@@ -346,19 +351,20 @@ static int markdircmd(UAContext *ua, TREE_CTX *tree)
 static int countcmd(UAContext *ua, TREE_CTX *tree)
 {
    int total, num_extract;
-   char ec1[50];
+   char ec1[50], ec2[50];
 
    total = num_extract = 0;
    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
       if (node->type != TN_NEWDIR) {
-        total++;
-        if (node->extract || node->extract_dir) {
-           num_extract++;
-        }
+         total++;
+         if (node->extract || node->extract_dir) {
+            num_extract++;
+         }
       }
    }
-   bsendmsg(ua, "%s total files/dirs. %d marked to be restored.\n", total, 
-           edit_uint64_with_commas(num_extract, ec1));
+   bsendmsg(ua, _("%s total files/dirs. %s marked to be restored.\n"),
+            edit_uint64_with_commas(total, ec1),
+            edit_uint64_with_commas(num_extract, ec2));
    return 1;
 }
 
@@ -370,21 +376,21 @@ static int findcmd(UAContext *ua, TREE_CTX *tree)
       bsendmsg(ua, _("No file specification given.\n"));
       return 0;
    }
-   
+
    for (int i=1; i < ua->argc; i++) {
       for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           const char *tag;
-           tree_getpath(node, cwd, sizeof(cwd));
-           if (node->extract) {
+         if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
+            const char *tag;
+            tree_getpath(node, cwd, sizeof(cwd));
+            if (node->extract) {
                tag = "*";
-           } else if (node->extract_dir) {
+            } else if (node->extract_dir) {
                tag = "+";
-           } else {
+            } else {
                tag = "";
-           }
+            }
             bsendmsg(ua, "%s%s\n", tag, cwd);
-        }
+         }
       }
    }
    return 1;
@@ -396,19 +402,19 @@ static int lscmd(UAContext *ua, TREE_CTX *tree)
 {
    TREE_NODE *node;
 
-   if (!tree_node_has_child(tree->node)) {     
+   if (!tree_node_has_child(tree->node)) {
       return 1;
    }
    foreach_child(node, tree->node) {
       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
-        const char *tag;
-        if (node->extract) {
+         const char *tag;
+         if (node->extract) {
             tag = "*";
-        } else if (node->extract_dir) {
+         } else if (node->extract_dir) {
             tag = "+";
-        } else {
+         } else {
             tag = "";
-        }
+         }
          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
       }
    }
@@ -418,26 +424,27 @@ static int lscmd(UAContext *ua, TREE_CTX *tree)
 /*
  * Ls command that lists only the marked files
  */
-static void rlsmark(UAContext *ua, TREE_NODE *node) 
+static void rlsmark(UAContext *ua, TREE_NODE *tnode)
 {
-   if (!tree_node_has_child(node)) {    
+   TREE_NODE *node;
+   if (!tree_node_has_child(tnode)) {
       return;
    }
-   foreach_child(node, node) {
+   foreach_child(node, tnode) {
       if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
-         (node->extract || node->extract_dir)) {
-        const char *tag;
-        if (node->extract) {
+          (node->extract || node->extract_dir)) {
+         const char *tag;
+         if (node->extract) {
             tag = "*";
-        } else if (node->extract_dir) {
+         } else if (node->extract_dir) {
             tag = "+";
-        } else {
+         } else {
             tag = "";
-        }
+         }
          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
-        if (tree_node_has_child(node)) {
-           rlsmark(ua, node);
-        }
+         if (tree_node_has_child(node)) {
+            rlsmark(ua, node);
+         }
       }
    }
 }
@@ -450,40 +457,61 @@ static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
 
 
 
-extern char *getuser(uid_t uid);
-extern char *getgroup(gid_t gid);
+extern char *getuser(uid_t uid, char *name, int len);
+extern char *getgroup(gid_t gid, char *name, int len);
 
 /*
  * This is actually the long form used for "dir"
  */
-static void ls_output(char *buf, const char *fname, const char *tag, struct stat *statp)
+static void ls_output(char *buf, const char *fname, const char *tag, 
+                      struct stat *statp, bool dot_cmd) 
+                    
 {
    char *p;
    const char *f;
    char ec1[30];
+   char en1[30], en2[30];
    int n;
+   time_t time;
 
    p = encode_mode(statp->st_mode, buf);
-   n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
-   p += n;
-   n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid), getgroup(statp->st_gid));
-   p += n;
-   n = sprintf(p, "%8.8s  ", edit_uint64(statp->st_size, ec1));
-   p += n;
-   p = encode_time(statp->st_ctime, p);
-   *p++ = ' ';
-   *p++ = *tag;
+   if (dot_cmd) {
+      *p++ = ',';
+      n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
+      p += n;
+      n = sprintf(p, "%s,%s,", getuser(statp->st_uid, en1, sizeof(en1)),
+                  getgroup(statp->st_gid, en2, sizeof(en2)));
+      p += n;
+      n = sprintf(p, "%s,", edit_uint64(statp->st_size, ec1));
+      p += n;
+      p = encode_time(statp->st_mtime, p);
+      *p++ = ',';
+      *p++ = *tag;
+      *p++ = ',';
+   } else {
+      n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
+      p += n;
+      n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid, en1, sizeof(en1)),
+                  getgroup(statp->st_gid, en2, sizeof(en2)));
+      p += n;
+      n = sprintf(p, "%10.10s  ", edit_uint64(statp->st_size, ec1));
+      p += n;
+      time = statp->st_mtime;
+      /* Display most recent time */
+      p = encode_time(time, p);
+      *p++ = ' ';
+      *p++ = *tag;
+   }
    for (f=fname; *f; ) {
       *p++ = *f++;
    }
    *p = 0;
 }
 
-
 /*
  * Like ls command, but give more detail on each file
  */
-static int dircmd(UAContext *ua, TREE_CTX *tree)
+static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
 {
    TREE_NODE *node;
    FILE_DBR fdbr;
@@ -491,55 +519,65 @@ static int dircmd(UAContext *ua, TREE_CTX *tree)
    char buf[1100];
    char cwd[1100], *pcwd;
 
-   if (!tree_node_has_child(tree->node)) {     
-      bsendmsg(ua, "Node %s has no children.\n", tree->node->fname);
+   if (!tree_node_has_child(tree->node)) {
+      bsendmsg(ua, _("Node %s has no children.\n"), tree->node->fname);
       return 1;
    }
 
    foreach_child(node, tree->node) {
       const char *tag;
       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
-        if (node->extract) {
+         if (node->extract) {
             tag = "*";
-        } else if (node->extract_dir) {
+         } else if (node->extract_dir) {
             tag = "+";
-        } else {
+         } else {
             tag = " ";
-        }
-        tree_getpath(node, cwd, sizeof(cwd));
-        fdbr.FileId = 0;
-        fdbr.JobId = node->JobId;
-        /*
-         * Strip / from soft links to directories.
-         *   This is because soft links to files have a trailing slash
-         *   when returned from tree_getpath, but db_get_file_attr...
-         *   treats soft links as files, so they do not have a trailing
-         *   slash like directory names.
-         */
-        if (node->type == TN_FILE && tree_node_has_child(node)) {
-           bstrncpy(buf, cwd, sizeof(buf));
-           pcwd = buf;
-           int len = strlen(buf);
-           if (len > 1) {
-              buf[len-1] = 0;        /* strip trailing / */
-           }
-        } else {
-           pcwd = cwd;
-        }
-        if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
-           int32_t LinkFI;
-           decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
-        } else {
-           /* Something went wrong getting attributes -- print name */
-           memset(&statp, 0, sizeof(statp));
-        }
-        ls_output(buf, cwd, tag, &statp);
+         }
+         tree_getpath(node, cwd, sizeof(cwd));
+         fdbr.FileId = 0;
+         fdbr.JobId = node->JobId;
+         /*
+          * Strip / from soft links to directories.
+          *   This is because soft links to files have a trailing slash
+          *   when returned from tree_getpath, but db_get_file_attr...
+          *   treats soft links as files, so they do not have a trailing
+          *   slash like directory names.
+          */
+         if (node->type == TN_FILE && tree_node_has_child(node)) {
+            bstrncpy(buf, cwd, sizeof(buf));
+            pcwd = buf;
+            int len = strlen(buf);
+            if (len > 1) {
+               buf[len-1] = 0;        /* strip trailing / */
+            }
+         } else {
+            pcwd = cwd;
+         }
+         if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
+            int32_t LinkFI;
+            decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+         } else {
+            /* Something went wrong getting attributes -- print name */
+            memset(&statp, 0, sizeof(statp));
+         }
+         ls_output(buf, cwd, tag, &statp, dot_cmd);
          bsendmsg(ua, "%s\n", buf);
       }
    }
    return 1;
 }
 
+int dot_dircmd(UAContext *ua, TREE_CTX *tree)
+{
+   return do_dircmd(ua, tree, true/*dot command*/);
+}
+
+static int dircmd(UAContext *ua, TREE_CTX *tree)
+{
+   return do_dircmd(ua, tree, false/*not dot command*/);
+}
+
 
 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
 {
@@ -553,52 +591,54 @@ static int estimatecmd(UAContext *ua, TREE_CTX *tree)
    total = num_extract = 0;
    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
       if (node->type != TN_NEWDIR) {
-        total++;
-        /* If regular file, get size */
-        if (node->extract && node->type == TN_FILE) {
-           num_extract++;
-           tree_getpath(node, cwd, sizeof(cwd));
-           fdbr.FileId = 0;
-           fdbr.JobId = node->JobId;
-           if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
-              int32_t LinkFI;
-              decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
-              if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
-                 total_bytes += statp.st_size;
-              }
-           }
-        /* Directory, count only */
-        } else if (node->extract || node->extract_dir) {
-           num_extract++;
-        }
+         total++;
+         /* If regular file, get size */
+         if (node->extract && node->type == TN_FILE) {
+            num_extract++;
+            tree_getpath(node, cwd, sizeof(cwd));
+            fdbr.FileId = 0;
+            fdbr.JobId = node->JobId;
+            if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
+               int32_t LinkFI;
+               decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+               if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
+                  total_bytes += statp.st_size;
+               }
+            }
+         /* Directory, count only */
+         } else if (node->extract || node->extract_dir) {
+            num_extract++;
+         }
       }
    }
-   bsendmsg(ua, "%d total files; %d marked to be restored; %s bytes.\n", 
-           total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
+   bsendmsg(ua, _("%d total files; %d marked to be restored; %s bytes.\n"),
+            total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
    return 1;
 }
 
 
 
-static int helpcmd(UAContext *ua, TREE_CTX *tree) 
+static int helpcmd(UAContext *ua, TREE_CTX *tree)
 {
    unsigned int i;
 
-/* usage(); */
    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
    for (i=0; i<comsize; i++) {
-      bsendmsg(ua, _("  %-10s %s\n"), _(commands[i].key), _(commands[i].help));
+      /* List only non-dot commands */
+      if (commands[i].key[0] != '.') {
+         bsendmsg(ua, "  %-10s %s\n", _(commands[i].key), _(commands[i].help));
+      }
    }
    bsendmsg(ua, "\n");
    return 1;
 }
 
 /*
- * Change directories. Note, if the user specifies x: and it fails,
+ * Change directories.  Note, if the user specifies x: and it fails,
  *   we assume it is a Win32 absolute cd rather than relative and
  *   try a second time with /x: ...  Win32 kludge.
  */
-static int cdcmd(UAContext *ua, TREE_CTX *tree) 
+static int cdcmd(UAContext *ua, TREE_CTX *tree)
 {
    TREE_NODE *node;
    char cwd[2000];
@@ -606,18 +646,19 @@ static int cdcmd(UAContext *ua, TREE_CTX *tree)
    if (ua->argc != 2) {
       return 1;
    }
+   strip_leading_space(ua->argk[1]);
    node = tree_cwd(ua->argk[1], tree->root, tree->node);
    if (!node) {
       /* Try once more if Win32 drive -- make absolute */
       if (ua->argk[1][1] == ':') {  /* win32 drive */
          bstrncpy(cwd, "/", sizeof(cwd));
-        bstrncat(cwd, ua->argk[1], sizeof(cwd));
-        node = tree_cwd(cwd, tree->root, tree->node);
+         bstrncat(cwd, ua->argk[1], sizeof(cwd));
+         node = tree_cwd(cwd, tree->root, tree->node);
       }
       if (!node) {
          bsendmsg(ua, _("Invalid path given.\n"));
       } else {
-        tree->node = node;
+         tree->node = node;
       }
    } else {
       tree->node = node;
@@ -627,7 +668,7 @@ static int cdcmd(UAContext *ua, TREE_CTX *tree)
    return 1;
 }
 
-static int pwdcmd(UAContext *ua, TREE_CTX *tree) 
+static int pwdcmd(UAContext *ua, TREE_CTX *tree)
 {
    char cwd[2000];
    tree_getpath(tree->node, cwd, sizeof(cwd));
@@ -641,21 +682,23 @@ static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
    TREE_NODE *node;
    int count = 0;
 
-   if (ua->argc < 2 || !tree_node_has_child(tree->node)) {     
+   if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
       bsendmsg(ua, _("No files unmarked.\n"));
       return 1;
    }
    for (int i=1; i < ua->argc; i++) {
       foreach_child(node, tree->node) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           count += set_extract(ua, node, tree, false);
-        }
+         if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
+            count += set_extract(ua, node, tree, false);
+         }
       }
    }
    if (count == 0) {
       bsendmsg(ua, _("No files unmarked.\n"));
+   } else if (count == 1) {
+      bsendmsg(ua, _("1 file unmarked.\n"));
    } else {
-      bsendmsg(ua, _("%d file%s unmarked.\n"), count, count==0?"":"s");
+      bsendmsg(ua, _("%d files unmarked.\n"), count);
    }
    return 1;
 }
@@ -672,30 +715,32 @@ static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
 
    for (int i=1; i < ua->argc; i++) {
       foreach_child(node, tree->node) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
-              node->extract_dir = false;
-              count++;
-           }
-        }
+         if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
+            if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
+               node->extract_dir = false;
+               count++;
+            }
+         }
       }
    }
 
    if (count == 0) {
       bsendmsg(ua, _("No directories unmarked.\n"));
+   } else if (count == 1) {
+      bsendmsg(ua, _("1 directory unmarked.\n"));
    } else {
-      bsendmsg(ua, _("%d director%s unmarked.\n"), count, count==1?"y":"ies");
+      bsendmsg(ua, _("%d directories unmarked.\n"), count);
    }
    return 1;
 }
 
 
-static int donecmd(UAContext *ua, TREE_CTX *tree) 
+static int donecmd(UAContext *ua, TREE_CTX *tree)
 {
    return 0;
 }
 
-static int quitcmd(UAContext *ua, TREE_CTX *tree) 
+static int quitcmd(UAContext *ua, TREE_CTX *tree)
 {
    ua->quit = true;
    return 0;