有几次朋友问我晚上的时候在干什么,我开玩笑似地说我在“磨刀”,他或许一下子不解了——我把折腾Emacs当做是磨刀。呵呵,“刀是不能不磨的”……长话短说,本文将介绍Mac OS X和GNU Emacs中的PATH环境变量及其设置方法。

首先你可能需要先了解一下Emacs For Mac OS。在Mac上,可能很多人偏爱Aquamacs这个发行版,相比较GNU Emacs(Cocoa),它是标准的OS X应用,而且针对Mac系统做了很多的优化和配置,让你使用起来得心应手,非常方便。但是,可能因为我当年第一次接触Emacs遇到的就是GNU Emacs(Cocoa),到后来再接触了Aquamacs,感觉Aquamacs看起来很不顺眼,而且这种感觉到今天也没有消失掉,我一如既往地对GNU Emacs钟情1

问题来源

很多 *nix 用户对于用户根目录下面同时出现的 .bash_profile .bashrc .bash_login .profile 这几个文件的作用和区别感到迷惑不解,当你想要把一个目录添加进PATH环境变量的时候,可能会不清楚选择哪个文件来操作。当你了解清楚了它们的差异并且明白了它们发生作用的原理之后,回过头来,又要处理Mac OS与传统 *nix 系统的不同2

Mac OS X中的Emacs.app3并不像在其他OS上那样或者像运行在Terminal以及Emacs中的shell,“简单地”继承系统的PATH环境变量,这就需要你额外进行设置。

Shell Configuration

在类UNIX系统中,你可以通过编辑相应shell启动时执行的 "startup" 文件来自定义shell程序的初始化工作。这些文件一般会被称为 "dot files" ,因为它们的名字经常是以 . (dot) 开始。用户自己的 "dot files" 一般都位于用户的根目录(User's Home folder)4下面,需要你自己使用一个文本编辑器(e.g. vim, nano, emacs etc.)来创建。因为它们的名字都是以 . 开始的,当你执行 ls 时,它们不会被显示,你需要加上 -a 参数,即使用 ls -a 来查看所有文件。如果你只需要列出以 . 开始的文件,可以使用命令 ls -ld .*

Shell startup文件当中经常包含下面一些设置:

  • shell execution PATH (determines where the shell will look for executables)
  • MANPATH (determines where the 'man' program will look for man pages)
  • shell aliases and functions (used to save you typing)
  • shell prompts
  • other environment variables

至于哪些初始文件会被shell启动时所读取取决于shell类型(e.g. bash, tcsh etc.)以及其是 login shell 还是 non-login shell5

Login Shell and Non-Login Shell

这里我们简单翻译为登录shell和非登录shell,至于为什么称呼 login shell ,是由于历史原因,当用户启动到文本模式的UNIX系统,输入用户口令登录后,shell便会启动。另外, login shell 也指使用 --login 参数启动的shell(bash --login)。以bash为例,下面的引用来自其man page6:

A login shell is one whose first character of argument zero is a -, or one started with the –login option.

对于bash,当一个登录shell启动时,它将按照下面的顺序读取初始化文件:

  1. /etc/profile
  2. ~/.bash_profile
  3. ~/.bash_login
  4. ~/.profile

首先如果 /etc/profile 文件存在,那么登录shell将首先读取它并且执行其中的命令,然后按照顺序搜索 ~/.bash_profile ~/.bash_login ~/.profile ,并且只读取执行其中的一个。

当一个非登录shell以交互方式启动时,将会读取 /etc/bashrc ,然后寻找并尝试读取执行 ~/.bashrc7

在Mac OS X中,你在Terminal程序中开启的shell都是登录shell,而当你打开一个xterm(在苹果的X11下)窗口时,你得到的则是非登录shell,这是Terminal和xterm在读取初始化文件方面的主要区别。

Maintain Shell Startup Files

更多时候,你不想为登录shell和非登录shell同时维护两个配置文件,比如当你改变PATH环境变量的时候,你可能会希望同时应用于两种shell。通过source命令来做到这点。比如,将PATH和其他设置都放到 .bashrc 里面,再让 .bash_profile “引用” .bashrc ,只需要在 .bash_profile 中添加下面的代码:

if [ -f ~/.bashrc ]; then
   source ~/.bashrc
fi

这样,当开启一个登录shell的时候, .bashrc 会被读取并执行,你在其中所作的设置将会生效。

Mac OS X Runtime Configuration

Mac OS X中,程序运行分为两种,一种是使用终端(console),一种是使用GUI(图形用户界面),环境变量的设置需要根据不同的程序运行情况来分别设置。

当你打开Terminal.app,默认你将启动一个bash shell8,它将像上文介绍到的那样读取初始化文件。

我们知道,一般系统级的环境变量包括 PATH 会在 /etc/profile 中设置,当bash启动的时候会被读取。在Mac OS X中, /etc/profile 会使用path_helper来设置PATH9:

if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
fi

path_helper 会读取 /etc/paths.d/etc/manpaths.d 目录下的文件内容,将它们添加进相应的 PATHMANPATH 环境变量,这些目录下的文件一般通过单独的一行来指定一个路径。在读取这些目录之前,默认的 PATHMANPATH 的值会从文件 /etc/paths/etc/manpaths 中获得。

OS X中的窗口程序则会从用户自己的environment.plist文件中获得环境变量,可以参考这里。文件 ~/.MacOSX/environment.plist 会由Xcode创建,如果你没有安装Xcode的话,可能需要手动创建(注意大小写敏感),之后你便可以通过Property List Editor10来编辑此文件,修改或者保存键值对。environment.plist可以被窗口程序包括Terminal使用来获得系统的各种环境变量,而你在 .bash_profile 或其他dot文件中做的设置将只会作用于bash或相应的shell。 defaults 11程序允许对此文件进行读写12,比如当你修改了enviroment.plist文件,并使当前shell使用其中 PATH 的值,可以这样:

# bash
export PATH=$(defaults read "${HOME}/.MacOSX/environment" PATH)

# tcsh
set path=(`defaults read ~/.MacOSX/environment PATH | tr ':' ' '`)

如果你希望 /etc/profile 中将通过 path_helper 获取的PATH路径的值自动更新到 environment.plist 中去,可以在 /etc/profile 做下面的改动:

#/etc/profile begin
if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
    defaults write $HOME/.MacOSX/environment PATH "$PATH"
fi

一般情况下,你只需要在 .bash_profile 中设置环境变量而不需要改动 .plist 文件。大多数的Mac OS X窗口程序都不需要任何自定义的环境变量,只在程序实际需要一个特定的环境变量的时候,你才可能有必要修改environment.plist。

Emacs.app set PATH

首先,根据上文的一系列介绍,我们可以得到关于Emacs如何从OS X中继承环境变量的三点总结:

  • 当你从shell中启动emacs的时候,emacs将继承shell的环境变量13
  • 当你以GUI方式直接启动Emacs.app,emacs将不会从shell那里继承环境变量,而是从 ~/.MacOSX/environment.plist 继承。
  • 当你从shell中以GUI方式启动emacs14,emacs也会继承shell的环境变量。

下面将介绍如何为Emacs设置 PATH

Setting PATH within Emacs

你可以直接为Emacs设置PATH环境变量,而不用以在OS中设置再让Emacs继承的方式。

使用命令 (getenv "PATH") 将会得到PATH环境变量的值,而 (setenv "PATH" VALUE) 用来将 PATH 设置为指定的字符串。如果你安装了MacPorts,想将其可执行文件目录添加进Emacs的PATH环境变量,可以在Emacs初始化文件中中添加下面的代码:

(setenv "PATH"
        (concat
         (getenv "PATH")
         ":""/opt/local/bin"
         ":""/opt/local/sbin"))

Emacs's exec-path

Emacs拥有一个 exec-path 变量,其值是一个文件目录的列表,当需要在子进程中运行程序的时候,Emacs会通过 exec-path 来寻找可执行文件(e.g. ispell or aspell, diff, grep, shell etc.) 。如果Emacs警告说它无法找到找到 ispellaspellgzip 等可执行文件,可能就是 exex-path 变量的问题。

默认,Emacs会将 (getenv "PATH") 的值赋给 exec-path ,所以 PATHexec-path 的值会一致。但是二者之间又有区别:

  • PATH 的值被Emacs使用如果你在Emacs中开启一个shell(类似于你在终端中运行shell),或者当Emacs调用一些外部命令时(比如通过 M-x compile 执行编译)。
  • exec-path 被Emacs自身使用用来寻找可执行文件当你需要使用它的某些特性的时候,例如拼写检查,diff等。

当你需要在Emacs中设置 PATH 时,你可能也想一起调整 exec-path ,比如你想要设置使emacs直接从shell那里得到 PATH 的值,并且设置 exec-path ,可以将下面的代码添加到Emacs初始化文件中:

(setq *is-a-mac* (eq system-type 'darwin))

(defun string-rtrim (str)
  "Remove trailing white-space from a string."
  (replace-regexp-in-string "[ \t\n]*$" "" str))

(defun set-exec-path-from-shell-PATH ()
  (interactive)
  (let ((path-from-shell
         (string-rtrim
          (shell-command-to-string "$SHELL --login -i -c 'echo $PATH'"))))
    (setenv "PATH" path-from-shell)
    (setq exec-path
          (split-string path-from-shell path-separator))))

(when (and *is-a-mac* window-system)
  (set-exec-path-from-shell-PATH))

可以使用 purcell 写的exec-path-from-shell,做法与上面的代码类似,并进行了一些拓展。

PATH in Emacs shell mode

在Emacs中,当你通过 M-x shell 开启一个shell,比如你设定了 shell-file-name 的值为 /bin/bash ,以使Emacs开启bash shell,这时候,bash是一个非登录shell,所以它会在用户根目录下寻找 .bashrc 来进行初始化,你可能需要将一些自定义的设置放到 .bashrc 中。上文中我们提到,这个shell还会继承Emacs中设置的 PATH ,这样就可能使 PATH 中出现重复的目录,比如为了简化维护,你的 .bashrc 中可能仅仅是下面一行内容:

source ~/.profile

而你在Emacs初始化文件中像这样设置 PATH :

(setenv "PATH"
         (shell-command-to-string "source $HOME/.profile && printf $PATH"))

然后,你用 echo $PATH 来查看 PATH 的值会发现其中很多目录是重复的,让人很不爽。你可以在 .profile 中编写一个函数用来将一个目录添加进 PATH 当且仅当这个目录不在当前的 PATH 中来消除一些重复,或者在 .bashrc 中复制 .profile 的内容,但是去掉相关的 PATH 设置。到目前为止,我还没有找到更好的方法。

Author's Words

本文很多内容参考了 EmacsWikiStackOverFlow.comApple 开发文档的相关主题,转载请注明来源网址: http://blog.galeo.me/path-environment-variable-on-mac-os-x-emacs-app.html

Footnotes:

1

Emacs 24.1马上就要来了,好期待。后面我可能会写一篇文章,介绍Lisp的词法变量和动态变量,因为elisp在Emacs 24.1版本开始支持词法变量,所以可能顺带着介绍一下Emacs 24.1版本的某些变化。

2

虽然Mac OS X经过的UNIX认证,但是一些你在 *nix 系统中习以为常的东西到了OS X里面需要换个脑筋,“Think Different”,然后你会发现OS X在很多方面领先业界。

3

GNU Emacs在Mac OS X上的发行版,详细请参考上文中的链接。

4

~ 来表示(designated by ~)。

5

注意:在命令提示符之后输入shell名称(e.g. bash, tcsh),将会开启一个子shell(sub-shell),子shell将会继承父shell(parent shell)的环境变量,即使通过 --noprofile 参数来禁止shell读取初始化文件。

6

使用命令 man bash 。同样的,可以使用命令 man tcsh 查看tcsh的man page。

7

更多shell启动的细节,请参考man page。

8

可以在 Terminal -> Preferences 里面设置shell启动路径。

9

注意: path_helper 只能通过shell文件来使用,而不能直接手动调用。

10

Property List Editor comes as part of Apple's Developer Tools Xcode 3 package, as of Xcode 4 on Lion, Apple no longer supplies it with its developer tools,不过你可以直接使用Xcode来编辑.plist文件。如果想要在Lion中继续使用Property List Editor,请参考这里,当然还有其他的选择,比如免费的Pref Setter,或者收费的PlistEdit Pro

11

In Terminal, type man defaults for details.

12

注意:当你改动了environment.plist文件之后,你需要重启系统来生效。

13

这一点对于Windows,Linux,Mac均适用。

14

Like this: nohup /Applications/Emacs.app/Contents/MacOS/Emacs &