6

Imagine I created an array like this:

IFS="|" read -ra ARR <<< "zero|one|||four"

now

echo ${#ARR[@]}
> 5
echo "${ARR[@]}"
> zero one   four
echo "${ARR[0]}"
> zero
echo "${ARR[2]}"
> # Nothing, because it is empty

The question is how can I replace the empty elements with another string?

I have tried

${ARR[@]///other}
${ARR[@]//""/other}

none of them worked.

I want this as output:

zero one other other four
chepner
  • 497,756
  • 71
  • 530
  • 681
stackoverflower
  • 3,885
  • 10
  • 47
  • 71
  • Obviously I can write a loop, but a more concise solution would be nice – stackoverflower Jan 27 '17 at 12:02
  • `"${arr[2]:-other}"` works, but `printf "%s\n" "${arr[@]:-other}"` does not, which makes sense but it is a pity because the trick on [Shell parameter expansion on arrays](http://stackoverflow.com/q/37698108/1983854) is useful. I guess a loop will be necessary – fedorqui Jan 27 '17 at 12:13
  • @sat, I was just giving a way the array could be created. There are obviously many other ways to create the array. – stackoverflower Jan 27 '17 at 12:15
  • @fedorqui, yes I thought too, but it seems parameter expansion doesn't work this way. – stackoverflower Jan 27 '17 at 12:19
  • The search and replace expansion works on multiple elements, but I do not think it is possible to specify an empty string pattern (like `^$` for a regex) that would match on empty elements. – Fred Jan 27 '17 at 12:24
  • If you can use `zsh`, this works: `IFS='/' read -A arr <<< "hi/im///fifth"; echo ${arr[*]/(#s)(#e)/other}` => `hi im other other fifth` – Tom Regner Jan 27 '17 at 13:09
  • The pattern isn't anchored to the beginning or end of the string to match (you can match the beginning *or* end with `/#` and `/%`, respectively, but not both), so in theory the empty string match at the beginning and the end of the string, as well as between each character. Much saner to simply define it to not match anywhere here. – chepner Jan 27 '17 at 13:16
  • Hey @stackoverflower remember you can accept answers! – fedorqui Feb 22 '17 at 11:50
  • 1
    @fedorqui accepted – stackoverflower Feb 22 '17 at 12:12

3 Answers3

4

If you want to replace all empty values (actually modifying the list), you could do this :

for i in "${!ARR[@]}" ; do ARR[$i]="${ARR[$i]:-other}"; done

Which looks like this when indented (more readable I would say) :

for i in "${!ARR[@]}"
do
  ARR[$i]="${ARR[$i]:-other}"
done
Fred
  • 6,590
  • 9
  • 20
  • I tried creating an array with three elements (`ARR[12]`="", `ARR[123]`=foo, `ARR[3456]`=bar), then performed the command I suggested, and it contained the expected result (`other` `foo` `bar`), at the same positions as before. Would you care to elaborate in which conditions you think it would not work? – Fred Jan 27 '17 at 12:39
  • I tested it with an associative array too (even though I do not think it was the OP intended usage), and it seems to work in my shell. – Fred Jan 27 '17 at 12:43
  • Agreed, mis-interpreted with a similar syntax I tried locally, would gladly do `+1`, but OP insisted on a answer without loops – Inian Jan 27 '17 at 12:44
  • Unless there actually is a loop-free solution someone comes up with (that does not turn to be loop-free at the expense of being clunky or inefficient just for the sake of being loop-free), the best solution probably will be some kind of simple loop like this. – Fred Jan 27 '17 at 13:00
  • I gave this an up vote, even though it uses a loop, as currently no one has come up with a loop free version, and this is succinct solution. – stackoverflower Jan 27 '17 at 14:18
  • Note though that you are very much focusing in keeping the indexes, while they do not seem to be important in the question. – fedorqui Jan 27 '17 at 15:16
  • Using the same indexes is my way of doing it "in-place" in the same array. Doing based only on values would simplify the expansions, and require a new array. Or do you see another way of doing it in the same array without keeping the same indexes? – Fred Jan 27 '17 at 15:39
  • I'd suggest quoting your expansions so this will work with associative arrays having arbitrary names. With `declare -A arr=( [one two]=12 [three four]=34 )`, the quotes in `"${!arr[@]}"` matter. (Similarly with conventional arrays, if IFS contains digits). – Charles Duffy Jan 27 '17 at 16:19
  • @CharlesDuffy Just to make sure I edit my answer correctly, do you mean quoting the array expansion, or the '$i' expansion, or both? – Fred Jan 27 '17 at 16:25
  • The array expansion is the only one I'm certain is necessary. Unless my memory is failing me, I *think* the `$i` expansion is safe unquoted in both places you're doing it. – Charles Duffy Jan 27 '17 at 16:35
  • @CharlesDuffy Corrected the `;`, I just made my edit too quickly to notice I enclosed it in the quotes. – Fred Jan 27 '17 at 16:44
4

To have the shell expansion behave, you need to loop through its elements and perform the replacement on each one of them:

$ IFS="|" read -ra ARR <<< "zero|one|||four"
$ for i in "${ARR[@]}"; do echo "${i:-other}"; done
zero
one
other
other
four

Where:

${parameter:-word}

If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

To store them in a new array, just do so by appending with +=( element ):

$ new=()
$ for i in "${ARR[@]}"; do new+=("${i:-other}"); done
$ printf "%s\n" "${new[@]}"
zero
one
other
other
four
Community
  • 1
  • 1
fedorqui
  • 275,237
  • 103
  • 548
  • 598
4
# Temporary array initialization       
NEW=()

# Loop over the array, add only non-empty values to the new array
for i in "${ARR[@]}"; do

   # Skip null items
   if [ -z "$i" ]; then
     continue
   fi

   # Add the rest of the elements to an array
   NEW+=("${i}")

done

# Reinitialize your array
ARR=(${NEW[@]})