0

Context

I'm trying to write a decent bash parser using separate files and functions for:

  • argument parsing (e.g. determining what to do)
  • parsed argument processing (e.g. doing those things)
  • printing the CLI usage.

However, when I try to pass the "POSITIONAL_ARGS" into the parse_args() {, the arguments don't seem to arrive.

Example:

Suppose I have the following main.sh file in directory: /src:

#!/bin/bash

POSITIONAL_ARGS=()
# source arg_parser.sh
source src/arg_parser/arg_parser.sh
source src/arg_parser/print_usage.sh

# print the usage if no arguments are given
[ $# -eq 0 ] && { print_usage; exit 1; }
echo "POSITIONAL_ARGS=$POSITIONAL_ARGS"
parse_args "$POSITIONAL_ARGS"

And the following arg_parser.sh file in: src/arg_parser/arg_parser.sh:

#!/bin/bash

parse_args() {
  local positional_args="$1"

  # Specify default argument values.
  local apply_certs_flag='false'
  local check_http_flag='false'
  local check_https_flag='false'
  local generate_certs_flag='false'
  local project_name_flag='false'
  local port_flag='false'


  while [[ $# -gt 0 ]]; do
    echo "dollar1=$1"
    echo "positional_args=$positional_args"
    echo "dollar hashtag=$#"
    case $1 in
      -a|--apply-certs)
        apply_certs_flag='true'
        shift # past argument
        ;;
      -ch|--check-http)
        check_http_flag='true'
        shift # past argument
        ;;
      -cs|--check-https)
        check_https_flag='true'
        shift # past argument
        ;;
      -g|--generate-certs)
        generate_certs_flag='true'
        shift # past argument
        ;;
      -n|--project-name)
        project_name_flag='true'
        project_name="$2"
        assert_is_non_empty_string "${project_name}"
        shift # past argument
        shift
        ;;
      -p|--port)
        port_flag='true'
        port="$2"
        assert_is_non_empty_string "${project_name}"
        shift # past argument
        shift
        ;;
      -*)
        echo "Unknown option $1"
        print_usage
        exit 1
        ;;
    esac
  done

  set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
}

Expected output

When I run ./src/main.sh -a, I would expect the output:

echo "POSITIONAL_ARGS=-a
dollar1=-a
positional_args=-a
dollar_hashtag=1

dollar1=-a
positional_args=-a
dollar_hashtag=0

Output

However, instead of parsing/eating/shifting the arguments, the actual output is an infinite loop:

dollar1=
positional_args=
dollar hashtag=1
dollar1=
positional_args=
dollar hashtag=1
dollar1=
positional_args=
dollar hashtag=1
.... etc.

Question

How could I pass the arguments from the CLI from src/main.sh into the parse_args() function in the file src/arg_parser/arg_parser.sh such that they can be parsed?

Notes

I used to have

 -p|--prereq)
      prerequistes_only_flag='true'
      shift # past argument
      ;;
    -*|--*)
      echo "Unknown option $1"
      print_usage
      exit 1
      ;;
    *)
      POSITIONAL_ARGS+=("$1") # save positional arg
      shift # past argument
      ;;
  esac

however, shellcheck says the --*) will never be reached because of the -*|.

Hypothesis

I think at least one error may be that I, in the original question, treated POSITIONAL_ARGS as a string instead of an array. However, shellcheck tells me it is an array:

In src/main.sh line 26:
echo "POSITIONAL_ARGS=$POSITIONAL_ARGS"
                      ^--------------^ SC2128 (warning): Expanding an array without an index only gives the first element.

a.t.
  • 2,002
  • 3
  • 26
  • 66

1 Answers1

0

Issue(s)

  • I thought POSITIONAL_ARGS contained the positional arguments.
  • I treated POSITIONAL_ARGS as a variable instead of an array.

Missing Information:

  • $@ gave the incoming arguments as an array.
  • $# gave the length/number of the incoming arguments.

Solution

A solution was found for the following src/main.sh content:

#!/bin/bash

source src/arg_parser/arg_parser.sh
source src/arg_parser/process_args.sh
source src/arg_parser/print_usage.sh


# print the usage if no arguments are given
[ $# -eq 0 ] && { print_usage; exit 1; }

parse_args "$@"

say_hello() {
  echo "Done parsing args. Hello world."
}
say_hello

and the accompanying src/arg_parser/arg_parser.sh filecontent:

#!/bin/bash

parse_args() {
  # local positional_args=("$@")

  # Specify default argument values.
  local apply_certs_flag='false'
  local check_http_flag='false'
  local check_https_flag='false'
  local generate_certs_flag='false'
  local project_name_flag='false'
  local port_flag='false'

  while [[ $# -gt 0 ]]; do
  # while [[ "${#positional_args[@]}" -gt 0 ]]; do
    case $1 in
      -a|--apply-certs)
        apply_certs_flag='true'
        shift # past argument
        ;;
      -ch|--check-http)
        check_http_flag='true'
        shift # past argument
        ;;
      -cs|--check-https)
        check_https_flag='true'
        shift # past argument
        ;;
      -g|--generate-certs)
        generate_certs_flag='true'
        shift # past argument
        ;;
      -n|--project-name)
        project_name_flag='true'
        project_name="$2"
        assert_is_non_empty_string "${project_name}"
        shift # past argument
        shift
        ;;
      -p|--port)
        port_flag='true'
        port="$2"
        assert_is_non_empty_string "${project_name}"
        shift # past argument
        shift
        ;;
      -*)
        echo "Unknown option $1"
        print_usage
        exit 1
        ;;
    esac
  done

  echo "apply_certs_flag=$apply_certs_flag"

}

Room for improvements

One can see that I removed the use of positional_args and instead used the implicit array $@ and array length $#. I think this solution could be improved a bit by swapping the $@ with positional_args to make it more human readable. One issue in this swap is that the shift commands eat away the first element in the incoming array $@, and not from the local positional_args array.

So to resolve this issue, one could change the shift arguments to eat the first element in the local positional_args array. I did not yet figure out how to do that.

a.t.
  • 2,002
  • 3
  • 26
  • 66