[HN Gopher] In praise of -dry-run
___________________________________________________________________
In praise of -dry-run
Author : ingve
Score : 284 points
Date : 2026-01-31 20:42 UTC (1 days ago)
(HTM) web link (henrikwarne.com)
(TXT) w3m dump (henrikwarne.com)
| ElevenLathe wrote:
| 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 wrote:
| Came here to say the same
| eichin wrote:
| 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 :-)
| tkclough wrote:
| I like the timer idea. I do something kinda similar by
| prompting the user to enter some short random code to
| continue.
|
| I guess the goal for both is to give the user a chance to get
| out of autopilot, and avoid up-arrowing and re-executing.
| bikelang wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| this is where design patterns come in handy even tho folks roll
| their eyes at it.
| cake-rusk wrote:
| Design patterns exist to paper over language deficiencies.
| Use a language which is not deficient.
| antinomicus wrote:
| Like what?
| frogulis wrote:
| First class functions and iterators are probably examples
| of what they mean, in terms of language features that
| obsolete (GoF) design patterns
| cake-rusk wrote:
| Here you go:
| https://fsharpforfunandprofit.com/fppatterns/
| WCSTombs wrote:
| 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 cliche. 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.
| cake-rusk wrote:
| > 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
|
| Then I would say you have not arrived at the optimal
| solution. Keep looking.
| awesome_dude wrote:
| Just my two cents - but a general purpose language is going
| to need to be coupled with design patterns in order to be
| useful for different tasks.
|
| I'm using MVC design patterns for some codebases, I'm using
| DDD plus Event sourcing and Event Driven for others.
|
| I suspect that you are thinking of a small subset of design
| patterns (eg. Gang of Four derived patterns like Visitor,
| Strategy, or Iterator )
| cake-rusk wrote:
| > I'm using MVC design patterns for some codebases, I'm
| using DDD plus Event sourcing and Event Driven for
| others.
|
| All examples of OO nonsense. There is only one pattern
| you need (functions) (tongue-in-cheek):
| https://fsharpforfunandprofit.com/fppatterns/
|
| Edit: Also consider using the ELM architecture instead of
| MVC: https://guide.elm-lang.org/architecture/
| awesome_dude wrote:
| I typically reach for Go when I am building things,
| bubbletea, a TUI library uses the Elm architecture and
| enforces it on people using it.
|
| Did not like, Won't use again.
|
| MVC is well understood and works perfectly in my
| experience.
|
| edit:
|
| Even though you were tongue in cheek about F# - this
| stood out on the page
|
| > The functional programming community has design
| patterns and principles as well.
| nstart wrote:
| 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 :,) .
| tbossanova wrote:
| What about the productivity gains from in memory db for
| tests though? Hard to measure I guess
| guhidalg wrote:
| In my experience, there is no in-memory database
| replacement that correctly replicates the behavior of
| your database. You need to use a real database, or at
| least an emulator of one.
|
| For example, I have an app that uses Postgres as the
| database. I have a lot of functions, schemas, triggers,
| constraints in Postgres for modifying database state,
| because the database is 100x faster at this than my
| application will ever be. If I have an in-memory version
| of Postgres, it would need to replicate those Postgres
| features, and at that point I really should just be
| standing up a database and testing against it.
|
| I have worked with people claiming that unit tests need
| to hermetically run in-memory, because reasons. Ok, I
| don't disagree, but if my bug or feature requires testing
| that the database is modified correctly, I need to test
| against a real database! Your in-memory mock will not
| replicate the behavior of a database _ask me how I know_
| ...
|
| These days, Docker makes this so easy that it's just lazy
| to not standup a database container and write tests
| againts it.
| wging wrote:
| 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.
| WCSTombs wrote:
| 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.
| IgorPartola wrote:
| That's what I prefer as well. A generation step and an
| execution step where the executor can be just a logger or
| the real deal.
| clawsyndicate wrote:
| we rely on this separation for our agents. the llm outputs a
| declarative json plan that gets validated against security
| policies before the runtime executes any side effects.
| catching a bad command during validation is way cheaper than
| rolling back a container.
| sh-run wrote:
| 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 wrote:
| 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.
| homebrewer wrote:
| 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.
|
| https://docs.kernel.org/filesystems/nilfs2.html
|
| https://wiki.archlinux.org/title/NILFS2
|
| https://en.wikipedia.org/wiki/NILFS
| aappleby wrote:
| 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.
| arjie wrote:
| No rule is ironclad. I think matching risk to functionality
| is usually a good idea. For example, most modern Linuxen
| carry `rm` protected against root removal with `--no-
| preserve-root`. That is indeed `rm --wet-run` by a different
| name in the dangerous case.
| skissane wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| pffft, if you aren't dropping production databases first thing in
| the morning by accident, how _are_ you going to wake yourself up
| :-)
| cjonas wrote:
| 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 wrote:
| 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 wrote:
| 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.
| IgorPartola wrote:
| I wouldn't want most things to work this way:
| $ rm file.bin $ rm ---commit file.bin $ cat
| foo.txt > bar.txt $ cat foo.txt | tee ---write-for-
| real bar.txt $ cp balm.mp3 pow.mp3 $ cp
| ---i-mean-it balm.mp3 pow.mp3
|
| There is a time and a place for it but it should not be the
| majority of use cases.
| Darfk wrote:
| 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.
| james_marks wrote:
| Yep. First thing I do for this kind thing is make a
| preview=true flag so I don't accidentally run destructive
| actions.
| Joker_vD wrote:
| if [ -n "$DRY_RUN" ] ; then alias rm='echo
| rm' alias cp='echo cp' fi
|
| Of course, output redirects will still overwrite the
| files, since the shell does it and IIRC this behaviour
| can't be changed.
| digiown wrote:
| set -o noclobber
| hdjrudni wrote:
| Even in those basic examples, it probably would be useful.
| `cp` to a blank file? No problem. `cp` over an existing
| file? Yeah, I want to be warned.
|
| `rm` a single file? Fine. `rm /`? Maybe block that one.
| Izkata wrote:
| That last one would error without doing anything anyway
| because it's not recursive.
| digiown wrote:
| 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.
| ronjakoi wrote:
| I used to have alias rm='rm -i' for a few years to be
| careful, but I took it out once I realised that I had just
| begun adding -f all the time
| wonger_ wrote:
| See also rm -I (capital i), which only prompts when
| deleting directories or >3 files
| Mawr wrote:
| Yeah, but that's because it's implemented poorly. It
| literally asks you to confirm deletion of each file
| individually, even for thousands of files.
|
| What it should do is generate a user-friendly overview of
| what's to be deleted, by grouping files together by some
| criteria, e.g. by directory, so you'd only need to
| confirm a few times regardless of how many files you want
| to delete.
| Mawr wrote:
| Uhuh: $ rm -rf / rm: it is
| dangerous to operate recursively on '/' rm: use
| --no-preserve-root to override this failsafe
| __turbobrew__ wrote:
| --dry-run should default to true
| fainpul wrote:
| In PowerShell there's a setting for this:
|
| https://learn.microsoft.com/en-
| us/powershell/module/microsof...
| xyse53 wrote:
| Yeah I'm more of a `--wet-run` `-w` fan myself. But it does
| depend on how serious/annoying the opposite is.
| aqme28 wrote:
| I've done that, but I hate the term "wet run."
|
| I use "live run" now, which I think gets the point across
| without being sort of uncomfortable.
| IgorPartola wrote:
| --with-danger
|
| --make-it-so
|
| --do-the-thing
|
| --go-nuts
|
| --safety-off
|
| So many fun options.
| Darfk wrote:
| I'm a fan of --safety-off. It gives off a 'aim away from
| face' or 'mishandle me and I'll blow a chunk out of your
| DB' vibe.
| JsonCameron wrote:
| I've done a few --execute --i-know-what-im-doing for some
| more dangerous scripts
| altairprime wrote:
| May I recommend --I-take-responsibility-for-the-outcome-
| of-proceeding and require a capital I?
| altairprime wrote:
| --commit is solid too
| torstenvl wrote:
| It's in the UI not the command line, but I like
| Chromium's thisisunsafe
| tetha wrote:
| I find it important to include system information in here
| as well, so just copy-pasting an invocation from system A
| to system B does not run.
|
| For example, our database restore script has a parameter
| `--yes-delete-all-data-in` and it needs to be
| parametrized with the PostgreSQL cluster name. So a
| command with `--yes-delete-all-data-in=pg-accounting`
| works on exactly one system and not on other systems.
| Quekid5 wrote:
| Moist run is the way.
| bregma wrote:
| --moisten
| lazide wrote:
| Just don't randomly mix and match the approaches or you are in
| for a bad time.
| spike021 wrote:
| 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.
| esafak wrote:
| Github will do that when you delete a repo.
| torstenvl wrote:
| 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.
| sigwinch wrote:
| rmlint will only run a previous dry-run snapshot.
| inglor_cz wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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.
| xigoi wrote:
| If you're using an iPad, the implicit assumption is that
| Apple knows better than you what you want to do.
| calvinmorrison wrote:
| --dry-run
|
| --really
|
| --really-really
|
| --yolo
| homebrewer wrote:
| You'll like fontconfig then, which has both --force and
| --really-force
|
| https://man.archlinux.org/man/fc-cache.1.en
| aappleby wrote:
| What if the tool required an "un-safeword" to do destructive
| things?
|
| "Do you really want to 'rm -rf /'? Type 'fiberglass' to proceed."
| nthdeui wrote:
| Like tarsnap's --nuke command: --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.
| drougge wrote:
| Probably inspired by Vinum's "NO FUTURE" for destructive
| operations. (Vinum was a raid system used on older versions
| of FreeBSD.)
| jabroni_salad wrote:
| 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.
| TZubiri wrote:
| 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.
| throwaway290 wrote:
| How do you easily diff what changed between Docker and host?
| TZubiri wrote:
| The way OverlayFS works is that there's a base directory. And
| then there's an overlay directory that only contains the
| changes. Docker is based on OverlayFS.
|
| There's two main ways overlays are used, first at build time,
| each line/command generates a new overlay based on the
| previous base, so when you do something like
|
| FROM debian RUN apt-get update
|
| it creates a base from the debian image , and then creates an
| overlay that only contains the changes introduced by apt-get
| update.
|
| If you use docker inspect or docker show on the image you get
| a json showing exactly where the overlay directories are, you
| just need to navigate the overlay directory.
|
| Second: on runtime. [Assuming you are not using volumes, (and
| if you use volumes, just make sure the volume starts out as
| empty, instead of sharing your host files)] OverlayFS is used
| for the runtime file changes as well, the last image is used
| as a base, and every files changed during runtime are added
| to the runtime overlay. That filesystem won't be deleted, if
| you only stop the docker container, the runtime files will
| still be present, and you can reach them by docker inspecting
| the running docker processes/instances, and then navigating
| the overlay fs as you would any directory.
|
| You can also just use overlayfs, as far as I recall, you just
| use mount and unmount while specifying the OverlayFS driver
| and special parameters like base and overlay. Conjugating a
| chain of overlays is a bit more complex, but it's the same
| interface.
| gooseyman wrote:
| https://news.ycombinator.com/item?id=27263136
|
| Related
| tegiddrone wrote:
| I'm interested to know the etymology and history of the term.
| Somehow I imagine an inked printing press as the "wet run."
| hydrox24 wrote:
| 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
| jofzar wrote:
| 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 wrote:
| 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 wrote:
| 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.
| tonymet wrote:
| Powershell cmdlet macros (and the resultant parameter
| consistency with flags and verbs) are a great example that
| should make its way over to bash/zsh scripts. Given we are
| rewriting so many Unix utilities, it would be a great time to
| learn from a positive example.
| alexhans wrote:
| 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 wrote:
| 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 wrote:
| And it's more important than ever in the age of coding agents.
| sixtram wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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.
| xavdid wrote:
| Yes, but it depends on the context.
|
| 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.
| AmbroseBierce wrote:
| 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.
| xavdid wrote:
| 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.
| lanyard-textile wrote:
| I'd argue the dry run is a form of integration testing:
| Essentially the writes are mocked, but the reads are still
| functional.
| friendzis wrote:
| > 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.
|
| Not really. Testing is a way to increase confidence that code
| does what it is specified to do, because it is cheaper than
| full-blown formal analysis :)
|
| The problem raised by OP here is granularity. Operation like
| `update(record, field, value)` is itself a tree of smaller
| sub-operations that may do some permissions checking,
| locking, network calls, even checking for presence of record
| if it has upsert semantics, all of which could fail. A dry
| run with a plan that is too coarse can succeed while the
| actual operation fails over things left unchecked.
| madeofpalk wrote:
| > ASAIK the purpose of a dry-run is to understand what will
| happen
|
| Right - so the dry-run has to actually do as much of 'what
| will happen' as possible, except the actual things.
|
| You want to put the check as far down, close to the 'action'
| as possible. You don't want any additional business logic
| gated by the dry run check.
| ziml77 wrote:
| This is why I end up creating an interface for the calls that
| perform modifications. Then I have one implementation that logs
| and one implementation that does the actual work. I end up with
| the output being as representative as possible as to what would
| happen. I also feel a lot more comfortable that a dry run truly
| won't write anything when the only class that could ever
| actually write anything is not even instantiated in dry run
| mode. I don't get that same comfort when there's a ton of
| branches sprinkled throughout checking for a dry run flag.
| giorgioz wrote:
| I didn't know about --dry-run until last summer Claude Code added
| it to a script it had created.
| bjt12345 wrote:
| 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 wrote:
| 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 wrote:
| 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.
| muvlon wrote:
| No need to overthink it. In any semi-modern language you can
| (de)serialize anything to and from JSON, so it's really not
| that hard. The only thing you need to do is have a
| representation for the plan in your program. Which I will
| argue is probably the least error-prone way to implement
| --dry-run anyway (as opposed to sprinkling branches
| everywhere).
| Jolter wrote:
| Right, but you still have to define every "verb" your plan
| will have, their "arguments", etc. Not need to write a
| parser (even Java can serialize/deserialize stuff), as you
| say, but you have to meta-engineer the tool. Not just
| script a series of commands.
| friendzis wrote:
| > you can (de)serialize anything to and from JSON, so it's
| really not that hard
|
| First, it is hard, especially in at least somewhat portable
| manner.
|
| Second, serialization only matters if you cannot (storage,
| IPC) pass data around in-memory anyway. That's not the
| problem raised, though. Whatever the backing
| implementation, the plan, ultimately, consists of some
| instructions (verbs in parent) over objects (arguments in
| parent). Serializing instructions any other way than
| dropping non-portable named references requires one to
| define execution language, which is not an easy feat.
|
| > The only thing you need to do is have a representation
| for the plan in your program.
|
| That "only" is doing lifting heavier than you probably
| realize. Such representation, which is by the way specified
| to be executable bidirectionally (roll back capabilities),
| is a full blown program, so you end up implementing
| language spec, godegen and execution engines. In cases of
| relatively simple business models that is going to be the
| majority of the engineering effort.
| michaelmior wrote:
| > First, it is hard, especially in at least somewhat
| portable manner.
|
| I'm curious what portability concerns you've run into
| with JSON serialization. Unless you need to deal with
| binary data for some reason, I don't immediately see an
| issue.
|
| > Such representation, which is by the way specified to
| be executable bidirectionally (roll back capabilities),
| is a full blown program
|
| Of course this depends on the complexity of your problem,
| but I'd imagine this could be as simple as a few
| configuration flags for some problems. You have a
| function to execute the process that takes the
| configuration and a function to roll back that takes the
| same configuration. This does tie the representation very
| closely to the program itself so it doesn't work if you
| want to be able to change the program and have previously
| generated "plans" continue to work.
| d7w wrote:
| It's not strictly related to the original theme, but I want
| to mention this.
|
| Ansible implementation is okay, but not perfect (plus, this
| is difficult to implement properly). For cases like file
| changes, it works, but if you install a package and rely on
| it later, the --check command will fail. So I am finding
| myself adding conditions like "is this a --check run?"
|
| Ansible is treated as an idempotent tool, which it's not. If
| I delete a package from the list, then it will pollute the
| system until I create a set of "tearing-down" jobs.
|
| Probably, Nix is a better alternative.
| GeneralMaximus wrote:
| 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 wrote:
| And just like that, you find yourself implementing a compiler
| (specs to plan) and a virtual machine (plan to actions)!
| lelanthran wrote:
| > And just like that, you find yourself implementing a
| compiler (specs to plan) and a virtual machine (plan to
| actions)!
|
| Not just _any_ compiler, but a non-typesafe, ad-hoc,
| informally specified grammar with a bunch of unspecified or
| under-specified behaviour.
|
| Not sure if we can call this a win :-)
| nvader wrote:
| Greenspun's tenth rule in action!
| bbkane wrote:
| I think you're already doing that? The only thing that's
| added is serializing the plan to a file and then
| deserializing it to make the changes.
| majormajor wrote:
| Yeah any time you're translating "user args" and "system
| state" to actions + execution and supporting a "dry run"
| preview it seems like you only really have two options: the
| "ad-hoc quick and dirty informal implementation", or the
| "let's actually separate the planning and assumption
| checking and state checking from the execution" design.
| jrockway wrote:
| This is why I think things like devops benefit from the
| traditional computer science education. Once you see the
| pattern, whatever project you were assigned looks like
| something you've done before. And your users will appreciate
| the care and attention.
| schindlabua wrote:
| I was thinking that he's describing implementing an initial
| algebra for a functor ([?]AST) and an F-Algebra for
| evaluation. But I guess those are different words for the
| same things.
| HackerThemAll wrote:
| > 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.
|
| And how do you imagine doing that for the "rm" command?
| jasode wrote:
| _> 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._
|
| Not to take anything away from your comment but just to add a
| related story... the previous big AWS outage had an unforeseen
| race condition between their DNS planner vs DNS executor:
|
| _> [...] Right before this event started, one DNS Enactor
| experienced unusually high delays needing to retry its update
| on several of the DNS endpoints. As it was slowly working
| through the endpoints, several other things were also
| happening. First, the DNS Planner continued to run and produced
| many newer generations of plans. Second, one of the other DNS
| Enactors then began applying one of the newer plans and rapidly
| progressed through all of the endpoints. The timing of these
| events triggered the latent race condition. When the second
| Enactor (applying the newest plan) completed its endpoint
| updates, it then invoked the plan clean-up process, which
| identifies plans that are significantly older than the one it
| just applied and deletes them. At the same time that this
| clean-up process was invoked, the first Enactor (which had been
| unusually delayed) applied its much older plan to the regional
| DDB endpoint, overwriting the newer plan. The check that was
| made at the start of the plan application process, which
| ensures that the plan is newer than the previously applied
| plan, was stale by this time due to the unusually high delays
| in Enactor processing. [...]_
|
| previous HN thread:
| https://news.ycombinator.com/item?id=45677139
| IanCal wrote:
| Overkill I'm sure for many things but I'm curious as to
| whether there's a TLA kind of solution for this sort of
| thing. It feels like it could although it depends how well
| modelled things are (also aware this is a 30s thought and
| lots of better qualified people work on this full time).
| thundergolfer wrote:
| Totally agree, and this is covered in an (identically named?)
| Google Research blog [1].
|
| Just last week I was writing a demo-focused Python file called
| `safetykit.py`, which has its first demo as this:
| def praise_dryrun(dryrun: bool = True) -> None: ...
|
| The snippet which demonstrates the plan-then-execute pattern I
| have is this: def gather(paths):
| files = [] for pattern in paths:
| files.extend(glob.glob(pattern)) return files
| def execute(files): for f in files:
| os.remove(f) files = gather([os.path.join(tmp_dir,
| "*.txt")]) if dryrun: print(f"Would remove:
| {files}") else: execute(files)
|
| I introduced dry-run at my company and I've been happy to see
| it spread throughout the codebase, because it's a coding
| practice that more than pays for itself.
|
| [1] https://www.gresearch.com/news/in-praise-of-dry-run/
| wrxd wrote:
| G-Research is a trading firm, not Google research
| thundergolfer wrote:
| The G stands for "Google", does it not?
| Bootvis wrote:
| There is no relation.
| scott_w wrote:
| I had a similar (but not as good) thought which was to separate
| out the action from the planning in code then inject the action
| system. So ---dry-run would pass the ConsoleOutput() action
| interface but without it passes a LiveExecutor() (I'm sure
| there's a better name).
|
| Assuming our system is complex enough. I guess it sits between
| if dry_run and execute(plan()) in its complexity.
| richstokes wrote:
| I think it's configurable, but my experience with terraform is
| that by default when you `terraform apply` it refreshes state,
| which seems to be tantamount to running a new plan. i.e. its
| not simply executing whats in the plan, its effectively running
| a fresh plan and using that. The plan is more like a preview.
| GauntletWizard wrote:
| That is the default, but the correct (and poorly documented
| and supported) way to use terraform is to save the plan and
| re-use it when you apply. See the -out parameter to terraform
| plan, and then never apply again without it.
| rook_line_sinkr wrote:
| 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
| spwa4 wrote:
| 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.
| joombaga wrote:
| Consider using work trees. They have separate reflogs, you'd
| still get separate directories, but less duplication.
| d7w wrote:
| 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 wrote:
| These kind of obstacles don't work for any action that the user
| does repeatedly.
|
| Dialogs that pop up and ask "Are you sure you want to delete
| ...?" -> users just automatically click yes, because they
| already did that the last 10 times and just want to get on with
| their work.
|
| Logged in to server "alpha" instead of "delta" because you
| thought that's the right one. Tool asks you to write the server
| name. You type "alpha" because you know you're on alpha.
| Reboots the wrong server.
|
| Github ask you to confirm the repo name before deleting by
| typing it into a text field. User looks at what the repo name
| is and types it without thinking. Or, like lazy me, mark and
| drag the displayed name into the field, so you don't even have
| to type.
|
| The point is, users already decided to do the action when they
| started. It's nearly impossible to consistently make them stop
| and re-evaluate their decision, because that's extremely high
| friction and annoying. They quickly learn to circumvent the
| friction as efficiently as possible (i.e. without thinking
| about it).
|
| A better solution is to just do it, but let the user undo it if
| it was a mistake (not always possible of course).
| d7w wrote:
| > Or, like lazy me, mark and drag the displayed name into the
| field
|
| Yes, the same, unfortunately. This rail guard works for only
| a couple of days or weeks when the perception is fresh. After
| that, it's always "select, copy, paste, confirm" without
| thinking.
|
| I believe the better approach is to practice "Chaos Monkeys"
| and improve the infrastructure to survive even if someone
| makes a mistake.
| Mawr wrote:
| > Github ask you to confirm the repo name before deleting by
| typing it into a text field. User looks at what the repo name
| is and types it without thinking. Or, like lazy me, mark and
| drag the displayed name into the field, so you don't even
| have to type.
|
| Pretty easy to solve this sort of stuff with timers: "Sure,
| the repo will be deleted in <timer>. Press here to abort:
| <button>."
|
| Or have the timers on the backend and just show the user an
| undo button.
| Arbortheus wrote:
| 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".
| jaynate wrote:
| > The downside is that the dryRun-flag pollutes the code a bit.
| In all the major phases, I need to check if the flag is set, and
| only print the action that will be taken, but not actually doing
| it.
|
| Sounds like a case for the state machine pattern
| dabedee wrote:
| From the article: "I added -dry-run on a whim early on in the
| project. I was surprised at how useful I found it to be."
|
| Not to be overly critical (I think it's great OP found value in
| adding and using --dry-run), but I am willing to bet that this
| was a suggestion/addition from a coding agent (and most likely
| Claude code/Opus). Having used it myself to build various CLI
| tools in different languages, it almost always creates that
| option when iterating on CLIs. To the point where it's almost a
| tell. I wonder if we're entering a moment of convergence where
| all the tools will have similar patterns/options because they are
| similarly written by agents.
| SPBS wrote:
| > Early in the development process, when testing the incomplete
| application, I remembered that Subversion (the version control
| system after CVS, before Git) had a -dry-run option.
|
| > I remembered how helpful that was, so I decided to add it to
| my command as well.
|
| He mentions the reason he added it, and it's a compelling
| enough story to be true.
| dabedee wrote:
| Of course and I am not trying to point fingers. But I do
| think it's interesting because it's also possible that it is
| confabulation. Not lying, but genuinely constructing coherent
| explanations for decisions whose true origins are different
| than we recall. I think working with coding agents has
| already made this immensely more common.
| mpyne wrote:
| I had the equivalent of --dry-run in my kdecvs-build script
| from 2003 (where it was called --pretend) so it's not
| _that_ spontaneous an idea that it must have been dreamed
| up by an AI.
|
| Any time you have a script that needs to run for a long
| time or might involve destructive actions, having a way to
| run the script in a "tell me what you would do without
| actually doing it" mode is a fairly obvious user story to
| throw in.
| dabedee wrote:
| Again, it's completely possible that OP and you are the
| wonderful exceptions (untouched and uninspired by coding
| agents) that have been using these patterns for as long
| as you can remember. My comment revolved around the
| psychological phenomenon, not whether dry-run is a
| clever/novel idea. It's about how we might tell ourselves
| stories about the origin of our ideas when working with
| those tools.
| nickjj wrote:
| One thing that I didn't see mentioned is making your own dry run
| for tools that don't have it built in.
|
| It doesn't always work but sometimes I use `diff` to help with
| that. For example, if you have a complicated `sed` replacement
| that you plan to run on a file, you can use diff like this `diff
| -u <(echo "hello") <(echo "hello" | sed "s/hello/hi/g")` to help
| show what has changed.
|
| I've written about the value of dry run too at:
| https://nickjanetakis.com/blog/cli-tools-that-support-previe...
| whalesalad wrote:
| The fact that every single `--` has become -- drives me bananas.
| For a technical blog this oversight is so sloppy.
| terrabitz wrote:
| A while ago when I was working with PowerShell a lot, I got
| spoiled by the easy inclusion of `-DryRun` flags in all my
| scripts.
|
| Nowadays I still use the technique for a lot of the tools I make.
| I typically do a Terraform-like approach: create a plan, validate
| the plan, render the plan to the user, exit early if we're in
| dry-run mode, and apply the plan as the final step. Whether dry-
| run is enabled by default depends on the risk of the operations
| and who will be using the tool.
|
| This makes it exceedingly clear what actions the tool will do.
| Plus it has the added benefit of being able to split the plan and
| apply steps into two separate steps. For example, you can create
| a plan, serialize it to JSON, store it (e.g. in VCS, Jira ticket,
| whatever) then apply it during a change window.
| plan = createPlan() print(plan.string()) if (dryRun){
| return } plan.apply()
| amelius wrote:
| In even more praise of Ctrl+Z.
|
| (Don't try it in a terminal window, though)
| mfonda wrote:
| If the changes your command makes are strictly to a relational
| database, then `--dry-run` becomes quite easy to implement: just
| start a transaction and never commit it. This avoids polluting
| the entire command with `if dryRun` checks everywhere. I've found
| this to be a great approach.
___________________________________________________________________
(page generated 2026-02-01 23:01 UTC)