Problem with manipulating global variable within function in bash

76 Views Asked by At

Code is to match '(' with ')' and '{' with '}'. My problem is that the way I did is request to pop() is creating subshell. Is there a way to modify it with locale -n or some new features from bash 4.3> ?

#!/bin/bash
set -x
top=0
STACK_SIZE=4
declare -a contents

read -rp "Enter parentheses: " input

if [ -z "$input" ]; then
echo "no input"
exit 1
fi

make_empty() {
top=0
}

is_empty() {
[ "$top" -eq 0 ] && return 0 || return 1
}

is_full() {
[ "$top" -eq "$STACK_SIZE" ] && return 0 || return 1
}

push() {
if is_full; then
    echo "Stack overflow"
    exit 1
else
    contents[$top]="$1"
    ((top++))
fi
}

pop() { #problematic function
if is_empty; then
    echo "parentheses arent matched"
    exit 1
else
    ((top--)) #this is the line that doesnt update $top var
    echo "${contents[$top]}" #but updates local copy of top inside its subshell
fi
}

i=${#input}

while read -n 1 char; do
    case $char in
        } | { | \( | \));;
        *) echo "wrong input"
           exit 1;;
    esac
((i--))
if [ "$char" == '(' ] || [ "$char" == "{" ]; then
    push "$char"
elif [ "$char" == ')' ] && [ "$(pop)" != '(' ]; then #here unintentional subshell creation
    echo "not nested properly"
elif [ "$char" == '}' ] && [ "$(pop)" != '{' ]; then #same here
    echo "not nested properly"
elif [ "$i" -eq 0 ]; then
    if is_empty; then
    echo "matched"
    break
    else
    echo "Not nested properly"
    fi
fi

done <<<"$input"

input/output:

+ top=0
+ STACK_SIZE=4
+ declare -a contents
+ read -rp 'Enter parentheses: ' input
Enter parentheses: () #INPUT
+ '[' -z '()' ']'
+ i=2
+ read -n 1 char
+ case $char in
+ (( i-- ))
+ '[' '(' == '(' ']'
+ push '('
+ is_full
+ '[' 0 -eq 4 ']'
+ return 1
+ contents[$top]='('
+ (( top++ )) # TOP IS 1 NOW
+ read -n 1 char
+ case $char in
+ (( i-- ))
+ '[' ')' == '(' ']'
+ '[' ')' == '{' ']'
+ '[' ')' == ')' ']'
++ pop
++ is_empty
++ '[' 1 -eq 0 ']'
++ return 1
++ (( top-- )) #TOP SHOULD BE 0 AFTER THIS
++ echo '('
+ '[' '(' '!=' '(' ']'
+ '[' ')' == '}' ']'
+ '[' 0 -eq 0 ']'
+ is_empty
+ '[' 1 -eq 0 ']' #TOP IS NOT UPDATED HERE
+ return 1
+ echo 'Not nested properly'
Not nested properly
+ read -n 1 char
+ case $char in
+ echo 'wrong input'
wrong input
+ exit 1

I commented the code to pinpoint the lines where the problem is. I solved the problem by rewriting the code from scratch but would like to see if its possible to do something with this subshell creation.

Working UPDATED CODE:

#set -x
top=0
STACK_SIZE=4
declare -a contents

read -rp "Enter parentheses: " input

if [ -z "$input" ]; then
    echo "no input"
    exit 1
fi

make_empty() {
    top=0
}

is_empty() {
    [ "$top" -eq 0 ] && return 0 || return 1
}

is_full() {
    [ "$top" -eq "$STACK_SIZE" ] && return 0 || return 1
}

push() {
    if is_full; then
        echo "Stack overflow"
        exit 1
    else
        contents[$top]="$1"
        ((top++))
    fi
}

pop() {
    if is_empty; then
        echo "Parentheses aren't matched"
        exit 1
    else
        ((top--))
    fi
}

i=0
while [ "$i" -lt "${#input}" ]; do
    char="${input:$i:1}"

    if [ "$char" == '(' ] || [ "$char" == '{' ]; then
       push "$char"
    elif [ "$char" == ')' ]; then
         if [ "$top" -eq 0 ] || [ \
            "${contents[$((top-1))]}" != '(' ]; then
             echo "not nested properly"
             exit 1
          else
             pop
          fi
     elif [ "$char" == '}' ]; then
          if [ "$top" -eq 0 ] || [ \
             "${contents[$((top-1))]}" != '{' ]; then
              echo "not nested properly"
              exit 1
           else
              pop
           fi
     else
            echo "wrong input"
            exit 1
     fi
((i++))
done

is_empty && echo "matched" || echo "Not nested properly"
4

There are 4 best solutions below

0
KamilCuk On

Is there a way to modify it with locale -n or some new features from bash 4.3> ?

No.

The only way is to implement inter-process communication, where the parent would listen for messages from child processes and act on them. This would be a lot of work.

0
user1934428 On

The problem is the design of pop. You are doing two unrelated things at the same time: Printing some information message, and then using the message to detect the outcome of pop. The latter could be done using an status code, but it would mean that you should pass to pop the expected popped element::

pop() { # unproblematic function
  local expected=$1
  if is_empty; then
    echo "parentheses arent matched" 1>&2
    return 2
  else
    ((top--)) #this is the line that doesnt update $top var
    if [[ $expected == ${contents[$top]} ]]
    then
      return 0
    else
      echo "not nested properly" 1>&2
      return 1
    fi
  fi
fi

}

and use it as

# Matching parentheses
declare -A matching
matching[')']='('
matching[']']='['

if [[ "(){}" == *$char* ]]; then
  echo "Wrong input: $char" 1>&2
elif [ "$char" == '(' ] || [ "$char" == "{" ]; then
  push "$char"
elif pop ${matching[$char]}; then
  : No error with pop ...
else
  : pop error ...
fi

Note that the semantics of pop ${matching[$char]} is like your original pop, but it also verifies that popped character.

2
Philippe On

Using nameref (declare -n) :

pop() { 
    declare -n local_var=$1
    if is_empty; then
        echo "parentheses aren't matched" >&2
        return 1
    else
        local_var="${contents[--top]}"
    fi
}

To use it, instead of [ "$(pop)" != '(' ], call like this :

pop var && [ "$var" != '(' ]
2
Walter A On

Catching the output of a subshell is hard.
You can try using a temporary file (or a named pipe), but you would like to make a function for the redirect to, reading from temp file and removing that file. And with this new function, you will have the same problem, how to get the output of that function into a variable.
Changing the pop() function is a possibility, as given in some other answers.
When you don't want to change your pop(), you can use

elif [ "$char" == ')' ] && pop > /dev/null && [ "${contents[$top]}" != '(' ]; then

The redirection > /dev/null is because of the echo "${contents[$top]}" in pop().

When you do want to make some more small changes in your code, remove the echo from pop() and consider using case "$char":

case "$char" in
  '('|'{')
    push "$char"
    ;;
  ')')
    pop && [ "${contents[$top]}" != '(' ] || echo "not nested properly"
    ;;
  '}')
    pop && [ "${contents[$top]}" != '{' ] || echo "not nested properly"
    ;;
  *)
    if [ "$i" -eq 0 ]; then
      if is_empty; then
        echo "matched"
        break
      else
        echo "Not nested properly"
      fi
    fi
    ;;
  esac