Script Workflow
Write standalone Emacs Lisp scripts for automation, batch processing, and command-line tools.
Batch Mode Basics
Simple Script
#!/usr/bin/emacs --script
;;; process-files.el --- Process files in batch mode
(message "Script started")
(dolist (file command-line-args-left)
(message "Processing: %s" file)
(with-temp-buffer
(insert-file-contents file)
(message "Size: %d bytes" (buffer-size))))
(message "Script completed")
Make executable:
chmod +x process-files.el
./process-files.el file1.txt file2.txt
Shebang Options
Direct Script
#!/usr/bin/emacs --script
;; Runs directly, no interactive mode
With Options
#!/bin/sh
":"; exec emacs --quick --script "$0" "$@" # -*-emacs-lisp-*-
;; --quick: Skip init files
;; --script: Batch mode
Load Init File
#!/bin/sh
":"; exec emacs --script "$0" "$@" # -*-emacs-lisp-*-
;; Loads ~/.emacs.d/init.el
Command-Line Arguments
Access Arguments
#!/usr/bin/emacs --script
;; Script name
(message "Script: %s" load-file-name)
;; All arguments
(message "Args: %S" command-line-args-left)
;; Process arguments
(let ((input-file (pop command-line-args-left))
(output-file (pop command-line-args-left)))
(unless (and input-file output-file)
(error "Usage: %s INPUT OUTPUT" load-file-name))
(message "Input: %s" input-file)
(message "Output: %s" output-file))
Parse Options
#!/usr/bin/emacs --script
(defun parse-args (args)
"Parse command-line ARGS."
(let ((verbose nil)
(output nil)
(files nil))
(while args
(let ((arg (pop args)))
(cond
((string= arg "--verbose")
(setq verbose t))
((string= arg "--output")
(setq output (pop args)))
((string-prefix-p "--" arg)
(error "Unknown option: %s" arg))
(t
(push arg files)))))
(list :verbose verbose
:output output
:files (nreverse files))))
(let ((options (parse-args command-line-args-left)))
(message "Options: %S" options))
Common Script Patterns
File Processing
#!/usr/bin/emacs --script
;;; convert-encoding.el --- Convert file encoding
(defun convert-file-encoding (file from to)
"Convert FILE from FROM encoding to TO encoding."
(let ((content (with-temp-buffer
(let ((coding-system-for-read from))
(insert-file-contents file)
(buffer-string)))))
(with-temp-buffer
(insert content)
(let ((coding-system-for-write to))
(write-region (point-min) (point-max) file)))))
(dolist (file command-line-args-left)
(message "Converting %s..." file)
(convert-file-encoding file 'utf-8 'iso-8859-1)
(message "Done"))
Directory Processing
#!/usr/bin/emacs --script
;;; list-el-files.el --- List all .el files
(require 'find-lisp)
(defun list-el-files (directory)
"List all .el files in DIRECTORY recursively."
(let ((files (find-lisp-find-files directory "\\.el$")))
(dolist (file files)
(princ file)
(terpri))))
(let ((dir (or (car command-line-args-left) ".")))
(list-el-files dir))
Text Transformation
#!/usr/bin/emacs --script
;;; markdown-to-org.el --- Convert Markdown to Org
(defun markdown-to-org (md-file org-file)
"Convert MD-FILE to ORG-FILE."
(with-temp-buffer
(insert-file-contents md-file)
;; Headers
(goto-char (point-min))
(while (re-search-forward "^\\(#+\\) \\(.*\\)$" nil t)
(let ((level (length (match-string 1)))
(title (match-string 2)))
(replace-match (format "%s %s" (make-string level ?*) title))))
;; Bold
(goto-char (point-min))
(while (re-search-forward "\\*\\*\\([^*]+\\)\\*\\*" nil t)
(replace-match "*\\1*"))
;; Write output
(write-region (point-min) (point-max) org-file)))
(let ((input (pop command-line-args-left))
(output (pop command-line-args-left)))
(markdown-to-org input output)
(message "Converted %s to %s" input output))
Exit Codes
Success/Failure
#!/usr/bin/emacs --script
(condition-case err
(progn
;; Script logic
(when (some-error-condition)
(error "Something went wrong"))
;; Success
(kill-emacs 0))
(error
(message "Error: %s" (error-message-string err))
(kill-emacs 1)))
Exit with Code
(kill-emacs 0) ; Success
(kill-emacs 1) ; General error
(kill-emacs 2) ; Misuse of command
Output
Standard Output
;; Print to stdout
(princ "Hello, World!")
(terpri) ; Newline
;; Or use message (goes to stderr in batch mode)
(message "Processing...")
;; Format output
(princ (format "Processed %d files\n" count))
Standard Error
;; In batch mode, message goes to stderr
(message "Warning: %s" warning-text)
;; Explicit stderr
(with-current-buffer (get-buffer-create "*stderr*")
(insert "Error message\n"))
Practical Examples
Batch Byte Compilation
#!/usr/bin/emacs --script
;;; batch-compile.el --- Byte compile all .el files
(require 'bytecomp)
(defun batch-byte-compile-directory (directory)
"Byte compile all .el files in DIRECTORY."
(let ((files (directory-files directory t "\\.el$")))
(dolist (file files)
(unless (string-match-p "-test\\.el$" file)
(message "Compiling %s..." file)
(byte-compile-file file)))))
(let ((dir (or (car command-line-args-left) ".")))
(batch-byte-compile-directory dir))
Generate Autoloads
#!/usr/bin/emacs --script
;;; generate-autoloads.el --- Generate autoloads file
(require 'autoload)
(let ((dir (or (car command-line-args-left) "."))
(output "my-package-autoloads.el"))
(with-current-buffer (find-file-noselect output)
(erase-buffer)
(insert ";;; Autoloads\n")
(save-buffer))
(update-directory-autoloads dir)
(message "Generated %s" output))
Run Tests
#!/usr/bin/emacs --script
;;; run-tests.el --- Run ERT tests
(require 'ert)
;; Load package
(add-to-list 'load-path ".")
(load "my-package.el")
(load "my-package-test.el")
;; Run tests
(ert-run-tests-batch-and-exit t)
Extract Documentation
#!/usr/bin/emacs --script
;;; extract-docs.el --- Extract function documentation
(defun extract-function-docs (file)
"Extract function documentation from FILE."
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward "^(defun \\([^ ]+\\)" nil t)
(let ((func-name (match-string 1)))
(forward-sexp)
(forward-line)
(when (looking-at "[ \t]*\"\\([^\"]+\\)\"")
(princ (format "* %s\n %s\n\n" func-name (match-string 1))))))))
(dolist (file command-line-args-left)
(extract-function-docs file))
Debugging Scripts
Enable Debug
#!/usr/bin/emacs --script
(setq debug-on-error t)
;; Your code
Print Variables
(message "Variable value: %S" my-var)
(prin1 my-var)
Backtrace
(condition-case err
(risky-operation)
(error
(message "Error: %s" (error-message-string err))
(message "Backtrace: %S" (backtrace-to-string))
(kill-emacs 1)))
Advanced Techniques
Load Dependencies
#!/usr/bin/emacs --script
;; Add package archives
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(package-initialize)
;; Ensure package is installed
(unless (package-installed-p 'dash)
(package-refresh-contents)
(package-install 'dash))
(require 'dash)
;; Use dash functions
(-map (lambda (x) (* x 2)) '(1 2 3))
Parallel Processing
#!/usr/bin/emacs --script
(defun process-file-async (file callback)
"Process FILE asynchronously, call CALLBACK when done."
(make-process
:name (format "process-%s" file)
:buffer nil
:command (list "emacs" "--batch"
"--eval" (format "(progn (load \"%s\") (process-file \"%s\"))"
load-file-name file))
:sentinel (lambda (proc event)
(when (string-match-p "finished" event)
(funcall callback file)))))
;; Process files in parallel
(let ((files command-line-args-left)
(count 0))
(dolist (file files)
(process-file-async file
(lambda (f)
(setq count (1+ count))
(when (= count (length files))
(kill-emacs 0)))))
;; Wait for completion
(while t (sleep-for 0.1)))
Best Practices
- Use
--script: Faster than--batch - Handle errors: Use
condition-case - Check arguments: Validate input
- Exit codes: Return appropriate codes
- Progress messages: Use
messagefor feedback - Load minimal: Only load needed libraries
- Test scripts: Like any other code
- Document: Add usage at top of file
- Make executable:
chmod +x - Clean output: Separate status from data