0

I'm writing a function that'll accept 2 arguments - the name of a scalar and and a filename and will write the scalar (newline delimited) to that file. I'm getting a "bad substitution" error. Here's my code:

#!/bin/bash

arr_to_f() {
for y in "${1[@]}"
       do
       echo "$y" >> "$2"
       done
}

mapfile -t arr < <(echo "one" ; echo "two" ; echo "three")

arr_to_f arr file

I could do it outside the function using my loop but ill be using this a lot i think so id rather be able to call it if possible.

AlexH
  • 49
  • 1
  • 9
  • Shouldn't it be "name of an array"? `mapfile` builds an array. – Benjamin W. Feb 26 '20 at 16:12
  • The immediate issue here is covered in [how to iterate over an array using indirect reference](https://stackoverflow.com/questions/11180714/how-to-iterate-over-an-array-using-indirect-reference). – Charles Duffy Feb 26 '20 at 16:20

2 Answers2

2

You could create a named reference to your arr variable. I also replaced echo with printf in your code, see Why is printf better than echo?.

#!/bin/bash

arr_to_f() {
  local -n myarray=$1
  for y in "${myarray[@]}"; do
    printf '%s\n' "$y"
  done >>"$2"
}

mapfile -t arr < <(printf '%s\n' one two three)
arr_to_f arr file

You could also get rid of the for-loop in your function:

arr_to_f() {
  local -n myarray=$1
  printf '%s\n' "${myarray[@]}" >>"$2"
}
Freddy
  • 4,548
  • 1
  • 7
  • 17
  • Generally, this is exactly what I would write myself. One change I would suggest, though, is replacing `>> "$2"` after each individual `printf` with one `>"$2"` after the `done`. That way we're keeping the file handle open through the whole loop rather than re-opening it over and over (thus also reducing filesystem load, as each `close()` updates the inode's mtime, not just the data contents), and also can keep appending new lines without the presumably-unintended side effect of leaving old content still present. – Charles Duffy Feb 26 '20 at 16:47
  • Thanks @CharlesDuffy. I moved `>>"$2"` after the `done`, I think that was your main point. I kept the redirection as "append" since I don't know if OP wants to overwrite or append. – Freddy Feb 26 '20 at 16:59
1

NOTE: This answer uses eval, and should be avoided where possible. See Freddy's answer, which uses declare -n / local -n to create a named reference.

This approach may be necessary if you're using an old version of bash.



As already pointed out, you're passing the literal string arr, and that isn't an array, hence the "bad substitution".

You can to use eval to build a string that results in expanding to what you're after:

arr_to_file() {
    local cmd
    printf -v cmd 'local -a _content=( "${%q[@]}" )' "${1}"
    eval "${cmd}"
    printf '%s\n' "${_content[@]}" > "${2}"
}

mapfile -t arr < <(echo "one" ; echo "two" ; echo "three")

arr_to_file arr file

eval usually gets a big warning notice, so here's yours: "eval is super dangerous if you don't trust the string you're giving it".

If we're very careful with eval, then it can be used somewhat safely. Here printf "%q" "${1}" is used to quote the first argument suitably for shell input (e.g: printf '%q' '}' won't break out of the variable's name). Thanks to Charles Duffy for the revised and significantly safer and more readable snippet.

This approach will also work for associative arrays (i.e: arrays that use keys instead of indexes), but the output will be unordered.

Attie
  • 6,690
  • 2
  • 24
  • 34