sounds familiar? This is how I've historically run benchmarks and other experiments that require repeated sequences of commands - type them in manually once, then rely on shell history (and maybe some terminal partitioning) for reproduction. Over the years I have arrived at a much better workflow pattern - <code>make.ts</code>. Once I started working with multiprocess applications I was forced to adapt to it, where entering commands manually is impossible. In retrospect, I should have optimized the workflow years ago.
</p><div id="The-Pattern">
<p>
Use (gitignored) file for interactive scripting. Instead of entering a command directly into the terminal, first write it to a file, and then run the file. For me, I type stuff <code>make.ts</code> and then run <code>./make.ts</code> in my terminal (well, I want <em>One</em> <kbd><kbd>enter above</kbd></kbd> for him).
I want to be clear here, I'm not advocating writing "proper" scripts, I'm simply capturing your interactive, ad-hoc commands into a persistent file. Of course any command you want to execute
<em>Frequently</em> Belongs to build system. Surprisingly, even the more complex one-off commands benefit from walking through the file, as it would take you several tries to get them right!
has many advantages over <kbd><kbd>up up up</kbd></kbd>
Workflow:
<ul>
<li>
The actual commands become larger, and it's great to use a real 2D text editor instead of the shell's line editor.
</li>
<li>
If you need more than one command, you can write multiple commands, and still run them all with the same key (first <code>make.ts</code>I was prone to creating rather horrendous && combinations for this reason).
</li>
<li>
With the sequence of commands outlined, you motivate yourself to sequentially improve them, make them inactive, and otherwise invest in your own workflow for the next few minutes, without falling into the YAGNI pit from the beginning.
</li>
<li>
At some point you may realize that after running a series of ad-hoc benchmarks interactively, you will write a proper script that executes a collection of benchmarks with different parameters. With the file approach, you already have the main body of the script implemented, and you just need to wrap it in some ifs and ifs.
</li>
<li>
Finally, if you work with multi-process projects, it will be easier for you to manage concurrency declaratively, by generating a tree of processes from the same script, rather than switching between terminal splits.
</li>
</ul>
</div><div id="Details">
Use a consistent file name for the script. I use <code>make.ts</code>and so there is a <code>make.ts</code> At the core of most of the projects I work on. Accordingly, I have <code>make.ts</code> line in project <code>.git/info/exclude</code>
- The <code>.gitignore</code> A file that is not shared. Fixed name reduces fixed costs - whenever I need complex interactivity I don't need to come up with a name for a new file, I just open my already existing file. <code>make.ts</code>Erase whatever was there and start hacking. Similarly, I have <code>./make.ts</code> In my shell history, that's why fish auto-suggestions work for me. At one point, I had a VS Code task to run <code>make.ts</code>However now I use the terminal editor.
Start the script with hash bang,
<span class="display"><code>#!/usr/bin/env -S deno run
--allow-all</code></span>
In my case, and
<span class="display"><code>chmod a+x make.ts</code></span>
To make it easier to play the file.
Write the script in a language that:
<ul>
<li>
You are comfortable with it,
</li>
<li>
Doesn't require a huge setup,
</li>
<li>
Makes it easier to generate subprocesses,
</li>
<li>
There is good support for concurrency.
</li>
</ul>
To me, that's TypeScript. Modern JavaScript is ergonomic enough, and structural, that sequential typing is a sweet spot that gives you reasonable code completeness, but still allows any problem to be solved by throwing sufficiently stringent instructions at it.
JavaScript's tagged template syntax is great for scripting use-cases:
<figure class="code-block">
<pre>
function $(literal, ...interpolated) {
console.log({ literal, interpolated });
}
const dir = "hello, world";
$ls <span class="hl-subst">${dir}</span>;
<figure class="code-block">
<pre>
{
literal: [ "ls ", "" ],
interpolated: [ "hello, world" ]
}
what happens here happens <code>$</code> Gets a list of literal string pieces inside the backticks, and then, separately, gets a list of values to interpolate between. it <em>can</em>
Concatenate everything into just one string, but it's not necessary to do so. This is exactly what is needed for process spawning, where you want to pass an array of strings <code>exec</code>
syscall.
Specifically, I use the Dax library with Deno, which is excellent as a single-binary battery-involved scripting environment (see <3 Deno). Bun's Box has a Dax-like library and it's a good alternative (although I personally stick with Deno
<code>deno fmt</code> And <code>deno lsp</code>). You can also use the famous zx, although keep in mind that it uses your shell as a middleman, which I consider sloppy (clarification).
Whereas <code>dax</code> Makes it convenient to create a single program, <code>async/await</code> Excellent for organizing various processes:
<figure class="code-block">
<pre>
await Promise.all([
$sleep 5,
$sleep 10,
]);