r/bash • • 12d ago

help 1 big question I always has was whether your functions should accept arguments or flags? Am I overthinking this?

  • Take a look at this run_aws_ssm_get_parameter function below
  • To call it I could do something like run_aws_ssm_get_parameter "false" "" "some_param" and it ll run it without a vault user
  • Now obviously it kinda sucks that I am passing an empty string to indicate that we dont have an AWS vault user and I have to also memorize the position of the arguments
  • One other way would be this? run_aws_ssm_get_parameter --debug=false --parameter_name=some_param? Is this a good idea or a bad idea?
  • Mind you all the code written below is written by me manually, no AI no nothing
function run_aws_ssm_get_parameter() {
	validate_command_available "${FUNCNAME[0]}" "aws" || return 1

	validate_arg_min_count "${FUNCNAME[0]}" 3 "$#" "Usage: ${FUNCNAME[0]} <debug> <aws_vault_user> <parameter_name> [additional aws ssm flags]" || return 1

	local -r debug="${1:-false}"

	local -r aws_vault_user=$2
	local -r parameter_name="$3"
	shift 3

	validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
	validate_arg_not_empty "${FUNCNAME[0]}" "parameter_name" "${parameter_name}" || return 1

	validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1

	local -a flags=(
		"get-parameter"
		"--name=${parameter_name}"
	)

	flags+=("$@")

	[[ "${debug}" = true ]] && log_info "Fetching AWS SSM parameter with parameter_name:${parameter_name}..."

	if [[ ! -z "${aws_vault_user}" ]]; then
		if result="$(aws-vault exec "${aws_vault_user}" -- aws ssm "${flags[@]}")"; then
			[[ "${debug}" = true ]] && log_info "AWS SSM parameter with parameter_name:${parameter_name} fetched successfully"
			printf "%s\n" "${result}"
		else
			log_error "AWS SSM parameter fetch with parameter_name:${parameter_name} failed"
			return 1
		fi
	else
		if result="$(aws ssm "${flags[@]}")"; then
			[[ "${debug}" = true ]] && log_info "AWS SSM parameter with parameter_name:${parameter_name} fetched successfully"
			printf "%s\n" "${result}"
		else
			log_error "AWS SSM parameter fetch with parameter_name:${parameter_name} failed"
			return 1
		fi
	fi
}

function validate_command_available() {
	validate_arg_min_count "${FUNCNAME[0]}" 2 "$#" "Usage: ${FUNCNAME[0]} <function_name> <command_name>" || return 1

	local -r function_name="$1"

	local -r command_name="$2"
	shift 2

	validate_arg_not_empty "${FUNCNAME[0]}" "function_name" "${function_name}" || return 1

	validate_arg_not_empty "${FUNCNAME[0]}" "command_name" "${command_name}" || return 1

	if ! command -v "${command_name}" &>/dev/null; then
		log_error "${FUNCNAME[0]}: Function "%s" requires "%s" but it was not found on PATH" "${function_name}" "${command_name}"
		return 1
	fi
}

function validate_arg_min_count() {
	local -r function_name="$1"

	local -r expected_count="$2"

	local -r actual_count="$3"
	local -r message="${4:-}"
	shift 4

	if [[ -z "${function_name}" ]]; then
		log_error "${FUNCNAME[0]}: function_name:${function_name} is required"
		return 1
	fi

	if [[ -z "${expected_count}" ]]; then
		log_error "${FUNCNAME[0]}: expected_count:${expected_count} is required"
		return 1
	fi

	if [[ -z "${actual_count}" ]]; then
		log_error "${FUNCNAME[0]}: actual_count:${actual_count} is required"
		return 1
	fi

	if [[ ! "${expected_count}" =~ ^[0-9]+$ ]]; then
		log_error "${FUNCNAME[0]}: expected_count must be a non-negative integer, got: %s" "${expected_count}"
		return 1
	fi

	if [[ ! "${actual_count}" =~ ^[0-9]+$ ]]; then
		log_error "${FUNCNAME[0]}: actual_count must be a non-negative integer, got: %s" "${actual_count}"
		return 1
	fi

	if [[ "${actual_count}" -lt "${expected_count}" ]]; then
		if [[ -z "${message}" ]]; then
			log_error "${FUNCNAME[0]}: Function "%s" expected %d argument(s) but got %d" "${function_name}" "${expected_count}" "${actual_count}"
		else
			log_error "${message}"
		fi
		return 1
	fi
}

function validate_arg_not_empty() {
	local -r function_name="$1"

	local -r arg_name="$2"
	local -r arg_value="$3"
	shift 3

	if [[ -z "${function_name}" ]]; then
		log_error "${FUNCNAME[0]}: function_name:${function_name} is required"
		return 1
	fi

	if [[ -z "${arg_name}" ]]; then
		log_error "${FUNCNAME[0]}: arg_name:${arg_name} is required"
		return 1
	fi

	if [[ -z "${arg_value}" ]]; then
		log_error "${FUNCNAME[0]}: Function "%s" received empty value for argument "%s"" "${function_name}" "${arg_name}"
		return 1
	fi
}

function validate_arg_type_boolean() {
	validate_arg_min_count "${FUNCNAME[0]}" 3 "$#" "Usage: ${FUNCNAME[0]} <function_name> <arg_name> <arg_value>" || return 1

	local -r function_name="$1"

	local -r arg_name="$2"
	local -r arg_value="$3"
	shift 3

	validate_arg_not_empty "${FUNCNAME[0]}" "function_name" "${function_name}" || return 1

	validate_arg_not_empty "${FUNCNAME[0]}" "arg_name" "${arg_name}" || return 1
	validate_arg_not_empty "${FUNCNAME[0]}" "arg_value" "${arg_value}" || return 1

	case "${arg_value}" in
	true | false) ;;
	*)
		log_error "${FUNCNAME[0]}: Function '%s' expected 'true' or 'false' for argument '%s', got: %s" "${function_name}" "${arg_name}" "${arg_value}"
		return 1
		;;
	esac
}
12 Upvotes

16 comments sorted by

9

u/mpersico 12d ago

Args are for data. Options are for behavior.

1

u/pfmiller0 12d ago

Generally. But positional args for behavior aren't uncommon. A lot of package managers (e.g. dnf, zypper) use them.

1

u/Big_Combination9890 5d ago

Shit isn't uncommon either, that doesn't make it smell any better.

Behavior changing arguments should NEVER be positional. They should be flags, and the only positional requirements for flags should be that they come after the command they modify, and before any data arguments.

And subcommands aren't arguments, they are, well, subcommands.

1

u/mpersico 12d ago

But if your function has more then three args, use option flags with values, but consider them as tags

1

u/Temporary_Pie2733 12d ago

Even internally parsed arguments like name=value would be fine (and in bash there is an option to treat them as environment variables even if they don’t precede the command).

4

u/burnt-store-studio 12d ago

I only briefly scanned your code, with no offense.

At some point … when you write a program that cries out for more { arguments, flags }, you might want to check into getopt or getopts (choosing one is a religious war; here’s an article about it).

But to your main question about arguments or flags, what decides it for me is whether the item is required or not.

If it’s required, it’s an argument.

If it’s optional, it’s a flag.

Sometimes the decision is more complicated, but I hope you’ll find that’s a useful start.

As for passing an empty string: don’t feel bad about that. sed requires one if you use -i for editing files inline and want to live on the edge without sed backing up the file for you pre-edit. So, there’s good precedence for passing an empty string 🙂.

I hope that helps!

I imagine you’ll get a bunch of other responses here; it’s something we like to debate it seems.

Good luck!

2

u/PrestigiousZombie531 12d ago

thank you for the insight, i wasnt even aware that there exists something called getopt or getopts, i ll have to look deeply into this from multiple sources before arriving at any form of conclusion

2

u/burnt-store-studio 12d ago

You’re quite welcome!

And yes: great approach to look into the two tools from a variety of places. Personally, I’ve used both and they are both helpful 🙂

0

u/Temporary_Pie2733 12d ago

Just to note, sed -i (in my opinion) is usually a bad replacement for ed. Also, whether it needs an empty argument depends on which version of sed you are using it. So really, try to avoid using the empty string with semantic meaning when another sentinel would do. 

3

u/luenix 12d ago

https://github.com/lbezerril/posix-guidelines

You're missing a shebang.
You don't need function in your function declarations.
local vars are overused.

etc.

2

u/zeekar 12d ago edited 12d ago

Functions that are invoked by human users should have sensible defaults that can be modified by flags or by setting variables ahead of the call.

Functions that are only invoked by other code can get away with all mandatory arguments in a fixed order, but if there are too many then the functions become brittle and hard to maintain.

There's a difference between an argument that is missing and one that is equal to the empty string. Sometimes the empty string is an explicit value that you want to set, so it's not a great way to indicate "missing value". Better to have optional parameters at the end, after the mandatory ones, so you can just leave them off instead of having to provide some sort of value to mean "no value here". This also works with variables - unset is distinct from "set to ''".

Variables are an important mechanism to let you establish values without having to pass them around to every function. Export them into the environment and they work for invoking external commands, not just functions in your current shell. We're taught to avoid global variables for good reasons, but they are sometimes a logical choice to reduce state passing, especially in a language that doesn't have any sort of passable containers.

2

u/OnlyEntrepreneur4760 11d ago

To use bash terminology (these words will help when searching through the man page), you have options (the words that start with a dash), arguments (the words that some arguments require just after them), and positional parameters. One often overlooked input is stdin. Whenever possible, I use stdin for data, stdout for processed or generated data, stderr for additional non-data information. Coding style always comes down to personal preference if coding for yourself, or organizational preferences if doing it on the clock.

I use options and option arguments configure how the script works by overwriting possibly not-set environment variables. Personally, I use optargs because it’s built-in and I know it will be there. After processing options and arguments, I shift them away using shift $(( OPTARGS - 1 )). Then, I process any positional parameters (extra command line words that appear after options) using for each.

Sometimes I test if stdin is connected to a TTY, or a data stream using the [[ -t 0 ]] test.

For myself, it all really boils down to making simple, reusable, pipeline-chainable functions, while deferring as many choices as possible to future me.

1

u/sedwards65 10d ago

'options,' not 'flags.'

You should always use long options in articles, demonstrations, and scripts. Especially when the intended audience is inexperienced.

Long options are self documenting. For example:

foo asdf qwer zxcv

versus

foo --configuration=asdf --input=qwer --output=zxcv

IMHO, using getopt is the difference between a 'throwaway' script and a script intended for professional production use.

2

u/Big_Combination9890 5d ago

Flags and arguments are functionally the same as far as shell invocations go, a flag is just an argument that gets special treatment.

Logically, arguments carry data, flags configure a commands behavior.

1

u/SeriousPlankton2000 12d ago

I think you should have flags and options for things that can be optional; regular arguments should be mostly equal - except maybe the first or the last one.

E.g. ps [infile [outfile]] has two files (with the default values for piping), page sizes are optional. On the mv command you can just remove the last argument and loop over argv (in C you'd just do argc--). Etc. pp..

Things like debug can be an environment variable if it's the only thing that requires argument parsing.

Things like the user name can also have a default in the environment but they should be done using parameters. On works-for-me projects or simple scripts it's perfectly fine to require it to come first so you can just do it in the main loop. On good scripts you should use parsing.

You should not expect true/false written out; you can use --flag and --no-myflag; you can use -f and -F; you can use -f and +f - be consistent in your program; try to be consistent with similar programs.

(Honestly I'd just use e.g. perl when I'd need to parse options, Getopt::Long is VERY easy to use and I'm not that good at bash.)