The following works only with lexical binding, because with dynamic binding (defvar foo)
has no real effect. It checks whether let-binding the variable affects the dynamic value or not. I used the term "declared" instead of "defined", because (defvar foo)
doesn't quite feel like a full definition to me (e.g., it only applies to the file it's in) so it's more like C's "declare".
;; -*- lexical-binding: t -*-
(defvar foo 2)
(defvar bar)
(defmacro check-var (var)
`(message "%s %s"
',var
(cond ((boundp ',var) "has been initialized")
((let ((,var t)) (boundp ',var))
"has been declared, but not initialized")
(t "has NOT been declared nor initialized"))))
(check-var foo)
(check-var bar)
(check-var qux)
defvar
does do a bit more that just make the variable exist, but I'm not sure any of that would be good enough. If you don't care about that distinction, you could useboundp
which just tests if there's a value.