banner
amtoaer

晓风残月

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

fish shell スクリプト作成ガイド

この記事はFISH SHELL SCRIPTING MANUALから翻訳されたものであり、私の知識が浅いため、誤りや不自然な表現があるかもしれません。読者の皆様にはコメント欄でご指導いただければ幸いです。

例を通じて、fish shell スクリプトの書き方を学びます。

スクリプトの先頭にある shebang 行#

ターミナルで fish スクリプトを実行するには、次の 2 つのことを行う必要があります。

  1. スクリプトの先頭に次の shebang 行を追加します:#!/usr/bin/env fish
  2. 次のコマンドを使用してファイルを実行可能としてマークします:chmod +x <あなたの fish スクリプトファイル名>

変数の設定方法#

注意点として、fish では変数に代入できるすべての値は文字列であり、ブール値、整数、浮動小数点数などの概念はありません。以下は変数に値を代入する簡単な例です。これに関する詳細情報があります。

set MY_VAR "some value"

最も便利なことの一つは、シェルで実行したコマンドの出力を変数に保存することです。特定のプログラムやコマンドが他のコマンドを実行する必要がある値を返すかどうかをテストする際(文字列比較、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のヘッドレス環境で実行される 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. return または return 0 - 関数が正常に終了したことを示します。
  2. return 1 または他の 0 より大きい数字 - 関数に問題が発生したことを示します。

fish shell 自体を終了するには exit を使用します。整数の終了コードの意味は上記と同じです。

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 演算子を使用できます。条件の逆をチェックしたい場合は、! を使用できます。以下は、コマンドラインで渡された 2 つの引数をチェックする関数の例です。これは、私たちが説明したロジックです:

  1. 両方の引数が欠落している場合、コマンドラインにヘルプ情報を印刷し、早期に return します。
  2. どちらか一方の引数が欠落している場合は、どちらか一方の引数が欠落していることを示すメッセージを表示し、早期に return します。
function requires-two-arguments
  # 引数が渡されていない
  if set -q "$argv"
    echo "Usage: requires-two-arguments arg1 arg2"
    return 1
  end
  # 引数が1つだけ渡されている
  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. set -q を使用して変数が存在するかどうかを判断するために test 関数を置き換えたい場合は、次のように使用できます:

    • 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 コマンド
  2. set コマンド
  3. if コマンド
  4. stackoverflow の回答: fish 変数が空かどうかを確認する方法
  5. stackoverflow の回答: fish if 文で複数の条件を指定する方法

区切り文字を使用して文字列を分割する方法#

特定の状況では、コマンドの出力(文字列)を取得し、特定の区切り文字で分割して出力文字列の一部だけを使用したい場合があります。例えば、特定のファイルの SHA チェックサムを取得する場合です。コマンド shasum <filename> は、df..d8 <filename> のような出力を生成します。SHA の部分だけを取得したいと仮定し、区切り文字が 2 つの空白文字であることがわかっている場合、次のようにしてチェックサムの部分を取得し、$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 を実行し、インストールされていない場合はこれらのパッケージをインストールします。

# $packageName がインストールされている場合は "true" を返し、そうでない場合は "false" を返します。
# これを if 文で次のように使用します:
#
# 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

# パッケージがインストールされているかどうかを確認するための詳細情報: 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)
    # ruby をインストールします
    echo "ruby-bundler または ruby-dev がインストールされていません; 今すぐインストールします..."
    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

以下は、関数に渡された引数が存在するかどうかをテストする別の例です:

# 引数名にはダッシュを含めることができないことに注意してください。アンダースコアのみ使用できます。
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 ""
  # $filepath が提供されていない、または $filepath が存在しない場合 -> $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

以下は、ファイルに複数行の文字列を書き込む例です:

# 複数行の文字列を書き込む方法に関する詳細情報: 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 に印刷されるテキストの内容に色を付けたり、フォーマットしたりできます。これは、異なる前景、背景色、太字、斜体、または下線の出力が必要なテキスト出力を作成する際に非常に便利です。このコマンドを使用する方法はたくさんありますが、以下は 2 つの例です(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 を返します。

# fish read 関数を使用してユーザーに確認を求める方法に関する詳細情報: https://stackoverflow.com/a/16673745/2085356
# fish `read` 関数に関する詳細情報: 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 を使用する方法#

これは、不要なファイルの部分を削除するのに非常に便利です。特に、xargs を使用して find の結果をパイプ処理する際に便利です。

以下は、各ファイルの先頭から './' を削除する例です:

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

sedfind、および xargs を一緒に使用するより複雑な例:

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 - トークン化された文字列の最初のフィールドを保持します

以下は、~/github/developerlife.com 内のすべての HTML ファイルを検索し、fonts.googleapis を含むファイルを見つけて subl で開く例です:

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

スクリプトの実行にかかる時間を計算する方法#

function timed -d 実行したいプログラムや関数を引数として渡します
  set START_TS (date +%s)

  # ここにあなたのコードが入ります。
  $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
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。