很多macOS的用户开发者可能不知道他们一直在使用非常老版本的Bash shell,部分原因是很多用户使用的是zsh。 但是,升级Bash其实是非常有意义的,除了能够使用新特性之外,还能够避免一些意外的错误。

写在前面:我在给一些项目如kubernetes提PR时,项目中脚本经常执行不成功。还有进行Hack工具开发completion功能时, 测试completion一直无法成功。问题的根源就是Bash版本的问题。虽然在日常开发过程中,我一直使用zsh, 但是安装新版本Bash对于本地开发调试十分方便。

Mac默认的Bash

执行以下命令查看macOS内置Bash版本:

1
2
3
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.

可以发现,macOS使用的是3.2版本的GNU Bash,发行于2007年!到目前为止,即使是最新的MacOS,默认Bash都是这个版本。

苹果在其操作系统上使用如此旧版本的Bash和许可(licensing)有关。Bash从4.0版本就使用GNU General Public License V3, 但是苹果并不想支持这个许可,相关讨论可以查看这里这里, 3.2版本的GNU Bash使用的GPLv2许可,这是苹果能够接受的许可,所以苹果一直使用该版本Bash。

这意味这整个世界都在使用新版本的Bash,可是macOS的用户却仍在使用10多年前的版本,到目前为止,GNU Bash的最新版本是5.0, 发布于2019年1月,本文将指导大家如何升级到最新版本。

为什么需要升级

很多人要问Bash 3.2使用正常,为什么还需要升级到最新版本呢?对我个人而言,执行一些第三方脚本会出现错误,如kubernetes staticcheck.sh, 还有一个重要的原因是Programmable completion补全,Bash自动补全功能大家都有使用过,您可以自动补全命令、文件名和变量,方法是先键入, 然后按Tab键自动完成当前单词(按两次显示列表),这是Bash的自动补全。

但是可编程式补全功能更加强大,因为它允许依赖于上下文的特定于命令的补全。比如输入cmd -[Tab][Tab], 仅会显示该命令接受的选项,再比如输入cmd host rm [tab][tab]会显示特定配置文件中host列表, 可编程式补全能够实现这些功能。

可编程式补全需要定义补全逻辑文件,通常以completion scripts存在,由命令的开发人员维护。 这些补全脚本需要被source才能够开启。

问题是Bash的可编程完成特性从3.2版本开始就得到了扩展,而且大多数补全脚本都使用了这些新特性,这也是很多补全功能在Mac中不能正常工作的原因, 如果你不升级,你将不能使用这种补全功能。

升级新版本能够解决这些问题,如果你需要详细了解macOS Bash的可编程式补全功能,你可以参考Programmable Completion for Bash on macOS

如何升级

macOS系统升级最新版本Bash,你需要做一下三件事:

  1. 安装最新Bash
  2. 将最新Bash添加到“白名单”
  3. 设置为默认shell(可选)

每一步都十分简单。

下面的说明并不更改旧版本的Bash,而是安装新版本并将其设置为默认shell。这两个版本将同时存在于您的系统中,但是您可以从此忽略旧版本。

安装

当然建议通过Homebrew安装:

1
brew install bash

等待安装完成,验证你系统的老版本的Bash和刚下载的新版本:

1
2
3
$ which -a bash
/usr/local/bin/bash
/bin/bash

第一个是新版本,第二个是内置3.2版本:

1
2
3
4
5
6
7
8
9
$ /usr/local/bin/bash --version
GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.

因为新版本的所在目录(/usr/local/bin)在内置版本所在目录(/bin)之前(PATH环境变量),所以当你输入bash, 将会使用新版本。

1
2
3
$ bash --version
GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)
...

到目前为止,已经默认新版本Bash了。(很多用户到这步就ok了,没有必要进行如下配置)

白名单

UNIX包括了一些安全特性限制哪些shell能够做为login shell, 你需要将信任的shell添加到/etc/shells中,一个shell只有在这列表才能够作为默认shell。

1
$ sudo vim /etc/shells

将/usr/local/bin/bash shell添加到该文件中,添加完成后的内容和一下内容类似:

1
2
3
4
5
6
7
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/local/bin/bash

设置为默认shell

到这里,你打开一个新的terminal window仍然使用的内置Bash,因为默认shell仍然是内置版本Bash。 你已通过以下命令修改默认shell。

1
$ chsh -s /usr/local/bin/bash

重新打开一个终端窗口,默认shell已经修改完成。

需要注意的点

脚本中的用法

之前提到过,以上操作并不是升级内置Bash版本,而是安装了一个新版本,所以系统存在两个版本的Bash。 在shell脚本中,你经常有一个像下面一样的shebang行。

1
2
#!/bin/bash
echo $BASH_VERSION

可以发现脚本指定的是/bin/bash,执行脚本(通过路径而不是bash)得到的输出可以发现使用的是内置Bash。

如果你需要使用新版本的Bash,你需要这样写。

1
2
#!/usr/local/bin/bash
echo $BASH_VERSION

很多人发现,这两个脚本具有不同的shebang行,所以没办法迁移,如果需要在其他系统执行,需要修改脚本文件。 通过比较以上两个脚本,你可以这些编写shebang行。

1
2
#!/usr/local/bin/bash
echo $BASH_VERSION

这是推荐的shebang行的写法,它会使用PATH中第一个发现的Bash版本执行脚本文件。

为什么不使用软链接

我们可不可以直接删除旧版本的Bash,把新版本的Bash放到旧版本的位置呢?比如,创建一个软链接/bin/bash指向新版本呢, 就像下面一样:

1
2
$ sudo rm /bin/bash
$ sudo ln -s /usr/local/bin/bash /bin/bash

这样shebang行#!/bin/bash将会使用新版本Bash,那为什么不能做呢?

您可以执行此操作,但必须避开称为系统完整性保护(SIP)(Wikipedia)的macOS安全功能。 此功能甚至禁止root用户对某些目录进行写访问(这就是为什么它也称为“无根”)的原因。 这些目录在此处列出,并包含/bin。这意味着即使以root用户身份,您也不能执行上述命令, 因为不允许您从/bin中删除任何内容或在/bin中创建任何文件。

解决方法是禁用SIP,在/bin中进行更改,然后再次启用SIP。可以根据此处的说明来启用和禁用SIP。它要求您将计算机引导到恢复模式, 然后使用csrutil disable和csrutil enable命令。如果您想完全取代旧的Bash版本,还是要满足于同时使用两个Bash版本, 则取决于您自己。

一句话

  1. macOS Catalina系统,zsh将作为默认shell。

译自

upgrading bash on macOS