fish 的简单学习
前言
有关 Fish 的介绍,我在之前已写过一篇文章,详见这里, 这里主要是对 Fish 的语法和功能进行学习,以及说明与 Bash 的不同之处。
使用
通配符
大部分用法和 Bash 一致,有个独特的功能就是连续用两个星号 **
可以匹配任意层级的目录,比如:
$ ls *.jpg
1.jpg 2.jpg 3.jpg
$ ls **.rs
main.rs lib/lib.rs
这样可以把当前目录下的所有 .rs
文件列出来,包括子目录下的。
慎重使用,万一目录层级太深,会导致命令执行时间过长。
重定向
一致的:
>
重定向到文件>>
追加到文件1>
重定向标准输出到文件1>>
追加标准输出到文件2>
重定向标准错误到文件2>>
追加标准错误到文件|
管道
多的功能:
&>
同时重定向标准输出和标准错误到文件,省得写1> out 2>err
自动补全
输入开头的几个字母,按下→键,会自动补全完整命令
如果只想补全接下来的词,按下option键 (MacOS) 即可
变量
双引号中的变量会被解析,单引号中的变量不会被解析
$ echo "My name is $USER"
$ echo 'My name is $USER'
设置变量:
$ set name "wwj"
$ echo $name
取消变量:
$ set -e name ## e for erase
变量导出:把这个变量传递到子进程中
$ set -x name
列表
这是一个特性,实际上 Fish 所有的变量都是列表,包含任意数量的值,或者空。
比如$PWD
只有一个值,实际上是该列表的第一个值。
再比如$PATH
, 就有很多个值了。
可以用count
命令查看列表的长度:
$ count $PATH
19
也可以用索引的方式获得列表中的值:
$ echo $PATH[1]
/usr/local/bin
如果要自己设置一个列表变量,就这样:
$ set a 1 2 3
$ echo $a
列表可以用for
循环:
$ for i in $a
echo "I'm $i"
end
这个列表比较实用的地方在于可以做两个列表的笛卡尔积,比如我们有 10 个样品名称,每个样本有 3 个重复,就可以这样操作:
$ set samples A B C D E F G H I J
$ set rep 1 2 3
$ set my_list $samples$rep
$ echo $my_list
A1 B1 C1 D1 E1 F1 G1 H1 I1 J1 A2 B2 C2 D2 E2 F2 G2 H2 I2 J2 A3 B3 C3 D3 E3 F3 G3 H3 I3 J3
$ for item in $my_list
ll $item.fastq.gz
end
命令替换
用$()
或 ()
包裹命令,可以把命令的输出作为变量的值。
$ echo In (pwd), running $(uname)
In /Users/wjwei/code/wgatools, running Darwin
最常用的就是设置变量:
$ set sample_list (ls *.fastq.gz|cut -f1 -d '.')
当然如果你是在双引号内,就要加上$
符号:
$ echo "testing_$(date +%s).txt"
testing_1703690857.txt
$ echo "testing_(date +%s).txt"
testing_(date +%s).txt
这里值得注意的一个点是,有些命令的输出会带有换行符,那么在 Linux 中,是逐行操作的,所以换行符有时候会引起一些问题
基于这个问题,Fish 默认的命令替换是会去掉换行符的。但是如果我就不想换,怎么样呢?
这时候可以用双引号
把命令替换包裹起来,这样就会把换行符去掉。
$ echo "first line
second line" > myfile ## 这里有个换行符
$ set myfile "$(cat myfile)"
$ printf '|%s|' $myfile
|first line
second line|
退出码
Fish 会把上一个命令的退出码保存在$status
变量中,而不是 Bash 中的$?
。
$ false
$ echo $status
1
条件判断
if else
语法略有不同,if
后面不需要加then
,else
后面不需要加fi
。
if grep fish /etc/shells
echo Found fish
else if grep bash /etc/shells
echo Found bash
else
echo Got nothing
end
判断条件
和 Bash 不一样的地方在于,Fish 中的判断条件不需要加[]
,也不需要加$
符号,而是用test
命令来判断。
if test "$fish" = "flounder"
echo FLOUNDER
end
# or
if test "$number" -gt 5
echo $number is greater than five
else
echo $number is five or less
end
# or
# This test is true if the path /etc/hosts exists
# - it could be a file or directory or symlink (or possibly something else).
if test -e /etc/hosts
echo We most likely have a hosts file
else
echo We do not have a hosts file
end
这种语法见仁见智吧,我个人觉得学习成本也不高,也比较好记忆,可能一些 Bash 老人会不习惯吧。
循环
while
最常用:
while read line
echo $line
end < myfile.txt
for
最常用:
for i in *.fa
echo $i
end
便捷添加环境变量
Fish 有个很方便的命令,可以把当前目录添加到环境变量中,这样就可以直接运行当前目录下的可执行文件了。
$ fish_add_path your_bin_path
小坑
在用管道作为输入的时候,之前用 Bash 是这样的用法:
$ while read l;do echo $l;done < ls *.csv
但是 Fish 这里不一样,最后的命令要用 psub 来包裹起来:
$ while read l;echo $l;end < (ls *.csv|psub)
工具
Fish 有一些包装好的工具命令,还是挺好用的。
read
其实这个命令在 Bash 中也有,但是 Fish 中的用法更多更实用:
- 最常用简单的:
$ echo "AAAA" | read foo
$ echo $foo
AAAA
$ while read l;echo $l;end < myfile.txt
- 用
-d
来指定分隔符,用-l
来分配局部变量
ls *.csv |
while read -d . -l a b ;
echo prefix is `{$a}` and surfix is `{$b}`;
end
prefix is `abc` and surfix is `csv`
prefix is `edf` and surfix is `csv`
这个例子中,-d
指定了分隔符为.
,-l
指定了局部变量为a
和b
, 相当于把文件名的前缀和后缀分别赋值给了a
和b
。
string
这个命令可以用来处理字符串
string collect
把多个输入拼接成一个输出
$ count (echo one\ntwo\nthree)
3 ## echo 打印了三行
$ count (echo one\ntwo\nthree | string collect)
1 ## string collect 把三个元素拼接成了一个字符串
string join
这个好用,可以把列表中的元素用指定的分隔符拼接起来
$ seq 10 | string join ,
1,2,3,4,5,6,7,8,9,10
$ string join ':' a b c
a:b:c
string length
计算字符串的长度,实际上没用过
$ string length "hello world"
11
string match
这个好用,可以用来匹配字符串,支持正则表达式
$ string match '?' a
a
$ string match 'a*b' axxb
axxb
$ string match -i 'a??B' Axxb
Axxb
$ echo 'ok?' | string match '*\?'
ok?
$ string match 'foo' 'foo1' 'foo' 'foo2'
foo ## 只会输出匹配的第一个
$ string match -e 'foo' 'foo1' 'foo' 'foo2'
foo1
foo
foo2
## -e 输出包含的所有匹配
$ string match 'foo?' 'foo1' 'foo' 'foo2'
foo1
foo2
这个用法有很多,可以配合test
来判断字符串是否匹配,也可以配合for
来遍历匹配的字符串。
string repeat
重复字符串
$ string repeat -n 2 'foo '
foo foo ## 重复两次
$ echo foo | string repeat -n 2
foofoo ## 重复两次
$ string repeat -n 2 -m 5 'foo'
foofo ## 重复两次,最多输出 5 个字符
$ string repeat -m 10 'foo'
foofoofoof ## 重复无限次,最多输出 10 个字符
就个人而言用的不多
string replace
这个用法和match
一样,也可以用来匹配字符串,支持正则表达式,但是它会把匹配到的字符串替换成指定的字符串。
$ string replace is was 'blue is my favorite'
blue was my favorite
$ string replace 3rd last 1st 2nd 3rd
1st 2nd last
$ string replace -a ' ' _ 'spaces to underscores'
spaces_to_underscores
我会把他当作脚本中来简单替代sed
, 来替换一些字符串。
string split
这个命令和join
相反,可以把字符串按照指定的分隔符拆分成列表
比如我最常用的就是看一个 tsv 文件的表头,我习惯把横的表头输出成竖的,也可以输出序号:
$ head -n1 00_tissue_info.tsv| string split \t|less -N
1 Reference
2 Version
3 Analysis
4 TissueClass
5 TissueStage
6 TissuePosition
7 TissueID
8 SvgClass
9 TissueDesc
string sub
这个命令可以拿出字符串中的一部分,支持正则表达式,也比较好用
$ string sub --length 2 abcde
ab ## 取前两个字符
$ string sub -s 2 -l 2 abcde
bc ## 从第二个字符开始取两个字符
$ string sub --start=-2 abcde
de ## 取倒数两个字符
$ string sub --end=3 abcde
abc ## 取到第三个字符
$ string sub -e -1 abcde
abcd ## 取到倒数第一个字符
$ string sub -s 2 -e -1 abcde
bcd ## 从第二个字符开始取到倒数第一个字符
string trim
这个简单,就是去掉字符串两端的空格,当然也可以指定去掉的字符
$ string trim ' abc '
abc
$ string trim --right --chars=yz xyzzy zany
x
zan ## 去掉右边的 y 和 z
```shell
综上所述,`string` 命令可以完成很多字符串的操作,有点像三剑客 `grep`, `sed`, `awk` 的结合体,但是使用场景主要集中在管道的输入输出或者作为参数传递给其他命令。具体得看个人的使用习惯了。
math
这个命令用来做数学运算,用法也很简单,就是把数学表达式作为参数传递给math
命令即可。
$ math 1+1
2
$ math 8-6
2
$ math 2\*3 ## 乘法要加转义符
6
$ math 18/3
6
$ math \(2+5\)\*3 ## 括号要加转义符
21
$ math "(2+5)*3" ## 也可以用双引号来包裹
当然也有其他数学函数,比如sin
, cos
, tan
, log
, sqrt
等等,具体可以查看官方文档, 我个人觉得用的不多。
abbr
abbr 是 abbreviation 的缩写,这个命令可以用来设置缩写,但是又和alias
不同的地方在于,他会补全完整的命令,比如:
$ abbr zzz bjobs -w -noheader
$ zzz # 输入 zzz,但是不会显示 zzz,而是会显示完整的命令
$ bjobs -w -noheader
以上面这个例子为例子,我输入zzz
后,再按一个空格,会自动补全成bjobs -w -noheader
, 并且光标会自动跳到命令的末尾,这样就可以直接输入其他参数了。当然了,你也可以指定光标停留的位置,比如:
abbr -a zzz --set-cursor "bjobs -w -noheader|rg %"
这个例子就是把光标停留在rg
后面,这样就可以直接输入要搜索的字符串了。
这种方法相对函数的输入参数而言,稍微灵活一点。
结语
整体而言,Fish 还是提供了很好的用户体验,语法也比较简单,但是有些命令的用法和 Bash 不一样,学习成本见仁见智,还是那句话,自己习惯的工具才是最好的工具。