The thing I miss most from Vim isn’t so much Vim itself as Sed. I like being able to issue sed-like regex substitution commands from the ex
command line.
;;; sedition.el --- Vi-like Sed Commands without Evil
And some package metadata:
;; Author Olivia Lucca Fraser <lucca.fraser@gmail.com>
;; Version: 0.1
;; Keywords: convenience, matching, tools, unix
;; URL: https://github.com/oblivia-simplex/emacs-sedition
We’ll keep the commentary here brief, and just refer the reader to this README.org.
;;; Commentary:
;; This package lets you issue sed commands to act on the currently active
;; region or buffer, more or less like you can in Vi with ex commands like
;; :%s/foo/bar/g
;; :'<,'>s/foo/bar/g
;; and so on. The work behind the scenes is all being done by sed, so you'll
;; need to have the sed tool installed. (On Unixy systems this won't be an
;; issue, generally speaking.)
;;
;; See the README.org at https://github.com/oblivia-simplex/emacs-sedition/
;; for more details.
And that’s just about it for the package header. We just need to indicate where the code begins, which will be right… here:
;;; Code:
Let’s create a variable to store the command history for our sed commands.
(defvar sedition--command-history nil
"History list for `sed/pipe-region'.")
We could use shell-command-region
for this, but that would be a mistake. It’s not malicious shell injection we’re worried about here – if the user is running emacs, they can presumably be trusted with arbitrary shell commands – but inadvertant shell commands. A command to substitute "->"
with "==>"
, for example, could inadvertantly invoke output redirection in the shell, thanks to the “>” character.
So we’ll take a couple precautions here: we’ll use call-process-region
instead, passing the sed command argument explicitly, and we’ll check the exit code of the command before we do anything to the buffer or region selected.
;;;###autoload
(defun sedition ()
"Pipe the contents of the region to the shell command sed.
When called interactively, `REGION-BEGINNING' and `REGION-END'
refer to the beginning and end points of the active region.
If no region is active, they are set to the beginning and end of
the current buffer."
(interactive)
(let* ((in-region (use-region-p))
(start (if in-region (region-beginning) (point-min)))
(end (if in-region (region-end) (point-max)))
(prompt (if in-region
(format "sed (region %d-%d): " start end)
"sed buffer: "))
(command (read-string prompt nil 'sedition--command-history))
;; generate-new-buffer will create a unique name with this prefix
;; the initial space hides it from list-buffers and buffer-menu
;; commands
(tmpbuffer (generate-new-buffer " *sed*"))
(exitcode (call-process-region start
end
"sed"
nil ;; don't delete anything yet
tmpbuffer
t
command)))
;; We only want to perform any action on the region/buffer
;; if the command succeeded, so check the exit code here.
(cond ((zerop exitcode)
(delete-region start end)
(goto-char start)
(insert-buffer-substring tmpbuffer))
((= exitcode 127)
(message "sed: command not found"))
((= exitcode 1)
(message "sed: error in command '%s'" command))
((stringp exitcode)
(message "sed: %s" exitcode))
(t (message "sed: exited with code %d" exitcode)))
(kill-buffer tmpbuffer)))
Okay, our sed tool is ready! Go ahead and bind a key to it. I have it set to C-c s, myself, and to / in Meow mode.
(global-set-key (kbd "C-c s") 'sedition-dwim)
This is the whole module, by the way. Just org-babel-tangle
this file, and you’ll have an up-to-date sedition.el.
(provide 'sedition)
;;; sedition.el ends here