2

Using advice-add from nadvice.el, I am trying to add a :filter-args advice to an interactive function. Is it somehow possible for it to detect, whether the advised function has been called interactively?

I was trying to use called-interactively-p, but M-x describe-function called-interactively-p says:

This function is very brittle, it may fail to return the intended result when the code is debugged, advised, or instrumented in some form. [...]

Instead of using this function, it is cleaner and more reliable to give your function an extra optional argument whose ‘interactive’ spec specifies non-nil unconditionally ("p" is a good way to do this), or via (not (or executing-kbd-macro noninteractive)).

Sadly, neither works:

  • The brittleness of called-interactively-p makes it useless here; Only around advise will detect interactiveness correctly with called-interactively-p, but then the original function doesn't see the interactive call that way anymore.
  • The suggested workaround is actually wrong. noninteractive determines whether the emacs session is interactive, not whether the command was called interactively.

Minimal working example for called-interactively-p

(defun tmpdemo-command (&rest args)
  (interactive '(was called interactively))
  (message "tmpdemo-command:     %-3S %-3S" 
    (called-interactively-p)
    (not (or executing-kbd-macro noninteractive)))
  (message "%S" args))

(advice-add #'tmpdemo-command :around #'tmpdemo-around)
(advice-add #'tmpdemo-command :filter-args #'tmpdemo-filter-args)

(defun tmpdemo-around (oldfun &rest args)
  (message "tmpdemo-around:      %-3S %-3S"
    (called-interactively-p)
    (not (or executing-kbd-macro noninteractive)))
  (apply oldfun args))

(defun tmpdemo-filter-args (args)
  (message "tmpdemo-filter-args: %-3S %-3S"
    (called-interactively-p)
    (not (or executing-kbd-macro noninteractive)))
  args)

When executing it interactively as M-x tmpdemo-command:

tmpdemo-filter-args: nil t      
tmpdemo-around:      t   t  <-- Only the around advise sees the
tmpdemo-command:     nil t      expected result.
(was called interactively)

When running it non-interactively as (tmpdemo-command 'noninteractive)

tmpdemo-filter-args: nil t  <-- the suggested workaround gives
tmpdemo-around:      nil t      the wrong result!!
tmpdemo-command:     nil t  
(noninteractive)
Drew
  • 75,699
  • 9
  • 109
  • 225
kdb
  • 1,561
  • 12
  • 21
  • 2
    You've misunderstood the `noninteractive` suggestion, the idea is to do `(defun tmpdemo-command (interactive-call &rest args) (interactive (list (not (or executing-kbd-macro noninteractive)) 'etc)) ...)` and then check the `interactive-call` argument *instead* of using `called-interactive-p`. That suggestion is not applicable if you're trying advise a function that's already using `called-interactive-p` (i.e., not following the suggestion) though. – npostavs Dec 06 '19 at 13:05
  • @npostavs - I am trying to advise `shell-command`, so mostly the problem is, that I can't just add an argument without breaking things. – kdb Dec 06 '19 at 13:18
  • "I can't just add an argument" - Right. But as far as I can tell `shell-command` doesn't use `called-interactively-p`, so it seems like you may have asked the wrong question. – npostavs Dec 06 '19 at 14:04
  • @npostavs The main problem is that the advise has no reliable way to detect an interactive call, when the interface of the original function cannot be changed. I mostly just meant to show, that even *trying* to may cause further breakage, e.g. when a function has two `:around` advises -- only one of them will be able to detect interactive calls, and they may not know if each other (e.g. user configuration and a package-provided advise). – kdb Dec 06 '19 at 16:38

1 Answers1

1

As npostavs explains in a comment, the noninteractive suggestion is to do:

(defun foo (&optional interactivep)
  (interactive "p")
  ...
  ...use `interactivep' when you want to know if the call was interactive.
  ...)

which corresponds to (called-interactive-p 'any) or

(defun foo (&optional interactivep)
  (interactive (list (not (or executing-kbd-macro noninteractive))))
  ...
  ...use `interactivep' when you want to know if the call was interactive.
  ...)

which corresponds to (called-interactive-p 'interactive), or even:

(defun foo (&optional interactivep)
  (interactive "p")
  ...
  ... use `(and interactivep
  ...           (not (or executing-kbd-macro noninteractive)))'
  ... when you want to know if the call was interactive.
  ...)

Note that you can also make the :around case work "correctly" (with the usual caveat that called-interactively-p remains brittle) as follows:

(defun tmpdemo-around (oldfun &rest args)
  (message "tmpdemo-around:      %-3S %-3S"
           (called-interactively-p)
           (not (or executing-kbd-macro noninteractive)))
  (apply (if (called-interactively-p 'any)
             #'funcall-interactively #'funcall)
         oldfun args))
Drew
  • 75,699
  • 9
  • 109
  • 225
Stefan
  • 26,154
  • 3
  • 46
  • 84
  • I.e., it is *possible* to make `called-interactively-p` work correctly in an around advise in such a manner that for the function nothing changes. But that still leaves the advise vulnerable to an incorrect result from `called-interactively-p` caused by an additional `:around` advise, correct? – kdb Dec 06 '19 at 16:40
  • @kdb: That's right. The inherent brittleness can still bite you ;-) – Stefan Dec 06 '19 at 17:16
  • There is also the issue that around advice is pretty awful for debugging - try using `trace-function` with it :/ – kdb Dec 07 '19 at 09:13