Yep, an oddly overlooked flag. In terms of pollution, all the standard advice about keeping state transition as isolated as possible applies. That way, it doesn't pollute too much.
mdaniel
The downside is that the dryRun-flag pollutes the code a bit.
I know that it's not cool in modern times, but that's a problem that dependency injection is great at solving: when wiring up the deps, replace all the "action" bits with "print" implementations and it means that all the control flow to the action bits will remain intact. In my opinion a dry-run setting is basically the same as trying to do code coverage during testing, so I would hope that one doesn't have isTesting all throughout the codebase
I am acutely aware of the hazard that dependent flows have when trying this stunt (e.g. BlobStore::write followed by BlobStore::read will not lead to good outcomes if write only prints that it would have done so), but the end result could be worth having several implementations
Having said all of that, what drives me stark-raving are systems that emit a plan and consider that as a form of dry-run. The mangled adage is that "no plan survives first contact with someone else's control plane" so tofu plan is way different experience from a hypothetical tofu apply --dry-run showing what actual values it actually would have actually tried. A nix installer that I tried a few months ago was almost comical in its version of a "plan," choosing to say "oh, yeah, I'm going to do these 5 things described in english" versus echo mkdir -p /some/actual/path or similar
That last bit is how I implement "dry run" for a lot of my shell scripts:
In my experience the trouble with dry run is that it adds cognitive overhead that can be missed. When I was working for <large corp> I had a habit of making sure I never had implicit credentials with write permissions, and more than once --dry-run failed because it tried to write. Once a coworker brought down prod cause they didn't.
DI isn't my favorite design pattern, but it is great for this, if we can't interact with the outside without calling the injected dep and --dry-run works by injecting an impotent dep, we're more likely for it to actually be a dry run than if it's just a script that can do anything and everyone who edits it needs an if(dry_run)... around the spicy parts.
mighmi
replace all the "action" bits with "print" implementations
The do nothing scripts are really a whole model of development, huh
deivid
the dryRun-flag pollutes the code
My preferred way is to write the code such that the first phase emits a plan for all the work to be done, then the second phase executes tbe plan.
This way you only check once whether it's dry run mode
muvlon
This has the added benefit of avoiding certain race conditions. With dry-run, it's possible that you check the output and it's fine but when you then run the same command again without --dry-run and it does something different because circumstances have changed.
Riolku
I came here to say exactly this. I've been doing a lot of terraform at work and I stole its model for another tool I wrote to get an effective dry run model
kototama
That's nice. I think it's similar to the usage of free monads in Haskell but someone with more Haskell experience might want to correct me.
In general once you start to decouple the side effects you probably always have the same pattern : one data structure (AST or whatever) and one or multiple way to interpret it.
The downside could be performance.
adam_d_ruppe
I often write up little ad hoc shell loops, and my first draft is always for i in stuff; do echo operation....... ; done and once im happy with it, delete the word echo and run it again. Especially with shell expansion being quirky, it really helps to check before i break my files.
scruss
Rather than deleting the echo, I pipe the output to sh.
If I'm feeling really fancy (and none of the job lines depend on each other), I'll pipe the output via awk '{print $0 " &";} NR%6==0 {print "wait";} END {print "wait";}' and get cheap-ass multiprocessing without having to learn parallel or xargs -P weird syntax.
hyperpape
Having a dry run command is a very natural thing to do when you structure software according to a plan then execute pattern: https://mmapped.blog/posts/29-plan-execute.html, which is one of the most useful patterns I know of for structuring software.
ajdecon
I love a dry-run flag.
Granted, from a design perspective I usually prefer the reverse: a —commit flag needed to actually make changes, and have dry run be the default. But if that behavior isn’t designed in from the start, it’s a breaking change to add it.
—dry-run has the advantage that it’s… well, not easy, but at least compatible to retrofit it in after the fact. Which I have done on so. many. tools over the years.
rtpg
Had to work on a script recently where I was deathly afraid of accidentally committing that I made dry running the default
Dry running is tough though. Even with a lot of care it's easy for an effect to sneak in. Maybe if you're using Haskell/Purescript your effect system will catch that but for most of us we're not so lucky lol
ElevenLathe
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.
weikju
Came here to say the same
eichin
I've committed "--i-meant-that" (for a destroy-the-remote-machine command that normally (without the arg) gives you a message and 10s to hit ^C if you're not sure, for some particularly impatient coworkers. Never ended up being used inappropriately, which is luck (but we never quantified how much luck :-)
bikelang
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).
zzo38computer
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.
arjie
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.
segmondy
this is where design patterns come in handy even tho folks roll their eyes at it.
wging
One nice way to do things, if you can get away with it, is to model the actions your application takes explicitly, and pass them to a central thing that actually handles them. Then there can be one place in your code that actually needs to understand whether it's doing a dry run or not. Ideally this would be just returning them from your core logic, "functional core, imperative shell" style.
sh-run
I don't like the sound of `--wet-run`, but on more than one occasion I've written tools (and less frequently services) that default to `dry-run` and require `--no-dry-run` to actually make changes.
For services, I prefer having them detect where they are running. Ie if it's running in a dev environment, it's going to use a dev db by default.
ryandrake
I don't want to have to type rm --wet-run tempfile.tmp every time, or mkdir -p --yes-really-do-it /usr/local/bin
The program should default to actually doing whatever thing you're asking it to do.
On the other hand it would be great if every tool had an --undo argument that would undo the last thing that program did.
skissane
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:`
scruple
Having 1 place (or just generally limiting them) that does the things keeps the dry_run check from polluting the entire codebase. I maintain a lot of CLI tooling that's run by headless VMs in automation pipelines and we do this with basically every single tool.
iberator
What's your obsession with REST AND HTTP for console tools?!
Rest bloat is insane. Kidss nowadays wants EVERYTHING to run over IP/TCP/https. Why?!
Learn to write local tools first.
awesome_dude
pffft, if you aren't dropping production databases first thing in the morning by accident, how are you going to wake yourself up :-)
cjonas
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
mycall
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.
Twirrim
I've biased towards this heavily in the last 8 or so years now.
I've yet to have anyone mistakenly modify anything when they need to pass --commit, when I've repeatedly had people repeatedly accidentally modify stuff because they forgot --dry-run.
xyse53
Yeah I'm more of a `--wet-run` `-w` fan myself. But it does depend on how serious/annoying the opposite is.
lazide
Just don’t randomly mix and match the approaches or you are in for a bad time.
spike021
There was a tool I used some time ago that required typing in a word or phrase to acknowledge that you know it's doing the run for real.
Pros and cons to each but I did like that because it was much more difficult to fat finger or absentmindedly use the wrong parameter.
torstenvl
I have a parallel directory deduper that uses hard links and adopted this pattern exactly.
By default it'll only tell you which files are identical between the two parallel directory structures.
If you want it to actually replace the files with hard links, you have to use the --execute flag.
inglor_cz
This is something I learnt here.
My latest script which deletes the entire content of a downloaded Sharepoint (locally only) and the relevant MS365 account from the computer runs by default in a read-only mode. You have to run it with an explicit flag to allow for changes.
Also, before it actually deletes the account, you need to explicitly type DELETE-ACCOUNT in order to confirm that this is indeed your intent.
So far, nobody managed to screw up, even in heated situations at client's place.
taude
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.
CGamesPlay
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.
throwaway314155
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).
CGamesPlay
I'm not aware of any CLI arguments that accept emdash for long arguments–but I'm here for it. "A CLI framework for the LLM era"
analog31
I only saw the emdash in the thread link, but I do know that an iPad "wants" to turn a double dash into an emdash automatically. I have no idea how to disable that default.
calvinmorrison
--dry-run
--really
--really-really
--yolo
homebrewer
You'll like fontconfig then, which has both --force and --really-force
I’m interested to know the etymology and history of the term. Somehow I imagine an inked printing press as the “wet run.”
hydrox24
It seems to have originated in the US with Fire Departments:
> These reports show that a dry run in the jargon of the fire service at this period [1880s–1890s] was one that didn’t involve the use of water, as opposed to a wet run that did.
Interestingly the one place I have seen "dry run" to actually mean "dry run" is using a air compressor to check to see if a water loop (in a computer) doesn't leak by seeing if there no drop in pressure.
BrouteMinou
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.
debugnik
Even better, it enables both -WhatIf and -Confirm, and provides a ShouldProcess function that interacts with both and with the user's impact threshold preferences. Really cool.
alexhans
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.
mystifyingpoi
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.
calebhwin
And it's more important than ever in the age of coding agents.
sixtram
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.
sixtram
To continue, this design has additional benefits:
The code is not littered with dry-run flag checks; the internal code doesn’t even know that a dry run is possible. Everything is rolled back at the end if needed.
All database referential integrity checks run correctly.
Some drawbacks: any audit logging should run in a separate transaction if you want to log dry runs.
xavdid
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
marhee
Doesn’t this conflate dry-running with integration testing? ASAIK the purpose of a dry-run is to understand what will happen, not to test what will happen. For the latter we have testing.
giorgioz
I didn't know about --dry-run until last summer Claude Code added it to a script it had created.
bjt12345
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.
muvlon
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()).
Jolter
I like that idea! For an application like Terraform, Ansible or the like, it seems ideal.
For something like in the article, I’m pretty sure a plan mode is overkill though.
Planning mode must involve making a domain specific language or data structure of some sort, which the execution mode will interpret and execute. I’m sure it would add a lot of complexity to a reporting tool where data is only collected once per day.
GeneralMaximus
Yes! I'm currently working on a script that modifies a bunch of sensitive files, and this the approach I'm taking to make sure I don't accidentally lose any important data.
I've split the process into three parts:
1. Walk the filesystem, capture the current state of the files, and write out a plan to disk.
2. Make sure the state of the files from step 1 has not changed, then execute the plan. Capture the new state of the files. Additionally, log all operations to disk in a journal.
3. Validate that no data was lost or unexpectedly changed using the captured file state from steps 1 and 2. Manually look at the operations log (or dump it into an LLM) to make sure nothing looks off.
These three steps can be three separate scripts, or three flags to the same script.
nlehuen
And just like that, you find yourself implementing a compiler (specs to plan) and a virtual machine (plan to actions)!
rook_line_sinkr
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
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.
d7w
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."
fainpul
[delayed]
Arbortheus
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”.
nickmonad
Yep, an oddly overlooked flag. In terms of pollution, all the standard advice about keeping state transition as isolated as possible applies. That way, it doesn't pollute too much.
mdaniel
The downside is that the dryRun-flag pollutes the code a bit.
I know that it's not cool in modern times, but that's a problem that dependency injection is great at solving: when wiring up the deps, replace all the "action" bits with "print" implementations and it means that all the control flow to the action bits will remain intact. In my opinion a dry-run setting is basically the same as trying to do code coverage during testing, so I would hope that one doesn't have isTesting all throughout the codebase
I am acutely aware of the hazard that dependent flows have when trying this stunt (e.g. BlobStore::write followed by BlobStore::read will not lead to good outcomes if write only prints that it would have done so), but the end result could be worth having several implementations
Having said all of that, what drives me stark-raving are systems that emit a plan and consider that as a form of dry-run. The mangled adage is that "no plan survives first contact with someone else's control plane" so tofu plan is way different experience from a hypothetical tofu apply --dry-run showing what actual values it actually would have actually tried. A nix installer that I tried a few months ago was almost comical in its version of a "plan," choosing to say "oh, yeah, I'm going to do these 5 things described in english" versus echo mkdir -p /some/actual/path or similar
That last bit is how I implement "dry run" for a lot of my shell scripts:
In my experience the trouble with dry run is that it adds cognitive overhead that can be missed. When I was working for <large corp> I had a habit of making sure I never had implicit credentials with write permissions, and more than once --dry-run failed because it tried to write. Once a coworker brought down prod cause they didn't.
DI isn't my favorite design pattern, but it is great for this, if we can't interact with the outside without calling the injected dep and --dry-run works by injecting an impotent dep, we're more likely for it to actually be a dry run than if it's just a script that can do anything and everyone who edits it needs an if(dry_run)... around the spicy parts.
mighmi
replace all the "action" bits with "print" implementations
The do nothing scripts are really a whole model of development, huh
deivid
the dryRun-flag pollutes the code
My preferred way is to write the code such that the first phase emits a plan for all the work to be done, then the second phase executes tbe plan.
This way you only check once whether it's dry run mode
muvlon
This has the added benefit of avoiding certain race conditions. With dry-run, it's possible that you check the output and it's fine but when you then run the same command again without --dry-run and it does something different because circumstances have changed.
Riolku
I came here to say exactly this. I've been doing a lot of terraform at work and I stole its model for another tool I wrote to get an effective dry run model
kototama
That's nice. I think it's similar to the usage of free monads in Haskell but someone with more Haskell experience might want to correct me.
In general once you start to decouple the side effects you probably always have the same pattern : one data structure (AST or whatever) and one or multiple way to interpret it.
The downside could be performance.
adam_d_ruppe
I often write up little ad hoc shell loops, and my first draft is always for i in stuff; do echo operation....... ; done and once im happy with it, delete the word echo and run it again. Especially with shell expansion being quirky, it really helps to check before i break my files.
scruss
Rather than deleting the echo, I pipe the output to sh.
If I'm feeling really fancy (and none of the job lines depend on each other), I'll pipe the output via awk '{print $0 " &";} NR%6==0 {print "wait";} END {print "wait";}' and get cheap-ass multiprocessing without having to learn parallel or xargs -P weird syntax.
hyperpape
Having a dry run command is a very natural thing to do when you structure software according to a plan then execute pattern: https://mmapped.blog/posts/29-plan-execute.html, which is one of the most useful patterns I know of for structuring software.
ajdecon
I love a dry-run flag.
Granted, from a design perspective I usually prefer the reverse: a —commit flag needed to actually make changes, and have dry run be the default. But if that behavior isn’t designed in from the start, it’s a breaking change to add it.
—dry-run has the advantage that it’s… well, not easy, but at least compatible to retrofit it in after the fact. Which I have done on so. many. tools over the years.
rtpg
Had to work on a script recently where I was deathly afraid of accidentally committing that I made dry running the default
Dry running is tough though. Even with a lot of care it's easy for an effect to sneak in. Maybe if you're using Haskell/Purescript your effect system will catch that but for most of us we're not so lucky lol
ElevenLathe
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.
weikju
Came here to say the same
eichin
I've committed "--i-meant-that" (for a destroy-the-remote-machine command that normally (without the arg) gives you a message and 10s to hit ^C if you're not sure, for some particularly impatient coworkers. Never ended up being used inappropriately, which is luck (but we never quantified how much luck :-)
bikelang
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).
zzo38computer
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.
arjie
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.
segmondy
this is where design patterns come in handy even tho folks roll their eyes at it.
wging
One nice way to do things, if you can get away with it, is to model the actions your application takes explicitly, and pass them to a central thing that actually handles them. Then there can be one place in your code that actually needs to understand whether it's doing a dry run or not. Ideally this would be just returning them from your core logic, "functional core, imperative shell" style.
sh-run
I don't like the sound of `--wet-run`, but on more than one occasion I've written tools (and less frequently services) that default to `dry-run` and require `--no-dry-run` to actually make changes.
For services, I prefer having them detect where they are running. Ie if it's running in a dev environment, it's going to use a dev db by default.
ryandrake
I don't want to have to type rm --wet-run tempfile.tmp every time, or mkdir -p --yes-really-do-it /usr/local/bin
The program should default to actually doing whatever thing you're asking it to do.
On the other hand it would be great if every tool had an --undo argument that would undo the last thing that program did.
skissane
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:`
scruple
Having 1 place (or just generally limiting them) that does the things keeps the dry_run check from polluting the entire codebase. I maintain a lot of CLI tooling that's run by headless VMs in automation pipelines and we do this with basically every single tool.
iberator
What's your obsession with REST AND HTTP for console tools?!
Rest bloat is insane. Kidss nowadays wants EVERYTHING to run over IP/TCP/https. Why?!
Learn to write local tools first.
awesome_dude
pffft, if you aren't dropping production databases first thing in the morning by accident, how are you going to wake yourself up :-)
cjonas
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
mycall
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.
Twirrim
I've biased towards this heavily in the last 8 or so years now.
I've yet to have anyone mistakenly modify anything when they need to pass --commit, when I've repeatedly had people repeatedly accidentally modify stuff because they forgot --dry-run.
xyse53
Yeah I'm more of a `--wet-run` `-w` fan myself. But it does depend on how serious/annoying the opposite is.
lazide
Just don’t randomly mix and match the approaches or you are in for a bad time.
spike021
There was a tool I used some time ago that required typing in a word or phrase to acknowledge that you know it's doing the run for real.
Pros and cons to each but I did like that because it was much more difficult to fat finger or absentmindedly use the wrong parameter.
torstenvl
I have a parallel directory deduper that uses hard links and adopted this pattern exactly.
By default it'll only tell you which files are identical between the two parallel directory structures.
If you want it to actually replace the files with hard links, you have to use the --execute flag.
inglor_cz
This is something I learnt here.
My latest script which deletes the entire content of a downloaded Sharepoint (locally only) and the relevant MS365 account from the computer runs by default in a read-only mode. You have to run it with an explicit flag to allow for changes.
Also, before it actually deletes the account, you need to explicitly type DELETE-ACCOUNT in order to confirm that this is indeed your intent.
So far, nobody managed to screw up, even in heated situations at client's place.
taude
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.
CGamesPlay
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.
throwaway314155
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).
CGamesPlay
I'm not aware of any CLI arguments that accept emdash for long arguments–but I'm here for it. "A CLI framework for the LLM era"
analog31
I only saw the emdash in the thread link, but I do know that an iPad "wants" to turn a double dash into an emdash automatically. I have no idea how to disable that default.
calvinmorrison
--dry-run
--really
--really-really
--yolo
homebrewer
You'll like fontconfig then, which has both --force and --really-force
I’m interested to know the etymology and history of the term. Somehow I imagine an inked printing press as the “wet run.”
hydrox24
It seems to have originated in the US with Fire Departments:
> These reports show that a dry run in the jargon of the fire service at this period [1880s–1890s] was one that didn’t involve the use of water, as opposed to a wet run that did.
Interestingly the one place I have seen "dry run" to actually mean "dry run" is using a air compressor to check to see if a water loop (in a computer) doesn't leak by seeing if there no drop in pressure.
BrouteMinou
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.
debugnik
Even better, it enables both -WhatIf and -Confirm, and provides a ShouldProcess function that interacts with both and with the user's impact threshold preferences. Really cool.
alexhans
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.
mystifyingpoi
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.
calebhwin
And it's more important than ever in the age of coding agents.
sixtram
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.
sixtram
To continue, this design has additional benefits:
The code is not littered with dry-run flag checks; the internal code doesn’t even know that a dry run is possible. Everything is rolled back at the end if needed.
All database referential integrity checks run correctly.
Some drawbacks: any audit logging should run in a separate transaction if you want to log dry runs.
xavdid
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
marhee
Doesn’t this conflate dry-running with integration testing? ASAIK the purpose of a dry-run is to understand what will happen, not to test what will happen. For the latter we have testing.
giorgioz
I didn't know about --dry-run until last summer Claude Code added it to a script it had created.
bjt12345
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.
muvlon
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()).
Jolter
I like that idea! For an application like Terraform, Ansible or the like, it seems ideal.
For something like in the article, I’m pretty sure a plan mode is overkill though.
Planning mode must involve making a domain specific language or data structure of some sort, which the execution mode will interpret and execute. I’m sure it would add a lot of complexity to a reporting tool where data is only collected once per day.
GeneralMaximus
Yes! I'm currently working on a script that modifies a bunch of sensitive files, and this the approach I'm taking to make sure I don't accidentally lose any important data.
I've split the process into three parts:
1. Walk the filesystem, capture the current state of the files, and write out a plan to disk.
2. Make sure the state of the files from step 1 has not changed, then execute the plan. Capture the new state of the files. Additionally, log all operations to disk in a journal.
3. Validate that no data was lost or unexpectedly changed using the captured file state from steps 1 and 2. Manually look at the operations log (or dump it into an LLM) to make sure nothing looks off.
These three steps can be three separate scripts, or three flags to the same script.
nlehuen
And just like that, you find yourself implementing a compiler (specs to plan) and a virtual machine (plan to actions)!
rook_line_sinkr
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
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.
d7w
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."
fainpul
[delayed]
Arbortheus
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”.
Yep, an oddly overlooked flag. In terms of pollution, all the standard advice about keeping state transition as isolated as possible applies. That way, it doesn't pollute too much.
I know that it's not cool in modern times, but that's a problem that dependency injection is great at solving: when wiring up the deps, replace all the "action" bits with "print" implementations and it means that all the control flow to the action bits will remain intact. In my opinion a dry-run setting is basically the same as trying to do code coverage during testing, so I would hope that one doesn't have
isTestingall throughout the codebaseI am acutely aware of the hazard that dependent flows have when trying this stunt (e.g.
BlobStore::writefollowed byBlobStore::readwill not lead to good outcomes ifwriteonly prints that it would have done so), but the end result could be worth having several implementationsHaving said all of that, what drives me stark-raving are systems that emit a plan and consider that as a form of dry-run. The mangled adage is that "no plan survives first contact with someone else's control plane" so
tofu planis way different experience from a hypotheticaltofu apply --dry-runshowing what actual values it actually would have actually tried. A nix installer that I tried a few months ago was almost comical in its version of a "plan," choosing to say "oh, yeah, I'm going to do these 5 things described in english" versusecho mkdir -p /some/actual/pathor similarThat last bit is how I implement "dry run" for a lot of my shell scripts:
followed by
N=echo ./my_script.shIn my experience the trouble with dry run is that it adds cognitive overhead that can be missed. When I was working for <large corp> I had a habit of making sure I never had implicit credentials with write permissions, and more than once --dry-run failed because it tried to write. Once a coworker brought down prod cause they didn't.
DI isn't my favorite design pattern, but it is great for this, if we can't interact with the outside without calling the injected dep and --dry-run works by injecting an impotent dep, we're more likely for it to actually be a dry run than if it's just a script that can do anything and everyone who edits it needs an if(dry_run)... around the spicy parts.
The do nothing scripts are really a whole model of development, huh
My preferred way is to write the code such that the first phase emits a plan for all the work to be done, then the second phase executes tbe plan.
This way you only check once whether it's dry run mode
This has the added benefit of avoiding certain race conditions. With dry-run, it's possible that you check the output and it's fine but when you then run the same command again without --dry-run and it does something different because circumstances have changed.
I came here to say exactly this. I've been doing a lot of terraform at work and I stole its model for another tool I wrote to get an effective dry run model
That's nice. I think it's similar to the usage of free monads in Haskell but someone with more Haskell experience might want to correct me.
In general once you start to decouple the side effects you probably always have the same pattern : one data structure (AST or whatever) and one or multiple way to interpret it.
The downside could be performance.
I often write up little ad hoc shell loops, and my first draft is always
for i in stuff; do echo operation....... ; doneand once im happy with it, delete the wordechoand run it again. Especially with shell expansion being quirky, it really helps to check before i break my files.Rather than deleting the
echo, I pipe the output tosh.If I'm feeling really fancy (and none of the job lines depend on each other), I'll pipe the output via
awk '{print $0 " &";} NR%6==0 {print "wait";} END {print "wait";}'and get cheap-ass multiprocessing without having to learnparallelorxargs -Pweird syntax.Having a dry run command is a very natural thing to do when you structure software according to a plan then execute pattern: https://mmapped.blog/posts/29-plan-execute.html, which is one of the most useful patterns I know of for structuring software.
I love a dry-run flag.
Granted, from a design perspective I usually prefer the reverse: a —commit flag needed to actually make changes, and have dry run be the default. But if that behavior isn’t designed in from the start, it’s a breaking change to add it.
—dry-run has the advantage that it’s… well, not easy, but at least compatible to retrofit it in after the fact. Which I have done on so. many. tools over the years.
Had to work on a script recently where I was deathly afraid of accidentally committing that I made dry running the default
Dry running is tough though. Even with a lot of care it's easy for an effect to sneak in. Maybe if you're using Haskell/Purescript your effect system will catch that but for most of us we're not so lucky lol
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.
Came here to say the same
I've committed "--i-meant-that" (for a destroy-the-remote-machine command that normally (without the arg) gives you a message and 10s to hit ^C if you're not sure, for some particularly impatient coworkers. Never ended up being used inappropriately, which is luck (but we never quantified how much luck :-)
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).
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.
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.
this is where design patterns come in handy even tho folks roll their eyes at it.
One nice way to do things, if you can get away with it, is to model the actions your application takes explicitly, and pass them to a central thing that actually handles them. Then there can be one place in your code that actually needs to understand whether it's doing a dry run or not. Ideally this would be just returning them from your core logic, "functional core, imperative shell" style.
I don't like the sound of `--wet-run`, but on more than one occasion I've written tools (and less frequently services) that default to `dry-run` and require `--no-dry-run` to actually make changes.
For services, I prefer having them detect where they are running. Ie if it's running in a dev environment, it's going to use a dev db by default.
I don't want to have to type rm --wet-run tempfile.tmp every time, or mkdir -p --yes-really-do-it /usr/local/bin
The program should default to actually doing whatever thing you're asking it to do.
On the other hand it would be great if every tool had an --undo argument that would undo the last thing that program did.
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:`
Having 1 place (or just generally limiting them) that does the things keeps the dry_run check from polluting the entire codebase. I maintain a lot of CLI tooling that's run by headless VMs in automation pipelines and we do this with basically every single tool.
What's your obsession with REST AND HTTP for console tools?!
Rest bloat is insane. Kidss nowadays wants EVERYTHING to run over IP/TCP/https. Why?!
Learn to write local tools first.
pffft, if you aren't dropping production databases first thing in the morning by accident, how are you going to wake yourself up :-)
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
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.
I've biased towards this heavily in the last 8 or so years now.
I've yet to have anyone mistakenly modify anything when they need to pass --commit, when I've repeatedly had people repeatedly accidentally modify stuff because they forgot --dry-run.
Yeah I'm more of a `--wet-run` `-w` fan myself. But it does depend on how serious/annoying the opposite is.
Just don’t randomly mix and match the approaches or you are in for a bad time.
There was a tool I used some time ago that required typing in a word or phrase to acknowledge that you know it's doing the run for real.
Pros and cons to each but I did like that because it was much more difficult to fat finger or absentmindedly use the wrong parameter.
I have a parallel directory deduper that uses hard links and adopted this pattern exactly.
By default it'll only tell you which files are identical between the two parallel directory structures.
If you want it to actually replace the files with hard links, you have to use the --execute flag.
This is something I learnt here.
My latest script which deletes the entire content of a downloaded Sharepoint (locally only) and the relevant MS365 account from the computer runs by default in a read-only mode. You have to run it with an explicit flag to allow for changes.
Also, before it actually deletes the account, you need to explicitly type DELETE-ACCOUNT in order to confirm that this is indeed your intent.
So far, nobody managed to screw up, even in heated situations at client's place.
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.
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.
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).
I'm not aware of any CLI arguments that accept emdash for long arguments–but I'm here for it. "A CLI framework for the LLM era"
I only saw the emdash in the thread link, but I do know that an iPad "wants" to turn a double dash into an emdash automatically. I have no idea how to disable that default.
--dry-run
--really
--really-really
--yolo
You'll like fontconfig then, which has both --force and --really-force
https://man.archlinux.org/man/fc-cache.1.en
What if the tool required an "un-safeword" to do destructive things?
"Do you really want to 'rm -rf /'? Type 'fiberglass' to proceed."
Like tarsnap's --nuke command:
There is a package called molly-guard that makes you type the computer's hostname when you are trying to do a shutdown or restart. I love it.
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.
How do you easily diff what changed between Docker and host?
https://news.ycombinator.com/item?id=27263136
Related
I’m interested to know the etymology and history of the term. Somehow I imagine an inked printing press as the “wet run.”
It seems to have originated in the US with Fire Departments:
> These reports show that a dry run in the jargon of the fire service at this period [1880s–1890s] was one that didn’t involve the use of water, as opposed to a wet run that did.
https://www.worldwidewords.org/qa/qa-dry1.htm
Interestingly the one place I have seen "dry run" to actually mean "dry run" is using a air compressor to check to see if a water loop (in a computer) doesn't leak by seeing if there no drop in pressure.
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.
Even better, it enables both -WhatIf and -Confirm, and provides a ShouldProcess function that interacts with both and with the user's impact threshold preferences. Really cool.
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.
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.
And it's more important than ever in the age of coding agents.
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.
To continue, this design has additional benefits:
The code is not littered with dry-run flag checks; the internal code doesn’t even know that a dry run is possible. Everything is rolled back at the end if needed.
All database referential integrity checks run correctly.
Some drawbacks: any audit logging should run in a separate transaction if you want to log dry runs.
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
Doesn’t this conflate dry-running with integration testing? ASAIK the purpose of a dry-run is to understand what will happen, not to test what will happen. For the latter we have testing.
I didn't know about --dry-run until last summer Claude Code added it to a script it had created.
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.
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()).
I like that idea! For an application like Terraform, Ansible or the like, it seems ideal.
For something like in the article, I’m pretty sure a plan mode is overkill though.
Planning mode must involve making a domain specific language or data structure of some sort, which the execution mode will interpret and execute. I’m sure it would add a lot of complexity to a reporting tool where data is only collected once per day.
Yes! I'm currently working on a script that modifies a bunch of sensitive files, and this the approach I'm taking to make sure I don't accidentally lose any important data.
I've split the process into three parts:
1. Walk the filesystem, capture the current state of the files, and write out a plan to disk.
2. Make sure the state of the files from step 1 has not changed, then execute the plan. Capture the new state of the files. Additionally, log all operations to disk in a journal.
3. Validate that no data was lost or unexpectedly changed using the captured file state from steps 1 and 2. Manually look at the operations log (or dump it into an LLM) to make sure nothing looks off.
These three steps can be three separate scripts, or three flags to the same script.
And just like that, you find yourself implementing a compiler (specs to plan) and a virtual machine (plan to actions)!
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
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.
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."
[delayed]
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”.
Yep, an oddly overlooked flag. In terms of pollution, all the standard advice about keeping state transition as isolated as possible applies. That way, it doesn't pollute too much.
I know that it's not cool in modern times, but that's a problem that dependency injection is great at solving: when wiring up the deps, replace all the "action" bits with "print" implementations and it means that all the control flow to the action bits will remain intact. In my opinion a dry-run setting is basically the same as trying to do code coverage during testing, so I would hope that one doesn't have
isTestingall throughout the codebaseI am acutely aware of the hazard that dependent flows have when trying this stunt (e.g.
BlobStore::writefollowed byBlobStore::readwill not lead to good outcomes ifwriteonly prints that it would have done so), but the end result could be worth having several implementationsHaving said all of that, what drives me stark-raving are systems that emit a plan and consider that as a form of dry-run. The mangled adage is that "no plan survives first contact with someone else's control plane" so
tofu planis way different experience from a hypotheticaltofu apply --dry-runshowing what actual values it actually would have actually tried. A nix installer that I tried a few months ago was almost comical in its version of a "plan," choosing to say "oh, yeah, I'm going to do these 5 things described in english" versusecho mkdir -p /some/actual/pathor similarThat last bit is how I implement "dry run" for a lot of my shell scripts:
followed by
N=echo ./my_script.shIn my experience the trouble with dry run is that it adds cognitive overhead that can be missed. When I was working for <large corp> I had a habit of making sure I never had implicit credentials with write permissions, and more than once --dry-run failed because it tried to write. Once a coworker brought down prod cause they didn't.
DI isn't my favorite design pattern, but it is great for this, if we can't interact with the outside without calling the injected dep and --dry-run works by injecting an impotent dep, we're more likely for it to actually be a dry run than if it's just a script that can do anything and everyone who edits it needs an if(dry_run)... around the spicy parts.
The do nothing scripts are really a whole model of development, huh
My preferred way is to write the code such that the first phase emits a plan for all the work to be done, then the second phase executes tbe plan.
This way you only check once whether it's dry run mode
This has the added benefit of avoiding certain race conditions. With dry-run, it's possible that you check the output and it's fine but when you then run the same command again without --dry-run and it does something different because circumstances have changed.
I came here to say exactly this. I've been doing a lot of terraform at work and I stole its model for another tool I wrote to get an effective dry run model
That's nice. I think it's similar to the usage of free monads in Haskell but someone with more Haskell experience might want to correct me.
In general once you start to decouple the side effects you probably always have the same pattern : one data structure (AST or whatever) and one or multiple way to interpret it.
The downside could be performance.
I often write up little ad hoc shell loops, and my first draft is always
for i in stuff; do echo operation....... ; doneand once im happy with it, delete the wordechoand run it again. Especially with shell expansion being quirky, it really helps to check before i break my files.Rather than deleting the
echo, I pipe the output tosh.If I'm feeling really fancy (and none of the job lines depend on each other), I'll pipe the output via
awk '{print $0 " &";} NR%6==0 {print "wait";} END {print "wait";}'and get cheap-ass multiprocessing without having to learnparallelorxargs -Pweird syntax.Having a dry run command is a very natural thing to do when you structure software according to a plan then execute pattern: https://mmapped.blog/posts/29-plan-execute.html, which is one of the most useful patterns I know of for structuring software.
I love a dry-run flag.
Granted, from a design perspective I usually prefer the reverse: a —commit flag needed to actually make changes, and have dry run be the default. But if that behavior isn’t designed in from the start, it’s a breaking change to add it.
—dry-run has the advantage that it’s… well, not easy, but at least compatible to retrofit it in after the fact. Which I have done on so. many. tools over the years.
Had to work on a script recently where I was deathly afraid of accidentally committing that I made dry running the default
Dry running is tough though. Even with a lot of care it's easy for an effect to sneak in. Maybe if you're using Haskell/Purescript your effect system will catch that but for most of us we're not so lucky lol
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.
Came here to say the same
I've committed "--i-meant-that" (for a destroy-the-remote-machine command that normally (without the arg) gives you a message and 10s to hit ^C if you're not sure, for some particularly impatient coworkers. Never ended up being used inappropriately, which is luck (but we never quantified how much luck :-)
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).
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.
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.
this is where design patterns come in handy even tho folks roll their eyes at it.
One nice way to do things, if you can get away with it, is to model the actions your application takes explicitly, and pass them to a central thing that actually handles them. Then there can be one place in your code that actually needs to understand whether it's doing a dry run or not. Ideally this would be just returning them from your core logic, "functional core, imperative shell" style.
I don't like the sound of `--wet-run`, but on more than one occasion I've written tools (and less frequently services) that default to `dry-run` and require `--no-dry-run` to actually make changes.
For services, I prefer having them detect where they are running. Ie if it's running in a dev environment, it's going to use a dev db by default.
I don't want to have to type rm --wet-run tempfile.tmp every time, or mkdir -p --yes-really-do-it /usr/local/bin
The program should default to actually doing whatever thing you're asking it to do.
On the other hand it would be great if every tool had an --undo argument that would undo the last thing that program did.
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:`
Having 1 place (or just generally limiting them) that does the things keeps the dry_run check from polluting the entire codebase. I maintain a lot of CLI tooling that's run by headless VMs in automation pipelines and we do this with basically every single tool.
What's your obsession with REST AND HTTP for console tools?!
Rest bloat is insane. Kidss nowadays wants EVERYTHING to run over IP/TCP/https. Why?!
Learn to write local tools first.
pffft, if you aren't dropping production databases first thing in the morning by accident, how are you going to wake yourself up :-)
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
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.
I've biased towards this heavily in the last 8 or so years now.
I've yet to have anyone mistakenly modify anything when they need to pass --commit, when I've repeatedly had people repeatedly accidentally modify stuff because they forgot --dry-run.
Yeah I'm more of a `--wet-run` `-w` fan myself. But it does depend on how serious/annoying the opposite is.
Just don’t randomly mix and match the approaches or you are in for a bad time.
There was a tool I used some time ago that required typing in a word or phrase to acknowledge that you know it's doing the run for real.
Pros and cons to each but I did like that because it was much more difficult to fat finger or absentmindedly use the wrong parameter.
I have a parallel directory deduper that uses hard links and adopted this pattern exactly.
By default it'll only tell you which files are identical between the two parallel directory structures.
If you want it to actually replace the files with hard links, you have to use the --execute flag.
This is something I learnt here.
My latest script which deletes the entire content of a downloaded Sharepoint (locally only) and the relevant MS365 account from the computer runs by default in a read-only mode. You have to run it with an explicit flag to allow for changes.
Also, before it actually deletes the account, you need to explicitly type DELETE-ACCOUNT in order to confirm that this is indeed your intent.
So far, nobody managed to screw up, even in heated situations at client's place.
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.
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.
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).
I'm not aware of any CLI arguments that accept emdash for long arguments–but I'm here for it. "A CLI framework for the LLM era"
I only saw the emdash in the thread link, but I do know that an iPad "wants" to turn a double dash into an emdash automatically. I have no idea how to disable that default.
--dry-run
--really
--really-really
--yolo
You'll like fontconfig then, which has both --force and --really-force
https://man.archlinux.org/man/fc-cache.1.en
What if the tool required an "un-safeword" to do destructive things?
"Do you really want to 'rm -rf /'? Type 'fiberglass' to proceed."
Like tarsnap's --nuke command:
There is a package called molly-guard that makes you type the computer's hostname when you are trying to do a shutdown or restart. I love it.
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.
How do you easily diff what changed between Docker and host?
https://news.ycombinator.com/item?id=27263136
Related
I’m interested to know the etymology and history of the term. Somehow I imagine an inked printing press as the “wet run.”
It seems to have originated in the US with Fire Departments:
> These reports show that a dry run in the jargon of the fire service at this period [1880s–1890s] was one that didn’t involve the use of water, as opposed to a wet run that did.
https://www.worldwidewords.org/qa/qa-dry1.htm
Interestingly the one place I have seen "dry run" to actually mean "dry run" is using a air compressor to check to see if a water loop (in a computer) doesn't leak by seeing if there no drop in pressure.
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.
Even better, it enables both -WhatIf and -Confirm, and provides a ShouldProcess function that interacts with both and with the user's impact threshold preferences. Really cool.
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.
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.
And it's more important than ever in the age of coding agents.
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.
To continue, this design has additional benefits:
The code is not littered with dry-run flag checks; the internal code doesn’t even know that a dry run is possible. Everything is rolled back at the end if needed.
All database referential integrity checks run correctly.
Some drawbacks: any audit logging should run in a separate transaction if you want to log dry runs.
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
Doesn’t this conflate dry-running with integration testing? ASAIK the purpose of a dry-run is to understand what will happen, not to test what will happen. For the latter we have testing.
I didn't know about --dry-run until last summer Claude Code added it to a script it had created.
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.
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()).
I like that idea! For an application like Terraform, Ansible or the like, it seems ideal.
For something like in the article, I’m pretty sure a plan mode is overkill though.
Planning mode must involve making a domain specific language or data structure of some sort, which the execution mode will interpret and execute. I’m sure it would add a lot of complexity to a reporting tool where data is only collected once per day.
Yes! I'm currently working on a script that modifies a bunch of sensitive files, and this the approach I'm taking to make sure I don't accidentally lose any important data.
I've split the process into three parts:
1. Walk the filesystem, capture the current state of the files, and write out a plan to disk.
2. Make sure the state of the files from step 1 has not changed, then execute the plan. Capture the new state of the files. Additionally, log all operations to disk in a journal.
3. Validate that no data was lost or unexpectedly changed using the captured file state from steps 1 and 2. Manually look at the operations log (or dump it into an LLM) to make sure nothing looks off.
These three steps can be three separate scripts, or three flags to the same script.
And just like that, you find yourself implementing a compiler (specs to plan) and a virtual machine (plan to actions)!
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
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.
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."
[delayed]
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”.