新时代的 shell 改进的地方大体上分为两个部分:语法交互

  1. 如何使用 shell(1/3)—— shell 兼容和历史
  2. 如何使用 shell(2/3)—— 新时代的 shell
  3. 如何使用 shell(3/3)—— 配置 zsh

Fish (Friendly Interactive shell)

这是一个划时代的 shell ,开创了全新 shell 用户交互的体验。语法高亮,根据 manpage 来自动补全,自动建议都是它的首创

2B 青年用 bash,普通青年用 zsh,文艺青年用 fish。

fish 的默认配置是真的好用,配置文件是: ~/.config/fish/config.fish

fish 通过函数来设置行为: fish 完全使用函数定制行为,你可以通过添加一些特殊的函数定制 fish 的行为,例如 prompt,fish 没有 PS1 这类特殊变量,而是使用函数。

语法高亮,自动建议


# 语法高亮
grep

# 路径建议
cat /bin/hostname

# 参数建议
grep --invert-match

路径折叠 ~/D/E/detail 避免路径过长

易懂的语法,这个不是 POSIX shell 兼容,自己有自己的语法

不支持 here doc (当然 here string 也不支持):

cat << EOF > file.txt
# file content
...
EOF

官方的解释是因为你可以用其它命令替代,例如:

printf "\
# file content
...
" > file.txt

printf 但写法也可以在 bash 里面使用,但在细节上和 fish 是有区别

  • fish 使用 $status 获取上一个命令的退出状态,而不是 $?
  • fish 里面的数组计数是从 1 开始的,而不是大多数程序的 0
  • fish 不使用 && 以及 || 语法来组合命令,而是使用 andor 命令:但 fish 3.x 已经支持了 &&, ||
  • fish 使用 echo (echo 233) 来代替 echo `echo 233`

fish_config 的奇葩命令,这个会开启一个 http server ,在网页上配置。作为一个 shell ,我一直都觉得打开浏览器去设置 shell 就比较奇怪

设置变量

很多人都觉得 fish 设置变量太奇怪了。实际上是 POSIX shell 更奇怪才对

回顾一下 Shell 变量的作用域可以分为三种:

  • 有的变量只能在函数内部使用,这叫做局部变量(local variable)
  • 有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable)
  • 而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)

我们通常知道的正常编程语言的变量:

  • g / --global 全局变量(默认),在本次运行环境中有效
  • l / --local 局部变量,仅在当前作用域中有效
gvar="Global content changed"
# 换成 fish 写法
set -g gvar "Global content changed"

local lvar="Local content"
# 局部变量 fish 写法
set -l lvar "Local content"

环境变量(环境变量其实是普通的变量是否具有 export 属性):

  • x / --export 该变量可以传递给子进程
  • u / --unexport (默认)该变量不可以传递给子进程
# 平时最常使用的命令
export foo=xxx
declare -x foo=xxx
typeset -x foo=xxx

# fish 的写法
set -x foo xxx

# 去除环境变量(或者说去除变量的 export 属性)
declare +x foo=xxx
typeset +x foo=xxx

# fish 的写法
set -u foo xxx

declare 的操作符有 +, - 两种:- 代表启用这个属性,+ 代表取消这个属性。是的,我没有写错,他就是反的

export 本质上就是 declare -x 命令

按照 GNU bash 文档的说法:提供 typeset 命令是为了与 Korn shell 兼容。它和 declare 命令是一样的。

POSIX shell 储存的就是字符串,整数和数组只是一个特殊属性(fish 的变量属性不能添加和删除)。 这几个是比较常用的(完整的去看官方的手册,还可以进行大(u upcase)小(l lowcase)写转换等)

  • a 索引数组变量
  • A 关联数组变量
  • f 函数名
  • i 该变量将被视为整数
  • r 只读,不能更改不能 unset
  • t 调试,追踪这个变量。(大概是 bash 特有的)
  • x 导出变量变成环境变量

持久化和清除变量:

  • U / --universal 通用变量,当前用户下全部有效,并持久化(会保存)
  • e / --erase 清除一个变量

U 相当于写在配置文件里,e 相当于 unset 命令

比如: set -Ux EDITOR vim 会让环境变量全局生效并持久化保存,即使 shell 重启也会保留。 推荐使用 set -Ux 保存常用环境变量,会自动保存到 ~/.config/fish/fish_variables 里面

对数组的操作:

  • a / --append 将值追加到变量的当前数组。 可以与 --prepend 一起使用,以同时添加和添加。 分配给可变切片时,不能使用此功能。
  • p / --prepend 这些值被预先设置为该变量的当前值集。 可以将它与 --append 一起使用,插入到数组最前面。 分配给可变切片时,不能使用此功能
PATH="$PATH:$HOME/go/bin"
PATH=$PATH:~/go/bin
# fish 的写法
set -a PATH ~/go/bin/

PATH="$HOME/go/bin:$PATH"
PATH=~/go/bin:$PATH
# fish 的写法
set -ap PATH ~/go/bin/

~$HOME 并不是等价的,PATH="$PATH:~/go/bin" 这种写法就是不行的。 "" 里面只会解释变量,不会对 ~ 展开。 还有 ~ 必须出现在表达式的最前面,否则它是普通的字符。 : 后面会被当成独立的表达式,所以也是正常的。推荐尽可能使用 $HOME

NODE_ENV=production node index.js 是无法在 fish 内使用的,可以换成一个更通用的写法,使用 env 命令:

env NODE_ENV=production node index.js

配置框架和插件管理

  • Oh-my-fish

类似于 oh-my-zsh 的配置框架,顺便附带了包管理器的功能

omf install [<name>|<url>]
omf list

当然,fish 默认已经非常好用了。不需要什么配置了,但还是可以从 oh-my-fish 配置里面找到一些更好的用法(比如类似 Elvish 的导航模式)

  • Fisher 这个原来叫 fisherman

这个是一个极为精简的 fish 插件管理器

Elvish

tuna 曾经的会长肖骐开发的 shell

截止到目前 Elvish 还没有发布 1.0 版

Elvish 对于交互的改进

  • Ctrl-R 对于搜索历史做了优化
  • Ctrl-L 显示目录栈。相当于列表选择 dirs -v 进行跳转
  • Ctrl-N 导航模式(Navigation mode) 这个是我最喜欢的功能,类似于 ranger 或 nnn 之类的文件管理功能,这个功能很棒

elvish-navigation-mode

Elvish 也自己集成包管理

use epm
epm:install <package name>

管道应该传递什么?

这里来自 Elvish 作者 的: elvish 的设计和实现 (零) shell 的优势和缺陷#管道的缺陷

Unix 的管道中传输的是一个接一个的字符,没有别的结构。 一个字符能表达的信息很有限;为了克服这一困难,大多数 Unix 工具把一行视为一条数据。 例如 sed 可以针对每一行进行替换等操作,grep 也是找出包含指定字符的所有行。这种「结构」不是管道内在的,而是工具约定的。

这样一来,数据本身包含换行符时就会有问题。例如这段删除当前目录及其任意层子目录下 .bak 文件的代码(不理解这段代码可以看 这里):

find . -name '*.bak' | xargs rm

如果当前目录下有个文件名叫 a\n.bak ,那么 rm 就会尝试删除文件 a 和文件 .bak,而不是 a\n.bak 。这是因为 xargs 无法区分分隔数据的换行和数据内部的换行。

通常的做法是,让 find\0 分隔输出的各项,并且让 xargs 在读入时也用 \0 分隔:

find . -name '*.bak' -print0 | xargs -0 rm

在文件名的问题上我们还比较幸运,因为 Unix 文件名不允许包含 \0 。那如果我们要处理的数据里刚好也可能包含 \0 呢?

这种纯文本的处理总是会出现有歧义的情况,为了解决这种情况,所以要去传输个「类型」的字段

我也不知道该如何描述,借用 elvish 作者的描述:分成 穷管道富管道

  • 穷管道就是传统的 raw 传输方式(POSIX shell 的传输方式)
  • 富管道是一个有类型的句柄(一段数据加上类型,这就是两个字段了,或者说传递对象)

对于都是内置命令的情况传递使用富管道,和外部命令交互传递穷管道保持兼容

富管道里面传输的是对象,比较有名的 PowerShell 也是这样管道里面传输 dotnet 对象

Ion Shell

这是一群疯狂的人造的轮子,Redox OS 的默认 shell

重点都在语法和执行效率上,用起来和默认 bash / zsh 差不多,不兼容 POSIX shell。

Ion 对于数据重定向方面使用了新的语法 ^ ,我觉得这个改进或许并不好。或许配合上 Redox OS 这个也许是很不错的

标准的 IO 有三种:

  • stdin 标准输入 /dev/stdin
  • stdout 标准输出 /dev/stdout
  • stderr 标准错误 /dev/stderr

输出信息有 stdoutstderr 两种,可以分开来重定向

Name POSIX shell Fish Elvish Ion PowerShell Nushell
从文件到标准输入 < < < < < <
将输出写入到一个文件 > , 1> > , 1> > , 1> > > , 1> > , 1>
输出追加写入到一个文件 >> , 1>> >> , 1>> >> , 1>> >> >> , 1>> >> , 1>>
将错误写入到一个文件 2> ^ , 2> 2> ^> 2> 2>
将错误追加写入到一个文件 2>> ^^ , 2>> 2>> ^>> 2>> 2>>
全部写入到一个文件 &> &> 不支持 &> 不支持 &>
输出和错误写入到一个文件 > file 2>&1 > file 2>&1 > file 2>&1 不支持 可以,但语意不同 > file 2>&1

全部写入到一个文件输出和错误写入到一个文件command >file 2>&1 或追加写入 command >>file 2>&1)是完全等价的

Fish 还支持 >? , ^? 如果文件存在则不写入输出,ion 还支持多个重定向 command > stdout ^> stderr &> combined

文件描述符 FD(File Descriptor)

fish 和 ion 的 ^ 描述 stderr 语法看起来好多了。不过,为什么 POSIX shell 要用 12 来区分 stdoutstderr

在 Unix 系统当中,一切皆文件,每个进程默认会打开三个文件作为输入,输出和错误,前三个文件描述符为(顺序是恒定的):

  • 0stdin
  • 1stdout
  • 2stderr

通过这里,就很容易理解, Unix 系统的标准输入输出,本质上就是三个文件的读写, 1> , 2> 指的的是文件描述符。所以说用 ^ 来描述错误或许并不合理(这是一个约定,并非规则)

打开文件会分配 3 往后的唯一的 FD,也可以手动指定。(可以在 /proc/self/fd 里面看到 FD 和文件的对应关系)

# std.sh

# stdin 自己对自己的输入
echo 000 >&0

# 打印 stdin 的数据
echo -n <&0

# stdout
echo 111 >&1

# stderr
echo 222 >&2

# 打开文件, FD 设置为 3
exec 3<> /tmp/foo

# 刚打开了新的文件,可以看到映射关系
ls -l /proc/self/fd

# 写入内容
echo 333 >&3

# 关闭文件(关闭 3 这个 FD)
exec 3>&-

这里注意:sh -c "echo 000 >&0; echo -n <&0" 在脚本内部传递数据, stdin 一直处于交互输入(使用 cat 会由于读不到文件结尾而阻塞)

而管道会写入 stdin 数据之后会有终止符 EOF ,这下面这种写法

  • echo 000 | sh -c "cat <&0"
  • sh -c "cat <&0" <<< 000
  • echo 000 | sh -c "cat /dev/stdin" 当然,/dev/stdin<&0 是一样的

使用 ls 查看 fd 时要注意,由于 ls 的工作原理,会产生一个临时的 fd

ls -l /proc/self/fd
total 0
lrwx------ 1 metal metal 64 May  5 13:21 0 -> /dev/pts/89
lrwx------ 1 metal metal 64 May  5 13:21 1 -> /dev/pts/89
lrwx------ 1 metal metal 64 May  5 13:21 2 -> /dev/pts/89
lr-x------ 1 metal metal 64 May  5 13:21 3 -> /proc/2663987/fd

PowerShell(pwsh)

PowerShell 是一个来自 Microsoft 的任务自动化和配置管理框架,由命令行 shell 和相关的脚本语言组成。

最初,它只是一个 Windows 组件,被称为 Windows PowerShell,在 2016 年 8 月 18 日,随着 PowerShell Core 的引入,目前分成两个版本:Windows PowerShell 和 跨平台的 PowerShell Core

PowerShell 离开了 Windows 似乎水土不服,一点也不好用。或许是我不会用的关系

PowerShell 的管道传递的是 .net object,而不是 raw 字符串

拥有的现在化的语法,类似 python 或 ruby 一类脚本语言的操作。所有的东西都看作 dotnet 对象(这点有点和 ipython, pry 类似)。可以去理解常见的数据结构

NuShell

和 PowerShell 思想很像,也可以理解数据结构,或者所是 Linux 下的 “PowerShell”

看这个默认的 ls ,这才是一个现在代化的 shell 应该有的样子。当然,这个 ls 是内置命令。Nushell 内置了大量的常用命令,给人眼前一亮的感觉

/Users/metal/Code/blog(master)> ls
────┬──────────────┬──────┬──────────┬──────────────
 #  │     name     │ type │   size   │   modified
────┼──────────────┼──────┼──────────┼──────────────
  0 │ 404.html     │ File │    398 B │ 3 months ago
  1 │ Gemfile      │ File │   1.2 KB │ 3 months ago
  2 │ Gemfile.lock │ File │   2.2 KB │ 3 months ago
  3 │ README.md    │ File │    322 B │ 3 months ago
  4 │ _config.yml  │ File │   2.0 KB │ 2 months ago
  5 │ _drafts      │ Dir  │     96 B │ 3 months ago
  6 │ _includes    │ Dir  │    160 B │ 3 months ago
  7 │ _layouts     │ Dir  │     96 B │ 4 days ago
  8 │ _posts       │ Dir  │   1.9 KB │ 4 days ago
  9 │ _site        │ Dir  │   1.3 KB │ 4 days ago
 10 │ about.md     │ File │   1.6 KB │ 3 months ago
 11 │ assets       │ Dir  │    224 B │ 3 months ago
 12 │ equipment.md │ File │   8.3 KB │ 3 months ago
 13 │ favicon.ico  │ File │ 238.1 KB │ 3 months ago
 14 │ index.md     │ File │    239 B │ 3 months ago
 15 │ links.md     │ File │   1.7 KB │ 2 months ago
 16 │ new.rb       │ File │   1.1 KB │ 3 months ago
 17 │ projects.md  │ File │   7.7 KB │ 3 months ago
 18 │ vendor       │ Dir  │     96 B │ 3 months ago
────┴──────────────┴──────┴──────────┴──────────────

nushell 具有极其丰富的数据结构,就如同现在化的编程语言一样

支持了常用的数据格式:

  • json
  • yaml
  • toml
  • xml
  • csv
  • ini

有了更合理的语法:

open people.txt | lines | split column "|" | str trim

甚至集成了类似 curl 的命令(使用 nushell 理解并解析 xml)

/home/metal/Code> fetch https://a-wing.top/feed.xml | get feed.children.subtitle.children.0
新一的个人博客

把 REPL(Read–Eval–Print Loop) 当 shell 用

现在化的编程语言的交互式的环境,比如 IPythonPry 当登录 shell 用好像也是可以的。 这不是传统意义上的 shell,但是经常有人建议用它做登录 shell。但启动实在是太慢了

还有,作为一个 shell,大部分时间都会被用来执行外部命令,每次都多打一个字符是很累的。

或许我们可以融合一下普通的 shell 和 REPL。比如:xonsh 融合 python 和 shell,既可以用 shell 写法,也可以直接使用 python。

类似的,也有融合其他语言的,比如融合了 Node.jsfresh-shell

Reference

elvish 的设计和实现 (零) shell 的优势和缺陷

管道、awk 和 JSON

elvish 的设计和实现 (二) 语法基础和词法

使用 fish 的一些注意点

set - display and change shell variables - fish-shell 3.2.2 documentation

Ion Documentation

FileDescriptor

What is the file descriptor 3 assigned by default?