Extrair substring no Bash

728

Dado um nome de arquivo no formulário someletters_12345_moreleters.ext, quero extrair os 5 dígitos e colocá-los em uma variável.

Então, para enfatizar o ponto, eu tenho um nome de arquivo com um número x de caracteres e, em seguida, uma sequência de cinco dígitos cercada por um único sublinhado de cada lado e outro conjunto de x números de caracteres. Quero pegar o número de 5 dígitos e colocá-lo em uma variável.

Estou muito interessado no número de maneiras diferentes pelas quais isso pode ser realizado.

Berek Bryan
fonte
5
A resposta de JB está claramente ganhando os votos - hora de mudar a resposta aceita?
19418 Jeff
3
A maioria das respostas parece não responder à sua pergunta porque é ambígua. "Eu tenho um nome de arquivo com x número de caracteres e, em seguida, uma sequência de cinco dígitos cercada por um único sublinhado de cada lado e outro conjunto de x número de caracteres" . Por essa definição, abc_12345_def_67890_ghi_defé uma entrada válida. O que você quer que aconteça? Vamos supor que haja apenas uma sequência de 5 dígitos. Você ainda tem abc_def_12345_ghi_jklou 1234567_12345_1234567ou 12345d_12345_12345ecomo entrada válida com base na sua definição de entrada e a maioria das respostas abaixo não irá lidar com isso.
gman
2
Esta pergunta tem um exemplo de entrada que é muito específico. Por esse motivo, obteve muitas respostas específicas para este caso específico (apenas dígitos, mesmo _delimitador, entrada que contém a cadeia de destino apenas uma vez etc.). A melhor (mais genérica e mais rápida) resposta tem, após 10 anos, apenas 7 votos positivos, enquanto outras respostas limitadas têm centenas. Me faz perder a fé em desenvolvedores 😞
Dan Dascalescu

Respostas:

692

Usar corte :

echo 'someletters_12345_moreleters.ext' | cut -d'_' -f 2

Mais genérico:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING
FerranB
fonte
1
a resposta mais genérica é exatamente o que eu estava procurando, graças
Berek Bryan
71
O sinalizador -f aceita índices baseados em 1, em vez dos índices baseados em 0 aos quais um programador estaria acostumado.
Matthew G
2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $ (eco $ INPUT | cut -d'_ '-f 2) echo $ SUBSTRING
mani deepak
3
Você deve usar aspas duplas corretamente nos argumentos para echo, a menos que tenha certeza de que as variáveis ​​não podem conter espaços em branco irregulares ou metacaracteres de shell. Veja mais stackoverflow.com/questions/10067266/…
tripleee
O número '2' após '-f' é dizer ao shell para extrair o segundo conjunto de substring.
Sandun
1088

Se x for constante, a seguinte expansão de parâmetro executa a extração de substring:

b=${a:12:5}

onde 12 é o deslocamento (baseado em zero) e 5 é o comprimento

Se os sublinhados ao redor dos dígitos forem os únicos na entrada, você poderá remover o prefixo e o sufixo (respectivamente) em duas etapas:

tmp=${a#*_}   # remove prefix ending in "_"
b=${tmp%_*}   # remove suffix starting with "_"

Se houver outros sublinhados, provavelmente é possível de qualquer maneira, embora mais complicado. Se alguém souber executar as duas expansões em uma única expressão, eu também gostaria de saber.

Ambas as soluções apresentadas são pura purificação, sem a geração de processos envolvidos e, portanto, muito rápido.

JB.
fonte
18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutionno meu GNU bash 4.2.45.
JB.
2
@ JonnyB, Algum tempo no passado que funcionou. Meus colegas me disseram que ele parou e eles mudaram para um comando sed ou algo assim. Olhando para a história, eu estava executando em um shscript, o que provavelmente foi arriscado. Neste ponto, não consigo mais fazê-lo funcionar.
Spencer Rathbun
22
JB, você deve esclarecer que "12" é o deslocamento (com base em zero) e "5" é o comprimento. Além disso, +1 no link da @gontard que explica tudo!
Doktor J
1
Ao executar isso dentro de um script como "sh run.sh", pode-se obter um erro de Substituição Ruim. Para evitar isso, altere as permissões para run.sh (chmod + x run.sh) e, em seguida, execute o script como "./run.sh"
Ankur
2
O parâmetro de deslocamento também pode ser negativo, BTW. Você só precisa tomar cuidado para não colá-lo nos dois pontos, ou o bash o interpretará como uma :-substituição "Usar valores padrão". Portanto, ${a: -12:5}produz os 5 caracteres 12 caracteres do final e ${a: -12:-5}os 7 caracteres entre o final 12 e o final 5.
JB.
97

Solução genérica em que o número pode estar em qualquer lugar do nome do arquivo, usando a primeira dessas seqüências:

number=$(echo $filename | egrep -o '[[:digit:]]{5}' | head -n1)

Outra solução para extrair exatamente uma parte de uma variável:

number=${filename:offset:length}

Se seu nome de arquivo sempre tiver o formato, stuff_digits_...você pode usar o awk:

number=$(echo $filename | awk -F _ '{ print $2 }')

Outra solução para remover tudo, exceto dígitos, use

number=$(echo $filename | tr -cd '[[:digit:]]')
Johannes Schaub - litb
fonte
2
E se eu quiser extrair o dígito / palavra da última linha do arquivo.
A Sahra
93

apenas tente usar cut -c startIndx-stopIndx

brown.2179
fonte
2
Existe algo como startIndex-lastIndex - 1?
Niklas
1
@Niklas In bash, startIndx-$((lastIndx-1))
proly
3
start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1))
brown.2179
1
O problema é que a entrada é dinâmica, pois eu também uso o pipe para obtê-lo, então é basicamente. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas
Isso pode ser feito com corte se quebrar em duas partes como line=git log --online | head -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `mas neste caso em particular, pode ser melhor usar sed comogit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
marrom
34

Caso alguém queira informações mais rigorosas, você também pode pesquisá-las no man bash como este

$ man bash [press return key]
/substring  [press return key]
[press "n" key]
[press "n" key]
[press "n" key]
[press "n" key]

Resultado:

$ {parâmetro: deslocamento}
       $ {parâmetro: deslocamento: comprimento}
              Expansão de Substring. Expande até caracteres de comprimento
              parâmetro iniciando no caractere especificado pelo deslocamento. E se
              omitido, expande para a substring do parâmetro start‐
              no caractere especificado pelo deslocamento. comprimento e deslocamento são
              expressões aritméticas (veja AVALIAÇÃO ARITMÉTICA abaixo). E se
              offset é avaliado como um número menor que zero, o valor é usado
              como um deslocamento do final do valor do parâmetro Aritmética
              expressões começando com a - devem ser separadas por espaços em branco
              do anterior: distinguir-se do Uso Padrão
              Expansão de valores. Se o comprimento for avaliado como um número menor que
              zero e o parâmetro não é @ e não é um indexado ou associativo
              matriz, é interpretado como um deslocamento do final do valor
              parâmetro em vez de um número de caracteres, e a extensão
              são os caracteres entre os dois deslocamentos. Se o parâmetro for
              @, o resultado são parâmetros posicionais de comprimento começando em off‐
              conjunto. Se parâmetro for um nome de matriz indexado inscrito por @ ou
              *, o resultado são os membros de comprimento da matriz que começam com
              $ {parâmetro [deslocamento]}. Um deslocamento negativo é obtido em relação a
              um maior que o índice máximo da matriz especificada. Sub-
              expansão de string aplicada a um array associativo produz
              resultados multados. Observe que um deslocamento negativo deve ser separado
              do cólon por pelo menos um espaço para evitar ser confundido
              com o: - expansão. A indexação de substring é baseada em zero, a menos que
              os parâmetros posicionais são utilizados; nesse caso, a indexação
              começa em 1 por padrão. Se o deslocamento for 0 e a posição
              parâmetros são usados, $ 0 é prefixado na lista.
jperelli
fonte
2
Uma ressalva muito importante com valores negativos, conforme declarado acima: As expressões aritméticas que começam com a - devem ser separadas por espaços em branco do anterior: para se diferenciar da expansão Usar valores padrão. Então, para obter os últimos quatro caracteres de um var:${var: -4}
sshow 27/07
26

Aqui está como eu faria isso:

FN=someletters_12345_moreleters.ext
[[ ${FN} =~ _([[:digit:]]{5})_ ]] && NUM=${BASH_REMATCH[1]}

Explicação:

Específico do Bash:

Expressões regulares (RE): _([[:digit:]]{5})_

  • _ são literais para demarcar / ancorar os limites correspondentes para a sequência que está sendo correspondida
  • () criar um grupo de captura
  • [[:digit:]] é uma classe de personagem, acho que fala por si
  • {5} significa exatamente cinco caracteres anteriores, classe (como neste exemplo) ou grupo deve corresponder

Em inglês, você pode pensar assim: a FNsequência é iterada caractere por caractere até vermos um _ponto em que o grupo de captura é aberto e tentamos corresponder cinco dígitos. Se essa correspondência for bem-sucedida nesse ponto, o grupo de captura salvará os cinco dígitos atravessados. Se o próximo caractere for um _, a condição for bem-sucedida, o grupo de captura será disponibilizado BASH_REMATCHe a próxima NUM=instrução poderá ser executada. Se alguma parte da correspondência falhar, os detalhes salvos serão descartados e o processamento de caractere por caractere continuará após o _. por exemplo, se FNonde _1 _12 _123 _1234 _12345_, haveria quatro partidas falsas antes de encontrar uma correspondência.

nicerobot
fonte
3
Essa é uma maneira genérica que funciona mesmo se você precisar extrair mais de uma coisa, como eu fiz.
Zebediah49
3
Esta é realmente a resposta mais genérica e deve ser aceita. Funciona para uma expressão regular, não apenas para uma sequência de caracteres em uma posição fixa ou entre o mesmo delimitador (que habilita cut). Também não depende da execução de um comando externo.
Dan Dascalescu
1
Esta resposta é criminalmente subestimada.
chepner
Isso é ótimo! Eu adaptei isso para usar diferentes dilômetros de início / parada (substitua o _) e números de comprimento variável (. Para {5}) para a minha situação. Alguém pode quebrar essa magia negra e explicá-la?
Paul
1
@ Paul Adicionei mais detalhes à minha resposta. Espero que ajude.
nicerobot
21

Estou surpreso que essa solução pura do bash não tenha surgido:

a="someletters_12345_moreleters.ext"
IFS="_"
set $a
echo $2
# prints 12345

Você provavelmente deseja redefinir o IFS para qual valor era antes ou unset IFSdepois!

user1338062
fonte
1
não é solução festa pura, eu acho que funciona em shell puro (/ bin / sh)
kayn
5
+1 Você poderia escrever isso de outra forma para evitar ter de unset IFSparâmetros e posicionais:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
kojiro
2
Isso está sujeito à expansão do nome do caminho! (então está quebrado).
gniourf_gniourf
20

Com base na resposta de jor (que não funciona para mim):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')
PEZ
fonte
12
Expressões regulares é o negócio real quando você tem algo complicado e simplesmente contar sublinhados não cuté?
Aleksandr Levchuk 29/08
12

Seguindo os requisitos

Eu tenho um nome de arquivo com x número de caracteres e, em seguida, uma sequência de cinco dígitos cercada por um único sublinhado de cada lado e outro conjunto de x número de caracteres. Quero pegar o número de 5 dígitos e colocá-lo em uma variável.

Eu encontrei algumas grepmaneiras que podem ser úteis:

$ echo "someletters_12345_moreleters.ext" | grep -Eo "[[:digit:]]+" 
12345

ou melhor

$ echo "someletters_12345_moreleters.ext" | grep -Eo "[[:digit:]]{5}" 
12345

E então com -Posintaxe:

$ echo "someletters_12345_moreleters.ext" | grep -Po '(?<=_)\d+' 
12345

Ou se você deseja ajustá-lo exatamente a 5 caracteres:

$ echo "someletters_12345_moreleters.ext" | grep -Po '(?<=_)\d{5}' 
12345

Finalmente, para armazená-lo em uma variável, basta usar a var=$(command)sintaxe.

fedorqui 'Então pare de prejudicar'
fonte
2
Eu acredito que hoje em dia não há necessidade de usar egrep, o próprio comando avisa: Invocation as 'egrep' is deprecated; use 'grep -E' instead. Eu editei sua resposta.
Neurotransmitter
11

Se focarmos no conceito de:
"Uma sequência de (um ou vários) dígitos"

Poderíamos usar várias ferramentas externas para extrair os números.
Poderíamos facilmente apagar todos os outros caracteres, sed ou tr:

name='someletters_12345_moreleters.ext'

echo $name | sed 's/[^0-9]*//g'    # 12345
echo $name | tr -c -d 0-9          # 12345

Mas se $ name contiver várias execuções de números, o acima irá falhar:

Se "name = someletters_12345_moreleters_323_end.ext", então:

echo $name | sed 's/[^0-9]*//g'    # 12345323
echo $name | tr -c -d 0-9          # 12345323

Precisamos usar expressões regulares (regex).
Para selecionar apenas a primeira execução (12345 e não 323) no sed e perl:

echo $name | sed 's/[^0-9]*\([0-9]\{1,\}\).*$/\1/'
perl -e 'my $name='$name';my ($num)=$name=~/(\d+)/;print "$num\n";'

Mas também poderíamos fazê-lo diretamente no bash (1) :

regex=[^0-9]*([0-9]{1,}).*$; \
[[ $name =~ $regex ]] && echo ${BASH_REMATCH[1]}

Isso nos permite extrair a PRIMEIRA execução de dígitos de qualquer tamanho,
cercados por qualquer outro texto / caracteres.

Nota : regex=[^0-9]*([0-9]{5,5}).*$;corresponderá exatamente a 5 dígitos. :-)

(1) : mais rápido do que chamar uma ferramenta externa para cada texto curto. Não é mais rápido do que todo o processamento no sed ou awk para arquivos grandes.


fonte
10

Sem nenhum subprocesso, você pode:

shopt -s extglob
front=${input%%_+([a-zA-Z]).*}
digits=${front##+([a-zA-Z])_}

Uma variante muito pequena disso também funcionará no ksh93.

Darron
fonte
9

Aqui está uma solução de prefixo-sufixo (semelhante às soluções fornecidas por JB e Darron) que corresponde ao primeiro bloco de dígitos e não depende dos sublinhados circundantes:

str='someletters_12345_morele34ters.ext'
s1="${str#"${str%%[[:digit:]]*}"}"   # strip off non-digit prefix from str
s2="${s1%%[^[:digit:]]*}"            # strip off non-digit suffix from s1
echo "$s2"                           # 12345
codista
fonte
7

Adoro seda capacidade de lidar com grupos regex:

> var="someletters_12345_moreletters.ext"
> digits=$( echo $var | sed "s/.*_\([0-9]\+\).*/\1/p" -n )
> echo $digits
12345

A opção um pouco mais geral seria não assumir que você tem um sublinhado _marcando o início da sua seqüência de dígitos, portanto, por exemplo, tirando todos os não-números que você começa antes de sua seqüência: s/[^0-9]\+\([0-9]\+\).*/\1/p.


> man sed | grep s/regexp/replacement -A 2
s/regexp/replacement/
    Attempt to match regexp against the pattern space.  If successful, replace that portion matched with replacement.  The replacement may contain the special  character  &  to
    refer to that portion of the pattern space which matched, and the special escapes \1 through \9 to refer to the corresponding matching sub-expressions in the regexp.

Mais sobre isso, caso você não esteja muito confiante com os regexps:

  • s é para _s_ubstitute
  • [0-9]+ corresponde a mais de 1 dígito
  • \1 links para o grupo n.1 da saída regex (o grupo 0 é a correspondência inteira, o grupo 1 é a correspondência entre parênteses neste caso)
  • p flag é para _p_rinting

Todas as fugas \existem para fazer sedo processamento de regexp funcionar.

Campa
fonte
6

Minha resposta terá mais controle sobre o que você deseja da sua string. Aqui está o código de como você pode extrair 12345sua string

str="someletters_12345_moreleters.ext"
str=${str#*_}
str=${str%_more*}
echo $str

Isso será mais eficiente se você quiser extrair algo que tenha caracteres como abcou caracteres especiais como_ ou -. Por exemplo: Se sua string for assim e você desejar tudo o que é posterior someletters_e anterior _moreleters.ext:

str="someletters_123-45-24a&13b-1_moreleters.ext"

Com o meu código, você pode mencionar exatamente o que deseja. Explicação:

#*Ele removerá a string anterior, incluindo a chave correspondente. Aqui, a chave que mencionamos é _ %Ela removerá a seguinte string, incluindo a chave correspondente. Aqui a chave que mencionamos é '_mais *'

Faça algumas experiências você mesmo e você achará isso interessante.

Alex Raj Kaliamoorthy
fonte
6

Dado test.txt é um arquivo que contém "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

cut -b19-20 test.txt > test1.txt # This will extract chars 19 & 20 "ST" 
while read -r; do;
> x=$REPLY
> done < test1.txt
echo $x
ST
Rick Osman
fonte
Isso é extremamente específico para essa entrada específica. A única solução geral para a pergunta geral (que o OP deveria ter solicitado) é usar uma regexp .
Dan Dascalescu
3

Ok, aqui vai pura Substituição de Parâmetro com uma string vazia. A ressalva é que defini someletters e moreletters como apenas caracteres. Se eles são alfanuméricos, isso não funcionará como está.

filename=someletters_12345_moreletters.ext
substring=${filename//@(+([a-z])_|_+([a-z]).*)}
echo $substring
12345
morbeo
fonte
2
impressionante, mas requer pelo menos o bash v4
olibre 25/11
2

semelhante ao substr ('abcdefg', 2-1, 3) no php:

echo 'abcdefg'|tail -c +2|head -c 3
diyism
fonte
Isso é extremamente específico para essa entrada. A única solução geral para a pergunta geral (que o OP deveria ter solicitado) é usar uma regexp .
Dan Dascalescu
1

Há também o comando 'expr' do bash:

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING
jor
fonte
4
exprnão é um builtin.
gniourf_gniourf
1
Também não é necessário à luz do =~operador suportado por [[.
chepner
1

Um pouco tarde, mas acabei de encontrar este problema e encontrei o seguinte:

host:/tmp$ asd=someletters_12345_moreleters.ext 
host:/tmp$ echo `expr $asd : '.*_\(.*\)_'`
12345
host:/tmp$ 

Usei-o para obter resolução de milissegundos em um sistema incorporado que não possui% N para a data:

set `grep "now at" /proc/timer_list`
nano=$3
fraction=`expr $nano : '.*\(...\)......'`
$debug nano is $nano, fraction is $fraction
russell
fonte
1

Uma solução bash:

IFS="_" read -r x digs x <<<'someletters_12345_moreleters.ext'

Isso derruba uma variável chamada x. O var xpode ser alterado para o var _.

input='someletters_12345_moreleters.ext'
IFS="_" read -r _ digs _ <<<"$input"

fonte
1

Final Inklusive, semelhante às implementações JS e Java. Remova +1 se você não desejar isso.

substring() {
    local str="$1" start="${2}" end="${3}"

    if [[ "$start" == "" ]]; then start="0"; fi
    if [[ "$end"   == "" ]]; then end="${#str}"; fi

    local length="((${end}-${start}+1))"

    echo "${str:${start}:${length}}"
} 

Exemplo:

    substring 01234 0
    01234
    substring 012345 0
    012345
    substring 012345 0 0
    0
    substring 012345 1 1
    1
    substring 012345 1 2
    12
    substring 012345 0 1
    01
    substring 012345 0 2
    012
    substring 012345 0 3
    0123
    substring 012345 0 4
    01234
    substring 012345 0 5
    012345

Mais exemplos de chamadas:

    substring 012345 0
    012345
    substring 012345 1
    12345
    substring 012345 2
    2345
    substring 012345 3
    345
    substring 012345 4
    45
    substring 012345 5
    5
    substring 012345 6

    substring 012345 3 5
    345
    substring 012345 3 4
    34
    substring 012345 2 4
    234
    substring 012345 1 3
    123

Você é bem vindo.

mmm
fonte