6

I'm writing a Git Bash utility that copies a project folder from one location to another. There are multiple destinations to which the user may want to copy the project, though only one location per execution of the script is permitted. Here is the logic thus far -

#!/bin/bash

# declare and initialize variables
source="/z/files/development/xampp/code/htdocs/Project7"

targets[0]="/z/files/development/xampp/code/htdocs/test/$(date +'%Y_%m_%d')"
targets[1]="/c/users/knot22/desktop/temp_dev/$(date +'%Y_%m_%d')"

# display contents of variables to user
echo "source " $source
echo -e "\nchoice \t target location"

for i in "${!targets[@]}"; do
  echo -e "$i \t ${targets[$i]}" 
done

echo

# prompt user for a target
read -p "Enter target's number for this copy operation: " target

So far, so good. Next I'd like to write an if statement that checks whether or not the value the user entered for target is a valid index in targets. In PHP it would be array_key_exists($target, $targets). What is the equivalent in Bash?

3
  • source is a reserved keyword. You shoulnt use it a as a variable name. Mar 31, 2022 at 21:36
  • @Daniele are you sure about that? I know it's a built-in command name, but that shouldn't prevent it being used as a variable name. The manual says that the reserved words are !, case, coproc, do, done, elif, else, esac, fi, for, function, if, in, select, then, until, while, {, }, time, [[ and ]]. Also, use as a variable is perfectly legitimate for a reserved word - the contexts in which they are reserved are as command names and a couple of other places where only in or do are allowed. Apr 1, 2022 at 12:14
  • I haven't noticed any strange behavior using source as a variable name.
    – knot22
    Apr 1, 2022 at 14:04

2 Answers 2

5

You can check if the array element is not null/empty with:

expr='^[0123456789]+$'
if [[ $target =~ $expr && -n "${targets[$target]}" ]]; then
    echo yes
else
    echo no
fi

You also have to check if the response is an integer since people can reply to the read prompt with a string which will evaluate to zero and therefore give you the first element in your array.

You may also want to consider using select here:

#!/bin/bash

# declare and initialize variables
source="/z/files/development/xampp/code/htdocs/Project7"

targets[0]="/z/files/development/xampp/code/htdocs/test/$(date +'%Y_%m_%d')"
targets[1]="/c/users/knot22/desktop/temp_dev/$(date +'%Y_%m_%d')"

select i in "${targets[@]}" exit; do
    [[ $i == exit ]] && break
    echo "$i which is number $REPLY"
done
10
  • select is an excellent suggestion. Mar 31, 2022 at 20:38
  • select is certainly more concise. However, it lists sequential integers starting at 1 whereas the original approach listed the array index values of 0, 1. Is there a way to get select to display the array index values instead (without doing " - 1" incase there are non sequential array index values)?
    – knot22
    Mar 31, 2022 at 20:48
  • 2
    @knot22: But do you even need the index values anymore since the array element selected will be represented by $i within the select statement. So you could just do cp "$i" ... instead of referencing the array again at all.
    – jesse_b
    Mar 31, 2022 at 20:50
  • 1
    Good call for trying to sanitise the input before using it as an arithmetic expression, but you can't use [0-9] for that, you need [0123456789] instead. For instance, in Ubuntu 20.04, and with the th_TH Thai locale enabled, if the script is called in an environment with the wrong variables, for instance with env LC_ALL=th_TH $'\xf3=a[$(reboot)]', if the user enters the 0xF3 byte at that prompt (Thai digit 3 in the TIS-620 charset used by that locale, also considered as alpha so as a valid variable name by bash), that will reboot. Mar 31, 2022 at 21:01
  • 1
    @knot22: You can find it here (also the same link Glenn posted in his answer): gnu.org/software/bash/manual/…
    – jesse_b
    Mar 31, 2022 at 23:26
2

TL;DR This answer is wrong, but I explain why.


I wrote:

You could use -v

if [[ -v targets[$target] ]]; then ...

Documented in 6.4 Bash Conditional Expressions

But that's wrong.

In a numerically indexed array, the index is evaluated as an arithmetic expression. In an arithmetic expression, "bare" strings are handled as shell variables and if the variable is empty or unset, it is handled as the value zero.

Demo:

targets=(zero one two)

target=2
[[ -v targets[target] ]] && echo "${targets[target]}" || echo n
# ==> two

target="x"
[[ -v targets[target] ]] && echo "${targets[target]}" || echo n
# ==> zero

But if there is a variable with the same name as the value of $target, then:

x=1
[[ -v targets[target] ]] && echo "${targets[target]}" || echo n
# ==> one
3
  • This seems to work correctly for integer entries but if a letter is entered, it returns the value in targets[0].
    – knot22
    Mar 31, 2022 at 20:41
  • 2
    And if someone enters $(reboot) at that read prompt, that will reboot. Mar 31, 2022 at 20:45
  • You could still use the same hacky workaround I used to check if it's an integer first. I was not aware that -v even existed. I guess that wouldn't work in the case of an associative array though.
    – jesse_b
    Mar 31, 2022 at 20:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .