;;; after-save-commands.el --- Run a shell command after saving a file

;; Copyright (C) 1997 by  Karl M. Hegbloom

;; $Id: after-save-commands.el,v 1.3 1997/10/16 16:16:15 karlheg Exp karlheg $
;; Author: Karl M. Hegbloom <karlheg@inetarena.com>
;; Keywords: processes,unix

;; This file is part of XEmacs.

;; 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 to run after saving the file.

;;; This is good for things like running `newaliases(1)' on
;;; "/etc/aliases", or `xrdb(1)' on "~/.Xresources", as well as
;;; sending signals to daemons whos configuration files you've just
;;; finished editting.
;;; 
;;; It is much safer 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.
;;;
;;; See: `After-save-alist' for more information.

;;; Code:
;;;-----------------------------------------------------

(require 'cl)

(defgroup after-save nil
  "Run a configurable shell command after a file is saved."
  :group 'files)

(defun After-save--initialize-global-toggle-menu ()
  "Puts the menus up for `after-save-commands'."
  (when (featurep 'menubar)
    (unless (assoc "File Options"
		   (assoc* "Options" default-menubar
			   ;; menus might be named like "%_Options".
			   :test #'(lambda (menu-name-regexp item-string)
				     (string-match menu-name-regexp item-string))))
      (add-menu-button '("Options")
		       '("File Options" ["After-save commands global enable" nil nil])
		       "General Options"))
    (add-menu-button '("Options" "File Options")
		     ["After-save commands global enable"
		      toggle-After-save-commands-globally
		      :style toggle
		      :selected After-save-commands--enable-globally])
    (add-menu-button '("File")
		     ["Run `After-save' command?"
		      toggle-After-save-commands-locally
		      :style toggle
		      :included (member 'After-save--after-save-hook after-save-hook)
		      :active After-save-commands--enable-globally
		      :selected After-save-commands--enable-locally]
		     "Save As...")))

(defvar After-save-commands--enable-globally nil)
(defcustom After-save-commands--enable-globally nil
  "*Non-nil means run a command in `After-save-alist' when a file is saved,
when its `buffer-file-name' matches one of the regexp specs.  Setting
this with `customize-variable' or the Options menu will set the
default, and saving it will set the startup default.  Turning this off
will globally disenable the `after-save-commands', turn it on will
globally enable them, except for those buffers that are locally
disenabled, from the toggle on the File menu.

See: `toggle-After-save-commands-globally'
and: `toggle-After-save-commands-locally'.
"
  :tag "Enable globally"
  :type 'boolean
  :require 'after-save-commands
  :group 'after-save)

(defun toggle-After-save-commands-globally (&rest ignore)
  "Globally Toggles whether the commands in `After-save-alist' are run or not.
This is run as the callback from a toggle on the [Options | File
Options...] menu.
"
  (interactive)
  (setq After-save-commands--enable-globally (not After-save-commands--enable-globally)))

(defvar After-save-commands--enable-locally nil)
(make-variable-buffer-local 'After-save-commands--enable-locally)
(defun toggle-After-save-commands-locally (&rest ignore)
  "Locally toggles whether the commands in `After-save-alist' are run or not,
on a per file buffer-local basis.  This is run as a callback from a
toggle on the [File] menu.
"
  (interactive)
  (setq After-save-commands--enable-locally (not After-save-commands--enable-locally)))

(defcustom After-save-alist
'(("/\\.X\\(resource\\|default\\)s$"	; File Regexp
   (("aft_save_Blahb" . "/path/to/blahb_data") ; environment additions
    ("aft_sav_Foo" . "$Blahb:/another/strange/path/")
    ("aft_sav_Unset_Me"))		; ... and deletions
   t					; confirm-exec-flag
   "echo xrdb -merge $f")		; shell commandline specification
  ("/etc/aliases" nil t "echo newaliases"))
    "*This is a list associating file name patterns to shell commands.

These are shell commands you would like XEmacs to run for you after a
file with a name that matches a regexp has been editted and saved.

You may also specify whether you want to be asked for confirmation
each time you save the file, prior to running that command, on a per
command basis.

They may be turned on and off, either globally, from the [Options |
File Options...]  menu, on the toggle button there, or per matching
file, from the [File] menu, where another toggle will appear while
you're standing in a buffer whos `buffer-file-name' is matched by one
of the `File Regexp' specifiers.

This facility can be very handy for doing things like running
`newaliases\(1)' after you've editted the `sendmail\(8)' daemon's
\"/etc/aliases\" file, running `xrdb\(1)' after you've hand tweaked
your X resource settings, or sending a signal to a system daemon whos
configuration file you've just modified.

You may change these settings while your file is in an editor buffer,
prior to saving it, thus running the command you've attached to it.
Changes you make here with this customization widget interface will
take effect immediately, iff the file had an entry when it was
visited.  If it did not, you'll have to save it, kill the buffer, and
re-visit it to cause a new buffer-local `after-save-hook' to be
installed on its buffer.  \(See: `after-save--after-save-hook')

Take care to specify the regular expressions carefully.  If you
encounter a `wrong type argument, stringp nil' when saving a file,
check that you've not changed the regexp for the file you are visiting
to one that no longer matches it's `buffer-file-name'.  The file has
already been saved when the error is signalled, so you have probably
not lost any work.

The command you specify will be run in a subshell , with the lisp
function `shell-command', out of the `after-save-hook'.  It will
inherit the `process-environment' of your XEmacs session, along with
the specified environment additions or undefines, as well as the
following automaticly defined variables:

   $f -- The full path and filename of the file.
   $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 run in.  You may reference previously
defined environment variables within the `Value' fields, since they
are expanded using `substitute-in-file-name' at the time the command's
environment is set up, just before it is run.  $f and $d are set
first, and so may be used for this purpose."
  :require 'after-save-commands
  :type '(repeat
	  (list :tag "file->command List"
		:indent 2
		(regexp :tag "File name regexp " "")
		(repeat :tag "Environment"
			(cons :tag "Var=\"Value\" pairs "
			      (string :tag "Variable " "")
			      (choice (string :tag "Value " "")
				      (const :tag "Undefine" nil))))
		(boolean :tag "Confirm before execution? " t)
		(string :tag "Shell Command line " "")))
  :group 'after-save)


(defun After-save--entry-lookup (buf-fn)
  "Lookup BUF-FN in `After-save-alist', and return that record."

  ;; Notes; - Things I thought of while I wrote this code.
  ;; This lookup gets done twice, but only for files for which we
  ;; install an `after-save-hook'.
  ;; 
  ;; By looking it up again in the `After-save--after-save-hook', it
  ;; becomes possible to modify an entry, using `customize-variable',
  ;; while the buffer is still alive, and have that take effect before
  ;; the file is saved.
  ;;
  ;; The alternative was to form a closure over the return value from
  ;; the first `assoc*' lookup using a `lexical-let' around the
  ;; `fset'.  That solution would have required that the buffer be
  ;; killed and refound for the new settings here to get installed
  ;; into its buffer-local `after-save-hook'.  I suppose that there
  ;; are cases where capturing state in a closure like that would be
  ;; useful, but here it is not.

  (dolist (elt After-save-alist)
    (when (string-match (regexp-quote (expand-file-name (car elt)))
			buf-fn)
      (return elt))))

(defun After-save--after-save-hook ()
  "An `after-save-hook' to run a shell-command.
This gets hung on the buffer-local `after-save-hook', in buffers whos
`buffer-file-name' match one of the regular expressions in
`After-save-alist', by `After-save--find-file-hook'."
  (when (and After-save-commands--enable-globally
	     After-save-commands--enable-locally)
    ;; The `copy-sequence' is important!
    ;; 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.
    (let* ((process-environment	(copy-sequence process-environment))
	   (file->cmd-entry	(After-save--entry-lookup (buffer-file-name)))
	   (environ-alist	(second file->cmd-entry))
	   (confirm-exec-flag	(third  file->cmd-entry))
	   (command		(fourth 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))
      (dolist (env-pair environ-alist)
	(setenv (car env-pair) (if (cdr env-pair)
				   ;; does not expand tilde's...
				   ;; there is no `expand-environ-in-string'
				   (substitute-in-file-name (cdr env-pair))
				 nil)))
      (if confirm-exec-flag
	  (when (y-or-n-p-maybe-dialog-box
		 (substitute-in-file-name
		  (format "Run:%S ? " command)))
	    (shell-command command))
	(shell-command command)))))

;; autoload this just in case it gets stuck on the hook before the
;; setting of `After-save-alist' brings this 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.
;;;###autoload
(defun After-save--find-file-hook ()
  "Look up the name of this file in `After-save-alist', and if it has
an entry, install `After-save--after-save-hook' on a buffer local
`after-save-hook' for it, and set `After-save-commands--enable-locally'."
  (let ((file->cmd-entry (After-save--entry-lookup (buffer-file-name))))
    (when file->cmd-entry
      (make-local-hook 'after-save-hook)
      (add-hook 'after-save-hook 'After-save--after-save-hook nil t)
      (setq After-save-commands--enable-locally t))))

;;;-------------------------------------------------------------------------
;; I shouldn't have to do this...  I think 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 will just end up being 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.
(add-hook 'after-init-hook
	  #'(lambda ()
	      (add-hook 'find-file-hooks 'After-save--find-file-hook)))
;; also here... `add-hook' will ensure that only one copy is there.
(add-hook 'find-file-hooks 'After-save--find-file-hook)
(After-save--initialize-global-toggle-menu)
;;;-------------------------------------------------------------------------
(provide 'after-save-commands)

;;; after-save-commands.el ends here
