Files
doomemacs/lisp/doom-cli.el
Henrik Lissner 5dcba2f89f fix: permissions for local dirs
This prevents edge cases where these directories are created with
permissions that prevent Emacs from writing to them. This can happy
either due to an overly-restrictive default umask,
`set-default-file-modes` call, or if `doom-profiles-save` is instructed
to write a file whose parent doesn't exist yet.

Fix: #8134
2024-11-01 03:34:09 -04:00

214 lines
9.2 KiB
EmacsLisp

;;; lisp/doom-cli.el --- API+DSL for Doom's CLI framework -*- lexical-binding: t; -*-
;;; Commentary:
;;
;; The heart of Doom's CLI framework. This is safe to load in interactive
;; sessions (for API access and syntax highlighting), but much of the API
;; expects a noninteractive session, so take care when testing code!
;;
;;; Code:
(unless noninteractive
(error "Don't load doom-cli in an interactive session!"))
;; PERF: Deferring the GC in non-interactive sessions isn't as important, but
;; still yields a notable benefit. Still, avoid setting it to high here, as
;; runaway memory usage is a real risk in longer sessions.
(setq gc-cons-threshold 134217728 ; 128mb
;; Backported from 29 (see emacs-mirror/emacs@73a384a98698)
gc-cons-percentage 1.0)
;; REVIEW: Remove these later. The endpoints should be responsibile for
;; ensuring they exist. For now, they exist to quell file errors.
(with-file-modes #o700
(mapc (doom-rpartial #'make-directory 'parents)
(list doom-local-dir
doom-data-dir
doom-cache-dir
doom-state-dir)))
;; HACK: bin/doom suppresses loading of site files so they can be loaded
;; manually, here. Why? To suppress the otherwise unavoidable output they
;; commonly produce (like deprecation notices, file-loaded messages, and
;; linter warnings). This output pollutes the output of doom's CLI (or
;; scripts derived from it) with potentially confusing or alarming -- but
;; always unimportant -- information to the user.
(quiet!
(require 'cl nil t) ; "Package cl is deprecated"
(unless site-run-file ; unset in doom.el
(when-let ((site-run-file (get 'site-run-file 'initial-value)))
(load site-run-file t inhibit-message))))
(setq-default
;; PERF: Don't generate superfluous files when writing temp buffers.
make-backup-files nil
;; COMPAT: Stop user configuration from interfering with package management.
enable-dir-local-variables nil
;; PERF: Reduce ambiguity, embrace specificity, enjoy predictability.
case-fold-search nil
;; UX: Don't clog the user's trash with our CLI refuse.
delete-by-moving-to-trash nil)
;; Load just the... bear necessities~
(require 'seq)
(require 'map)
;; Suppress any possible coding system prompts during CLI sessions.
(set-language-environment "UTF-8")
;; Load and set up our debugger first, so backtraces can be made more
;; presentable and logged to file.
(doom-require 'doom-lib 'debug)
(if init-file-debug (doom-debug-mode +1))
;; Then load the rest of Doom's libs eagerly, since autoloads may not be
;; generated/loaded yet.
(doom-require 'doom-lib 'process)
(doom-require 'doom-lib 'system)
(doom-require 'doom-lib 'git)
(doom-require 'doom-lib 'plist)
(doom-require 'doom-lib 'files)
(doom-require 'doom-lib 'print)
(doom-require 'doom-lib 'autoloads)
;; Ensure straight and core packages are ready to go for CLI commands.
(require 'doom-cli-lib)
;; Last minute initialization at the end of loading this file.
(with-eval-after-load 'doom-cli
(doom-run-hooks 'doom-before-init-hook))
;;
;;; Predefined CLIs (:help, :version, and :dump)
(defvar doom-help-commands '("%p %c {-?,--help}")
"A list of help commands recognized for the running script.
Recognizes %p (for the prefix) and %c (for the active command).")
;; When __DOOMDUMP is set, doomscripts trigger this special handler.
(defcli! (:root :dump)
((pretty? ("--pretty") "Pretty print output")
&context context
&args commands)
"Dump metadata to stdout for other commands to read."
(let* ((prefix (doom-cli-context-prefix context))
(command (cons prefix commands)))
(funcall (if pretty? #'pp #'prin1)
(cond ((equal commands '("-")) (hash-table-values doom-cli--table))
(commands (doom-cli-find command))
((doom-cli-find (list prefix)))))
(terpri)
;; Kill manually so we don't save output to logs.
(let (kill-emacs) (kill-emacs 0))))
(defcli! (:root :help)
((localonly? ("-g" "--no-global") "Hide global options")
(manpage? ("--manpage") "Generate in manpage format")
(commands? ("--commands") "List all known commands")
&multiple
(sections ("--synopsis" "--subcommands" "--similar" "--envvars"
"--postamble")
"Show only the specified sections.")
&context context
&args command)
"Show documentation for a Doom CLI command.
OPTIONS:
--synopsis, --subcommands, --similar, --envvars, --postamble
TODO"
(doom-cli-load-all)
(when (doom-cli-context-error context)
(terpri))
(let* ((command (cons (doom-cli-context-prefix context) command))
(cli (doom-cli-get command t))
(rcli (doom-cli-get cli))
(fallbackcli (cl-loop with targets = (doom-cli--command-expand (butlast command) t)
for cmd in (cons command targets)
if (doom-cli-get cmd t)
return it)))
(cond (commands?
(let ((cli (or cli (doom-cli-get (doom-cli-context-prefix context)))))
(print! "Commands under '%s':\n%s"
(doom-cli-command-string cli)
(indent (doom-cli-help--render-commands
(or (doom-cli-subcommands cli)
(user-error "No commands found"))
:prefix (doom-cli-command cli)
:inline? t
:docs? t)))))
((null sections)
(if (null cli)
(signal 'doom-cli-command-not-found-error command)
(doom-cli-help--print cli context manpage? localonly?)
(exit! :pager?)))
((dolist (section sections)
(unless (equal section (car sections)) (terpri))
(pcase section
("--synopsis"
(print! "%s" (doom-cli-help--render-synopsis
(doom-cli-help--synopsis cli)
"Usage: ")))
("--subcommands"
(print! "%s\n%s" (bold "Available commands:")
(indent (doom-cli-help--render-commands
(doom-cli-subcommands rcli 1)
:prefix command
:grouped? t
:docs? t)
doom-print-indent-increment)))
("--similar"
(unless command
(user-error "No command specified"))
(let ((similar (doom-cli-help-similar-commands command 0.4)))
(print! "Similar commands:")
(if (not similar)
(print! (indent (warn "Can't find any!")))
(dolist (command (seq-take similar 10))
(print! (indent (item "(%d%%) %s"))
(* (car command) 100)
(doom-cli-command-string (cdr command)))))))
("--envvars"
(let* ((key "ENVIRONMENT VARIABLES")
(clis (if command (doom-cli-find command) (hash-table-values doom-cli--table)))
(clis (seq-remove #'doom-cli-alias clis))
(clis (seq-filter (fn! (cdr (assoc key (doom-cli-docs %)))) clis))
(clis (seq-group-by #'doom-cli-command clis)))
(print! "List of environment variables for %s:\n" command)
(if (null clis)
(print! (indent "None!"))
(dolist (group clis)
(print! (bold "%s%s:"
(doom-cli-command-string (car group))
(if (doom-cli-fn (doom-cli-get (car group)))
"" " *")))
(dolist (cli (cdr group))
(print! (indent "%s") (markup (cdr (assoc key (doom-cli-docs cli))))))))))
("--postamble"
(print! "See %s for documentation."
(join (cl-loop with spec =
`((?p . ,(doom-cli-context-prefix context))
(?c . ,(doom-cli-command-string (cdr (doom-cli-command (or cli fallbackcli))))))
for cmd in doom-help-commands
for formatted = (trim (format-spec cmd spec))
collect (replace-regexp-in-string
" +" " " (format "'%s'" formatted)))
" or ")))))))))
(defcli! (:root :version)
((simple? ("--simple"))
&context context)
"Show installed versions of Doom, Doom modules, and Emacs."
(doom/version)
(unless simple?
(terpri)
(with-temp-buffer
(insert-file-contents (doom-path doom-emacs-dir "LICENSE"))
(re-search-forward "^Copyright (c) ")
(print! "%s\n" (trim (thing-at-point 'line t)))
(print! (p "Doom Emacs uses the MIT license and is provided without warranty "
"of any kind. You may redistribute and modify copies if "
"given proper attribution. See the LICENSE file for details.")))))
(provide 'doom-cli)
;;; doom-cli.el ends here