4

I'm trying to do this, what is the most powerful way to attain this goal ?

#!/bin/bash

# Remove DOGS from CATSNDOGS to give CATS

DOGS="fido rover oscar bowwow spike max"

CATSNDOGS="bowwow figaro pussy oscar boots rover kitty max spike meowser fluffles fido"

CATS="" #?? How do I do this?

7 Answers 7

2

The comm answer is innovative, but of course not the only method. You can also do this purely in bash, without using extra tools.

#!/bin/bash

DOGS="fido rover oscar bowwow spike max"
CATSNDOGS="bowwow figaro pussy oscar boots rover kitty max spike meowser fluffles fido"

# make an associative array...
declare -A dogs_a
for dog in $DOGS; do
  dogs_a[$dog]=1;
done

CATS=""
# step through everything
for beast in $CATSNDOGS; do
  # if it's not a dog...
  if [ -z "${dogs_a[$beast]}" ]; then
    CATS="$CATS $beast"
  fi
done

echo $CATS

Note that this also relies on spaces as field separators, and you should read about always wrapping your variables in quotes when programming in bash.

Sign up to request clarification or add additional context in comments.

Comments

2

Pure Bash (note the blanks):

CATS=" $CATSNDOGS "

for dog in  $DOGS ; do
  CATS=${CATS/ $dog / }
done

echo -e "CATS : '$CATS'"

The result:

CATS : ' figaro pussy boots kitty meowser fluffles '

Comments

1

You could do it with the program comm. The -3 option gets rid of matching lines (not words), and the inputs need to be sorted, so there is a little more to it. Something like this:

comm -3 <(echo $DOGS | tr ' ' '\n' | sort) <(echo $CATSNDOGS | tr ' ' '\n' | sort)

To support your original input format (with spaces) and avoid creating temporary files, we transform spaces to newlines, sort both inputs, and use them as "virtual" file arguments to comm.

Edit: I didn't capture the output, it'll just be printed to stdout. You could say CATS=$(...) to store it, though you may need to massage it slightly to get back to spaces if that's what you want.

1 Comment

It breaks if a dog is not in $CATSNDOGS. To solve this, the -1 option could be added.
1

another approach:

for i in $CATSNDOGS
do
        skip=0
        for j in $DOGS
        do
                if [ "$j" == "$i" ]; then
                        skip=1
                else
                        continue
                fi
        done
        if [ "$skip" == "0" ]; then
          CATS="$CATS $i"
        else
          continue
        fi
done

echo -e "cats: $CATS"

However I like ghoti's version with associative arrays more.

Comments

1

In a single command, keeping the order of the cats, but using complex sed logic:

sed -e 'N;s/^/ /;s/$/ /;s/\n/ \n /;bbegin' \
    -e ':begin;s/ \(.*\) \(.*\)\n\(.*\) \1 / \2\n\3 /;tbegin' \
    -e 's/^ //;s/ \n //' << EOF
$CATSNDOGS
$DOGS
EOF

This is the logic explained:

  1. Put $CATSNDOGS and $DOGS on the same line, delimited by a new line (\n).
  2. Add a space before and after $CATSNDOGS and $DOGS to ease the following logic.
  3. If a word is found before and after the newline, remove it.
  4. Try again above as long as a word is remove.
  5. Before printing, remove the leading space and trailing space and new line.

Edit:

I realize that above breaks if a dog is not in $CATSNDOG or if a dog is twice in $CATSNDOG. The improved version is:

sed -e 'N;s/^/ /;s/$/ /;s/\n/ \n /;bbegin' \
    -e ':begin;s/ \(.*\) \(.*\)\n\(.*\) \1 / \2\n\3 \1 /;tbegin' \
    -e 's/^ //;s/ \n.*//' << EOF
$CATSNDOGS
$DOGS
EOF

Comments

0

This is a job for join using the print unpairable lines (-a) argument. Then we keep the lines ending with a space, and remove that space. To avoid using temporary files, we use bash process substitution.

join -a 1 -j 1 -o 1.1,2.1 \
  <(tr " " "\n" <<< "$CATSNDOGS" | sort) \
  <(tr " " "\n" <<< "$DOGS" | sort) | sed -e '/ $/!d;s/ //'

It looses the initial order of $CATSNDOGS, but we could easily add a cat -n and a sort to get back the initial ordering.

To put that back in a variable, do:

CATS="$(join -a 1 -j 1 -o 1.1,2.1 \
  <(tr " " "\n" <<< "$CATSNDOGS" | sort) \
  <(tr " " "\n" <<< "$DOGS" | sort) | sed -e '/ $/!d;s/ //' | paste -s -d " ")"

Comments

0

another bash-only approach

cats=()
for animal in $CATSNDOGS; do
  if [[ " $DOGS " == *" $animal "* ]]; then
    # animal is a dog
  else
    cats+=$animal
  fi
done
echo "${cats[@]}"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.