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

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] これは知っておく価値があるから、両方の機能を避けるためにね。

Denoにはnpmスクリプトに似た「タスク」というツールがdeno.jsonにあるんだ。これには、設定されたタスクのリストやさまざまなIDE統合に表示される1行の説明を含めることを促すという小さな利点もある。だけど、俺の経験では、ほとんどのDenoタスクは、npmスクリプトよりもむしろdeno run …コマンド(記事のシェバン行)として、_scripts/みたいなディレクトリ内のスクリプトに対して書かれていることが多いんだ。

私は主にpackage.jsonの「scripts」セクションにスクリプトを置いてるけど、実際に呼び出すスクリプトは.tsファイルだったり、状況によってはBashだったりするよ。基本的にはbunを使ってこれらのスクリプトを実行してるんだけど、bunの方がdenoより好きなんだ。

「Just」はまさにこれのために作られていて、すごく便利だよ。makefileに似たjustfileを書くだけで、面倒なところがなくて、実行したいコマンドのCLIインターフェースを提供してくれるんだ。

俺はしばらくの間Justを楽しんでたけど、miseを試してからは変わった。MiseはJustと同じことを全部やってくれるけど、ビルドジョブを再実行しないようにソース/出力トラッキングもしてくれるし、asdfみたいなランタイムもまとめてくれる。今では俺のお気に入りのオールインワンタスクランナーになったよ。

タイトルの「make」はちょっと誤解を招くと思う。著者は実際には、アドホックなスクリプトやテストに使うための一貫したファイルを持つことを提唱してるだけだから。この記事の要点は、シェルに複数のコマンドを入力するなら、スクリプトを作れってことだね。

直感的には、自分のお気に入りのタスクランナー(mise tasks[1]、今はシェルエイリアス[2]も!)を宣伝したくてコメント欄に駆け込みたくなるけど、それを超えて、ファイルにスクリプトを書くっていう基本的なアイデアは素晴らしいと思う。ただ、ここでのこの部分には賛成できないな。「ここで明確にしておきたいのは、私は“ちゃんとした”スクリプトを書くことを勧めているわけではなく、インタラクティブでアドホックなコマンドを永続的なファイルに記録することを言っている。」って。何が違うの?バージョン管理して、同僚と共有すればいいじゃん。新機能をテストするためにユニットテストを書いて、終わったらそれを削除するなんて、もったいないよ。確かに、これらのスクリプトは回帰テストには使わないから完全に同じではないけど、役立つ学びや文脈は再利用できるし。スクリプトを書くための言語は、ランタイムが固定されていて全エンジニアのマシンで簡単に使えるなら、あまり重要じゃないと思うよ。たぶん、ツールチェインマネージャーを使うのがいいかもね... mise[3]。 [1] https://mise.jdx.dev/tasks/ [2] https://mise.jdx.dev/shell-aliases.html [3] https://mise.jdx.dev/dev-tools/

この部分もよくわからないな、「ちゃんとした」がBashを意味するなら。どんな状況でもBashを書くべきじゃないと思うよ。

何が違うの?バージョン管理しないの?だって、ディレクトリパスをハードコーディングしてるから。特定の設定がされてる前提でやってるし、私のマシンのやり方に合わせてるから。今使ってるワークフローに特化してハードコーディングしてるだけで、それ以上でもそれ以下でもない。もう必要なくなった後は責任を持ちたくないし、正当化するのも面倒。チェックインすべきじゃないものをハードコーディングしてるから、これに基づいてやり方を決める責任も負いたくないんだよね。

なんで俺が最初にfzfを挙げる人なんだ?fzfをシェルに統合して、ctrl-rを使えば、すぐにファジーシェル履歴検索ができて、履歴からどんなコマンドでも再実行できるんだ!これなしでターミナルを使うなんて考えられないよ。複数のコマンドを繰り返す必要があるときはまだたくさんスクリプトを書くけど、ワンライナーならfzfを使って再実行すればいいし。共有プロジェクトでは、.git/info/excludeでスクリプトファイルを無視できるから、個人の除外パターンをメインブランチにチェックインする必要もないんだ。マジで、ターミナルを使うなら、以下のツールは必須だよ:ripgrep、zoxide、fzf、fd。

fdとzoxideのことをずっと知らなかったなんて信じられない。zoxideは今や俺のトップコマンドの一つだし、fdはripgrepに切り替えたときの感覚に似てる。めちゃくちゃ速くて簡単だから、使わない理由がないよ。

Up Up Upワークフローに対して多くの利点があるよ。シェルのviモードを使えば、さらに良くなる。L -> k k k それとも/で検索することもできるし。vimに慣れているなら、前の一行をすごく早く編集できるよ(CapsLockをEscapeにリマップ/スワップするのは、LinuxやMacOSではただのGUI設定、Windowsではレジストリキーの設定だけ)。

何度もこれをやろうとしたけど、ちょっとした規律すら足りなかった。結局、スクリプトじゃなくてコマンドラインで実行したいコマンドを変更しちゃって、後でスクリプトを編集するのを忘れちゃうんだよね。今はatuin.shに頼ってる。これが、私が入力したコマンドを全部記憶してくれるから。ちょっと悪いところもあって、ちゃんとしたスクリプトができるわけじゃなくて、ただ長いコマンドができるだけだけど、努力ゼロで50%は進むよ。前の仕事を辞めるときには、自分の(すごく長い)atuin履歴を後任に寄付したけど、書いたドキュメントよりも役に立ったと思う。唯一のアドバイスは、atuinがデフォルトで上矢印をオーバーライドするから、atuin init zsh --disable-up-arrowを実行して、Ctrl-Rのときだけ動くようにすること。

著者がこれに気づいているかは分からないけど、実際にはreadline / bashにはそれをするための2キーショートカット、^X^Eがあるんだよね。これを使うと、ターミナルエディタに入って、コマンドの現在の状態がバッファの最初の行になり、エディタを出るとそのコマンドが実行される(もちろん、エディタ内で直接出るのではなく、まずファイルに保存することもできるよ)。

zshのvimモードでは、現在のコマンドをエディタで開くショートカットはvだよ。

この記事はそのパターンに注目するのが上手だね。PowerShellを使ってると、最初はターミナルで作業を始めて、必要なものが整ったら履歴(get-history)を取得してファイルに書き出すことができるんだ。これを俺はいつも「サンプル」って呼んでる。で、他の人がそれについてよく聞いてくるようになったら、その「サンプル」を本格的な生産用の「スクリプト」にリファクタリングするんだ。最初は明確な方向性がないことが多いし、別のファイルを作るのは単に無駄な儀式みたいなもので、実際に「up-enter」パターンが現れるまでいじって、後からエクスポートすればいいと思う。