bash script function scope - linux

function generateFileList {
for entry in "$ORIGINATION_PATH"/*
do
entry=${entry%.*} # retain the part before the dot
entry=${entry##*/} # retain the part after the last slash
if [ $(contains "${FILENAME[#]}" $entry) == "n" ]; then
FILENAME[$fn_counter]=$entry
fn_counter=(expr $fn_counter + 1)
echo $entry "added to filelist"
echo ${FILENAME[$fn_counter]}
fi
done
NUMBER_OF_FILES=$(expr ${#FILENAME[#]} + 1)}
I have this function .My $ORIGINATION_PATH has many files in it. However, when I call this function my $FILENAME array gets populated only with one entry.Why? Inside the function everything seems fine, and it seems that $FILENAME array gets all the values it needs to get, but when I check outside the function I only get one value in the $FILENAME aray

Problems with your code and suggestions for improvement:
You should initialize ${FILENAME[#]} to an empty array (either in the function itself if you always want the function to generate a new list of files from scratch, or before calling the function if you want to be able to build up a composite list of files by calling the function repeatedly on different base directories).
You should initialize $fn_counter to zero before starting the loop. Or, for the composite build-up idea, to the number of elements currently in ${FILENAME[#]}. Actually, another, perhaps preferable solution, would be to remove the $fn_counter variable entirely and replace it with ${#FILENAME[#]}, since it should always be equal to that value.
In the line fn_counter=(expr $fn_counter + 1), you're assigning $fn_counter to an array, rather than incrementing it. This is because you forgot the dollar before the open parenthesis. If you ran fn_counter=$(expr $fn_counter + 1) then it would work. But there's a better way to increment a numeric variable: let ++fn_counter.
You don't have to dollar-prefix variables in arithmetic expressions. So, for example, we can say ${FILENAME[fn_counter]} instead of ${FILENAME[$fn_counter]}.
You're trying to echo the element of ${FILENAME[#]} that was just added in the current iteration, but indexing it with $fn_counter after it was incremented, which is incorrect. You can solve this by subtracting 1 from it, i.e. echo "${FILENAME[fn_counter-1]}". Or, if removing $fn_counter, echo "${FILENAME[${#FILENAME[#]}-1]}".
When assigning $NUMBER_OF_FILES, I don't know why you're adding 1 to ${#FILENAME[#]}. The number of elements in the ${FILENAME[#]} array should be equal to the number of files, without requiring an increment, no? I recommend removing this variable entirely, since the value can be accessed directly as ${#FILENAME[#]}.
I recommend you pass inputs as arguments (e.g. pass $ORIGINATION_PATH as an argument) and use the local keyword to reduce the likelihood of variable clashes between functions. Globals are the default in bash, which creates dangerous possibilities for different functions to step on each others' toes. For example, imagine if the contains function (assuming it's a shell function) assigned a value to the global $entry variable.
I recommend always using the [[ command rather than [, as it's more powerful, and it's good to be consistent.
As written, your script won't work correctly on an empty directory. You could test in advance if the directory is empty (e.g. [[ -n "$(find "$ORIGINATION_PATH" -maxdepth 0 -empty)" ]]). Another solution is to set nullglob. Another solution is to skip glob words that don't actually exist (e.g. if [[ ! -e "$entry" ]]; then continue; fi;).
Always double-quote variable expansions to protect against word splitting, which takes place after variable expansion. For example, the contains call should be contains "${FILENAME[#]}" "$entry" (notice the double-quoting around $entry). The only exceptions are (1) when assigning a string variable to a string variable, i.e. new=$old, in which case you don't have to quote it, and (2) when expanding a numeric variable, which is guaranteed not to be corrupted by word splitting.
Here's a working solution, filling in the missing pieces:
function contains {
local target="${#:$#:1}";
local -a array=("${#:1:$#-1}");
local elem='';
for elem in "${array[#]}"; do
if [[ "$elem" == "$target" ]]; then
echo 'y';
return;
fi;
done;
echo 'n';
} ## end contains()
function generateFileList {
local path="$1";
local entry='';
for entry in "$path"/*; do
if [[ ! -e "$entry" ]]; then continue; fi;
entry=${entry%.*}; ## retain the part before the dot
entry=${entry##*/}; ## retain the part after the last slash
if [[ "$(contains "${FILENAME[#]}" "$entry")" == 'n' ]]; then
FILENAME[${#FILENAME[#]}]=$entry;
echo "$entry added to filelist";
echo "${FILENAME[${#FILENAME[#]}-1]}";
fi;
done;
} ## end generateFileList()
ORIGINATION_PATH='...';
FILENAME=(); ## build up result on global ${FILENAME[#]} var
generateFileList "$ORIGINATION_PATH";
echo "\${#FILENAME[#]} == ${#FILENAME[#]}";
echo "\${FILENAME[#]} == (${FILENAME[#]})";

Related

With shell, how to extract (to separate variables) values that are surrounded by "=" and space?

For example, I have a string /something an-arg=some-value another-arg=another-value.
What would be the most straightforward way to extract an-arg's value to a variable and another-arg's value to another variable?
To better exemplify, this is what I need to happen:
STRING="/something an-arg=some-value another-arg=another-value"
AN_ARG=... # <-- do some magic here to extract an-arg's value
ANOTHER_ARG=... # <-- do some magic here to extract another-arg's value
echo $AN_ARG # should print `some-value`
echo $ANOTHER_ARG # should print `another-value`
So I was looking for a simple/straightforward way to do this, I tried:
ARG_NAME="an-arg="
AN_ARG=${STRING#*$ARG_NAME}
But the problem with this solution is that it will print everything that comes after an-arg, including the second argument's name and its value, eg some-value another-arg=another-value.
Letting data set arbitrary variables incurs substantial security risks. You should either prefix your generated variables (with a prefix having at least one lower-case character to keep the generated variables in the namespace POSIX reserves for application use), or put them in an associative array; the first example below does the latter.
Generating An Associative Array
As you can see at https://ideone.com/cKcMSM --
#!/usr/bin/env bash
# ^^^^- specifically, bash 4.0 or newer; NOT /bin/sh
declare -A vars=( )
re='^([^=]* )?([[:alpha:]_-][[:alnum:]_-]+)=([^[:space:]]+)( (.*))?$'
string="/something an-arg=some-value another-arg=another-value third-arg=three"
while [[ $string =~ $re ]]; do : "${BASH_REMATCH[#]}"
string=${BASH_REMATCH[5]}
vars[${BASH_REMATCH[2]}]=${BASH_REMATCH[3]}
done
declare -p vars # print the variables we extracted
...correctly emits:
declare -A vars=([another-arg]="another-value" [an-arg]="some-value" [third-arg]="three" )
...so you can refer to ${vars[an-arg]}, ${vars[another-arg]} or ${vars[third-arg]}.
This avoids faults in the original proposal whereby a string could set variables with meanings to the system -- changing PATH, LD_PRELOAD, or other security-sensitive values.
Generating Prefixed Names
To do it the other way might look like:
while [[ $string =~ $re ]]; do : "${BASH_REMATCH[#]}"
string=${BASH_REMATCH[5]}
declare -n _newVar="var_${BASH_REMATCH[2]//-/_}" || continue
_newVar=${BASH_REMATCH[3]}
unset -n _newVar
declare -p "var_${BASH_REMATCH[2]//-/_}"
done
...which work as you can see at https://ideone.com/zUBpsC, creating three separate variables with a var_ prefix on the name of each:
declare -- var_an_arg="some-value"
declare -- var_another_arg="another-value"
declare -- var_third_arg="three"
Assumptions:
OP understands all the issues outlined by Charles Duffy but still wants standalone variables
all variables names to be uppercased
hyphens (-) converted to underscores (_)
neither variable names nor the associated values contain embedded white space
One bash idea using namerefs:
unset newarg AN_ARG ANOTHER_ARG 2>/dev/null
STRING="/something an-arg=some-value another-arg=another-value"
read -ra list <<< "${STRING}" # read into an array; each space-delimited item is a new entry in the array
#typeset -p list # uncomment to display contents of the list[] array
regex='[^[:space:]]+=[^[:space:]]+' # search pattern: <var>=<value>, no embedded spaces in <var> nor <value>
for item in "${list[#]}" # loop through items in list[] array
do
if [[ "${item}" =~ $regex ]] # if we have a pattern match (<var>=<val>) then ...
then
IFS="=" read -r ndx val <<< "${BASH_REMATCH[0]}" # split on '=' and read into variables ndx and val
declare -nu newarg="${ndx//-/_}" # convert '-' to '_' and assign uppercased ndx to nameref 'newarg'
newarg="${val}" # assign val to newarg
fi
done
This generates:
$ typeset -p AN_ARG ANOTHER_ARG
declare -- AN_ARG="some-value"
declare -- ANOTHER_ARG="another-value"
NOTE:
once the for loop processing has completed, accessing the new variables will require some foreknowledge of the new variables' names
using an associative array to manage the list of new variables makes post for loop accessing quite a bit easier (eg, the new variable names are simply the indices of the associative array)

How do you compare the value of an array to a variable in bash script?

I'm practicing bash and honestly, it is pretty fun. However, I'm trying to write a program that compares an array's value to a variable and if they are the same then it should print the array's value with an asterisk to the left of it.
#!/bin/bash
color[0]=red
color[1]=blue
color[2]=black
color[3]=brown
color[4]=yellow
favorite="black"
for i in {0..4};do echo ${color[$i]};
if {"$favorite"=$color[i]}; then
echo"* $color[i]"
done
output should be *black
There's few incorrect statements in your code that prevent it from doing what you ask it to. The comparison in bash is done withing square brackets, leaving space around them. You correctly use the = for string comparison, but should enclose in " the string variable. Also, while you correctly address the element array in the echo statement, you don't do so inside the comparison, where it should read ${color[$i]} as well. Same error in the asterisk print. So, here a reworked code with the fixes, but read more below.
#!/bin/bash
color[0]=red
color[1]=blue
color[2]=black
color[3]=brown
color[4]=yellow
favorite=black
for i in {0..4};do
echo ${color[$i]};
if [ "$favorite" = "${color[$i]}" ]; then
echo "* ${color[$i]}"
fi
done
While that code works now, few things that probably I like and would suggest (open to more expert input of course by the SO community): always enclose strings in ", as it makes evident it is a string variable; when looping an array, no need to use index variables; enclose variables always within ${}.
So my version of the same code would be:
#!/bin/bash
color=("red" "blue" "black" "brown" "yellow")
favorite="black"
for item in ${color[#]}; do
echo ${item}
if [ "${item}" = "${favorite}" ]; then
echo "* $item"
fi
done
And a pointer to the great Advanced Bash-Scripting Guide here: http://tldp.org/LDP/abs/html/

Access a variable referred back twice UNIX

This is a simple question, but I have the variables
classNext1="Exists!"
classNext2="Exists!"
classNext3="Does not exist yet!"
classNext4="Does not exist yet!"
I am trying to figure out which variable is next in order, the next variable that wouldn't equal "Exists!"
I have this while loop
i=0
while [ $i -lt 10 ]
do
i=`expr $i + 1` # Increment i
temp="extraClass$i"
if [ "$($temp)" != 'Exists!' ];then
echo "SUCUDESSS"
nextClass="extraClass$i"
break
fi
echo $temp
done
The next class in the list would be classNext3, but it would just go and assign nextClass=nextClass1.
So the if statement would always be true in the first iteration. The problem is that $temp equals extraClass$i, it wouldn't actually equal the value of $extraClass($i) at the i th iteration. How would I structure it so that the if statement would actually get the value of $extraClass($i) instead of the literal string"extraClass($i)"??
To use the value of a variable as the name of a variable to access, use variable indirection, as explained here.
if [ "${!temp}" != 'Exists!' ]; then
But whenever you find yourself naming variables with numerical suffixes, and wanting to access them dynamically, that practically always means you should be using an array instead of separate variables. This goes for just about any programming language.

Shell Programming: Access Element of List

It is my understanding that when writing a Unix shell program you can iterate through a string like a list with a for loop. Does this mean you can access elements of the string by their index as well?
For example:
foo="fruit vegetable bread"
How could I access the first word of this sentence? I've tried using brackets like the C-based languages to no avail, and solutions I've read online require regular expressions, which I would like to avoid for now.
Pass $foo as argument to a function. Than you can use $1, $2 and so on to access the corresponding word in the function.
function try {
echo $1
}
a="one two three"
try $a
EDIT: another better version is:
a="one two three"
b=( $a )
echo ${b[0]}
EDIT(2): have a look at this thread.
Using arrays is the best solution.
Here's a tricky way using indirect variables
get() { local idx=${!#}; echo "${!idx}"; }
foo="one two three"
get $foo 1 # one
get $foo 2 # two
get $foo 3 # three
Notes:
$# is the number of parameters given to the function (4 in all these cases)
${!#} is the value of the last parameter
${!idx} is the value of the idx'th parameter
You must not quote $foo so the shell can split the string into words.
With a bit of error checking:
get() {
local idx=${!#}
if (( $idx < 1 || $idx >= $# )); then
echo "index out of bounds" >&2
return 1
fi
echo "${!idx}"
}
Please don't actually use this function. Use an array.

Syntax error: operand expected when using Bash

I have two arrays that I want to loop in. I construct those properly and before going into for loop, I do echo them to be sure everything is ok with arrays.
But when I run the script, it outputs an error:
l<=: syntax error: operand expected (error token is "<="
I consulted the mighty Google and I understood it suffers from the lack of the second variable, but I mentioned earlier I do echo the values and everything seems to be OK. Here is the snippet..
#!/bin/bash
k=0
#this loop is just for being sure array is loaded
while [[ $k -le ${#hitEnd[#]} ]]
do
echo "hitEnd is: ${hitEnd[k]} and hitStart is: ${hitStart[k]}"
# here outputs the values correct
k=$((k+1))
done
k=0
for ((l=${hitStart[k]};l<=${hitEnd[k]};l++)) ; do //this is error line..
let array[l]++
k=$((k+1))
done
The variables in the for loop are echoed correctly but for loop won't work.. where am I wrong?
#
as gniourf_gniourf answered:
"... At some point, k will reach the value ${#hitEnd[#]}, and this is
exactly when hitEnd[k] is not defined and expands to an empty string!
Bang!"
meaning error output is displayed not at the beginning of the loop, but when k has a greater value than array's indices, pointing an index that array does not include...
That's because at some point ${hitEnd[k]} expands to nothing (it is undefined). I get the same error with ((l<=)). You should write your for loop as:
k=0
for ((l=${hitStart[0]};k<${#hitEnd[#]} && l<=${hitEnd[k]};l++)); do
so as to always have an index k that corresponds to a defined field in the array ${hitEnd[#]}.
Also, instead of
k=$((k+1))
you can just write
((++k))
Done!
Your script revised using better modern bash practice:
#!/bin/bash
k=0
#this loop is just for being sure array is loaded
while ((k<=${#hitEnd[#]})); do
echo "hitEnd is: ${hitEnd[k]} and hitStart is: ${hitStart[k]}"
# here outputs the values correct
((++k))
done
k=0
for ((l=hitStart[0];k<${#hitEnd[#]} && l<=hitEnd[k];++l)); do
((++array[l]))
((++k))
done
Now, I'm not too sure the for loop does exactly what you want it to... Don't you mean this instead?
#!/bin/bash
# define arrays hitStart[#] and hitEnd[#]...
# define array array[#]
#this loop is just for being sure array is loaded
for ((k=0;k<${#hitEnd[#]};++k)); do
echo "hitEnd is: ${hitEnd[k]} and hitStart is: ${hitStart[k]}"
# here outputs the values correct
((++k))
done
for ((k=0;k<${#hitEnd[#]};++k)); do
for ((l=hitStart[k];l<=hitEnd[k];++l)); do
((++array[l]))
done
done
A bit bandaid-y, but you rewrite your for-loop into a while loop:
l="${hitStart[k]}"
while [[ "$l" -le "${hitEnd[k]}" ]]; do
let array[l]++
k=$((k+1))
l=$((l+1))
done

Resources