Regex não ganancioso (relutante) correspondente no sed?

406

Estou tentando usar o sed para limpar linhas de URLs para extrair apenas o domínio.

Então de:

http://www.suepearson.co.uk/product/174/71/3816/

Eu quero:

http://www.suepearson.co.uk/

(com ou sem a barra à direita, não importa)

Eu tentei:

 sed 's|\(http:\/\/.*?\/\).*|\1|'

e (escapando do quantificador não ganancioso)

sed 's|\(http:\/\/.*\?\/\).*|\1|'

mas não consigo fazer com que o quantificador não ganancioso ( ?) funcione, portanto ele sempre corresponde à seqüência inteira.

Joel
fonte
54
Uma observação: se você delimitar suas expressões regulares com "|", não precisará escapar dos "/" s. De fato, a maioria das pessoas delimita com "|" em vez de "/" s para evitar as "cercas".
AttishOculus
12
@AttishOculus O primeiro caractere após o 's' em uma expressão substituta no sed é o delimitador. Por isso, ^ foo ^ bar ^ 'ou' foo! Bar! ' também funciona #
Squidly
1
Para regex estendido, use sed -E 's.... Ainda assim, nenhum operador relutante.
Ondra Žižka
Não responda ao título da pergunta, mas, neste caso específico, cut -d'/' -f1-3funciona simples .
18719 Petr Javorik

Respostas:

421

Nem o Regex Posix / GNU básico nem estendido reconhece o quantificador não ganancioso; você precisa de uma regex posterior. Felizmente, o regex Perl para esse contexto é muito fácil de obter:

perl -pe 's|(http://.*?/).*|\1|'
caos
fonte
12
Para fazer isso no local, use as opções -pi -e.
reallynice
11
Caramba, eu não acredito que funcionou :-) A única coisa que é péssima agora é que meu script tem uma dependência de Perl :-( No lado positivo, praticamente todas as distribuições de Linux têm Perl já que provavelmente não é um problema :-)
Freedom_Ben
6
@Freedom_Ben: IIRC perlé exigido pelo POSIX
MestreLion
4
@ dolphus333: "Nem o Regex Posix / GNU básico nem estendido reconhece o quantificador não ganancioso" significa "você não pode usar o quantificador não ganancioso no sed".
caos
3
@ Sérgio é como você fazer a coisa solicitado, que está no impossível sed, usando uma sintaxe basicamente idêntico ao desed
caos
251

Nesse caso específico, você pode fazer o trabalho sem usar uma regex não gananciosa.

Experimente este regex não ganancioso em [^/]*vez de .*?:

sed 's|\(http://[^/]*/\).*|\1|g'
quiabo
fonte
3
Como fazer com que o sed match não seja ganancioso com uma frase usando esta técnica?
user3694243
6
Infelizmente você não pode; veja a resposta do caos .
21717 Daniel H:
Muito obrigado ... já que o perl não está mais na base de instalação padrão em muitas distribuições linux!
st0ne
@DanielH Na verdade, é possível corresponder frases de forma não avarenta usando esta técnica, conforme solicitado. Pode ser necessário escrever um padrão com precisão suficiente. Por exemplo, ao analisar uma atribuição de valor-chave na consulta de uma URL, pode ser necessário pesquisar a atribuição usando ([^&=#]+)=([^&#]*). Existem casos que não funcionam dessa maneira com certeza, por exemplo, ao analisar o URL da parte do host e do nome do caminho com a barra final considerada opcional, excluída da captura:^(http:\/\/.+?)/?$
Thomas Urban
121

Com o sed, eu geralmente implemento uma pesquisa não gananciosa procurando qualquer coisa, exceto o separador até o separador:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*\)/.*;\1;p'

Resultado:

http://www.suon.co.uk

isto é:

  • não produza -n
  • pesquisar, corresponder padrão, substituir e imprimir s/<pattern>/<replace>/p
  • use o ;separador de comandos de pesquisa em vez de/ facilitar a digitação,s;<pattern>;<replace>;p
  • lembre-se da correspondência entre colchetes \(... \), mais tarde acessível com \1, \2...
  • partida http://
  • seguido por qualquer coisa em colchetes [], [ab/]significaria tanto aou bou/
  • primeiro ^em []meios not, então seguido por qualquer coisa, menos a coisa no[]
  • isso [^/]significa qualquer coisa, exceto /caráter
  • *é repetir o grupo anterior, o que [^/]*significa caracteres, exceto /.
  • até agora sed -n 's;\(http://[^/]*\)significa pesquisar e lembrar http://seguido por qualquer caractere, exceto /e lembrar o que você encontrou
  • queremos pesquisar até o final do domínio, para parar no próximo /e adicionar outro /no final: sed -n 's;\(http://[^/]*\)/'mas queremos corresponder ao restante da linha após o domínio, para adicionar.*
  • agora que a correspondência lembrada no grupo 1 ( \1) é o domínio, substitua a linha correspondente pelas coisas salvas no grupo \1e imprima:sed -n 's;\(http://[^/]*\)/.*;\1;p'

Se você deseja incluir barra invertida também após o domínio, adicione mais uma barra invertida no grupo para lembrar:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*/\).*;\1;p'

resultado:

http://www.suon.co.uk/
stefanB
fonte
8
Com relação às edições recentes: parênteses são um tipo de caractere entre colchetes, portanto, não é incorreto chamá-los entre colchetes, especialmente se você seguir a palavra com os caracteres reais, como o autor fez. Além disso, é o uso preferido em algumas culturas, portanto, substituí-lo pelo uso preferido em sua própria cultura parece um pouco rude, embora eu tenha certeza de que não é isso que o editor pretendia. Pessoalmente, acho melhor usar nomes puramente descritivos, como colchetes , colchetes e angulares .
Alan Moore
2
É possível substituir o separador por uma string?
Cálculo
37

O sed não suporta o operador "não ganancioso".

Você precisa usar o operador "[]" para excluir "/" da correspondência.

sed 's,\(http://[^/]*\)/.*,\1,'

PS: não há necessidade de barra invertida "/".

andcoz
fonte
Na verdade não. se o delimitador puder ser um dos muitos caracteres possíveis (digamos apenas uma sequência de números), sua correspondência de negação pode ficar cada vez mais complexa. que é bom, mas certamente seria bom ter uma opção para fazer * não ganancioso.
Gesell
1
A questão era mais geral. Essas soluções funcionam para URLs, mas não (por exemplo) para o meu caso de uso de eliminar zeros à direita. s/([[:digit:]]\.[[1-9]]*)0*/\1/obviamente não funcionaria bem 1.20300. Como a pergunta original era sobre URLs, eles deveriam ser mencionados na resposta aceita.
11557 Daniel N:
33

Simulando um quantificador preguiçoso (não guloso) no sed

E todos os outros sabores de regex!

  1. Localizando a primeira ocorrência de uma expressão:

    • POSIX ERE (usando a -ropção)

      Regex:

      (EXPRESSION).*|.

      Sed:

      sed -r 's/(EXPRESSION).*|./\1/g' # Global `g` modifier should be on

      Exemplo (localizando a primeira sequência de dígitos) Demonstração ao vivo :

      $ sed -r 's/([0-9]+).*|./\1/g' <<< 'foo 12 bar 34'
      12

      Como isso funciona ?

      Esse regex se beneficia de uma alternância |. Em cada posição, o mecanismo tenta escolher a correspondência mais longa (este é um padrão POSIX que também é seguido por outros dois mecanismos), o que significa que continua .até que uma correspondência seja encontrada ([0-9]+).*. Mas a ordem também é importante.

      insira a descrição da imagem aqui

      Como o sinalizador global está definido, o mecanismo tenta continuar correspondendo caractere por caractere até o final da string de entrada ou nosso destino. Assim que o primeiro e único grupo de captura do lado esquerdo da alternância for correspondido, o (EXPRESSION)restante da linha também será consumido imediatamente .*. Agora mantemos nosso valor no primeiro grupo de captura.

    • POSIX BRE

      Regex:

      \(\(\(EXPRESSION\).*\)*.\)*

      Sed:

      sed 's/\(\(\(EXPRESSION\).*\)*.\)*/\3/'

      Exemplo (localizando a primeira sequência de dígitos):

      $ sed 's/\(\(\([0-9]\{1,\}\).*\)*.\)*/\3/' <<< 'foo 12 bar 34'
      12

      Este é como a versão ERE, mas sem alternância envolvida. Isso é tudo. Em cada posição única, o mecanismo tenta corresponder a um dígito.

      insira a descrição da imagem aqui

      Se for encontrado, outros dígitos a seguir serão consumidos e capturados e o restante da linha corresponderá imediatamente, caso contrário, *significa que mais ou zero pula sobre o segundo grupo de captura \(\([0-9]\{1,\}\).*\)*e chega a um ponto .para corresponder a um único caractere e esse processo continua.

  2. Localizando a primeira ocorrência de um delimitado expressão :

    Essa abordagem corresponderá à primeira ocorrência de uma sequência delimitada. Podemos chamá-lo de um bloco de string.

    sed 's/\(END-DELIMITER-EXPRESSION\).*/\1/; \
         s/\(\(START-DELIMITER-EXPRESSION.*\)*.\)*/\1/g'

    String de entrada:

    foobar start block #1 end barfoo start block #2 end

    -EDE: end

    -SDE: start

    $ sed 's/\(end\).*/\1/; s/\(\(start.*\)*.\)*/\1/g'

    Resultado:

    start block #1 end

    O primeiro regex \(end\).*corresponde e captura o primeiro delimitador final ende substitui todos com caracteres capturados recentes, que é o delimitador final. Nesta fase, a nossa saída é: foobar start block #1 end.

    insira a descrição da imagem aqui

    Em seguida, o resultado é passado para o segundo regex \(\(start.*\)*.\)*igual à versão POSIX BRE acima. Ele corresponde a um único caractere se o delimitador inicial startnão for correspondido, caso contrário ele corresponderá e captura o delimitador inicial e o restante dos caracteres.

    insira a descrição da imagem aqui


Respondendo diretamente à sua pergunta

Usando a abordagem nº 2 (expressão delimitada), você deve selecionar duas expressões apropriadas:

  • EDE: [^:/]\/

  • SDE: http:

Uso:

$ sed 's/\([^:/]\/\).*/\1/g; s/\(\(http:.*\)*.\)*/\1/' <<< 'http://www.suepearson.co.uk/product/174/71/3816/'

Resultado:

http://www.suepearson.co.uk/

Nota: isso não funcionará com delimitadores idênticos.

revo
fonte
3) ao sugerir sites como o regex101 para demonstração, adicione uma nota que nem sempre é adequado para ferramentas cli devido a diferenças de sintaxe e de recursos
Sundeep 27/04
1
@ Sundeep Obrigado. Transformei todas essas aspas em aspas simples. Também considerei a regra de correspondência mais longa à esquerda mencionada. No entanto, em sedtodos os outros mecanismos que seguem a mesma ordem padrão , importa quando se trata de igualdade. Portanto echo 'foo 1' | sed -r 's/.|([0-9]+).*/\1/g', não tem correspondência, mas echo 'foo 1' | sed -r 's/([0-9]+).*|./\1/g'sim.
revo 27/04
O @Sundeep também a solução alternativa para expressões delimitadas não funcionou para delimitadores iniciais e finais idênticos aos quais adicionei uma observação.
revo 27/04
grande ponto sobre o que acontece quando diferentes alternâncias começar do mesmo local e têm o mesmo comprimento, acho que vai seguir a ordem da esquerda para a direita como outros motores .. necessidade de olhar para cima se que é descrito no manual
Sundeep
existe um caso estranho aqui: stackoverflow.com/questions/59683820/…
Sundeep
20

Solução não gananciosa para mais de um único caractere

Este tópico é realmente antigo, mas eu suponho que as pessoas ainda precisem. Digamos que você queira matar tudo até a primeira ocorrência de HELLO. Você não pode dizer [^HELLO]...

Portanto, uma boa solução envolve duas etapas, supondo que você possa poupar uma palavra única que não está esperando na entrada, digamos top_sekrit.

Nesse caso, podemos:

s/HELLO/top_sekrit/     #will only replace the very first occurrence
s/.*top_sekrit//        #kill everything till end of the first HELLO

Obviamente, com uma entrada mais simples, você poderia usar uma palavra menor, ou talvez até um único caractere.

HTH!

ishahak
fonte
4
Para torná-lo ainda melhor, útil em situações em que você não pode esperar caracteres não utilizados: 1. substitua esse caractere especial por WORD realmente não utilizado, 2. substitua a sequência final pelo caractere especial, 3. faça a pesquisa que termina com caractere especial, 4 5. substituir caracteres especiais de volta, 5. substituir palavras especiais de volta. Por exemplo, você deseja um operador ganancioso entre <hello> e </hello>:
Jakub
3
Aqui exemplo: echo "Find: <hello> fir ~ st <br> yes </hello> <hello> sec ~ ond </hello>" | sed -e "s, ~, MUITO ESPECIAL, g" -e "s, </hello>, ~, g" -e "s,. * Encontre: <hello> ([^ ~] *). *, \ 1 , "-e" s, \ ~, </hello>, "-e" s, MUITO ESPECIAL, ~, ""
Jakub
2
Concordo. boa solução. Eu reformularia o comentário para dizer: se você não pode confiar em ~ não ser usado, substitua as ocorrências atuais primeiro usando s / ~ / VERYspeciaL / g, faça o truque acima e retorne o original ~ usando s / VERYspeciaL / ~ / g
Ishahak 28/05
1
Eu gosto de usar "variáveis" mais raras para esse tipo de coisa, então, em vez de `, eu usaria <$$>(já que se $$expande para o ID do processo no shell, embora você precise usar aspas duplas em vez de aspas simples, e isso pode quebrar outras partes do seu regex) ou, se o unicode estiver disponível, algo como <∈∋>.
Adam Katz
Em algum momento, você deve se perguntar por que não está apenas usando perlou pythonou algum outro idioma. perlfaz isso de uma maneira menos frágil em uma única linha ...
ArtOfWarfare
18

sed - correspondência não gananciosa de Christoph Sieghart

O truque para obter uma correspondência não gananciosa no sed é corresponder a todos os caracteres, exceto aquele que termina a correspondência. Eu sei, um acéfalo, mas desperdicei minutos preciosos e os scripts de shell devem ser, afinal, rápidos e fáceis. Portanto, caso alguém mais precise:

Correspondência gananciosa

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Correspondência não gananciosa

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar
gresolio
fonte
17

Isso pode ser feito usando o cut:

echo "http://www.suepearson.co.uk/product/174/71/3816/" | cut -d'/' -f1-3
Dee
fonte
9

outra maneira, não usando regex, é usar o método de campos / delimitadores, por exemplo

string="http://www.suepearson.co.uk/product/174/71/3816/"
echo $string | awk -F"/" '{print $1,$2,$3}' OFS="/"
ghostdog74
fonte
5

sed certamente tem o seu lugar, mas este não é um deles!

Como Dee apontou: Basta usar cut. É muito mais simples e muito mais seguro nesse caso. Aqui está um exemplo em que extraímos vários componentes da URL usando a sintaxe do Bash:

url="http://www.suepearson.co.uk/product/174/71/3816/"

protocol=$(echo "$url" | cut -d':' -f1)
host=$(echo "$url" | cut -d'/' -f3)
urlhost=$(echo "$url" | cut -d'/' -f1-3)
urlpath=$(echo "$url" | cut -d'/' -f4-)

da-te:

protocol = "http"
host = "www.suepearson.co.uk"
urlhost = "http://www.suepearson.co.uk"
urlpath = "product/174/71/3816/"

Como você pode ver, essa é uma abordagem muito mais flexível.

(todo o crédito para Dee)

peterh
fonte
3
sed 's|(http:\/\/[^\/]+\/).*|\1|'
Lucero
fonte
1
Se você usar "|" como seu separador, não há necessidade de escapar "/".
Michael Back
3

sed -E interpreta expressões regulares como expressões regulares estendidas (modernas)

Atualização: -E no MacOS X, -r no GNU sed.

stepancheg
fonte
4
Não, não ... Pelo menos não GNU sed.
Michel de Ruiter
7
Mais amplamente, -Eé exclusivo do BSD sede, portanto, do OS X. Links para páginas de manual. -rtraz expressões regulares estendidas para o GNU,sed conforme observado na correção de @ stephancheg. Cuidado ao usar um comando de variabilidade conhecida nas distribuições 'nix. Eu aprendi isso da pior maneira.
24512 fny
1
Esta é a resposta correta se você deseja usar o sed e é a mais aplicável à pergunta inicial.
Will Tice
8
A -ropção GNU sed apenas altera as regras de escape, de acordo com Appendix A Extended regular expressionso arquivo de informações e alguns testes rápidos; ele realmente não adicionar um qualificador não ganancioso (a partir de GNU sed version 4.2.1, pelo menos.)
Eichin
1
O GNU sed reconhecido -Ecomo uma opção não documentada por um tempo, mas na versão 4.2.2.177 , a documentação foi atualizada para refletir isso, o que -Eé bom para os dois agora.
Benjamin W.
3

Ainda há esperança de resolver isso usando puro (GNU) sed. Apesar de essa não ser uma solução genérica, em alguns casos, você pode usar "loops" para eliminar todas as partes desnecessárias da string como esta:

sed -r -e ":loop" -e 's|(http://.+)/.*|\1|' -e "t loop"
  • -r: use regex estendido (para parênteses + e sem escape)
  • ": loop": define um novo rótulo chamado "loop"
  • -e: adiciona comandos ao sed
  • "t loop": volte ao rótulo "loop" se houver uma substituição bem-sucedida

O único problema aqui é que ele também cortará o último caractere separador ('/'), mas se você realmente precisar, pode simplesmente colocá-lo de volta depois que o "loop" terminar, basta acrescentar este comando adicional no final do anterior linha de comando:

-e "s,$,/,"
mTUX
fonte
2

Como você declarou especificamente que está tentando usar sed (em vez de perl, cut, etc.), tente agrupar. Isso evita que o identificador não ganancioso potencialmente não seja reconhecido. O primeiro grupo é o protocolo (ou seja, 'http: //', 'https: //', 'tcp: //', etc). O segundo grupo é o domínio:

eco "http://www.suon.co.uk/product/1/7/3/" | sed "s | ^ \ (. * // \) \ ([^ /] * \). * $ | \ 1 \ 2 |"

Se você não está familiarizado com o agrupamento, comece aqui .

BrianB
fonte
1

Sei que essa é uma entrada antiga, mas alguém pode achar útil. Como o nome completo do domínio não pode exceder um comprimento total de 253 caracteres, substitua. * Por. \ {1, 255 \}

Iain Henderson
fonte
1

É assim que se faz uma correspondência não gananciosa de seqüências de caracteres múltiplos usando sed. Digamos que você queira alterar todos os itens foo...barpara <foo...bar>, por exemplo, esta entrada:

$ cat file
ABC foo DEF bar GHI foo KLM bar NOP foo QRS bar TUV

deve se tornar esta saída:

ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

Para fazer isso, você converte foo e barra em caracteres individuais e, em seguida, use a negação desses caracteres entre eles:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/g; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

No exemplo acima:

  1. s/@/@A/g; s/{/@B/g; s/}/@C/gestá convertendo {e }em seqüências de espaço reservado que não podem existir na entrada, para que esses caracteres estejam disponíveis para conversão fooebar para.
  2. s/foo/{/g; s/bar/}/gestá convertendo fooe barpara {e} respectivamente
  3. s/{[^{}]*}/<&>/gestá realizando a operação que queremos - convertendo foo...barpara<foo...bar>
  4. s/}/bar/g; s/{/foo/gestá convertendo {e de }volta para fooebar .
  5. s/@C/}/g; s/@B/{/g; s/@A/@/g está convertendo as seqüências de caracteres do espaço reservado em seus caracteres originais.

Observe que o acima exposto não depende de nenhuma string em particular não estar presente na entrada, pois ela fabrica essas strings na primeira etapa, nem se importa com a ocorrência de qualquer regexp em particular que você deseja corresponder, pois você pode usar {[^{}]*}quantas vezes for necessário. na expressão para isolar a correspondência real desejada e / ou com o operador de correspondência numérica seds, por exemplo, para substituir apenas a 2ª ocorrência:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/2; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC foo DEF bar GHI <foo KLM bar> NOP foo QRS bar TUV
Ed Morton
fonte
1

Ainda não vi essa resposta, então veja como você pode fazer isso com viou vim:

vi -c '%s/\(http:\/\/.\{-}\/\).*/\1/ge | wq' file &>/dev/null

Isso executa a vi :%ssubstituição globalmente (a direita g), evita gerar um erro se o padrão não for encontrado ( e) e salva as alterações resultantes no disco e sai. Os &>/dev/nullimpede a GUI a partir brevemente piscando na tela, que pode ser irritante.

Eu gosto de usar vialgumas vezes para regexes super-complicado, porque (1) perl é morto morte, (2) vim tem um muito motor regex avançado, e (3) Eu já estou intimamente familiarizado com viexpressões regulares na minha edição de uso do dia-a-dia documentos.

Luke Davis
fonte
0
echo "/home/one/two/three/myfile.txt" | sed 's|\(.*\)/.*|\1|'

não se preocupe, eu o comprei em outro fórum :)

Dee
fonte
4
assim que você começa jogo gananciosos: /home/one/two/three/, se você adicionar outro /como /home/one/two/three/four/myfile.txtvocê vai avidamente corresponder fourbem: /home/one/two/three/four, a pergunta é sobre não-gananciosos
stefanB
0

sed 's|\(http:\/\/www\.[a-z.0-9]*\/\).*|\1| funciona também

GL2014
fonte
0

Aqui está algo que você pode fazer com uma abordagem em duas etapas e o awk:

A=http://www.suepearson.co.uk/product/174/71/3816/  
echo $A|awk '  
{  
  var=gensub(///,"||",3,$0) ;  
  sub(/\|\|.*/,"",var);  
  print var  
}'  

Saída: http://www.suepearson.co.uk

Espero que ajude!

VINAY NAIR
fonte
0

Outra versão sed:

sed 's|/[:alnum:].*||' file.txt

Corresponde /seguido por um caractere alfanumérico (não outra barra), assim como o restante dos caracteres até o final da linha. Depois, o substitui por nada (ou seja, exclui-o.)

sycamorex
fonte
1
Eu acho que deveria ser "[[:alnum:]]", não "[:alphanum:]".
oli_arborum 30/09/19