fish shell 脚本编写指南
该文章翻译自FISH SHELL SCRIPTING MANUAL,因本人才疏学浅,难免有错误/不通顺的地方,还望读者在评论区不吝赐教。
通过例子学习如何编写 fish shell 脚本。
脚本顶部的 shebang 行
为了在终端中运行 fish 脚本,你需要做如下两件事:
- 将以下 shebang 行添加到脚本顶部:
#!/usr/bin/env fish
。 - 使用以下命令将文件标记为可执行:
chmod +x <你的 fish 脚本文件名>
。
如何设置变量
注意,在 fish 中可以赋给变量的所有类型的值都是字符串,没有布尔值、整数或浮点数等概念。下面是一个给变量赋值的简单示例。这里有关于此的更多信息。
1 | set MY_VAR "some value" |
你可以做的最有用的事情之一就是把 shell 中执行命令的输出存储到一个变量中。在你测试某个程序或命令是否返回了一些需要你执行其它命令的值(使用字符串比较、 if 语句和 switch 语句)时,这是很有用的。下面是一些这样做的例子。
1 | set CONFIG_FILE_DIFF_OUTPUT (diff ~/Downloads/config.fish ~/.config/fish/config.fish) |
变量作用域: local,global,global-export
有时你需要把变量导出到子进程,有时你需要把变量导出到全局作用域。还有一些时候,你希望把变量限制在正在编写的函数的局部作用域内。fish 文档中的 set
函数部分有关于此的更多信息。
- 把变量限制在函数的局部作用域内(即使有同名的全局变量)应该使用
set -l
。这种类型的变量不在整个 fish shell 可用。一个例子是仅用于保存函数范围内某个值的局部变量,如set -l fname (realpath)
。 - 使用
set -x
导出变量(仅在当前 fish shell 可用)。例如在一个运行在crontab
headless 环境的 fish 函数中为 X11 会话设置DISPLAY
环境变量。 - 使用
set -gx
全局导出变量(可用于操作系统的任何程序,而不仅仅是当前运行的 fish shell 进程)。例如为本机上运行的所有程序设置JAVA_HOME
环境变量。
列表
以下是一个在变量中追加值的例子。默认情况下 fish 变量是列表。
1 | set MY_VAR $MY_VAR "another value" |
这是创建列表的方法。
1 | set MY_LIST "value1" "value2" "value3" |
存储运行命令的返回值
下面是一个将命令执行返回的值存储到变量中的示例。
1 | set OUR_VAR (math 1+2) |
因为所有的 fish 变量都是列表,你可以使用 [n]
运算符访问单个元素,其中n=1
表示第一个元素(而不是0)。下面是一个例子。负数表示从末尾访问元素。
1 | set LIST one two three |
范围
你可以使用变量/列表的范围,接着上面的例子。
1 | set LIST one two three |
如何编写 for 循环
因为变量默认包含列表,所以很容易遍历他们。这是一个例子:
1 | set FOLDERS bin |
你也可以把set
命令放在同一行以简化上述代码,就像这样:
1 | set FOLDERS bin .atom "my foldername" |
还可以把整个 for 语句放到单行,像这样:
1 | set FOLDERS bin .atom "my foldername" |
如何编写 if 语句
编写 if 语句的关键是使用test
命令对某个表达式求布尔值。可以是字符串比较,甚至是测试文件或文件夹的存在。下面是一些例子。你还可以使用not
运算符作为test
的前缀,以检查逆条件。
常用的条件
检查数组的大小。$argv
包含从命令行传递给脚本的参数列表。
1 | if test (count $argv) -lt 2 |
变量的字符串比较。
1 | if test $hostname = "mymachine" |
检查文件是否存在:
1 | if test -e "somefile" |
检查文件夹是否存在:
1 | if test -d "somefolder" |
检查文件通配符是否存在与文件和文件夹的检查略有不同。原因在于 fish 处理通配符的方式——在对它们执行任何命令之前,fish 会首先将其展开。
1 | set -l files ~/Downloads/*.mp4 # 这个通配符表达式被展开,包含了实际的文件 |
一个在上述示例中使用not
运算符的示例:
1 | if not test -d "somefolder" |
程序,脚本或函数的退出码
使用退出码的想法是,你的函数或者整个 fish 脚本可以被能够理解退出码的其他程序使用。换句话说,可能会有 if 语句使用退出码来确定某个条件。这是与其它命令行程序一起使用的一个非常常见的模式。退出码不同于函数的返回值。
下面是一个使用git
命令的退出码的示例:
1 | if (git pull -f --rebase) |
一个测试命令执行是否没有错误的示例:
1 | if sudo umount /media/user/mountpoint |
你还可以检查$status
变量的值。fish 会在执行命令后将返回值存储在这个变量中。这儿有关于此的更多信息。
当你写函数时,可以使用以下关键字来退出函数或循环:return
。return
后可能会跟随一个数字,它的意思是:
return
或return 0
- 表示函数正常退出。return 1
或者其他大于0的数字 - 表示函数出了些问题。
你可以使用exit
退出 fish shell 本身。整数退出码的含义与上述相同。
set -q 和 test -z 的不同
在 if 语句中使用set -q
和test -z
检查变量是否为空时,有一些细微的区别。
- 在使用
test -z
时,请确保使用引号包裹变量,因为如果变量不在引号中,该命令可能在某些边缘情况下出错。 - 不过,你可以使用
set -q
来测试是否已经设置了变量,而无需将其括在引号中。
下面是示例:
1 | set GIT_STATUS (git status --porcelain) |
带有 and,or 运算符的多条件判断
如果想把多个条件组合到单个语句中,可以使用or
和and
运算符。如果想检查条件的逆,你可以使用!
。下面是一个函数的示例,该函数用于检查命令行传递的两个参数。这是我们描述的逻辑:
- 如果两个参数都缺失,应该向命令行打印帮助信息,然后提前
return
。 - 如果其中一个参数缺失,则显示一个提示,说明其中一个参数缺失,然后提前
return
。
1 | function requires-two-arguments |
下面是一些代码的注释:
set -q $variable
函数做了什么?当$variable
是空时,它返回 true。如果你想使用
test
函数替换掉set -q
来判断一个变量是否存在,可以使用:if test -z "$variable"
if test ! -n "$variable"
或if not test -n "$variable"
如果你想要把上面的
or
检查替换为test
,它看起来会像:if test -z "$argv[1]"; or test -z "$argv[2]"
。译者注:上面的代码本来就是
test -z
,这句话有点画蛇添足了。推测作者的代码原文可能是
set -q "$argv[1]"; or set -q "$argv[2]"
,所以才会有这种表述。当你使用
or
,and
运算符时,你必须使用;
来结束条件表达式 。确保把变量括在空引号中。如果变量中包含一个空字符串,那么如果没有这些引号,语句将导致错误。
这是另一个用于测试$variable
是否为空的例子:
1 | if test -z "$variable" ; echo "empty" ; else ; echo "non-empty" ; end |
这是另一个用于测试$variable
是否包含字符串的例子:
1 | if test -n "$variable" ; echo "non-empty" ; else ; echo "empty" ; end |
另一个常见运算符: not
下面是一个使用not
运算符测试字符串是否包含字符串片段的示例:
1 | if not string match -q "*md" $argv[1] |
参考资料
- test command
- set command
- if command
- stackoverflow answer: how to check if fish variable is empty
- stackoverflow answer: how to put multiple conditions in fish if statement
如何使用分隔符拆分字符串
在某些情况下,您希望获取命令的输出(一个字符串),然后用某个分隔符将其分割,以便只使用输出字符串的一部分。例如获取给定文件的 SHA 校验和。命令shasum <filename>
产生类似df..d8 <filename>
的输出。假设我们只想要这个字符串的第一部分(SHA),已知分隔符是两个空格字符,我们可以执行以下操作来获取校验和部分,并将其存储在$checksum
中。这里有关于string split
命令的更多信息。
1 | set CHECKSUM_ARRAY_STRING (shasum $FILENAME) |
如何执行字符串比较
为了测试字符串中的子串匹配,你可以使用string match
命令。这里有关于该命令的更多信息:
下面是一个实际应用的例子。注意,使用-q
或--quiet
时,如果匹配条件满足(成功),它不会打印字符串的输出。
1 | if string match -q "*myname*" $hostname |
下面是一个字符串精确匹配的示例:
1 | if test $hostname = "machine-name" |
一个测试字符串是否为空的示例:
1 | if set -q $my_variable |
下面是一个复杂的示例,测试是否安装了ruby-dev
和ruby-bundler
包。如果是,则运行jekyll
;如果不是,则安装这些包。
1 | # Return "true" if $packageName is installed, and "false" otherwise. |
如何为字符串编写 switch 语句
为了为字符串创建 switch 语句,这里也使用了test
命令(就像用于 if 语句一样)。case
语句需要匹配子字符串,可以使用通配符和想要匹配的子字符串的组合来表示子字符串。这是一个示例。
1 | switch $hostname |
你也可以把这些和 if 语句混合使用,最终看起来会像这样:
1 | if test (uname) = "Darwin" |
如何执行字符串
执行脚本中生成的字符串最安全的方法是使用以下模式。
1 | echo "ls \ |
这不仅使调试更容易,还在使用\
进行多行换行时避免了奇怪的错误。
如何编写函数
一个 fish 函数只是一个可选地接受参数的命令列表。这些参数作为列表传入(因为 fish 的所有变量都是列表)。
这是一个例子:
1 | function say_hi |
写完一个函数后,你可以通过使用type
查看它是什么。例如:type say_hi
将展示你刚刚创建的函数。
向函数传递参数
除了使用$argv
找出传递给函数的参数外,你还可以提供一个函数所期望的具名参数列表。这是官方文档中的更多信息。
需要记住的一些关键事项:
- 参数名不能含有
-
字符,使用_
替代。 - 不要使用
(
和)
向函数传递参数,只需要在带空格的单行中传递参数即可。
以下是一个示例:
1 | function testFunction -a param1 param2 |
下面是另一个测试传递给函数的参数是否存在的示例:
1 | # Note parameter names can't have dashes in them, only underscores. |
从函数中返回值
你可能需要从函数返回值(通常只是一个字符串),还可以返回许多由新行分割的字符串。无论如何,实现这一目标的机制是相同的。只需要使用echo
将返回值转储到 stdout 即可。
这是一个示例:
1 | function getSHAForFilePath -a filepath |
如何处理依赖项的文件和文件夹路径
随着脚本变得更加复杂,您可能需要处理加载多个脚本的问题。在这种情况下,您可以使用source my-script.sh
从当前脚本导入其它脚本。然而,fish 会在当前目录,即你开始执行脚本的目录寻找my-script.sh
文件,而该目录可能与你需要加载此依赖项的位置不匹配。如果你的主脚本在$PATH
中而依赖项不在,就会发生这种情况。在这种情况下,可以在主脚本中执行以下操作:
1 | set MY_FOLDER_PATH (dirname (status --current-filename)) |
这段代码实际做的是获取主脚本运行的文件夹,并将其存储在MY_FOLDER_PATH
中,然后就可以使用source
命令加载任何依赖项了。这种方法有一个限制,即在MY_FOLDER_PATH
中存储的是相对于主脚本执行位置的路径。这是一个你可能不关心的微妙细节,除非你需要一个绝对路径名。在这种情况下,你可以执行以下操作:
1 | set MY_FOLDER_PATH (realpath (dirname (status --current-filename))) |
使用realpath
可以提供文件夹的绝对路径,以便在需要此功能的情况下使用。
如何将多行字符串写入文件
在许多情况下,你需要在脚本中将字符串和多行字符串写入到新的或者现有的文件。
以下是一个向文件中写入单行字符串的例子:
1 | # echo "echo 'ClientAliveInterval 60' >> recurring-tasks.log" | xargs -I% sudo sh -c % |
下面是一个向文件写入多行字符串的例子:
1 | # More info on writing multiline strings: https://stackoverflow.com/a/35628657/2085356 |
如何创建彩色的 echo 输出
set_color
函数允许 fish 对使用echo
打印到stdout
的文本内容进行着色和格式化。这在创建需要不同前景、背景颜色以及粗体、斜体或下划线输出的文本输出时非常有用。有很多方法来使用该命令,以下是两个例子(echo
语句内联,以及单独使用):
1 | function myFunction |
注意:
- 必须调用
set_color normal
来重置以前语句中设置的格式化选项。 set_color -u
表示下划线,set_color -o
表示粗体。
如何获取用户输入
在某些情况下,你需要在执行一些可能具有破坏性的操作之前要求用户进行确认,或者可能需要用户输入函数的某些参数(该参数不是通过命令行传递的)。在这些情况下,可以通过使用read
函数读取stdin
以获取用户输入。
下面的函数简单地为 ‘Y’/‘y’ 返回 0,为 ‘N’/‘n’ 返回 1。
1 | # More info on prompting a user for confirmation using fish read function: https://stackoverflow.com/a/16673745/2085356 |
下面是使用_promptUserForConfirmation
函数的一个示例:
1 | if _promptUserForConfirmation "Delete branch $featureBranchName" |
如何使用 sed
这对删除不需要的文件片段非常有用。特别是在使用xargs
对find
结果进行管道处理时。
以下是一个从每个文件的开头删除 ‘./‘ 的示例:
1 | echo "./.Android" | sed 's/.\///g' |
一个一起使用sed
,find
和xargs
的更复杂的例子:
1 | set folder .Android* |
如何使用 xargs
这对于将某些命令的输出作为更多命令的参数很有用。
这是一个简单的例子:ls | xargs echo "folders: "
。
- 产生的输出是:
folders: idea-http-proxy-settings images tmp
- 注意参数在输出中是如何连接的
下面是一个稍微不同的例子,使用-I %
可以将参数放在任何地方(而不仅仅是放在末尾)。
1 | ls | xargs -I % echo "folder: %" |
产生如下输出:
1 | folder: idea-http-proxy-settings |
注意,每个参数都在单独的一行中。
如何使用 cut 切分字符串
假设你有一个字符串"token1:token2"
,你想切分这个字符串并只保留它的第一部分,这可以使用下述的cut
命令完成。
1 | echo "token1:token2" | cut -d ':' -f 1 |
-d ':'
- 将通过:
分隔符分割字符串-f 1
- 保留 token 化的字符串中的第一个字段
下面是一个在~/github/developerlife.com
中查找所有含有fonts.googleapis
的 HTML 文件,然后使用subl
打开的示例:
1 | cd ~/github/developerlife.com |
如何计算脚本运行所需的时间
1 | function timed -d Pass the program or function that you want to execute as an argument |