0

I'd like to use Org mode to write stuff like essays, where a specific word count is required. This would be much easier if I could use headings to structure my essay, but my current solution for displaying the word count —just adding (setq doom-modeline-enable-word-count t) to my config.el file— includes headings and keywords in the word count.

I want to see only the word count for the "regular" text in my file, excluding stuff like headings and keywords.

I'm relatively new to Emacs and Elisp. Could I have a hand?

2
  • A fairly cheap solution is to filter out keyword lines and heading lines and then just count the rest. On Unix-y systems: grep -v -E '^\*|^#' file.org | wc -w will do more-or-less what you want. If you want something within emacs, wrap the above command in a function that uses shell-command to run it, and then bind the function to a key.
    – NickD
    Commented May 22 at 10:47
  • @NickD Honestly, that sounds great to me, as I am on Linux. I'd prefer to accept this as my solution (no offence @Ian!) so if you want to do that, I will be a happy guy
    – DJMoffinz
    Commented May 23 at 7:51

1 Answer 1

0

As described in my comment, you can run a command that filters out the lines you don't want and then counts the words in the part that remains. It basically runs grep -v -E '^(#|\*|:)' <file> | wc -w which you can run from a shell command line. The grep invocation ignores (-v) any lines that start (^) with one of the characters #, *, :. The asterisk has a special meaning in regular expression syntax, so it needs to be escaped (\) to lose that meaning and become a literal asterisk. The -E enables the extended syntax that allows for parenthesized subexpressions and the alternation (OR) syntax (|). The output is piped into wc -w which counts words and prints out the result.

The regular expression matches keyword lines and comment lines (both start with #), headlines (which start with *) and property/drawer lines (which start with :). All of these characters are rather unlikely to appear as the first character of a line in regular text. The # case also excludes the markers for block (#+BEGIN_EXAMPLE ... #+END_EXAMPLE e.g.) but the contents of such blocks are not excluded: they are counted in the word count. Some experimentation from the command line with various files will clarify what is included and what is excluded. Try just the grep part (i.e. don't pipe the output into wc -w) to see what is included.

This much has nothing to do with Emacs. Now for the Emacs part. Here's a function that runs the command above on a given file:

(defun ndk/org-wc (&optional file)s 
  (interactive (list (if current-prefix-arg (read-file-name "File: "))))

  (let ((ndk/org-wc-cmd "grep -v -E '^(\\*|#|:)' %s | wc -w"))
    (if file
        (progn 
          (shell-command (format ndk/org-wc-cmd file))
          (with-current-buffer shell-command-buffer-name
            (buffer-substring-no-properties (point-min) (point-max))))
      ;; file is nil - deal with the current buffer
      (when (buffer-modified-p (current-buffer))
        (message "Buffer is modified - word count may be inaccurate")
        (sit-for 2))
      (let ((file (buffer-file-name)))
        (ndk/org-wc file)))))

You can bind the function to a key1 to make it easy to invoke:

(with-eval-after-load 'org
   (keymap-set org-mode-map "C-c w" #'ndk/org-wc))

This assumes you are running Emacs 29 which provides keymap-set. If you are running an earlier version, you might have to say the following instead:

(with-eval-after-load 'org
    (define-key org-mode-map (kbd "C-c w") #'ndk/org-wc))

I assume you only want the key defined when you are in an Org mode file, so I bind it in org-mode-map.

Put the function definition and the code to bind it to a key into your init file, restart Emacs, visit an Org mode file and say C-c w. Or you can invoke it with a prefix argument, C-u C-c w, in which case you will be asked for the name of a file on which to run the word-count.

The code that does the word-counting is basically just three lines:

...
        (progn 
          (shell-command (format ndk/org-wc-cmd file))
          (with-current-buffer shell-command-buffer-name
            (buffer-substring-no-properties (point-min) (point-max))))
...

The rest is there to implement the user interface described above.

One caveat: the function counts words in a file, so if you are in an Org mode buffer and you say C-c w to count the words in it, you might be surprised. If you have not saved the buffer at all, then the file does not exist and you are going to get an error message from grep. If you have saved it previously, but have modified the buffer and not saved it yet, the result you get will reflect the state of the file, not the state of the buffer. So the function checks and issues a warning in that case. Save the buffer and then do C-c w again to get the correct result (and no warning).


[1] I use C-c w as the key, because unless you have defined it, nothing should be bound to it: key sequences like C-c <letter> for any letter are supposed to be available to users for their customizations - any packages that you use should heed that restriction and not bind such sequences (although there might be rogue packages out there - if you find one, open a bug report). You should always check of course, just in case: in this case, open an Org mode buffer and say C-h k C-c w - it should say C-c w is undefined. If so, you can go ahead and use it for your purposes. Feel free however to choose your own mnemonic: just check it first. See the second bullet in Keybinding Conventions in the Emacs Lisp Reference Manual for the "official" statement and some more details.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.