[HN Gopher] In praise of --dry-run ___________________________________________________________________ In praise of --dry-run Author : Smaug123 Score : 295 points Date : 2021-05-24 11:26 UTC (11 hours ago) (HTM) web link (www.gresearch.co.uk) (TXT) w3m dump (www.gresearch.co.uk) | blt wrote: | Abstracting a bit, this is an application of the principle | "represent important intermediate results as explicit data | structures". It shows up in many places: testing, extensibility, | logging, and parallelization are all easier to implement when the | result is represented explicitly instead of implicitly in the | program's call stack. | | Has anyone written about this idea? It seems to have some | interesting interaction with laziness. | nerpderp82 wrote: | Even better is to invert the flag, that default is to dump the | plan and have to pass an `--actually-do-it` to have it make the | changes. | nicoburns wrote: | For particularly dangerous operations, even better is to make dry | run the default, and force users to explicitly opt-in to actually | running it. | ben509 wrote: | I've done that and called the counter option --for-real, but | I'd like both args so runbooks can spell out: | 1. do-step-one --dry-run 2. (do validation) 3. | do-step-one --real-run 4. (do validation) | | Many commandline parsing libraries assume you're doing | booleans, and I think --dry-run and --no-dry-run is confusing. | And, internally, you have a boolean flag so there's always the | possibility of some code getting it backwards. | | Internally, I'd like an enum flag that's clearly dry_run or | real_run, so the guards are using positive logic: | switch(run) { case dry_run: print("Would | do this..."); case real_run: | do_real_thing(); } | Smaug123 wrote: | _coughs gently_ I use `DryRunMode.Dry` and `DryRunMode.Wet` | in the OP. | kelnos wrote: | I think the point here, though, is that the user needs to | be explicit on the command line: they have to specify one | of `--wet` or `--dry`; specifying neither is an error. It's | not clear from your code if you do that, or if you | interpret `--dry-run` as `DryRunMode.Dry` and the absence | of an option as `DryRunMode.Wet`. | | While I kinda like this idea in principle, I haven't really | seen any CLI apps that require an explicit option for both | modes, so it might be a bit of unexpected UX for people. | llimllib wrote: | I've written a couple tools that default to dry run and require | `--yes-actually-delete` and similar, but I can't think of any | publicly available tools that follow this pattern - anybody | have suggestions? | Phlogistique wrote: | https://github.com/dmerejkowsky/ruplacer | | Uses `--go` to confirm. | jerf wrote: | rm has "--no-preserve-root", which isn't quite "--no- | seriously-delete-my-root-directory" but is at least heading | in that direction. | llimllib wrote: | if you squint, you can kind of see file permissions as | implementing this pattern? rm /usr fails unless you sudo rm | /usr | gbrown_ wrote: | Obligatory Cantrill reference | https://youtu.be/wTVfAMRj-7E?t=5046. The whole series of | these are well worth a listen. | TFortunato wrote: | Not quite the same, but apt commands like apt-get require | user interaction to confirm what you are about to do, unless | you explicitly add a "-y" / "--yes" to the command. | stevekemp wrote: | apt will sometimes prompt you to confirm really dangerous | options too: sudo apt-get purge login | .. WARNING: The following essential packages | will be removed. This should NOT be done unless | you know exactly what you are doing! login | 0 upgraded, 0 newly installed, 1 to remove and 303 not | upgraded. After this operation, 1,212 kB disk | space will be freed. You are about to do | something potentially harmful. To continue type | in the phrase 'Yes, do as I say!' ?] | OskarS wrote: | "git clean" sort-of works like this. It errors out unless you | explicitly specify if it's a real run or dry run. | nicoburns wrote: | `git push` in cases of conflict too. | btdmaster wrote: | sed does not operate on files directly unless turned | (-i)nteractive. | adrianstoll wrote: | I only use sed on version controlled files and immediately | run git diff afterwards to check the results. | llimllib wrote: | oh good one, prettier does the same thing - output to | stdout unless --write is set | okl wrote: | -i is short for --in-place | gbrown_ wrote: | Minor quibble the -i option mnemonic is "in-place" rather | than "interactive". | Smaug123 wrote: | Also not quite the same, but `git clean` bails out extremely | early unless you supply one of -i, -f, or -n (the latter of | which is `--dry-run`). | Izkata wrote: | Not exactly the same, but along the same lines: mysql has an | --i-am-a-dummy flag so that DELETE queries won't run without | a WHERE. | tshaddox wrote: | React has a deliberately inconvenient API for injecting | unsanitized HTML markup straight into the DOM: | <div dangerouslySetInnerHTML={{ __html: "Some raw HTML" | }}></div> | | https://reactjs.org/docs/dom- | elements.html#dangerouslysetinn... | matja wrote: | `hdparm --yes-i-know-what-i-am-doing --please-destroy-my- | drive --fwdownload` | | https://github.com/Distrotech/hdparm/blob/4517550db29a91420f. | .. | wgarvin wrote: | Perforce's command line app uses this pattern for one of its | most dangerous commands: p4 obliterate //depot/path/... which | prints out exactly which files will be obliterated from the | server (which not only removes their entire history, but can | corrupt other files too if you don't p4 snap them first). | | To actually carry out the obliterate action, you have to add | the -y option. | kirkules wrote: | Definitely true. In this case, it's important to vet | guides/examples to make sure you minimize copypastable examples | that have the --no_dry_run flag | globular-toast wrote: | I don't like this because it trains users to assume that | commands can never do dangerous stuff by default. | steego wrote: | It's a good opportunity to use long options like: | | --i-acknowledge-this-is-dangerous-and-i-gave-this-more-than- | cursory-thought | jakebasile wrote: | Yep, this is what I do now. The dry run is the default, you | have to flag `--really` to do anything permanent. | xupybd wrote: | Gresearch puts out some interesting articles, and they work in | F#. I wish I were in the UK, I'd love to try and get a job there. | dkarp wrote: | They're an interesting organization. | | See this past submission: | https://news.ycombinator.com/item?id=18499712 | Smaug123 wrote: | I believe (though can't swear to it) that we do provide | immigration/relocation assistance and money, at least for quant | researchers and possibly for all employees. If that would be | enough to get you interested, feel free to drop me a line at | patrick.stevens@gresearch.co.uk and I can find out actual | details rather than half-remembered possibly-false snippets | from years ago. | yuchi wrote: | Fantastic article, and I promote a stricter approach: `--dry-run` | is the default, and you need to disable with `--no-dry`. | | For irreversible (such as deletions) actions I even go one step | further with a `--no-dry=<some-non-trivial-value>` in order to | force some thinking. | | Edit: apparently this is way more common than I thought, by | reading comments here! | kevincox wrote: | Their example of pushing it into the type system is nice but | requires a language with a fairly powerful type system. Once | simple way to do this in basically any statically typed language | is just requiring a "Run Token" for any bit of code that does | something "for real". This method works well if you want the | output to be similar or identical for dry run and actual run. | struct RunToken; fn do_the_thing(_: RunToken, ...); | | The general approach works for just about any statically typed | languages (although null is the enemy). | | You can make it a bit more tedious to generate that RunToken so | that a `do_the_think(RunToken, ...)` looks more natural. But the | idea is simply that if the only place you create a RunToken is | when parsing the --no-dry-run flag you can't accidentally | do_the_thing when you move the code out of the `if is_dry_run` | block without noticing. | | Of course this is a simple way to get fairly reliable dry run. I | do agree that having seperate plan and apply steps is approach is | the best when you can do it. | wheybags wrote: | I've made a further extension of this principle for a video game | release script, by adding a --unsafe flag. The script was | responsible for building the game on a bunch of platforms, | uploading it to various storefronts and announcing the release on | twitter, reddit etc. There was a --dry-run flag too, that just | prints what it would do, but sometimes you want to test part of | the script without doing anything public. There were various | flags like --no-upload and --no-twitter etc, but it was easy to | forget them, so I added in a top level --unsafe flag. At every | point where we do something public, at the deepest level of the | call stack just before doing it, I check for unsafe, and if its | not present, throw an exception. It worked pretty well, and | definitely saved my ass a few times, especially during | refactoring. | agumonkey wrote: | I wonder how it would feel to have every program dry-runnable but | composable.. basically FP/virtual systems. You can (dry-run | cmd-0) | (dry-run ...) | (dry-run cmd-n) and it will yield a | potential system state. | notafraudster wrote: | How do you all write dry-run logic for utilities that perform | file operations (edits, deletions, creations) and have some kind | of sequential dependency in operations such that later steps | depend on the previous steps having been executed? | | I have found it a frustrating nut to crack because it seems like | it needs to involve either writing a simulation layer that tracks | the state of all the file calls virtually, or else copy all the | files to a temp folder (so the dry run isn't dry, it's just on a | separate version of the data). Both of these seem like bad | solutions. | Eridrus wrote: | Besides having dry-run support, I think this gets to the | question of what the level of abstraction should be, and I | think that is clearly application-specific. | | Maybe in your case the level you are trying to report dry-run | information at is too granular. | | Or maybe you have too tight coupling between your code that | determines what needs to be changed and that which actually | does the change and you might want to refactor the code that | determines changes to the code that makes the changes. | gameman144 wrote: | I don't see anything wrong with the "copy to a temporary | directory" approach if your function actually does operate on | files within a directory. In that scenario, copying to a | different directory is actually probably _exactly_ what you | want for a dry-run, since then the code that you 're executing | is the same exact code that would run were you to execute in | non-dry-run mode (as opposed to virtual operations which are | prone to bugs if that virtualization layer and your real | operations ever fall out of sync). | ItsMonkk wrote: | I don't know of any system that works like this currently, but | it seems like you are asking if you can switch branches, do the | work, then your "dry-run" is a pull request complete with all | files changes which you can approve manually. | programmarchy wrote: | Copying to a temp folder sounds pretty reasonable to me. | lamontcg wrote: | Yes, if you call a black box outside of your code base which | does something then subsequent operations can't easily be dry- | run'd. | | So if you need to install a package which sets up a systemd | service, the subsequent dry-run of managing that systemd | service would fail because the service doesn't exist yet. Or | you need to make assumptions in the service dry run that it | should have been set up before. | | You can just assume that the service would have been setup and | report that you were, say, enabling and starting the service. | Or you could require some kind of hint to be setup from the | package installation to the specific service configuration (for | well known things this could be done by default, but for | arbitrary user-supplied packages this cannot be done). Or you | could list the entire package contents and try to determine if | the manifest looks like it is configuring a service. | | And it still may fail because you don't know the contents of | that file without extracting it and the service may not parse | at all. | | And that's just a simple package-service interaction example. | You could spend a week noodling on how to do that fairly | precisely, then there's a hundred or a thousand other | interactions to get correct. | | You're being told not to do the thing, so there's fundamentally | a black-box there, and you need to figure out how much you're | going to cheat, how much you're going to try to crack open the | black box into some kind of sandbox so you can figure out its | internals, and how much you're going to offload onto the user. | Not actually an easy problem at all. | swiley wrote: | Yeah, the ability to indicate a shell command is pure (or | that it must be pure) is something that's really missing in | POSIX-like APIs. It's something I've certainly missed. | Something like fork that disables write outside a handful of | file descriptors (like one the parent starts with popen) | would be pretty awesome. Maybe BSD jails do that. | lamontcg wrote: | In general an composable dry run API with the ability to | make promises would be good. Then its on the lower level | black box to have been tested correctly and make accurate | promises. | | In practice though what you'll find is that its easier to | treat whole systems as black boxes, and test changes in | throwaway virt systems, then test them in development | environments, then roll them out to prod (or that whole | immutable infrastructure thing and throw it all away and | replace so you don't get bitten by dev-prod discrepancies | in theory). | zbentley wrote: | Even better than a dry-run flag is to have the noop mode be the | default behavior and have e.g. "--execute" enable non-read-only | behavior (or, for really scary commands like programmatic data | destroyers, "--nuke-my-data-i-solemnly-swear-i-know-what-i-am- | doing"). Prevents plenty of accidental mistakes by sleepy | operators. | | That's a quibble though, the article is well-put and quite | correct about the value of a dry-run mode. | zbentley wrote: | Oops, I read the article but not the comments. I see I'm far | from the first person to suggest this, apologies for the | duplication. | aranchelk wrote: | Dry-run can be implemented succinctly in Python with a function | decorator wrapping those functions that should run conditionally. | | Nice aspects of this technique: | | Inside the decorator you have access to both the function's name | and its arguments so you can print descriptive messages to the | user e.g. "In dry run, would have run function 'delete' on | '/foo/bar'" | | You can use the name of the decorator as de facto documentation, | e.g. "@destructive" | amirkdv wrote: | I'm adamantly pro dry-run and like OP I've found that you really | need to design for it. From my experience, OP's two main ideas | are spot on: librarification and pushing dry-run logic deeper | into library code. | | Here are two other things I've found: | | 1. Regardless of where you push your run/dry-run dispatch to, the | underlying "do it" logic really needs to be factored properly for | individual side effects (think functional). Otherwise, you | inevitably end up with loads of "if dry-run / else" soup or | worse, bugs _caused_ by your dry-run support. | | 2. You still need safety nets for when someone is doing dangerous | operations without dry-run. Pointing and calling [0] is a great | trick for this. For example, say your tool is about to delete X, | Y, and Z. Instead of a simple "Yes" confirmation which the user | would quickly end up doing on auto-pilot, you could have a more | involved confirmation like "Enter the number of resources listed | above to proceed" and then only proceed to delete if the user | enters "3". | | Very curious to hear about design idioms folks have come up with! | | [0] https://en.wikipedia.org/wiki/Pointing_and_calling | lytefm wrote: | > 2. You still need safety nets for when someone is doing | dangerous operations without dry-run. | | Another important property is idempotency. Especially if the | script involves network requests or moving files around, you'll | want to reach the goal by re-running in case something breaks | half-way. | IgorPartola wrote: | What I like is a system where you first create a plan for what | you will do, then there is an executor that will execute the | plan. So --dry-run just doesn't run the executor. Of course, | depending on what you are working with that might not be | possible, but if you can design it like so, do it. It also | makes everything nicely decoupled. | renewiltord wrote: | I'm sure there have been older tools with this, but Terraform | is the first I encountered with it and I really like this | model. | ficklepickle wrote: | rsync is where I first encountered it. I've made it a habit | to always do a dry run first as a sanity check and it has | saved me some headaches. | renewiltord wrote: | rsync has a plan mode? I had no idea. | cortesoft wrote: | Yeah, not sure how this would work with any workflow that | caLLs out to another service and uses the result of that to | determine the next step, if that intermediate call changes | state somewhere. | dundarious wrote: | Then you can only really dry run that first phase anyway. | minitoar wrote: | Indeed, the other service then needs to support dry runs. | bcbrown wrote: | Are you worried about that intermediate call changing | between the dry run and the actual run? That can be avoided | by saving the execution plan and allowing the actual run to | load the execution plan from the dry run. | ItsMonkk wrote: | I think a large part of the next 10 years is us figuring | out that frameworks are an anti-pattern and that everything | is going to have to move to libraries that abide by CQRS. | klodolph wrote: | Yes, I like this a lot. You often end up with a main code | path that looks like this: do a bunch of | stuff make a plan if dryRun { | plan.Print() exit() } | plan.Execute() | toxik wrote: | Lines 1 & 3 had side effects, sad panda | toxik wrote: | I guess you really do have to preface your jokes with | joke disclaimers these days. | klodolph wrote: | It's impossible to tell if someone is being sarcastic on | the internet, because no matter how stupid your comment | is at face value, there's always a percentage of people | on the forum stupid enough to say it in earnest. | | https://en.wikipedia.org/wiki/Poe%27s_law | | Use a winking smiley face emoticon ;-) or /s | klodolph wrote: | Reading from disk, spending CPU cycles, allocating | memory, and making queries on the network are all | technically side effects--but they're not really cause | for "sad panda". | | Everything on a computer has _some_ side effects, and the | purpose of --dry-run is to stop execution without certain | effects that we care about. It 's impossible to eliminate | side effects entirely, so this is not a goal. | numpad0 wrote: | I had http:// 192.168.100.123 :7654/lights_on.sh that | idempotentially turns on lights in my room. Trying to | query or even pre-fetching does it. I was aware it's not | wisest, wasn't as stupid as to expose it, also it broke | so it is no more, but there's always the guy who does | that. | klodolph wrote: | The "n" key on your keyboard is connected to some | dynamite attached under the desk, if you type "--dry-run" | or even just "-n", it plays the song "Those Endearing | Young Charms" and then explodes, killing everyone in the | room. | | https://www.youtube.com/watch?v=ZLNduq2pnN0 | | At some point it doesn't matter if there's "the guy who | does that", because the right choice is to let "the guy | who does that" deal with the consequences of their own | crazy setup. | GuB-42 wrote: | Dry run is among the things you really need to design for, | others are: | | - cancel | | - undo/redo | | - progress bars | | Progress bars with accurate timing are notoriously difficult, | or even impossible to get right. But even regardless of timing, | having a progress bars that really shows progress and doesn't | freeze is hard. Every slow operation has to have some sort of | callback mechanism to update progress, and you have to know the | number of steps in advance. | | Undo requires, for each operation, to know how to roll back. | You also need to have every operation go through the undo | stack. Another option is to have an efficient snapshot system, | which may be just as hard or even harder. | | Cancel is actually the hardest to do right because it combines | the difficulties of both the progress bar and undo. You have to | have a way to interrupt a long operation at any time and get | back to before it started. And because "cancel" is most often | used when things go wrong (ex: disk full, bad connection, ...) | you have to be very careful with error handling. | whateveracct wrote: | > Regardless of where you push your run/dry-run dispatch to, | the underlying "do it" logic really needs to be factored | properly for individual side effects (think functional). | Otherwise, you inevitably end up with loads of "if dry-run / | else" soup or worse, bugs _caused_ by your dry-run support. | | --dry-run is one of the best motivations for using granular, | extensible effects | | when you constrain literally every bit of IO your program is | allowed to do, you can now 100% know you've stubbed them all | out when doing a dry run | hinkley wrote: | > I've found that you really need to design for it | | It's an architectural step a lot of people skip, and by doing | so you often end up in a situation where the only way to make | things faster is via caching, and chasing caching bugs for the | rest of your tenure. | | One of the less appreciated aspects of model-view-controller is | that usually it demands that you plan out your action before | you do it - especially when that action is read-only. Like a | cooking recipe, you gather up all of the ingredients at the | beginning and then use them afterward. | | By fetching all of the data early, you reduce the length of the | read transactions to the database, allowing MVCC to work more | efficiently. You also paint a picture of the dependency tree in | your application, up high where people can spot performance | regressions as they show up in the architecture - where there's | a better opportunity to intercede, _and_ to create teachable | moments. | | To make dry-run work you are best served by book-ending your | reads and your writes, because if the writes are spread across | your codebase how do you disable all of the writes? And if any | reads are after writes, how do you simulate that in your dry- | run code? It won't be easy, that's for sure. | | The problem is that we tend to write code stream-of- | consciousness, grabbing things just as we need them instead of | planning things out. This results in a web of data dependencies | that is scattered throughout the code and difficult or | impossible to reason about. This is where your caching insanity | really kicks into high gear. | | To my eye, Dependency Injection was sort of a compromise. You | still get a good deal of the ability to statically analyze the | code but you can work more stream-of-consciousness on a new | feature. But it does rob you of some more powerful caching | mechanisms (memoization, hand tuning of concurrency control, | etc) | Smaug123 wrote: | This is the first comment I've used HN's "favorite" feature | on. How wonderfully insightful. | kbenson wrote: | Usually I just use dry run directives to make sure stuff is | working right when I first develop it, so all that is worked | out from the very beginning. I also like to develop with a lot | of debug output gated behind debug as well, and just leave that | there for the inevitable but at some point that would make me | add it if not already present. | | What I don't like is passing those as params as args. Currying | this stuff around is very error prone and cumbersome. I prefer | to set environment variables for DRY_RUN and DEBUG (with debug | accepting higher integer levels for more debugging). | | This works wonderfully for me since I never have to worry I | didn't pass something along correctly, I'm always asking the | global set at runtime. | d4mi3n wrote: | There are other approaches to this as well: | | 1. Passing around a configuration or context (similar to your | gripe around params) | | 2. Referencing some kind of global configuration (a la env | vars) | | 3. Referencing a local or scoped configuration (for example, | a method of an object can check instance variables that | dictate behavior) | | I personally prefer either a passed context or a local | configuration; I find both easier to test in isolation. | Global contexts have their uses, but tend to become | problematic when they clash with other libraries or tools | that may also be present in the execution environment. | kbenson wrote: | Those are good approaches, and I wouldn't try to use | manually set env vars for most things. I do tend to think | they work very well for debug an dry run options though, | because those are generally ephemeral, and things you might | want to set ad-hoc in different environments easily without | changing the config for everything in that environment, or | passing around a special config which may not be updated | when the real one is. | | That said, I'm not married to it, if I saw something that | seemed obviously better, I would switch. I also suspect | that different languages may make one approach | easier/better than others based on their capabilities, | idioms, etc. In many scripting languages, accessing an | environment variable is extremely easy. In some compiled or | more strictly typed languages, the access and conversion to | the expected type might be cumbersome enough to do on site | that it's worse, and if you are standardizing in come | parsing routine, that might tip the benefits in favor of | some global context that is used instead. | hinkley wrote: | I have some code that lacks permissions to run to completion | except on Bamboo or other servers, so I practically have to | have a dry-run mode anyway. | Buttons840 wrote: | So, for a top level bit of code to pass arguments to lower | level bits of code you set environment variables? Seems a bit | odd to me | | Using environment variables for the user to configure their | environment or pass information into a program seems normal, | but code passing arguments to other bits of code in the same | language with environment variables seems odd. | kbenson wrote: | The only two things I use this for are for setting things | that I consider runtime flags that I want honored | throughout the instance of that applicaton run. | | In the instance of dry run, I think it's far more important | that it's correctly seen and honored than that globals | should be avoided. I view dry run mode as a contract with | the person running, and if I can avoid having to pass | arguments along every time, and avoid having to got change | the arguments of all the utility functions I call and then | all their call sites, then that's a win because if that | needs to be done to correctly honor that sometimes it _won | 't_ be done and some worse solution will happen, if | anything. | | > but code passing arguments to other bits of code in the | same language with environment variables seems odd. | | I don't pass the environment variables around, they exist | as part of the program state. if foo() calls bar(), I don't | pass the environment variable to bar, it exists as a global | flag, I just check for it in bar(). That's the benefit, | it's a global and set at runtime. | | The other benefit is that as an environment variable, it's | inherited by child processes. Even if I system out to | another utility script, I don't have to pass a debug flag | their either, it's inherited as part of the environment the | child runs in. | [deleted] | raziel2p wrote: | In many programming languages, environment variables are a | mutatable dictionary/map, which makes it the ultimate place | to store global variables. | | It's definitely my guilty go-to-hack when I'm not up for | refactoring everything to be more functional and/or take a | dry_run function parameter everywhere. | Buttons840 wrote: | It's a global mutable map that multiple uncoordinated | processes can change. If you want a global map, just make | a global map. Or just make global variables, since the | variable namespace itself is a map. | kbenson wrote: | All that assumes you have some stuff to set up that | global map, and that means that setup code is a | requirement. | | I end up writing lots of library code. Sometimes that | library code is called from within a command line utility | I created, sometimes it's called from a web service, | sometimes it's just a small driver script because I'm not | developing or testing the utility, but the library code | itself. | | I can make that include to set up the shared global and | try to make sure it's included in all instances and all | ways I want to use the code, or make all the call sites | resilient to it not existing, or I can just use the | included OS mechanism for doing this and since that's | _always_ available, I get it for free. | | Also, dry run mode isn't necessarily something you want | set in a config. It's generally something you run once or | twice prior to running for real (the normal case) or | while in development/debugging. It's not something you | would want to set in code and accidentally forget and | push live, and generally a good dry run mode will look | like it succeeded without actually succeeding, mocking | responses that would fail along the way, because you | aren't testing one small thing you're testing a workflow | of some sort generally which has a few steps. | | That said, I fully admit the trade-off might go a | different way for different languages. Using a compiled | strongly typed language may mean there's enough bits to | check that you need to write a debug/dry run helper | function to make it convenient, so there's not a lot lost | by requiring setup in that as well. But for something | like Perl (and I assume Python and Ruby and JS, to almost | the same degree) where I can do: warn | "Calling out to foo() with args: " . Dumper($args) if | $ENV{DEBUG} and $ENV{DEBUG} >= 2; foo($args) if | not $ENV{DRY_RUN}; | | and it will be completely valid, obvious and idiomatic | with zero additional work, there's a real draw to using | environment vars for these two specific cases (even if | not for all config). | voakbasda wrote: | Simulation is a great thing. I use this wrapper, even in my most | trivial shell scripts: | pretend=${scriptname_pretend:-false} run() { echo "$@"; | $pretend || "$@"; } | | Sure, this doesn't give copy/paste re-usable output, but it's | really just a quick and dirty sanity check when things start | getting complicated. | deckard1 wrote: | If you're doing a lot of modifications to a filesystem or a | database, one pattern I've used and liked in the past is to have | your code simply dump out the commands _to_ run. Usually to | stdout and you save it as a script. | | That way you don't need to worry about issues with bugs in the | dry run logic _and_ you have a record (a script) of what you | actually ran. | | Even better if you can extend this output script so that it's | idempotent and fails on the first failed command. | slver wrote: | Dry run is OK, but if we want to look at it closer, it occurs in | tools with mixed query + command responsibilities. | | For example rm PATTERN | | Is really a shortcut for something like (pseudo code): | find -name PATTERN | rm | | If things were always factored for command/query seggregation | you'd not need dry-run, you'd simply just run the query. | rwl wrote: | This is a good point, but notice that what makes the | segregation easy in your example is that the query and the | command have a shared way to refer to what the command will | operate on: the filename. | | Consider a conceptually very similar case: instead of finding | and deleting files in a filesystem, think about finding and | deleting lines matching a pattern within a file. Then the query | is something like grep...but what do you put for the command on | the other side of the pipe? | | Of course, you can tell grep to output line numbers and use a | command that operates on line numbers, or similar. The point | is, in order to achieve this kind of segregation, you need | _some_ common way of naming operands on both sides of the | divide. And naming things is hard, so segregating things this | way is hard, and thus there 's a lot of tools with mixed | query/command responsibilities. | slver wrote: | PowerShell solved this problem by outputting easy to map | objects instead of just plain text. Unfortunately they made | it a bit more verbose than it could've been, and the industry | didn't recognize the potential, so it remained a niche | product. | | I still love PowerShell. | ItsMonkk wrote: | Finding is a query. Deleting is destructive. Make sure that | you are not nesting your destructions within a query. | | You would first build a pure function that read the files and | returned matching lines. You could then display the count of | the matched lines, and if the user wants they can see those | lines and what line number it is. This looks a lot like a | dry-run. | | You can then pass these results into an executor. You then | build a system that combines all deletes of a file and passes | that to your DeleteLinesInFile fuction that does so in a | single transaction. Cycle through each file and voila. | lbhdc wrote: | I have been incorporating dry run flags into the tools I have | been working on (typically etl like tools). Typically this flag | only stops the final call to external services that would change | the internal state of those services. | | Its been really helpful when hacking on things, adjusting logic, | or validating that input data does what I think it will. | JustSomeNobody wrote: | I use --dry-run with rsync almost every time I use rsync. I don't | really use rsync enough to remember if I should use a trailing | slash or not... | overshard wrote: | At my work I've gone through and setup a variety of "replacement" | commands for commands that may have dire impact on production | systems. All of these replacement commands dry run by default (or | fake dry run/try to create a dry run the best they can) and | require a "--do-it" (think emperor palpatine voice) flag to do | the intended operation. I've had multiple coworkers thank me for | this setup as it's saved people from silly typos and mistakes, | one such being the different handling of programs in the use of | and ending "/" in folders. ex. "/etc" is different than "/etc/" | sometimes for certain operations. | | I'd like to one day see a global flag on operating systems to dry | run nearly all commands that changed anything. | Hjfrf wrote: | It's official best practice for powershell to enable - whatif | in any potentially dangerous scripts. | | Seems to be followed most of the time. | jonnycomputer wrote: | Good idea. I'm writing some db scripts, and I think I'll do | this. | Twirrim wrote: | I've largely switched to writing all my tools dry-run by | default. We've had a few undesirable events from tools that had | --dry-run modes and accidentally got run without. Making --run | or similar necessary almost guarantees it's never run without | intention. | greggyb wrote: | It should be a parameter that takes as an argument the current | user and logs the invocation, "--if-it-breaks-i-take-personal- | responsibility $USER" to really make sure folks understand what | they're doing. | nicoburns wrote: | > All of these replacement commands dry run by default | | Same here. In addition if there's never a use case for running | in production (e.g. at one point we had a command that reset a | dev environment to a clean slate) then the command will | actually specifically check for the production environment and | refuse to run as an extra failsafe. | IgorPartola wrote: | --make-it-so is even better. | andrewshadura wrote: | I sometimes create a makefile with "it so: build" so that I | can make it so :) | danmur wrote: | Ditto :). I use --really though. | MAGZine wrote: | --void-warranty | andrewshadura wrote: | --deliberately-do-insane-thing | matsemann wrote: | React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED | wtetzner wrote: | --launch-the-missles | maherbeg wrote: | +1. I prefer having a --real flag that you have to invocate. | ayoisaiah wrote: | This resonates with me. I recently published a cross-platform | tool [1] for bulk renaming files with dry run as the default. In | this mode, it shows the changes about to be made and any possible | problems that may occur (such as overwriting existing files). To | actually carry out the changes a flag must be used. | | I felt it was a good design decision since the effects of bulk | renaming can be substantial. Most other similar tools have it | backwards with their dry-run mode relegated to a secondary | action. | | [1]: https://github.com/ayoisaiah/f2 | swyx wrote: | all CLIs that do substantial work should have dry runs. | | more examples I've collected: | | - Angular CLI https://malcoded.com/posts/angular-fundamentals- | cli/#--dry-r... | | - AWS CLI https://docs.aws.amazon.com/cli/latest/userguide/cli- | usage-h... | | - Rspec https://relishapp.com/rspec/rspec- | core/v/3-8/docs/command-li... | | - Serverless framework https://forum.serverless.com/t/dry-run- | with-serverless-frame... | | at netlify we didnt do a dry run but Netlify Dev is close: | https://news.ycombinator.com/item?id=19615546 | | I'm requesting one at Gatsby: | https://github.com/gatsbyjs/gatsby/discussions/16384 | mr-wendel wrote: | For all you Bash heads out there: DRYRUN=1 | VERBOSE=1 cmd() { [ $DRYRUN -eq 1 -o | $VERBOSE -eq 1 ] \ && echo -e "\033[0;33;40m# | $(printf "'%s' " "$@")\033[0;0m" >&2 if [ $DRYRUN -eq 0 | ]; then "$@" fi } | | And now you can put this all over your scripts to very easily | implement dry-run behavior without any quoting worries. Note that | you can't invoke aliases this way. cmd echo | "don't worry about quoting" cmd do_something_dangerous | "$scary_arg" "$scary_arg2" | tremon wrote: | Why not simply DRYRUN=echo cmd() { | $DRYRUN "$@" } ? | croehrig wrote: | I use this all the time (I call it $EXEC instead), and have | gotten into the habit of starting all my scripts with | EXEC=echo and put it on every command that does any file | changes. It has saved my bacon many times. | | It doesn't work all the time as the previous poster noted, | but it is very low friction which is especially important | when writing quick throw-away scripts. | mr-wendel wrote: | The printf version will give you automatic quoting making | copy'n'paste really easy to test. It' also mixes well w/ a | verbose option for "--verbose" or such. | ebeip90 wrote: | For those using zsh, you can get better quoting (and only | when necessary, and handles non-printable characters and | newlines better) than using printf. | | run() { if dry; then echo "${(q-)@}" else "$@" fi } | Smaug123 wrote: | (Author here.) | | A whistlestop tour of the technique "insert a lightweight API | boundary down the middle of a tool, for fun and profit". It leads | you towards a structure that has lots of benefits, such as | meaningful `--dry-run` output and more ready librarification. | tasogare wrote: | Nice to see F# in posts from time to time. I might try the | technic in the future. | b3morales wrote: | This was a great read! | | It's not material to the point, but I think there's a small | typo on line 12 of the second "Finishing the example" snippet: | it looks like let instructions = gather args | | should be let instructions = gather | inputGlobs | Smaug123 wrote: | Thanks! | | You're quite right; thanks for letting me know. I'll get that | fixed. | gm wrote: | What do you guys think of "dry run by default" where you have to | turn dry run OFF via an argument? I've done this on some of my | utilities just because screwing up the arguments would result in | much grief (lost data). | gorgoiler wrote: | I don't even trust dry run for some things. | | Yesterday I wrote a cleanup tool. In fact it's two tools: one | that outputs a parseable list of things to clean up... | # snapshotX # keep snapshotY # delete | | ...and another that consumes the list and does stuff. It's the | only way to be sure. | im3w1l wrote: | First I found myself nodding along, but then a troublesome | question popped into my mind. What about Toc/tou race conditions? | iudqnolq wrote: | I think this can be mitagated with more abstract descriptions, | like: | | "Would have gotten all servers tagged foo (currently 1,341) and | deleted them" | Smaug123 wrote: | That's all very well for just `--dry-run`, but it's still a | problem if you use the method in OP to _architect the entire | tool_ (and in particular the non-dry-run execution) around | the existence of a `--dry-run` phase. | iudqnolq wrote: | I don't think so? The planning stage generates something | like DeleteOp(ServerSpec { tag: "foo" }). | Smaug123 wrote: | I think the problem is that describing it as "would have | deleted 1378 servers", and then going off and deleting | those 1378 servers, doesn't solve the race condition | problem that leaves you with some servers left undeleted | because they popped into existence after the check. | | With server deletion, of course, the problem is less | visible because it's naturally idempotent (unless you're | referring to servers by some non-unique key). In that | case, you can safely just pretend the leftover servers | came into existence after the entire tool came into | being. | | Of course, if you're referring to servers by name or IP | or something, then you hit exactly the problem. | Concretely: I run a command that deletes all servers | older than one day. The 'dry-run' section determines what | servers need to be deleted and it turns out that server | "foo" needs to be deleted. Elsewhere, someone spots that | "foo" exists, deletes it, and creates a new server with | the name "foo". Then the 'execute' flow deletes the new | "foo", which is entirely not the server we wanted to | delete. | cmeacham98 wrote: | Your example inaccurately describes what would happen | with GP's approach. | | Planning stage would generate | `Delete(Server{CreatedBefore: 1621887357}))`. | | It would output something like "Deleting servers created | before $time$, currently $servers$." | | The point is the planning stage would encode the | operation being done, not a list of servers to delete. | Smaug123 wrote: | Ohh, sorry - I got the wrong end of the stick entirely. | Yes, in that case I'm satisfied! | Smaug123 wrote: | Indeed, that's the thing I rather swept under the rug. I'll | definitely take precautions if it's at all easy to avoid the | assumption that just because my `--dry-run` stage said | something was safe, it will definitely be safe by the time | execution passes to the "do-it" stage. However, in general you | are indeed doomed the instant you decided to split the tool up | this way. | | I have never actually encountered such a race condition while | using any tool I've written this way. But I do remember one | place where I specifically didn't add a feature to the tool | because it was so vulnerable to a TOC/TOU race condition. | Instead, I turned that feature into a "did I just manage to do | the thing successfully, and if not, why not" check at the end | of the "do-it" stage. | | So yes, that concern is extremely valid, and if you're doing an | operation whose validity is liable to change after you've done | the check, then you do just need to follow a different approach | for that operation. (But then there's probably no possibility | even in principle of a `--dry-run` for such an operation | anyway!) | mst wrote: | Something I've found can work is "check, output plan, prompt | for confirmation, if 'yes' execute plan in a way that | maximises the odds of bailing out if anything changed while | you were displaying the prompt" - for bonus points, | recalculate after each action that had to be simulated and do | something policy-defined if anything differs from your prior | expectations. | | Obviously the effort of doing this becomes a trade-off but | it's worth considering. | da39a3ee wrote: | This is obvious but a good way to implement dry run in some | settings is to obtain a read-only connection to the relevant | datastore / roll back a transaction without committing, etc. | SavantIdiot wrote: | Duplicity has this option. I love it. Especially when something | bad can happen, like blowing up my disk because an ISO image is | in the backup area. ___________________________________________________________________ (page generated 2021-05-24 23:00 UTC)