Skip to content
Blog Programming

How I Run the Right Command After Months of Not Running It

By Romi Fadli, S.Kom. 16 min read Tooling & Automation
On this page
Three horizontal bars of very different lengths, one blue and two orange, comparing how many guards each command has

There is a category of command I run roughly four times a year. Not often enough to remember, not rarely enough to look up from scratch each time, and — this is the part that matters — always at a moment when something else has already gone slightly wrong. A repository that needs its remote pointed somewhere new. A working tree that has to go back to clean. A virtual disk that ran out of room while a build was in progress.

Introduction

A. The commands that fall through the gap

Frequency determines how a command is remembered, and it does so in a shape that is less convenient than it sounds.

Commands run daily live in muscle memory. I do not recall the syntax of git status; my fingers produce it. Commands run once ever are looked up from scratch, which is fine, because approaching them with no assumptions is exactly the right posture.

The trouble sits in the middle. A command run every few months feels familiar enough that looking it up seems unnecessary, and that feeling is wrong. What I actually retain after four months is the shape of the thing — that it involves remote, that there is a flag with upstream in it somewhere — which is enough to construct something plausible and not enough to construct something correct.

B. Why plausible-but-wrong is the expensive case

A command I cannot remember at all is harmless. I stop, I read the manual, I proceed carefully because I know I do not know.

A command I half-remember produces a different sequence. I type something, it is syntactically valid, and it does something — just not the thing intended. Git in particular is generous here: there are several verbs that accept similar arguments and behave entirely differently, and none of them will ask whether I am sure.

All three tools in this article touch things that resist being undone. A remote URL that points at the wrong repository sends the next push to a place nobody is watching. A working tree cleaned is a working tree whose uncommitted changes no longer exist anywhere. A disk image is a file containing a filesystem, and operations on it are not transactional.

C. Three tools that share nothing but a frequency

The set is deliberately unalike. Two are git operations; one resizes a disk image for QEMU, the open-source program that runs virtual machines. They were not designed together — each appeared separately, months apart, when the underlying need arrived.

That distance is the interesting part. If the pattern were about git, a git-shaped solution would follow. The pattern is about how often a command is used, which is a property of my working life rather than of any technology, and which is why a virtualisation tool ended up in the same category as two version-control ones.

D. What these scripts are not

None of them implement anything. Between the three there are sixty-six lines, and the part doing real work is three commands that git and QEMU already provide.

What the scripts hold is a decision — which flags, which order, which of two similar-looking operations is the correct one for the situation. That decision was made once, carefully, with the documentation open. Storing it means it does not have to be made again at a moment when attention is elsewhere.

E. Six obstacles in the way

So the goal is easy to state: run the correct command at the moment it is needed, months after last needing it, without reconstructing it from a half-memory. Correct on the first attempt, on operations where a second attempt may not be available.

Easy to state, awkward to build. Six obstacles stand between that goal and three commands I can run without hesitating, and none of them are about the commands being hard.

The first is that the cost is in the recall, not the work. Every one of these operations is a single line once the line is known. The expensive part is retrieving it correctly after a long gap, which no amount of understanding the underlying tool prevents. The rejected alternatives are instructive: a notes file holds the text but cannot be executed, so it still requires reading and retyping, and history | grep surfaces every previous attempt including the wrong ones, with nothing marking which was which.

The second is that two situations which feel identical require entirely different commands. Pointing a repository at a remote is one sentence in English and two distinct operations in git, depending on whether a remote already exists. The commands share no verbs. Choosing wrong produces an error that describes what failed without indicating which operation was actually wanted.

The third is that a rarely-run command is executed with the least context and offered the least. After several months I am not certain which directory I am in, which branch is checked out, or what the current remote points at — and the command that most needs that context is precisely the one that by default prints nothing at all before acting.

The fourth is that running in the wrong place is cheaper to prevent than to repair. A git command issued outside a repository fails harmlessly; one issued inside the wrong repository succeeds. Meanwhile a script running several steps has a failure mode of its own — step one fails, step two runs anyway on the assumption that step one worked, and the result is a state that neither step intended.

The fifth is that a name can promise more than the code delivers. One of these is called a rollback, which suggests undoing commits. It does not do that. It discards uncommitted changes and untracked files, leaving history untouched — a genuinely useful operation with a name that describes a different one, which matters because the person reading the name is the person who has not run it in four months.

The sixth is that adding validation has a cost too. The instinct after writing several guards is to add guards everywhere. But when the underlying tool already produces a clear error for bad input, a wrapper that checks first only duplicates that message — and every duplicated message is one that can drift out of sync with the real one.

Each has a section below. The first four are about one tool that grew guards because it needed them; the last two are about the two tools that did not, and why that asymmetry is deliberate rather than neglectful.

Method

A. Storing the shape rather than the logic

Here is the answer to the first obstacle, and it is the smallest possible idea: put the command in a file with a name that describes the situation rather than the mechanism.

bash
# the name is the index into memory — not "qemu-img", but what I want
qemu-resize-img disk.qcow2 +20G

What I remember after four months is not qemu-img resize. It is that I want to make a disk image bigger. A command named after the intention is reachable from the intention, which is the only thing still present in memory when the need arrives.

This is also why a notes file does not solve it. A note holds the same text, but reaching it requires remembering that the note exists, finding it, and retyping the contents without transcription errors — three steps that each fail at exactly the moment I am in a hurry. An executable name is reachable by typing the first three letters and pressing tab.

B. Making the choice explicit rather than clever

This is the answer to the second obstacle. Two situations, two modes, stated as the first argument.

bash
git-set-remote create git@example.com:user/repo.git   # no remote yet
git-set-remote update git@example.com:user/repo.git   # remote exists

The underlying operations have nothing in common. A repository with no remote needs one added and then needs its first push to establish tracking. A repository whose remote is wrong needs that remote's URL rewritten, and pushing is neither required nor wanted.

bash
if [ "$ACTION" == "create" ]; then
    git remote add origin "$REPO_URL"
    git push --set-upstream origin "$(git branch --show-current)"
elif [ "$ACTION" == "update" ]; then
    git remote set-url origin "$REPO_URL"
fi

The tempting alternative was to detect the situation — check whether a remote exists, then pick the right branch automatically. I rejected it, and the reason is about what happens when the detection is right but my belief about the situation is wrong. If I think a repository is fresh and it is not, a smart tool quietly does the other thing and reports success. An explicit mode fails instead, and failing is what I want when my model of the situation is incorrect, because that is the only signal that it is incorrect.

C. Printing the context the situation lacks

This is the answer to the third obstacle: say where you are before doing anything, and what changed after.

bash
echo "📂 Current directory: $(pwd)"
echo "🔎 Current branch: $(git branch --show-current)"
# ... the operation ...
echo "📡 Current remote configuration:"
git remote -v

Neither line is required for the operation. Both exist because the person running this has several terminals open and has not touched this repository since before a holiday. The directory and branch answer "am I where I think I am" at the moment that question is cheapest to answer — before anything is modified.

The closing git remote -v answers the other half. Having pointed a repository at a URL I typed from memory, the immediate question is whether it landed correctly, and the alternative to printing it is running a second command to check, which is a step that gets skipped when the first one appeared to work.

D. Refusing early, and stopping at the first failure

This is the answer to the fourth obstacle, and it is two guards addressing two different failure shapes.

bash
set -e   # stop at the first failure, not the second

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    echo "Error: not a git repository."
    exit 1
fi

The repository check is the ordinary kind of guard. Its value is not that it prevents damage — git would reject the command anyway — but that it produces a message naming the actual problem, rather than a git error about an operation the caller did not consciously choose to run.

set -e addresses something less obvious. Create mode runs two commands in sequence, and the second assumes the first succeeded. Without it, a failed remote add is followed by a push that has nothing to push to, and the output contains two errors describing the same underlying cause. With it, execution stops at the first, which is the one that explains what happened.

This is also the difference between a script and a list of commands. A list runs everything regardless; a script can decide that a step's failure invalidates what follows.

E. Doing exactly what it does, under a name that suggests more

This is the answer to the fifth obstacle, and the honest version is that the name is imprecise.

bash
git clean --force -d   # delete untracked files and directories
git reset --hard       # discard all uncommitted changes

Nothing here touches commits. History is exactly as it was. What disappears is everything not committed: modified files revert, untracked files are deleted, and the working tree returns to whatever the current commit says it should be.

That is a frequently useful operation. An experiment that went nowhere, a dependency install that scattered files, a generated mess — all of it gone in one command, with no reading of git status to decide what to remove.

It is also unrecoverable, and it deliberately does not ask. The synchronisation tool I use for copying folders refuses a dangerous destination and prompts before overwriting a non-empty one, and the contrast is worth stating: there, the destination is an argument that can be mistyped, and the guard protects against a path that was never intended. Here there is no argument at all. The command operates on the directory I am standing in, and I typed a name that says clean. Confirming would mean asking whether I meant the only thing this command can possibly do.

F. When passing the arguments through is the right answer

This is the answer to the sixth obstacle, and the tool is three lines including the shebang.

bash
#!/bin/bash

qemu-img resize $1 $2

No validation, no guards, nothing printed. Every check I might add — does the file exist, is the size well-formed, is the image in a resizable format — is a check qemu-img already performs, with a better error message than I would write, because it knows about image formats and I would be pattern-matching on strings.

A wrapper that validates first would produce two error messages for the same problem, and the one I wrote would be the one that goes stale. The script's entire contribution is the name and the argument order, and that is genuinely the whole value: after six months I remember that I want to resize an image and I remember it takes the image and a size, in that order, because that is the order the name implies.

What makes this worth a script rather than a note is what the operation actually does. It grows the image without recreating it, which means the filesystem and everything on it stays where it is — the entire reason to reach for this command rather than building a new image and copying data across.

Results and Discussion

A. Guards in proportion to what can go wrong

The three scripts apply very different amounts of care, and laid side by side the pattern is clearer than it felt while writing them.

ToolLinesGuardsWhy that amount
Set a repository's remote59Argument check, repository check, set -e, context printed before and afterTwo modes that are easy to confuse; several steps where one depends on another
Return to a clean tree4NoneNo arguments to get wrong; the name states the only possible outcome
Resize a disk image3NoneThe underlying tool already validates, better than a wrapper could

The first row earned its guards by having something to guard: a choice between modes, an argument that can point anywhere, and a sequence where a failed first step makes the second meaningless. The other two have none of those properties, and adding guards would have produced ceremony rather than safety.

The useful question turned out not to be "is this operation dangerous" — discarding uncommitted work is plainly dangerous — but "is there a decision here that could be made incorrectly". Where there is none, a guard has nothing to catch.

B. What the emoji in the output are doing

One of these scripts prints emoji before each line, which looks decorative and is not entirely.

The output of a rarely-run command is scanned, not read. A wall of uniform text requires reading to locate the line that matters; a leading symbol makes the structure visible at a glance — this is where I am, this is what is about to happen, this is the result. For output appearing perhaps four times a year to someone reconstructing context, that difference is worth more than it would be in a tool running hourly.

It is also a decision I would not repeat in anything called from another script, where output is parsed rather than looked at, and where emoji become an encoding problem instead of a visual aid.

C. Where this approach stops fitting

These scripts encode one person's habits, and the encoding is the point rather than a limitation — but it does bound where they apply.

They assume origin as the only remote name, which is correct for my repositories and wrong for anyone working with forks and upstreams. They print for a human, so none of them belong in a pipeline. And the whole premise — that a decision made once can be stored and reused — holds only while the decision stays valid; a tool whose flags change between versions would need the stored form revisited rather than trusted.

Conclusion

Sixty-six lines across three files, of which the part doing work is three commands I did not write. What the files hold is the decision about how to invoke them: which mode, which order, which flags, and — in two cases — the decision that no further ceremony was warranted.

The thing I did not expect was that the guards would be so unevenly distributed, and that the uneven distribution would be the correct answer. My instinct after building one careful tool was to make the others careful in the same way, and that instinct was wrong. Ceremony that catches nothing is not caution; it is noise that makes the real warnings harder to see. What decides the amount is not how dangerous the operation is, but whether there is a decision inside it that I could make wrongly — and for the commands I run four times a year, that decision is usually the only thing worth writing down.

Terms

Remote
A named reference to a repository elsewhere, conventionally called origin. Adding one and changing an existing one's URL are separate operations with separate commands.
Upstream branch
The remote branch a local branch tracks, so that pushing and pulling need no arguments. Established on the first push with a dedicated flag.
Working tree
The files as they currently exist on disk, as opposed to what is committed. Uncommitted changes live only here, which is why discarding them cannot be undone.
Untracked file
A file present in the working tree that git has never been asked to manage. Not affected by commands that operate on tracked files, which is why removing it takes a separate command.
Disk image
A file containing an entire filesystem, used by a virtual machine as its disk. Resizing grows the file in place, leaving its contents intact.
qcow2
QEMU's copy-on-write image format, which allocates space as it is used rather than upfront, and supports being resized after creation.

Technologies covered

  • Bash
  • Git
  • QEMU

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →