Como remover valores de $ VAR2 de $ VAR1 e exportar os valores restantes para $ VAR3?

2

SO: kernel 2.6.x

Shell: shell compatível com POSIX

Utilitários: BusyBox 1,25

Pergunta: Como você remove os valores em $ VAR2 de $ VAR1 e envia os valores restantes para $ VAR3? Cada valor nas variáveis é separado por espaço.

Lógica:

VAR1="1 2 3 4 5"
VAR2="1 3 5"
for i in $VAR1
   if $i is not found in $VAR2; do
   append $i to $VAR3
   remove trailing space character
done

Saída desejada:

VAR3="2 4"
    
por uihdff 24.09.2017 / 23:33

3 respostas

2

Primeira variante simples, mas limitada

VAR3=$(printf "%d\n" $VAR1 $VAR2 | sort | uniq -u | tr '\n' ' ' | sed 's/\s$//)

$ echo "$VAR3"
2 4

A principal falha é: ele deixa apenas valores únicos da variável VAR1 . Ou seja, se $VAR1 tiver um valor repetido algumas vezes, esse valor não aparecerá no $VAR3 , porque não é exclusivo.

Exemplo:

VAR1="1 2 2 3 4 4 4 5"
VAR2="1 3 5"
# the resulting VAR3 variable is empty
VAR3 = "" # because it is containing only unique values and '2' and '4' repeated few times in the 'VAR1', therefore, they are not unique.

# The right result should be
VAR3 = "2 2 4 4 4" 

Segunda variante, mais universal e correta

VAR3=$(printf "%s\n" $VAR2 | awk -v var1="$VAR1" '
{arr2[$1] = 1;}

END {
    size = split(var1, arr1); 
    for(i = 1; i <= size; i++) {
        if(!arr2[arr1[i]]) 
            printf "%s ", arr1[i];
    }
}' | sed 's/\s$//')

Explicação

  1. printf "%s\n" $VAR2 - converte o $VAR2 para a coluna - um valor por linha.
  2. awk ... - remove os valores de $VAR2 de $VAR1 .

    • {arr2[$1] = 1;} - coloca todos os valores de VAR2 (eles são canalizados para awk por printf ) na matriz, onde os valores se tornam os índices da matriz. O = 1 significa apenas true - esse valor existe. Esse truque nos dá o próximo comportamento: a ocorrência do primeiro valor cria o elemento array, então, se o mesmo valor aparecer novamente, ele vai para o mesmo índice de array, ou seja, o item não muda, quando o mesmo valor aparece algumas vezes . Assim, no final, estamos tendo todos os valores exclusivos da variável VAR2 . Se VAR2="one three five" , então arr2 será: arr2[one] = 1, arr2[three] = 1, arr2[five] = 1 .
    • END { size = split(var1, arr1); - quando as linhas de entrada terminaram ( VAR2 processamento concluído), estamos dividindo o VAR1 na matriz - cada valor vai para o item separado. Se VAR1="one two three four five" , então, obteremos a seguinte matriz: arr1[1] = one, arr1[2] = two, arr1[3] = three ... , e assim por diante. A função split retorna o tamanho da nova matriz criada.
    • if(!arr2[arr1[i]]) printf "%s ", arr1[i]; - então, itera através de arr1 itens e verifica se arr2 tem índice para este item. Por exemplo: i = 1; arr1[1] = "one" then arr2[arr1[i]] é isso - arr2[one] . Este item existe, não imprima. %código%. O i = 2; arr1[2] = "two" não existe, portanto imprima-o. Assim, estamos imprimindo todos os valores do arr2[two] , que não aparecem no arr1 .
  3. arr2 - remove o espaço à direita.

Prós deste caminho em comparação com a primeira variante:

    # It can process strings
    VAR1="one two three four five"
    VAR2="one three five"
    # the resulting VAR3 variable
    VAR3 = "two four"

    # It doesn't remove multiple occurrence of one value in the VAR1
    VAR1="1 2 2 3 4 4 4 5"
    VAR2="1 3 5"
    # the resulting VAR3 variable
    VAR3 = "2 2 4 4 4"
    
por 25.09.2017 / 00:08
3

Parece que essas são uma lista de strings que você codifica, armazenando-as em espaços separados em uma variável escalar (assume que as strings não contêm esse caractere de espaço).

Seria mais sensato usar variáveis do tipo list / array, com shells que as suportam. Por exemplo, com zsh e seu operador de disjunção de matriz ${varX:|varY} :

VAR1=(1 2 3 4 5)
VAR2=(1 3 5)
VAR3=(${VAR1:|VAR2})

( VAR3=("${(@)VAR1:|VAR2}") para preservar elementos vazios)

Agora, se você estiver limitado a POSIX sh sem suporte à matriz, exceto $@ , terá que ser mais criativo.

O comando padrão para conjunção de lista e disjunção é comm . Mas as listas precisam ser fornecidas como uma lista ordenada, separada por novas linhas e arquivos internos cujo nome é passado como argumentos (embora - possa ser usado para um deles como stdin).

Então, aqui, torna-se estranho usar. Se o seu sistema suporta arquivos especiais /dev/fd/<n> :

VAR3=$(printf '%s\n' "$VAR1" | tr ' ' '\n' | sort | {
  printf '%s\n' "$VAR2" | tr ' ' '\n' | sort |
    comm -23 /dev/fd/3 -
} 3<&0 | paste -sd ' ' -)

Ou:

to_comm() { printf '%s\n' "$@" | tr ' ' '\n' | sort; }
from_comm() { paste -sd ' ' -; }
VAR3=$(to_comm "$VAR1" | { to_comm "$VAR2" | comm -23 /dev/fd/3 -;} 3<&0 |from_comm)

(que também assume que $VAR1 contém pelo menos um elemento (como você expressaria uma lista com um elemento vazio de forma diferente de uma lista vazia, BTW) e que os elementos não contêm caracteres de nova linha).

Então, você também pode implementá-lo manualmente. Faça um loop em cada elemento da primeira lista e procure-os na segunda lista.

Em shells POSIX, você pode usar o operador split + glob:

IFS=' ' # split on space
set -o noglob # we don't want the glob part
VAR3= sep=
for i in $VAR1; do
  case " $VAR2 " in
    (*" $i "*) ;;
    (*) VAR3=$VAR3$sep$i; sep=$IFS;;
  esac
done

Isso não pode ser usado se houver elementos vazios (como em VAR1=' 2 3' ou VAR1='1 3' ). Para isso, seria melhor usar um separador sem espaço em branco (como | below) para o qual as regras de divisão são diferentes:

VAR1='*|foo bar||blah' VAR2='|blah'
IFS='|' # split on |
set -o noglob # we don't want the glob part
VAR3= sep=
for i in $VAR1''; do
  # that $VAR1 split+glob invocation will split the content of $VAR1
  # into "*", "foo bar", "", "blah" while with IFS=" ", the empty
  # element wouldn't have been there as sequences of spaces would
  # have been seen as a single separator. 
  case "|$VAR2|" in
    (*"|$i|"*) ;;
    (*) VAR3=$VAR3$sep$i; sep=$IFS;;
  esac
done

O '' em $VAR1'' é para garantir que foo| seja dividido em "foo" e "" em vez de apenas "foo" , por exemplo, em shells POSIX (a maioria, como é um requisito POSIX) que trata $IFS como um campo terminator em vez de separador .

Ou você pode usar awk :

export VAR1 VAR2
VAR3=$(awk 'BEGIN{
  n = split(ENVIRON["VAR1"], a1, /[ ]/)
      split(ENVIRON["VAR2"], a2, /[ ]/)
  for (i in a2) in_a2[a2[i]]
  for (i = 1; i <= n; i++)
    if (! (a1[i] in in_a2)) $(++NF) = a1[i]
  print}')
    
por 25.09.2017 / 13:54
1
> echo $VAR1 $VAR2 | tr ' ' '\n' | sort | uniq --unique | tr '\n' ' '
2 4
    
por 25.09.2017 / 00:04