banner
amtoaer

晓风残月

叹息似的渺茫,你仍要保存着那真!
github
telegram
email
x
bilibili
steam

fish shell 腳本編寫指南

該文章翻譯自FISH SHELL SCRIPTING MANUAL,因本人才疏學淺,難免有錯誤 / 不通順的地方,還望讀者在評論區不吝賜教。

通過例子學習如何編寫 fish shell 腳本。

腳本頂部的 shebang 行#

為了在終端中運行 fish 腳本,你需要做如下兩件事:

  1. 將以下 shebang 行添加到腳本頂部:#!/usr/bin/env fish
  2. 使用以下命令將文件標記為可執行:chmod +x <你的 fish 腳本文件名>

如何設置變量#

注意,在 fish 中可以賦給變量的所有類型的值都是字符串,沒有布爾值、整數或浮點數等概念。下面是一個給變量賦值的簡單示例。這裡有關於此的更多信息

set MY_VAR "some value"

你可以做的最有用的事情之一就是把 shell 中執行命令的輸出存儲到一個變量中。在你測試某個程序或命令是否返回了一些需要你執行其它命令的值(使用字符串比較、 if 語句和 switch 語句)時,這是很有用的。下面是一些這樣做的例子。

set CONFIG_FILE_DIFF_OUTPUT (diff ~/Downloads/config.fish ~/.config/fish/config.fish)
set GIT_STATUS_OUTPUT (git status --porcelain)

變量作用域: 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 變量是列表。

set MY_VAR $MY_VAR "another value"

這是創建列表的方法。

set MY_LIST "value1" "value2" "value3"

存儲運行命令的返回值#

下面是一個將命令執行返回的值存儲到變量中的示例。

set OUR_VAR (math 1+2)
set OUR_VAR (date +%s)
set OUR_VAR (math $OUR_VAR / 60)

因為所有的 fish 變量都是列表,你可以使用 [n]運算符訪問單個元素,其中n=1表示第一個元素(而不是 0)。下面是一個例子。負數表示從末尾訪問元素。

set LIST one two three
echo $LIST[1]  # one
echo $LIST[2]  # two
echo $LIST[3]  # three
echo $LIST[-1] # 和上一行等同

範圍#

你可以使用變量 / 列表的範圍,接著上面的例子。

set LIST one two three
echo $LIST[1..2]  # one two
echo $LIST[2..3]  # two three
echo $LIST[-1..2] # three two

如何編寫 for 循環#

因為變量默認包含列表,所以很容易遍歷他們。這是一個例子:

set FOLDERS bin
set FOLDERS $FOLDERS .atom
set FOLDERS $FOLDERS "my foldername"
for FOLDER in $FOLDERS
  echo "item: $FOLDER"
end

你也可以把set命令放在同一行以簡化上述代碼,就像這樣:

set FOLDERS bin .atom "my foldername"
for FOLDER in $FOLDERS
  echo "item: $FOLDER"
end

還可以把整個 for 語句放到單行,像這樣:

set FOLDERS bin .atom "my foldername"
for FOLDER in $FOLDERS ; echo "item: $FOLDER" ; end

如何編寫 if 語句#

編寫 if 語句的關鍵是使用test命令對某個表達式求布爾值。可以是字符串比較,甚至是測試文件或文件夾的存在。下面是一些例子。你還可以使用not運算符作為test的前綴,以檢查逆條件。

常用的條件#

檢查數組的大小。$argv包含從命令行傳遞給腳本的參數列表。

if test (count $argv) -lt 2
  echo "Usage: my-script <arg1> <arg2>"
  echo "Eg: <arg1> can be 'foo', <arg2> can be 'bar'"
else
  echo "👋 Do something with $arg1 $arg2"
end

變量的字符串比較。

if test $hostname = "mymachine"
  echo "hostname is mymachine"
end

檢查文件是否存在:

if test -e "somefile"
  echo "somefile exists"
end

檢查文件夾是否存在:

if test -d "somefolder"
  echo "somefolder exists"
end

檢查文件通配符是否存在與文件和文件夾的檢查略有不同。原因在於 fish 處理通配符的方式 —— 在對它們執行任何命令之前,fish 會首先將其展開。

set -l files ~/Downloads/*.mp4 # 這個通配符表達式被展開,包含了實際的文件
if test (count $files) -gt 0
  mv ~/Downloads/*.mp4 ~/Videos/
  echo "📹 Moved '$files' to ~/Videos/"
else
  echo "⛔ No mp4 files found in Downloads"
end

一個在上述示例中使用not運算符的示例:

if not test -d "somefolder"
  echo "somefolder does not exist"
end

程序,腳本或函數的退出碼#

使用退出碼的想法是,你的函數或者整個 fish 腳本可以被能夠理解退出碼的其他程序使用。換句話說,可能會有 if 語句使用退出碼來確定某個條件。這是與其它命令行程序一起使用的一個非常常見的模式。退出碼不同於函數的返回值

下面是一個使用git命令的退出碼的示例:

if (git pull -f --rebase)
  echo "git pull with rebase worked without any issues"
else
  echo "Something went wrong that requires manual intervention, like a merge conflict"
end

一個測試命令執行是否沒有錯誤的示例:

if sudo umount /media/user/mountpoint
  echo "Successfully unmounted /media/user/mountpoint"
end

你還可以檢查$status變量的值。fish 會在執行命令後將返回值存儲在這個變量中。這兒有關於此的更多信息

當你寫函數時,可以使用以下關鍵字來退出函數或循環:returnreturn後可能會跟隨一個數字,它的意思是:

  1. returnreturn 0 - 表示函數正常退出。
  2. return 1或者其他大於 0 的數字 - 表示函數出了些問題。

你可以使用exit退出 fish shell 本身。整數退出碼的含義與上述相同。

set -q 和 test -z 的不同#

在 if 語句中使用set -qtest -z檢查變量是否為空時,有一些細微的區別。

  1. 在使用test -z時,請確保使用引號包裹變量,因為如果變量不在引號中,該命令可能在某些邊緣情況下出錯。
  2. 不過,你可以使用set -q來測試是否已經設置了變量,而無需將其括在引號中。

下面是示例:

set GIT_STATUS (git status --porcelain)
if set -q $GIT_STATUS ; echo "No changes in repo" ; end
if test -z "$GIT_STATUS" ; echo "No changes in repo" ; end

帶有 and,or 運算符的多條件判斷#

如果想把多個條件組合到單個語句中,可以使用orand運算符。如果想檢查條件的逆,你可以使用!。下面是一個函數的示例,該函數用於檢查命令行傳遞的兩個參數。這是我們描述的邏輯:

  1. 如果兩個參數都缺失,應該向命令行打印幫助信息,然後提前return
  2. 如果其中一個參數缺失,則顯示一個提示,說明其中一個參數缺失,然後提前return
function requires-two-arguments
  # 沒有參數傳入
  if set -q "$argv"
    echo "Usage: requires-two-arguments arg1 arg2"
    return 1
  end
  # 只傳入一個參數
  if test -z "$argv[1]"; or test -z "$argv[2]"
    echo "arg1 or arg2 can not be empty"
    return 1
  end
  echo "Thank you, got 1) $argv[1] and 2) $argv[2]"
end

下面是一些代碼的註釋:

  1. set -q $variable函數做了什麼?當$variable是空時,它返回 true。

  2. 如果你想使用test函數替換掉set -q來判斷一個變量是否存在,可以使用:

    • if test -z "$variable"
    • if test ! -n "$variable"if not test -n "$variable"
  3. 如果你想要把上面的or檢查替換為test,它看起來會像:if test -z "$argv[1]"; or test -z "$argv[2]"

    译者注:上面的代码本来就是test -z,这句话有点画蛇添足了。

    推测作者的代码原文可能是set -q "$argv[1]"; or set -q "$argv[2]",所以才会有这种表述。

  4. 當你使用orand運算符時,你必須使用;來結束條件表達式 。

  5. 確保把變量括在空引號中。如果變量中包含一個空字符串,那麼如果沒有這些引號,語句將導致錯誤。

這是另一個用於測試$variable是否為空的例子:

if test -z "$variable" ; echo "empty" ; else ; echo "non-empty" ; end

這是另一個用於測試$variable是否包含字符串的例子:

if test -n "$variable" ; echo "non-empty" ; else ; echo "empty" ; end

另一個常見運算符: not#

下面是一個使用not運算符測試字符串是否包含字符串片段的示例:

if not string match -q "*md" $argv[1]
  echo "The argument passed does not end in md"
else
  echo "The argument passed ends in md"
end

參考資料#

  1. test command
  2. set command
  3. if command
  4. stackoverflow answer: how to check if fish variable is empty
  5. stackoverflow answer: how to put multiple conditions in fish if statement

如何使用分隔符拆分字符串#

在某些情況下,您希望獲取命令的輸出(一個字符串),然後用某個分隔符將其分割,以便只使用輸出字符串的一部分。例如獲取給定文件的 SHA 校驗和。命令shasum <filename>產生類似df..d8 <filename>的輸出。假設我們只想要這個字符串的第一部分(SHA),已知分隔符是兩個空格字符,我們可以執行以下操作來獲取校驗和部分,並將其存儲在$checksum中。這裡有關於string split命令的更多信息

set CHECKSUM_ARRAY_STRING (shasum $FILENAME)
set CHECKSUM_ARRAY (string split "  " $SOURCE_CHECKSUM_ARRAY)
set CHECKSUM $CHECKSUM_ARRAY[1]

如何執行字符串比較#

為了測試字符串中的子串匹配,你可以使用string match命令。這裡有關於該命令的更多信息:

  1. string match 的官方文檔
  2. 關於如何使用它的 Stackoverflow 回答

下面是一個實際應用的例子。注意,使用-q--quiet時,如果匹配條件滿足(成功),它不會打印字符串的輸出。

if string match -q "*myname*" $hostname
  echo "$hostname contains myname"
else
  echo "$hostname does not contain myname"
end

下面是一個字符串精確匹配的示例:

if test $hostname = "machine-name"
  echo "Exact match"
else
  echo "Not exact match"
end

一個測試字符串是否為空的示例:

if set -q $my_variable
  echo "my_variable is empty"
end

下面是一個複雜的示例,測試是否安裝了ruby-devruby-bundler包。如果是,則運行jekyll;如果不是,則安裝這些包。

# Return "true" if $packageName is installed, and "false" otherwise.
# Use it in an if statement like this:
#
# if string match -q "false" (isPackageInstalled my-package-name)
#   echo "my-package-name is not installed"
# else
#   echo "my-package-name is installed"
# end
function isPackageInstalled -a packageName
  set packageIsInstalled (dpkg -l "$packageName")
  if test -z "$packageIsInstalled"
    set packageIsInstalled false
  else
    set packageIsInstalled true
  end
  echo $packageIsInstalled
end

# More info to find if a package is installed: https://askubuntu.com/a/823630/872482
if test (uname) = "Linux"

  echo "🐒isPackageInstalled does-not-exist:" (isPackageInstalled does-not-exist)

  if string match -q "false" (isPackageInstalled ruby-dev) ;
    or string match -q "false" (isPackageInstalled ruby-bundler)
    # Install ruby
    echo "ruby-bundler or ruby-dev are not installed; installing now..."
    echo sudo apt install -y ruby-bundler ruby-dev
  else
    bundle install
    bundle update
    bundle exec jekyll serve
  end

end

如何為字符串編寫 switch 語句#

為了為字符串創建 switch 語句,這裡也使用了test命令 (就像用於 if 語句一樣)。case語句需要匹配子字符串,可以使用通配符和想要匹配的子字符串的組合來表示子字符串。這是一個示例。

switch $hostname
case "*substring1*"
  echo "Matches $hostname containing substring1"
case "*substring2*"
  echo "Matches $hostname containing substring2"
end

你也可以把這些和 if 語句混合使用,最終看起來會像這樣:

if test (uname) = "Darwin"
  echo "Machine is running macOS"
  switch $hostname
  case "*MacBook-Pro*"
    echo "hostname has MacBook-Pro in it"
  case "*MacBook-Air*"
    echo "hostname has MacBook-Air in it"
  end
else
  echo "Machine is not running macOS"
end

如何執行字符串#

執行腳本中生成的字符串最安全的方法是使用以下模式。

echo "ls \
  -la" | sh

這不僅使調試更容易,還在使用\進行多行換行時避免了奇怪的錯誤。

如何編寫函數#

一個 fish 函數只是可選地接受參數的命令列表。這些參數作為列表傳入(因為 fish 的所有變量都是列表)。

這是一個例子:

function say_hi
  echo "Hi $argv"
end
say_hi
say_hi everbody!
say_hi you and you and you

寫完一個函數後,你可以通過使用type查看它是什麼。例如:type say_hi將展示你剛剛創建的函數。

向函數傳遞參數#

除了使用$argv找出傳遞給函數的參數外,你還可以提供一個函數所期望的具名參數列表。這是官方文檔中的更多信息

需要記住的一些關鍵事項:

  1. 參數名不能含有-字符,使用_替代。
  2. 不要使用()向函數傳遞參數,只需要在帶空格的單行中傳遞參數即可。

以下是個示例:

function testFunction -a param1 param2
  echo "arg1 = $param1"
  echo "arg2 = $param2"
end
testFunction A B

下面是另一個測試傳遞給函數的參數是否存在的示例:

# Note parameter names can't have dashes in them, only underscores.
function my-function -a extension search_term
  if test (count $argv) -lt 2
    echo "Usage: my-function <extension> <search_term>"
    echo "Eg: <extension> can be 'fish', <search_term> can be 'test'"
  else
    echo "✋ Do something with $extension $search_term"
  end
end

從函數中返回值#

你可能需要從函數返回值(通常只是字符串),還可以返回許多由新行分割的字符串。無論如何,實現這一目標的機制是相同的。只需要使用echo將返回值轉儲到 stdout 即可。

這是一個示例:

function getSHAForFilePath -a filepath
  set NULL_VALUE ""
  # No $filepath provided, or $filepath does not exist -> early return w/ $NULL_VALUE.
  if set -q $filepath; or not test -e $filepath
    echo $NULL_VALUE
    return 0
  else
    set SHASUM_ARRAY_STRING (shasum $filepath)
    set SHASUM_ARRAY (string split "  " $SHASUM_ARRAY_STRING)
    echo $SHASUM_ARRAY[1]
  end
end

function testTheFunction
  echo (getSHAForFilePath ~/local-backup-restore/does-not-exist.fish)
  echo (getSHAForFilePath)
  set mySha (getSHAForFilePath ~/local-backup-restore/test.fish)
  echo $mySha
end

testTheFunction

如何處理依賴項的文件和文件夾路徑#

隨著腳本變得更加複雜,您可能需要處理加載多個腳本的問題。在這種情況下,您可以使用source my-script.sh從當前腳本導入其它腳本。然而,fish 會在當前目錄,即你開始執行腳本的目錄尋找my-script.sh文件,而該目錄可能與你需要加載此依賴項的位置不匹配。如果你的主腳本在$PATH中而依賴項不在,就會發生這種情況。在這種情況下,可以在主腳本中執行以下操作:

set MY_FOLDER_PATH (dirname (status --current-filename))
source $MY_FOLDER_PATH/my-script.fish

這段代碼實際做的是獲取主腳本運行的文件夾,並將其存儲在MY_FOLDER_PATH中,然後就可以使用source命令加載任何依賴項了。這種方法有一個限制,即在MY_FOLDER_PATH中存儲的是相對於主腳本執行位置的路徑。這是一個你可能不關心的微妙細節,除非你需要一個絕對路徑名。在這種情況下,你可以執行以下操作:

set MY_FOLDER_PATH (realpath (dirname (status --current-filename)))
source $MY_FOLDER_PATH/my-script.fish

使用realpath可以提供文件夾的絕對路徑,以便在需要此功能的情況下使用。

如何將多行字符串寫入文件#

在許多情況下,你需要在腳本中將字符串和多行字符串寫入到新的或者現有的文件。

以下是一個向文件中寫入單行字符串的例子:

# echo "echo 'ClientAliveInterval 60' >> recurring-tasks.log" | xargs -I% sudo sh -c %
set linesToAdd "TCPKeepAlive yes" "ClientAliveInterval 60" "ClientAliveCountMax 120"
for line in $linesToAdd
  set command "echo '$line' >> /etc/ssh/sshd_config"
  executeString "$command | xargs -I% sudo sh -c %"
end

下面是一個向文件寫入多行字符串的例子:

# More info on writing multiline strings: https://stackoverflow.com/a/35628657/2085356
function _workflowWriteEmptyMarkdownContentToFile --argument datestr filename
  echo > $filename "\
---
Title: About $filename
Date: $datestr
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Your heading
"
end

如何創建彩色的 echo 輸出#

set_color函數允許 fish 對使用echo打印到stdout的文本內容進行著色和格式化。這在創建需要不同前景、背景顏色以及粗體、斜體或下劃線輸出的文本輸出時非常有用。有很多方法來使用該命令,以下是兩個例子(echo語句內聯,以及單獨使用):

function myFunction
  if test (count $argv) -lt 2
    set -l currentFunctionName (status function)
    echo "Usage: "(set_color -o -u)"$currentFunctionName"(set_color normal)\
      (set_color blue)" <arg1> "\
      (set_color yellow)"<arg2>"(set_color normal)
    set_color blue
    echo "- <arg1>: Something about arg1."
    set_color yellow
    echo "- <arg2>: Something about arg2"
    set_color normal
    return 1
  end
end

注意:

  1. 必須調用set_color normal來重置以前語句中設置的格式化選項。
  2. set_color -u表示下劃線,set_color -o表示粗體。

如何獲取用戶輸入#

在某些情況下,你需要在執行一些可能具有破壞性的操作之前要求用戶進行確認,或者可能需要用戶輸入函數的某些參數(該參數不是通過命令行傳遞的)。在這些情況下,可以通過使用read函數讀取stdin以獲取用戶輸入。

下面的函數簡單地為 'Y'/'y' 返回 0,為 'N'/'n' 返回 1。

# More info on prompting a user for confirmation using fish read function: https://stackoverflow.com/a/16673745/2085356
# More info about fish `read` function: https://fishshell.com/docs/current/cmds/read.html
function _promptUserForConfirmation -a message
  if not test -z "$message"
    echo (set_color brmagenta)"🤔 $message?"
  end

  while true
    # read -l -P '🔴 Do you want to continue? [y/N] ' confirm
    read -l -p "set_color brcyan; echo '🔴 Do you want to continue? [y/N] ' ; set_color normal; echo '> '" confirm
    switch $confirm
      case Y y
        return 0
      case '' N n
        return 1
    end
  end
end

下面是使用_promptUserForConfirmation函數的一個示例:

if _promptUserForConfirmation "Delete branch $featureBranchName"
  git branch -D $featureBranchName
  echo "👍 Successfully deleted $featureBranchName"
else
  echo "⛔ Did not delete $featureBranchName"
end

如何使用 sed#

這對刪除不需要的文件片段非常有用。特別是在使用xargsfind結果進行管道處理時。

以下是一個從每個文件的開頭刪除 './' 的示例:

echo "./.Android" | sed 's/.\///g'

一個一起使用sedfindxargs的更複雜的例子:

set folder .Android*
find ~ -maxdepth 1 -name $folder | sed 's/.\///g' | \
  xargs -I % echo "cleaned up name: %"

如何使用 xargs#

這對於將某些命令的輸出作為更多命令的參數很有用。

這是一個簡單的例子:ls | xargs echo "folders: "

  1. 產生的輸出是:folders: idea-http-proxy-settings images tmp
  2. 注意參數在輸出中是如何連接的

下面是一個稍微不同的例子,使用-I %可以將參數放在任何地方(而不僅僅是放在末尾)。

ls | xargs -I % echo "folder: %"

產生如下輸出:

folder: idea-http-proxy-settings
folder: images
folder: tmp

注意,每個參數都在單獨的一行中。

如何使用 cut 切分字符串#

假設你有一個字符串"token1:token2",你想切分這個字符串並只保留它的第一部分,這可以使用下述的cut命令完成。

echo "token1:token2" | cut -d ':' -f 1
  • -d ':' - 將通過:分隔符分割字符串
  • -f 1 - 保留 token 化的字符串中的第一個字段

下面是一個在~/github/developerlife.com中查找所有含有fonts.googleapis的 HTML 文件,然後使用subl打開的示例:

cd ~/github/developerlife.com
echo \
"find . -name '*html' | \
 xargs grep fonts.googleapis | \
 cut -d ':' -f 1 | \
 xargs subl" \
 | sh

如何計算腳本運行所需的時間#

function timed -d Pass the program or function that you want to execute as an argument
  set START_TS (date +%s)

  # This is where your code would go.
  $argv

  sleep 5

  set END_TS (date +%s)
  set RUNTIME (math $END_TS - $START_TS)
  set RUNTIME (math $RUNTIME / 60)
  echo "⏲ Total runtime: $RUNTIME min ⏲"
end
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。