LEFログ

:smile

gstでファイルに変更を加えずGitの状態をまとめて安全に見る

変更ファイルをgit status で確認したり、履歴を確認するために git log を見たり、差分を確認するために git diff を使ったり……。

作業中に何度もこれらのコマンドを行ったり来たりしていると、「今見たい情報をまとめて見られたら楽なのにな〜」と感じることがあります。

そんなわけで、Git の状態をまとめて確認できる CLI ツール gst を作りました。

github.com

作った動機

プログラミングを始めた知人にGitについて教えていたら、

  • git statusよくわからぬ
  • 色んなコマンド覚えるの大変
  • lazygitみたいなツールがあるって聞いたけど、誤って操作して状態を書き換えるのが怖い

みたいなことを言われました。

ならその問題を解決できるツールを作っちゃえばいいのでは? というのが開発したきっかけです。

gst で見られるもの

gst を起動すると、現在のブランチ、リモートとの差分、変更されたファイル、ステージ済みの変更、ワーキングツリーの差分、stash、コミットグラフなどを、ターミナル上でまとめて確認できます。

gst overview

デモ動画

画面の雰囲気は画像だけだと伝わりにくい部分もあるので、デモ動画も用意しました。

実際にどう動くのか、どんな感じで Git の状態を確認できるのかは、動画を見ると分かりやすいと思います。

youtu.be

https://youtu.be/EMO3DaNkqT0

インストール

インストール方法は2種類あります。

Go を使う方法はこちら。

go install github.com/lef237/gst/cmd/gst@latest

homebrewにも対応させました。次のコマンドで入ります。

brew install lef237/tap/gst

使い方

通常は、リポジトリ内で次のように実行します。

gst

一度だけ表示して終了したい場合は、--once を付けて実行します。

gst --once

TUI 上では、タブキーや矢印キーでビューを移動できます。

r で再読み込み、q で終了です。

読み取り専用

gst は、リポジトリの状態を確認するためのツールです。

pushpullcheckoutcommitmergerebase のように、リポジトリの状態を変更する操作は行いません。

Git にあまり慣れていない場合でも、何かを間違って変更してしまう心配が少ないので、状態確認用のツールとして使いやすいと思います。

差分表示

Git では、ステージ済みの変更と、まだステージしていないワーキングツリー上の変更が分かれて管理されます。

gst では、変更されたファイルに INDEXWORKTREENEWCONFLICT などのマーカーを付けて表示します。 そのため、どのファイルがステージ済みで、どのファイルがまだワーキングツリー上の変更なのかを把握しやすくなっています。

差分ビューでは、ワーキングツリーの差分とステージ済みの差分を切り替えて見ることができます。

次のコミットに何が入るのかを確認したいときや、コミットを分ける前に変更内容を整理したいときに役立ちます。

差分のコピー機能

差分ビューでは、表示している差分をそのままクリップボードにコピーできます。

yank diff from gst

y を押すとワーキングツリーの差分をコピーできます。

i を押すとステージ済みの差分をコピーできます。

コピーされる内容は patch 形式なので、git apply でそのまま適用できます。

また、a を押すと、HEAD から現在のワーキングツリーまでの変更をまとめた patch をコピーできます。(allを選べば未追跡ファイルも含めることができます)

作っている途中で閃いて実装したのですが、この機能、すごく便利だと思います。

まとめ

gst は、Git の操作を置き換えるためのツールではありません。

どちらかというと、何か操作をする前に「今リポジトリがどういう状態なのか」を確認するためのツールです。

現在のブランチ、リモートとの差分、変更内容、コミット履歴などを一画面で見られるので、作業前の確認や、作業中の状態把握が少し楽になります。

Git の状態確認をもう少し手軽にしたい方は、ぜひ試してみてください!

Mugaのv0.1.0をリリースしました

自分が今月から開発を進めていた言語 Muga について、crates.ioで公開しました。

Mugaの公開ページ

https://crates.io/crates/muga

v0.1.0なのでまだあまり機能はないですが、気軽に試しやすい状態になったと思います。

cargoがあれば次のコマンドですぐインストールできます。

cargo install muga

あとは、ファイルを作成して実行するだけです。

muga hoge.muga

リリースについてはもう少し機能を追加してからおこなう予定だったのですが、最近tanstackが非公式な第三者に名前をtake overされている事件*1を見て、早めに名前を取っておいたほうが良いかもなぁと判断してcrates.ioでの配布を決めました。

v0のうちはガシガシコードを書いてどんどん変更をしていく予定です。仕様や機能が安定したら、v1にしようと思います。

現状どんなコードを動かせるかについては、samples/ を見て頂けると分かりやすいです。

ぜひ試してみてね🥁

Mugaという名前のプログラミング言語を作っています

Muga という新しいプログラミング言語 作っています。まだ初期の実験段階ですが、仕様と実装を GitHub に公開しました。

https://github.com/lef237/muga

Rust製です。

cargoが使えれば、READMEのQuickstartを読んですぐにプログラムを実行できると思います。

動画

百聞は一見に如かず!ということで動画を用意しました。実際に動いているところが分かります。

youtu.be

https://youtu.be/ndEshgkMVgU

動画内では正常系と異常系の両方を試しています。テキストで貼り付けるとこんな感じです。

$ cargo run -- samples/inferred_types.muga
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/muga samples/inferred_types.muga`
10
10

正常系の場合はちゃんと出力されています。

うまくいかない場合はtype mismatchのエラーもちゃんと出力されます。

$ cargo run -- samples/inferred_types.muga
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/muga samples/inferred_types.muga`
10:12: T002 type mismatch: expected Int, found String

設計判断

Muga の設計にあたっていくつか決めたことがあります。今のところは次の2つ。

基本的に immutable

すべての束縛はデフォルトで immutable。変更したいときだけ mut を明示的に書きます。

x = 1          // immutable
mut total = 0  // mutable
total = total + x

letにするか迷ったのですが、letは言語ごとに意味的なばらつきがあるので、mutableを推測しやすいmutにしました。

shadowing を禁止

同じスコープで同じ名前を再束縛することはできません。これはElmの影響を受けています。

すごく悩んだのですが、決め手となったのはRedditのこのコメントかも。

https://www.reddit.com/r/rust/comments/1e4p5hb/comment/ldgjpp4/?tl=ja&utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

引用は長いのでdetailsに囲みます。

A lot of people are talking about why they think shadowing is fine, but in order to attempt to answer the question:

People don't like shadowing because, in the enterprise context, you often deal with code that is 1) not written by you and 2) a little messier (often longer) than you'd personally like. These two things combine to make shadowing a source of confusion and mental overhead.

Let's take the example you used from another comment:

for rr in sol.sln.rr.iter() {
    for rr in rr.iter() {
        if !rr.is_finite() {
             println!("ERROR: Solution diverged at timestep: {}:{}", iter, m);
             break 'outer;
        }
    }
 }

That's all well and good --- it's fairly easy to see that the rr in the if statement is not the same rr from the initial for loop. However, if this function evolves from what it is now to something like:

for rr in sol.sln.rr.iter() {
    rr.doStuff();
    some_other_function();
    maybe_this_one_takes_rr();
    // ... 10 more lines

    for rr in rr.iter() {
        some_other_calls_unrelated_to_rr();
        // ... 10 more lines, maybe a loop or conditional

        if !rr.is_finite() { // Now --- what is rr and where did it come from? What's its type?
             println!("ERROR: Solution diverged at timestep: {}:{}", iter, m);
             break 'outer;
        }
    }
 }

Now, by the time you get to the if statement inside the nested loop, in order to properly understand which rr you're talking about, you have to carefully read (and keep in your working memory) the entire function context from the start; you can't just skim from the top for the declaration of the variable. Heck, if you do skim from the top, you might end up thinking rr is a totally different type than it actually is! This is particularly awful for immutable values, since you might see an immutable declaration at the start of a function and assume you know to what data that variable name refers, but then it changes over the course of the function (bc now it's shadowed).

You might counter --- "but this is because of bad coding practice; if you kept your functions small and only carefully used shadowing, you wouldn't have this issue!" You're right. But, the same is true about memory errors in unsafe languages, or type errors in languages like Python or Javascript.

Even though careful programmer discipline can prevent many problems, in the long-term, multiple-maintainer context, relying on careful programmer discipline just isn't sound. Instead, people like language features to forcibly reduce mental overhead and the required program context you have to keep "in [human] memory."

コード例

https://github.com/lef237/muga/tree/main/samples

このリンクに色々置いていますが、一番分かりやすいものを貼ってみます。*1

record Counter {
  value: Int
}

fn start(n: Int): Counter {
  Counter {
    value: n
  }
}

fn inc(counter: Counter): Counter {
  counter.with(value: counter.value + 1)
}

fn double(x: Int): Int {
  x * 2
}

fn main(): Int {
  // This chain is equivalent to:
  // double(inc(inc(start(10))).value)
  10.start().inc().inc().value.double()
}

関数をネストすることもできるし、ドットを使ってチェーンもできます。好きな方を自由に選べるようにしています。

recordはC#から影響を受けました。書き換え不可を基本にしたかったので、StructではなくてRecordにしています。

レコード - C# reference | Microsoft Learn

ファイルは.mugaという拡張子にしました。

名前の由来

"muga" は日本語の「無我」から取りました。無我夢中でプログラミングに取り組めるような言語を目指しています。

キャッチフレーズは A quiet programming language.

簡単な現状紹介

だいたいこんな感じです。

  • Rust 実装 (parse / name resolution / type check / HIR / bytecode VM) が動作
  • 18 件のテストをpass
  • ライセンスは MIT
  • 未実装: receiver-style 関数の解決規則、関数型の型注釈 A -> B、など

仕様ドキュメントの方が実装より少し先行していて、spec/ ディレクトリに章ごとの草案があります。

なんでつくりはじめたの?

好きな言語をいいどこ取りした最強のプログラミング言語を作りたくてェ…

これから

まだシンプルな機能しか実装されていませんが……動きます!

(あとでmuga foo.mugaで実行できるようにする予定です)

もしかしたらフィードバックしてくれる方がいらっしゃるかもしれないので早い段階で公開してみました。

設計も実装も徐々に育てていこうと思っています。

気になった点、疑問、設計へのツッコミ、Issue でも pull-req でもなんでも歓迎します。

リポジトリはこちら

github.com

https://github.com/lef237/muga

*1:シンタックスハイライトはいったんRustのものを使っています

macOSでサクッとローカルLLMを動かしてOpenCodeやCodexで使ってみる

サクッとローカルLLMをmacOSで動かしたい方向けの記事です。macbook neoが流行っているので書いてみました。

ノートPCでも動きやすいモデル選びについても自分なりにまとめました。

ちなみに、タイトルにはOpenCodeと書いていますが、Claude CodeやCodexからもローカルLLMを使えます。

Homebrew

Homebrewが入っている前提です。homebrewを知らない方のために念のため説明すると、様々なツールを簡単に管理するためのものです。インストール方法はネット上にたくさん記事があります。

Ollamaを入れて動かすまで

まずはOllamaをインストールします。OllamaはCLIで操作できるローカルLLMツールです。

brew install ollama

インストールできたらサーバーを起動します。

ollama serve

次にモデルを指定して走らせます。

モデルはここからPopularなものを探せます。

ollama.com

BというのはBillionのことで、パラメータの数。パラメータが少ないものは小型のモデルなのでノートPCでも動きやすいです。

サイズはOllamaからページを開くとこのような感じで載っています。

今回はministralを選んでみます。

ministral-3のモデル表

https://ollama.com/library/ministral-3

ministral-3:3bはサイズが小さそうですね。(ざっくりした目安ですが、3b以下だとロースペックなPCでも動きやすいと思います。)

ということで、動かしてみます。

ollama run ministral-3:3b

初回だけダウンロードに時間がかかりますが、そのあとは普通に動かせます。

実際に動かした図

普通に使うぶんにはこれで終わりです。

翻訳などのちょっとしたプロンプトにはこれで充分だと思います。

コーディングエージェントを使う

せっかくローカルLLMを入れたのだから、コーディングエージェントを使いたいですよね。

ということでやっていきます。

まずはOpenCodeを使います。

opencode.ai

homebrewでインストールできます。

brew install anomalyco/tap/opencode

OpenCodeを使えるようになれば後は簡単です。

先ほどのollamaのministral-3のページに次のように表示されていました。

Applicationsの項目

ここに書いてあるとおり、次のコマンドを実行しましょう。

ollama launch opencode --model ministral-3

先ほどと同様、小さいモデルを使いたい場合は、ministral-3:3bを指定してください。

ollama launch opencode --model ministral-3:3b

あとはこのように、OpenCode上でministral-3:3bが使えます。

opencodeでministral-3:3bが表示される

さあ、使ってみましょう。

opencode経由で使ってみた

お、ちゃんとREADMEの内容を読んでくれてそう。

次はコードを書いてくれるかな?

コードを書いて その1

コードを書いて その2

実際に書けてるのか、別のウィンドウを開いて確認してみます。

❯ ruby hello_world.rb
hello world!

OKそう。

Claude CodeとCodexでも動かしてみる

ちなみにClaude CodeやCodexも、先ほどのOllamaのモデル説明のページに書かれたコマンドを実行するだけで使えます。(3bを指定するので少し書き換えます)

ollama launch claude --model ministral-3:3b
ollama launch codex --model ministral-3:3b

参考に、実行した様子の画像も添付しちゃいます。

claude codeで実行した様子

codexで実行した様子

ここまでやって、PCがめちゃくちゃ熱くなってきたので中断しました。

動かした感想

手元のPCで動くのは感動。だけどやっぱり指示を伝えてもうまくいかないことも多くて、モデル次第だなぁ、という印象。

このあとちょっと複雑めのプログラムをお願いしたところ(ちょっとしたunixコマンド)、すぐにハルシネーションを起こしてしまいました。

軽いモデルだと、実用性はあまりないかもしれません。使えるとしても、コードを読み解くのになんとか使える、という感じです。(Readにはギリギリ使えるけど、Writeは厳しそうな印象)

あと、結構頻繁にハルシネーションするし、嘘もつきます。嘘を嘘だと(ry…間違いを間違いだと見抜ける力がないと、扱いきれないかもしれないです。

おもちゃとしては最適です。みなさんもぜひ使ってみてください。

オチ

最速の人間、ハル・マーシャル

地球上の陸上記録: もし「地球上の陸上競技で最速の記録」を指すのであれば、それは陸上競技の100m走で、日本のハル・ マーシャルが持つ 9.58秒 の記録が最速です。

ハル・マーシャルって誰ぞ?

送信前の下書きに使えるエディタ One-Time Editor をリリースしました

One-Time Editorというアプリを作りました。

One-Time Editorの見た目

上のはダークモード。ライトモードだとこんな感じ。

ライトモードの見た目

どんなアプリかというと、チャット送信前の下書きに使えるテキストエディタです。

このアプリを使うことで、Enterで誤って送信を誤爆する心配から逃れられます。

どんな使い勝手かについては動画を見ると速いと思うので、撮影したものを下に添付します。

youtu.be

https://youtu.be/qwj9fr77vQg

インストール方法

macOSの場合はhomebrewを使います。ターミナルで次のコマンドを打つだけですぐに使い始められます。

brew tap lef237/tap
brew install --cask one-time-editor

Electron製のアプリなので、一応クロスプラットフォームにも対応しています。

WindowやLinuxの方は、ビルド済みのアプリをここからインストールできます。

github.com

https://github.com/lef237/one-time-editor/releases

Windowsの方はexe、Linuxの方はAppImageを選択すればたぶんインストールできるはず。*1

特徴

一番のポイントは、ショートカットキーでウィンドウをトグル(表示/非表示)できることです。

しかも、トグルと同時に入力した内容をコピーしてくれるので、一瞬でペーストできます。

  1. ショートカットキーでOne-Time Editorを開く
  2. 送信したい内容を入力する
  3. ショートカットキーで閉じる
    • 自動で内容もコピーされます
    • macOSの場合は、自動で直前に操作していたウィンドウにフォーカスが戻ります
  4. ペーストして、後は送信するだけ

ライトモードとダークモードの切り替えにも対応しています。デザインもお洒落だと思います。

もちろん、ショートカットキーもカスタマイズできます! デフォルトはCtrl+jですが、好きなショートカットに割り当ててください。

なぜ作ったのか

チャットなどの送信で、EnterかShift+Enterか、はたまたCtrl+Enterなのかに悩んだ経験がある方は多いと思います。

そんでもって、文章の作成途中で誤爆してしまったり……

何かしらのエディタを使って下書きを書くのも悪くはないのですが、毎回ファイルを作成するかを聞かれたり、ウィンドウをどう管理するかがやや面倒だったりします。

このアプリの場合は書き捨ての用途で使えるし、1つのショートカットで表示/非表示&内容のコピーをしてくれるので、かなりスピーディかなと。

開発で苦労したところ

いくつかありました。

が、一番難しかったのは、コード署名と公証をどうするか問題でしょう。

Electronでアプリを作ると、手元で動かす分には問題ないのですが、ネットに上げてダウンロードするとアプリが壊れてしまいます。

Dockerなどのダウンロード時に見たことある方もいらっしゃるのではないでしょうか?

このアプリは壊れているため開けません、の表示

なので、xattrコマンドを使う必要があります。

qiita.com

https://qiita.com/quattro_4/items/f5b56c1897c0cc235c0f

つまりインストールしたあとに、このコマンドxattr -rc "/Applications/hoge.app"を打つ必要があります。これはやや面倒です。

シンプルにできないか検討したところ、postflightを使えば、homebrew-tap経由なら自動化できることに気づきました。

Electronアプリを配布したい方は参考にしてみてください!

GitHub

GitHubも公開しています。

追加の機能の要望があればIssueなどお待ちしています。🐘

github.com

https://github.com/lef237/one-time-editor

*1:もしWindows等で動かなかったらGitHubにコメントください!

Rails の find はどう動く? Active Recordの内部実装を見てみよう!

この記事はフィヨルドブートキャンプ Advent Calendar 2025の22日目の記事です。

昨日はodentakashiさんのgem の PR から学んだ ActiveRecord と lease_connection の話でした!

自分もおでんくんに引き続き、ActiveRecordの話題、いっちゃいます🍢

はじめに

皆さんはRuby on Rails使っていますか? Ruby on RailsにはActive RecordというORMがあります。

RailsのActive Recordを使う上で欠かせないのが、『Ruby on Railsガイド(以下Railsガイド)』です。この公式ドキュメントを読むことで、全体像を理解しながら体系的に学ぶことができます。

railsguides.jp

Ruby on Rails ガイド:体系的に Rails を学ぼう

Railsガイドには載っていないような細かい挙動を調べたいときもあります。そんなときは、『Ruby on Rails API』を調べるのが鉄板です。

api.rubyonrails.org

Ruby on Rails API

この2つのドキュメントを参考にするだけで、基本的には困らないのですが……もっと先へ進んでみたくないですか?

そうです。Ruby on Railsそのもののソースコードです。今回は find を取り上げて、このメソッドを読み解いていきましょう。

find ってそもそもなに?

まずはおさらいから。

ActiveRecord の find は 主キー検索 を行うためのメソッドです。

User.find(1)        # id=1 の User を取得
User.find([1, 2, 3]) # 複数レコードも取得可能

シンプルではありますが、内部ではキャッシュの利用、例外の扱い、SQL の組み立てなど様々な処理が行われます。

どこで定義されている?

では、findメソッドはどこで定義されているでしょうか?

GitHubにある rails/rails で調べてみましょう。

github.com

find メソッドは ActiveRecord の FinderMethods モジュールで定義されています。

具体的には次のファイルです。

それでもって、中のコードを読んでいくと、findが定義されています。

def find(*args)
  return super if block_given?
  find_with_ids(*args)
end

おっ、findは find_with_ids を呼んでいますね。今度はこのメソッドを見てみましょう。

  def find_with_ids(*ids)

    # 中略

    case ids.size
    when 0
      error_message = "Couldn't find #{model_name} without an ID"
      raise RecordNotFound.new(error_message, model_name, primary_key)
    when 1
      result = find_one(ids.first)
      expects_array ? [ result ] : result
    else
      find_some(ids)
    end
  end

このメソッドの最後の方を見ると、idsのサイズが0のときはエラー、1のときはfind_one、それ以外のときはfind_someを呼んでいます。

つまり、findfind_oneかfind_someを呼んでいる!ということです。*1

両方とも追いかけるとややこしいので、今回はidを一つしか渡さないと仮定して、find_oneを潜っていきましょう。

find_oneはこのように定義されています。

  def find_one(id)

    # 中略

    relation = if model.composite_primary_key?
      where(primary_key.zip(id).to_h)
    else
      where(primary_key => id)
    end

    record = relation.take

    raise_record_not_found_exception!(id, 0, 1) unless record

    record
  end

複合キーではない場合と仮定して、ものすごくざっくりと解説してみますが、つまり、whereメソッドで条件に合うものを検索して、そのなかから一つだけを take する。これがfind_oneがやっていることです。

whereはどうなってる?

つまりfindは内部的にwhereを呼んでいることが、Railsの実装を見ていくとわかります。

次にwhereを紹介していきます。*2

whereメソッドはこのファイルで定義されています。

具体的なコードだとここになります。

def where(*args)
  if args.empty?
    WhereChain.new(spawn)
  elsif args.length == 1 && args.first.blank?
    self
  else
    spawn.where!(*args)
  end
end

引数はempty?でもblank?でもないはずなので、とりあえずwhere!を見てみましょう。

def where!(opts, *rest) # :nodoc:
  self.where_clause += build_where_clause(opts, rest)
  self
end

どうやら、build_where_clauseっていうメソッドの結果を、どんどんオブジェクトに追加していっているらしいということがわかります。

build_where_clauseへ移動します。

  def build_where_clause(opts, rest = []) # :nodoc:
    opts = sanitize_forbidden_attributes(opts)

    if opts.is_a?(Array)
      opts, *rest = opts
    end

    case opts
    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end
    when Hash
      opts = opts.transform_keys do |key|
        if key.is_a?(Array)
          key.map { |k| model.attribute_aliases[k.to_s] || k.to_s }
        else
          key = key.to_s
          model.attribute_aliases[key] || key
        end
      end
      references = PredicateBuilder.references(opts)
      self.references_values |= references unless references.empty?

      parts = predicate_builder.build_from_hash(opts) do |table_name|
        lookup_table_klass_from_join_dependencies(table_name)
      end
    when Arel::Nodes::Node
      parts = [opts]
    else
      raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
    end

    Relation::WhereClause.new(parts)
  end
  alias :build_having_clause :build_where_clause

一気に難しくなってきました。

この引数に、例えば文字列を渡すとします(User.where("age > 20")みたいなやつ)。すると、when Stringの条件に入ります。

    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end

するとparts = [Arel.sql(opts)]が呼ばれるはず。そして、[Arel.sql("age > 20")]がpartsの中に入ります。*3

最後にRelation::WhereClause.new(parts)が呼び出され、WhereClause オブジェクトが生成されます。

この WhereClause オブジェクトが、Relation オブジェクトの where_clause フィールドに+=でどんどん貯まっていき、最終的にはSQLに変換されて、実行されます。*4

SQLに変換される仕組みや、実行される仕組みまで解説すると、かなり長くなってしまうので、このあたりで止めたいと思います。

おわりに

いかがでしたでしょうか? 後半はちょっとむずかしくなってしまいましたが、前半は分かりやすかったと思います。

RailsRubyのコードでできています。なので、Rubyに慣れて親しんでいれば、Railsのコードは理解できるようになっています。

もし、公式ドキュメントなどを読んでも分からない挙動に遭遇した場合は、Railsそのもののソースコードを読んでみると、原因を特定できるかもしれません。*5

コードを読み解くことで、勉強になる面は多いですし、貢献できる可能性もあります。みなさんもぜひ、トライしてみてください!


明日のAdvent Calendarは ゆーかさんと mh-mobile さんです。🎄

*1:ブロック無しで ID を渡した一般的なケースにおいて

*2:コードが難しいので、自信がない箇所もあります。間違いがあったらごめんなさい。🐧

*3:Arelについては、この記事が詳しく解説していてオススメです。→https://www.timedia.co.jp/tech/activerecord-arel-5-1/

*4:自分の理解が間違っていなければ、WhereClause が積み重なった Relation が、最終的に Arel を通して SQL に変換されて、take や to_a などで DB に対して実行されるはずです。

*5:実際の業務でも、理解を深めるために元のソースコードを読む場面はたびたびありました。

RustでCSVをJSONに変換するツールをリリースしました

概要

最近、Rustを使って、CSVJSONに変換するツールをリリースしました。

ctj - crates.io: Rust Package Registry

cargo install ctjですぐインストールして使うことができます。

2025-07-17現在のバージョンは0.1.8。ちょこちょこ機能を追加しています

このツールを作った理由について、簡単に書いてみたいと思います。

作った理由

一言でいうと、ヘッダ行がないCSVファイルにも対応をしたかったのが一番大きな理由です。他のツールを使うと情報が欠落することがありました。

また、CSVの真偽値や数値について、ツール側で文字列に変換されるのが困っていました。

具体的には…?

,b,
,,FALSE
,55.5,

このようなファイルがあったとき、別のツールを使うと次のような結果になります。

[
  {
    "": "FALSE",
    "b": ""
  },
  {
    "": "",
    "b": "55.5"
  }
]

大文字のFALSEがそのまま文字列として扱われています。また、55.5という数値も文字列になってしまっています。

真偽値と数値はそのままJSONで扱えるので、できればその型情報を識別し、同じように展開したいです。

それでは自分が新たに作成したツールを紹介しましょう。このctjを使うと、次のような結果になります。

[
  {
    "": false,
    "b": ""
  },
  {
    "": "",
    "b": 55.5
  }
]

FALSEfalseに、55.5は数値(float)として扱われます(小数点が無ければintegerとして処理されます)。

また、今回のCSVファイルはヘッダ行がありません。このような状況は実際にあり得ると思います。ヘッダ行がないと、情報の欠落が生じるリスクが高まります。

そのユースケースに対応するため、オプションを用意しました--no-headerをつけると、ヘッダ行がないことを ctj に伝えられるため、このように出力できます。

[
  {
    "column_0": "",
    "column_1": "b",
    "column_2": ""
  },
  {
    "column_0": "",
    "column_1": "",
    "column_2": false
  },
  {
    "column_0": "",
    "column_1": 55.5,
    "column_2": ""
  }
]

何列目にどの情報が入力されていたのか、すぐに把握できます。

無事に変換時の情報の欠落を防ぐという目的を達成することができました!

おわりに

あえて機能はシンプルにしつつ、実務に活かせるように設計してみました。すでにあるCSVファイルだけでなく、パイプで渡した文字列を処理することも可能です。

CSVJSONに変換するツールを探している方は、ぜひ使ってみてください。コントリビュートも歓迎です!

github.com