Source

edit-utils / after-save-commands.el

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
;;; after-save-commands.el --- Run a shell command or lisp form after saving a file

;; Copyright (C) 1997,98,99 by  Karl M. Hegbloom

;; $Id$
;; Author: Karl M. Hegbloom <karlheg@cathcart.sysc.pdx.edu>
;; Keywords: processes,unix

;; 25-Oct-2002 vladimir@worklogic.com: 
;; - the predicate can also be a lisp form instead of a regexp string
;; - the command can also be a lisp form instead of a shell command  
;; - documented After-save-alist better

;; This file is part of XEmacs.

;;; This might be rolled into `files.el' at some point in the near
;;; future, pending bug fixes, functionality/feature froze, and the
;;; approval of the XEmacs development team (and perhaps RMS... who
;;; will need to find someone to port it some for GNU Emacs if he
;;; would like to.)


;; XEmacs 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, or (at your option)
;; any later version.

;; XEmacs 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 XEmacs; see the file COPYING.  If not, write to the Free
;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
;; 02111-1307, USA.

;;; Commentary:

;;; Set up a list of file-name matching regular expressions associated
;;; with shell commands or lisp forms to run after saving the file.

;;; This is good for things like running `newaliases(1)' on
;;; "/etc/aliases", `xrdb(1)' on "~/.Xresources", installing a new
;;; "~/.crontab", as well as for sending signals to daemons whos
;;; configuration files you've just finished editing.
;;; 
;;; It is much safer and more powerful than using exec statements in
;;; "Local Variables" sections, and can safely be used by root for
;;; system administration tasks.  The shell command can run about
;;; anything you can think of.
;;;
;;; Who knows?  Maybe Homekey Symsun will use this feature, and it
;;; will allow per to quickly run a daemon HUP after reconfiguration
;;; of the nuclear plant control file, thus saving an entire duchy
;;; from immanent destruction!
;;;
;;; See variable `After-save-alist' for more information.

;;; Devel Notes:
;;;
;;;  I would like to perhaps... pull out the ~/ and ~name/ expansion
;;;  from `expand-file-name', and make a `tilde-expand'
;;;  function... and also a `expand-environment-in-string' that
;;;  doesn't try to do any tilde expanditsomes.  The regexp ought to
;;;  be split (by my code in this file?) on \\|, etc., and in the
;;;  relevant locations, expansion should be done, so that the regexp
;;;  can contain ~'s, and have it do what I mean there, like to match
;;;  files in a user's home directory...  ? maybe.

;;; Code:
;;;-----------------------------------------------------
(require 'env)
(require 'advice)

(defmacro After-save--with-modeline-process-extent-ext (&rest body)
  `(and modeline-process
	(consp modeline-process)
	(let ((ext (car modeline-process)))
	  (and (extentp ext) 
	       ,@body))))

(defun After-save--set-help-flyover (str)
  (After-save--with-modeline-process-extent-ext
   (let ((state (After-save--ascmd-property)))
     (unless (stringp str) (setq str (format "%s" str)))
     (cond
      ((eq :on state)
       (setq str (concat "Run: " str)))
      ((eq :off state)
       (setq str (concat "DON'T run: " str)))))
   (set-extent-property ext 'help-echo str)))

(defun After-save--get-help-flyover ()
  (After-save--with-modeline-process-extent-ext
   (let ((str (extent-property ext 'help-echo)))
     (string-match "^[^:]+:\\(.*\\)\\'" str)
     (match-string 1 str))))

(defun After-save--ascmd-property ()
  "Return the 'ascmd property of our extent in `modeline-process' if it
exists."
  (After-save--with-modeline-process-extent-ext
   (extent-property ext 'ascmd)))


(defun After-save--entry-lookup (buf-fn)
  "Lookup BUF-FN in `After-save-alist', and return that record."
  (if buf-fn
      (catch 'return
	(mapc #'(lambda (elt)
                  (if (cond ((stringp (car elt))
                             (string-match (car elt) buf-fn))
                            ((eval (car elt))))
                      (throw 'return elt)))
	      After-save-alist)
	(throw 'return nil))))

;; Q: Is `modeline-process' ok?  Or is there a more standard place for
;; this?  At some point I'll be able to answer my own question, but
;; now have lots else I ought to be doing. (like reading for
;; instance.)  Please advise.  Yeah or Nay?

;;;###autoload
(defun After-save--find-file-hook ()
  "Look up the name of this file in `After-save-alist', and if it has
an entry, turn on the modeline button and indicator."
  (let ((file->cmd-entry (After-save--entry-lookup (buffer-file-name))))
    ;;; (declarelike (special file->cmd-entry)) ; need dynamic scope,
    ;;; this time.  Put up with one byte compiler warning.
    (if file->cmd-entry
	(After-save--install-ascmd))))

;; `autoload' these just in case then get stuck on the hook before the
;; setting of `After-save-alist' brings this program in with its
;; :require.  I think that could happen if the `find-file-hooks' gets
;; saved in `options.el' after this has been installed on it.  That
;; variable might come before the `defcustom'ed variables at the top
;; of this program.  Of course, once this feature is rolled into
;; "files.el", there's no need for it to be in the hooks anymore;
;; it'll be more inline... right?

;;;###autoload
(defun After-save--after-save-hook ()
  "An `after-save-hook' to run a shell-command.
This gets hung on the `after-save-hook'.
See: `After-save-alist'."
  (let ((file->cmd-entry (After-save--entry-lookup (buffer-file-name))))
    (if (and file->cmd-entry
	     (eq :on (After-save--ascmd-property)))
        (let ((confirm-exec-flag (third  file->cmd-entry))
              (command           (fourth file->cmd-entry)))
          (cond ((and confirm-exec-flag
                      (not (y-or-n-p (format "Run: %s ? " command))))
                 ;; user declined: do nothing
                 )
                ((not (stringp command)) ;; command is lisp sexp
                 (message "%s ->%s" command (eval command)))
                (t                      ;; shell command
                 ;; The `copy-sequence' is important, since `setenv' mutates
                 ;; `process-environment' in place. (uses `setcar'...)  We want
                 ;; a copy of `process-environment' to get bound here, so the
                 ;; more global one, outside of this `let*' block, doesn't get
                 ;; its elements modified like that.
                 (let ((process-environment (copy-sequence process-environment))
                       (environ-alist (second file->cmd-entry)))
                   ;; Here, the bound `process-environment' is modified, not
                   ;; the more global one.  The current dynamic value of it
                   ;; will get passed to the command's shell.
                   (setenv "f" (buffer-file-name))
                   (setenv "d" (default-directory))
                   (if environ-alist
                       (mapc #'(lambda (env-pair)
                                 (setenv (car env-pair)
                                         (if (cdr env-pair)
                                             ;; expand $vars
                                             ;; does not expand tilde's...
                                             ;; there is no `expand-environ-in-string'
                                             ;; or `tilde-expand' AFAIK
                                             (substitute-in-file-name (cdr env-pair))
                                           nil)))
                             environ-alist))
                   (shell-command command))))))))

;; I'd just use `minor-mode-alist', but I want the fly-over to show
;; the after save command rather than "button2 will...".
(defun After-save--install-ascmd ()
  "Install a modeline indicator."
  (let ((ext (make-extent nil nil))
	(km (make-sparse-keymap)))
    (set-extent-property ext 'ascmd :on)
    (set-extent-face ext 'modeline-mousable-minor-mode)
    ;;; ref to free var file->cmd-entry : rely on dynamic scope.
    (define-key km [(button2)] #'(lambda ()
				   (interactive)
				   (After-save--toggle-ascmd)))
    (set-extent-keymap ext km)
    (setq modeline-process (cons ext " AScmd"))
    (After-save--set-help-flyover (fourth file->cmd-entry))
    (redraw-modeline)))


(copy-face 'modeline-mousable-minor-mode 'After-save--strikethu-face)
;;; Wish: (set-face-strikethru-p 'After-save--strikethu-face t :strikethru-spaces nil)
(set-face-strikethru-p 'After-save--strikethu-face t)

(defun After-save--toggle-ascmd ()
  "Turn AScmd off if on, on if off, but not on if not installed in this
buffer yet."
  (interactive)
  (let* ((ext (and modeline-process
		   (consp modeline-process)
		   (car modeline-process)))
	 (state (After-save--ascmd-property)))
    (and (extentp ext)
	 (cond
	  ((eq state :off)
	   (set-extent-property ext 'ascmd :on)
	   (set-extent-face ext 'modeline-mousable-minor-mode))
	  ((eq state :on)
	   (set-extent-property ext 'ascmd :off)
	   (set-extent-face ext 'After-save--strikethu-face))))
    (After-save--set-help-flyover (After-save--get-help-flyover))
    (redraw-modeline)))

;; Will of course be unnecessary once this is part of "files.el"
;; someday when it grows up and is ready to join the core of
;; xemacs/lisp/*.el society as a full fledged member.
(defadvice write-file (before After-save activate)
  (if (After-save--ascmd-property)
      (setq modeline-process nil)))

 ;; At least in XEmacs-21.0 Pyrenean63, `write-file' calls
 ;; `set-visited-file-name' which uses `kill-local-variable' to clear
 ;; both `write-file-hooks' and `after-save-hook', amoung others...

(defadvice write-file (after After-save activate)
  (After-save--find-file-hook))

;; I shouldn't have to use `after-init-hook' like this...  Or should
;; I?  I think maybe I ought to be able to use just a straight out
;; `add-hook' here.  But when `find-file-hooks' has been customized,
;; it's value can get set to an arbitrary list after this `add-hook'
;; is run, thus wiping it out. So I have to install it on the
;; `after-init-hook' like this.  It might be nice if the hook type
;; would be initialized by custom with an add-hook... Then again,
;; maybe sticking an add-hook onto the `after-init-hook' like this is
;; really just the standard way of getting a function onto the list?
;; Perhaps by makeing `custom-set-variables' set the hook to an
;; absolute value, we make it possible to know for certain what it's
;; startup time value will be...  minus additions by packages like
;; this one.  YTMAWBK OTOH, perhaps `custom-set-variables' ought to
;; use `add-hook', so that whenif things like this are installed
;; earlier than when the options.el file is run, they won't get wiped
;; out.
(add-hook 'after-init-hook
	  #'(lambda ()
	      (add-hook 'find-file-hooks 'After-save--find-file-hook)
	      (add-hook 'after-save-hook 'After-save--after-save-hook)))

;; And once for when we load this, in case that's sometime after the
;; `after-init-hook' has already been run, like the first time a new
;; user customizes `After-save-alist'.  `add-hook' will ensure it's
;; only in there once.
(add-hook 'find-file-hooks 'After-save--find-file-hook)
(add-hook 'after-save-hook 'After-save--after-save-hook)

;;; "... or should I just put ;;;###autoload cookies in front of those
;;; add-hook's?"  No, again, I'm afraid that somebody might customize
;;; the hooks and they'll overwrite what the `autoload' brings in. And
;;; besides, there's a :require statement in the `defcustom' for
;;; `After-save-alist', which is easily accessed from the [ Options |
;;; Customize | Emacs | Files ] menu.

(defcustom After-save-alist
  '(("/etc/X11/Xresources/\\|/\\.Xresources" nil t "xrdb $f")
    ("/\\.crontab\\'" nil t "crontab $f")
    ("/etc/inetd.conf" nil t "echo /etc/init.d/netbase reload")
    ("\\.mailrc" nil t (build-mail-aliases))
    ((eq major-mode 'sh-mode) nil t "chmod a+x $f")
    ("#  __JUST_FOR_EXAMPLE__  #"
     (("Set_ME" . "to some value")
      ("UN_Set_ME"))
     nil "echo 'rm -rf / && Bwahahahha!'"))
  "*List associating file regexp or condition to shell command or lisp form.
Each element is of the form
  (PREDICATE ((VAR1 . VAL1) (VAR2 . VAL2) (VAR3)...) PROMPT-P COMMAND)
where
  PREDICATE    is a string regexp to match against the file name,
               or a lisp form to evaluate.
  (VAR . VAL)  are environment variable assignments.
               Omit VAL to unset VAR.
  PROMPT-P     whether to ask for confirmation each time the file is saved 
               and prior to running the command.
  COMMAND      Shell command (string) to run or lisp form to eval,
               after the file is saved.

While you are visiting a file that has an `after-save-command' associated with
it, the modeline will display \"AScmd\" in the minor mode list, and moving the
mouse over that indicator will cause the buffer's associated shell command to
be displayed in the minibuffer. Clicking button 2 there will toggle whether
the command will be run or not.

This facility can be very handy for doing things like:
- running `newaliases(1)' after you've edited the `sendmail(8)' daemon's
  \"/etc/aliases\" file,
- running `xrdb\(1)' after you've hand-tweaked your \".Xresource\" settings
- installing a \".crontab\"
- sending a signal to a system daemon whose configuration file you've edited.
- running (build-mail-aliases) after you've edited .mailrc

You may create or change these settings while you are visiting a file, since
it works by installing a function in the global `after-save-hook', and a
lookup in `After-save-alist' for your command spec happens then. You may also
change the settings for a file that's already got an after-save entry, prior
to saving it.

The command you specify will be run in a subshell, out of the
`after-save-hook', using the lisp function `shell-command'. You can cause it
to background by suffixing the command with the usual \"&\". It will inherit
the `process-environment' of your XEmacs session, along with the specified
environment modifications, as well as the following automatically defined
variables:

   $f -- The full path and filename of the file, `buffer-file-name'
   $d -- The directory, with a trailing \"/\" where the file was saved.

The `Var=\"Value\" pair' environment variables will be defined in the context
the shell command will be run in. You may reference previously defined
environment variables within the `Value' fields, since they are expanded
sequentially, from top to bottom, using `substitute-in-file-name', just before
the command is run. $f and $d are set first, and so may be used for expansion
within your environment specifications, as well as in the commandline.

Note that no shell processing will be done until the commandline is fed to
your shell. That is, globbing or brace expansions and things don't happen
until the command is run.

If you use `write-file' (`C-x C-w') to write the visited buffer to a different
filename, the `after-save-command' will not be run, and the after save command
property will be removed from the buffer, unless the new file name matches one
of your `After-save-alist' specifications."

  :require 'after-save-commands
  :set #'(lambda (var val)
	   (set-default var val)
	   (mapc #'(lambda (b)
		     (with-current-buffer b
		       (if (After-save--ascmd-property)
			   (setq modeline-process nil))
		       (After-save--find-file-hook)))
		 (buffer-list))
	   (redraw-modeline t))
  :type '(repeat
	  (list :tag
		"------------------------------------------------------------"
		:indent 2
                (choice (regexp :tag "File name regexp" "")
                        (sexp :tag "ELisp form (predicate)" ""))
		(repeat :tag "Environment"
			:indent 1
                        (cons :tag "Var=\"Value\" pair"
                              (string :tag "Variable" "")
                              (choice (string :tag "Value" "")
				      (const :tag "unset" nil))))
		(boolean :tag "Confirm before execution? " t)
                (choice (string :tag "Shell Command line (use $f and $d)" "")
                        (sexp :tag "ELisp form to evaluate " ""))))
  :group 'files)

(provide 'after-save-commands)
;;; after-save-commands.el ends here


;; vladimir@worklogic.com: this below has some nicer output capabilities,
;; consider merging into the code above.

'(defun my-after-save-action (predicate action)
  (if (cond ((and (stringp predicate)
                  (eq ?$ (aref predicate (1- (length predicate)))))
             (string-match predicate buffer-file-name))
            ((stringp predicate)
             (string= buffer-file-name (expand-file-name predicate)))
            (t (eval predicate)))
      (let (async descr result time)
        (if (setq async (eq (car action) :async))
            (setq action (cdr action)))
        (when (stringp (car action))
          (setq descr (concat "`" (mapconcat 'identity
            (append action (list (file-name-nondirectory buffer-file-name)))
            " ") "'"))
          (setq action (append action (list buffer-file-name))))
        (cond
         (async
          (apply 'start-process descr
                 (setq buf (concat "*output " descr "*"))
                 action)
          (message "%s started, see buffer %s." descr buf))
         (descr
          (setq time (second (current-time)))
          (message "%s started, please wait..." descr)
          (setq result (apply 'call-process
                              (car action) nil nil nil (cdr action)))
          (message "%s returned %s." descr result)
          (if (< 3 (- (second (current-time)) time))
              (ding)))
         (t (message "%s returned %s." action (eval action)))))))
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.