🐟

fish shellからgoogle検索するコマンドを作ってみた

2022/01/16に公開約16,300字

モチベーション

シェルを触っている最中に検索したい事柄がよくでてくることがあります。コマンドラインからそのまま流れるように検索したい&&シェルスクリプトを勉強してみたい、ということで fish shell で CLI から google 検索できるようにする関数(コマンド)を作ってみました。

環境は macOS/iterm2 で、fish 言語(fish language)を使って作りました。

fish でのこういったスクリプトについての情報はあまり多くなかったので bash での実装を参考にしてつくりました。

https://s10i.me/whitenote/post/40

ただ検索できるようにするだけだとそのままになってしまうので、google 検索で使える英語検索画像検索完全一致検索個人最適化検索無効化などのコマンドオプションを追加しました。この記事を読めばfish関数でのオプション処理の基本が関数の実装を通して分かるように解説しています。

スクリプト

実際のスクリプト ggl.fish は以下の gist で公開しています。

実際のスクリプト ggl.fish

参考資料

先に使用する fish の文法と機能とコマンドについての参考資料をあげておきます。

fish の文法と機能

fish コマンド

bash でのパラメータ展開の代わりに fish での文字列操作

google 検索パラメータ

解説

fish では、Autoloading Functionという機能があり、コマンドにエンカウントすると、~/.config/fish/functions/ ディレクトリ内のコマンド名と同一名のファイルを探して、自動ロードします。この機能を使って、ggl という名前で「引数をブラウザで google 検索する」コマンドを作成します。

cd ~/.config/fish/functions/
touch ggl.fish
# ファイル作成したら、vscode等で開きます
code ggl.fish 

fish 言語での関数作成は、function 関数名; 処理内容; end で作成できます。この場合ファイル名と同じ ggl という名前の関数を作成します。ちなみに、fish ではセミコロン ; を使うことで、複数のコマンドを一行で書くことできます(これは他のシェルも同じだそうです)。

例えば、function コマンドで関数を定義する際にも簡単な内容であれば function ll; ls -l $argv; end というようにワンラインで書くことができます。

fish ドキュメントの各コマンドのページの Synopsis の項目には、そのコマンドの使い方が記載されていますが、このセミコロンを使ってワンラインで記載されている場合があります。例えば、fish の function コマンドの Synopsis は次の通りです。

function NAME [OPTIONS]; BODY; end

セミコロンを使わずに複数行で書く場合には、次のように書きます。

function ll
    ls -l $argv
end

function コマンドの詳細な使い方については公式ドキュメントの次のページに記載されています。
function - create a function — fish-shell 3.3.1 documentation

さて、それでは実際に ggl コマンドを作成します。

function ggl
    # 処理内容
end

fish の関数は単一コマンドにように呼ぶことができるコマンドのブロックのことであり、関数を使うことで複数の単純なコマンドの集合をより高度な 1 つのコマンドとしてまとめておくことができます。上記のコードの処理内容の部分に一連のコマンドを記載することでひとつのコマンドとしてまとめます。

Fish がインストールされていれば、macOS に元々入っている BSD 系のコマンド以外にビルトインと呼ばれるコマンドが使えるようになります。builtin -n ですべてのビルトインコマンドを確認できます。function コマンドのそのビルトインの 1 つです。これらのコマンドを集合させて fish 関数を作成します。

fish では、関数を使った自作コマンドの引数は $argv という変数へ格納されます。引数は複数個入力でき、その際に $argv にすべて格納されますが、この $argv はリスト変数(list variable)であり、関数へ渡されたすべての引数を含みます。要素へのアクセスには $argv[1] などインデックスを指定することでできます(ただし、インデックスは 1 から始まります)。

$ function myfunc
    echo $argv[1]
    echo $argv[3]
end
$ myfunc 一個目 二個目 三個目
一個目
三個目

自作した関数では、argparseコマンドを使うことで、コマンドに渡されれるオプションの解析を行うことができます。これで自分の関数に好きなオプションを設定できます。

function mybetterfunc
    argparse 'h/help' 's/second' -- $argv
    or return # 認識できないオプションによってargparseが失敗したらexitする
    
    # -h または --help オプションが与えられた場合、以下のような文書を出力してreturnする
    if set -q _flag_help
        echo "mybetterfunction [-h|--help] [-s|--second] [ARGUMENTS...]"
        return 0
    end

    # -s または --second オプションが与えられた場合、二番目の引数を出力する
    if set -q _flag_second
        echo $argv[2]
    else
        echo $argv[1]
        echo $argv[3]
    end
end

オプション引数が ggl -h のように渡された場合には、ローカル変数として _flag_help という変数が生成されます。これで関数内でこの変数を使ってオプションの処理を行うことができます。オプション処理は、ifset -q などを使って処理します(この方法は fish のドキュメントに記載されていますが、他に、もっとよい方法があれば教えて下さい)。

argparse の使い方は、例えば、ヘルプオプションのみを設定したいとして、argparse 'h/help' -- $argv とします。h/help-h--help という short バージョンと long バージョンの 2 つのオプションを設定することを意味します。そのあとの -- $argv はお約束として書きます。これによって関数内で $argv を使うときに渡されたオプションの文字列が取り除かれた状態のリストとして使うことができます。さらに、or return は引数のパースに失敗した場合のために argprse のつぎの行につけておきます。これが基本的な使い方です。

# 上記の mybetterfunc を使ってみる
$ mybetterfunc -s 二個目の引数 三個目の引数 四個目の引数
二個目の引数
四個目の引数
# 関数内での$argv は -s という文字列が含まれないリストになる
# $argv[1] と $argv[3] が出力されると、実際の引数である文字列 二個目の引数 と 四個目の引数 が出力される

それでは、ggl コマンドで設定するオプションを考えます。まず、基本的なオプションオプションとして、次の 3 つを設定します。

  • 英語での検索オプション : e/english
  • 画像での検索オプション : i/image
  • ヘルプオプション : h/help

さらに、どのブラウザで検索するかのブラウザオプションとして 4 つを設定します。

  • Vivaldi : v/vivaldi
  • Chrome : c/chrome
  • Firefox : f/firfox
  • Safari : s/safari

とりあえず、この 7 つのオプションを設定していきます。関数の冒頭で argparse コマンドを使って、残りの部分でオプション引数を取り除いた状態で $argv を使えるようにします。

function ggl 
    argparse \
        'e/english' 'i/image' \
        'h/help' \
        'v/vivaldi' 'c/chrome' 'f/firefox' 's/safari' -- $argv
    or return
    
    # オプション処理
    # 基本処理
end

fish では、一行のコマンドを複数行にできるように改行する手段があり、\ で改行できます。これを使ってオプションをわかりやすくグループ化しておきます。

それでは ggl 関数の基本的な機能である検索機能を実装していきます。検索機能といっても、引数に渡したキーワードをブラウザ上で検索するといった簡単な処理です。googler のようにターミナル内で表示してブラウジングするような高度なものではないです。

https://github.com/jarun/googler

macOS なので open -a Safari https://www.google.com/search?q=keywrods のようにコマンドを使ってアプリケーションを指定して URL を開くようにします。まずは、引数に渡したキーワードを検索できるように URL を作成します。とりあえず Safari で開けるようにします。

function ggl
    argparse # 省略
    or return
    
    oepn -a Safari "https://www.google.com/search?q=$argv"
end

この実装では、次のように英単語を引数にすれば URL を開くことができますが、日本語だとうまく URL を開くことができません。

$ ggl keyword

そこで URL として使えるように%エンコーディングします。%エンコーディングには fish のstring escapeコマンドを使用します。--style=url オプションで URL エンコーディングが可能です。

$ string escape --style=url あいうえお
%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A
# あいうえお という文字列をURL用の%エンコーディングしたもの

このエンコーディングした文字列を https://www.google.com/search?q= のあとに結合して日本語でも検索できるようにします。また、複数の引数を渡せるように各引数をホワイトスペース(空文字)で連結します。最終的には連結した状態の文字列を string escape でエンコーディングします。

set -l baseURL "https://www.google.com/search?q="
set -l keyword (string join " " $argv)
set -l encoding (string escape --style=url $keyword)
set -l searchURL (string join "&" (string join "" $baseURL $encoding))

URL 生成の処理はだいたい上のようなコードになります。それでは、ひとつずつ説明していきます。

fish では、ローカル変数、グローバル変数、ユニバーサル変数というようにシェル変数に種類が3つありますが、今回は関数内のみで使うの変数を設定するのでローカル変数を変数設定用のsetコマンドにローカルオプション -l を付けて変数の設定を行います。

# set -l 変数名 値
set -l foo hoge # ローカル変数 foo に値 hoge を設定
set foo huga # 再代入
echo $foo # huga という文字列が出力される

fish では、変数の値を参照したい場合には $foo のように変数名の頭に $ をつけます。

また、参考記事の「ターミナルからでもGoogle検索がしたい!」にある bash スクリプトでは、${prev_dir%/*} のようなブレースによるパラメーター展開を行っていますが、fish ではそのような bash のブレース展開はサポートしていないので、string コマンド系の文字列操作によって代用します(公式ドキュメントでも明示されています)。

さらに、fish のコマンド置換(command substitution)という、() で囲むことによってコマンドの一部に他のコマンドを埋め込むことができる機能を使うと、文字列の操作によって行をいくつか圧縮して書くことができます。以下のように関数の引数である $argv を空文字で連結した文字列をローカル変数として設定する際にコマンド置換を使用しています。

set -l keyword (string join " " $argv)
# ()内のコマンドで返ってくる文字列をローカル変数 keyword にセットします

string joinコマンドは、第一引数によって第二引数以降の引数の文字列を連結できます。ホワイトスペースを指定するなら " " をセパレーターとして第一引数に、そのまま連結したい場合には "" をセパレーターとして第一引数に渡します。

今回のケースでは、例えば、日本語で「fish shell 使い方」というように検索したいので、ggl fish shell 使い方 というように ggl コマンドに文字列が 3 つ渡されるような使い方になるはずです。この使い方だと、コマンドの引数は $argv にリストの形でセットされます。

$argv はリストなので string join " " $argv としたときには、リストの値を 1 つずつとりだして string join コマンドの第一引数に渡した " " というホワイトスペースで連結されます。つまり、ggl fish shell 使い方 なら fish shell 使い方 という文字列が生成されます。例えば、string join の第一引数を "+" にしたら、fish+shell+使い方 という文字列が生成されます。

さて、これで先程のコードの意味がわかりやすくなりました。

set -l baseURL "https://www.google.com/search?q="
set -l keyword (string join " " $argv)
# コマンドの引数から渡される複数の文字列を "fish shell 使い方" のような一つの文字列に連結

set -l encoding (string escape --style=url $keyword)
# 連結した文字列をURLエンコーディングしてURLとして開けるようにする
# "fish shell 使い方" なら "fish%20shell%20%E4%BD%BF%E3%81%84%E6%96%B9" のようになる

set -l searchURL (string join "&" (string join "" $baseURL $encoding))
# baseURLとして設定した google検索用の文字列と、エンコーディングした文字列を結合してクエリを作成
# "https://www.google.com/search?q=fish%20shell%20%E4%BD%BF%E3%81%84%E6%96%B9" が生成される

これで一通り ggl を書いてみると

function ggl 
    argparse #省略
    or return

    set -l keyword (string join " " $argv)
    set -l encoding (string escape --style=url $keyword)
    set -l baseURL "https://www.google.com/search?q="

    if test -n "$encoding"
        set -l searchURL (string join "" $baseURL $encoding)
        open -a Safari "$searchURL"
        return
    else 
        echo "検索したい言葉を引数として実行してください"
    end
end

fish では、if 条件; 処理内容; end で if 節を書くことができるので、検索する単語の引数があった場合にのみブラウザを開くようにします。testコマンドで encoding 変数に値があるかどうか調べて、あった場合にのみブラウザを開かせます。test -n-n オプションで引数の文字列の長さが 0 でない場合にのみ true を返します。"$encoding" で変数の値を文字列にしてテストします。

これで引数が無い場合には次のようなコメントが出力されるようになります。

$ ggl 
検索したい言葉を引数として実行してください

こんな感じで「ブラウザで検索する」という基本的な機能ができたのであとは argparse で指定したオプションの処理を加えていきます。また、生成した URL を確認できるように新しく、t/test オプションも追加しておきます。

function ggl
    argparse \
        't/test' 'h/help' \
        'e/english' 'i/image' \
        'v/vivaldi' 'c/chrome' 's/safari' 'f/firefox' -- $argv
    or return 
    
    # 変数とURL生成の準備
    set -l keyword (string join " " $argv)
    set -l encoding (string escape --style=url $keyword)
    set -l baseURL "https://www.google.com/search?q="
    # 色の設定
    set -l pcolor bryellow

    if set -q _flag_help
        echo "ヘルプの内容(オプション等)"
        return
    end

    if test -n "$encoding"
        # google検索のオプションが指定されているか調べて、あったらクエリパラメータを設定
        set -q _flag_english; and set _flag_english "lr=lang_en"
        set -q _flag_image; and set _flag_image "tbm=isch"

        # 検索用のURLを生成
        set -l searchURL (string join "&" (string join "" $baseURL $encoding) $_flag_english $_flag_image) 

        # テストオプション(生成されるURLを確認する)
        if set -q _flag_test
            echo (set_color $pcolor)"Keyword     :"(set_color normal) "$keyword"
            echo (set_color $pcolor)"URL encoding:"(set_color normal) "$encoding"
            echo (set_color $pcolor)"Search URL  :"(set_color normal) "$searchURL"
            return
        end

        # ブラウザオプションが指定されているか調べる
        if set -q _flag_vivaldi
            open -a Vivaldi "$searchURL"
        else if set -q _flag_chrome
            open -a "Google Chrome" "$searchURL"
        else if set -q _flag_safari
            open -a Safari "$searchURL"
        else if set -q _flag_firefox
            open -a Firefox "$searchURL"
        else
            #### デフォルトブラウザで開く
            open "$searchURL"
        end
            echo "\"$argv\"" "について検索完了しました"
        return
    else
        echo "検索したい言葉を引数として実行してください"
        return
    end
end

fish での条件処理として if コマンド以外に and コマンドなどが便利に使えます。これらを使って、オプション引数が渡されているかどうかを調べます。set -q 変数名 で変数が存在するかテストできるので、これと組み合わせて set -q 変数名; and 処理内容 で変数が存在したら and 以降の処理を実行できます。

google 検索では、英語で検索したい場合や画像検索したい場合があるので、これらをコマンドのオプションとして e/englishi/image として argparse コマンドに設定しました。これらのオプションが渡されていればローカル変数 _flag_english_flag_image が設定されているはずなので、それらをテストし、存在すれば google 検索で使えるクエリパラメータを _flag_english_flag_image に設定しなおします。

# google検索のオプションが指定されているか調べて、あったらクエリパラメータを設定
set -q _flag_english; and set _flag_english "lr=lang_en"
set -q _flag_image; and set _flag_image "tbm=isch"

google の検索パラメータについては次のサイトの記事を参考にしました。

Google検索のパラメータ(URLパラメータ)一覧 - fragment.database.

これで、ggl コマンドのオプションとして -e-i が渡されていれば、検索用の URL にそのパラメータを文字列として結合させます。例えば ggl -e fish shell 使い方 なら英語検索のオプションが渡されているので、クエリに lr=lang_en&strin join コマンドを再び使って結合します。

# 検索用のURLを生成
set -l searchURL (string join "&" (string join "" $baseURL $encoding) $_flag_english $_flag_image) 

これで、ggl -e fish shell というようにオプション指定して実行した際に、実際に生成される URL は https://www.google.com/search?q=fish%20shell&lr=lang_en となります。
上のコードでは、baseURL と検索 Word を URL エンコーディングした encoding を結合してから、オプションのクエリパラメータを & で結合しています。これで URL は生成できるはずなので、t/test オプションで実際に生成される URL を出力して確認できるようにします。

# テストオプション(生成されるURLを確認する)
if set -q _flag_test
    echo (set_color $pcolor)"Keyword     :"(set_color normal) "$keyword"
    echo (set_color $pcolor)"URL encoding:"(set_color normal) "$encoding"
    echo (set_color $pcolor)"Search URL  :"(set_color normal) "$searchURL"
    return
end	

-t オプションが ggl コマンドに渡されていれば、if の後で true で内部処理が実行でるはずです。また、テストしたい場合はブラウザを開かないように if ブロック終了時に return してコマンドを終了するようにしています。set_color コマンドを使うと出力される文字列の色を変更できるのでこれをコマンド置換で埋め込んで echo でそれぞれの変数の値を出力してテスト内容を見やすくします。また、set_color normal を使って出力の色を元に戻します。

$ ggl -t fish shell 使い方
Keyword     : fish shell 使い方
URL encoding: fish%20shell%20%E4%BD%BF%E3%81%84%E6%96%B9
Search URL  : https://www.google.com/search?q=fish%20shell%20%E4%BD%BF%E3%81%84%E6%96%B9
$ ggl -tei how to use fish shell
Keyword     : how to use fish shell
URL encoding: how%20to%20use%20fish%20shell
Search URL  : https://www.google.com/search?q=how%20to%20use%20fish%20shell&lr=lang_en&tbm=isch

これで、基本的なオプション処理は完了しました。いくつかのオプションを排他的にして、例えば、ヘルプオプション(-h)とテストオプション(-t)が両方一緒に使えないようにします。これに argparse コマンドの -x オプションで実現できます。

具体的には、ブラウザオプションについてそれぞれ排他的になるように -x 'v,c,s,f' というように設定します。テストとヘルプのユーティリティオプションが排他的になるように -x 't,h' というように設定します。

function ggl
    argparse \
        -x 'v,c,s,f' \
        -x 't,h' \
        't/test' 'h/help' \
        'e/english' 'i/image' 'p/perfect' \
        'v/vivaldi' 'c/chrome' 's/safari' 'f/firefox' -- $argv
    or return

これで、-t オプションと -h オプションを同時に使った際に次のようにエラーが出力されるようになりました。

$ ggl -th
ggl: Mutually exclusive flags 'h/help' and `t/test` seen

あとは、追加で完全一致検索のオプション(p/perfect)と個人最適化検索無効のオプション(n/nonperson)もつけておきます。google での完全一致(exact match)は引用符("")で単語を囲んで検索することで実現します。Personalized サーチを無効にするにはパラメータ pws=0 をクエリに連結します。

set -q _flag_english; and set _flag_english "lr=lang_en"
set -q _flag_image; and set _flag_image "tbm=isch"
set -q _flag_nonperson; ande set _flag_nonperson "pws=0"
# 完全一致のオプションがあれば、「"」 をエンコーディングした文字列「%22」で文字列を囲む
set -q _flag_perfect; and set _flag_perfect "%22"
and set encoding (string join "" $_flag_perfect $encoding $_flag_perfect)

set -l searchURL (string join "&" (string join "" $baseURL $encoding) $_flag_english $_flag_image $_flag_nonperson)

これで、最初の ggl コマンドが完成しました。

ggl.fish

おわり

コマンドラインから google 検索したいというモチベーションで、fish 言語について調べて実装してみました。fish 言語の取っ掛かりとしてかなりよかったと思います。今回は、説明用に小さく色々書いてみたのでよい復習となりました。これから、fish を使って色々つくってみようと思います。

追記

gist から更に改造したコードを Github のリポジトリでプロジェクトとして公開しました。fisherを使ってインストールできますので、使ってみてください。

https://github.com/yo-goto/ggl.fish
fisher install yo-goto/ggl.fish

Discussion

ログインするとコメントできます