Emacs Lisp并不难学

作者:陈斌 (redguardtoo)

原创日期: 2012-11-30 五

本文的目的是把Lisp去神秘化.

我学的是Emacs Lisp(Emacs Lisp),开发环境也是Emacs.所以我举的例子都是只基于Elisp.

本文针对的读者是有相当经验的开发者,目的是尽可能简明扼要地突出重点.

我对于目前的主流开发语言都很熟悉,但是Lisp还只能算是入门水准,所以错过很多Lisp精彩之处是很可能的.

我所谓的重点,主要是指Lisp对于开发一个现实世界的产品有什么优点.

所有的函数和数据可以在系统运行时改变

其意义是,假设你一个系统上线了,你可以随时改变正在运行的代码,不需要重启系统.

我的理解是,一个函数实际上就是带有字符串Key(该key就是函数名)的数据,运行某个函数就是在系统运行时根据Key找到对应的数据(或者代码,在Lips中是一回事)运行之.

例如,我可以在hook中申明某个函数将被调用,而这个函数的定义可以还不存在.系统在运行时才会去寻找这个函数.解释一下,hook可以认为是事件触发机制,就是在系统运行的某个时刻调用用户自定义的函数)

优点是灵活性很高.缺点是Lisp写的东西快不了.

没有namespace,所有的函数默认都是可以全局访问的

正因为这两条,所以要多打字(每个函数都要手动输入名字前缀).

这点我完全可以接受,没有namespace就是多打点字而已.那些不了解历史或者没有用过C的初级程序员可能会大惊小怪.实际上没什么大不了的.

所有函数都可以访问在Emacs场景下有个巨大的优点,见下一条.

defadivce可以改变系统中任意函数的行为.

defadvice可以重定义系统中的任意函数.原因如前文所说.

循环语句和条件判断语句

和其他语言没什么不同,foreach之类的语法糖也不少.

我唯一不喜欢的是没有C中的return,break,和continue语句,这样的缺点是有可能让代码嵌套过深.

不过不是什么大不了的问题,Lisp也提供了一些替代语法.

另外其他所有语言提供了这些语法糖又怎么样呢,我遇到的超过50%的程序员还是一样瞎写.

不同寻常的语法

Lisp语法是操作符号在前,被操作对象在后.

例如2+3+4,在Lisp的语法中是这样的:

(+ 2 3 4)

所有的语法都是这样的前缀表达式(Polish Notation),很多人不习惯这样的语法,但是它有一些突出优点:

  1. 这样的语法做语法解析特别容易,所以第三方支持工具很容易开发. 在大规模系统开发时,这点很有用.例如,分析大型项目的源代码时,你能唯一依靠的grep和正则表达式,ELisp简单严格的语法使得正则表达式很好写.
  2. 实际编程时少打很多运算符号,这对于有实战经验的程序员是巨大的优点.如果你和我说什么重要的是思想和设计模式,打字速度不重要,那么菜鸟请走开.
  3. 最重要的优点是,这种语法相当于一种过滤机制,能够接受这种语法的人通常都是头脑比较开放思维敏捷的人.说到底产品开发的决定性因素是人,所以这个能过滤人的优点是决定性的.

函数可内嵌文档,且该文档可被Emacs帮助系统调用

现在能做到这点的系统也没几家.从细节我们可以看出Emacs和Elisp的完美之处.

How to use ctags with Emacs effectively

UPDATE on <2018-02-24 Sat>: I released counsel-etags, a complete solution for Ctags/Emacs.

See http://emacswiki.org/emacs/EmacsTags for general usage of tags file. A file name of tags files is TAGS by default. But in Emacs manual the tags file is named tags table. Either tags file or tags table or TAGS is only different name for the same thing.

According wiki page, people usually use ctags to do two things,

  • Code navigation
  • Auto-complete/IntelliSense

In my opinion, ctags is OK for code navigation because it's integrated into Emacs well and ctags supports more programming language. But ctags only uses regular expression to analyze the code. So it's not as precise as professional tools like cscope or GNU Global. According to my own experience, ctags plus grep is actually good enough for code navigation in big enterprise projects with mixed environments. If you are among a few talented geeks who are developing Linux Kernel in the most efficient way, you should use better tools like GNU Global. See the head up from Nick Alcock. His summary of the Pro/Con of GNU Global is great.

IntelliSense is a different story. For programming languages like Javascript, ctags provide some IntelliSense with is better than nothing. C++ is a more complex language which is a litter harder for ctags to handle unless you are not using those "advanced" features of C++.

I found one missing piece from EmacsWiki is the tip on how to manage tags file easily when the TAGS is built from some code of third party libraries (QT, wxWidgets, GTK, Boost, just name a few).

My typical work flow is:

  • Load the tags file for my own code.
  • Load other tags files built from third party libraries related to my current task.
  • Unload third party tags immediately if its not needed.

The purpose to load tags files is to look up API easily. For example, I use C++ library wxWidgets to develop my application. I want to use its API called wxWindow::Maximize. I type M-x find-tag and input the keyword Maximize. So I can check the function definition and know what parameters I need fill in to use "Maximize".

The reason to do the unload thing is to avoid function name conflict from some tags files. EmacsWiki has a section "Choosing Among Multiple Tags for the Same Name" to solve this problem. But unloading is more convenient. In theory, it would be annoying if I need repeat load and unload the same tags files. In reality, it seldom happens because if I need work on Javascript (so I unload C++ related TAGS), I will usually keep working on Javascript until the end of the day.

Produce tags files for all the environments

For example, I produce tags files of Library A for Mac and Linux. I put tags file from Mac at directory "/home/cb/tags/Library_A/mac" and file from Linux at /home/cb/tags/Library_A/linux.

Use git and github to manage the tags files

So I can have copies of the tags files at github server and my local computers. That's also a part of my strategy to setup the development environment. My projects on different computers usually use same external library.

EmacsWiki discusses how to update your tags file frequently because your own code are updated frequently. But the tags file from a big library like wxWidgets need not be updated to often because I only use limited old APIs in that library.

Write emacs lisp code to load/unload these tags files

Here is the code. In this example, if I need load tags file for wxWidgets, I call the function "add-wx-tags". If I need unload the tags file, I press "Ctrl-u" and call the same function "add-wx-tags". Since I use Smex, calling function doesn't mean pressing many keys.

; Set up tags built from third party libraries ===begin
; define tags alias like "wx-tags" here
(setq wx-tags (expand-file-name "~/tags/wx/osx/TAGS"))

; @see <Selecting a Tags Table> in Emacs manual for details.
; We only change the list "tags-table-list". It is documented officialy.
(defun insert-into-tags-table-list(e)
  (add-to-list 'tags-table-list e t))
(defun delete-from-tags-table-list (e)
  (setq tags-table-list (delete e tags-table-list)))
; This is a sample command, all you need is copy/paste this template
; for other new commands
(defun add-wx-tags (&optional del)
  "Add or delete(C-u) wxWidgets tags into tags-table-list"
  (interactive "P")
  (let (mytags)
    ; here add your third party tags files
    ; Usually you need load/unload tags files combination in one command
    ; change below line to add them
    (setq mytags (list wx-tags))
    (if del (mapc 'delete-from-tags-table-list mytags)
      (mapc 'insert-into-tags-table-list mytags))))

This solution only uses one standard variable tags-table-list in Emacs. So it will always work with any other Emacs plugins you install. And you can use all the old hotkeys and functions ("find-tag", for example) without any problem.

See Emacs manual for technical details.

My personal emacs customization (custom.el)

custom.el is my personal emacs configuration. I use it with my publicized Emacs configuration.

;; I'm in Australia now
;; I'm in Australia now
(setq system-time-locale "C")

;; {{ stardict
(setq sdcv-dictionary-simple-list '("朗道英汉字典5.0"))
(setq sdcv-dictionary-complete-list '("WordNet"))
;; }}

;; {{ elpa-mirror
(setq elpamr-default-output-directory "~/myelpa")
(setq elpamr-repository-name "myelpa")
(setq elpamr-repository-path "https://dl.dropboxusercontent.com/u/858862/myelpa/")
(setq elpamr-email "myname@mydomain.com")
;; }}

;; lock my package
(if (file-readable-p (expand-file-name "~/Dropbox/Public/myelpa/archive-contents"))
     (setq package-archives '(("myelpa" . "~/Dropbox/Public/myelpa/"))))

;; {{ Set up third party tags
(defun insert-into-tags-table-list(e)
  (add-to-list 'tags-table-list e t)
  )
(defun delete-from-tags-table-list (e)
  (setq tags-table-list (delete e tags-table-list))
  )

(defun add-wx-tags (&optional del)
  "Add or delete(C-u) wxWidgets tags into tags-table-list"
  (interactive "P")
  (let (mytags)
    ; here add your third party tags
    (setq mytags (list "~/tags/wx/osx/TAGS"))
    (if del (mapc 'delete-from-tags-table-list mytags)
      (mapc 'insert-into-tags-table-list mytags)
      )
    )
  )
;; }}

;; (getenv "HOSTNAME") won't work because $HOSTNAME is not an env variable
;; (system-name) won't work because as Optus required, my /etc/hosts is changed
(defun my/at-office ()
  (interactive)
  (let ((my-hostname (with-temp-buffer
                       (shell-command "hostname" t)
                       (goto-char (point-max))
                       (delete-char -1)
                       (buffer-string))
                     ))
    (and (string= my-hostname "my-sydney-workpc")
         (not (or (string= my-hostname "sydneypc")
                  (string= my-hostname "ChenBinMacAir")
                  (string= my-hostname "eee")
                  )))
    ))

(defun my/use-office-style ()
  (interactive)
  (let ((dir (if (buffer-file-name)
                 (file-name-directory (buffer-file-name))
               "")))
    (string-match-p "CompanyProject" dir)
    ))

(defun my/setup-develop-environment ()
  (cond
   ((my/use-office-style)
    (message "Office code style!")
    (setq coffee-tab-width 4)
    (setq javascript-indent-level 4)
    (setq js-indent-level 4)
    (setq js2-basic-offset 4)
    (setq web-mode-indent-style 4))
   (t
    (message "My code style!")
    (setq coffee-tab-width 4)
    (setq javascript-indent-level 2)
    (setq js-indent-level 2)
    (setq js2-basic-offset 2)
    (setq web-mode-indent-style 2))
   ))

(add-hook 'js2-mode-hook 'my/setup-develop-environment)
(add-hook 'web-mode-hook 'my/setup-develop-environment)

(my/setup-develop-environment)

;; {{ gnus setup
(require 'nnir)

;; ask encyption password once
(setq epa-file-cache-passphrase-for-symmetric-encryption t)


;;@see http://www.emacswiki.org/emacs/GnusGmail#toc1
;; (add-to-list 'gnus-secondary-select-methods '(nntp "news.gmane.org"))
;; (add-to-list 'gnus-secondary-select-methods '(nntp "news.gwene.org"))
(if (my/at-office)
    (add-to-list 'gnus-secondary-select-methods '(nnml "optus"))
    (setq mail-sources
      '((pop :server "localhost"
         :port 1110
         :user "CP111111"
         :password "MyPassword"
         :stream network)))
    )


(setq gnus-select-method
             '(nnimap "gmail"
                      (nnimap-address "imap.gmail.com")
                      (nnimap-server-port 993)
                      (nnimap-stream ssl)
                      (nnir-search-engine imap)
                      (nnimap-authinfo-file "~/.authinfo.gpg")
                      ; @see http://www.gnu.org/software/emacs/manual/html_node/gnus/Expiring-Mail.html
                      ;; press 'E' to expire email
                      (nnmail-expiry-target "nnimap+gmail:[Gmail]/Trash")
                      (nnmail-expiry-wait 90)
                      ))

;;@see http://gnus.org/manual/gnus_397.html
;; (add-to-list 'gnus-secondary-select-methods
;;              )

(setq-default
  gnus-summary-line-format "%U%R%z %(%&user-date;  %-15,15f  %B%s%)\n"
  gnus-user-date-format-alist '((t . "%Y-%m-%d %H:%M"))
  gnus-summary-thread-gathering-function 'gnus-gather-threads-by-references
  gnus-sum-thread-tree-false-root ""
  gnus-sum-thread-tree-indent ""
  gnus-sum-thread-tree-leaf-with-other "-> "
  gnus-sum-thread-tree-root ""
  gnus-sum-thread-tree-single-leaf "|_ "
  gnus-sum-thread-tree-vertical "|")
(setq gnus-thread-sort-functions
      '(
        (not gnus-thread-sort-by-date)
        (not gnus-thread-sort-by-number)
        ))

;; we want to browse freely from gwene (RSS)
(setq gnus-safe-html-newsgroups "\\`nntp[+:]news\\.gwene\\.org[+:]")

; NO 'passive
(setq gnus-use-cache t)
(setq gnus-use-adaptive-scoring t)
(setq gnus-save-score t)
(add-hook 'mail-citation-hook 'sc-cite-original)
(add-hook 'message-sent-hook 'gnus-score-followup-article)
(add-hook 'message-sent-hook 'gnus-score-followup-thread)
; @see http://stackoverflow.com/questions/945419/how-dont-use-gnus-adaptive-scoring-in-some-newsgroups
(setq gnus-parameters
      '(("nnimap.*"
         (gnus-use-scoring nil)) ;scoring is annoying when I check latest email
        ))

(defvar gnus-default-adaptive-score-alist
  '((gnus-kill-file-mark (from -10))
    (gnus-unread-mark)
    (gnus-read-mark (from 10) (subject 30))
    (gnus-catchup-mark (subject -10))
    (gnus-killed-mark (from -1) (subject -30))
    (gnus-del-mark (from -2) (subject -15))
    (gnus-ticked-mark (from 10))
    (gnus-dormant-mark (from 5))))

;; Fetch only part of the article if we can.  I saw this in someone
;; else's .gnus
(setq gnus-read-active-file 'some)

;; Tree view for groups.  I like the organisational feel this has.
(add-hook 'gnus-group-mode-hook 'gnus-topic-mode)

;; Threads!  I hate reading un-threaded email -- especially mailing
;; lists.  This helps a ton!
(setq gnus-summary-thread-gathering-function
      'gnus-gather-threads-by-subject)

;; Also, I prefer to see only the top level message.  If a message has
;; several replies or is part of a thread, only show the first
;; message.  'gnus-thread-ignore-subject' will ignore the subject and
;; look at 'In-Reply-To:' and 'References:' headers.
(setq gnus-thread-hide-subtree t)
(setq gnus-thread-ignore-subject t)

; Personal Information
(setq user-full-name "My Name"
      user-mail-address (if (my/at-office) "myname@office.com" "myname@mydomain.com")
      )

;; Change email address for work folder.  This is one of the most
;; interesting features of Gnus.  I plan on adding custom .sigs soon
;; for different mailing lists.
;; Usage, FROM: chen bin <work>
(setq gnus-posting-styles
      '((".*"
         (name "My Name"
          (address "myname@mydomain.com"
                   (organization "")
                   (signature-file "~/.signature")
                   ("X-Troll" "Emacs is better than Vi")
                   )))))


(setq mm-text-html-renderer 'w3m)

;; http://www.gnu.org/software/emacs/manual/html_node/gnus/_005b9_002e2_005d.html
(setq gnus-use-correct-string-widths nil)
;; @see http://emacs.1067599.n5.nabble.com/gnus-compile-td221680.html
;;(gnus-compile)
; =Gnus Tips=
;; @see http://www.scottmcpeak.com/gnus.html
;; }}

How to use FTP in Emacs

Emacs's default Ftp client is a little outdated compared to other modern Ftp clients.

What I want is a two panel file explorer with ftp client integrated (An excellent example is Total Commander on windows).

In Emacs, I've got Sunrise Commander which is close to Total Commander, but NO Ftp integration.

I dig around the internet for a while and find that the perfect solution is mount the ftp server into my local file system with CurlFtpFS.

The best part of this solution is CurlFtpFS supports both OS X and Linux.

In order to install CurlFtpFS on OS X, you need use the package manager Homebrew. The only catch I met on OS X is that I need upgrade Fuse4X driver manually for some wired reason to avoid "incompatible with the kernel version" error. Upgrading is simple. Download dmg plus mouse click stuff only. See FAQ for details.

For the usage of CurlFtpFS, see HERE for documentation. It's for Linux. But totally fine for OS X users.

Practical guide to use Gnus with Gmail

The article is moved to GitHub.

The latest version of yasnippet is NOT compatible with auto-complete

Yes. The latest yasnippet (yasnippet-20120822.52) changed too many APIs and I cannot figure out how to work around it. Upgrading to the latest auto-complete (auto-complete-20120922.1815) won't solve the problem.

See https://github.com/capitaomorte/yasnippet/issues/311 for the details.

So here is the fix. You can use older version of yasnippet (yasnippet-20120718) and wait until the issue is fixed.

Here is the link of older version, http://dl.dropbox.com/u/858862/yasnippet-20120718.tar.gz.

Why Facebook app for android pad sucks?

I don't use facebook. I just record my roommates' complaining.

Here are his points,

failed to take full advantage of big screen of android pad
The right blank margin could be used to display comments instead, as some application has done.
scroll down/up to view the images sucks
Slow and hard to locate the exact item he wants.
Performance may be not good enough
Especially when viewing the images, it's not as fluent as other applications.

python code to migrate the MySQL database

The script reads configuration and requires python 2.4.

#!/usr/bin/python
import sys
import os
import string
import time
import datetime
import getopt

#############
# CONFIG
#############
DB_NAME='mydb'
DB_PASSWORD='111111'
DB_USER='root'
DB_HOST='mylinux0'
SQL_DB_SCHEMA=os.path.abspath("db_schema.sql")
SQL_CREATE_TABLE=os.path.abspath("000.sql")
CLONE_TABLES=['MyJobs','MyUsers']

#############
# GLOBALS
#############
backup_db=False
migration=False
config=None
verbose=False
delete=False
remote=False

def sql_cmd(cmd):
    if remote:
        return 'mysql -h %s -u %s -p%s -e " %s " %s' % (DB_HOST,DB_USER,DB_PASSWORD,cmd,DB_NAME)
    return 'mysql -u %s -p%s -e " %s " %s' % (DB_USER,DB_PASSWORD,cmd,DB_NAME)

def sql_script(script):
    if remote:
        return 'mysql -h %s -u %s -p%s %s < %s' % (DB_HOST,DB_USER,DB_PASSWORD,DB_NAME,script)
    return 'mysql -u %s -p%s %s < %s' % (DB_USER,DB_PASSWORD,DB_NAME,script)


def make_sql_to_create_table():
    s=open(SQL_DB_SCHEMA).read()
    for t in CLONE_TABLES:
        s=s.replace(t,t+'2')
    open(SQL_CREATE_TABLE,'w').write(s)

def backup_database():
    fname="%s-%s.dump.gz" % (DB_NAME,time.strftime('%Y%m%d%H%M%S', time.gmtime()))
    if remote:
        cmd="mysqldump -h %s -u %s -p%s --compact %s|gzip -9 > %s" % (DB_HOST,DB_USER,DB_PASSWORD,DB_NAME,fname)
    else:
        cmd="mysqldump -u %s -p%s --compact %s|gzip -9 > %s" % (DB_USER,DB_PASSWORD,DB_NAME,fname)

    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

def delete_db_records():
    cmd=sql_cmd("delete from sources;delete from cameras;")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

def package():
    cmd="tar zcvf migrate-db.tar.gz *.sql migrate.py .migrate_*"
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

def read_config(config):
    global DB_NAME
    global DB_PASSWORD
    global DB_USER
    global DB_HOST
    f=open(config,"r")
    for line in f:
        a=line.split("=")
        if len(a)!=2:
            continue
        n=a[0].strip()
        v=a[1].strip()
        if n=="DB_NAME":
            DB_NAME=v
        elif n=="DB_PASSWORD":
            DB_PASSWORD=v
        elif n=="DB_USER":
            DB_USER=v
        elif n=="DB_HOST":
            DB_HOST=v

def usage():
    print '''
USAGE: python migrate.py [OPTIONS]

OPTIONS:
    -h, --help
        print this help

    -b, --backup-database
        backup the old database before migration (recommended)

    -m, --migrate
        donot start migration

    -v, --verbose
        print database related information

    -p, --package
        packge all the scripts into migrate-db.tar.gz

    -d, --delete-db-records
        delete database records so we can restart migration

    -c, --config

SAMPLES:
    python migrate.py -b                        #back database and exit
    python migrate.py -m                        #start migration right now
    python migrate.py -b -m                     #backup the database and start the migration (recommended)
    python migrate.py -v                        #print database information and exit
    python migrate.py -c .migrate_staging -v    #print staging database information and exit
'''

def show_info():
    print '''
==Database Information
name:{name}
password:{password}
user:{user}
host:{host}
'''.format(name=DB_NAME, password=DB_PASSWORD,user=DB_USER,host=DB_HOST)

def confirm(prompt=None, resp=False):
    """prompts for yes or no response from the user. Returns True for yes and
    False for no.

    'resp' should be set to the default value assumed by the caller when
    user simply types ENTER.

    >>> confirm(prompt='Create Directory?', resp=True)
    Create Directory? [y]|n:
    True
    >>> confirm(prompt='Create Directory?', resp=False)
    Create Directory? [n]|y:
    False
    >>> confirm(prompt='Create Directory?', resp=False)
    Create Directory? [n]|y: y
    True

    """

    if prompt is None:
        prompt = 'WARNING: DONOT run me twice and migration CANNOT be rolled back, GO?'

    if resp:
        prompt = '%s [%s]|%s: ' % (prompt, 'y', 'n')
    else:
        prompt = '%s [%s]|%s: ' % (prompt, 'n', 'y')

    while True:
        ans = raw_input(prompt)
        if not ans:
            return resp
        if ans not in ['y', 'Y', 'n', 'N']:
            print 'please enter y or n.'
            continue
        if ans == 'y' or ans == 'Y':
            return True
        if ans == 'n' or ans == 'N':
            return False

if __name__=="__main__":
    '''
    @see http://linux.byexamples.com/archives/366/python-how-to-run-a-command-line-within-python/
    '''
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hbmvpdc:r", ["help", "backup-database","--migrate","--verbose","--package","--delete-db-records","--config=","--remote"])
    except getopt.GetoptError, err:
        print str(err) # will print something like "option -a not recognized"
        usage()
        exit(2)

    if len(opts)==0:
        usage()
        exit(2)

    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit()
        elif o in ("-v", "--verbose"):
            verbose=True
        elif o in ("-c", "--config"):
            config=a
            print config
        elif o in ("-d", "--delete-db-records"):
            delete=True
        elif o in ("-p", "--package"):
            package()
            exit(0)
        elif o in ("-b", "--backup-database"):
            backup_db=True
        elif o in ("-m", "--migrate"):
            migration=True
        elif o in ("-r", "--remote"):
            remote=True
        else:
            assert False, "unhandled option"

    if config!=None:
        read_config(config)

    if delete:
        delete_db_records()
        sys.exit()

    if verbose:
        show_info()
        sys.exit()

    if backup_db:
        backup_database()

    if migration==False:
        exit(0)

    if confirm()==False:
        print "Aborting ..."
        exit(0)

    make_sql_to_create_table()

    print "==START MIGRATION"

    # test data base connection
    cmd=sql_cmd('show databases;use %s;' % (DB_NAME))
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    cmd=sql_script("000.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('001.sql')"))
    cmd=sql_script("001.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('002.sql')"))
    cmd=sql_script("002.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('003.sql')"))
    cmd=sql_script("003.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('004.sql')"))
    cmd=sql_script("004.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('005.sql')"))
    cmd=sql_script("005.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('006.sql')"))
    cmd=sql_script("006.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    os.system(sql_cmd("insert into migrate (Action) values ('100.sql')"))
    cmd=sql_script("100.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    cmd=sql_script("insert_sample_sources.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    cmd=sql_script("app_config.sql")
    print "==WILL RUN [ %s ]" % (cmd)
    os.system(cmd)
    print "==DONE"

    print "==END MIGRATION"
# vim: set expandtab tabstop=4 shiftwidth=4:

Emacs中的自动完成(Auto Completion)完全指南

作者: 陈斌(redguardtoo)

更新时间: <2017-04-05 Wed>

原创时间: <2012-08-21 Tue>

Emacs比普通IDE的Intellisense强大,例如可输入当前文件名(带或者不带文件名后缀都可以).

我不是来赞美Emacs的强大的,我是来吐槽的.自动完成框架太多,功能重叠,让人困扰.

所以本文目的有两个:

  • 说明哪些框架流行
  • 如何使用框架

概述

函数/插件名 快捷键 使用频率
company-mode (结合clang) 不需要 (也可启用TAB)
hippie-expand M-/
evil-mode C-n/C-p(完成词),C-x C-n/p(完成行)
auto-complete (结合clang) TAB
complete-symbol C-M-i

auto-complete结合clang

auto-complete是第三方开发的插件,提供了自动完成需要的支持(例如在命令行环境下对下拉菜单的模拟).

clang是苹果公司C/C++/Objective-C编译器,对C++语法的解析很好.

显然,这种l案只适用于 Clang 支持的语言.

需要通过elpa(Emacs的AppStore)安装第三方插件 auto-completeauto-complete-clang.

具体配置请参考我的init-auto-complete.el.

已被company-mode取代,不推荐.

company-mode

company-mode和auto-complete功能类似,唯一的区别是clang以及其他语言支持已内置,所以不需要安装其他elisp插件或者额外配置.目前我已用company-mode代替了auto-complete.

hippie-expand

hippie-expand是Emacs自带的自动完成框架.

其默认的一些特色功能(例如,完成词/文件名/行的功能,或在 minibuffer 中使用)可作为 company-mode 补充.

complete-symbol

Emacs自带的一个函数,我对这个方案感觉一般,因为在Emacs23中,它默认仅仅调用了后台的ctags而已.

hippie-expand 经过调教也可用 Ctags,且默认特色功能比 complete-symbol 多,所以我没兴趣配置第三个自动完成框架了.

通过阅读Emacs24的ChangeLog,我发现 complete-symbol 经过配置后可以用 semantic 作为语法解析后端(Emacs23.4也支持,不明白为什么把该特性放在Emacs24的ChangeLog介绍).

semantic是lisp写的语法解析器,Emacs自带,智能程度介于ctags和clang之间,解析速度比较慢,如果机器配置较差会很卡.

我不喜欢这个语法解析器,过去在 Semantic 尚是Cedet的一个组件时,我折腾过 Cedet,当时感觉速度慢,配置繁琐,不稳定.

Semantic 支持多种语言,如C++/Java.

配置请参考我的init-semantic.el(注意,我不用 Semantic,所以相应代码被注释了.你可取消注释).

已被 company-mode 取代,不推荐.

evil-mode

Evil-mode把Emacs模拟成了Vim,是我最喜欢的第三方插件,其自动完成很简单,就是根据当前文件内容自动完成词或行.

值得推荐是因为我也是重度Vim用户,Vim的快捷键已成为我本能了.

无需配置,启动 evil-mode 即可,参见我的init-evil.el.

其他

我的一个想法. 类似于Evil的完成行.但是可以扫瞄指定目录中所有的文件,列出相似的行.

优点在于:

  • 匹配可以更加宽容一点
  • 使用目前最快的grep工具如ripgrep扫瞄文本

实现见init-ivy中的=counsel-complete-line-by-grep=.

试用好几个月了,非常好用.

小结

自动完成包括前端用户界面和后端语法解析引擎两个部分.

作为前端的用户界面, company-modeauto-complete 相当成熟.

后端 Clang 最优秀.

Ctags 谈不上语法解析,只是正则表达式罢了,但是因此才能通吃所有语言.

Semantic 支持的语言比 Clang 多一点,性能和稳定性比较差.

作为一个C++程序员,我过去用 auto-complete 加上 Clang,现在用 company-modeClang.

脚本语言(Ruby/Python/Javascript)用 Ctagscompany-mode 自带的 company-etags.

基于框架自己发明新技术会更好用.

My git alias in .bashrc

CREATED: <2012-12-26 Wed>

UPDATED: <2017-11-02 Thu>

My favorite alias are g, gau, gc, gt, gl,and gdc.

# {{ git
# Git alias
function gitshortlogcmd () {
    git log --date=short --decorate --graph "$@"
}
function gitlogcmd () {
    git log --date=short --decorate --graph --pretty=format:'%C(yellow)%h%Creset%C(green)%d%Creset %ad %s %Cred(%an)%Creset' "$@"
}
alias g="git status --short -b"
alias gb="git branch"
alias gn="git status --untracked-files=no --short -b"
alias gfl="git diff-tree --no-commit-id --name-only -r"
alias ga="git add"
alias gr="git rebase -i `git remote`/`git symbolic-ref --short HEAD`"
alias gap='git add --patch'
alias gai='git add -i'
alias gau="git add -u"
alias gc="git commit -m"
alias gca="git commit --amend"
alias gja="git --no-pager commit --amend --reuse-message=HEAD" # git just amend
alias gt="git stash"
alias gta="git stash apply"
alias gmt="git mergetool"
alias gl="gitlogcmd"
alias glp="gitlogcmd -p"
alias gls="gitlogcmd --stat"
alias gnb="git checkout -b"
alias gss="git show --stat"
alias gsl="git log --pretty=format:'%h %s (%an)' --date=short -n1 | pclip"
alias gd="git diff"
alias gds="git diff --stat"
alias gdc="git diff --cached"
alias gdcs="git diff --cached --stat"
alias gps="git push"
alias gpf="git push --force"
alias gpl="git pull"
alias gpr="git pull -r"
alias cg='cd $(git rev-parse --show-toplevel)' #goto root dir
alias ghe='git diff --name-only --diff-filter=U|grep "\.html\|\.min\.js"|xargs -I{} sh -c "git checkout --theirs {} && git add {}"'
alias gme='git diff --name-only --diff-filter=U|grep "\.html\|\.min\.js"|xargs -I{} sh -c "git checkout --our {} && git add {}"'
# delete selected local branch
alias gdd='git branch -D $(git branch | sed "s/[\* ]\+//g" | ~/bin/percol.py)'
# show diff from the branch to current HEAD
alias gdp='git diff $(git branch | sed "s/[\* ]\+//g" | ~/bin/percol.py)..HEAD'
alias grh='git reset --hard HEAD'
alias gr1='git reset --hard HEAD^'
alias gr2='git reset --hard HEAD^^'
alias gs="git show"

function gsh {
    git log --date=short --pretty=format:'%h%d %ad %s (%an)' | python ~/bin/percol.py | awk '{print $1}' | xargs -i git show {}
}

function gcn()
{
    # commit with timestamp
    local d=`date +%m%d-%H%M%S`
    git add -u . && git commit -m ${d}
}

# find full path of file who under git controll
# the optional parameter is the keyword
function gf()
{
    if [ -z "$1" ]; then
        local cli=`git ls-tree -r HEAD --name-status | python ~/bin/percol.py`
    else
        local cli=`git ls-tree -r HEAD --name-status | grep "$1" | python ~/bin/percol.py`
    fi
    local rlt=$(cd $(dirname $cli); pwd)/$(basename $cli)
    echo ${rlt}
    echo -n ${rlt} | pclip
}

function glwho () {
    local guy=`git shortlog -sn | ~/bin/percol.py | sed 's/^\s*[0-9]\+\s*//g'`
    # space is a problem, so we can't use gitshortlogcmd here
    gitshortlogcmd --pretty=format:'%C(yellow)%h%Creset%C(green)%d%Creset %ad %s %Cred(%an)%Creset' --author="$guy" "$@"
}

function gfp ()  {
    if [ -z "$1" ]; then
        echo "Usage: gfp since [file]"
        echo "  Just alias of 'git format-patch -n --stdout since -- [file]'"
        echo "  'gfp since | git am' to apply the patch"
    fi
    git format-patch -n --stdout $1 -- $2
}

function gcnb () {
    local remoteb=$(git branch --all | sed '/no branch/d' | ~/bin/percol.py)
    local localb=$(echo $remoteb | sed 's/^ *remotes\/[a-z]*\///g')
    git checkout -b $localb $remoteb
}
function grb () {
    # switch to recent git branch or just another branch

    local crtb=`git branch | grep \*`
    local ptn="no branch"
    # compatible way to detect sub-strin in bash
    # @see http://stackoverflow.com/questions/229551/string-contains-in-bash
    if [ -z "${crtb##*$ptn*}" ]; then
        # detached HEAD
        git checkout $(git branch | sed '/no branch/d' | ~/bin/percol.py)
    else
        local myrbs=`git for-each-ref --sort=-committerdate refs/heads/ | sed -e s%.*refs\/heads\/%%g`
        local crb=`git symbolic-ref --short HEAD`
        git checkout `echo "$myrbs" | sed '/$crb/d' | ~/bin/percol.py`
    fi
}

# rebase on LOCAL branches
function gri () {
    local b=`git branch | sed 's/[\* ]\+//g' | ~/bin/percol.py`
    git rebase -i ${b}
}

# rebase on ALL braches
function gra () {
    local b=`git branch --all | sed 's/[\* ]\+//g' | sed 's/remotes\///g' | ~/bin/percol.py`
    git rebase -i ${b}
}

# select a local git branch
function gsb () {
    local b=`git branch | sed "s/[\* ]\+//g" | ~/bin/percol.py`
    echo -n ${b} | pclip;
    echo ${b}
}

# print current branch name
function gcb () {
    local crb=`git symbolic-ref --short HEAD`
    echo -n ${crb} | pclip;
    echo ${crb}
}

# new local branch based on remote branch
function gnr () {
    local myrb=`git for-each-ref --sort=-committerdate refs/remotes/ | sed -e s%.*refs\/remotes\/%%g | ~/bin/percol.py`
    local mylb=`echo -n $myrb | sed 's/.*\/\([^\/]\+\)$/\1/'`
    git checkout -b $mylb $myrb
}

function gchk () {
    if [ -z "$1" ]; then
        echo "Usage: gchk commit_id"
        echo "reset hard certain version of current working directory"
    else
        rm -rf $PWD/*
        git checkout $1 -- $PWD
    fi
}

function git2fullpath {
    local fullpath=$(git rev-parse --show-toplevel)/$1
    echo -n $fullpath | pclip
    echo $fullpath
}

function glf () {
    local str=`git --no-pager log --oneline --name-only $* |  ~/bin/percol.py`
    git2fullpath $str
}


function gsf () {
    local str=`git --no-pager show --pretty=format:'%h %s (%an)' --name-only $* |  ~/bin/percol.py`
    git2fullpath $str
}

function gdf () {
    local str=`git --no-pager diff --oneline --name-only $*| ~/bin/percol.py`
    git2fullpath $str
}

function gdcf () {
    local str=`git --no-pager diff --oneline --cached --name-only $* |  ~/bin/percol.py`
    git2fullpath $str
}

function ggr () {
  if [ -z "$1" ]; then
      echo "Grep files under git controll"
      echo "Usage: ggr [filename-pattern] text-pattern"
  elif [ $# -eq "1" ]; then
      git ls-tree -r HEAD --name-only |  xargs grep -sn "$1"
  elif [ $# -eq "2" ]; then
      git ls-tree -r HEAD --name-only | grep "$1" | xargs grep -sn --color -E "$2"
  fi
}

function grpc() {
  if [ -z "$1" ]; then
      echo "Replace the content of file in latest git commit"
      echo "Usage: grpc [commit-hash] old_string new_string (string could be perl regex)"
  elif [ $# -eq "2" ]; then
      git diff-tree --no-commit-id --name-only -r HEAD | xargs perl -pi -e "s/$1/$2/g"
  elif [ $# -eq "3" ]; then
      git diff-tree --no-commit-id --name-only -r $1 | xargs perl -pi -e "s/$2/$3/g"
  fi
}

function grpf() {
  if [ -z "$1" ]; then
      echo "Replace the content of file under git"
      echo "Usage: grpf old_string new_string (string could be perl regex)"
  elif [ $# -eq "2" ]; then
      git grep -l "$1" | xargs perl -pi -e "s/$1/$2/g"
  fi
}

function gp() {
    if [ $# = 0 ]; then
        local from=`gitshortlogcmd --pretty=format:'%h %ad %s (%an)' $* | ~/bin/percol.py|sed -e"s/^[ *|]*\([a-z0-9]*\) .*$/\1/"`;
        local fn=from-$from-`date +%Y%m%d-%H%M`.patch
        git format-patch -n --stdout $from > $fn && ls $fn
    else
        local fn=from-$1-`date +%Y%m%d-%H%M`.patch
        git format-patch -n --stdout $1 > $fn && ls $fn
    fi;
}
# }}