O IFS (Internal Field Separator) pode funcionar como um único separador para vários caracteres delimitadores consecutivos?

10

A análise de uma matriz usando o IFS com valores de espaço não brancos cria elementos vazios.
Mesmo o uso tr -spara encolher várias delimitações em uma única delim não é suficiente.
Um exemplo pode explicar o problema com mais clareza.
Existe uma maneira de obter resultados "normais" por meio de um ajuste do IFS (existe uma configuração associada para alterar o comportamento do IFS? ... ou seja, para agir da mesma forma que o espaço em branco padrão IFS.

var=" abc  def   ghi    "
echo "============== IFS=<default>"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
#
sfi="$IFS" ; IFS=':'
set -f # Disable file name generation (globbing)
       # (This  data won't "glob", but unless globbing     
       #  is actually needed, turn if off, because   
       #  unusual/unexpected combinations of data can glob!
       #  and they can do it in the most obscure ways...  
       #  With IFS, "you're not in Kansas any more! :)  
var=":abc::def:::ghi::::"
echo "============== IFS=$IFS"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
echo "============== IFS=$IFS and tr"
arr=($(echo -n "$var"|tr -s "$IFS"))
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
set +f     # enable globbing 
IFS="$sfi" # re-instate original IFS val
echo "============== IFS=<default>"

Aqui está a saída


============== IFS=<default>
# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
============== IFS=:
# arr[0] ""
# arr[1] "abc"
# arr[2] ""
# arr[3] "def"
# arr[4] ""
# arr[5] ""
# arr[6] "ghi"
# arr[7] ""
# arr[8] ""
# arr[9] ""
============== IFS=: and tr
# arr[0] ""
# arr[1] "abc"
# arr[2] "def"
# arr[3] "ghi"
============== IFS=<default>
Peter.O
fonte
Há uma melhor (eu acho) resposta a mesma pergunta: stackoverflow.com/a/14789518/1765658
F. Hauri

Respostas:

3

Para remover vários caracteres delimitadores consecutivos (sem espaço), duas expansões de parâmetros (string / array) podem ser usadas. O truque é definir a IFSvariável como a sequência vazia para a expansão do parâmetro da matriz.

Isso está documentado no man bashsob repartição de palavras :

Os argumentos nulos implícitos sem aspas, resultantes da expansão de parâmetros que não têm valores, são removidos.

(
set -f
str=':abc::def:::ghi::::'
IFS=':'
arr=(${str})
IFS=""
arr=(${arr[@]})

echo ${!arr[*]}

for ((i=0; i < ${#arr[@]}; i++)); do 
   echo "${i}: '${arr[${i}]}'"
done
)
nazad
fonte
Boa! Um método simples e eficaz - sem a necessidade de um loop bash e sem a necessidade de chamar um aplicativo utilitário - BTW. Como você mencionou "(não espaço)" , gostaria de salientar, para maior clareza, que ele funciona bem com qualquer combinação de caracteres delimitadores, incluindo espaço.
Peter.O
Nos meus testes, a configuração IFS=' '(ou seja, um espaço em branco) se comporta da mesma maneira. Acho isso menos confuso do que um argumento nulo explícito ("" ou '') de IFS.
Micha Wiedenmann
Essa é uma solução terrível se seus dados contiverem espaços em branco incorporados. Se seus dados forem 'a bc' em vez de 'abc', o IFS = "" dividiria 'a' em um elemento separado de 'bc'.
precisa saber é o seguinte
5

Da página de bashmanual:

Qualquer caractere no IFS que não seja espaço em branco do IFS, juntamente com qualquer caractere de espaço em branco do IFS adjacente, delimita um campo. Uma sequência de caracteres de espaço em branco do IFS também é tratada como um delimitador.

Isso significa que o espaço em branco do IFS (espaço, tabulação e nova linha) não é tratado como os outros separadores. Se você deseja obter exatamente o mesmo comportamento com um separador alternativo, pode fazer alguma troca de separador com a ajuda de trou sed:

var=":abc::def:::ghi::::"
arr=($(echo -n $var | sed 's/ /%#%#%#%#%/g;s/:/ /g'))
for x in ${!arr[*]} ; do
   el=$(echo -n $arr | sed 's/%#%#%#%#%/ /g')
   echo "# arr[$x] \"$el\""
done

A %#%#%#%#%coisa é um valor mágico para substituir os possíveis espaços dentro dos campos; espera-se que seja "único" (ou muito pouco). Se tiver certeza de que nunca haverá espaço nos campos, basta soltar esta parte).

jon_d
fonte
@FussyS ... Obrigado (veja as modificações na minha pergunta) ... Você pode ter me dado a resposta à minha pergunta pretendida ... e essa resposta pode ser (provavelmente é) "Não há como fazer com que o IFS se comporte no maneira que eu quero "... pretendo que os trexemplos mostrem o problema ... eu quero evitar uma chamada do sistema, então examinarei uma opção do bash além da ${var##:}que mencionei no meu comentário para o pesquisador de Glen .... . Eu vou esperar por um tempo .. talvez haja uma maneira de IFS coaxiais, caso contrário, a primeira parte da sua resposta for estava atrás ....
Peter.O
Esse tratamento IFSé o mesmo em todas as conchas no estilo Bourne, é especificado no POSIX .
Gilles 'SO- stop be evil'
Mais de 4 anos desde que fiz essa pergunta - achei a resposta do @ nazad (postada há um ano) como a maneira mais simples de manipular o IFS para criar uma matriz com qualquer número e combinação de IFScaracteres como delimitador-string. Minha pergunta foi melhor respondida por jon_d, mas a resposta de @ nazad mostra uma maneira bacana de usar, IFSsem loops e sem aplicativos utilitários.
Peter.O
2

Como o IFS bash não fornece uma maneira interna de tratar caracteres de delimitador consecutivos como um delimitador único (para delimitadores que não são de espaço em branco), eu montei uma versão totalmente básica (vs.usando uma chamada externa, por exemplo, tr, awk, sed )

Ele pode lidar com IFS com vários caracteres.

Aqui estão os resultados do tempo de execução, juntamente com testes semelhantes para as opções tre awkmostrados nesta página de perguntas e respostas ... Os testes são baseados em 10000 iterações de apenas construir o arrray (sem E / S) ...

pure bash     3.174s (28 char IFS)
call (awk) 0m32.210s  (1 char IFS) 
call (tr)  0m32.178s  (1 char IFS) 

Aqui está a saída

# dlm_str  = :.~!@#$%^&()_+-=`}{][ ";></,
# original = :abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'single*quote?'..123:
# unified  = :abc::::def::::::::::::::::::::::::::::'single*quote?'::123:
# max-w 2^ = ::::::::::::::::
# shrunk.. = :abc:def:'single*quote?':123:
# arr[0] "abc"
# arr[1] "def"
# arr[2] "'single*quote?'"
# arr[3] "123"

Aqui está o script

#!/bin/bash

# Note: This script modifies the source string. 
#       so work with a copy, if you need the original. 
# also: Use the name varG (Global) it's required by 'shrink_repeat_chars'
#
# NOTE: * asterisk      in IFS causes a regex(?) issue,     but  *  is ok in data. 
# NOTE: ? Question-mark in IFS causes a regex(?) issue,     but  ?  is ok in data. 
# NOTE: 0..9 digits     in IFS causes empty/wacky elements, but they're ok in data.
# NOTE: ' single quote  in IFS; don't know yet,             but  '  is ok in data.
# 
function shrink_repeat_chars () # A 'tr -s' analog
{
  # Shrink repeating occurrences of char
  #
  # $1: A string of delimiters which when consecutively repeated and are       
  #     considered as a shrinkable group. A example is: "   " whitespace delimiter.
  #
  # $varG  A global var which contains the string to be "shrunk".
  #
# echo "# dlm_str  = $1" 
# echo "# original = $varG" 
  dlms="$1"        # arg delimiter string
  dlm1=${dlms:0:1} # 1st delimiter char  
  dlmw=$dlm1       # work delimiter  
  # More than one delimiter char
  # ============================
  # When a delimiter contains more than one char.. ie (different byte` values),    
  # make all delimiter-chars in string $varG the same as the 1st delimiter char.
  ix=1;xx=${#dlms}; 
  while ((ix<xx)) ; do # Where more than one delim char, make all the same in varG  
    varG="${varG//${dlms:$ix:1}/$dlm1}"
    ix=$((ix+1))
  done
# echo "# unified  = $varG" 
  #
  # Binary shrink
  # =============
  # Find the longest required "power of 2' group needed for a binary shrink
  while [[ "$varG" =~ .*$dlmw$dlmw.* ]] ; do dlmw=$dlmw$dlmw; done # double its length
# echo "# max-w 2^ = $dlmw"
  #
  # Shrik groups of delims to a single char
  while [[ ! "$dlmw" == "$dlm1" ]] ; do
    varG=${varG//${dlmw}$dlm1/$dlm1}
    dlmw=${dlmw:$((${#dlmw}/2))}
  done
  varG=${varG//${dlmw}$dlm1/$dlm1}
# echo "# shrunk.. = $varG"
}

# Main
  varG=':abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'\''single*quote?'\''..123:' 
  sfi="$IFS"; IFS=':.~!@#$%^&()_+-=`}{][ ";></,' # save original IFS and set new multi-char IFS
  set -f                                         # disable globbing
  shrink_repeat_chars "$IFS" # The source string name must be $varG
  arr=(${varG:1})    # Strip leading dlim;  A single trailing dlim is ok (strangely
  for ix in ${!arr[*]} ; do  # Dump the array
     echo "# arr[$ix] \"${arr[ix]}\""
  done
  set +f     # re-enable globbing   
  IFS="$sfi" # re-instate the original IFS
  #
exit
Peter.O
fonte
Bom trabalho, +1 interessante!
10293 F. Hauri
1

Você pode fazer isso com o gawk também, mas não é bonito:

var=":abc::def:::ghi::::"
out=$( gawk -F ':+' '
  {
    # strip delimiters from the ends of the line
    sub("^"FS,"")
    sub(FS"$","")
    # then output in a bash-friendly format
    for (i=1;i<=NF;i++) printf("\"%s\" ", $i)
    print ""
  }
' <<< "$var" )
eval arr=($out)
for x in ${!arr[*]} ; do
  echo "# arr[$x] \"${arr[x]}\""
done

saídas

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
Glenn Jackman
fonte
Obrigado ... Parece que não fui claro no meu pedido principal (pergunta modificada) ... É fácil fazê-lo apenas alterando o meu $varpara ${var##:}... Estava realmente procurando uma maneira de ajustar o próprio IFS .. Quero para fazer isso sem uma chamada externa (tenho a sensação de que o bash pode fazer isso com mais eficiência do que qualquer lata externa ... portanto, continuarei nessa trilha) ... seu método funciona (+1) .... como modifica a entrada, eu preferiria tentar com bash, em vez de awk ou tr (isso evitaria uma chamada do sistema), mas eu estou realmente esperando por um ajuste do IFS ...
Peter.O
@fred, como mencionado, o IFS reduz apenas vários delímetros consecutivos para o valor padrão de espaço em branco. Caso contrário, delimitadores consecutivos resultarão em campos vazios estranhos. Espero que uma ou duas chamadas externas sejam extremamente improváveis ​​de afetar o desempenho de maneira real.
Glenn Jackman
@glen .. (Você disse que sua resposta não é "bonita" .. Eu acho que é! :) No entanto, montei uma versão totalmente bash (vs uma chamada externa) e com base em 10000 iterações de apenas criar o arrray ( nenhum I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Do que algumas vezes e você pode pensar que o bash é lento! ... é awk mais fácil neste caso? ... não se você já possui o trecho :) ... eu o publicarei mais tarde; tenho que ir agora.
Peter.O
Aliás, re seu script gawk ... Eu basicamente não usei o awk antes, então eu estive olhando para ele (e outros) em detalhes ... Eu não sei por que, mas vou mencionar a questão de qualquer maneira .. Quando dados dados citados, eles perdem as aspas e se dividem em espaços entre as aspas .. e travam para números ímpares de aspas ... Aqui estão os dados de teste:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O
-1

A resposta simples é: reduza todos os delimitadores para um (o primeiro).
Isso requer um loop (que é executado menos que o log(N)tempo):

 var=':a bc::d ef:#$%_+$$%      ^%&*(*&*^
 $#,.::ghi::*::'                           # a long test string.
 d=':@!#$%^&*()_+,.'                       # delimiter set
 f=${d:0:1}                                # first delimiter
 v=${var//["$d"]/"$f"};                    # convert all delimiters to
 :                                         # the first of the delimiter set.
 tmp=$v                                    # temporal variable (v).
 while
     tmp=${tmp//["$f"]["$f"]/"$f"};        # collapse each two delimiters to one
     [[ "$tmp" != "$v" ]];                 # If there was a change
 do
     v=$tmp;                               # actualize the value of the string.
 done

Tudo o que resta a fazer é dividir corretamente a sequência em um delimitador e imprimi-la:

 readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
 printf '<%s>' "${arr[@]}" ; echo

Não há necessidade de set -fnem IFS mudança.
Testado com espaços, novas linhas e caracteres glob. Todo o trabalho. Muito lento (como se espera que um loop de shell seja).
Mas apenas para o bash (bash 4.4+ por causa da opção -dde readarray).


sh

Uma versão shell não pode usar uma matriz, a única matriz disponível são os parâmetros posicionais.
Usar tr -sé apenas uma linha (o IFS não muda no script):

 set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'

E imprima:

 printf '<%s>' "$@" ; echo

Ainda lento, mas não muito mais.

O comando commandé inválido no Bourne.
No zsh, commandchama apenas comandos externos e faz com que eval falhe se commandfor usado.
Em ksh, mesmo com command, o valor do IFS é alterado no escopo global.
E commandfaz com que a divisão falhe em shells relacionados ao mksh (mksh, lksh, posh) A remoção do comando commandfaz com que o código seja executado em mais shells. Mas: a remoção commandfará com que o IFS mantenha seu valor na maioria dos shells (eval é um built-in especial), exceto no bash (sem modo posix) e zsh no modo padrão (sem emulação). Esse conceito não pode ser criado para funcionar no zsh padrão com ou sem command.


IFS de vários caracteres

Sim, o IFS pode ter vários caracteres, mas cada caractere irá gerar um argumento:

 set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
 printf '<%s>' "$@" ; echo

Saída:

 <><a bc><><d ef><><><><><><><><><      ><><><><><><><><><
 ><><><><><><ghi><><><><><>

Com o bash, você pode omitir a commandpalavra se não estiver na emulação sh / POSIX. O comando falhará no ksh93 (o IFS mantém o valor alterado). No zsh, o comando commandfaz com que o zsh tente encontrar evalcomo um comando externo (que não encontra) e falha.

O que acontece é que os únicos caracteres IFS recolhidos automaticamente para um delimitador são os espaços em branco do IFS.
Um espaço no IFS recolherá todos os espaços consecutivos para um. Uma guia recolherá todas as guias. Um espaço e uma guia recolherão execuções de espaços e / ou guias em um delimitador. Repita a ideia com nova linha.

Para recolher vários delimitadores, é necessário algum malabarismo.
Supondo que ASCII 3 (0x03) não seja usado na entrada var:

 var=${var// /$'\3'}                       # protect spaces
 var=${var//["$d"]/ }                      # convert all delimiters to spaces
 set -f;                                   # avoid expanding globs.
 IFS=" " command eval set -- '""$var""'    # split on spaces.
 set -- "${@//$'\3'/ }"                    # convert spaces back.

A maioria dos comentários sobre ksh, zsh e bash (about commande IFS) ainda se aplica aqui.

Um valor de $'\0'seria menos provável na entrada de texto, mas as variáveis ​​bash não podem conter NULs ( 0x00).

Não há comandos internos no sh para executar as mesmas operações de cadeia, portanto, tr é a única solução para scripts sh.

Isaac
fonte
Sim, escrevi que para o shell o OP solicitava: Bash. Nesse shell, o IFS não é mantido. E sim, não é portátil, para zsh, por exemplo. @ StéphaneChazelas
Isaac
No caso de bash e zsh, eles se comportam como especifica POSIX quando invocado como sh
Stéphane Chazelas
@ StéphaneChazelas Adicionadas (muitas) notas sobre as limitações de cada shell.
Isaac
@ StéphaneChazelas Por que o voto negativo?
Isaac
Não sei, não fui eu. BTW, eu acho que há um dedicado Q & A aqui sobre command evalIIRC por Gilles
Stéphane Chazelas