Shell Programming: Access Element of List - string

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.

Related

Get variable name while iterating over array in bash

What I have is an array with some variables. I can iterate to get the values of those vars but what I need is actually their names (values will be used elsewhere).
Going with var[i] won't work cause I will have different names. I guess I could workaround this by creating another array with the names - something similar to this:
Getting variable values from variable names listed in array in Bash
But I'm wondering if there is a better way to do this.
var1=$'1'
var2=$'2'
var3=$'3'
Array=( $var1 $var2 $var3)
for ((i=0; i<${#Array[#]}; i++))
do
echo ${Array[i]}
done
Is:
>1
>2
>3
Should be:
>var1
>var2
>var3
It sounds like you want an associative array.
# to set values over time
declare -A Array=( ) || { echo "ERROR: Need bash 4.0 or newer" >&2; exit 1; }
Array[var1]=1
Array[var2]=2
Array[var3]=3
This can also be assigned at once:
# or as just one assignment
declare -A Array=( [var1]=1 [var2]=2 [var3]=3 )
Either way, one can iterate over the keys with "${!Array[#]}", and retrieve the value for a key with ${Array[key]}:
for var in "${!Array[#]}"; do
val="${Array[$var]}"
echo "$var -> $val"
done
...will, after either of the assignments up top, properly emit:
var1 -> 1
var2 -> 2
var3 -> 3
What about this solution?
#!/bin/bash
var1=$'1'
var2=$'2'
var3=$'3'
Array=( var1 var2 var3 )
for var in "${Array[#]}"; do
echo "$var = ${!var}"
done
The idea just consists in putting your variable names in the array, then relying on the indirection feature of Bash.
But as pointed out by #CharlesDuffy, the use of associative arrays sounds better adapted to the OP's use case.
Also, this related article may be worth reading: How can I use variable variables… or associative arrays?

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/

bash script function scope

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[#]})";

Bash scripting - Iterating through "variable" variable names for a list of associative arrays

I've got a variable list of associative arrays that I want to iterate through and retrieve their key/value pairs.
I iterate through a single associative array by listing all its keys and getting the values, ie.
for key in "${!queue1[#]}" do
echo "key : $key"
echo "value : ${queue1[$key]}"
done
The tricky part is that the names of the associative arrays are variable variables, e.g. given count = 5, the associative arrays would be named queue1, queue2, queue3, queue4, queue5.
I'm trying to replace the sequence above based on a count, but so far every combination of parentheses and eval has not yielded much more then bad substitution errors. e.g below:
for count in {1,2,3,4,5} do
for key in "${!queue${count}[#]}" do
echo "key : $key"
echo "value : ${queue${count}[$key]}"
done
done
Help would be very much appreciated!
The difficulty here stems from the fact that the syntax for indirect expansion (${!nameref}) clashes with the syntax for extracting keys from an associative arrays (${!array[#]}). We can have only one or the other, not both.
Wary as I am about using eval, I cannot see a way around using it to extract the keys of an indirectly referenced associative array:
keyref="queue${count}[#]"
for key in $(eval echo '${!'$keyref'}'); do ... ; done
You can however avoid eval and use indirect expansion when extracting a value from an array given the key. Do note that the [key] suffix must be part of the expansion:
valref="queue${count}[$key]"
echo ${!valref}
To put this in context:
for count in {1..5} ; do
keyref="queue${count}[#]"
for key in $(eval echo '${!'$keyref'}'); do
valref="queue${count}[$key]"
echo "key = $key"
echo "value = ${!valref}"
done
done
I was able to make it work with the following script:
for count in {1..5} ; do
for key in $(eval echo '${!q'$count'[#]}') ; do
eval echo '${q'$count"[$key]}"
done
done
Note it breaks if any key contained a space. If you want to deal with complex data structures, use a more powerful language like Perl.
I think this might work (but untested). The key is to treat the indexing
as the full name of a variable. (That is, the array queue5 can be
treated as a sequence of variables named queue5[this], queue5[that], etc.)
for count in {1,2,3,4,5} do
assoc="queue$count[#]"
for key in "${!assoc}" do
echo "key : $key"
val="queue$count[$key]"
echo "value : ${!val}"
done
done

Check and modify format of variable in expect script

I am trying to verify that the format of a variable is a number and is at least 10 digits long with leading zeros, inside of an expect script.
In a bash script it would look something like this:
[[ "$var" != +([0-9]) ]] && echo "bad input" && exit
while [[ $(echo -n ${var} | wc -c) -lt 10 ]] ; do var="0${var}" ; done
For the following input:
16
I am trying to achieve the following output:
0000000016
The simplest way to check whether a variable has just digits is to use a regular expression. Expect's regular expressions are entirely up to the task:
if {![regexp {^\d+$} $var]} {
puts "bad input"
exit
}
Padding with zeroes is best done by formatting the value; if you know C's printf(), you'll recognize the format:
set var [format "%010d" $var]
Expect is actually just an extension of TCL, so you can use any facility that TCL provides. TCL is an unusual language, but it's not hard to do what you want.
# Set a test string.
set testvar 1234567890
# Store the match (if any) in matchvar.
regexp {\d{10,}} $testvar matchvar
puts $matchvar
# Test that matchvar holds an integer.
string is integer $matchvar
The string is command is relatively new, so you might have to rely on the return value of regexp if your TCL interpreter doesn't support it.

Resources