Bash Select position of array element by args - bash

I want make a bash script which returns the position of an element from an array by give an arg. See code below, I use:
#!/bin/bash
args=("$#")
echo ${args[0]}
test_array=('AA' 'BB' 'CC' 'DD' 'EE')
echo $test_array
elem_array=${#test_array[#]}
for args in $test_array
do
echo
done
Finally I should have output like:
$script.sh DD
4

#!/bin/bash
A=(AA BB CC DD EE)
for i in "${!A[#]}"; do
if [[ "${A[i]}" = "$1" ]]; then
echo "$i"
fi
done
Note the "${!A[#]}" notation that gives the list of valid indexes in the array. In general you cannot just go from 0 to "${#A[#]}" - 1, because the indexes are not necessarily contiguous. There can be gaps in the index range if there were gaps in the array element assignments or if some elements have been unset.
The script above will output all indexes of the array for which its content is equal to the first command line argument of the script.
EDIT:
In your question, you seem to want the result as a one-based array index. In that case you can just increment the result by one:
#!/bin/bash
A=(AA BB CC DD EE)
for i in "${!A[#]}"; do
if [[ "${A[i]}" = "$1" ]]; then
let i++;
echo "$i"
fi
done
Keep in mind, though, that this index will have to be decremented before being used with a zero-based array.

Trying to avoid complex tools:
test_array=('AA' 'BB' 'CC' 'D D' 'EE')
OLD_IFS="$IFS"
IFS="
"
element=$(grep -n '^D D$' <<< "${test_array[*]}" | cut -d ":" -f 1)
IFS="$OLD_IFS"
echo $element
However, it consumes 2 processes. If we allow ourselves sed, we could do it with a single process:
test_array=('AA' 'BB' 'CC' 'D D' 'EE')
OLD_IFS="$IFS"
IFS="
"
element=$(sed -n -e '/^D D$/=' <<< "${test_array[*]}")
IFS="$OLD_IFS"
echo $element
Update:
As pointed out by thkala in the comments, this solution is broken in 3 cases. Be careful not to use it if:
You want zero indexed offset.
You have newlines in your array elements.
And you have a sparse array, or have other keys than integers.

Loop over the array and keep track of the position.
When you find the element matching the input argument, print out the position of the element. You need to add one to the position, because arrays have zero-based indexing.
#! /bin/sh
arg=$1
echo $arg
test_array=('AA' 'BB' 'CC' 'DD' 'EE')
element_count=${#test_array[#]}
index=0
while [ $index -lt $element_count ]
do
if [ "${test_array[index]}" = "$arg" ]
then
echo $((index+1))
break
fi
((index++))
done

Without loop:
#!/bin/bash
index() {
local IFS=$'\n';
echo "${*:2}" | awk '$0 == "'"${1//\"/\\\"}"'" { print NR-1; exit; }'
}
array=("D A D" "A D" bBb "D WW" D "\" D \"" e1e " D " E1E D AA "" BB)
element=${array[5]}
index "$element" "${array[#]}"
Output:
5

Related

Is there a way I can pass an array as argument into a function? [duplicate]

As we know, in bash programming the way to pass arguments is $1, ..., $N. However, I found it not easy to pass an array as an argument to a function which receives more than one argument. Here is one example:
f(){
x=($1)
y=$2
for i in "${x[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "${a[#]}" $b
f "${a[*]}" $b
As described, function freceives two arguments: the first is assigned to x which is an array, the second to y.
f can be called in two ways. The first way use the "${a[#]}" as the first argument, and the result is:
jfaldsj
jflajds
The second way use the "${a[*]}" as the first argument, and the result is:
jfaldsj
jflajds
LAST
Neither result is as I wished. So, is there anyone having any idea about how to pass array between functions correctly?
You cannot pass an array, you can only pass its elements (i.e. the expanded array).
#!/bin/bash
function f() {
a=("$#")
((last_idx=${#a[#]} - 1))
b=${a[last_idx]}
unset a[last_idx]
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f "${x[#]}" "$b"
echo ===============
f "${x[*]}" "$b"
The other possibility would be to pass the array by name:
#!/bin/bash
function f() {
name=$1[#]
b=$2
a=("${!name}")
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f x "$b"
You can pass an array by name reference to a function in bash (since version 4.3+), by setting the -n attribute:
show_value () # array index
{
local -n myarray=$1
local idx=$2
echo "${myarray[$idx]}"
}
This works for indexed arrays:
$ shadock=(ga bu zo meu)
$ show_value shadock 2
zo
It also works for associative arrays:
$ declare -A days=([monday]=eggs [tuesday]=bread [sunday]=jam)
$ show_value days sunday
jam
See also nameref or declare -n in the man page.
You could pass the "scalar" value first. That would simplify things:
f(){
b=$1
shift
a=("$#")
for i in "${a[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "$b" "${a[#]}"
At this point, you might as well use the array-ish positional params directly
f(){
b=$1
shift
for i in "$#" # or simply "for i; do"
do
echo $i
done
....
}
f "$b" "${a[#]}"
This will solve the issue of passing array to function:
#!/bin/bash
foo() {
string=$1
array=($#)
echo "array is ${array[#]}"
echo "array is ${array[1]}"
return
}
array=( one two three )
foo ${array[#]}
colors=( red green blue )
foo ${colors[#]}
Try like this
function parseArray {
array=("$#")
for data in "${array[#]}"
do
echo ${data}
done
}
array=("value" "value1")
parseArray "${array[#]}"
Pass the array as a function
array() {
echo "apple pear"
}
printArray() {
local argArray="${1}"
local array=($($argArray)) # where the magic happens. careful of the surrounding brackets.
for arrElement in "${array[#]}"; do
echo "${arrElement}"
done
}
printArray array
Here is an example where I receive 2 bash arrays into a function, as well as additional arguments after them. This pattern can be continued indefinitely for any number of bash arrays and any number of additional arguments, accommodating any input argument order, so long as the length of each bash array comes just before the elements of that array.
Function definition for print_two_arrays_plus_extra_args:
# Print all elements of a bash array.
# General form:
# print_one_array array1
# Example usage:
# print_one_array "${array1[#]}"
print_one_array() {
for element in "$#"; do
printf " %s\n" "$element"
done
}
# Print all elements of two bash arrays, plus two extra args at the end.
# General form (notice length MUST come before the array in order
# to be able to parse the args!):
# print_two_arrays_plus_extra_args array1_len array1 array2_len array2 \
# extra_arg1 extra_arg2
# Example usage:
# print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
# "${#array2[#]}" "${array2[#]}" "hello" "world"
print_two_arrays_plus_extra_args() {
i=1
# Read array1_len into a variable
array1_len="${#:$i:1}"
((i++))
# Read array1 into a new array
array1=("${#:$i:$array1_len}")
((i += $array1_len))
# Read array2_len into a variable
array2_len="${#:$i:1}"
((i++))
# Read array2 into a new array
array2=("${#:$i:$array2_len}")
((i += $array2_len))
# You can now read the extra arguments all at once and gather them into a
# new array like this:
extra_args_array=("${#:$i}")
# OR you can read the extra arguments individually into their own variables
# one-by-one like this
extra_arg1="${#:$i:1}"
((i++))
extra_arg2="${#:$i:1}"
((i++))
# Print the output
echo "array1:"
print_one_array "${array1[#]}"
echo "array2:"
print_one_array "${array2[#]}"
echo "extra_arg1 = $extra_arg1"
echo "extra_arg2 = $extra_arg2"
echo "extra_args_array:"
print_one_array "${extra_args_array[#]}"
}
Example usage:
array1=()
array1+=("one")
array1+=("two")
array1+=("three")
array2=("four" "five" "six" "seven" "eight")
echo "Printing array1 and array2 plus some extra args"
# Note that `"${#array1[#]}"` is the array length (number of elements
# in the array), and `"${array1[#]}"` is the array (all of the elements
# in the array)
print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
"${#array2[#]}" "${array2[#]}" "hello" "world"
Example Output:
Printing array1 and array2 plus some extra args
array1:
one
two
three
array2:
four
five
six
seven
eight
extra_arg1 = hello
extra_arg2 = world
extra_args_array:
hello
world
For further examples and detailed explanations of how this works, see my longer answer on this topic here: Passing arrays as parameters in bash
You can also create a json file with an array, and then parse that json file with jq
For example:
my-array.json:
{
"array": ["item1","item2"]
}
script.sh:
ARRAY=$(jq -r '."array"' $1 | tr -d '[],"')
And then call the script like:
script.sh ./path-to-json/my-array.json

How can I highlight given values in a generated numeric sequence?

I often receive unordered lists of document IDs. I can sort and print them easy enough, but I'd like to print a line for each available document and show an asterisk (or anything really, just to highlight) next to all values in the given list.
Such as ...
$ ./t.sh "1,4,3" 5
1*
2
3*
4*
5
$
The first parameter is the unordered list, and the second is the total number of documents.
If by "available document" you mean an "existing file on disk", then assuming you have 5 total files, and you are checking to see if you have 1, 4 and 3. The following script will produce sorted output.
#!/bin/bash
#Store the original IFS
ORGIFS=$IFS
#Now Set the Internal File Separater to a comma
IFS=","
###Identify which elements of the array we do have and store the results
### in a separate array
#Begin a loop to process each array element
for X in ${1} ; do
if [[ -f ${X} ]] ; then
vHAVE[$X]=YES
fi
done
#Now restore IFS
IFS=$ORGIFS
#Process the sequence of documents, starting at 1 and ending at $2.
for Y in $(seq 1 1 $2) ; do
#Check if the sequence exists in our inventoried array and mark accordingly.
if [[ ${vHAVE[$Y]} == YES ]] ; then
echo "$Y*"
else
echo "$Y"
fi
done
Returns the result:
rtcg#testserver:/temp/test# ls
rtcg#testserver:/temp/test# touch 1 3 4
rtcg#testserver:/temp/test# /usr/local/bin/t "1,4,3" 5
1*
2
3*
4*
5
The following code works for me on your example.
Generate a sequence of the length given by the user
Split the first argument of your script (it will gives you an array A for example)
Use the function contains to check if one element from A is in the sequence generated by the step one
I don't check the arguments length and you should do that to have a more proper script.
#!/bin/bash
function contains() {
local n=$#
local value=${!n}
for ((i=1;i < $#;i++)) {
if [ "${!i}" == "${value}" ]; then
echo "y"
return 0
fi
}
echo "n"
return 1
}
IFS=', ' read -a array <<< $1
for i in $(seq $2); do
if [ $(contains "${array[#]}" "${i}") == "y" ]; then
echo "${i}*"
else
echo "${i}"
fi
done
You can use parameter substitution to build an extended pattern that can be used to match document numbers to the list of documents to mark.
#!/bin/bash
# 1,4,3 -> 1|4|3
to_mark=${1//,/|}
for(( doc=1; doc <= $2; doc++)); do
# #(1|4|3) matches 1, 4 or 3
printf "%s%s\n" "$doc" "$( [[ $doc = #($to_mark) ]] && printf "*" )"
done

How to fetch last argument and stop before last arguments in shell script?

I want to merge all files into one. Here, the last argument is the destination file name.
I want to take last argument and then in loop stop before last arguments.
Here code is given that I want to implement:
echo "No. of Argument : $#"
for i in $* - 1
do
echo $i
cat $i >> last argument(file)
done
How to achieve that?
Using bash:
fname=${!#}
for a in "${#:1:$# - 1}"
do
echo "$a"
cat "$a" >>"$fname"
done
In bash, the last argument to a script is ${!#}. So, that is where we get the file name.
bash also allows selecting elements from an array. To start with a simple example, observe:
$ set -- a b c d e f
$ echo "${#}"
a b c d e f
$ echo "${#:2:4}"
b c d e
In our case, we want to select elements from the first to the second to last. The first is number 1. The last is number $#. We want to select all but the last. WE thus want $# - 1 elements of the array. Therefore, to select the arguments from the first to the second to last, we use:
${#:1:$# - 1}
A POSIX-compliant method:
eval last_arg=\$$#
while [ $# -ne 1 ]; do
echo "$1"
cat "$1" >> "$last_arg"
shift
done
Here, eval is safe, because you are only expanding a read-only parameter in the string that eval will execute. If you don't want to unset the positional parameters via shift, you can iterate over them, using a counter to break out of the loop early.
eval last_arg=\$$#
i=1
for arg in "$#"; do
echo "$arg"
cat "$arg" >> "$last_arg"
i=$((i+1))
if [ "$i" = "$#" ]; then
break
fi
done

Need to printf script's arguments

I'm trying to list script's argument by printf function. Arguments are counted by iteration of $i. What should be in printf function?
I need something like
eval echo \$$i
but in printf function.
Edit: Have while cycle with iteration of $i and among other code, I have
printf "%s" $i
But, instead of $i, I need some code, that shows me value of argument.
In my case, it is name of file, and I need list them. One file in one iteration.
As noted in a comment, you normally do that with a loop such as:
i=1
for arg in "$#"
do
echo "$i: [$arg]"
((i++))
done
(If echo isn't allowed, use printf "%s\n" … where the … is whatever would have followed echo.)
You might also use indirect expansion to avoid the use of eval:
for i in 1 2 3 4; do echo "$i: [${!i}]"; done
You can generalize that with:
for i in $(seq 1 $#); do echo "$i: [${!i}]"; done
or
for ((i = 1; i <= $#; i++)); do echo "$i: [${!i}]"; done
For example, given:
set -- a b 'c d' ' e f '
all the loops produce the output:
1: [a]
2: [b]
3: [c d]
4: [ e f ]
The square brackets are merely there to delimit the argument values; it allows you to see the trailing blanks on the fourth line of output.
You might also be able to use:
printf "[%s]\n" "$#"
to get:
[a]
[b]
[c d]
[ e f ]
It is not very clear what you are asking for, but this will list the arguments passed to the script:
while [ $# -gt 0 ]; do
printf "%s\n" "$1"
shift
done
I guess what you are asking for is how to make the indirect reference to positional parameter i (i containing the position):
print "%s\n" ${!i}
To get your arguments in such a way that they can be fed back into the shell with the exact same value, the following bash extension can be used:
printf '%q ' "$#"; printf '\n'
This works even for rather unusual cases. Let's say that one of your arguments contains a newline:
./your-script 'hello
world' 'goodbye world'
This will be represented in the printf output:
$'hello\nworld' goodbye\ world
...with something you can use again in the shell:
$ echo $'hello\nworld' goodbye\ world
hello
world goodbye world

parse and expand interval

In my script I need to expand an interval, e.g.:
input: 1,5-7
to get something like the following:
output: 1,5,6,7
I've found other solutions here, but they involve python and I can't use it in my script.
Solution with Just Bash 4 Builtins
You can use Bash range expansions. For example, assuming you've already parsed your input you can perform a series of successive operations to transform your range into a comma-separated series. For example:
value1=1
value2='5-7'
value2=${value2/-/..}
value2=`eval echo {$value2}`
echo "input: $value1,${value2// /,}"
All the usual caveats about the dangers of eval apply, and you'd definitely be better off solving this problem in Perl, Ruby, Python, or AWK. If you can't or won't, then you should at least consider including some pipeline tools like tr or sed in your conversions to avoid the need for eval.
Try something like this:
#!/bin/bash
for f in ${1//,/ }; do
if [[ $f =~ - ]]; then
a+=( $(seq ${f%-*} 1 ${f#*-}) )
else
a+=( $f )
fi
done
a=${a[*]}
a=${a// /,}
echo $a
Edit: As #Maxim_united mentioned in the comments, appending might be preferable to re-creating the array over and over again.
This should work with multiple ranges too.
#! /bin/bash
input="1,5-7,13-18,22"
result_str=""
for num in $(tr ',' ' ' <<< "$input"); do
if [[ "$num" == *-* ]]; then
res=$(seq -s ',' $(sed -n 's#\([0-9]\+\)-\([0-9]\+\).*#\1 \2#p' <<< "$num"))
else
res="$num"
fi
result_str="$result_str,$res"
done
echo ${result_str:1}
Will produce the following output:
1,5,6,7,13,14,15,16,17,18,22
expand_commas()
{
local arg
local st en i
set -- ${1//,/ }
for arg
do
case $arg in
[0-9]*-[0-9]*)
st=${arg%-*}
en=${arg#*-}
for ((i = st; i <= en; i++))
do
echo $i
done
;;
*)
echo $arg
;;
esac
done
}
Usage:
result=$(expand_commas arg)
eg:
result=$(expand_commas 1,5-7,9-12,3)
echo $result
You'll have to turn the separated words back into commas, of course.
It's a bit fragile with bad inputs but it's entirely in bash.
Here's my stab at it:
input=1,5-7,10,17-20
IFS=, read -a chunks <<< "$input"
output=()
for chunk in "${chunks[#]}"
do
IFS=- read -a args <<< "$chunk"
if (( ${#args[#]} == 1 )) # single number
then
output+=(${args[*]})
else # range
output+=($(seq "${args[#]}"))
fi
done
joined=$(sed -e 's/ /,/g' <<< "${output[*]}")
echo $joined
Basically split on commas, then interpret each piece. Then join back together with commas at the end.
A generic bash solution using the sequence expression `{x..y}'
#!/bin/bash
function doIt() {
local inp="${#/,/ }"
declare -a args=( $(echo ${inp/-/..}) )
local item
local sep
for item in "${args[#]}"
do
case ${item} in
*..*) eval "for i in {${item}} ; do echo -n \${sep}\${i}; sep=, ; done";;
*) echo -n ${sep}${item};;
esac
sep=,
done
}
doIt "1,5-7"
Should work with any input following the sample in the question. Also with multiple occurrences of x-y
Use only bash builtins
Using ideas from both #Ansgar Wiechers and #CodeGnome:
input="1,5-7,13-18,22"
for s in ${input//,/ }
do
if [[ $f =~ - ]]
then
a+=( $(eval echo {${s//-/..}}) )
else
a+=( $s )
fi
done
oldIFS=$IFS; IFS=$','; echo "${a[*]}"; IFS=$oldIFS
Works in Bash 3
Considering all the other answers, I came up with this solution, which does not use any sub-shells (but one call to eval for brace expansion) or separate processes:
# range list is assumed to be in $1 (e.g. 1-3,5,9-13)
# convert $1 to an array of ranges ("1-3" "5" "9-13")
IFS=,
local range=($1)
unset IFS
list=() # initialize result list
local r
for r in "${range[#]}"; do
if [[ $r == *-* ]]; then
# if the range is of the form "x-y",
# * convert to a brace expression "{x..y}",
# * using eval, this gets expanded to "x" "x+1" … "y" and
# * append this to the list array
eval list+=( {${r/-/..}} )
else
# otherwise, it is a simple number and can be appended to the array
list+=($r)
fi
done
# test output
echo ${list[#]}

Resources