LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Problem with getting array from function (bash) (https://www.linuxquestions.org/questions/programming-9/problem-with-getting-array-from-function-bash-756635/)

DrLove73 09-20-2009 06:20 PM

Problem with getting array from function (bash)
 
Hi to all,

I have an problem with making changes to the array that is populated inside the function. I need to use it in main script and further functions.

I use CentOS 5.3 with bash 3.2.25.

Problem is when I finish "while read" multiple lines from grep output, I loose all changes made to the array menuitems (not menuitem which is local array).

How can I retrieve that array from function, or write directly to global array so element are not lost after the while loop?


Code:

#!/bin/bash


declare -a menuitems

LoadMenus () {
echo "Menu's:"
echo ""

OLD_IFS=$IFS # Store the original input field separator

declare -a tempmenuitem
declare -a menuitem

# Load all relevant data into array
string=$1
menuitem=($@)
echo "Local menuitem is "${menuitem[*]}

mojindex=0
totalindex=0
echo "Local menuitems is "${menuitems[*]}
echo "Local menuitem is "${menuitem[*]}

grep -e "menuitem," $UnitsDatabaseFile | while read menuline
do
  IFS=','
  # Load the comma separated fields into array
  tempmenuitem=(`echo "$menuline"`)
...
...
...
      menuitem[num]=${tempmenuitem[i]}
      menuitems[num]=${tempmenuitem[i]}
      echo -n "Item of menuitems $num je '" ${menuitem[num]} "'; "
  done
  echo "" 
  mojindex=$(($mojindex+1))

  echo "Local menuitem after is "${menuitem[*]}
  echo "Local tempmenuitem after is "${tempmenuitem[*]}
  echo "Local menuitems after is "${menuitems[*]} # So far menuitems has ~20 elements
done

# After while read loop, array menuitems is back to initial 3 elements
echo "Global menuitem after is "${menuitem[*]}
echo "Global menuitems after is "${menuitems[*]}

IFS="$OLD_IFS"
}

menuitems=(1 2 3)
echo "Menuitems before loading is "${menuitems[*]} # gives 1 2 3
LoadMenus ${menuitems[@]}
exit 0


David the H. 09-20-2009 08:32 PM

A function runs as a child process, and so all variables inside it are simply not available to the parent-level of the script. You'll need to redesign your function and the top-level command using it so that it's output sets a global array, or to otherwise generate the values you need directly.

DrLove73 09-21-2009 07:45 AM

Ok, thanks. I solved it by creating temporary file that accepts grep output:
Code:

#!/bin/bash

declare -a menuitems

LoadMenus () {
echo "Menu's:"
echo ""

OLD_IFS=$IFS # Store the original input field separator

declare -a tempmenuitem

# Load all relevant data into array
mojindex=0
totalindex=0

grep -e "linux," $UnitsDatabaseFile > $TempDatabaseFile
exec 7< $TempDatabaseFile
while read -u 7 linuxline
do
  IFS=','
  # Load the comma separated fields into array
  tempmenuitem=(`echo "$menuline"`)
...
...
...
      menuitems[num]=${tempmenuitem[i]}
      echo -n "Item of menuitems $num je '" ${menuitem[num]} "'; "
  done
  echo "" 
  mojindex=$(($mojindex+1))
done
exec 7<&- # free file descriptor 7
echo "" > $TempDatabaseFile

echo "Global menuitems after is "${menuitems[*]}

IFS="$OLD_IFS"
}

LoadMenus

exit 0

Is there any better way (avoiding temp file?) in Bash, to grab just certain lines from file and feed them to the function as separate lines while been able to return them to parent process?

slakmagik 09-21-2009 01:09 PM

Actually, with regard to functions:

Code:

:cat function
notachild() {
    var=set
}

notachild
echo parent_level: $var

:sh function
parent_level: set

which illustrates what the bash manual says:

Quote:

Functions are executed in the context of the current shell; no new process is created to interpret them
This is what the 'local' builtin is for - to restrict variables to their functions.

Your actual problem and some other ways around it in addition to your own: BashFAQ24

David the H. 09-21-2009 01:24 PM

Huh. So I was wrong then. Functions don't run as subshells. I just tested it myself, and it looks like I wasn't just wrong in my first post, but completely wrong. Variables set in functions are modified globally, unless declared locally.

Sorry about that. :(

On the other hand, at least it does look like I was right that a subshell was to blame, since the loop in question was running inside a subprocess created by a pipe. :)

On the gripping hand, I should've noticed it. I've experienced that issue before myself. :(

catkin 09-21-2009 01:39 PM

Try changing num to $num in
Code:

menuitems[num]=${tempmenuitem[i]}
There's no need to save and restore the IFS if you use this idiom
Code:

IFS=',' tempmenuitem=(`echo "$menuline"`)
It has the effect of setting IFS to ',' for the following (here assignment) command only.

There's no need for the echo in
Code:

tempmenuitem=(`echo "$menuline"`)
This will do what you want
Code:

IFS=',' tempmenuitem=( $menuline )
EDIT:

You can avoid having to use a temporary file like this
Code:

while read menuline
do
    <commands>
done < "$( grep -e "menuitem," $UnitsDatabaseFile )"

This replaces the pipe with redirection and so avoids the while loop being run in a sub-shell. The grep is run in a sub-shell but that's OK here -- it isn't setting any variables.

DrLove73 09-23-2009 05:01 AM

Thanks catkin. When you lear from examples like: "you can do this in many ways" , you are bound to lack optimization.

As for while loop, done at the end expects filename, not text, so it give error that filename is too long, and no filename lika that

"/usr/local/bin/sv3multi-db: line 464: menuitem,1,0,a,SRB,showMenuG,0,0,0,0,0,0,0" where line 464 is function start, functionname()

Any idea how to solve this? lines from greped file are:
"menuitem,1,0,a,SRB,showMenuG,0,0,0,0,0,0,0"

catkin 09-23-2009 05:51 AM

Quote:

Originally Posted by DrLove73 (Post 3693951)
As for while loop, done at the end expects filename, not text, so it give error that filename is too long, and no filename lika that

What was I thinking?! Just ignore that nonsense. Sorry :redface:

DrLove73 09-23-2009 06:53 AM

Populating array inside a loop with Multiple-line text [SOLVED]
 
Ok, I solved my problem after lurking in Advanced Bash Shell Scripting Guide with this:

Code:

#!/bin/bash

declare -a menuitems

LoadMenus () {
echo "Menu's:"
echo ""

OLD_IFS=$IFS # Store the original input field separator

declare -a tempmenuitem

# Load all relevant data into array
mojindex=0
totalindex=0

for linuxline in $( grep -e "menuitem," $UnitsDatabaseFile | sed -e '/#/d' )
do
  IFS=','
  # Load the comma separated fields into array
  tempmenuitem=( $menuline )
...
...
...
      menuitems[num]=${tempmenuitem[i]}
      echo -n "Item of menuitems $num je '" ${menuitem[num]} "'; "
  done
  echo "" 
  mojindex=$(($mojindex+1))
done
echo "Global menuitems after is "${menuitems[*]}

IFS="$OLD_IFS"
}

LoadMenus

exit 0

Stupidly simple (when you know HOW)!!! :doh:

DrLove73 09-23-2009 07:05 AM

Quote:

Originally Posted by catkin (Post 3691971)
This will do what you want
Code:

IFS=',' tempmenuitem=( $menuline )

I am sorry I must say that this does not work only for one command, but for entire script. Maybe it works for one command/script when invoked from command line, but inside the script it wrecks havoc.

Thanks anyway for all of your help. On another forum (wireless stuff) I reached ~2,800 posts in 3 years (avg 2.5 posts per day) so I know that you need willpower to help, to provide any kind of answer, not to mention finding a correct answer with scarce input.

catkin 09-23-2009 12:20 PM

Quote:

Originally Posted by DrLove73 (Post 3694060)
I am sorry I must say that this does not work only for one command, but for entire script. Maybe it works for one command/script when invoked from command line, but inside the script it wrecks havok.

Many thanks for pointing it out. Two serious mistakes in one post :redface: At least it made me look into the technique further and learn some more. See this post for the result.

DrLove73 09-23-2009 08:06 PM

It is no problem really. Since I am new to BASH, I just add A LOT of echo commands for all relevant variables after every relevant block of code and just observe the output and adjust as needed.

Also, there is nothing wrong in expanding on your(our) knowledge every day we breathe, that is how we get smarter. A matter of fact, if you had not gave me the "While read" redirection example, I would not have been able to trace it to this "for loop" elegant solution for arrays from multiple-line text, so many thanks for that.

slakmagik 09-23-2009 10:24 PM

Quote:

Originally Posted by DrLove73 (Post 3694944)
I just add A LOT of echo commands for all relevant variables after every relevant block of code and just observe the output and adjust as needed.

If you've got a specific problem area, that can be very helpful but, if you're doing a lot of that, you can just stick an '-x' on the end of your shebang line or run the script as '(ba)sh -x script', possibly redirecting stderr to a file so you can look through that.

chrism01 09-23-2009 10:25 PM

You could just put

set -xv

as the 2nd line of your code. It shows everything before and after substitutions etc as it goes.
Great for debugging.
:)

DrLove73 09-24-2009 03:02 AM

Wellllll, that is nice to know, but my current (combined) script has ~1000 lines and output definitely crosses that line. And just one of my arrays will fill ~100 rows on the screen when I populate database file. "Not everything that shines is made of gold...". :tisk: :D

I put echo lines as I see fit, and then just comment them out when I need them, parts at the time. If I change something before that point, I just uncomment them out again until I am pleased with changes. It's a best way to go.

Thanks on the tip, I've save it for future use.

P.S. I have been programming in BASIC, DOS Batch files, Pascal/Delphi, VisualBasic/Access, SQL code, and finaly BASH since 1993-1994. On and off of course, but let's say I have been arround. ;)


All times are GMT -5. The time now is 01:53 AM.