Могу ли я создавать каталоги, которые не существуют, при создании нового файла в emacs?

29

В emacs я создаю файл, посещая его с помощью Cx Cf. Допустим, я хотел бы создать /home/myself/new_directory/file.txt.

Если new_directory еще не существует, есть ли способ создать его во время создания file.txtбез каких-либо дополнительных шагов? (Я думаю о чем-то вроде использования -pфлага mkdirв Linux.)

Я чувствую, что вместо Cx Cf можно нажать другую клавишу, но я не могу вспомнить, что это такое.

Джим Хунзикер
источник
В emacs нет эквивалента для запуска команды при редактировании ?? в vi вы могли бы:!mkdir -p ~/new_dir/to/some/crazy/path
DaveParillo
3
@DaveParillo: Конечно, есть, M-!например.
Никана Рекламикс
Я использую плагин prelude ( github.com/bbatsov/prelude ). Всякий раз, когда я создаю файлы, как указано выше, мне предлагается «Создать каталог ...». Я могу просто выбрать «у». Затем он спрашивает меня: «Файл не существует, создать? (Y или n)». Я выбираю y, который создает новый файл. Когда я сохраняю файл, он создает файл с вышеуказанной информацией.
Прамод

Ответы:

25

Вы также можете посоветовать функцию find-fileпрозрачного создания необходимых каталогов.

(defadvice find-file (before make-directory-maybe (filename &optional wildcards) activate)
  "Create parent directory if not exists while visiting file."
  (unless (file-exists-p filename)
    (let ((dir (file-name-directory filename)))
      (unless (file-exists-p dir)
        (make-directory dir)))))

Просто поместите это в свое .emacsместо и используйте C-x C-fкак обычно.

Тёрёк Габор
источник
2
Прекрасное решение. Мне нравится, как улучшение мелких вещей может открыть целый новый мир подсказок, чтобы emacs мог делать вещи лучше (да, я не знал об этом defadviceраньше).
Никана Рекламикс
18

Когда я предоставляю путь с несуществующим компонентом find-file(например, Cx Cf ), я получаю дополнительное сообщение, которое говорит

Используйте Mx make-directory RET RET, чтобы создать каталог и его родителей

Поскольку файл не создается до тех пор, пока вы не сохраните буфер в первый раз, вы можете запустить его make-directoryсразу после запуска нового буфера или сделать это в любое другое время, прежде чем вам нужно будет сохранить файл. Затем из буфера, который нуждается в каталоге, введите Mx make-directory RET RET (он предложит создать каталог (значение по умолчанию получено из пути буфера); второй RET должен принять значение по умолчанию).

Крис Джонсен
источник
14

Режим Ido предусматривает ido-find-fileзаменуfind-file и предоставляет вам гораздо больше возможностей. Например, он позволяет вам создавать новый каталог, пока вы открываете файл.

  • Введите C-x C-fкак обычно (который переназначается наido-find-file ),

  • предоставить несуществующий путь,

  • Нажмите M-m который предложит создать новый каталог,

  • и затем укажите имя файла для посещения во вновь созданном каталоге.

Тёрёк Габор
источник
Я не понимаю: зачем нажимать M-mв какой-то момент, и C-x C-fвообще, если это не создает все автоматически? Если будет предложено создать каталог, M-! mkdirили Dired будет делать то же самое ...
Никана Рекламикс
Есть ли способ ido-find-fileавтоматически создать каталог? Или, что еще лучше, спросите меня, хочу ли я его создать? Я попытался использовать совет в ответе Тёрока Габора, но я не мог понять, к какой функции его применять (поскольку ido не вызывает find-fileнапрямую.
Трой Дэниелс
1

Мое решение приходит с бонусом: если вы убьете буфер, не сохранив его, Emacs предложит удалить те пустые каталоги, которые были созданы (но только если их не было до того, как вы вызвали find-file):

;; Automatically create any nonexistent parent directories when
;; finding a file. If the buffer for the new file is killed without
;; being saved, then offer to delete the created directory or
;; directories.

(defun radian--advice-find-file-automatically-create-directory
    (original-function filename &rest args)
  "Automatically create and delete parent directories of files.
This is an `:override' advice for `find-file' and friends. It
automatically creates the parent directory (or directories) of
the file being visited, if necessary. It also sets a buffer-local
variable so that the user will be prompted to delete the newly
created directories if they kill the buffer without saving it."
  ;; The variable `dirs-to-delete' is a list of the directories that
  ;; will be automatically created by `make-directory'. We will want
  ;; to offer to delete these directories if the user kills the buffer
  ;; without saving it.
  (let ((dirs-to-delete ()))
    ;; If the file already exists, we don't need to worry about
    ;; creating any directories.
    (unless (file-exists-p filename)
      ;; It's easy to figure out how to invoke `make-directory',
      ;; because it will automatically create all parent directories.
      ;; We just need to ask for the directory immediately containing
      ;; the file to be created.
      (let* ((dir-to-create (file-name-directory filename))
             ;; However, to find the exact set of directories that
             ;; might need to be deleted afterward, we need to iterate
             ;; upward through the directory tree until we find a
             ;; directory that already exists, starting at the
             ;; directory containing the new file.
             (current-dir dir-to-create))
        ;; If the directory containing the new file already exists,
        ;; nothing needs to be created, and therefore nothing needs to
        ;; be destroyed, either.
        (while (not (file-exists-p current-dir))
          ;; Otherwise, we'll add that directory onto the list of
          ;; directories that are going to be created.
          (push current-dir dirs-to-delete)
          ;; Now we iterate upwards one directory. The
          ;; `directory-file-name' function removes the trailing slash
          ;; of the current directory, so that it is viewed as a file,
          ;; and then the `file-name-directory' function returns the
          ;; directory component in that path (which means the parent
          ;; directory).
          (setq current-dir (file-name-directory
                             (directory-file-name current-dir))))
        ;; Only bother trying to create a directory if one does not
        ;; already exist.
        (unless (file-exists-p dir-to-create)
          ;; Make the necessary directory and its parents.
          (make-directory dir-to-create 'parents))))
    ;; Call the original `find-file', now that the directory
    ;; containing the file to found exists. We make sure to preserve
    ;; the return value, so as not to mess up any commands relying on
    ;; it.
    (prog1 (apply original-function filename args)
      ;; If there are directories we want to offer to delete later, we
      ;; have more to do.
      (when dirs-to-delete
        ;; Since we already called `find-file', we're now in the buffer
        ;; for the new file. That means we can transfer the list of
        ;; directories to possibly delete later into a buffer-local
        ;; variable. But we pushed new entries onto the beginning of
        ;; `dirs-to-delete', so now we have to reverse it (in order to
        ;; later offer to delete directories from innermost to
        ;; outermost).
        (setq-local radian--dirs-to-delete (reverse dirs-to-delete))
        ;; Now we add a buffer-local hook to offer to delete those
        ;; directories when the buffer is killed, but only if it's
        ;; appropriate to do so (for instance, only if the directories
        ;; still exist and the file still doesn't exist).
        (add-hook 'kill-buffer-hook
                  #'radian--kill-buffer-delete-directory-if-appropriate
                  'append 'local)
        ;; The above hook removes itself when it is run, but that will
        ;; only happen when the buffer is killed (which might never
        ;; happen). Just for cleanliness, we automatically remove it
        ;; when the buffer is saved. This hook also removes itself when
        ;; run, in addition to removing the above hook.
        (add-hook 'after-save-hook
                  #'radian--remove-kill-buffer-delete-directory-hook
                  'append 'local)))))

;; Add the advice that we just defined.
(advice-add #'find-file :around
            #'radian--advice-find-file-automatically-create-directory)

;; Also enable it for `find-alternate-file' (C-x C-v).
(advice-add #'find-alternate-file :around
            #'radian--advice-find-file-automatically-create-directory)

;; Also enable it for `write-file' (C-x C-w).
(advice-add #'write-file :around
            #'radian--advice-find-file-automatically-create-directory)

(defun radian--kill-buffer-delete-directory-if-appropriate ()
  "Delete parent directories if appropriate.
This is a function for `kill-buffer-hook'. If
`radian--advice-find-file-automatically-create-directory' created
the directory containing the file for the current buffer
automatically, then offer to delete it. Otherwise, do nothing.
Also clean up related hooks."
  (when (and
         ;; Stop if there aren't any directories to delete (shouldn't
         ;; happen).
         radian--dirs-to-delete
         ;; Stop if `radian--dirs-to-delete' somehow got set to
         ;; something other than a list (shouldn't happen).
         (listp radian--dirs-to-delete)
         ;; Stop if the current buffer doesn't represent a
         ;; file (shouldn't happen).
         buffer-file-name
         ;; Stop if the buffer has been saved, so that the file
         ;; actually exists now. This might happen if the buffer were
         ;; saved without `after-save-hook' running, or if the
         ;; `find-file'-like function called was `write-file'.
         (not (file-exists-p buffer-file-name)))
    (cl-dolist (dir-to-delete radian--dirs-to-delete)
      ;; Ignore any directories that no longer exist or are malformed.
      ;; We don't return immediately if there's a nonexistent
      ;; directory, because it might still be useful to offer to
      ;; delete other (parent) directories that should be deleted. But
      ;; this is an edge case.
      (when (and (stringp dir-to-delete)
                 (file-exists-p dir-to-delete))
        ;; Only delete a directory if the user is OK with it.
        (if (y-or-n-p (format "Also delete directory `%s'? "
                              ;; The `directory-file-name' function
                              ;; removes the trailing slash.
                              (directory-file-name dir-to-delete)))
            (delete-directory dir-to-delete)
          ;; If the user doesn't want to delete a directory, then they
          ;; obviously don't want to delete any of its parent
          ;; directories, either.
          (cl-return)))))
  ;; It shouldn't be necessary to remove this hook, since the buffer
  ;; is getting killed anyway, but just in case...
  (radian--remove-kill-buffer-delete-directory-hook))

(defun radian--remove-kill-buffer-delete-directory-hook ()
  "Clean up directory-deletion hooks, if necessary.
This is a function for `after-save-hook'. Remove
`radian--kill-buffer-delete-directory-if-appropriate' from
`kill-buffer-hook', and also remove this function from
`after-save-hook'."
  (remove-hook 'kill-buffer-hook
               #'radian--kill-buffer-delete-directory-if-appropriate
               'local)
  (remove-hook 'after-save-hook
               #'radian--remove-kill-buffer-delete-directory-hook
               'local))

Каноническая версия здесь .

Радон Росборо
источник
0

В дополнение к предложению @Chris для Mx make-directory вы можете написать короткую функцию elisp, которая будет выполнять find-file, а затем make-directory ... Вы можете попробовать это:

(defun bp-find-file(filename &optional wildcards)
  "finds a file, and then creates the folder if it doesn't exist"

  (interactive (find-file-read-args "Find file: " nil))
  (let ((value (find-file-noselect filename nil nil wildcards)))
    (if (listp value)
    (mapcar 'switch-to-buffer (nreverse value))
      (switch-to-buffer value)))
  (when (not (file-exists-p default-directory))
       (message (format "Creating  %s" default-directory))
       (make-directory default-directory t))
  )

Это не красиво, и он вспыхивает «it MX make-directory ....», прежде чем сказать «Создание каталога ...», но если ничего другого, это должно дать вам начало.

Брайан Постов
источник
2
В случае такого подхода лучше рекомендовать исходную find-fileфункцию вместо определения новой, чтобы другие функции, вызывающие find-fileнапрямую, могли даже извлечь выгоду из измененного поведения.
Тёрёк Габор
но find-file, похоже, не принимает никаких аргументов, которые могут сказать, что он делает это ... Если только в find-file-hooks нет ничего полезного ...
Brian Postow
Я имел в виду это: superuser.com/questions/131538/…
Török Gábor
0
(make-directory "~/bunch/of/dirs" t)

Если ваши каталоги уже существуют, он просто выдаст предупреждение.

Из ( C-h f make-directory RET) руководства:

make-directory - это интерактивная скомпилированная функция Lisp.

(make-directory DIR &optional PARENTS)

Создайте каталог DIRи, возможно, любые несуществующие родительские каталоги. Если DIRкаталог уже существует, сообщите об ошибке, если только PARENTS является ноль.

В интерактивном режиме по умолчанию выбирается каталог для создания - каталог по умолчанию для текущего буфера. Это полезно, когда вы посетили файл в несуществующем каталоге.

Неинтерактивно второй (необязательный) аргумент PARENTS, если не ноль, указывает, следует ли создавать родительские каталоги, которые не существуют. Интерактивно это происходит по умолчанию.

Если создать каталог или каталоги не удастся, возникнет ошибка.

yPhil
источник