Capturando grupos de um Grep RegEx

380

Eu tenho esse pequeno script no sh(Mac OSX 10.6) para examinar uma variedade de arquivos. O Google parou de ser útil neste momento:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

Até agora (obviamente, para você, shell gurus), $nameapenas contém 0, 1 ou 2, dependendo se for grepencontrado que o nome do arquivo corresponde ao problema fornecido. O que eu gostaria é capturar o que está dentro dos parênteses ([a-z]+)e armazenar isso em uma variável .

Eu gostaria de usar grepapenas, se possível . Se não, por favor, não Python ou Perl, etc. sedou algo parecido - eu sou novo no shell e gostaria de atacar isso do ângulo purista * nix.

Além disso, como um bônus super legal , estou curioso para saber como concatenar uma string com casca? O grupo que eu capturei era a string "somename" armazenada em $ name e eu queria adicionar a string ".jpg" ao final, não é cat $name '.jpg'?

Por favor, explique o que está acontecendo, se você tiver tempo.

Isaac
fonte
30
O grep é realmente mais puro do que o sed?
martin clayton
3
Ah, não quis sugerir isso. Eu só esperava que uma solução pudesse ser encontrada usando uma ferramenta que estou especificamente tentando aprender aqui. Se não for possível resolver usando grep, então sedseria ótimo, se é possível resolver usando sed.
Isaac
2
Eu deveria ter colocado um :) em que btw ...
martin clayton
Psh, meu cérebro está muito frito hoje haha.
Isaac
2
@martinclayton Isso seria um argumento interessante. Eu realmente acho que sed, (ou ed para ser mais preciso) seria mais antigo (e, portanto, mais puro? Talvez?) Unix porque grep deriva seu nome da expressão ed g (lobal) / re (expressão gular) / p (rint).
ffledgling

Respostas:

500

Se você estiver usando o Bash, nem precisará usar grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

É melhor colocar o regex em uma variável. Alguns padrões não funcionarão se incluídos literalmente.

Isso usa =~qual é o operador de correspondência de regex do Bash. Os resultados da correspondência são salvos em uma matriz chamada $BASH_REMATCH. O primeiro grupo de captura é armazenado no índice 1, o segundo (se houver) no índice 2, etc. O índice zero é a correspondência completa.

Você deve estar ciente de que, sem âncoras, essa regex (e a que está sendo usada grep) corresponderá a qualquer um dos seguintes exemplos e mais, que pode não ser o que você está procurando:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Para eliminar o segundo e o quarto exemplos, faça sua regex assim:

^[0-9]+_([a-z]+)_[0-9a-z]*

que diz que a sequência deve começar com um ou mais dígitos. O quilate representa o início da string. Se você adicionar um cifrão no final da regex, faça o seguinte:

^[0-9]+_([a-z]+)_[0-9a-z]*$

o terceiro exemplo também será eliminado, pois o ponto não está entre os caracteres na regex e o cifrão representa o final da string. Observe que o quarto exemplo também falha nessa correspondência.

Se você possui o GNU grep(por volta de 2.5 ou posterior, acho, quando o \Koperador foi adicionado):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

O \Koperador (look-behind de comprimento variável) faz com que o padrão anterior corresponda, mas não inclui a correspondência no resultado. O equivalente de comprimento fixo é (?<=)- o padrão seria incluído antes do parêntese de fechamento. Você deve usar \Kse os quantificadores podem coincidir com cordas de comprimentos diferentes (por exemplo +, *, {2,4}).

O (?=)operador corresponde a padrões de comprimento fixo ou variável e é chamado de "antecipação". Também não inclui a sequência correspondente no resultado.

Para fazer a correspondência não diferenciar maiúsculas de minúsculas, o (?i)operador é usado. Afeta os padrões que o seguem, portanto sua posição é significativa.

A regex pode precisar ser ajustada, dependendo da existência de outros caracteres no nome do arquivo. Você notará que, neste caso, mostro um exemplo de concatenação de uma sequência de caracteres ao mesmo tempo em que a substring é capturada.

Pausado até novo aviso.
fonte
48
Nesta resposta, desejo fazer um voto positivo na linha específica que diz "É melhor colocar o regex em uma variável. Alguns padrões não funcionarão se incluídos literalmente".
Brandin
5
@FrancescoFrassinelli: Um exemplo é um padrão que inclui espaço em branco. É difícil escapar e você não pode usar aspas, pois isso o força de uma expressão regular a uma sequência comum. A maneira correta de fazer isso é usar uma variável. As cotações podem ser usadas durante a tarefa, tornando as coisas muito mais simples.
Pausado até novo aviso.
5
/Koperador balança.
Razz
2
@Brandon: Funciona. Qual versão do Bash você está usando? Mostre-me o que você está fazendo que não funciona e talvez eu possa lhe dizer o porquê.
Pausado até novo aviso.
2
@mdelolmo: Minha resposta inclui informações sobre grep. Também foi aceito pelo OP e votou bastante. Obrigado pelo voto negativo.
Pausado até novo aviso.
145

Isso não é realmente possível com o puro grep, pelo menos geralmente não.

Mas se o seu padrão for adequado, você poderá usar grepvárias vezes em um pipeline para reduzir sua linha para um formato conhecido e, em seguida, extrair o pouco que desejar. (Embora ferramentas gostem cute sedsejam muito melhores nisso).

Suponha, por uma questão de argumento, que seu padrão seja um pouco mais simples: [0-9]+_([a-z]+)_você pode extrair isso da seguinte maneira:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

O primeiro grepremoveria todas as linhas que não correspondessem ao seu padrão geral; o segundo grep(que --only-matchingespecificou) exibirá a parte alfa do nome. Isso funciona apenas porque o padrão é adequado: "parte alfa" é específica o suficiente para extrair o que você deseja.

(Além disso: pessoalmente, eu usaria grep+ cutpara obter o que você procura:. echo $name | grep {pattern} | cut -d _ -f 2Isso cutanalisa a linha em campos dividindo o delimitador _e retorna apenas o campo 2 (os números dos campos começam em 1)).

A filosofia do Unix é ter ferramentas que fazem uma coisa, e fazê-lo bem, e combiná-las para realizar tarefas não triviais, então eu diria que grep+ sedetc é uma maneira mais Unixy de fazer as coisas :-)

RobM
fonte
3
for f in $files; do name=eco $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | corte -d _ -f 2 ;Aha!
Isaac
2
eu discordo dessa "filosofia". se você pode usar os recursos incorporados do shell sem chamar comandos externos, seu script terá desempenho muito mais rápido. existem algumas ferramentas que se sobrepõem em função. por exemplo, grep, sed e awk. todos eles manipulam cordas, mas o awk se destaca acima de todos porque pode fazer muito mais. Praticamente, todos os encadeamentos de comandos, como o double greps acima ou o grep + sed, podem ser reduzidos fazendo-os com um processo awk.
ghostdog74
7
@ ghostdog74: Nenhum argumento aqui de que encadear muitas operações minúsculas é geralmente menos eficiente do que fazer tudo em um só lugar, mas eu mantenho minha afirmação de que a filosofia do Unix é muitas ferramentas trabalhando juntas. Por exemplo, o tar apenas arquiva arquivos, ele não os compacta e, como ele é enviado para STDOUT por padrão, você pode canalizá-lo através da rede com netcat ou compactá-lo com bzip2, etc. ethos que as ferramentas Unix devem poder trabalhar juntas em pipes.
1013 RobM
corte é incrível - obrigado pela dica! Quanto ao argumento ferramentas versus eficiência, gosto da simplicidade das ferramentas de encadeamento.
ether_joe
adereços para opção o do grep, que é muito útil
chiliNUT
96

Percebo que uma resposta já foi aceita para isso, mas, de um "ângulo estritamente * nix purista", parece que é a ferramenta certa para o trabalho pcregrep, que parece não ter sido mencionada ainda. Tente alterar as linhas:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

para o seguinte:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

para obter apenas o conteúdo do grupo de captura 1.

A pcregrepferramenta utiliza a mesma sintaxe com a qual você já usou grep, mas implementa a funcionalidade necessária.

O parâmetro -ofunciona exatamente como a grepversão, se estiver vazio, mas também aceita um parâmetro numérico pcregrep, que indica qual grupo de captura você deseja mostrar.

Com esta solução, há um mínimo de mudanças necessárias no script. Você simplesmente substitui um utilitário modular por outro e ajusta os parâmetros.

Nota interessante: Você pode usar vários argumentos -o para retornar vários grupos de captura na ordem em que aparecem na linha.

John Sherwood
fonte
3
pcregrepnão está disponível por padrão no Mac OS Xque é o que os usos OP
grebneke
4
Meu pcregrepparece não entender o dígito após o -o: "Letra de opção desconhecida '1' em" -o1 ". Também não há menção dessa funcionalidade ao olharpcregrep --help
Peter Herdenborg
11
@ WAF desculpe, acho que eu deveria ter incluído essa informação no meu comentário. Estou no CentOS 6.5 ea versão pcregrep é aparentemente muito antiga: 7.8 2008-09-05.
Peter Herdenborg
2
sim, muito de ajuda, por exemploecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei
5
pcregrep8.41 (instalado com apt-get install pcregrepon Ubuntu 16.03) não reconhece o -Eicomutador. Funciona perfeitamente sem ele, no entanto. No macOS, com o pcregrepinstalado via homebrew(também 8.41), como @anishpatel menciona acima, pelo menos no High Sierra, o -Eswitch também não é reconhecido.
Ville
27

Não é possível apenas em grep eu acredito

para sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

Vou dar uma facada no bônus:

echo "$name.jpg"
cobbal
fonte
2
Infelizmente, essa sedsolução não funciona. Simplesmente imprime tudo no meu diretório.
Isaac
atualizado, saída será uma linha em branco se não houver uma correspondência, por isso certifique-se de verificar se há que
cobbal
Agora ele gera apenas linhas em branco!
Isaac
este sed tem um problema. O primeiro grupo de captura de parênteses abrange tudo. Claro que \ 2 não terá nada.
Ghostdog74
ele trabalhou para alguns casos de teste simples ... \ 2 recebe o grupo interno
cobbal
16

Esta é uma solução que usa gawk. É algo que eu acho que preciso usar com frequência, então criei uma função para ele

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

usar apenas fazer

$ echo 'hello world' | regex1 'hello\s(.*)'
world
opsb
fonte
Ótima idéia, mas parece não funcionar com espaços no regexp - eles precisam ser substituídos por \s. Sabes como arranjar isso?
Adam Ryczkowski
4

Uma sugestão para você - você pode usar a expansão de parâmetros para remover a parte do nome do último sublinhado em diante e da mesma forma no início:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Então nameterá o valor abc.

Consulte os documentos para desenvolvedores da Apple , procure 'Expansão de parâmetros'.

Martin Clayton
fonte
isso não verificará ([az] +).
ghostdog74
@levislevis - isso é verdade, mas, como comentado pelo OP, ele faz o que era necessário.
martin clayton
2

se você tem bash, pode usar globbing estendido

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

ou

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done
ghostdog74
fonte
Isso parece intrigante. Você poderia talvez acrescentar uma pequena explicação para isso? Ou, se você estiver inclinado, vincule-se a um recurso particularmente perspicaz que o explica? Obrigado!
Isaac