Something I've not seen done, but to me seems like it would be much more useful than a shell script (or maybe would have, in these tools' heyday), would have been to use a tool like Chef or Ansible to manage dev dependencies on developer laptops.
The trouble with shell scripts is that they generally require you to manage the state of the machine - whether or not something is installed, before I go install and configure it. Or, if it's already configured, what configs to leave in place, and what configs to reset.
With Chef, for instance, everything's declarative. I don't say "install this package"; rather, I say "this package should be installed". Nobody ever runs the setup script just once, since the dev environment is constantly changing, and all of those edge cases (maybe I last ran it on rev 3, or rev 7 - how do I get to rev 12?) become hard to manage.
I've actually done this before with Ansible, but it still required a bootstrapping script. Basically it installs homebrew, then Ansbile, asks a few setup questions, clones a git repo with the rest of the setup process, and then kicks off the Ansible script. The dev environment has a lot of legacy stuff in it and has been grown over years so it's difficult to break it down into simpler pieces. It works surprisingly well in comparison to trying to do things manually or the old mixed manual/pure shell script approach.
That said, it's also surprisingly finicky. The scripts have to be regularly maintained or things keep changing out from under you and causing problems. Especially if you need to support more than one platform (i.e. macOs and a Linux say...).
Boxen (https://github.com/boxen/puppet-boxen) used Puppet to achieve this. It worked, but it was quite opinionated and of course you needed to know Puppet so the learning curve was steep. It's since been superseded by Homebrew which I find is a far better experience.
Declarativeness (vs imperativeness) is the property of a language. Whether it allows you to express the desired state, as opposed to a sequence of simple operations.
Idempotency is the runtime behavior.
You can absolutely write idempotent program using imperative language (case in point: chef and its recipe can be expressed as machine code, which is a very imperative language, yet it is idempotent when you run it).
The trouble with shell scripts is that they generally require you to manage the state of the machine - whether or not something is installed, before I go install and configure it. Or, if it's already configured, what configs to leave in place, and what configs to reset.
With Chef, for instance, everything's declarative. I don't say "install this package"; rather, I say "this package should be installed". Nobody ever runs the setup script just once, since the dev environment is constantly changing, and all of those edge cases (maybe I last ran it on rev 3, or rev 7 - how do I get to rev 12?) become hard to manage.