[Rust] 独自のコマンドラインツールを構築します。Rust はシンプルな wget を実装します。
記事ディレクトリ
序文
build-your-own-x プロジェクト: https://github.com/codecrafters-io/build-your-own-x
上記のプロジェクトの 1 つ: https://mattgathu.github.io/2017/08/29/writing-cli-app-rust.html
新しいバージョンの Rust を使用してツールを再実装する
この演習では、ファイルをダウンロードするための簡単な一般的なツールを作成することにより、Rust でコマンドライン ツールを作成する方法を示しますwget
。
ターゲット関数
wget
この単純な実装には、コマンド ライン ツールに必要な次の関数が含まれます。
- パラメータ分析
- カラー出力
- プログレスバー
新しいプロジェクト
Cargo
プロジェクトのフレームワークをセットアップするには、Rust のビルド ツール (およびパッケージ マネージャー) を使用します。
cargo new myget
これにより、myget という新しいプロジェクトが作成されます。
cd myget
tree .
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
パラメータ分析
cargo add clap --features derive
clapクレートを使用してコマンドライン引数を解析します。Cargo のマニフェスト ファイルの依存関係セクションを更新して、プロジェクトに追加します。
[package]
name = "myget"
version = "0.1.0"
edition = "2021"
authors = ["Star-tears"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.1.4", features = ["derive"] }
次に、パラメータ解析を実行するためにmain.rs を更新します。
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author="Star-tears", version="0.1.0", about="myget clone written in Rust", long_about = None)]
struct Cli {
/// Download url
#[arg(short, long)]
url: String,
}
fn main() {
let cli = Cli::parse();
println!("Value of url: {}", cli.url);
}
これで、Cargo を使用して引数パーサーをテストできるようになりました。
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target\debug\myget.exe`
error: the following required arguments were not provided:
--url <URL>
Usage: myget.exe --url <URL>
For more information, try '--help'.
error: process didn't exit successfully: `target\debug\myget.exe` (exit code:
呼び出し時に追加することで--
プログラムに引数を渡すことができます
cargo run -- -h
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target\debug\myget.exe -h`
myget clone written in Rust
Usage: myget.exe --url <URL>
Options:
-u, --url <URL> Download url
-h, --help Print help
プログレスバーと色付き出力
コマンド ライン アプリケーションの進行状況を示すためのIndicatifライブラリ。これを使用して、wget クローンの進行状況バーとスピナーを実装します。
Indicatif は別のクレートconsoleに依存しており、それを色付きの出力に使用します。これを使用して、色付きのテキストをコンソールに出力します。
reqwestライブラリを使用して、URL を受信してローカル ファイルにダウンロードするファイル ダウンロード関数を実装します。
cargo add [email protected]
cargo add [email protected]
cargo add reqwest --features blocking
最終: Cargo.toml
_dependencies
[dependencies]
clap = { version = "4.1.4", features = ["derive"] }
console = "0.15.5"
indicatif = "0.17.3"
reqwest = { version = "0.11.14", features = ["blocking"] }
プログレスバーの作成に使用される関数は次のとおりです。
fn create_progress_bar(quiet_mode: bool, msg: String, length: Option<u64>) -> ProgressBar {
let bar = match quiet_mode {
true => ProgressBar::hidden(),
false => match length {
Some(len) => ProgressBar::new(len),
None => ProgressBar::new_spinner(),
},
};
bar.set_message(msg);
match length.is_some() {
true => bar
.set_style(ProgressStyle::default_bar()
.template("{msg} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} eta: {eta}").unwrap()
.progress_chars("=> ")),
false => bar.set_style(ProgressStyle::default_spinner()),
};
bar
}
wgetのロジックを実装する
また、ダウンロード機能では、ファイルの各チャンクがダウンロードされると、進行状況バーが更新され、ダウンロード メタデータを含む色付きのテキストが印刷されます。
fn download(target: String, quiet_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
let url = Url::parse(&target)?;
let client = Client::new();
let mut res = client.get(url).send().unwrap();
println!(
"HTTP request sent... {}",
console::style(format!("{}", res.status())).green()
);
if res.status().is_success() {
let headers = res.headers().clone();
let ct_len = res.content_length();
let ct_type = headers.get("content-type").unwrap();
match ct_len {
Some(len) => {
println!(
"Length: {} ({})",
console::style(len).green(),
console::style(format!("{}", HumanBytes(len))).red()
);
}
None => {
println!("Length: {}", console::style("unknown").red());
}
}
println!(
"Type: {}",
console::style(ct_type.to_str().unwrap()).green()
);
let fname = target.split("/").last().unwrap();
println!("Saving to: {}", console::style(fname).green());
let chunk_size = match ct_len {
Some(x) => x as usize / 99,
None => 1024usize, // default chunk size
};
let mut buf: Vec<u8> = Vec::new();
let bar = create_progress_bar(quiet_mode, fname.to_string(), ct_len);
loop {
let mut buffer = vec![0; chunk_size];
let bcount = res.read(&mut buffer[..]).unwrap();
buffer.truncate(bcount);
if buffer.is_empty() {
break;
}
buf.extend(buffer.into_boxed_slice().into_vec().iter().clone());
bar.inc(bcount as u64);
}
bar.finish();
let mut file = match File::create(Path::new(fname)) {
Ok(file) => file,
Err(why) => panic!("couldn't create {}: {}", fname, why),
};
file.write_all(buf.as_slice()).unwrap();
}
Ok(())
}