2

If I want to delete non-matching lines in an array this works fine:

array=( ${(M)array:#*${filter}*} )

However, it compacts the array too. But I need to keep the array the same length because later on I'll be merging it with another array of the original length. That is, I want to 'blank' non matching lines but not actually delete them -- I don't want to change the length of the array or change the index number of lines that do match.

At the moment I'm working around the problem by replacing non-matching strings with a dummy string as a place holder, doing my merge and then deleting the dummy string but it's clumsy.

2
  • 1
    It does smell like a XY problem. I suspect that whatever you want to achieve would be more easily done some other way (like with some text-processing tool such as perl instead of using shell arrays as your mention of lines suggests). Nov 14, 2022 at 14:47
  • Ha! That was a most interesting link. Sure there might be other tools but I'll solve this in zsh if I can. My workaround is doing it, I'm just hoping for a more elegant code. Nov 14, 2022 at 15:11

2 Answers 2

4

Unquoted expansions do remove empty elements. You need double quotes and the @ flag or use [@] to preserve them like in Korn-like shells.

You'd also need to use the Korn-style ${var/pattern/replacement} instead of ${array:#pattern} as the latter is one to remove elements, not edit their contents. So:

set -o extendedglob
array=( "${array[@]/#%^*$filter*}" )
print -rC1 -- "$array[@]"

The # following / is to anchor the match at the start, % to anchor at the end (same as in the Korn shell except that you can't combine them in ksh) so that we match the pattern as a whole; in the ${param:#pattern} operator anchoring is implicit, pattern has to match the contents of $param as a whole while with ${param#pattern} it's anchored at the start only and with ${param%pattern} at the end.

^ is the extendedglob negation operator.

Now, zsh expansion operators can get quite hairy especially when you start combining several of them, but you could always do as you would in most other languages and iterate over the array elements.

Like editing the elements in place:

for (( i = 1; i <= $#array; i++ ))
  if [[ $array[i] != *$filter* ]] array[i]=

Or create a new array:

new_array=()
for element ( "$array[@]" )
  case $element in
    (*$filter*) new_array+=( "$element" );;
    (*)         new_array+=( '' )
  esac

Or:

new_array=()
for element ( "$array[@]" ) {
  [[ $element = *$filter* ]] || element=
  new_array+=( "$element" )
}
7
  • That does it. I don't understand the '#%' tho. It doesn't work without it, but strings are matched anywhere in the line, there seems to be no reason to anchor -- more than that, it seems to be NOT anchored to either the beginning or end of the string since we get matches anywhere in the line. Nov 14, 2022 at 16:28
  • @RayAndrews, you ^*foo* matches any string that doesn't match *foo*. For instance, in foobar, fo and oobar are strings don't match *foo*. Here you want to replace only the whole contents if it doesn't match *foo*. Nov 14, 2022 at 16:46
  • Sure but what do the anchors have to do with that? What you say would seem to be true without any anchors yet they are needed. Nov 14, 2022 at 16:54
  • @RayAndrews let me try to phrase that differently. #%^*foo* matches on the start of the subject followed by any string other than those containing foo followed by the end of the subject. So matches the subject as a whole if it doesn't contain foo. ^*foo* without #% would match on the first string in the subject that does not contain foo (as long as possible). So if the subject (an array element) is foobar for instance, that would be fo, with barfoo, that would be barfo. Nov 14, 2022 at 17:04
  • But 'fo' does start at the beginning of the line anyway. Seems to me the '#' is redundant. Sorry for being so thick headed :( Nov 14, 2022 at 17:22
2
array=( "${(@M)array##*${filter}*}" )

This keeps all elements of the array, performing the ##pattern longest prefix removal on each element, but with the M flag so that only the matched part is kept. Double quotes and the @ flag are needed so that empty elements are not removed from the result.

3
  • It seems to produce the same results as Stephane's and it's easier to understand too. Unless I hear different I'll keep that one operational. Nov 16, 2022 at 20:22
  • Nice find, though note that it only works for patterns like here that end in *. For instance, you can't use it with a pattern like ??? to replace all elements not made of 3 characters with the empty string. Nov 17, 2022 at 5:01
  • 1
    @StéphaneChazelas IIUC ???(#e) (w/ set -o extendedglob) to anchor the pattern at the end should work in that case.
    – rowboat
    Nov 17, 2022 at 7:34

You must log in to answer this question.

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