如何使用 shell(2/3)—— 新时代的 shell
- Fish (Friendly Interactive shell)
- Elvish
- Ion Shell
- PowerShell(pwsh)
- NuShell
- 把 REPL(Read–Eval–Print Loop) 当 shell 用
- Reference
新时代的 shell
改进的地方大体上分为两个部分:语法 和 交互
- 如何使用 shell(1/3)—— shell 兼容和历史
- 如何使用 shell(2/3)—— 新时代的 shell
- 如何使用 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 不使用
&&
以及||
语法来组合命令,而是使用and
和or
命令:但 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 也自己集成包管理
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
输出信息有 stdout
和 stderr
两种,可以分开来重定向
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 要用 1
和 2
来区分 stdout
和 stderr
?
在 Unix 系统当中,一切皆文件,每个进程默认会打开三个文件作为输入,输出和错误,前三个文件描述符为(顺序是恒定的):
-
0
:stdin
-
1
:stdout
-
2
:stderr
通过这里,就很容易理解, 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 用
现在化的编程语言的交互式的环境,比如 IPython
,Pry
当登录 shell 用好像也是可以的。
这不是传统意义上的 shell,但是经常有人建议用它做登录 shell。但启动实在是太慢了
还有,作为一个 shell,大部分时间都会被用来执行外部命令,每次都多打一个字符是很累的。
或许我们可以融合一下普通的 shell 和 REPL。比如:xonsh 融合 python 和 shell,既可以用 shell 写法,也可以直接使用 python。
类似的,也有融合其他语言的,比如融合了 Node.js
的 fresh-shell
Reference
elvish 的设计和实现 (零) shell 的优势和缺陷
set - display and change shell variables - fish-shell 3.2.2 documentation