世界を動かす技術を、日本語で。

Make.tsの作成

概要

  • 手動コマンド入力 から スクリプトファイル運用 への移行による効率化
  • make.ts のような一時スクリプトファイル活用の提案
  • コマンド履歴依存から脱却し、 再現性・編集性 向上
  • Deno+dax 等のモダンなスクリプト環境の活用
  • スクリプトの 漸進的発展 によるワークフロー改善

シェル履歴からスクリプトファイル運用へ

  • これまで Up Enter Up Up Enter のような履歴呼び出しでコマンドを繰り返し実行していた運用
  • 複雑なコマンド列マルチプロセス環境 では手動入力が非効率
  • make.ts のような(gitignore対象の)一時スクリプトファイルを用いる新しいワークフローへの転換
  • コマンドを直接ターミナルで入力せず、 まずファイルに書いてから実行 する習慣
  • この方法は 「ちゃんとしたスクリプト」作成を推奨するものではなく、あくまで一時的な作業記録と再利用を目的

スクリプトファイル運用のメリット

  • 長いコマンドや複数コマンド も2Dエディタで編集可能
  • 一度に複数コマンドを記述し、 一発実行 できる利便性
  • コマンド列を 段階的に改善・洗練 しやすい
  • 再現性の高い作業将来的なスクリプト化 へのスムーズな移行
  • マルチプロセス管理並列実行 の容易さ
  • 履歴依存からの脱却 によるワークフローの安定化

スクリプトファイルの運用方法

  • 一貫したファイル名 (例: make.ts)をプロジェクトルートに配置
    • .git/info/exclude(共有されないgitignore)で除外
  • コマンド実行履歴 (例: ./make.ts)をシェル履歴に保持し、オートサジェスト活用
  • シバン (例: #!/usr/bin/env -S deno run --allow-all)を先頭に記述し、chmod a+xで実行権限付与
  • 自分が慣れた言語 (TypeScript推奨)で記述
    • サブプロセス起動並行処理 が容易な言語
  • Deno+daxBunzx などのモダンなスクリプト環境
    • daxは シェルを介さず直接プロセス起動 できる点が特徴
    • async/await による並列実行サポート

JavaScript/TypeScriptによるスクリプト例

  • Tagged Template によるコマンド記述
    • 例: $ls ${dir} のような書き方で、コマンドと引数を明確に分離
  • daxライブラリ を使ったサブプロセス管理
  • Promise.all で複数コマンドの並列実行

実践例:TigerBeetleクラスタのベンチマーク

  • 複数マシンでの一連の手順 (ビルド、配布、フォーマット、起動)をスクリプト化
  • コマンドの追加・修正も ファイル編集のみで即反映
  • ベンチマークとクラスタ起動の並列実行 も簡単
  • for文や関数化 でスクリプトの拡張性確保
  • パラメータマトリクス による自動化ベンチマークスケジューリング

漸進的なスクリプト化の価値

  • 最初は 一時的なコマンド集 として活用
  • 作業を進めるうちに 徐々に本格的なスクリプトへ発展
  • 複数ターミナルや履歴編集の手間 を大幅削減
  • 再現性・保守性の高いワークフロー の確立

まとめ

  • シェル履歴 に頼らず、 まずファイルにコマンドを記録
  • 一時的なスクリプトファイル運用 が効率化・再現性・拡張性を実現
  • 現代的なスクリプト環境 (Deno、Bun、TypeScript+dax等)の活用推奨

Hackerたちの意見

そうだね。シェルはスクリプト言語としては最悪で、最初の if を「簡単な」スクリプトに入れなきゃいけない時とか、もうちょっと複雑な文字列操作をしなきゃいけない時に、選んだことを後悔し始めるんだよね。今はLLMがBashをJS/Python/Rubyにすぐ書き換えてくれるから、少しは楽になったけど。

私はSwiftを使ってるよ!外部モジュールをスクリプトにインポートできるように、swift-sh[0]を(再)作ったんだ(uv風にね)。 [0] https://github.com/xcode-actions/swift-sh

まあ、少なくとも5年後には自分のBashスクリプトを実行できるようになるだろうね。

同意するよ。シェルはプレーンテキストの原子的な操作をつなげるのには最高だよね。つまり、そういう一行スクリプトには向いてる。多分、プレーンテキストでどう動くかよりも、プロセスを始めたり、プロセス置換やリダイレクションを簡単にできることが主な理由だと思う。どこかに状態が蓄積されると、分岐やループが出てきた時にすぐに混乱しちゃうんだよね。

一般的にはAWKをスクリプト言語として使ってるか、全体をAWKで直接書くことが多いかな。変わらないし、すべてのPOSIXプラットフォームに常にインストールされてるし、コマンドラインとも簡単に連携できるし、学びやすい小さな言語だからね。

これが正解。シェルはひどいスクリプト言語で、最初のifを「シンプル」なスクリプトに入れなきゃいけなくなると、選んだことを後悔し始める。もうちょっと複雑な文字列操作をしなきゃならないときも同様。JS環境にいるならいいかもしれないけど、著者のニーズはシェルコマンドを.shファイルに入れるだけで満たされるんじゃない?このやり方はちょっとオーバーエンジニアリングすぎて、その分のメリットが少ない。著者がMake.tsファイルを作る理由は、コマンドを.shファイルに入れるだけで十分に満たされるし、プロジェクトをチェックアウトするときにビルドシステムに何をインストールする必要があるか気にしなくて済むっていう利点もある。メリットが見えないんだよね。

これがまさに私がRad [0]を書くきっかけになったフラストレーション。READMEには例が載ってるよ。もう1年以上取り組んでて、目標はCLIを書くためのプログラミング言語を提供すること。宣言的な引数(毎回Bashの操作を解析しない)、自動的な--help生成、親しみやすい(Pythonっぽい)構文を目指してるし、開発用ビルドスクリプトにぴったり。こんな感じのスクリプトになることが多いかな:#!/usr/bin/env rad --- 開発自動化スクリプト。--- args: build b bool # プロジェクトをビルド test t bool # テストを実行 lint l bool # リンターを実行 run r bool # 開発サーバーを起動 release R bool # リリースモード filter f str? # テストフィルターパターン filter requires test if build: mode = release ? "--release" : "" print("Building ({release ? 'release' : 'debug'})...") $cargo build {mode} if lint: print("Linting...") $cargo clippy -- -D warnings if test: f = filter ? "-- {filter}" : "" print("Running tests{filter ? ' (filter: {filter})' : ''}...") $cargo test {f} if run: bin = release ? "target/release/server" : "target/debug/server" $./{bin} 使用方法: ./dev -b (ビルド), ./dev -blt -f "test_auth" (ビルド、リンティング、テスト), ./dev -r (ただ実行)。現在も開発中![0] https://github.com/amterp/rad

最近、かなり複雑なシェルスクリプトをたくさん書いてる(とはいえ、1000行を少し超える程度だけど)。その中にはローカルで動く小さなプログラムもあれば、Terraformのためのコンポーザブルなcloud-initモジュールを駆動するものもある。これを使えば、ユーザーは自分でシェルスクリプトを書くことなく、複数のLinuxディストリビューションでEC2ホストのさまざまな機能を設定できる。適切なツールがあれば、思っているほど悪くはないよ。どちらのスクリプトも、面白いものはすべてNixを通じてインストールされるから、特定のディストロのパッケージマネージャに依存することは少ない。どちらの場合も、すべてのスクリプトはShellCheckを通過しないと「ビルド」できない。明らかなパースエラーや引用符の曖昧さ、変数名のタイプミスがある状態ではデプロイやコミットできない。開発者向けのツールとしてのスクリプトの場合、Bashインタープリタ、coreutils、すべての外部コマンドはNixによって提供され、スクリプト内にそのフルパスがハードコーディングされてる。スクリプトはLinuxでもmacOSでも関係なく動くし、PATHに何があるか(空でも)気にしない。彼らは「モダン」なBash機能を取り入れて、最も読みやすいインターフェースを提供するCLIツールを使ってる。私のお気に入りの言語?いいえ。でも、しばしば最も良いROIを持ってるし、ポータビリティや多くの問題も、使うべきツールを知っていればかなりうまく解決できる。特にスクリプトがシンプルならね。

web/js/tsエコシステムでは、ほとんどの人がpackage.jsonのnpmスクリプトを使ってて、カスタムのmake.tsはあまり使われてないよ。そこから起動するスクリプトはどんな言語でもOKだから、TSのシェルスクリプトを使うのも全然問題ないよ。コマンド履歴をファイルに保存する標準的な方法として「make」ってのがあって、これを使うと入力が少し楽になるし、みんながカスタムシステムを発見する必要もないし、オートコンプリートもすぐに使えるから便利だよね。

私のモノレポは年々多言語化してきてて、依存関係のせいで、makeファイルやcargo.toml、package.json、deno.json、venv + requirements.jsonなんかが同じルートにあるのは珍しくないよ。ウェブのバックグラウンドから来たから、通常はpackage.jsonにスクリプトを全部入れるようにしてる。全部にmakeを使いたいけど、いろんなことに対してはオーバーキルだし、私が働いてる分野では標準的じゃないことも多いからね。

package.json(またはNXのproject.json)にスクリプトを入れる主な欠点は、JSONでラップしなきゃいけないことだね。シンプルなコマンドには問題ないけど、引用符や複数コマンドを追加し始めると、ちょっとごちゃごちゃしてくる。makeやタスクランナーとして使うのが好きなんだけど、構文やインデントのオーバーヘッドがかなり少ないからね。ただ、まだJSベースのプロジェクトに導入したことはないんだ。新しいツールを追加することになるからね。

Makeはプロジェクトの一般的なメンテナンスコマンドを保存するのにとても良い選択だよ。仕事でもこれを使ってる。10年以上前にDockerに移行したときに始まったんだ - docker-composeが存在する前に、コンテナのセットをビルドして実行するにはかなりのシェルスクリプトが必要だったから、Makeを使うことにしたんだ。Makeはどこにでもあって、クロスプラットフォームで、ターゲットは基本的にシェルのスニペットに追加機能や構文が加わったものだし、依存関係のシステムもある(「Xを実行したいなら、まずZとYをビルドしてからXを実行する必要がある」みたいなことを自然に表現できる)。パラメータ化も簡単だし(make ARG=val)、実際にはチューリング完全な言語で、ファーストクラスのラムダや自己修正コードの能力もあるんだ[1]。ルールが複雑になりすぎたら、scripts/something.shにダンプしてMakeに呼び出させるのも簡単だし、別の言語でスクリプトを書き直すのも可能だよ。Makeはターゲット間の依存関係も提供してくれる。要するに、Makeはプロジェクトに必要な「補助的」スクリプトを言語に依存しない形で集めるのにとても良いツールなんだ。setup.pyやpackage.jsonよりも優れているのは、両方の種類のプロジェクトに対して単一のインターフェースを提供しているからだよ。[1] これは知っておく価値があるから、両方の機能を避けるためにね。

Hacker Newsで議論の続きを見る