In praise of –dry-run

henrikwarne.com

305 points by ingve 3 days ago


muvlon - 2 days ago

If you're interacting with stateful systems (which you usually are with this kind of command), --dry-run can still have a race condition.

The tool tells you what it would do in the current situation, you take a look and confirm that that's alright. Then you run it again without --dry-run, in a potentially different situation.

That's why I prefer Terraform's approach of having a "plan" mode. It doesn't just tell you what it would do but does so in the form of a plan it can later execute programmatically. Then, if any of the assumptions made during planning have changed, it can abort and roll back.

As a nice bonus, this pattern gives a good answer to the problem of having "if dry_run:" sprinkled everywhere: You have to separate the planning and execution in code anyway, so you can make the "just apply immediately" mode simply execute(plan()).

mycall - 3 days ago

I like the opposite too, -commit or -execute as it is assumed running it with defaults is immutable as the dry run, simplifying validation complexity and making the go live explicit.

xavdid - 2 days ago

I like this pattern a lot, but it's important that the code in the dry path is representative. I've been bitten a few too many times by dry code that just runs `print("would have updated ID: 123")`, but not actually running most of the code in the hot path. Then when I run it for real, some of the prep for the write operation has a bug / error, so my dry run didn't actually reveal much to me.

Put another way: your dry code should do everything up until the point that database writes / API calls / etc actually happen. Don't bail too early

nickjj - 2 days ago

One thing that I didn't see mentioned is making your own dry run for tools that don't have it built in.

It doesn't always work but sometimes I use `diff` to help with that. For example, if you have a complicated `sed` replacement that you plan to run on a file, you can use diff like this `diff -u <(echo "hello") <(echo "hello" | sed "s/hello/hi/g")` to help show what has changed.

I've written about the value of dry run too at: https://nickjanetakis.com/blog/cli-tools-that-support-previe...

arjie - 3 days ago

In order to make it work without polluting the code-base I find that I have to move the persistence into injectable strategy, which makes it good anyway. If you keep passing in `if dry_run:` everywhere you're screwed.

Also, if I'm being honest, it's much better to use `--wet-run` for the production run than to ask people to run `--dry-run` for the test run. Less likely to accidentally fire off the real stuff.

terrabitz - 2 days ago

A while ago when I was working with PowerShell a lot, I got spoiled by the easy inclusion of `-DryRun` flags in all my scripts.

Nowadays I still use the technique for a lot of the tools I make. I typically do a Terraform-like approach: create a plan, validate the plan, render the plan to the user, exit early if we're in dry-run mode, and apply the plan as the final step. Whether dry-run is enabled by default depends on the risk of the operations and who will be using the tool.

This makes it exceedingly clear what actions the tool will do. Plus it has the added benefit of being able to split the plan and apply steps into two separate steps. For example, you can create a plan, serialize it to JSON, store it (e.g. in VCS, Jira ticket, whatever) then apply it during a change window.

  plan = createPlan()
  print(plan.string())
  if (dryRun){
   return
  }
  
  plan.apply()
ElevenLathe - 3 days ago

I usually do the opposite and add a --really flag to my CLI utilities, so that they are read-only by default and extra effort is needed to screw things up.

skissane - 3 days ago

In one (internal) CLI I maintain, I actually put the `if not dry_run:` inside the code which calls the REST API, because I have a setting to log HTTP calls as CURL commands, and that way in dry-run mode I can get the HTTP calls it would have made without it actually making them.

And this works well if your CLI command is simply performing a single operation, e.g. call this REST API

But the moment it starts to do anything more complex: e.g. call API1, and then send the results of API1 to API2 – it becomes a lot more difficult

Of course, you can simulate what API1 is likely to have returned; but suddenly you have something a lot more complex and error-prone than just `if not dry_run:`

BrouteMinou - 2 days ago

One of the kick-ass feature of PowerShell is you only need to add `[CmdletBinding(SupportsShouldProcess)] ` to have the `-whatIf` dry-run for your functions.

Quite handy.

throwaway314155 - 3 days ago

Sort of a strange article. You don't see that many people _not_ praising --dry-run (speaking of which, the author should really learn to use long options with a double dash).

sixtram - 2 days ago

I use a similar strategy for API design. Every API call is wrapped in a large database transaction, and I either roll back or commit the transaction based on dry-run or wet-run flags. This works well as long as you don’t need to touch the file system. I even wrap emails this way—emails are first written to a database queue, and an external process picks them up every few seconds.

CGamesPlay - 3 days ago

For me the ideal case is three-state. When run interactively with no flags, print a dry run result and prompt the user to confirm the action; and choose a default for non-interactive invocations. In both cases, accept either a --dry-run or a --yes flag that indicates the choice to be made.

This should always be included in any application that has a clear plan-then-execute flow, and it's definitely nice to have in other cases as well.

zzo38computer - 3 days ago

I think dry run mode is sometimes useful for many programs (and, I sometimes do use them). In some cases, you can use standard I/O so that it is not needed because you can control what is done with the output. Sometimes you might miss something especially if the code is messy, although security systems might help a bit. However, you can sometimes make the code less messy if the I/O is handled in a different way that makes this possible (e.g. by making the functions that make changes (the I/O parts of your program) to handle them in a way that the number of times you need to check for dry run is reduced if only a few functions need to); my ideas of a system with capability-based security would allow this (as well as many other benefits; a capability-based system has a lot of benefits beyond only the security system). Even with the existing security it can be done (e.g. with file permissions), although not as well as capability-based security.

gooseyman - 2 days ago

https://news.ycombinator.com/item?id=27263136

Related

tegiddrone - 2 days ago

I’m interested to know the etymology and history of the term. Somehow I imagine an inked printing press as the “wet run.”

bikelang - 3 days ago

I love `—-dry-run` flags for CLI tooling I build. If you plan your applications around this kind of functionality upfront - then I find it doesn’t have to pollute your code too much. In a language like Go or Rust - I’ll use a option/builder design pattern and whatever I’m ultimately writing to (remote file system, database, pubsub, etc) will instead write to a logger. I find this incredibly helpful in local dev - but it’s also useful in production. Even with high test coverage - it can be a bit spooky to turn on a new, consequential feature. Especially one that mutates data. I like to use dry run and enable this in our production envs just to ensure that things meet the functional and performance qualities we expect before actually enabling. This has definitely saved our bacon before (so many edge cases with prod data and request traffic).

cjonas - 3 days ago

We have an internal framework for building migrations and the "dry run" it's a core part of the dev cycle. Allows you to test your replication plan and transformations without touching the target. Not to mention, a load that could take >24 hours completes in minutes

spwa4 - 2 days ago

I would love to have this available in git. I know if you make mistakes you can use the reflog, but if you need 5 tries to get something right reading the reflog quickly becomes impossible. Plus there are operations, like rebase or merge, that feel the need to make 50 entries in the reflog.

I've resorted to copying the entire directory (including the .git part) and then trying on the copy. The issue is that I'm working on a C++ program that has a few gigabytes of data.

Arbortheus - 2 days ago

I prefer “—really-do”, so the default behaviour of the tool is to do nothing. That’s more fault tolerant for the scenario you forget to add “—dry-run”.

mfonda - 2 days ago

If the changes your command makes are strictly to a relational database, then `--dry-run` becomes quite easy to implement: just start a transaction and never commit it. This avoids polluting the entire command with `if dryRun` checks everywhere. I've found this to be a great approach.

aappleby - 3 days ago

What if the tool required an "un-safeword" to do destructive things?

"Do you really want to 'rm -rf /'? Type 'fiberglass' to proceed."

d7w - 2 days ago

Funny, I recalled a tool called "molly-guard" which solves the problem when you want to reboot a Unix server, but can be on the wrong one. It asks to type the server name.

Anybody who rebooted a wrong server can say that this tool is brilliant.

Like "--dry-run" but for "reboot."

giorgioz - 2 days ago

I didn't know about --dry-run until last summer Claude Code added it to a script it had created.

jaynate - 2 days ago

> The downside is that the dryRun-flag pollutes the code a bit. In all the major phases, I need to check if the flag is set, and only print the action that will be taken, but not actually doing it.

Sounds like a case for the state machine pattern

rook_line_sinkr - 2 days ago

Dry run is great, but if you are using your script in a serious pipeline like that, you may want to go tho extra mile and write tests

https://github.com/shellspec/shellspec

TZubiri - 3 days ago

I use --dry-run when I'm coding and I control the code.

Otherwise it's not very wise to trust the application on what should be a deputy responsibility.

Nowadays I'd probably use OverlayFS (or just Docker) to see what the changes would be, without ever risking the original FS.

taude - 3 days ago

Funny enough, when creating CLIs with Claude Code (and Github Copilot), they've both added `--dry-run` to my CLIs without me even prompting it.

I prefer the inverse, better, though. Default off, and then add `--commit` or `--just-do-it` to make it actually run.

alexhans - 2 days ago

Agreed. For me a good help, a dry run and a readme with good examples has been the norm for work tools for a while.

It's even more relevant now that you can get the LLMs/CLI agents to use your deterministic CLI tools.

bjt12345 - 2 days ago

I like to use the term "--no-clobber", so to set a script to not delete any information but re-use the previous configuration or files, otherwise error out if not possible.

mystifyingpoi - 2 days ago

I like doing the same in CI jobs, like in Jenkins I'll add a DRY_RUN parameter, that makes the whole job readonly. A script that does the deployment would then only write what would be done.

whalesalad - 2 days ago

The fact that every single `--` has become — drives me bananas. For a technical blog this oversight is so sloppy.

amelius - 2 days ago

In even more praise of Ctrl+Z.

(Don't try it in a terminal window, though)

calebhwin - 2 days ago

And it's more important than ever in the age of coding agents.

calvinmorrison - 3 days ago

--dry-run

--really

--really-really

--yolo

awesome_dude - 3 days ago

pffft, if you aren't dropping production databases first thing in the morning by accident, how are you going to wake yourself up :-)

devcraft_ai - 2 days ago

[dead]

techpulse_x - 2 days ago

[dead]

clawsyndicate - 2 days ago

[dead]

dabedee - 2 days ago

From the article: "I added –dry-run on a whim early on in the project. I was surprised at how useful I found it to be."

Not to be overly critical (I think it's great OP found value in adding and using --dry-run), but I am willing to bet that this was a suggestion/addition from a coding agent (and most likely Claude code/Opus). Having used it myself to build various CLI tools in different languages, it almost always creates that option when iterating on CLIs. To the point where it's almost a tell. I wonder if we're entering a moment of convergence where all the tools will have similar patterns/options because they are similarly written by agents.