I have several Bash functions that install tools such as Docker, Docker Compose, Git, and jq. Each function repeats argument-count checks, empty-value validation, boolean validation for a debug flag, command execution, error handling, and optional success logging. The tools also need slightly different actions: Docker requires adding a user to a group and enabling a service, while Docker Compose requires downloading a file and changing its permissions. Is there a clean way to extract the common logic without making the code harder to understand?
3 Answers
You could also centralize the debug logging and validation in small helpers, then use early returns for each operation. For the service setup, `systemctl enable --now docker.service` can replace separate enable and start commands. Just be careful with `newgrp`: it starts a new shell and usually does not update the group membership of the current script in the way people expect. Adding the user to the group is enough for future sessions; the user may need to log in again.
The repeated package-install functions can share a helper that accepts the package name, version, and display command. Keep the special Docker and Compose steps separate, since they have different behavior. For example, a helper could run `dnf install "${package}-${version}" --assumeyes --quiet`, log a generic failure, and optionally run a version command after success. That removes most of the duplication without forcing unrelated operations into one giant function.
That makes sense. I was mainly worried about duplicating the same validation and install/error-handling block for every package.
There is nothing inherently wrong with the longer version if clarity and predictable error handling are the priorities. Compact code is not automatically better. I would first extract only the clearly identical parts—argument validation, package installation, and debug logging—while leaving Docker-specific group and service handling in its own function. That keeps the code readable instead of hiding all behavior behind a heavily generic abstraction.

The validation is thorough, but repeating it manually in every function makes maintenance harder. A shared argument parser or a generic installer would make changes much less error-prone.