bash script array variables in for loop - linux

I am trying to write bash script that get 3 arguments of paths.
for ex /tmp/1 /tmp/2 /tmpnew
I want to iterate over the argument except the last one and each time copy the file to the path of the last argument.
I have problem with echo '${files[$(($len))]}' inside the for. I cant pull the last argument like that.
files=( "$#" )
len=${#files[#]}
echo $len
for (( i=0; i<$(( $len -1 )); i++ ))
do
echo ${files[$(($len))]}
echo ${files[$i]}
done

The last element is ${files[len-1]}, or simply ${files[-1]}.
Similarly, you can use just ${files[i]}. If the array is not associative, bash interprets the index as an arithmetic expression.
#!/bin/bash
files=("$#")
len=${#files[#]}
echo $len
for (( i=0; i<len-1; i++ )) ; do
echo "${files[-1]}"
echo "${files[i]}"
done

Related

Why variable changed after pipe?

The related stub is like:
tag=('*' '#')
i=0
function output()
{
ifs="$IFS"
IFS=$'\n'
for line in $#
do
echo $'\t' "${tag[$i]}" $line
done
IFS="$ifs"
echo $i
i=$((i+1))
echo $i
i=$((i%2))
echo $i
}
output a|tee README
output b
What I want to do is:
Every time execute output to output a message block, different prefix(${tag[$ind]}) can be used for distinguishing itself from context. Besides, part-message can be redirect to file.
Result of it is:
* a
0
1
1
* b
0
1
1
With the pipe |tee README, variable $i had been reset to 0.
Why it happened and can I implement the function by this train of thought?
Thanks.
It happens becase, as stated at Bash manual, each command in a pipeline is executed as a separate process (i.e., in a subshell).
In order to preserve i variable value I suggest you to enclose the two output calls into a single shell process as follow:
#!/bin/bash
tag=('*' '#')
i=0
function output()
{
ifs="$IFS"
IFS=$'\n'
for line in $#
do
echo $'\t' "${tag[$i]}" $line
done
IFS="$ifs"
echo $i
i=$((i+1))
echo $i
i=$((i%2))
echo $i
}
(
output a
output b
) | tee README

Linux script reading an ini file and splitting into variables by a specified character

I'm stuck in the following task: Lets pretend we have an .ini file in a folder. The file contains lines like this:
eno1=10.0.0.254/24
eno2=172.16.4.129/25
eno3=192.168.2.1/25
tun0=10.10.10.1/32
I had to choose the biggest subnet mask. So my attempt was:
declare -A data
for f in datadir/name
do
while read line
do
r=(${line//=/ })
let data[${r[0]}]=${r[1]}
done < $f
done
This is how far i got. (Yeah i know the file named name is not an .ini file but a .txt since i got problem even with creating an ini file,this teacher didn't even give a file like that for our exam.)
It splits the line until the =, but doesn't want to read the IP number because of the (first) . character.
(Invalid arithmetic operator the error message i got)
If someone could help me and explain how i can make a script for tasks like this i would be really thankful!
Both previously presented solutions operate (and do what they're designed to do); I thought I'd add something left-field as the specifications are fairly loose.
$ cat freasy
eno1=10.0.0.254/24
eno2=172.16.4.129/25
eno3=192.168.2.1/25
tun0=10.10.10.1/32
I'd argue that the biggest subnet mask is the one with the lowest numerical value (holds the most hosts).
$ sort -t/ -k2,2nr freasy| tail -n1
eno1=10.0.0.254/24
Don't use let. It's for arithmetic.
$ help let
let: let arg [arg ...]
Evaluate arithmetic expressions.
Evaluate each ARG as an arithmetic expression.
Just use straight assignment:
declare -A data
for f in datadir/name
do
while read line
do
r=(${line//=/ })
data[${r[0]}]=${r[1]}
done < $f
done
Result:
$ declare -p data
declare -A data=([tun0]="10.10.10.1/32" [eno1]="10.0.0.254/24" [eno2]="172.16.4.129/25" [eno3]="192.168.2.1/25" )
awk provides a simple solution to find the max value following the '/' that will be orders of magnitude faster than a bash script or Unix pipeline using:
awk -F"=|/" '$3 > max { max = $3 } END { print max }' file
Example Use/Output
$ awk -F"=|/" '$3 > max { max = $3 } END { print max }' file
32
Above awk separates the fields using either '=' or '/' as field separator and then keeps the max of the 3rd field $3 and outputs that value using the END {...} rule.
Bash Solution
If you did want a bash script solution, then you can isolate the wanted parts of each line using [[ .. =~ .. ]] to populate the BASH_REMATCH array and then compare ${BASH_REMATCH[3]} against a max variable. The [[ .. ]] expression with =~ considers everything on the right side an Extended Regular Expression and will isolate each grouping ((...)) as an element in the array BASH_REMATCH, e.g.
#!/bin/bash
[ -z "$1" ] && { printf "filename required\n" >&2; exit 1; }
declare -i max=0
while read -r line; do
[[ $line =~ ^(.*)=(.*)/(.*)$ ]]
((${BASH_REMATCH[3]} > max)) && max=${BASH_REMATCH[3]}
done < "$1"
printf "max: %s\n" "$max"
Using Only POSIX Parameter Expansions
Using parameter expansion with substring removal supported by POSIX shell (Bourne shell, dash, etc..), you could do:
#!/bin/sh
[ -z "$1" ] && { printf "filename required\n" >&2; exit 1; }
max=0
while read line; do
[ "${line##*/}" -gt "$max" ] && max="${line##*/}"
done < "$1"
printf "max: %s\n" "$max"
Example Use/Output
After making yourscript.sh executable with chmod +x yourscript.sh, you would do:
$ ./yourscript.sh file
max: 32
(same output for both shell script solutions)
Let me know if you have further questions.

How to add called variable as a suffix to another variable and then call the suffixed variable?

My script:
for (( i=1; i <= $j; i++ ))
do
list_$i = $i
echo "$list_$i"
done
Expected output:
1
2
3
.
.
.
etc
I have a problem with the echo statement while calling the variable.
Please help me on this.
Assuming that $j has an nonnegative integral value,
for (( i=1; $i<=$j; i=$i+1 ))
do
list[$i]=$i
echo "${list[$i]}"
done
Bash arrays are used, whereby $list is a single structure, a Bash array.
First remember that a variable assignment is without spaces around the =.
What you are trying to do, is possible but complicated.
for (( i=1; i <= 6; i++ )); do
source <(echo "list_$i=$i")
varname=list_$i
echo "${!varname}"
done
You can also view the results in a different loop
for result in list_{1..6}; do
echo "${result}=${!result}"
done

How to echo arguments in loop in bash

I have a bash that should be run in this way:
./script.sh <arg1> <arg2> <arg3>...<argn>
I want to show these args in my bash:
<arg3> <arg4> ... <argn>
So I wrote this bash:
for (( i=1; i<=$#-3; i++ ))
do
echo $((3+i))
done
but it shows me number of args.
How can I put # in order to see my real args?
Thanks
If you want to show arguments starting from arg3, you can simply use
echo "${#:3}" # OR
printf "%s\n" "${#:3}"
If you really want to show argument indices, use
for (( i=3; i < $#; i++)); do
echo $i
done
You can store all arguments in a BASH array and then use them for processing later:
args=( "$#" )
for (( i=2; i<${#args[#]}; i++ ))
do
echo "arg # $((i+1)) :: ${args[$i]}"
done
A minimal solution that displays the desired arguments without the math:
shift 2
for word
do
echo ${word}
done
I prefer #anubhava's solution of storing the arguments in an array, but to make your original code work, you could use eval:
for ((i=1;i<=$#;i++)); do
eval echo "\$$i"
done
After your all good answers I found this solution that works well for my thread:
ARG=( $(echo "${#:3}") )
for (( i=1; i<=$#-3; i++ ))
do
echo ${ARG[i]}
done

Bash reading txt file and storing in array

I'm writing my first Bash script, I have some experience with C and C# so I think the logic of the program is correct, it's just the syntax is so complicated because apparently there are many different ways to write the same thing!
Here is the script, it simply checks if the argument (string) is contained in a certain file. If so it stores each line of the file in an array and writes an item of the array in a file. I'm sure there must be easier ways to achieve that but I want to do some practice with bash loops
#!/bin/bash
NOME=$1
c=0
#IF NAME IS FOUND IN THE PHONEBOOK THEN STORE EACH LINE OF THE FILE INTO ARRAY
#ONCE THE ARRAY IS DONE GET THE INDEX OF MATCHING NAME AND RETURN ARRAY[INDEX+1]
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY"
while read line
do
myArray[$c]=$line # store line
c=$(expr $c + 1) # increase counter by 1
done < /root/phonebook.txt
else
echo "Name not found"
fi
c=0
for i in myArray;
do
if myArray[$i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
This code returns the only the second item of myArray (myArray[2]) or the second line of the file, why?
The first part (where you build the array) looks ok, but the second part has a couple of serious errors:
for i in myArray; -- this executes the loop once, with $i set to "myArray". In this case, you want $i to iterate over the indexes of myArray, so you need to use
for i in "${!myArray[#]}"
or
for ((i=0; i<${#a[#]}; i++))
(although I generally prefer the first, since it'll work with noncontiguous and associative arrays).
Also, you don't need the ; unless do is on the same line (in shell, ; is mostly equivalent to a line break so having a semicolon at the end of a line is redundant).
if myArray[$i]="$NOME" ; then -- the if statement takes a command, and will therefore treat myArray[$i]="$NOME" as an assignment command, which is not at all what you wanted. In order to compare strings, you could use the test command or its synonym [
if [ "${myArray[i]}" = "$NOME" ]; then
or a bash conditional expression
if [[ "${myArray[i]}" = "$NOME" ]]; then
The two are very similar, but the conditional expression has much cleaner syntax (e.g. in a test command, > redirects output, while \> is a string comparison; in [[ ]] a plain > is a comparison).
In either case, you need to use an appropriate $ expression for myArray, or it'll be interpreted as a literal. On the other hand, you don't need a $ before the i in "${myArray[i]}" because it's in a numeric expression context and therefore will be expanded automatically.
Finally, note that the spaces between elements are absolutely required -- in shell, spaces are very important delimiters, not just there for readability like they usually are in c.
1.-This is what you wrote with small adjustments
#!/bin/bash
NOME=$1
#IF NAME IS FOUND IN THE PHONE-BOOK **THEN** READ THE PHONE BOOK LINES INTO AN ARRAY VARIABLE
#ONCE THE ARRAY IS COMPLETED, GET THE INDEX OF MATCHING LINE AND RETURN ARRAY[INDEX+1]
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
IFS= while read -r line #IFS= in case you want to preserve leading and trailing spaces
do
myArray[c]=$line # put line in the array
c=$((c+1)) # increase counter by 1
done < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
else
echo "Name not found"
fi
2.-But you can also read the array and stop looping like this:
#!/bin/bash
NOME=$1
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
readarray myArray < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
break # stop looping
fi
done
else
echo "Name not found"
fi
exit 0
3.- The following improves things. Supposing a)$NAME matches the whole line that contains it and b)there's always one line after a $NOME found, this will work; if not (if $NOME can be the last line in the phone-book), then you need to do small adjustments.
!/bin/bash
PHONEBOOK="/root/phonebook.txt"
NUMBERTOCALL="/root/numbertocall.txt"
NOME="$1"
myline=""
myline=$(grep -A1 "$NOME" "$PHONEBOOK" | sed '1d')
if [ -z "$myline" ]; then
echo "Name not found :-("
else
echo -n "$NOME FOUND.... "
echo "$myline" >> "$NUMBERTOCALL"
echo " .... AND SAVED! :-)"
fi
exit 0

Resources