320 lines
13 KiB
Bash
Executable file
320 lines
13 KiB
Bash
Executable file
#!/usr/bin/env sh
|
|
:; # -*- mode: emacs-lisp; lexical-binding: t -*-
|
|
:; case "$EMACS" in *term*) EMACS=emacs ;; *) EMACS="${EMACS:-emacs}" ;; esac
|
|
:; [ "x$EMACS" = xemacs ] && { type emacs >/dev/null 2>&1 || err=1; }
|
|
:; [ "x$err" = x ] || { echo "Error: failed to run Emacs with command '$EMACS'"; echo; echo "Are you sure Emacs is installed and in your \$PATH?"; exit 1; } >&2
|
|
:; emacs="$EMACS ${DEBUG:+--debug-init} -q --no-site-file --batch"
|
|
:; export __DOOMPID="${__DOOMPID:-$$}"
|
|
:; export __DOOMSTEP="${__DOOMSTEP:-0}"
|
|
:; export __DOOMGEOM="${__DOOMGEOM:-`tput cols 2>/dev/null`x`tput lines 2>/dev/null`}"
|
|
:; export __DOOMGPIPE=${__DOOMGPIPE:-$__DOOMPIPE}
|
|
:; export __DOOMPIPE=; [ -t 0 ] || __DOOMPIPE="${__DOOMPIPE}0"; [ -t 1 ] || __DOOMPIPE="${__DOOMPIPE}1"
|
|
:; $emacs --load "$0" -- "$@" || exit=$?
|
|
:; [ "${exit:-0}" -eq 254 ] && { export TMPDIR="${TMPDIR:-${TMP:-${TEMP:-`$emacs -Q --eval '(princ temporary-file-directory)' 2>/dev/null`}}}"; sh "${TMPDIR}/doom.${__DOOMPID}.${__DOOMSTEP}.sh" "$0" "$@" && true; exit="$?"; }
|
|
:; exit $exit
|
|
|
|
;; This magical mess of a shebang is necessary for any script that relies on
|
|
;; Doom's CLI framework, because Emacs' tty libraries and capabilities are too
|
|
;; immature (borderline non-existent) at the time of writing (28.1). This
|
|
;; shebang sets out to accomplish these three goals:
|
|
;;
|
|
;; 1. To produce a more helpful error if Emacs isn't installed or broken. It
|
|
;; must do so without assuming whether $EMACS is a shell command (e.g. 'snap
|
|
;; run emacs') or an absolute path (to an emacs executable). I've avoided
|
|
;; 'command -v $EMACS' for this reason.
|
|
;;
|
|
;; 2. To allow this Emacs session to "exit into" a child process (since Elisp
|
|
;; lacks an analogue for exec system calls) by calling an auto-generated and
|
|
;; self-destructing "exit script" if the parent Emacs process exits with code
|
|
;; 254. It takes care to prevent nested child instances from clobbering the
|
|
;; exit script.
|
|
;;
|
|
;; 3. To expose some information about the terminal and session:
|
|
;; - $__DOOMGEOM holds the dimensions of the terminal (W . H).
|
|
;; - $__DOOMPIPE indicates whether the script has been piped (in and/or out).
|
|
;; - $__DOOMGPIPE indicates whether one of this process' parent has been
|
|
;; piped to/from.
|
|
;; - $__DOOMPID is a unique identifier for the parent script, so
|
|
;; child processes can identify which persistent data files (like logs) it
|
|
;; has access to.
|
|
;; - $__DOOMSTEP counts how many levels deep we are in the dream (appending
|
|
;; this to the exit script's filename avoids child processes clobbering the
|
|
;; same exit script and causing read errors).
|
|
;; - $TMPDIR (or $TEMP and $TMP on Windows) aren't guaranteed to have values,
|
|
;; and mktemp isn't available on all systems, but you know what is? Emacs!
|
|
;; So I use it to print `temporary-file-directory'. And it seconds as a
|
|
;; quick sanity check for Emacs' existence (for goal #1).
|
|
;;
|
|
;; Other weird facts about this shebang line:
|
|
;;
|
|
;; - The :; hack exploits properties of : and ; in shell scripting and elisp to
|
|
;; allow shell script and elisp to coexist in the same file without either's
|
|
;; interpreter throwing foreign syntax errors:
|
|
;;
|
|
;; - In elisp, ":" is a valid keyword symbol literal; it evaluates to itself
|
|
;; and has no side-effect.
|
|
;; - In the shell, ":" is a valid command that does nothing and ignores its
|
|
;; arguments.
|
|
;; - In elisp, ";" begins a comment. I.e. the interpreter ignores everything
|
|
;; after it.
|
|
;; - In the shell, ";" is a command separator.
|
|
;;
|
|
;; Put together, plus a strategically placed exit call, the shell will read
|
|
;; one part of this file and ignore the rest, while the elisp interpreter will
|
|
;; do the opposite.
|
|
;; - I intentionally suppress loading site files (must be done with '-q
|
|
;; --no-site-file' NOT '-Q', as the latter omits the site-lisp dir from
|
|
;; `load-path' too), so lisp/doom-cli.el can load them manually later (early
|
|
;; in lisp/doom-cli.el). Look there for an explanation why I do this.
|
|
;; - POSIX-compliancy is paramount: there's no guarantee what /bin/sh will be
|
|
;; symlinked to in the esoteric OSes/distros Emacs users use.
|
|
;; - The user may have mounted /tmp with a noexec flag, so pass the exit script
|
|
;; to /bin/sh rather than executing them directly.
|
|
|
|
;; In CLI sessions, prefer correctness over performance.
|
|
(setq load-prefer-newer t)
|
|
|
|
;; Doom's core sets up everything we need; including `doom-*-dir' variables,
|
|
;; universal defaults, and autoloads for doom-*-initialize functions.
|
|
(condition-case e
|
|
(let* ((bin-dir (file-name-directory (file-truename load-file-name)))
|
|
(init-file (expand-file-name "../early-init.el" bin-dir)))
|
|
(or (and (load init-file nil 'nomessage 'nosuffix)
|
|
(featurep 'doom))
|
|
(user-error "Failed to load Doom from %s" init-file)))
|
|
;; Prevent ugly backtraces for trivial errors
|
|
(user-error (message "Error: %s" (cadr e))
|
|
(kill-emacs 2)))
|
|
|
|
;; UX: Abort if the user is using 'doom' as root, unless $EMACSDIR is owned by
|
|
;; root, in which case we can safely assume the user genuinely wants root to
|
|
;; be their primary user account for this session.
|
|
(when (equal 0 (user-real-uid))
|
|
(unless (equal 0 (file-attribute-user-id (file-attributes doom-emacs-dir)))
|
|
(message
|
|
(concat
|
|
"Error: this script was executed as root, which is likely not what you want.\n"
|
|
"It will cause file permissions errors later, when you run Doom as another\n"
|
|
"user.\n\n"
|
|
"If this really *is* what you want, then change the owner of your Emacs\n"
|
|
"config to root:\n\n"
|
|
;; TODO Add cmd.exe/powershell commands
|
|
" chown root:root -R " (abbreviate-file-name doom-emacs-dir) "\n\n"
|
|
"Aborting..."))
|
|
(kill-emacs 2)))
|
|
|
|
|
|
;;
|
|
;;; Entry point
|
|
|
|
(defcli! doom (&args _command)
|
|
"A command line interface to Doom Emacs.
|
|
|
|
Includes package management, diagnostics, unit tests, and byte-compilation.
|
|
|
|
This tool also makes it trivial to launch Emacs out of a different folder or
|
|
with a different private module.
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
`$EMACS'
|
|
The Emacs executable or command to use for any Emacs operations in this or
|
|
other Doom CLI shell scripts (default: first emacs found in `$PATH').
|
|
|
|
`$EMACSDIR'
|
|
The location of your Doom Emacs installation (defaults to ~/.config/emacs or
|
|
~/.emacs.d; whichever is found first). This is *not* your private Doom
|
|
configuration. The `--emacsdir' option also sets this variable.
|
|
|
|
`$DOOMDIR'
|
|
The location of your private configuration for Doom Emacs (defaults to
|
|
~/.config/doom or ~/.doom.d; whichever it finds first). This is *not* the
|
|
place you've cloned doomemacs/doomemacs to. The `--doomdir' option also sets
|
|
this variable.
|
|
|
|
`$DOOMPAGER'
|
|
The pager to invoke for large output (default: \"less +g\"). The `--pager'
|
|
option also sets this variable.
|
|
|
|
`$DOOMPROFILE'
|
|
Which Doom profile to activate (default: \"_@0\"). The `--profile' option
|
|
also sets this variable.
|
|
|
|
`$DOOMPROFILELOADFILE'
|
|
Doom generates a profile loader script on 'doom sync' or 'doom profiles
|
|
sync'. By default, this file is written to and loaded from
|
|
$EMACSDIR/profiles/load.el. Set this envvar to change that. Note that this
|
|
envvar must be in scope for both interactive and non-interactive sessions
|
|
for it to be effective. This is especially useful for folks on Nix/Guix, who
|
|
have deployed Doom to a read-only directory.
|
|
|
|
Note: this file *must* end with a .el extension. It will be byte-compiled
|
|
after it is generated.
|
|
|
|
`$DOOMPROFILELOADPATH'
|
|
A colon-delimited (semi-colon on Windows) list of profile config files or
|
|
directories under which Doom will search for implicit profiles. See
|
|
`var:doom-profile-load-path' for its default value.
|
|
|
|
EXIT CODES:
|
|
0 Successful run
|
|
1 General internal error
|
|
2 Error with Emacs/Doom install or execution context
|
|
3 Unrecognized user input error
|
|
4 Command not found, or is incorrect/deprecated
|
|
5 Invalid, missing, or extra options/arguments
|
|
6-15 Reserved for Doom
|
|
16-192 Reserved for the user's extensions
|
|
254 Successful run (then execute the dynamically generated after-script)
|
|
255 Uncaught critical errors
|
|
|
|
SEE ALSO:
|
|
https://doomemacs.org Homepage
|
|
https://docs.doomemacs.org Official documentation
|
|
https://discourse.doomemacs.org Discourse (discussion & support forum)
|
|
https://doomemacs.org/discord Discord chat server
|
|
https://doomemacs.org/roadmap Development roadmap
|
|
https://git.doomemacs.org Shortcut to Github org
|
|
https://git.doomemacs.org/todo Global issue tracker"
|
|
:partial t)
|
|
|
|
(defcli! :before
|
|
((force? ("-!" "--force") "Suppress prompts by auto-accepting their consequences")
|
|
(debug? ("-D" "--debug") "Enable debug output")
|
|
(verbose? ("-v" "--verbose") "Enable verbose output")
|
|
(doomdir ("--doomdir" dir) "Use Doom config living in `DIR' (e.g. ~/.doom.d)")
|
|
(emacsdir ("--emacsdir" dir) "Use Doom install living in `DIR' (e.g. ~/.emacs.d)")
|
|
(pager ("--pager" cmd) "Pager command to use for large output")
|
|
(profile ("--profile" name) "Use profile named NAME")
|
|
(bench? ("--benchmark") "Always display the benchmark")
|
|
&flags
|
|
(color? ("--color") "Whether or not to show ANSI color codes")
|
|
&multiple
|
|
(loads ("-L" "--load" "--strict-load" file) "Load elisp `FILE' before executing `COMMAND'")
|
|
(evals ("-E" "--eval" form) "Evaluate `FORM' before executing commands")
|
|
&input input
|
|
&context context
|
|
&args _)
|
|
"OPTIONS:
|
|
-E, -eval
|
|
Can be used multiple times.
|
|
|
|
-L, --load, --strict-load
|
|
Can be used multiple times to load multiple files. Both -L and --load will
|
|
silently fail on missing files, but --strict-load won't.
|
|
|
|
Warning: files loaded this way load too late to define new commands. To
|
|
define commands, do so from `$DOOMDIR'/cli.el, `$DOOMDIR'/init.el, or a
|
|
.doomrc file in the current project tree."
|
|
(when bench?
|
|
(setq doom-cli-benchmark-threshold 'always))
|
|
(unless init-file-debug ; debug-mode implies verbose
|
|
(when verbose?
|
|
(setq doom-print-minimum-level 'info)))
|
|
(when color?
|
|
(setq doom-print-backend (if (eq color? :yes) 'ansi)))
|
|
(when pager
|
|
(setq doom-cli-pager pager))
|
|
(when force?
|
|
(setf (doom-cli-context-suppress-prompts-p context) t))
|
|
;; For these settings to take full effect, the script must be restarted:
|
|
(when (or debug?
|
|
profile
|
|
emacsdir
|
|
doomdir)
|
|
(let (omit)
|
|
(when debug?
|
|
(setenv "DEBUG" "1")
|
|
(setq init-file-debug t)
|
|
(push "--debug" omit))
|
|
(when profile
|
|
(setenv "DOOMPROFILE" profile)
|
|
(push "--profile=" omit))
|
|
(when emacsdir
|
|
(setenv "EMACSDIR" emacsdir)
|
|
(push "--emacsdir=" omit))
|
|
(when doomdir
|
|
(setenv "DOOMDIR" doomdir)
|
|
(push "--doomdir=" omit))
|
|
(exit! :restart :omit omit)))
|
|
;; Load extra files and forms, as per given options.
|
|
(dolist (file loads)
|
|
(load (doom-path (cdr file))
|
|
(not (equal (car file) "--strict-load"))
|
|
(not init-file-debug) t))
|
|
(dolist (form evals)
|
|
(eval (read (cdr form)) t)))
|
|
|
|
|
|
;;
|
|
;;; Load user config + commands
|
|
|
|
;; Load $DOOMDIR/init.el, to read the user's `doom!' block and give users an
|
|
;; opportunity to customize the CLI environment, if they like. Otherwise, they
|
|
;; can do so in .doomrc or .doomproject.
|
|
(load! doom-module-init-file doom-user-dir t)
|
|
|
|
;; There are a lot of CLIs, and some have expensive initialization, so best we
|
|
;; load them lazily.
|
|
(defcli-group!
|
|
:prefix 'doom
|
|
;; Import this for implicit 'X help' commands for your script:
|
|
(defcli-alias! ((help h)) (:root :help))
|
|
;; And suggest its use when errors occur.
|
|
(add-to-list 'doom-help-commands "%p h[elp] %c")
|
|
|
|
(defcli-group! "Config Management"
|
|
:docs "Commands for maintaining your Doom Emacs configuration."
|
|
(defcli-autoload! ((sync s)))
|
|
(defcli-autoload! ((profiles profile)))
|
|
(defcli-autoload! ((upgrade up)))
|
|
(defcli-autoload! (env))
|
|
(defcli-autoload! ((build b purge p gc rollback)) "packages")
|
|
(defcli-autoload! ((install i)))
|
|
|
|
;; TODO Post-3.0 commands
|
|
;; (load! "gc" dir)
|
|
;; (load! "module" dir)
|
|
;; (load! "nuke" dir)
|
|
;; (load! "package" dir)
|
|
;; (load! "profile" dir)
|
|
)
|
|
|
|
(defcli-group! "Diagnostics"
|
|
:docs "Commands for troubleshooting and debugging Doom."
|
|
(defcli-autoload! ((doctor doc)))
|
|
(defcli-autoload! (info))
|
|
(defcli-alias! ((version v)) (:root :version)))
|
|
|
|
(defcli-group! "Development"
|
|
:docs "Commands for developing or launching Doom."
|
|
(defcli-autoload! (ci))
|
|
(defcli-autoload! (make))
|
|
(defcli-autoload! (run))
|
|
|
|
;; FIXME Test framework
|
|
;; (load! "test" dir)
|
|
)
|
|
|
|
(let ((cli-file "cli.el"))
|
|
(defcli-group! "Module commands"
|
|
(doom-context-with 'modules
|
|
(dolist (key (doom-module-list))
|
|
(when-let (path (doom-module-locate-path (car key) (cdr key) cli-file))
|
|
(defcli-group! :prefix (if (cdr key) (format "+%s" (cdr key)))
|
|
(doom-load (file-name-sans-extension path))))))))
|
|
|
|
;; Allow per-project Doom settings in .doom files.
|
|
(let (doomrc)
|
|
(cond
|
|
((setq doomrc (getenv "DOOMRC"))
|
|
(load! doomrc default-directory))
|
|
((setq doomrc (locate-dominating-file default-directory ".doomrc"))
|
|
(load! ".doomrc" doomrc)))))
|
|
|
|
|
|
;;
|
|
;;; Let 'er rip
|
|
|
|
(run! "doom" (cdr (member "--" argv)))
|
|
|
|
;;; doom ends here, unless...
|