😎

Rustでコマンド自作してみた!

2024/02/16に公開

背景

Rustをつかって何か作りたかったので簡単なコマンドを作成しました!

新年一発目に作成したもので、お正月にちなんだ機能を実装したHappy(New)Yearコマンド作成しました!
ここからダウンロードできます!

https://github.com/takeru-a/happyYearCLI

前提

作成したコマンドはWindow 64bit(x64)環境のみ対象にビルドしているため、
Window 64bit環境のみで実行できます。
使用したプログラミング言語 Rust

実装した機能一覧

  • 出力するメッセージを変更できる ✅
  • 出力するアスキーアートを変更できる ✅
  • 現在時間を表示できる(年月日) ✅
  • おみくじ機能 ✅
  • ゆっくり出力する ✅
  • ロケーション特定、気温、天気 ✅
  • 近くの神社を出力する ✅

実装した機能について

色々な機能を実装しましたが、その中からいくつかの機能の実装方法について紹介します!

コマンドオプション

今回、コマンドを作成するにあたって、clapというクレート(ライブラリ)を使用しました。
clapはコマンドラインパーサーのクレートで簡単にCLIツールを作成することができます。
clapを使用してコマンドオプション機能を実装しました。

今回実装したオプションは、出力するメッセージを変更するオプションと出力するアスキーアートを変更するものになります。以下にオプションの対応表をまとめました。
コマンドのオプション対応表

option value description
-t 0 メッセージ1
-t 1 メッセージ2
-t 2 メッセージ3
-a 0
-a 1 鏡餅
-a 2 富士山

以下のようにコマンドを打つと、メッセージ2と龍のアスキーアートが出力されます。

> happyyear -t 1 -a 0

オプションは構造体のフィールドに#[arg(~)]を付与することで実装します。
フィールドにはコマンドラインで渡される値が格納されます。
argのパラメータを設定することでオプションの省略名、デフォルト値、許容する値などを定義することができます。詳しくはドキュメントを参照してください!

struct Cli {

    // 出力する文章の種類を指定する(0~2)
    #[arg(
        short='t',
        default_value_t=0,
        help="出力する文章の種類を指定する(0~2)", 
        num_args=1, 
        value_parser=clap::value_parser!(u8).range(0..=2)
    )]
    message_type: u8,

    // アスキーアートを指定する(0~2)
    #[arg(
        short='a', 
        default_value_t=0, 
        help="アスキーアートを指定する(0~2)", 
        num_args=1, 
        value_parser = clap::value_parser!(u8).range(0..=2))]
    art: u8,
}

helpを指定することでhelpオプションを使用する際に出力する内容を定義できます。

> happyyear --help
Usage: happyyear [OPTIONS]

Options:
  -t <MESSAGE_TYPE>      出力する文章の種類を指定する(0~2) [default: 0]
  -a <ART>               アスキーアートを指定する(0~2) [default: 0]
  -h, --help             Print help
  -V, --version          Print version

構造体のフィールドの#[arg(~)]を設定し、各フィールドで受け取った値を基に処理を分岐させればオプション機能を実装することができます!🎉

ロケーション特定機能

コマンド使用者のロケーション(住んでいる地域)を特定する機能を実装しました。
この機能は次に紹介する気温・天気出力機能、付近の神社出力機能に必要な緯度経度情報が欲しかったため実装しました。

ロケーションを特定する方法

ロケーションの特定方法ですが、今回はコマンド使用者のグローバルIPからAPIを使用し、緯度経度を取得しています。

  1. OpenDNSのDNSリゾルバを定義する
  2. OpenDNSに自身のグローバルIPを問い合わせる
    OpenDNSにmyip.opendns.comという特殊なドメイン名でDNSクエリを行うとクライアントのグローバルIPを返却するように定義されている。
// グローバルIPアドレスを取得する
pub async fn get_global_ip() -> Result<String> {

    // OpenDNSのresolver1.opendns.com(IPアドレス)を設定
    let resolver = TokioAsyncResolver::tokio(
        ResolverConfig::from_parts(
            None,
            vec![],
            NameServerConfigGroup::from_ips_clear(&[IpAddr::V4(Ipv4Addr::new(208, 67, 222, 222))], 53,true),
        ),
        ResolverOpts::default());
    
    // 'myip.opendns.com' のDNSクエリを実行する
    let response = resolver.lookup_ip("myip.opendns.com.").await?;
    let ip = response.iter().next().ok_or_else(|| anyhow!("IPアドレスを取得できませんでした。\n"))?;
    Ok(ip.to_string())
}
  1. 取得したグローバルIPを使用してAPIからロケーション情報を取得する
// グローバルIPからロケーション情報を取得する
pub async fn get_location(ip_address: String) -> Result<Location, reqwest::Error> {
    let url = format!("http://ip-api.com/json/{}?fields=16600", ip_address);
    let response = reqwest::get(&url).await?.json::<Location>().await?;
    Ok(response)
}

上記のコードはhttp://ip-api.com/ 対してGETリクエストを送信してます。

# GET
http://ip-api.com/json/{グローバルIP}?fields=16600

/json~で返却データ形式をjsonに指定しており、fields=~でどういったデータが欲しいかを選択しています。(指定しなければ全て取得される)

fieldsにどういった値を設定するかはこのサイトで確認してください!
Return dataの部分で、必要な項目だけチェックをいれます。

そうするとgenerated numericが変更されるので、その値をfieldsに渡せば必要なデータのみ使用できます。

今回はstatus、regionName(都道府県)、city(市)、lat、lon(緯度経度)のみ取得するように設定します。

サイトで生成されたURLにGETリクエストをすればロケーション情報を取得することができます!🎉

気温・天気出力機能

この機能はロケーション特定機能で取得した緯度経度情報を基にその地域の気温と天気を出力するものです。

// ロケーション情報から天気情報を取得する
pub async fn get_weather(location: &Location) -> Result<Weather, reqwest::Error> {

    // 現在の天気と気温、今日の天気、最高気温と最低気温を取得する
    let url = format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&current=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FTokyo&forecast_days=1",
     location.lat, location.lon);
    let response = reqwest::get(&url).await?;
    let response = response.json::<Weather>().await?;
    Ok(response)
}

上記のコードはhttps://api.open-meteo.com/v1/forecast に対してGETリクエストを送信してます。
クエリパラメータを指定することで必要な情報だけ取得することができます!
指定できるパラメータはドキュメントを参照してください!

このAPIも簡単にクエリを生成してくれるツールがあります。



サイトの上部にパラメータを設定できるチェックボックスがあるので必要な情報のものだけチェックを入れます。

チェックをいれるとAPI URLの部分に必要な情報のクエリパラメータが設定されたurlが生成されます。

このURLにGETリクエストを叩けば住んでいる地域の天気&気温を取得することができます!🎉

参考(WMOについて)

このAPIはWMO 気象解釈コードというもので天気を表しており、以下がその対応表になります!

WMO 気象解釈コード (WW)
コード	説明
0	晴天
1、2、3	晴れ時々曇り、曇り
45、48	霧と降る霧氷
51、53、55	霧雨: 軽い、中程度、そして濃い強度
56、57	氷結霧雨: 軽くて濃い強度
61、63、65	雨:小雨、中程度、激しい雨
66、67	凍てつく雨:軽くて激しい雨
71、73、75	降雪量: わずか、中程度、激しい
77	雪の粒
80、81、82	にわか雨:小雨、中程度、激しい雨
85、86	雪が少し降ったり、激しく降ったりします

付近の神社出力機能

この機能は初詣に行くであろう近くの神社を出力するために実装しました。
取得した緯度経度から半径5000mにある神社をランダムに5つ出力します。(5つない場合はその個数分)

// ロケーション情報から近くの神社を取得する
pub async fn get_shrine(location: &Location) -> Result<Shrine, reqwest::Error> {

    // 現在地から5000m以内の神社を取得する
    let query = format!(r#"[out:json];node["amenity"="place_of_worship"]["religion"="shinto"](around:5000,{},{});out;"#,
    location.lat, location.lon);
    let url = format!("https://overpass-api.de/api/interpreter?data={}", query);
    let response = reqwest::get(&url).await?.json::<Shrine>().await?;
    Ok(response)
}

上記のコードはhttps://overpass-api.de/api/interpreter にGETリクエストを送信してます。

このAPIはOverpass QLというクエリ言語を使用してリクエストを送信することでOpenStreetMapデータベースから特定の条件に合致するデータを抽出することができます。
指定できる条件の詳細はドキュメントを参照してください!

このサイトでOverpass QLの実行結果を確認することができます!

今回はOverpass QLで、出力形式をjsonに指定しており、条件は
["amenity"="place_of_worship"]["religion"="shinto"]で神社を指定してます。

let query = format!(r#"[out:json];node["amenity"="place_of_worship"]["religion"="shinto"](around:5000,{},{});out;"#, location.lat, location.lon);

あとクエリにはロケーション特定機能で取得した緯度経度情報を設定します。

このクエリを実行することで近くの神社情報を取得することができます!🎉

APIについて

使用したAPI

今回、ロケーション特定、気温・天気の出力と付近の神社出力機能を実装する際に以下のAPIを使用しました。

  • グローバルIPからロケーション特定するAPI 非営利使用は無料でAPIKeyも不要なので以下のAPIを使用させていただいております。 → https://ip-api.com/
  • ロケーション情報から天気を取得するAPI こちらも非営利使用は無料でAPIKeyも不要なので以下のAPIを使用させていただいております。 → https://open-meteo.com/
  • 地図情報取得API(OpenStreetMap) OpenStreetMap上の情報を取得できます。無料で利用できます。 → https://wiki.openstreetmap.org/wiki/JA:Overpass_API

代替のAPI

今回使用したAPI以外でも同じようなことを実現できるAPIがあったので、紹介いたします。

グローバルIPアドレス取得、ロケーション特定、天気API、地図APIともに他にも色々APIがあるため1つがダメになっても変更はできると思います。

最後に

はじめてRustを使ってなにか作成してみましたが、クレートやエラーハンドリングの方法などRustの理解を深めることができたと思います!
実装の詳細はhappyYearCLIリポジトリを参照してください!

参考

https://rust-cli.github.io/book/index.html

Discussion