1

I am trying to write a simple function in bash which takes 2 arguments, a string and an array.

At the very beggining of the function I check for the number of arguments

function is2args() {
    if [ $# -le 1 ]; then
        return 1
    fi

    return 0
 }

I then attempt the following

arr=()
is2args "" "${arr[@]}" # returns 1

This triggers the if statement as bash for some reason thinks there is only one argument however if the list is an empty string it works

arr=()
is2args "" "" # returns 0

or filled with elements

arr=(
     "element"
)
is2args "" "${arr[@]}" # returns 0

and default to an empty string

arr=()
is2args "" "${arr[@]:-""}" # returns 0

I don't quite understand what is going on. It is my understanding this is the correct way to pass a list however it seems that an empty list is ignored for whatever reason.

Should I really be setting a default empty string every time I send through an array or is there a way to catch this in the function itself?

Alexandre Thenorio
  • 2,288
  • 3
  • 31
  • 50
  • 1
    If you expand an array onto a command line, the array's elements become your subsequent arguments. You pass a zero-argument array after a single fixed command, you have one argument total. You pass a three-argument array, you have four arguments total. That's expected behavior -- work *with* the language, not against it. – Charles Duffy Sep 06 '17 at 20:23
  • 1
    There is no such thing as an array *value* in shell. You can pass a string that *names* an array variable, or you can pass the *elements* of the array as one or more arguments. – chepner Sep 06 '17 at 21:12
  • @charles If what you are saying it true it makes me.very confused because I have written functions before where I pass 2 arrays to a function and I am able to distinguish between both. Shouldn't they all mix in one big bag and not be possible to distinguish? – Alexandre Thenorio Sep 07 '17 at 06:26
  • @ByteFlinger, yes, that **is** in fact what happens. If you're distinguishing them, you're presumably doing something like `${array[*]}`, converting them to strings -- in which case they aren't arrays any longer at all. – Charles Duffy Sep 07 '17 at 12:11
  • @ByteFlinger, if you can show such a function, I can describe its actual behavior (and provide some sample data with which it will misbehave to demonstrate why that behavior is buggy/wrong). – Charles Duffy Sep 07 '17 at 12:12
  • @ByteFlinger, ...moreover, the answer you accepted says the same thing I'm saying here, if you read it closely. – Charles Duffy Sep 07 '17 at 12:14
  • @ByteFlinger, ...if you want to pass two arrays into a function, to do it safely you either need to use namevars (as given in glenn's newer answer) or length-prefix them, and pull only the desired number of elements off; see the latter in use at https://stackoverflow.com/a/9899041/14122 – Charles Duffy Sep 07 '17 at 12:18
  • You were right about `${array[*]}`. This works fine to pass several arrays into a function. I think I need to go away from bash :) Thank you for the reply – Alexandre Thenorio Sep 07 '17 at 14:05
  • @ByteFlinger, no, it does not by any means "work fine" (unless you've put significant restrictions on the data stored in the arrays). Consider `array=( "first item" "second item" )` -- you can't distinguish `"${array[*]}"` from that and `array=( "first" "item" "second" "item" )`. And that's a relatively simple case; there are others as well. – Charles Duffy Sep 07 '17 at 15:38
  • Actually I am making use of `IFS=$'\n\t'` so, yes, some care has been taken against that issue – Alexandre Thenorio Sep 07 '17 at 17:02
  • "Some" care, but not nearly enough. Tabs and literal newlines are valid in more data than you think. They're valid in filenames, they're valid in command lines. The only character that can't appear in any of those places is a NUL, and bash uses C strings, so it's unable to store NULs in string literals (as languages using Pascal strings can). – Charles Duffy Sep 08 '17 at 11:51
  • `array=( $'first\nitem' $'second\titem' )` becomes our relevant case. – Charles Duffy Sep 08 '17 at 11:56

3 Answers3

3

This is expected. The "${arr[@]}" form expands to put each element of the array on the command line, and there are no elements in the array.

The "${arr[*]}" form will expand to one string consisting of the elements concatenated with a space (by default).

You might try this to test the arguments passed:

$ nargs () {
  local i=0 
  for arg do printf "%d\t>%s<\n" $((++i)) "$arg"; done
}

$ nargs foo "${arr[@]}"
1   >foo<
$ nargs foo "${arr[*]}"
1   >foo<
2   ><

$ arr=(1 2 3)
$ nargs foo "${arr[@]}"
1   >foo<
2   >1<
3   >2<
4   >3<
$ nargs foo "${arr[*]}"
1   >foo<
2   >1 2 3<

$ IFS=,
$ nargs foo "${arr[*]}"
1   >foo<
2   >1,2,3<
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Everything here is true, but a string is a less expressive data structure than an array -- encouraging folks to compress one into the other without any means of encoding or escaping is encouraging them to, at best, restrict the range of data their code can accurately process. – Charles Duffy Sep 06 '17 at 22:02
2

If you want to pass a fixed argument and an array, shift the first element off; what remains in "$@" is the contents of your array. If the array is empty, $@ will be empty too. (If $# is 0 after the shift, that means your array's length was 0 -- or no array was passed; the two cases are precisely identical).

myfn() {
  local string_arg=$1; shift || { echo "ERROR: Initial argument missing" <&2; return 1; }
  local -a array_args=( "$@" )

  echo "Initial argument is: $string_arg"
  echo "Array has length ${#array_args[@]}"
  echo "Array elements:"
  printf ' - %q\n' "${array_args[@]}"
}

array=( "first array member" "second array member" )
myfn "first argument" "${array[@]}"
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
2

You might want to pass the name of the array (note that the below requires bash 4.3):

myfunc() {
  local arg1=$1
  local -n array=$2
  printf "first arg: %q\n" "$arg1"
  echo array:
  declare -p array
}

arr=()
myfunc "foo" arr
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 1
    This looks good to me (other than the `print`/`printf` bug, now fixed). Might also choose a less likely name for the local -- `myfunc foo array` wouldn't behave so well. (I tend to use `declare -n myfunc_array=$2`). Indicating explicitly that bash 4.3 is required wouldn't hurt. – Charles Duffy Sep 07 '17 at 12:13