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.
For little scripts, I'm not writing unit tests- running it is the test. But I want to be able to iterate without side effects, so it's important that the dry mode be as representative as possible for what'll happen when something is run for real.
You understand how subjective that is right? Someone might expect that the database doesn't do the last commit step while other people is perfectly happy that the database engine checks that it has enough writing permissions and is running as a user that can start the process without problems.
Sure, where you draw the line will vary between projects. As long as its exact placement doesn't matter too much.
For me personally, I tend to draw the line at write operations. So in your example, I'd want a dry run to verify the permissions that it can (if I expect those to be a problem). But if that can't easily be done without a write, then maybe it's not worth it. There are also situations where you want a dry run to be really fast, so you forego some checks (allowing for more surprises later). Really just depends.
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.
Totally agree it shouldn't be for basic tools; but if I'm ever developing a script that performs any kind of logic before reaching out to a DB or vendor API and modifies 100k user records, creating a flag to just verify the sanity of the logic is a necessity.
For most of these local data manipulation type of commands, I'd rather just have them behave dangerously, and rely on filesystems snapshots to rollback when needed. With modern filesystems like zfs or btrfs, you can take a full snapshot every minute and keep it for a while to negate the damage done by almost all of these scripts. They double as a backup solution too.
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.
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 totally agree with both this and the comment you replied to. The common thread is that you can architect the application in such a way that dry vs. wet running can be handled transparently, and in general these are just good designs.
That undo program is called nilfs2, which unfortunately never became popular. I'll simply quote the kernel docs:
> NILFS2 is a log-structured file system (LFS) supporting continuous snapshotting. In addition to versioning capability of the entire file system, users can even restore files mistakenly overwritten or destroyed just a few seconds ago.
Sure, in those cases - but if a command has a chance of nuking prod, you want some extra step in there. Preferably something that can't be muscle-memoried through.
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.
Design patterns are one of those things where you have to go through the full cycle to really use it effectively. It goes through the stages:
no patterns. -> Everything must follow the gang of four's patterns!!!! -> omg I can't read code anymore I'm just looking at factories. No more patterns!!! -> Patterns are useful as a response to very specific contexts.
I remember being religious about strategy patterns on an app I developed once where I kept the db layer separated from the code so that I could do data management as a strategy. Theoretically this would mean that if I ever switched DBs it would be effortless to create a new strategy and swap it out using a config. I could even do tests using in memory structures instead of DBs which made TDD ultra fast.
DB switchover never happened and the effort I put into maintaining the pattern was more than the effort it would have taken me to swap a db out later :,) .
There's some truth to this, since some design patterns can simply be implemented "for good" in a sufficiently powerful language, but I don't find it's true in general. Unfortunately, it has become something of a thought-terminating cliché. Some common design patterns are so flexible that if you really implemented them in full generality as, say, some library function, its interface would be so complex that it likely wouldn't be a net win.
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.
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.
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 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.
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.
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.
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.
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 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.
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 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.
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.
--nuke Delete all of the archives stored. To protect against accidental
data loss, tarsnap will ask you to type the text "No Tomorrow"
when using the --nuke command.
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 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.
reply