読者です 読者をやめる 読者になる 読者になる

ubnt-intrepid's blog

書いてあることがブログの内容です

わん

Programming Rust neta

mattn.kaoriya.net

上の記事を読んでいて,最後のコード例を眺めていたらできそうな気がしたので作ってみました.

github.com

やってることは wandbox-run と同じ(パクリ)ですが,serdecurl-rust を使っている分 Rust っぽい感じに出来ていると思います.

せっかく作ったので,今後は wandbox のコマンドラインインターフェースとして成熟させたいなぁなどと思っています.

Rustacean のためのリポジトリ管理ツールを作りました

Rust Programming

何度かつぶやいたりしていましたが、READMEを書くのが面倒になったので より多くの人に使ってもらうため少し真面目に解説記事を作りました。

rhq の概要

GitHub - ubnt-intrepid/rhq: Manages your local repositories

言わずと知れた Golang 製のリポジトリ管理ツールである ghq を Rust に移植したものです。 もともと ghqrs というものを作って放置していたのですが、内部の実装に若干気に入らない点があり再度自前で実装したのが本ツールになります。 現状では ghq すべての機能を移植できているわけではないので代替というには若干役不足ですが、自分の欲しい機能は概ね実装できたので思い切って公開することにしました。

基本的に出来ることは ghq と同じような感じですが,あまり使っていない機能などは省略しています。 下のコマンド実行例を見れば雰囲気だけでも伝わると思います。

$ rhq clone ubnt-intrepid/rhq
# git clone https://github.com/ubnt-intrepid/rhq.git ~/.rhq/github.com/ubnt-intrepid/rhq と等価
$ rhq list | sk

インストール方法

現状はソースのみの配布なため、Rust および Cargo が必要になります(ビルド済みバイナリの配布は検討中です)。 すでに Rust の開発環境が整っている場合は cargo コマンドでインストールできます。

$ cargo install rhq
# 開発版を使用する場合
$ cargo install --git https://github.com/ubnt-intrepid/rhq.git

ディレクトリ構成

基本的には ghq (および Golang)と同様、ルートディレクトリ(デフォルトでは ~/.rhq)直下に ホスト名/ユーザ名/リポジトリ名 という形式でクローンしたリポジトリが格納されます. ghq では複数のルートディレクトリを指定することが出来るようになっていますが、クローン時のディレクトリの決定が面倒だったためrhq では原則として単一のルートディレクトリを用いるようにしました. ただしそれでは不便な場合もあるので,後述するようにリポジトリ探索用のディレクトリを追加するための設定項目を設けています.

  • ~/.rhq/
    • github.com/
      • user1/
        • project1/
        • project2/
      • user2/
        • project1/
    • gitlab.com/
      • user1/
        • project3/

コマンド一覧

rhq clone [<query>] [--arg=<arg>] [-n | --dry-run]

<query> に指定した文字列を元にリモートリポジトリのURLを推測し, ルートディレクトリ(デフォルトでは ~/.rhq)下の指定場所にクローンします(要は ghqghq get です)。 例えばこのプロジェクトのリポジトリを取得したい場合は次のようにします.

$ rhq clone ubnt-intrepid/rhq
# git clone https://github.com/ubnt-intrepid/rhq.git ~/.rhq/github.com/ubnt-intrepid/rhq が実行される

--arg オプションで git clone に渡すオプションを指定できます。 ghq get では --shallow オプションなどを用いてクローン時のオプションを設定しますが,より柔軟な制御をおこなえるように rhq では Git のオプションをそのまま渡す方針を取りました。

<query> を省略した場合は,標準入力から1行ずつ読みこみリポジトリのクローンを行います。

rhq list

ルートディレクトリ直下にあるリポジトリのパスを取得し、標準出力に表示します。 後述する探索用ディレクトリに vim-plug や zplug のディレクトリを指定しておくことで,(別途スクリプトを用意することなく)一括してリポジトリを管理することが可能です。

rhq foreach [-n | --dry-run] <command> [<args>...]

管理下のリポジトリディレクトリ下で指定したコマンドを実行します。 例えば、管理下のリポジトリ全てに対しリモート側の変更を同期する場合は次のように実行します。

$ rhq foreach -- git fetch --all

現状はシングルスレッドで実行するためすべてのコマンドの実行が完了するまでに時間を要します。そのうち並列しようと思ってはいますが現状では未定です…

rhq completion <shell> [<out-file>]

各種シェル用の補完スクリプトを生成します。 clap の補完スクリプト生成機能をそのまま使っているだけですが. <out-file> にはスクリプトの出力先のパスを指定し,省略した場合は標準出力にスクリプトを吐き出します.

設定ファイル

TOML 形式での設定ファイルの読み込みをサポートしています. 設定ファイルは ~/.config/rhq/config~/.rhqconfig の順番で読み込み,各項目は上書き・追記されます. 現在は以下の項目を用意しています.

root = "~/.rhq"

supplements = [
  "~/.go/src",
  "~/.vim/plugged",
  "~/.zplug/repos",
  "~/.dotfiles"
]

応用例

ghq 側の応用例がほぼそのまま使うことができます。 そのため、そこまで説明する必要はない気もしますが、はじめての方向けに簡単に紹介しておきます。

GitHub から特定ユーザのリポジトリを一括クローンする

GitHub APIコマンドラインJSONを整形できる jq を使った例です。 現状は rhq clone が並列化をしていないので,愚直に xargs で並列化したほうが良い気がしますが…

$ ghuser=ubnt-intrepid
$ curl -s "https://api.github.com/users/$ghuser/repos?max_pages=100" \
  | jq -r '.[].name' \
  | awk '{printf("'$ghuser'/%s\n",$1)}'
  | rhq clone --arg="--depth 50"

この辺 を使って直接 GitHub API を呼び出しても面白そう

シェル・テキストエディタとの連携

peco や fzf などの fuzzy selector を用いることで端末上での作業が効率化できることが知られています。 例として、Zsh 上で Rust 製の fuzzy selector である skim と連携した例を紹介します. 下のコードを ~/.zshrc などに書いておくことで, Ctrl+Gリポジトリの一覧を skim で表示し, 選択したディレクトリに移動することが出来ます.

function __fuzzy-select-repositories() {
  local selected=$(rhq list | sk --prompt='REPOS> ' --query="$LBUFFER")
  if [[ -n $selected ]]; then
    BUFFER="cd \"${selected}\""
    zle accept-line
  fi
  zle clear-screen
}

zle -N __fuzzy-select-repositories
bindkey '^g' __fuzzy-select-repositories

Vim では Unite や ctrlp を使用することが考えられます. 例えば mattn/ctrlp-ghq を使用する場合は次のような感じになります.

Plug 'mattn/ctrlp-ghq'

let g:ctrlp_ghq_command = 'rhq'
let g:ctrlp_ghq_actions = [
  \   { "label": "Open", "action": "Explore", "path": 0 },
  \ ]

" <leader>g でセレクタを起動する
noremap <Leader>g :<C-u>CtrlPGhq<CR>

Visual Studio Code への組み込みですが,せっかくなので自前で拡張機能作ってみましたVisual Studio Code Marketplace で公開しているので ext install vscode-rhq などしてインストールして下さい.

おわりに

今後の計画として、せっかくなので少しリッチな機能を用意しようかなぁと思っています…

  • rhq clone および rhq foreach の並列化
  • custom hook
  • ホストの設定

特に並列化などは良さげな並列処理のライブラリを試してみる意味でも早急に実装しようと考えています…

プルリクエストやツッコミなど、随時募集しています。

最近作った・メンテしたRustプロダクト

Programming Rust

供養および近況報告

rustplotlib

github.com

Matplotlib の Rust バインディングが欲しいなぁと思っていたので勢いで作ったもの. Rust という言語の特徴を考えると必要となるケースはあまりない気がするが,作ってしまった以上便利なものであるとだけ言っておく.

使い方は,まず下のように Builder でグラフを構築した後,

extern crate rustplotlib;
use rustplotlib::{Figure, Axes2D, Scatter, Line2D, FillBetween};

let x: Vec<f64> = (0..40).into_iter().map(|i| (i as f64) * 0.08 * PI).collect();
let y1: Vec<f64> = x.iter().map(|x| x.sin()).collect();
let y2: Vec<f64> = x.iter().map(|x| x.cos()).collect();

let ax1 = Axes2D::new()
  .add(Scatter::new(r"$y_1 = \sin(x)$")
    .data(&x, &y1)
    .marker("o"))
  .add(Line2D::new(r"$y_2 = \cos(x)$")
    .data(&x, &y2)
    .color("red")
    .marker("x")
    .linestyle("--")
    .linewidth(1.0))
  .xlabel("Time [sec]")
  .ylabel("Distance [mm]")
  .legend("lower right")
  .xlim(0.0, 8.0)
  .ylim(-2.0, 2.0);

let ax2 = Axes2D::new()
  .add(FillBetween::new()
    .data(&x, &y1, &y2)
    .interpolate(true))
  .xlim(0.0, 8.0)
  .ylim(-1.5, 1.5);

let fig = Figure::new()
  .subplots(2, 1, vec![Some(ax1), Some(ax2)])

Figure::apply(&mut backend) を呼び出すことで実際の描画に反映させる.

use rustplotlib::Backend;
use rustplotlib::backend::Matplotlib;

let mut mpl = Matplotlib::new().unwrap();
mpl.set_style("ggplot").unwrap();

fig.apply(&mut mpl).unwrap();

mpl.savefig("simple.png").unwrap();
mpl.dump_pickle("simple.fig.pickle").unwrap();
mpl.wait().unwrap();

調子乗った名前をしている割には大した機能を実装できていないので,余裕が出来たら適宜機能を拡張していく予定…

rwm

github.com

なんかウィンドウマネージャ作ってみたくなったので作ったもの. といってもウィンドウマネージャを実装するための知識が皆無だったので,以下のサイトで紹介されている Standard ML の実装をベースに Rust に移植した.

MLでウィンドウマネージャ作成

現状はリサイズ時の処理に不具合があり,クリック場所が悪いと panic してウィンドウマネージャが終了する.

rhq

github.com

以前作成した ghqrs という ghq クローンを再度書き直したもの. ghqrs は元々 Bash on Windowsgo build が動かなくてムキーッとなって作ったという経緯があり, メンテナンスするのが面倒になった 実装の所々に気に入らない箇所が目立ったので作り直した.

おわりに

  • 生きるの辛い
  • Windows10 Creators Update で Rust が BashOnWindows でも動くようになるらしいですね(今も動いてない…?)
  • ご意見,プルリクエストなどお待ちしています

rustup を Vim から呼び出すプラグインを作りました

Programming Rust Vim

タイトルの通りです。 少し前に rustup のメジャーアップデートがアナウンスされたばかりですがそれとは関係ないです。

Vim のタブライン上に使用中の Rust のツールチェインを表示するように設定を弄っているときに思いつき、この手のプラグインが見つからなかったことを踏まえて勢いで作りました。

github.com

Rust 的にも Vim script 的にも特に難しいことをしていることはなく、単に関数名に対応したコマンドライン引数で rustup を実行し、得られる標準出力を文字列を返す仕様にしました。

一応セールスポイントを挙げておくと、 rustup#active_toolchain() という関数を呼び出すことで現在有効になっているツールチェインを取得することができるようになっています。

使用例として、タブラインの右端に現在使用しているチャンネルを表示するための設定を下に書いておきました(Vim まわりの知識が乏しいのであまり良い例ではないですがご了承ください)。

let s:current_rustup_toolchain = ''

function! s:rustup_toolchain()
  if s:current_rustup_toolchain !=# ''
    return s:current_rustup_toolchain
  endif
  try
    let s:current_rustup_toolchain = rustup#active_toolchain()
  catch
  endtry
endfunction

function! s:make_tabline()
  let envstatus = ''

  let toolchain = s:rustup_toolchain()
  if toolchain !=# ''
    let toolchain = split(toolchain, '-')[0]
    let envstatus = envstatus . '[rust:' . toolchain . ']'
  endif

  return '%#TabLineFill#%T%=' . envstatus
endfunction


set showtabline=2
set tabline=%!tabline#make_tabline()
augroup TabLine
  autocmd!
  autocmd VimEnter * call s:tabline()
augroup END

あまり活用する機会が無い気もしますが、Rustacean かつ Vimmer な方々はぜひお試しください。

Rust 製の汎用機械学習ライブラリ rusty-machine

Rust Programming Machine Learning

本記事は Rust Advent Calendar 2016 10日目の記事です。

Rust Advent Calendar 2016 へ多くの方にご参加いただき、(一応)主催者としては嬉しい限りです。 遅くなってしまいましたが、この場を借りてお礼を申し上げます。

今回は Rust 製の汎用機械学習ライブラリである rusty-machine を紹介します。

rusty-machine

rusty-machine は Rust で書かれた汎用の機械学習ライブラリです。 100% Rust で書かれており、外部ライブラリへの依存なく使用できることが特長です。

github.com

公式の README によると、2016年12月10日現在以下の手法に対応しています。 scikit-learn など既存の機械学習ライブラリと比較するとまだ足りない機能もありますが、そのあたりは今後の発展次第でしょうか。

教師あり学習:

教師なし学習:

使用例 : 混合ガウスモデルの教師なし学習

使用例として、混合ガウスモデルによる教師なし学習のサンプルを作成しました。ただ、学習結果を表示する方法について延々と考えていたら時間切れになってしまい後半部分がごちゃごちゃになっているのでご注意ください…

extern crate csv;
extern crate rand;
extern crate rustc_serialize;
extern crate rusty_machine as rm;
extern crate rmp_serialize as msgpack;

use rand::distributions::IndependentSample;
use rm::linalg::{Vector, Matrix};
use rm::learning::gmm::{CovOption, GaussianMixtureModel};
use rm::learning::UnSupModel;

#[allow(dead_code)]
#[derive(Debug, Clone, RustcDecodable)]
struct Iris {
    sepal_length: f64,
    sepal_width: f64,
    petal_length: f64,
    petal_width: f64,
    class: i32,
}

fn load_iris_dataset() -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
    let mut reader = csv::Reader::from_file("./data/iris.csv").unwrap().has_headers(true);

    let mut dest = Vec::new();
    for row in reader.decode() {
        let row: Iris = row.unwrap();
        dest.push(vec![row.sepal_length, row.sepal_width]);
    }

    let mut rng = rand::thread_rng();
    let dist = rand::distributions::Range::new(0.0, 1.0);

    dest.iter()
        .cloned()
        .partition(|_| dist.ind_sample(&mut rng) >= 0.02)
}

fn into_matrix(data: &Vec<Vec<f64>>, size: usize) -> Matrix<f64> {
  Matrix::new(data.len(), size, data.iter().flat_map(Clone::clone).collect::<Vec<f64>>())
}

fn main() {
    // read iris dataset.
    let (train_inputs, test_inputs) = load_iris_dataset();

    // construct Gaussian mixture model.
    let mut gmm = GaussianMixtureModel::new(2);
    gmm.set_max_iters(100);
    gmm.cov_option = CovOption::Full;

    // train.
    let inputs = into_matrix(&train_inputs, 2);
    gmm.train(&inputs).unwrap();

    // calculate
    let inputs = into_matrix(&test_inputs, 2);
    let probs = gmm.predict(&inputs).unwrap();

    plot_result(train_inputs,
                test_inputs,
                probs.into_vec(),
                gmm.means().cloned().unwrap(),
                gmm.covariances().cloned().unwrap(),
                gmm.mixture_weights().clone());
}

fn plot_result(train_inputs: Vec<Vec<f64>>,
               test_inputs: Vec<Vec<f64>>,
               probs: Vec<f64>,
               means: Matrix<f64>,
               covariances: Vec<Matrix<f64>>,
               mixture_weights: Vector<f64>) {
    #[derive(RustcEncodable)]
    struct Value {
        train_inputs: Vec<Vec<f64>>,
        test_inputs: Vec<Vec<f64>>,
        probs: Vec<f64>,
        means: Vec<f64>,
        covariances: Vec<Vec<f64>>,
        mixture_weights: Vec<f64>,
    }
    let val = Value {
        train_inputs: train_inputs,
        test_inputs: test_inputs,
        probs: probs,
        means: means.into_vec(),
        covariances: covariances.into_iter().map(|c| c.into_vec()).collect(),
        mixture_weights: mixture_weights.into_vec(),
    };

    use rustc_serialize::Encodable;
    use msgpack::Encoder;
    let mut buf = Vec::new();
    val.encode(&mut Encoder::new(&mut buf)).unwrap();

    use std::io::Write;
    use std::process::{Command, Stdio};
    let mut child = Command::new("python")
        .arg("./scripts/plot.py")
        .stdin(Stdio::piped())
        .spawn()
        .unwrap();

    child.stdin.as_mut().unwrap().write_all(&buf[..]).unwrap();

    child.wait_with_output().unwrap();
}

リポジトリGitHub に公開しておきました。ちまちま更新するかもしれませんがこちらもご参照ください。

github.com

おわりに

正直 Python に比べると必要な crate がまだまだ不足しているため苦労しますが、今後の発展次第では快適に機械学習を組み込んだアプリケーションを作成することができると感じました(雑)。

以上、Rustアドベントカレンダー 10日目の記事でした。

Rust のコマンドラインオプション解析色々

Rust Programming

本エントリは Rust その2 Advent Calendar 2016 - Qiita の 10 日目の記事です。

本記事では、Rust を使って CLI アプリケーションを作成してきた過程で得られた知見のひとつとして、コマンドラインオプション解析に用いる crate を簡単にまとめたいと思います。

基本事項

コマンドライン引数は std::env モジュールの args() (または args_os() )を用いて取得します。 最初の要素には通常実行ファイルのパスが格納され、引数自体は2番目以降に格納されます。

use std::env;

fn main() {
  let program: String = env::args().next().unwrap();
  let args: Vec<String> = env::args().skip(1).collect();
}

getopts

getopts は公式の提供しているコマンドライン解析用の crate です。 使い勝手としては Python の argparse が近いと思います。

github.com

getopts の特長として、必要最小限の機能しか提供していないためシンプルであり、導入における敷居が低いことが挙げられます。 そのため、単純なオプションを解析を行う場合にはこれで十分事足りると思います。

使い方は以下のようになります(公式ドキュメントの例を一部修正)。

extern crate getopts;
use std::{env, process};
use getopts::Options;

#[derive(Debug)]
struct Args {
  input: Vec<String>,
  output: Option<String>,
  // ...
}

fn print_usage(program: &str, opts: &Options) {
  let brief = format!("Usage: {} FILE [options]", program);
  print!("{}", opts.usage(&brief));
  process::exit(0);
}

fn parse_args() -> Args {
  let args: Vec<String> = env::args().collect();
  let program = args[0].clone();

  let mut opts = Options::new();
  opts.optopt("o", "", "set output file name", "NAME");
  opts.optflag("h", "help", "print this help menu");
  // ...

  let matches = opts.parse(&args[1..])
    .unwrap_or_else(|f| panic!(f.to_string()));

  if matches.opt_present("h") {
    print_usage(&program, &opts);
  }

  if matches.free.is_empty() {
    print_usage(&program, &opts);
  }

  Args {
    input: matches.free.clone(),
    output: matches.opt_str("o"),
    // ...
  }
}

fn main() {
  let args = parse_args();
  println!("{:?}", args);
}

docopt.rs

docopt.rs はドキュメント文字列を元にオプション解析器を生成する crate です。 多言語にも同名のパッケージがあると思いますが、それらと同じ感覚で使用することが出来ます。

github.com

docopt.rs 特有の機能として、RustcDecodable を実装した構造体を用いてオプションの解析結果を 受け取ることができます。 また、nightly 限定ですがドキュメントを解析して解析結果を受け取る構造体を自動生成する docopt! という マクロを使用することでオプション解析を簡略化することが出来ます(僕は stable 派なので使ったことないですが…)。

使用例は以下の通りです。

extern crate docopt;
extern crate rustc_serialize;

use docopt::Docopt;

const USAGE: &'static str = r"
JSON version of xargs
Usage:
  jsonargs [--parallel] <name> [<args>...]
  jsonargs (-h | --help)
Options:
  -h --help     Show this message.
  --parallel    Run each command parallel
";

#[derive(Debug, RustcDecodable)]
struct Args {
  flag_parallel: bool,
  arg_name: String,
  arg_args: Vec<String>,
}

fn main() {
  let args = Docopt::new(USAGE)
    .and_then(|opt| opt.decode())
    .unwrap_or_else(|e| e.exit());
  let Args { arg_name: name, arg_args: args, flag_parallel: parallel } = args;

  // ...
}

docopt.rs の詳細な使用方法ははすでに良質な解説記事がありますのでそちらもご参照ください。

qiita.com

clap.rs

getoptsdocopt.rs はどちらかと言うと小規模なアプリケーション向けのパーサであり、 複数のサブコマンドを用いるなど複雑なオプションが必要なアプリケーション向きでは無いです。

複雑なコマンドラインオプションの解析には clap.rs を用いることが出来ます。

github.com

claprustup でも用いられているコマンドラインパーサであり、 公式リポジトリで紹介されている機能一覧を見てもわかるように とにかく高機能です(語彙力)。

とりあえず気になるものだけ列挙してみると、次のような機能があります。

  • 色付きのヘルプメッセージの出力に対応
  • YAML によるオプション定義への対応
  • 各種シェル用の補完スクリプトの自動生成 (Bash, fish shell, Zsh)

使用例は以下の通りです。

extern crate clap;
use clap::{App, AppSettings, Arg};

fn build_app() -> clap::App<'static, 'static> {
  let program = std::env::args()
    .nth(0)
    .and_then(|s| {
      std::path::PathBuf::from(s)
        .file_stem()
        .map(|s| s.to_string_lossy().into_owned())
    })
    .unwrap();

  App::new(program)
    .about("find files")
    .version("0.0.1")
    .author("Author name <author@example.com>")
    .setting(AppSettings::VersionlessSubcommands)
    .arg(Arg::from_usage("-i --ignore=[IGNORE]   'Ignored pattern'"))
    .arg(Arg::from_usage("-m --matches=[MATCHES] 'Pattern to match'"))
    .arg(Arg::from_usage("-a --absolute          'Show absolute path'"))
    .arg(Arg::from_usage("-d --directory         'Show only directories'"))
    .arg(Arg::from_usage("-A --async             'Search asynchronously'"))
    .arg(Arg::from_usage("-M --max-items=[N]     'Limit of displayed items'"))
    // ...
}

fn main() {
  let matches = build_app().get_matches();
  let matchre : Option<&str> = matches.value_of("matches");
  let ignore: Option<&str> = matches.value_of("ignore");
  // ...、
}

個人的に便利だと思ったのが、各種シェル用の補完関数を自動生成する機能です。 例えば Zsh 用の補完スクリプトを生成する場合は次のようにすることで可能です。

use clap::Shell;

let mut file = std::fs::OpenOptions::new()
  .write(true)
  .create(true)
  .open(concat!("_", env!("CARGO_PKG_NAME")))
  .unwrap();

build_app().gen_completions_to(env!("CARGO_PKG_NAME"), Shell::Zsh, &mut file);

おわりに

以上、Rust でコマンドラインオプションの解析に使える crate の紹介でした。

近況

衝動的に Rust で色々作ったので、宣伝がてら忘れないようにまとめておく。 正直 Rust 関係ない気もするが気にしない。 あと全体的にドキュメントが貧弱なので他の人に使ってもらうためにはもう少し頑張らないといけない気がする。

ghqrs

github.com

Git のローカルリポジトリの管理ツール。 名前からお察しの通り ghq の Rust クローンである。 といっても ghq との互換性はそこまで意識しておらず、自分が使いやすければ良いかなぁというモチベーションでコマンドラインオプションを設定した。 現状は対応している VCS が Git だけだったりと色々物足りないのでそのうち本腰入れて開発を進めたいと考えている。

dot.rs

github.com

dotfiles の管理ツール。 もともと dotfiles の管理には ssh0 氏の作った dot というツールを使っていたのだけど、どうせなら bash/zsh に依存しないものが欲しくなり作ってみた。

github.com

Windows で使うことを意識したため、シンボリックリンクの作成時に自動的に管理者へと昇格するなどの機能をつけた。

なお、実装時には下のプロジェクトを微妙に参考にした(マッピング機能など)。というより、このリポジトリを見つけてしまったのが開発の動機だったりする。

github.com

vcs_info.rs

github.com

こちらはバージョン管理システムの情報をプロンプトに表示するためのツール。 Zshvcs_info が遅かったのでかっとなって作ってみた。 現状は Git, Mercurial, Subversion のみに対応している。 表示形式の変更などはまだ対応していないが、そのうちするかも。

これを作る過程で Go 言語の勉強がてらも作ってみたので一応紹介しておく。 github.com

regrun.rs

github.com

レジストリに登録された Path の値を検索してプログラムを実行する CLI ツール。 これはまだ始めたばかりのプロジェクトで、今後の展開次第では direnv 的なものに移行する可能性がある。

おわりに

最近精神が荒んできている気がするので安寧がほしい