【Go】ターミナルからWebサイトを開くCLIツールを作ってみた

# はじめに

　どうも、Shakkuです。
　都内某高専情報科の3年生です。(2023/12/8時点)

　普段Chromeとかブラウザを使っていて、ブックマークに登録したいサイトが多すぎる！とか、ブックマークに入れたくはないけどよく開くサイトがあるな〜ってことありますよね？(あるよね...？)
　ということで今回は、ターミナル上からWebサイトを「URL」で開いたり、「サイト名」で開いたり、「検索」したりできるCLIツールを作ってみました。

　githubにリリースしているので、ぜひ使ってみてください。(https://github.com/Shakkuuu/website-cli)

# 仕様
　CLI上で以下のコマンドを実行することで使用できる。
```
website-cli [-search 検索したい用語][site サイト名][url URL] 
```

`-search string`
　searchフラグでは、検索したい用語を入れることで、Googleでの検索結果が開かれる。
　スペースを入れて複数用語を検索したいような場合は、クオーテーション`'',""`で囲むか、用語を`+`で繋ぐ必要がある。

`-site string`
　siteフラグでは、あらかじめwebsite-cli-setting.jsonファイルで登録したサイト名を入力することで、そのサイト名に対応したサイトが開かれる。

`-url string`
　urlフラグでは、開きたいWebサイトの(https://またはhttp://から始まる)URLを入力することでそのURLのサイトが開かれる(タイプミスに注意)

　コマンドを実行するとデフォルトのブラウザで開かれる。
　フラグを複数選択した場合は、site,url,searchの順で全て開かれる。

# 対応OS

対応をしているOSは以下の通り。

- darwin：amd64,arm64
- linux：386,amd64,arm,arm64
- windows：386,amd64,arm,arm64

(対応しているはず...)

# 使用方法

　このURL[https://github.com/Shakkuuu/website-cli/releases](https://github.com/Shakkuuu/website-cli/releases)から最新バージョンをダウンロードする。

　ダウンロードしたzipファイルを展開し、適切な場所に配置する。(実行ファイルをMacとかであれば、`/usr/local/bin/`とかに置く)

　`website-cli-setting.json`というファイルを、実行ファイルと同じディレクトリに配置する。
　記述方法は以下の通り。

```:json
[
    {
        "url": "https://twitter.com",
        "sitename": [
            "twitter",
            "Twitter",
            "x",
            "X",
            "ツイッター",
            "ついったー"
        ]
    },
    {
        "url": "https://www.youtube.com",
        "sitename": [
            "youtube",
            "Youtube",
            "ようつべ",
            "ユーチューブ"
        ]
    }
]

```

　配列のjsonで、`url`と`sitename`の項目が必要。
　`sitename`はstringの配列となっている。
　`url`には、そのサイトのURL、`sitename`には、コマンドを使用する際に実行できるようにしたいサイト名を設定する。

`website-cli`で実行！

# コードの簡単な説明

### 構造体

　jsonをGoの形式に読み込むための構造体。
　jsonタグをつけて、jsonと構造体それぞれのフィールドを結びつける。

```:go
type SiteList struct {
	Url      string   `json:"url"`
	Sitename []string `json:"sitename"`
}
```

### フラグ

　ここで`-site`,`-url`,`-search`などのフラグを取得。
　flag.Stringなので、stringのみ対応。
　一つ目の引数でフラグ名、二つ目の引数でデフォルトの値、三つ目の引数でフラグがなかった時に出力されたり、`-help`時に表示される文章を与える。

```:go
// フラグ定義
urlFlag := flag.String("url", "", "開きたいWebサイトの(https://から始まる)URLを入力してください。")
siteFlag := flag.String("site", "", "(website-cli-setting.jsonで登録した)開きたいサイト名を入力してください。")
searchFlag := flag.String("search", "", "検索したい用語を入力してください。スペースを入れて検索したい場合は、クオーテーションで囲むか単語+でつないでください。")
flag.Parse()
```

### 設定ファイルの読み込み

　`os.Executable`で実行した実行ファイルのパスを取得。
　`os.Executable`で取得したパスは、実行ファイル名も入ってしまっているため、`path/filepath`パッケージの`filepath.Dir`で実行ファイルの置かれているディレクトリのパスに変換する。
　そこへ`filepath.Join`で設定ファイル名を追加することで、設定ファイルを読み込むパスの完成。
　`os.ReadFile`で設定ファイルを読み込む。
　定義していたSiteList構造体の配列型で`json.Unmarshall`を使用してjsonをGoの形式に読み込む。

```:go
// 実行ファイルのパスを取得
exePath, err := os.Executable()
if err != nil {
	log.Fatal("os.Executable err:", err)
}

// 実行ファイルのディレクトリのパスに変換
exeDir := filepath.Dir(exePath)

// ディレクトリのパスに読み込む設定ファイルの名前を追加
settingfilePath := filepath.Join(exeDir, "website-cli-setting.json")

// 設定ファイル読み込み
f, err := os.ReadFile(settingfilePath)
if err != nil {
	log.Fatal("os.ReadFile err:", err)
}

// json読み込み
var sl []SiteList
err = json.Unmarshal(f, &sl)
if err != nil {
	log.Fatal("json.Unmarshal err:", err)
}
```

### OS確認

　OSによって、webサイトを開くコマンドが違うため、実行しているデバイスのOSを`runtime.GOOS`で確認する。
　OSによって、webサイトを開く際のコマンドを返している。(Linuxは本当にこれなのか不明...)

```:go
func OSCheck() (string, error) {
	switch runtime.GOOS {
	case "windows":
		fmt.Println("Windows使用中")
		return "start", nil
	case "darwin":
		fmt.Println("Mac OSX使用中")
		return "open", nil
	case "linux":
		fmt.Println("Linux使用中")
		return "xgd-open", nil
	default:
		return "", errors.New("対応外のOSです")
	}
}
```

### フラグ確認

　フラグを複数立てていても同時に実行できるように、switchで分岐するのではなく、毎回ifでフラグが立っているか確認している。
　各フラグの関数でサイトを開くコマンドを生成し、OpenWebSite関数に渡してWebサイトを開いている。
```:go
if *siteFlag != "" {
	// サイト名選択
	cmd, err = Site(siteFlag, UsingOSCmd, sl)
	if err != nil {
		fmt.Println(err)
		flag.PrintDefaults()
		os.Exit(1)
	}

	// websiteを開く
	err = OpenWebSite(cmd)
	if err != nil {
		fmt.Println("Webサイトを開く際にエラーが発生しました:", err)
		os.Exit(1)
	}
}

if *urlFlag != "" {
	// URL指定
	cmd, err = Url(urlFlag, UsingOSCmd)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// websiteを開く
	err = OpenWebSite(cmd)
	if err != nil {
		fmt.Println("Webサイトを開く際にエラーが発生しました:", err)
		os.Exit(1)
	}
}

if *searchFlag != "" {
	// 検索ワード指定
	cmd, err = Search(searchFlag, UsingOSCmd)
	if err != nil {
		fmt.Println(err)
		flag.PrintDefaults()
		os.Exit(1)
	}

	// websiteを開く
	err = OpenWebSite(cmd)
	if err != nil {
		fmt.Println("Webサイトを開く際にエラーが発生しました:", err)
		os.Exit(1)
	}
}
```

### Site

　フラグとOSごとのコマンドとSiteListを引数にもらって、SiteListを展開しながら登録されているかを確認していく。
　登録しているものにフラグが引っ掛かったら、`exec.Command`で実行するコマンドを生成。

```:go
func Site(siteFlag *string, UsingOSCmd string, sitelist []SiteList) (*exec.Cmd, error) {
	// サイト名選択
	for _, site := range sitelist {
		for _, sn := range site.Sitename {
			if *siteFlag == sn {
				cmd := exec.Command(UsingOSCmd, site.Url)
				return cmd, nil
			}
		}
	}
	return nil, errors.New("登録されていないサイト名です")
}
```

### Url

　フラグとOSごとのコマンドを引数にもらって、URLが正しい書き方かどうかを確認してから、`exec.Command`で実行するコマンドを生成。
　URLのプロトコル`https://`または`http://`から始まっているかを確認している。

```:go
func Url(urlFlag *string, UsingOSCmd string) (*exec.Cmd, error) {
	s_urlflag := *urlFlag
	// URLが正しく入力されているか
	if len(s_urlflag) < 7 {
		return nil, errors.New("URLのプロトコルに誤りがあります")
	} else if s_urlflag[:8] != "https://" && s_urlflag[:7] != "http://" {
		return nil, errors.New("URLのプロトコルに誤りがあります")
	}
	cmd := exec.Command(UsingOSCmd, *urlFlag)
	return cmd, nil
}
```

### Search

　フラグとOSごとのコマンドを引数にもらって、URLを作成する。
　google.comの後に、`/search`で検索、`?q=`で検索ワードを指定して、そこにフラグの検索ワードを入れて、`exec.Command`で実行するコマンドを生成。

```:go
func Search(searchFlag *string, UsingOSCmd string) (*exec.Cmd, error) {
	// URL作成
	url := "https://google.com"
	path := "/search"
	params := "?q=" + *searchFlag
	cmd := exec.Command(UsingOSCmd, url+path+params)
	return cmd, nil
}
```

### Webサイトを開く

　生成された実行コマンドを引数で受け取り、`cmd.Start`で実行される。。
　これで、デフォルトのWebブラウザで開かれる。

```:go
func OpenWebSite(cmd *exec.Cmd) error {
	// コマンドを実行してWebサイトを開く
	if err := cmd.Start(); err != nil {
		return err
	}
	return nil
}
```

# 終わりに

　今回は、ターミナルからWebサイトを開くCLツールを作成してみました。
　Go言語はCLIツールを作成するのにも使用されるという話を聞いたことをきっかけに作ってみたのですが、調べているうちに`Cobra`というツールを見つけました。
 しかし、今回は初めてのCLIツール作成ということで、シンプルに標準パッケージのみで作成してみました。
　`flag`や`os.exec`は過去に使用したことがあったのですが、`path/filepath`や`runtime`などは初めて使用したため、触れてみてよかったです。
 　また、jsonの読み込みは、apiでのjsonデータの受け渡しばっかりでやっていたので、ディレクトリに置かれたjsonファイルを読み込むのにちょっと戸惑いました。
　何かいいアイデアが思いついたら、また作成してみようと思います。

　今回作成したCLIツールはGithubにリリースしていますので、ぜひ使用してみてください。(https://github.com/Shakkuuu/website-cli)

# 参考

- [Go言語でコマンドラインツールを作成して配布する](https://blog.fox-hound.tech/1301/)
- [GoReleaser で Go 製 CLI のリリースを自動化＆ Homebrew でインストールできるようにする](https://zenn.dev/kou_pg_0131/articles/goreleaser-usage)
- [GoReleaser覚え書き](https://qiita.com/progrhyme/items/d6db1bbb2a9d3c43ab33)
- [Golang でカレントディレクトリ、実行ファイルのパスを取得する (os.Getwd, os.Executable)](https://maku77.github.io/p/egs2bjt/)