4

My script:

#! /bin/bash --

set -x

## docker-compose wrapper
compose_fn() {
  local ENV="${1}"
  local VERB="${2}"
  local SERVICE="${3}"
  local CMD="docker-compose -f ${ENV}.yml"
  case "${VERB}" in
    (exec)
      shift "$#" # remove args passed to this fn
      # Execute a command in a running container.
      if [ -n "${SERVICE}" ]; then
        ${CMD} "${VERB}" "${SERVICE}" "$@"
      else
        echo "## Err: You must specify service name..."
        exit 1
      fi
    ;;
  esac
}

compose_fn "${1}" "${2}" "${3}"

Is giving me a hard time with the following error:

$ ./tst.sh dev exec django sh
+ compose_fn dev exec django
+ local ENV=dev
+ local VERB=exec
+ local SERVICE=django
+ local 'CMD=docker-compose -f dev.yml'
+ case "${VERB}" in
+ shift 3
+ '[' -n django ']'
+ docker-compose -f dev.yml exec django
Execute a command in a running container

Usage: exec [options] [-e KEY=VAL...] SERVICE COMMAND [ARGS...]

Options:
....

Where is my mistake? How can it be done better?

As far as I can tell I've passed 4 args [dev, exec, django, sh] to the script, then within the script removed 3 (shift 3), therefore sh should have been left in the $@ var.

NarūnasK
  • 2,345
  • 5
  • 25
  • 37

1 Answers1

11

With shift "$#" you empty $@ completely. The $@ in the function is separate from the $@ in the main script. Since you know exactly how many elements of the script's $@ you need to use and shift off in the function, why don't you just pass all arguments to the function and then shift off the first three?

#! /bin/bash --

set -x

## docker-compose wrapper
compose_fn() {
  local env="$1"
  local verb="$2"
  local service="$3"

  local cmd=( docker-compose -f "$env.yml" )

  shift 3 # we've now used up three arguments

  case $verb in
      exec)
          # Execute a command in a running container.
          if [ -n "$service" ]; then
              "${cmd[@]}" "$verb" "$service" "$@"
          else
              echo '## Err: You must specify service name...' >&2
              exit 1
          fi
          ;;
      *)
          printf 'Unknown verb: %s\n' "$verb" >&2
          exit 1
    esac
}

compose_fn "$@"

I've also used lower-case variable names so that no system or special shell variables are used by accident (ENV is one that some shells uses under some circumstances, for example), and I've removed all unneeded quotes and curly braces.

I've also put the command into an array, so that we can quote the YAML filename properly.

You could also just move the setting of the three variables outside of the function, depending on what the rest of the script looks like and if this makes any sense at all. The three variables would then be global in the script.

#! /bin/bash --

set -x

## docker-compose wrapper
compose_fn() {
  local cmd=( docker-compose -f "$env.yml" )

  case $verb in
      exec)
          # Execute a command in a running container.
          if [ -n "$service" ]; then
              "${cmd[@]}" "$verb" "$service" "$@"
          else
              echo '## Err: You must specify service name...' >&2
              exit 1
          fi
          ;;
      *)
          printf 'Unknown verb: %s\n' "$verb" >&2
          exit 1
    esac
}

env="$1"
verb="$2"
service="$3"

shift 3

compose_fn "$@"

You may also bypass the [ -n "$service" ] test with

service=${3:?'## Err: You must specify service name...'}

The parameter expansion ${parameter:?word} will exit the shell with the message defined by word if parameter is unset or empty. The bash shell would format this as

script.sh: line 9: 3: ## Err: You must specify service name...

Related:

Kusalananda
  • 333,661