At least nowadays LLMs can rewrite Bash to JS/Python/Ruby pretty quickly.
#!/usr/bin/env rad
---
Dev automation script.
---
args:
build b bool # Build the project
test t bool # Run tests
lint l bool # Run linter
run r bool # Start dev server
release R bool # Release mode
filter f str? # Test filter pattern
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}`
Usage: ./dev -b (build), ./dev -blt -f "test_auth" (build, lint, test auth), ./dev -r (just run).
Actively being developed!I suppose it can be nice if you are already in a JS environment, but wouldn't the author's need be met by just putting their shell commands into a .sh file? This way is more than a little over-engineered with little benefit in return for that extra engineering.
The reasons (provided by the author) for creating a Make.ts file is completely met by popping your commands into a .sh file.
With the added advantage that I don't need to care about what else needs to be installed on the build system when I check out a project.
I just don't see the advantages.
For both scripts, everything interesting is installed via Nix, so there's little reliance on special casing various distros', built-in package managers.
In both cases, all scripts have to pass ShellCheck to "build". They can't be deployed or committed with obvious parse errors or ambiguities around quoting or typos in variable names.
In the case of the scripts that are tools for developers, the Bash interpreter, coreutils, and all external commands are provided by Nix, which hardcodws their full path into the scripts. The scripts don't care if you're on Linux or macOS— they don't even care what's on your PATH (or if it's empty). They embrace "modern" Bash features and use whatever CLI tools provide the most readable interface.
Is it my favorite language? No. But it often has the best ROI, and portability and most gotchas are solved pretty well if you know what tools to use, especially if your scripts are simple.
As soon as you have state accumulating somewhere, branching or loops it becomes chaotic too quickly.
The thrust of the article could be summarized as: if you type more than one command into the shell, make a script.
If you combine that with relative working folders it’s very easy to manage large projects.
And you can get shell completion, which is extra nice.
Just integrate fzf into your shell and use ctrl-r to instantly summon a fuzzy shell history search and re-execute any command from your history!
I cannot imagine going back to using a terminal without this.
I still write plenty of scripts if I need to repeat multi command processes but for one liners just use fzf to reexecute it.
Also in a shared project you can ignore script files with .git/info/exclude instead of .gitignore so you don’t have to check in your personal exclusion patterns to the main branch.
Seriously people if you use a terminal you need the following tools to dominate the shell:
ripgrep, zoxide, fzf, fd
"I want to be clear here, I am not advocating writing “proper” scripts, just capturing your interactive, ad-hoc command to a persistent file."
What's the difference? Why not version control it, share it with colleagues. Imagine writing a unit test to test a new feature then deleting it when done, what a waste. Ok it's not exactly the same because you aren't using these scripts to catch regressions, but all of that useful learning and context can be reused.
I don't think the language you use for scripting is too important as long as the runtime is pinned and easily available on all engineers machines, perhaps using a toolchain manager like... mise[3].
[1] https://mise.jdx.dev/tasks/ [2] https://mise.jdx.dev/shell-aliases.html [3] https://mise.jdx.dev/dev-tools/
Because I'm hardcoding directory paths.
Because I'm assuming things are set up a particular way: the way they are on my machine.
Because this is hardcoded to a particular workflow that I'm using here and now, and that's it.
Because I do not want to be responsible for it after no longer needing it.
Because I don't want to justify it.
Because I'm hard-coding things that shouldn't be checked in.
Because I don't want to be responsible for establishing the way we do things based on this script.
Another quite standard way of savings your command history in a file that I have seen used in all ecosystems is called "make", which even saves you a few characters when you have to type it, and at least people don't have to discover your custom system, have auto complete work out of the box, etc
I quite like make or just as a task runner, since the syntax / indentation / etc overhead is a lot lower. I haven't yet tried to introduce it in any JS based projects though, because it adds yet another tool.
Most Deno tasks though, more so than a lot of npm scripts in my experience, tend to just be `deno run …` commands (the shebang line in the article) to a script in a directory like `_scripts/` rather than written as CLI commands.
Coming from a web background, my usual move is to put all scripts in the package.json, if present. I'd use make for everything, but it's overkill for a lot of stuff and is non-standard in a lot of the domains I work in.
TL;DR: Make is a very nice tool for gathering the "auxiliary" scripts needed for a project in a language-agnostic manner. It's better than setup.py and package.json precisely because it provides a single interface for projects of both kinds.
[1] Which is worth knowing so you can avoid both features like the plague.
Historically we had to use pip which was super janky. Uv solves most of pip's issues but you still do have to deal with venvs and one issue it doesn't solve is that you can't do imports by relative file path which is something you always end up wanting for ad-hoc scripting. You can use relative package paths but that's totally different.
I may be the author and only user but i still use it frequently and it was worth the effort just for my own projects
Just add the targeted path to sys.path, or write your own importhandler. importlib might help there. But true, out of the box, imports in python3 are a bit wacky for more flexible usage.
I’m not sure about that. All those ‘await’s, parentheses really kill my mojo. Why do you find it better than Python?
Though, I generally run these scripts using bun (and the corresponding `$` in bun) - basically the same thing, but I just prefer bun over deno
Instead, I now swear by atuin.sh, which just remembers every command I've typed. It's sort of bad, since I never actually get nice scripts, just really long commands, but it gets you 50% of the way there with 0 effort. When leaving my last job, I even donated my (very long) atuin history to my successor, which I suspect was more useful than any document I wrote.
My only hot tip: atuin overrides the up-arrow by default, which is really annoying, so do `atuin init zsh --disable-up-arrow` to make it only run on Ctrl-R.
With your shell's vi mode, it's even better L -> k k k
Or search them with /
And if you are proficient with vim, you can edit your previous one-line really fast
(Remap/Swap CapsLock with Escape system-wide. It's just a gui setting on linux and MacOS and a registry key way on Windows)
Some of the scripts are bash, but many are TypeScript via Deno... it's great that you can reference your dependency modules directly as well as not needing a separate install step like node. Most of my shell scripting is now in Deno.
In fact, now VS just added shebang detection for TS files without the .ts extension... So I don't even need that little extra to edit properly anymore. It works great as a shell scripting language.
If you work in powershell you can start out in the terminal, then when you've got whatever you need working you can grab the history (get-history) and write it to a file, which I've always referred to as a `sample`. Then when it becomes important enough that other people ask me about it regularly I refactor the `sample` into a true production grade `script`. It often doesn't start out with a clear direction and creating a separate file is just unnecessary ceremony when you can just tinker and export later when the `up-enter' pattern actually appears.
Instead of juggling dashboards and collections of requests, or relying on your shell history as Matklad mentions, you have it in a file that you can commit and plug into CI. Win-win.
At some point, that testing shell script can be integrated into your codebase using your working language and build tooling.
What about this instead: select any number of lines, in any file, and pass it through to the shell. You get convenience of text editing, file management, and shell’s straightforwardness.
(This approach was tried and cemented in Acme, a text editor from Bell Labs.)
Edit: zero-dependency Python.
This article used Dax instead which also looks fine! Https://github.com/dsherret/dax
Anyway, what kills this for me is the need to add await before every command.
I don't understand why you wouldn't want your scripts in your Git - but I guess OP's context is different from mine.
...this is the same sort of 'works for me' philosophy as in Matklads post though, it's so heavily opinionated and personalized that I don't expect other people to pick it up, but it makes my day-to-day work a lot easier (especially since I switch multiple times between macOS, Linux and Windows on a typical day).
I'm not sure if Bun can do it too, but the one great thing about Deno is that it can directly import without requiring a 'manifest file' (e.g. package.json or deno.json), e.g. you can do something like this right in the code:
import { Bla } from 'jsr:@floooh/bla^1';
This is just perfect for this type of command line tools....and that was also the one concrete example where it makes sense to have extra dependency and abstraction layer on top of a shell script:)
say you know TS and even if you walk back to where $ is defined, can you tell immediately why $`ls {dir}` gets executed and not just logged?
No repetitive short sentences, no "Not X, just Y." patterns, and lots of opinionated statements, written confidently in the first person.
Please more of this.