25

According to the Emacs documentation, Directory Variables apply to all files below a directory that contains an .dir-locals.el file.

How can I, in that file, set a variable to the full path that contains the file? For example:

((nil . ((indent-tabs-mode . t)
          (my-project-path **THIS_DIRECTORY**))))
Chris R
  • 17,546
  • 23
  • 105
  • 172
  • 4
    You didn't mention it explicitly, but I wonder if you'd be interested in any of the various "project" packages. For example, I just started using https://github.com/bbatsov/projectile; it might do some of what you wanted to do with directory-locals. – offby1 Jul 05 '14 at 23:49

7 Answers7

14

I asked myself the same question and found no solution on the web, so I think this answer may help. Actually, it turns out we can reuse dir-locals-find-file to get the directory containing the .dir-locals.el file. So here's what I found for, e.g, setting up an aspell personal dictionary dedicated to a whole directory:

((nil . ((eval . (setq ispell-personal-dictionary
                       (expand-file-name
                        ".aspell_words"
                        (file-name-directory
                         (let ((d (dir-locals-find-file ".")))
                           (if (stringp d) d (car d))))))))))

Also, it seems entries are evaluated in the order they are specified, so the following code should work:

((nil . ((eval . (set (make-local-variable 'my-project-path)
                      (file-name-directory
                       (let ((d (dir-locals-find-file ".")))
                         (if (stringp d) d (car d))))))
         (eval . (message "Project directory set to `%s'." my-project-path)))))

Emacs will complain about unsafe local variables (due to the eval construct), yet one can still permanently mark it safe.

Update: Since Emacs ≥ 26.3 (and maybe older versions as well), it appears that one needs to use (dir-locals-find-file "./") instead of (dir-locals-find-file ".").

nberth
  • 561
  • 5
  • 9
  • This approach won't work reliably with more than one .dir-locals.el in scope. – SamB Jul 05 '14 at 20:21
  • @SamB I just tried it, and it seems to work fine for me (as long as you don't have intricate variable dependencies between your `.dir-locals.el` files); with a series of `.dir-locals.el` each exactly containing my last example above, `my-project-path` is set to the longest path in scope of the visited file. Yet, I don't know precisely how the behavior of Emacs w.r.t imbricated `.dir-locals.el` is specified. – nberth Jul 07 '14 at 08:02
  • 2
    My bad: my quick test was biased, as the `.dir-locals.el` were all identical. So indeed, `dir-locals-find-file` always returns the first `.dir-locals.el` encountered by Emacs (the closest to the visited file). I acknowledge the limitation. – nberth Jul 07 '14 at 10:14
  • 2
    Okay, so I've now actually *looked* at the code, and it turns out that only one `.dir-locals.el` is ever consulted WRT any particular buffer in any case. So I'm thinking that the manual needs to be clarified on this point ... – SamB Jul 07 '14 at 17:43
  • 2
    In other words: **My criticism was based on a misunderstanding of how `.dir-locals.el` works**. – SamB Jul 07 '14 at 17:44
  • For whatever reason (my elisp is extremely poor), this file caused problems for me in other modes (magit would "break" every other invocation, complaining that "my-project-path" didn't exist). See my answer below for another way. – Steve Broberg Aug 16 '17 at 14:44
4

I think (file-name-directory (or load-file-name buffer-file-name)) should give you the directory path.

See Link

Edit: Except it won't, because any eval expressions are evaluated in the context of the buffer whose variables are being hacked.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
slu
  • 1,244
  • 9
  • 14
  • 3
    Yeah, but ... as far as I know, emacs doesn't actually evaluate the forms in .dir-locals; instead, it merely reads them, and assigns values to the named variables ... and the values have to be constants. I hope I'm wrong, but I suspect there's no way to do i. – offby1 Oct 25 '10 at 15:11
  • @offby1: This sort of approach seems a bit more likely to work than nberth's, at least ... – SamB Jul 05 '14 at 20:19
  • @SamB I'm disappointed that none of us, apparently, has actually tried this; so I just did. It doesn't work :-( ```((nil . ((did-it-work (file-name-directory (or load-file-name buffer-file-name)))))) ``` I did however get an "unsafe-variable" warning. – offby1 Jul 05 '14 at 23:46
  • @offby1: you have to use `eval`, except it won't work anyway. Oops. – SamB Jul 07 '14 at 04:27
4

In my case, I wanted to locate a file that was relative to my current working directory for my repository, and the .dir-locals.el file was checked into the root, so a file "local" to the .dir-locals.el was also a file "local" to the project root.

pajato0's answer above worked for some cases, but it was also breaking other modes (like magit). I got around the issue by using the projectile package's projectile-project-root function to find my the base path for me:

((nil . ((eval . (setq cmake-ide-build-dir
                   (concat (projectile-project-root) "/build-make"))
         ))))
Steve Broberg
  • 4,255
  • 3
  • 28
  • 40
3

I've found the locate-dominating-file procedure, which comes out-of-the-box with Emacs, useful to retreive the current directory of a known file. The example below sets the guix-directory variable to the topmost directory of the project containing a .dir-locals.el file.

((nil . ((eval . (setq guix-directory
                       (locate-dominating-file default-directory
                                               ".dir-locals.el"))))))

It's not a safe .dir-locals.el setting, due to relying on eval, but it gets the job done.

Apteryx
  • 5,822
  • 3
  • 16
  • 18
  • This will give the wrong answer if there is a `.dir-locals.el` in an ancestor directory, right? So if `/a/.dir-locals.el` and `/a/b/.dir-locals.el` exist, `guix-directory` will be `/a`, regardless of which file this code is in. – Troy Daniels Apr 18 '23 at 15:32
2

In case it still matters, to the OP or some other, I would suggest you create a function to generate the .dir-locals.el file. Then one could write something like:

(let ((path default-directory)
      file)
  (setq file (format "%s/.dir-locals.el" path))
  (with-temp-buffer
    (insert (format "((nil . ((indent-tabs-mode . t)
          (my-project-path \"%s\"))))" path))
    (when (file-writable-p file)
      (write-region (point-min)
                    (point-max)
                    file))))

to be executed within the project home directory.

pajato0
  • 3,628
  • 3
  • 31
  • 37
  • 1
    Hmm. Might not be the *worst* idea in the world, but it still leaves a great deal to be desired! – SamB Jul 05 '14 at 20:16
  • Works until you move the directory. e.g. project specific settings committed to a git repo and someone checks it out elsewhere. – Steve Buzonas Feb 01 '15 at 06:21
1

hack-local-variables is the main function for processing all local variables, and it calls hack-dir-local-variables to deal with the .dir-locals.el file (or a dir local class variable, if you're not using that file).

The code for establishing the directory is not isolated in its own function, so we'll have to copy it out into a new function (this from GNU Emacs 24.0.95.1):

(defun my-dir-locals-dir ()
  "Return the directory local variables directory.
Code taken from `hack-dir-local-variables'."
  (let ((variables-file (dir-locals-find-file (or (buffer-file-name) default-directory)))
        (dir-name nil))
    (cond
     ((stringp variables-file)
      (setq dir-name (file-name-directory variables-file)))
     ((consp variables-file)
      (setq dir-name (nth 0 variables-file))))
    dir-name))
phils
  • 71,335
  • 11
  • 153
  • 198
  • This code seems correct, if a bit too unwieldy to put in `.dir-locals.el` itself ... – SamB Jul 07 '14 at 17:55
  • The function would doubtless live in the same library as the code which required the information in the first place. You would probably just call it as a function instead of setting a variable (although you could pair it up with an automatically buffer-local variable, extending the function to check whether that variable is already set, and just returning the existing value if there is one; only doing the processing to set the value if it's *not* already set). – phils Jul 07 '14 at 22:08
  • Library? Who said anything about a library? – SamB Jul 13 '14 at 04:00
  • Read "elisp file". If there isn't any code which needs the value, there's little point in knowing what it is. I presume that code exists, and will live in an elisp file. – phils Jul 13 '14 at 07:32
0

If you are working on *nix, you might get the work directory by the following elisp code,

(defun get-working-directory ()
    (getenv "PWD))

Btw, I have to mentioned that, (shell-command "pwd") will result in the directory where file (which is corresponding to the buffer you are currently editing).

Petter Friberg
  • 21,252
  • 9
  • 60
  • 109
Junwei WANG
  • 239
  • 2
  • 12