Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named parameters break if the function returns immediately after parameter definition #41

Open
nkakouros opened this issue Jan 30, 2018 · 3 comments

Comments

@nkakouros
Copy link
Collaborator

nkakouros commented Jan 30, 2018

Consider this example bash file:

function q() {
  [string] var
}

In the above example, there is no command for the DEBUG trap to hook into. This is expected.

Now consider this:

function q(){
  @required [array] var
  for i in "${var[@]}"; do
    echo $i
  done
}

q "$(@get array_var)"
echo 'Done'

In the above, (it seems that) the var array will be set when the for i in "${var[@]}"; do line gets run. But bash will not even run this line as there is no array to expand. So, it will simply skip the loop and return from the function as there is no other command to run. As a result, the context will change and __assign_paramNo and the other variables will be unset. The @required is there just for the example, it is not crucial to repeating the behavior. If there were more lines after the loop in the function, bash would still skip the loop, but the array would eventually be evaluated when the line after the loop was run.

I haven't found a way to solve this.

@niieani
Copy link
Owner

niieani commented Jan 31, 2018

Indeed, this is a serious problem.
I've thought about it for a while and come up with a re-written, simpler (but more powerful) way to declare and assign parameters. It comes with a new syntax though (two options, comma separated or new line starting with :), but I'm sure it will be possible to make backwards-compatible aliases.

It looks like this:

# syntax with commas:
function example { args : @required string firstName , string lastName , integer age } {
  echo "My name is ${firstName} ${lastName} and I am ${age} years old."
}

# syntax with colons and new lines:
function example {
  args
    : @required string firstName
    : string lastName
    : integer age
    : string[] ...favoriteHobbies

  echo "My name is ${firstName} ${lastName} and I am ${age} years old."
  echo "My favorite hobbies include: ${favoriteHobbies[*]}"
}

# normal usage:
example Mark McDonald 32 singing jumping

Try playing around with this snippet and feel free to give me your feedback:

#!/usr/bin/env bash

shopt -s expand_aliases

function echo_stderr {
  echo "$*" >&2
}

function assignTrap {
  local evalString
  local -i paramIndex=${__paramIndex-0}
  local initialCommand="${1-}"

  if [[ "$initialCommand" != ":" ]]
  then
    echo "trap - DEBUG; eval \"${__previousTrap}\"; unset __previousTrap; unset __paramIndex;"
    return
  fi

  while [[ "${1-}" == "," || "${1-}" == "${initialCommand}" ]] || [[ "${#@}" -gt 0 && "$paramIndex" -eq 0 ]]
  do
    shift # first colon ":" or next parameter's comma ","
    paramIndex+=1
    local -a decorators=()
    while [[ "${1-}" == "@"* ]]
    do
      decorators+=( "$1" )
      shift
    done

    local declaration=
    local wrapLeft='"'
    local wrapRight='"'
    local nextType="$1"
    local length=1

    case ${nextType} in
      string | boolean) declaration="local " ;;
      integer) declaration="local -i" ;;
      reference) declaration="local -n" ;;
      arrayDeclaration) declaration="local -a"; wrapLeft= ; wrapRight= ;;
      assocDeclaration) declaration="local -A"; wrapLeft= ; wrapRight= ;;
      "string["*"]") declaration="local -a"; length="${nextType//[a-z\[\]]}" ;;
      "integer["*"]") declaration="local -ai"; length="${nextType//[a-z\[\]]}" ;;
    esac

    if [[ "${declaration}" != "" ]]
    then
      shift
      local nextName="$1"
      local handleless=

      for decorator in "${decorators[@]}"
      do
        case ${decorator} in
          @readonly) declaration+="r" ;;
          @required) evalString+="[[ ! -z \$${paramIndex} ]] || echo_stderr \"Parameter '$nextName' ($nextType) is marked as required by '${FUNCNAME[1]}' function.\"; " ;;
          @handleless) handleless=1 ;;
          @global) declaration+="g" ;;
        esac
      done

      local paramRange="$paramIndex"

      if [[ -z "$length" ]]
      then
        # ...rest
        paramRange="{@:$paramIndex}"
        # trim leading ...
        nextName="${nextName//\./}"
        if [[ "${#@}" -gt 1 ]]
        then
          echo_stderr "Unexpected arguments after a rest array ($nextName) in '${FUNCNAME[1]}' function."
        fi
      elif [[ "$length" -gt 1 ]]
      then
        paramRange="{@:$paramIndex:$length}"
        paramIndex+=$((length - 1))
      fi

      evalString+="${declaration} ${nextName}=${wrapLeft}\$${paramRange}${wrapRight}; "

      # if [[ "$handleless" != 1 ]]
      # then
      #   evalString+="Type::CreateHandlerFunction \"$nextName\"; "
      # fi

      # continue to the next param:
      shift
    fi
  done
  # echo_stderr "evaling $evalString"
  echo "${evalString} local -i __paramIndex=${paramIndex};"
}

alias args='local __previousTrap=$(trap -p DEBUG); trap "eval \"\$(assignTrap \$BASH_COMMAND)\";" DEBUG;'

# syntax with commas:
function example { args : @required string firstName , string lastName , integer age } {
  echo "My name is ${firstName} ${lastName} and I am ${age} years old."
}

# syntax with colons and new lines:
function example {
  args
    : @required string firstName
    : string lastName
    : integer age
    : string[] ...favoriteHobbies

  echo "My name is ${firstName} ${lastName} and I am ${age} years old."
  echo "My favorite hobbies include: ${favoriteHobbies[*]}"
}

example Mark McDonald 32 singing jumping

@nkakouros
Copy link
Collaborator Author

alias args='local __previousTrap=$(trap -p DEBUG); trap "eval "$(assignTrap $BASH_COMMAND)";" DEBUG;'

This line missing from the current version had me spending 3 hours of going through thousands of set -x output lines. 😛 .

There is also the issue of the functrace option being set which might break things (it does in the current version and I think it will cause problems here as well when local __previousTrap=$(trap -p DEBUG); gets run a second time, ie when a second argument is being evaluated).

I find this implementation to be awesome! I cannot decide if I like the new syntax more or not. I would still like to explore the possibility of having the old aliases work.

My only objection is the comma syntax. I think it breaks the bash function "signature", ie instead of

[function] func_name[()] {.*}

we now have:

[function] func_name[()] {.*}{.*}

This might break existing tools that rely on the first "signature" to scan the code and identify function definitions, etc.

@niieani
Copy link
Owner

niieani commented Mar 8, 2018

Thanks for the feedback!

I think old aliases could be ported - I was thinking how to do it and it shouldn't be too hard to add.

As for the comma signature, it shouldn't break. Here's why: the second } and { are really just arguments passed to the first command (args), formatted on the same line as the opening brace {.

function example {
  args : @required string firstName , string lastName , integer age } {
  echo "My name is ${firstName} ${lastName} and I am ${age} years old."
}

The curly braces really just there as a decoration, as they're ignored by assignTrap.

At least when I've tried with shellcheck, it worked.

No idea about how it might work with functrace -- worth checking. There's probably a way to workaround any problems, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants