elisp 新手练习


#1

大家好,刚刚开始学习 elisp,在一点点看 eintro ( C-h i d m emacs Lisp Intro)

11.4 Looping Exercise

• Write a function for Texinfo mode that creates an index entry at the beginning of a paragraph for every ‘@dfn’ within the paragraph. (In a Texinfo file, ‘@dfn’ marks a definition. This book is written in Texinfo.)

这是我写的一个解,用来提取一个自然段内 dfn 的内容,然后在段首加上索引。

	(defun my-create-cindex ()
	   "A writing helper for writing tex-info: 
	 1. extract every `dfn' in current paragraph
	 2. put content of `dfn' at the beginning of
	    the current paragraph."
	   (interactive)
	   (let ((beg (progn (backward-paragraph) (point)))
		 (end (progn (forward-paragraph) (point))))
	     (message "beg %s, end %s" beg end)
	     (save-excursion
	       (save-restriction
		 (narrow-to-region beg end)
		 (goto-char beg)
		 (setq oldCindex ())  
		 (while  (< 0 (how-many "dfn{" (point) (point-max)))
		   (copy-region-as-kill (search-forward "dfn{" (point-max))
					(- (search-forward "}") 1))
		   (setq newCindex (concat "@cindex " (substring-no-properties (nth 0 kill-ring))))
		   (setq oldCindex (concat oldCindex newCindex))))))
	   (backward-paragraph)
	   (insert-before-markers oldCindex))

希望大家批评指正。

谢谢。


#2

在narrow之前不先用widen解除用户手动的narrow?


#3
  1. 代码缩进有点乱
  2. 现场没保护好,{backward,forward}-paragraph 已经改变了光标位置
  3. 局部变量应在 (let ...) 中声明,然后才可以 (setq ...)
  4. 变量名不要用驼峰形式
  5. while + search-forwad 就可以了,不用每次都 how-many
  6. 滥用 kill ring

#4

5.1 search-forwad-regexp match-string


#5

非常感谢大家的帮助,对之前的这个函数最做了一些改进,这是新的内容:

(defun my-create-cindex ()
  "A writing helper for writing tex-info: 
 1. extract every `dfn' in current paragraph
 2. put content of `dfn' at the beginning of
    the current paragraph."
  (interactive)
  (my-delete-old-cindex)
  (let ((beg (progn (save-excursion
		      (save-restriction
			(backward-paragraph) (point)))))
	(end (progn (save-excursion
		      (save-restriction
			(forward-paragraph) (point)))))
	oldCindex
	newCindex)
    (save-excursion
      (save-restriction
	(widen)
	(narrow-to-region beg end)
	(goto-char beg)
	(while  (< 0 (how-many "dfn{" (point) (point-max)))
	  (copy-region-as-kill (search-forward "dfn{" (point-max))
			       (- (search-forward "}") 1))
	  (setq newCindex (concat "@cindex " (substring-no-properties (nth 0 kill-ring)) "\n"))
	  (setq oldCindex (concat oldCindex newCindex)))
	(backward-paragraph)
	(newline)
	(insert-before-markers oldCindex)))))


(defun my-delete-old-cindex ()
  "delete old cindex before the current paragraph"
  (interactive)
  (save-excursion
    (save-restriction
      (let ((beg (progn (save-excursion
			  (save-restriction
			    (widen)
			    (backward-paragraph 2)
			    (forward-paragraph)
			    (point)))))
	    (end (progn (save-excursion
			  (save-restriction
			    (widen)
			    (backward-paragraph) (point))))))
	(flush-lines "@cindex" beg end)))))

@cireu

在narrow之前不先用widen解除用户手动的narrow?

这是之前没想到的。没有考虑到如果用户在 “a narrowed region” 的情况下 使用这个函数。

我在 “a narrowed region” 下测试,不能得到结果。在提醒下,我在进入 “a narrowed region” 之前手动解除了这个状态。也就是添加了 (widen)。

这样修改后,是否达到这个要求?

@twlz0ne

代码缩进有点乱

我是在 scratch 中编辑 elisp 代码。主模式是:Lisp Interaction mode。

编辑好一段函数之后,将整个函数标记,然后: TAB (indent-for-tab-command) 来实现缩进。

相关的变量值。 #+BEGIN_SRC elisp tab-always-indent is a variable defined in ‘indent.el’. Its value is t indent-line-function is a variable defined in ‘indent.el’. Its value is ‘indent-relative’ #+END_SRC

另外可能是我从 emacs 中复制到浏览器的时候出现了问题。在下面更新的函数 中,我注意了这个问题。

现场没保护好,{backward,forward}-paragraph 已经改变了光标位置

在定义局部变量的时候用了 (save-excursion (save-restriction …)。有没 有更好的方法?另外,把插值 'oldCindex 放在了 (save-excursion (save-restriction …) 内部。目前,使用这个函数后,光标位置还在原处。

局部变量应在 (let …) 中声明,然后才可以 (setq …)

这个之前也没想到过。以为 (setq …) 在 (let VARLIST BODY…) 的 BODY 中,就是在定义局部变量。这是我的过度解读。在 let 的 VARLIST 中添加了局 部变量的声明。

变量名不要用驼峰形式

我是参考这个资料 (c++: https://www.learncpp.com/cpp-tutorial/keywords-and-naming-identifiers/) 来命名变量的。有没有 elisp 变量命名的相关参考资料?

while + search-forwad 就可以了,不用每次都 how-many

这一点可以展开讲一下吗?因为我用来存储 CINDEX (@cindex XXX @cindex YYY…) 内容的方法是 (copy-region-as-kill)。如果用 (search-forward) 做 为循环终止条件,第一个 CINDEX 里面的第一个元素不好处理。

滥用 kill ring

有没有更好方法?类似于把搜索到的内容储存成“动态数组”,或者别的什么方法?

@zhouchongzxc

5.1 search-forwad-regexp match-string

学习了一下,发现搜索也可以这样干:`search-forward-regexp @dfn{.+?}’

但是这里我用到了 (copy-region-as-kill), 把 “@dfn{” 的 point 以及下一 个 “}” 的 point 做为传入参量给了 (copy-region-as-kill)。不知道这样的正 则: `search-forward-regexp @dfn{.+?}’ 能将搜索到的内容传递给 kill-ring 么?或者用别的什么方式来存储 @dfn{} 中的内容?

PS 添加了新的东西:

如果段落之前已经存在 cindex, 则将其删除,重新插入当前段落最新的 @dfn 内容。

希望大家批评指正。

谢谢。


#6
(catch 'finish
  (let (result)
    (while t
      (if-let* ((start-point (search-forward "dfn{" nil t))
                (end-point (search-forward "}" nil t)))
          (push (buffer-substring-no-properties start-point end-point)
                result)
        (throw 'finish result)))))

Emacs 24没有自带if-let*

(cl-loop with result
         while t
         for start-point = (search-forward "dfn{" nil t)
         for end-point = (end-point (search-forward "}" nil t))
         if (and start-point end-point)
         collect (buffer-substring-no-properties start-point end-point) into result
         else return result)

#7
(with-temp-buffer
  (url-insert-file-contents "https://raw.githubusercontent.com/emacs-mirror/emacs/master/doc/emacs/basic.texi")
  (let ((dfns '()))
    (goto-char (point-min))
    (while (re-search-forward "@dfn{\\([^}]+\\)}" nil t)
      (push (match-string 1) dfns))
    dfns))
;; =>
;; ("prefix argument" "prefix
;; argument" "numeric argument" "argument" "word wrapping" "truncate" "continued line" "continuation" "line wrapping" "screen lines" "logical line" "save" ...)

#8

感谢大家的帮助,我根据大家的建议又深入研究了一下,下面主要是我的一些记 录,在最后给出了一个新版本对 Elisp intro 11.4.4 的解法。

再次谢谢大家。

indentation issue

已经安装了 agreesive-indent-mode 来让缩进更好看。

lisp style

Always enable lexical scoping. This must be done on the first line as a file local variable.

  • (- x 1) 改用 (1- x)

  • 关于函数及变量命名

    这个是因为 LISP 产生的年代 (1958 年),ASCII 码还在没创造出 来 (Work on the ASCII standard began on October 6, 1960),那时侯计 算机是无法区别大小写的,所以 lisp 对函数的命名就使用了短横线的方式来划分单词。以后我也会沿用这个传统的。

Appendix D Tips and Conventions

  • M-x checkdoc 用来检查文档

dynamic array

感谢分享这个代码:

  (catch 'finish
    (let (result)
      (while t
        (if-let* ((start-point (search-forward "dfn{" nil t))
                  (end-point (search-forward "}" nil t)))
            (push (buffer-substring-no-properties start-point end-point)
                  result)
          (throw 'finish result)))))
  • catch 和 throw 的这个用法很好用。我进一步看了 Emacs Reference Manual:

    The ‘throw’ need not appear lexically within the ‘catch’ that it jumps to. It can equally well be called from another function called within the ‘catch’. As long as the ‘throw’ takes place chronologically after entry to the ‘catch’, and chronologically before exit from it, it has access to that ‘catch’.

    throw 还可以在写在别的函数中,只要 throw 在 catch 后执行。 以后可能会用上这样的方法。

  • 关于 let

    我在 Eintro 中看到 let 是这样说明的:

      (let ((VARIABLE VALUE)
            (VARIABLE VALUE)
            ...)
        BODY...)
    

    这里的能不能写成: (let result …? 也就是去掉 result 的括号? 或者 result '())?

  • 关于 if-let*

    这个用法很方便。在绑定局部变量的之后,返回真或假。如果真,则执行 (push …); 如果假,则 (throw …)

  • 关于 search-forward 的传入参量: nil t

    A value of nil means search to the end of the accessible portion of the buffer. Optional third argument, if t, means if fail just return nil (no error).

    这个比我原来用 (point-max) 简洁多了。

整合成了一个新的函数

我把 @cireu 提供的方法,尝试将其整合成了一个新的函数。我把原来的函数搜索内容的循环代码:

(while  (< 0 (how-many "dfn{" (point) (point-max)))
  (copy-region-as-kill (search-forward "dfn{" (point-max))
                       (- (search-forward "}") 1))
  (setq newCindex (concat "@cindex "
                          (substring-no-properties (nth 0 kill-ring)) "\n"))

用建议代码替换:

(catch 'finish
(let (result)
  (while t
    (if-let* ((start-point (search-forward "dfn{" nil t))
              (end-point (search-forward "}" nil t)))
        (push (buffer-substring-no-properties start-point end-point)
              result)
      (throw 'finish result)))

按照我的理解,这段函数提取了所有 @dfn{} 中的内容,然后将结果以 result 返回。

发现,不能如愿。

然后我从从 Elisp Manual 中找到一个小例子 ((elisp) Catch and Throw),稍微做了一点修改:

(defun foo-outer ()
  (interactive)
  (message 
   (catch 'foo
     (foo-inner))))

(defun foo-inner ()
  (let ((test-string "test-AAA"))
    (throw 'foo test-string)))

;; M-x foo-outer
;; ⇒ test-AAA

也就是通过 catch and throwtest-AAAfoo-inner 传给 foo-outer

之后,打算将下面的代码修改成可用执行函数:

(catch 'finish
(let (result)
  (while t
    (if-let* ((start-point (search-forward "dfn{" nil t))
              (end-point (search-forward "}" nil t)))
        (push (buffer-substring-no-properties start-point end-point)
              result)
      (throw 'finish result)))

这是我修改后的代码:

(defun return-result ()
  (interactive)  
  (message 
   (catch 'finish
     (collection-dfn-content))))

(defun collection-dfn-content ()
  (catch 'finish
    (let (result)
      (while t
        (if-let* ((start-point (search-forward "dfn{" nil t))
                  (end-point (search-forward "}" nil t)))
            (push (buffer-substring-no-properties start-point end-point)
                  result)
          (throw 'finish result))))))

在某个 *.texi buffer 的某个含有 @dfn{} 的段落内执行: M-x return-result 出现下面的错误:

return-result: Wrong type argument: stringp, ("\" (point) (point-max)))
(copy-region-as-kill (search-forward \"dfn{\" (point-max))
                     (- (search-forward \"}" "fixed}" "file}" "begining}" "paragraphindent}" "found ddd}" "write more}" "looks}" "make}" "second}" ...)

abuse “kill ring”

(with-temp-buffer
(url-insert-file-contents "https://raw.githubusercontent.com/emacs-mirror/emacs/master/doc/emacs/basic.texi")
(let ((dfns '()))
  (goto-char (point-min))
  (while (re-search-forward "@dfn{\\([^}]+\\)}" nil t)
    (push (match-string 1) dfns))
  dfns))

感谢你的代码,非常简单好用。几个从来没接触到的东西:

  • with-temp-buffer

  • url-insert-file-contents

用来分享代码片断非常好用。

  • dfns '(): 对空 list 初始化

  • 在搜索的时候大家都用这样方式: search-forward nil t

  • 研究了这个正则: @dfn{\\([^}]+\\)}

    1. 学到了这个表达,也就是排除 “}”:

    2. 看到了这个章节 (elisp) Simple Match Data 才知道 subexpression 的用法。

    我发现 @dfn{\\([^{]+\\)} 这个正则同样能达到目的,但是不知道为什么。

    我自己的理解是 [^{] 在 subexpression 中剔除一个字符,这里我看到用了 ‘}’, 我自己用 '{'发现 也实现了相同的功能。

    进一步,可能这是一个很技巧的东西。在 subexpression 为了,提取 {内容}, 用到了这个 [^{]+,请问这是普遍的做法么?

  • 原来我一直在寻找的东西叫 (push)

更新

根据大家的建议,我把代码进行了一些修改,主要是避免滥用 kill-ring (谢谢 @twlz0ne)。

(defun my-create-cindex ()
  "Extract and update contents in @dfns{CONTENTS} in current
 paragraph."
  (interactive)
  (my-delete-old-cindex)
  (let ((beg (progn (save-excursion
                      (backward-paragraph) (point))))
        (end (progn (save-excursion
                      (forward-paragraph) (point))))
        (dfns '()))
    (save-excursion
      (save-restriction
        (widen)
        (narrow-to-region beg end)
        (goto-char (point-min))
        (while (re-search-forward "@dfn{\\([^}]+\\)}" nil t)
          (push (match-string 1) dfns))
        (backward-paragraph)
        (newline)
        (print-elements-of-list dfns)))))

(defun my-delete-old-cindex ()
  "Delete old cindex before the current paragraph."
  (interactive)
  (save-excursion
    (save-restriction
      (let ((beg (progn (save-excursion
                          (save-restriction
                            (widen)
                            (backward-paragraph 2)
                            (forward-paragraph)
                            (point)))))
            (end (progn (save-excursion
                          (save-restriction
                            (widen)
                            (backward-paragraph) (point))))))
        (flush-lines "@cindex" beg end)))))

(defun print-elements-of-list (list)
  "Print each element of LIST on a line of its own."
  (while list
    (insert-before-markers
     (concat (concat "@cindex "
                     (format "%s" (car list)))) "\n")
    (setq list (cdr list))))


#9

你的form格式变了解释器就无法把各个参数的值bind到参数symbol了(格式不按定义来,怎么知道谁是谁嘛),除非,这个函数自己有代码来适应多种参数格式(let的文档里没写多种格式,肯定是没有了),例如doom的enlist

(defun doom-enlist (exp)
  "Return EXP wrapped in a list, or as-is if already a list."
  (if (listp exp) exp (list exp)))