🕌

【Scala-CLI×SQLite】ワンファイルで作る “極小メモ帳” ── cs bootstrap に挫折したあなたへ

に公開

0. なにをつくるか

コマンド 挙動
./memo add "<本文>" メモ追加
./memo list 一覧表示 (id: 内容)
./memo delete <id> 指定DIを削除

SQLite に永続化し、1 ファイルのソースをそのままバイナリ化します。

1. 準備(1 分)

# Scala CLI
brew install scala-cli                    # v1.3 以上推奨

# (任意)SQLite CLI が欲しければ
brew install sqlite

2. Memo.scala を丸ごと書く

mkdir memo-app/bin && cd memo-app && touch Memo.scala

//> using scala "2.13.14"
//> using lib "org.xerial:sqlite-jdbc:3.50.3.0"
//> using jar resources

import java.sql.Connection
import java.sql.DriverManager
import scala.util.Using

object Memo {
    private val DbUrl = "jdbc:sqlite:memo.db"

    def main(args: Array[String]): Unit ={
        if (args.isEmpty) {usage(); return}
        initTable()

        args.head match {
            case "add" => add(args.drop(1).mkString(" "))
            case "list" => list()
            case "delete" => delete(args.lift(1).flatMap(_.toIntOption))
            case _ => usage()
        }
    }

    // サブコマンド

    private def add(text: String): Unit = 
        if (text.isEmpty) println("No memo text.")
        else{
            exec("INSERT INTO memos(content) VALUES (?)")(_.setString(1, text))
            println("Memo added.")
        }
    
    private def list(): Unit =
        query("SELECT id, content FROM memos") { rs =>
            while(rs.next()) println(s"${rs.getInt(1)}: ${rs.getString(2)}")
        }
    
    private def delete(idOpt: Option[Int]): Unit =
        idOpt match {
            case None => println("Need id.")
            case Some(id) =>
                val rows = exec("DELETE FROM memos WHERE id = ?")(_.setInt(1, id))
                println(if (rows == 0) "Not found." else "Memo deleted.")
        }
    
    private def usage(): Unit =
        println(
            """Usage:
            | memo add <text>
            | memo list
            | memo delete <id>""".stripMargin
        )

    // DB helpers
    private def withConn[A](f: Connection => A): A =
        Using.resource(DriverManager.getConnection(DbUrl))(f)

    private def exec(sql: String)(prep: java.sql.PreparedStatement => Unit): Int =
        withConn { conn =>
            val st = conn.prepareStatement(sql)
            prep(st)
            st.executeUpdate()
        }

    private def query(sql: String)(body: java.sql.ResultSet => Unit): Unit =
        withConn {conn =>
            val rs = conn.createStatement().executeQuery(sql)
            body(rs)    
        }

    private def initTable(): Unit =
        exec(
            """CREATE TABLE IF NOT EXISTS memos(
            | id INTEGER PRIMARY KEY AUTOINCREMENT,
            | content TEXT NOT NULL
            |)""".stripMargin
        )(_ => ())
}

“//> using …” って?

  • scala : コンパイルする Scala バージョン
  • lib : 依存ライブラリ (ここでは SQLite JDBC)

3. 一撃バイナリ化

# JVM 版(5 秒で終わる)
scala-cli package Memo.scala -o bin/memo     # bin/memo は JAR ランチャ

# 完全ネイティブ(GraalVM を自動取得 → 初回数分)
scala-cli package Memo.scala --native-image --output bin/memo

ネイティブ版は 実行ファイル1つだけ 配れば OK。
(GraalVM に 3 GB 近いキャッシュが必要なので初回は気長に)

4. 動作チェック:追加 → 一覧 → 削除

% ./bin/memo add "aaa"
Memo added.
% ./bin/memo add "bbb"
Memo added.
% ./bin/memo list
1: aaa
2: bbb
% ./bin/memo delete 1
Memo deleted.
% ./bin/memo list    
2: bbb

ファイル実体はカレントに memo.db としてできあがり。
sqlite3 memo.db .dump で中身も確認できます。

まとめ 💡

  • Coursier bootstrap の依存地獄・シェル芸から解放
  • 依存宣言は ソース先頭の 1 行
  • scala-cli package だけで 配布可能バイナリ 完成

「Go みたいに単体バイナリを配りたい」──
Scala 2 でも普通にできます。ぜひ試してみてください!

Discussion