Subject: Re: bug#72919: 29.1; chart-space-usage in chart.el does not work correctly on windows

From: Eli Zaretskii <[email-masked]>
Date: Sat, 07 Sep 2024 12:24:21 +0300

> Cc: [email-masked]
> Date: Sun, 01 Sep 2024 21:35:03 +0300
> From: Eli Zaretskii <[email-masked]>
> 
> > I've tried your changes with and without du. This uncovered something in the
> > original implementation, namely that the original implementation did not count
> > hidden files and directories. du * skips dotfiles for me.
> >
> > With du present chart-space-usage shows the lisp directory as the largest in the 
> > emacs repository root. Without du it shows .git as the largest.
> 
> This just means minor adjustments in the code I posted: we need to use
> a different regexp in the call to directory-files-recursively, and
> also ignore files that start with a dot in the command itself.  I will
> make those changes, thanks for pointing them out

Actually, there's more here than meets the eye.  'du' does NOT skip
dotfiles (unless instructed to do so via the --exclude= command-line
option).  What happens here is that a typical Unix shell does not
include dotfiles in the expansion of the "*" wildcard, so "du *" does
not count dotfiles, but only in the directory in which 'du' was
invoked; dotfiles in subdirectories _are_ counted.

I made the Lisp implementation skip dotfiles in the directory for
which the command is invoked, but not in the subdirectories.  Windows
users who do have 'du' installed will now depend on how "*" is
expanded, which is probably different in different ports of 'du'.  I
considered using --exclude=, but that would exclude dotfiles in
subdirectories as well, which is not what happens in the Unix case.

> I will post a version that ignores dotfiles.

I installed a modified version on the emacs-30 branch.  The patch is
below in case you want to try it.

With that, I'm closing this bug.

diff --git a/lisp/emacs-lisp/chart.el b/lisp/emacs-lisp/chart.el
index da61e45..2ca9b64 100644
--- a/lisp/emacs-lisp/chart.el
+++ b/lisp/emacs-lisp/chart.el
@@ -641,27 +641,68 @@ chart-file-count
 		       (lambda (a b) (> (cdr a) (cdr b))))
     ))
 
+;; This assumes 4KB blocks
+(defun chart--file-size (size)
+  (* (/ (+ size 4095) 4096) 4096))
+
+(defun chart--directory-size (dir)
+  "Compute total size of files in directory DIR and its subdirectories.
+DIR is assumed to be a directory, verified by the caller."
+  (let ((size 0))
+    (dolist (file (directory-files-recursively dir "." t))
+      (let ((fsize (nth 7 (file-attributes file))))
+        (if (> fsize 0)
+            (setq size
+                  (+ size (chart--file-size fsize))))))
+    size))
+
 (defun chart-space-usage (d)
   "Display a top usage chart for directory D."
   (interactive "DDirectory: ")
   (message "Collecting statistics...")
   (let ((nmlst nil)
 	(cntlst nil)
-	(b (get-buffer-create " *du-tmp*")))
-    (set-buffer b)
-    (erase-buffer)
-    (insert "cd " d ";du -sk * \n")
-    (message "Running `cd %s;du -sk *'..." d)
-    (call-process-region (point-min) (point-max) shell-file-name t
-			 (current-buffer) nil)
-    (goto-char (point-min))
-    (message "Scanning output ...")
-    (while (re-search-forward "^\\([0-9]+\\)[ \t]+\\([^ \n]+\\)$" nil t)
-      (let* ((nam (buffer-substring (match-beginning 2) (match-end 2)))
-	     (num (buffer-substring (match-beginning 1) (match-end 1))))
-	(setq nmlst (cons nam nmlst)
-	      ;; * 1000 to put it into bytes
-	      cntlst (cons (* (string-to-number num) 1000) cntlst))))
+        b)
+    (if (executable-find "du")
+        (progn
+	  (setq b (get-buffer-create " *du-tmp*"))
+          (set-buffer b)
+          (erase-buffer)
+          (if (and (memq system-type '(windows-nt ms-dos))
+                   (fboundp 'w32-shell-dos-semantics)
+                   (w32-shell-dos-semantics))
+              (progn
+                ;; With Windows shells, 'cd' does not change the drive,
+                ;; and ';' is not reliable for running multiple
+                ;; commands, so use alternatives.  We quote the
+                ;; directory because otherwise pushd will barf on a
+                ;; directory with forward slashes.  Note that * will not
+                ;; skip dotfiles with Windows shells, unlike on Unix.
+                (insert "pushd \"" d "\" && du -sk * \n")
+                (message "Running `pushd \"%s\" && du -sk *'..." d))
+            (insert "cd " d ";du -sk * \n")
+            (message "Running `cd %s;du -sk *'..." d))
+          (call-process-region (point-min) (point-max) shell-file-name t
+			       (current-buffer) nil)
+          (goto-char (point-min))
+          (message "Scanning output ...")
+          (while (re-search-forward "^\\([0-9]+\\)[ \t]+\\([^ \n]+\\)$" nil t)
+            (let* ((nam (buffer-substring (match-beginning 2) (match-end 2)))
+	           (num (buffer-substring (match-beginning 1) (match-end 1))))
+	      (setq nmlst (cons nam nmlst)
+	            ;; * 1000 to put it into bytes
+	            cntlst (cons (* (string-to-number num) 1000) cntlst)))))
+      (dolist (file (directory-files d t directory-files-no-dot-files-regexp))
+        (let ((fbase (file-name-nondirectory file)))
+          ;; Typical shells exclude files and subdirectories whose names
+          ;; begin with a period when it expands *, so we do the same.
+          (unless (string-match-p "\\`\\." fbase)
+            (setq nmlst (cons fbase nmlst))
+            (if (file-regular-p file)
+                (setq cntlst (cons (chart--file-size
+                                    (nth 7 (file-attributes file)))
+                                   cntlst))
+              (setq cntlst (cons (chart--directory-size file) cntlst)))))))
     (if (not nmlst)
 	(error "No files found!"))
     (chart-bar-quickie 'vertical (format "Largest files in %s" d)