I want to assign the value of an array dynamically, and when I change the variable the array changes automatically.
var=5
arr=( $\[$var-1\] $var $\[$var+1\] )
echo ${arr\[#\]}
var=10
echo ${arr\[#\]}
result
4 5 6
4 5 6
I wanted
4 5 6
9 10 11
Try something like this:
v1=abc
v2=efd
v3=ghj
arr=( v1 v2 v3 )
for i in ${arr[#]}; { echo ${!i}; }
abc
efd
ghj
Now lets change values of those vars:
v1=123
v2=456
v3=789
$ for i in ${arr[#]}; { echo ${!i}; }
123
456
789
Elaborating Ivan's trick and applying get/set "method"-style functions -
[P2759474#sdp-bastion ~]$ cat tst
#! /bin/bash
arr=( v1 v2 v3 )
v1(){ (($1))&& var=$(($1+1)); echo $((var-1)); }
v2(){ (($1))&& var=$1; echo $var; }
v3(){ (($1))&& var=$(($1-1)); echo $((var+1)); }
var=5
for i in ${arr[#]}; { printf "%s=" $i; $i; }
v1 14
for i in ${arr[#]}; { printf "%s=" $i; $i; }
[P2759474#sdp-bastion ~]$ ./tst
v1=4
v2=5
v3=6
14
v1=14
v2=15
v3=16
While such automatic updates are perfectly common in spreadsheet applications, Bash doesn’t work that way. If you want to recalculate a few values based on formulas, you have to explicitly tell Bash to do so. The example below generates the outputs that you expect and defines a function called recalculate that you need to call after changes to the formulas or the formulas’ input variables. The rest of the trick is based around how integer evaluation works in Bash.
recalculate() {
local -n source="$1" target="$2"
target=("${source[#]}")
}
formulas=('var - 1' 'var' 'var + 1')
declare -ai arr
var=5
recalculate formulas arr
echo "${arr[#]}" # 4 5 6
var=10
recalculate formulas arr
echo "${arr[#]}" # 9 10 11
(It would be awesome if Bash had an additional pseudo-signal for the trap command, say assignment, which could work like trap 'echo "variable ${1} set to ${!1}"' assignment, but AFAIK, there is no such functionality (plus no separate argument handling in trap); hence the strikethrough. Without that kind of functionality, a function like recalculate might be the closest you can get to the updates you asked for.)
A slightly more elaborate version of recalculate could also (1) handle sparse arrays of formulas correctly, i.e. guarantee to store results under the same indices under which the corresponding formulas were found, and (2) introduce a “reserved” variable name, say index, which can occur in the formulas with an obvious meaning. Just for fun, “because we can”:
recalculate() {
local -n source="$1" target="$2"
local -i index
target=()
for index in "${!source[#]}"; do
target[index]="${source[index]}"
done
}
formulas[1]='index * var - 1'
formulas[3]='index * var'
formulas[5]='index * var + 1'
declare -ai arr
var=5
recalculate formulas arr
echo "${arr[#]#A}" # declare -ai arr=([1]="4" [3]="15" [5]="26")
var=10
recalculate formulas arr
echo "${arr[#]#A}" # declare -ai arr=([1]="9" [3]="30" [5]="51")
Related
I have string="1,2,3,4,5,6,7,8" I want to split string in 2 different variable with unique values as below
string_p1="1,2,3,4,5" string_p2="6,7,8"
Here i dont want any specific defined logic while splitting variable any random splitting is okay.
but i need to ensure that i am not missing any number present in variable string
Please suggest bash script to get the above results ?
One idea using an array and some basic bash string manipulation:
string='1,2,3,4,5,6,7,8'
IFS=, arr=( ${string} ) # break on comma delimiter, store in array
Result:
$ typeset -p arr
declare -a arr=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5" [5]="6" [6]="7" [7]="8")
$ len="${#arr[#]}"
8
To generate a random number between 0 and 8 (length of arr[]), eg:
$ n=$(( RANDOM % len ))
$ echo $n
2 # obviously this will vary between 0 and 8 based on value of $RANDOM
Build our final strings:
$ string_p1=${arr[#]:0:n}
$ string_p2=${arr[#]:n}
$ typeset -p string_p1 string_p2
declare -- string_p1="1 2"
declare -- string_p2="3 4 5 6 7 8"
Now replace spaces with commas:
$ string_p1="${string_p1// /,}"
$ string_p2="${string_p2// /,}"
$ typeset -p string_p1 string_p2
declare -- string_p1="1,2"
declare -- string_p2="3,4,5,6,7,8"
NOTE: because the split is based solely on $RANDOM there is a good chance one of the resulting strings can be empty (eg, n=0); OP can add more logic to address this issue as needed (eg, if n=0 then set n=1 to ensure string_p1 is not empty)
Taking for a spin with a different input:
string='3,24,666.83,2,9,0,34,23,45,12,1'
IFS=, arr=( ${string} )
len="${#arr[#]}"
n=$(( RANDOM % len ))
string_p1=${arr[#]:0:n}
string_p2=${arr[#]:n}
string_p1="${string_p1// /,}"
string_p2="${string_p2// /,}"
typeset -p len n string_p1 string_p2
This generates:
declare -- len="11"
declare -- n="8"
declare -- string_p1="3,24,666.83,2,9,0,34,23"
declare -- string_p2="45,12,1"
I tried with this:
string="1,2,3,4,5,6,7,8"
echo ${string:0:${#string}/2}
echo ${string:${#string}/2}
and it splits the string in half, this is the output:
1,2,3,4
,5,6,7,8
Taking count from file, say if count = 5, I want to print 5 variables. i.e. A B C D E.
If count = 2, Print 2 variables A B, etc.
I have tried using the ASCII values but couldn't go through it.
for i in {1..5}; do
count=5; a=0;
printf "\x$(printf %x '65+$a')";
count=count+1;
done
if count = 5, I want to print 5 variables. i.e. A B C D E. If count = 2, Print 2 variables A B, etc.
Here's a program that matches your style that does what you are looking for:
a=0
for i in {1..5}; do
printf "\x$(printf %x $(( 65 + a )) )";
a=$((a+1));
done
The first thing to note is that in order to do math in bash, you'll need to use the $(( )) operation. Above, you can see I replaced you '65+$a' with $(( 65 + a )) . That's the big news that you need to get math done.
There were a couple of other little issues, but you were stuck on the $(()) stuff so they weren't clear yet. Incidentally, the 'a' variable can be completely removed from the program to just use the 'i' variable like this:
for i in {1..5}; do
printf "\x$(printf %x $(( 64 + i )) )";
done
I had to change the constant to 64 since we are now counting starting at 1.
The {1..5} expression is a good short cut for 1 2 3 4 5, but you won't be able to put a variable into it. So, if you need to add a count variable back in, consider using the seq program instead like this:
count=$1
for i in $(seq 1 $count); do
printf "\x$(printf %x $(( 64 + i )) )";
done
Note that $() is different than the math operator $(()). $() runs a subcommand returning the results.
method 1: simple brace expansion
#!/bin/bash
# generate a lookup table
vars=( - $(echo {A..Z}) )
# use the elements
for i in {1..5}; do
echo ${vars[$i]}
done
{A..Z} generates 26 strings: A, B, ..., Z
which get stored in an array variable by vars=(...)
we prepend a - that we'll ignore
we can then do 1-based indexing into the array
limited to 26 variables (or whatever range we choose)
method 2: multiple brace expansion to generate arbitrary long variables
#!/bin/bash
if [[ ! $1 =~ ^[0-9]+$ ]]; then
echo "Usage: $0 count"
exit
fi
cmd='{A..Z}'
for (( i=$1; i>26; i=i/26 )); do
cmd="${A..Z}$cmd"
done
vars=( $(eval echo $cmd) )
for (( i=0; i<$1; i++ )); do
echo ${vars[$i]}
done
i/26 does integer division (throws away the remainder)
I'm lazy and generate "more than enough" variables rather than attempting to calculate how many is "exactly enough"
{a..b}{a..b}{a..b} becomes aaa aab aba abb baa bab bba bbb
using eval lets us do the brace expansion without knowing in advance how many sets are needed
Sample output:
$ mkvar.sh 10000 |fmt -64 | tail -5
ORY ORZ OSA OSB OSC OSD OSE OSF OSG OSH OSI OSJ OSK OSL OSM
OSN OSO OSP OSQ OSR OSS OST OSU OSV OSW OSX OSY OSZ OTA OTB
OTC OTD OTE OTF OTG OTH OTI OTJ OTK OTL OTM OTN OTO OTP OTQ
OTR OTS OTT OTU OTV OTW OTX OTY OTZ OUA OUB OUC OUD OUE OUF
OUG OUH OUI OUJ OUK OUL OUM OUN OUO OUP
This block of code is looping through a file and loading each word into a multi dimensional array.
lcv=0
declare -A db
while read line;
do
lcv1=0
echo $line
for i in $line;
do
db[$lcv,$lcv1]=$i
echo $lcv,$lcv1,${db[$lcv,$lcv1]};
#echo ${db[$lcv]}
((++lcv1))
done
((++lcv))
done < data.txt # File Contains records of 4 fields.
echo ${db[0,1]}
echo ${db[0,0]}
Little pseudo 2D array using bash
I just re-use your algorithm, whiping all echo and useless steps.
#!/bin/bash
unset x y db
y=0
declare -A db
while read line ;do
for i in $line ;do
db[$((x++)),$y]=$i
done
((y++))
x=0
done <<<$'0 1 2 3\n4 5 6 7\n8 9 a b\nc d e f'
Now if you
declare -p db x y
bash will print:
declare -A db='([0,0]="0" [0,1]="4" [0,2]="8" [0,3]="c" [3,3]="f" [3,2]="b" [3,1]="7" [3,0]="3" [2,2]="a" [2,3]="e" [2,0]="2" [2,1]="6" [1,1]="5" [1,0]="1" [1,3]="d" [1,2]="9" )'
declare -- x="0"
declare -- y="4"
At this point, I just wanna purpose to change 9th line: ((y++)) by ((y++,maxx=maxx>x?maxx:x)). This will populate maxx (to 4 in this sample)
Then inverting the array:
for i in {0..4};do # this syntax is nice, but don't support variables
for((j=0;j<y;j++)){ # this syntaxe could use variables
echo -n ${db[$i,$j]}\
}
echo
done
will print:
0 4 8 c
1 5 9 d
2 6 a e
3 7 b f
If the data.txt contains this:
$ cat data.txt
l0val0 l0val1 l0val2 l0val3
l1val0 l1val1 l1val2 l1val3
l2val0 l2val1 l2val2 l2val3
l3val0 l3val1 l3val2 l3val3
l4val0 l4val1 l4val2 l4val3
l5val0 l5val1 l5val2 l5val3
Your program produce this:
$ ./script
l0val0 l0val1 l0val2 l0val3
0,0,l0val0
0,1,l0val1
0,2,l0val2
0,3,l0val3
l1val0 l1val1 l1val2 l1val3
1,0,l1val0
1,1,l1val1
1,2,l1val2
1,3,l1val3
l2val0 l2val1 l2val2 l2val3
2,0,l2val0
2,1,l2val1
2,2,l2val2
2,3,l2val3
l3val0 l3val1 l3val2 l3val3
3,0,l3val0
3,1,l3val1
3,2,l3val2
3,3,l3val3
l4val0 l4val1 l4val2 l4val3
4,0,l4val0
4,1,l4val1
4,2,l4val2
4,3,l4val3
l5val0 l5val1 l5val2 l5val3
5,0,l5val0
5,1,l5val1
5,2,l5val2
5,3,l5val3
l0val1
l0val0
That goes to show that the value of $lcv selects each row (line), and the value of $lcv1 selects each word (record) divided on spaces or tabs.
It is working correctly from what I can see.
If we add this lines at the end of the script:
echo "end of first script"
for i in {0..5}; do
for j in {0..3}; do
printf 'db[%s,%s]=%s ' "$i" "$j" "${db[$i,$j]}"
done
echo
done
echo
declare -p db
We will get this output:
end of first script
db[0,0]=l0val0 db[0,1]=l0val1 db[0,2]=l0val2 db[0,3]=l0val3
db[1,0]=l1val0 db[1,1]=l1val1 db[1,2]=l1val2 db[1,3]=l1val3
db[2,0]=l2val0 db[2,1]=l2val1 db[2,2]=l2val2 db[2,3]=l2val3
db[3,0]=l3val0 db[3,1]=l3val1 db[3,2]=l3val2 db[3,3]=l3val3
db[4,0]=l4val0 db[4,1]=l4val1 db[4,2]=l4val2 db[4,3]=l4val3
db[5,0]=l5val0 db[5,1]=l5val1 db[5,2]=l5val2 db[5,3]=l5val3
declare -A db=([1,1]="l1val1" [1,0]="l1val0" [1,3]="l1val3" [1,2]="l1val2" [0,0]="l0val0" [0,1]="l0val1" [0,2]="l0val2" [0,3]="l0val3" [5,1]="l5val1" [5,0]="l5val0" [5,3]="l5val3" [5,2]="l5val2" [3,3]="l3val3" [3,2]="l3val2" [3,1]="l3val1" [3,0]="l3val0" [2,2]="l2val2" [2,3]="l2val3" [2,0]="l2val0" [2,1]="l2val1" [4,0]="l4val0" [4,1]="l4val1" [4,2]="l4val2" [4,3]="l4val3" )
Now, the question is: What do you think that is wrong?.
I'm working with an existing script which was written a bit messily. Setting up a loop with all of the spaghetti code could make a bigger headache than I want to deal with in the near term. Maybe when I have more time I can clean it up but for now, I'm just looking for a simple fix.
The script deals with virtual disks on a xen server. It reads multipath output and asks if particular LUNs should be formatted in any way based on specific criteria. However, rather than taking that disk path and inserting it, already formatted, into a configuration file, it simply presents every line in the format
'phy:/dev/mapper/UUID,xvd?,w',
UUID, of course, is an actual UUID.
The script actually presents each of the found LUNs in this format expecting the user to copy and paste them into the config file replacing each ? with a letter in sequence. This is tedious at best.
There are several ways to increment a number in bash. Among others:
var=$((var+1))
((var+=1))
((var++))
Is there a way to do the same with characters which doesn't involve looping over the entire alphabet such that I could easily "increment" the disk assignment from xvda to xvdb, etc?
To do an "increment" on a letter, define the function:
incr() { LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'$1")+1))")"; }
Now, observe:
$ echo $(incr a)
b
$ echo $(incr b)
c
$ echo $(incr c)
d
Because, this increments up through ASCII, incr z becomes {.
How it works
The first step is to convert a letter to its ASCII numeric value. For example, a is 97:
$ printf '%d' "'a"
97
The next step is to increment that:
$ echo "$((97+1))"
98
Or:
$ echo "$(($(printf '%d' "'a")+1))"
98
The last step is convert the new incremented number back to a letter:
$ LC_CTYPE=C printf "\\$(printf '%03o' "98")"
b
Or:
$ LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'a")+1))")"
b
Alternative
With bash, we can define an associative array to hold the next character:
$ declare -A Incr; last=a; for next in {b..z}; do Incr[$last]=$next; last=$next; done; Incr[z]=a
Or, if you prefer code spread out over multiple lines:
declare -A Incr
last=a
for next in {b..z}
do
Incr[$last]=$next
last=$next
done
Incr[z]=a
With this array, characters can be incremented via:
$ echo "${Incr[a]}"
b
$ echo "${Incr[b]}"
c
$ echo "${Incr[c]}"
d
In this version, the increment of z loops back to a:
$ echo "${Incr[z]}"
a
How about an array with entries A-Z assigned to indexes 1-26?
IFS=':' read -r -a alpharray <<< ":A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
This has 1=A, 2=B, etc. If you want 0=A, 1=B, and so on, remove the first colon.
IFS=':' read -r -a alpharray <<< "A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
Then later, where you actually need the letter;
var=$((var+1))
'phy:/dev/mapper/UUID,xvd${alpharray[$var]},w',
The only problem is that if you end up running past 26 letters, you'll start getting blanks returned from the array.
Use a Bash 4 Range
You can use a Bash 4 feature that lets you specify a range within a sequence expression. For example:
for letter in {a..z}; do
echo "phy:/dev/mapper/UUID,xvd${letter},w"
done
See also Ranges in the Bash Wiki.
Here's a function that will return the next letter in the range a-z. An input of 'z' returns 'a'.
nextl(){
((num=(36#$(printf '%c' $1)-9) % 26+97));
printf '%b\n' '\x'$(printf "%x" $num);
}
It treats the first letter of the input as a base 36 integer, subtracts 9, and returns the character whose ordinal number is 'a' plus that value mod 26.
Use Jot
While the Bash range option uses built-ins, you can also use a utility like the BSD jot utility. This is available on macOS by default, but your mileage may vary on Linux systems. For example, you'll need to install athena-jot on Debian.
More Loops
One trick here is to pre-populate a Bash array and then use an index variable to grab your desired output from the array. For example:
letters=( "" $(jot -w %c 26 a) )
for idx in 1 26; do
echo ${letters[$idx]}
done
A Loop-Free Alternative
Note that you don't have to increment the counter in a loop. You can do it other ways, too. Consider the following, which will increment any letter passed to the function without having to prepopulate an array:
increment_var () {
local new_var=$(jot -nw %c 2 "$1" | tail -1)
if [[ "$new_var" == "{" ]]; then
echo "Error: You can't increment past 'z'" >&2
exit 1
fi
echo -n "$new_var"
}
var="c"
var=$(increment_var "$var")
echo "$var"
This is probably closer to what the OP wants, but it certainly seems more complex and less elegant than the original loop recommended elsewhere. However, your mileage may vary, and it's good to have options!
I'm trying to get a (key,multiple-value) structure (some sort of hashmap) in bash, like this :
[
[ "abc" : 1, 2, 3, 4 ],
[ "def" : "w", 33, 2 ]
]
I'd like to iterate through eack key (some kind of for key in ..., and get each value with something like map["def",2] or map[$key,2].
I've seen a couple of threads talking about single-value hashmap, but nothing about this issue.
I could go with N arrays, N being the amount of key in my map, filled with every field in a row, but I don't want to duplicate code as much as possible.
Thanks in advance !
Edit :
I'd like to go through the structure with something like this :
for key in ${map[#]} do;
echo $key # "abc" then "def"
for value in ${map[$key,#]} do;
...
done
done
Using modern bash features with the multiple-array case:
Assignment (manual):
map_abc=( 1 2 3 4 )
map_def=( w 33 2 )
Assignment (programmatic):
append() {
local array_name="${1}_$2"; shift; shift
declare -g -a "$array_name"
declare -n array="$array_name" # BASH 4.3 FEATURE
array+=( "$#" )
}
append map abc 1 2 3 4
append map def w 33 2
Iteration (done inside a function to contain the namevar's scope):
iter() {
for array in ${!map_#}; do
echo "Iterating over array ${array#map_}"
declare -n cur_array="$array" # BASH 4.3 FEATURE
for key in "${!cur_array[#]}"; do
echo "$key: ${cur_array[$key]}"
done
done
}
iter
This can also be done without namevars, but in an uglier and more error-prone fashion. (To be clear, I believe the code given here uses eval safely, but it's easy to get wrong -- if trying to build your own implementation on this template, please be very cautious).
# Compatible with older bash (should be through 3.x).
append() {
local array_name="${1}_$2"; shift; shift
declare -g -a "$array_name"
local args_str cmd_str
printf -v args_str '%q ' "$#"
printf -v cmd_str "%q+=( %s )" "$array_name" "$args_str"
eval "$cmd_str"
}
...and, to iterate in a way compatible with bash back through 3.x:
for array in ${!map_#}; do
echo "Iterating over array ${array#map_}"
printf -v cur_array_cmd 'cur_array=( ${%q[#]} )' "$array"
eval "$cur_array_cmd"
for key in "${!cur_array[#]}"; do
echo "$key: ${cur_array[$key]}"
done
done
This is more computationally efficient than filtering through a single large array (the other answer given) -- and, when namevars are available, arguably results in cleaner code as well.
Do-able. The declaration is somewhat ugly
declare -A map=(
[abc,0]=1
[abc,1]=2
[abc,2]=3
[abc,3]=4
[def,0]=w
[def,1]=33
[def,2]=2
)
key="def"
i=1
echo "${map[$key,$i]}" # => 33
Iterating: helpful to keep a separate array of "keys":
keys=(abc def)
Then
for key in "${keys[#]}"; do
echo "$key"
for idx in "${!map[#]}"; do
if [[ $idx == $key,* ]]; then
n=${idx##*,}
printf "\t%s\t%s\n" "$n" "${map["$idx"]}"
fi
done
done
abc
0 1
1 2
2 3
3 4
def
1 33
0 w
2 2