Extraer subcadena en Bash

729

Dado un nombre de archivo en el formulario someletters_12345_moreleters.ext, quiero extraer los 5 dígitos y ponerlos en una variable.

Entonces, para enfatizar el punto, tengo un nombre de archivo con x número de caracteres, luego una secuencia de cinco dígitos rodeada por un solo guión bajo a cada lado y luego otro conjunto de x número de caracteres. Quiero tomar el número de 5 dígitos y ponerlo en una variable.

Estoy muy interesado en la cantidad de formas diferentes en que esto se puede lograr.

Berek Bryan
fuente
55
La respuesta de JB es claramente ganar los votos: ¿es hora de cambiar la respuesta aceptada?
Jeff
3
La mayoría de las respuestas no parecen responder a su pregunta porque la pregunta es ambigua. "Tengo un nombre de archivo con x número de caracteres, luego una secuencia de cinco dígitos rodeada por un solo guión bajo a cada lado y luego otro conjunto de x número de caracteres" . Por esa definición abc_12345_def_67890_ghi_defes una entrada válida. ¿Qué es lo que desea que suceda? Supongamos que solo hay una secuencia de 5 dígitos. Todavía tiene abc_def_12345_ghi_jklo 1234567_12345_1234567o 12345d_12345_12345ecomo entrada válida basada en su definición de la entrada y la mayor parte de las respuestas a continuación no va a manejar esto.
gman
2
Esta pregunta tiene una entrada de ejemplo que es demasiado específica. Debido a eso, obtuvo muchas respuestas específicas para este caso en particular (solo dígitos, mismo _delimitador, entrada que contiene la cadena de destino solo una vez, etc.). La mejor respuesta (la más genérica y más rápida) tiene, después de 10 años, solo 7 votos a favor, mientras que otras respuestas limitadas tienen cientos. Me hace perder la fe en los desarrolladores 😞
Dan Dascalescu

Respuestas:

693

Usar corte :

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

Más genérico:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING
FerranB
fuente
1
la respuesta más genérica es exactamente lo que estaba buscando, gracias
Berek Bryan
71
El indicador -f toma índices basados ​​en 1, en lugar de los índices basados ​​en 0 a los que un programador estaría acostumbrado.
Matthew G
2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $ (echo $ INPUT | cut -d'_ '-f 2) echo $ SUBSTRING
mani deepak
3
Debería usar comillas dobles alrededor de los argumentos a echomenos que sepa con certeza que las variables no pueden contener espacios en blanco irregulares o metacaracteres de shell. Ver más stackoverflow.com/questions/10067266/…
tripleee
El número '2' después de '-f' es decirle a Shell que extraiga el segundo conjunto de subcadenas.
Sandun
1088

Si x es constante, la siguiente expansión de parámetros realiza la extracción de subcadenas:

b=${a:12:5}

donde 12 es el desplazamiento (basado en cero) y 5 es la longitud

Si los guiones bajos alrededor de los dígitos son los únicos en la entrada, puede quitar el prefijo y el sufijo (respectivamente) en dos pasos:

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

Si hay otros guiones bajos, probablemente sea factible de todos modos, aunque más complicado. Si alguien sabe cómo realizar ambas expansiones en una sola expresión, me gustaría saber también.

Ambas soluciones presentadas son puro bash, sin proceso de generación involucrado, por lo tanto, muy rápido.

JB
fuente
18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutionen mi GNU bash 4.2.45.
JB.
2
@jonnyB, alguna vez en el pasado eso funcionó. Mis compañeros de trabajo me dijeron que se detuvo, y lo cambiaron para ser un comando sed o algo así. Al mirarlo en la historia, lo estaba ejecutando en un shscript, que probablemente era un guión. En este punto ya no puedo hacer que funcione.
Spencer Rathbun
22
JB, debes aclarar que "12" es el desplazamiento (basado en cero) y "5" es la longitud. Además, ¡+1 para el enlace de @gontard que lo muestra todo!
Doktor J
1
Al ejecutar esto dentro de un script como "sh run.sh", uno podría obtener un error de sustitución incorrecta. Para evitar eso, cambie los permisos para run.sh (chmod + x run.sh) y luego ejecute el script como "./run.sh"
Ankur
2
El parámetro de compensación también puede ser negativo, por cierto. Solo debe tener cuidado de no pegarlo al colon, o bash lo interpretará como una :-sustitución de "Usar valores predeterminados". Entonces ${a: -12:5}produce los 5 caracteres a 12 caracteres del final, y ${a: -12:-5}los 7 caracteres entre el final 12 y el final 5.
JB.
97

Solución genérica donde el número puede estar en cualquier parte del nombre del archivo, utilizando la primera de tales secuencias:

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

Otra solución para extraer exactamente una parte de una variable:

number=${filename:offset:length}

Si su nombre de archivo siempre tiene el formato stuff_digits_..., puede usar awk:

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

Otra solución más para eliminar todo, excepto los dígitos, usar

number=$(echo $filename | tr -cd '[[:digit:]]')
Johannes Schaub - litb
fuente
2
¿Qué pasa si quiero extraer el dígito / palabra de la última línea del archivo?
Un Sahra
93

solo trata de usar cut -c startIndx-stopIndx

marrón.2179
fuente
2
¿Hay algo como startIndex-lastIndex - 1?
Niklas
1
@Niklas In bash, proly startIndx-$((lastIndx-1))
brown.2179
3
start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1))
brown.2179
1
El problema es que la entrada es dinámica ya que también uso la tubería para obtenerla, así que básicamente es. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas
Esto se puede hacer con corte si se line=divide en dos partes como git log --oneline | head -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `pero en este caso particular, podría ser mejor usar sed comogit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179
34

En caso de que alguien quiera información más rigurosa, también puede buscarla en 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: desplazamiento}
       $ {parámetro: desplazamiento: longitud}
              Expansión de subcadenas. Se expande a caracteres de hasta
              parámetro que comienza en el carácter especificado por offset. Si
              la longitud se omite, se expande a la subcadena del parámetro start‐
              ing en el carácter especificado por offset. longitud y desplazamiento son
              expresiones aritméticas (ver EVALUACIÓN ARITMÉTICA a continuación). Si
              offset se evalúa a un número menor que cero, se usa el valor
              como un desplazamiento desde el final del valor del parámetro. Aritmética
              las expresiones que comienzan con a - deben estar separadas por espacios en blanco
              de lo anterior: para distinguirse del Uso predeterminado
              Expansión de valores. Si la longitud se evalúa en un número menor que
              cero, y el parámetro no es @ y no es un índice o asociativo
              matriz, se interpreta como un desplazamiento desde el final del valor
              de parámetro en lugar de una serie de caracteres, y la expansión
              sion son los caracteres entre los dos desplazamientos. Si el parámetro es
              @, el resultado son parámetros posicionales de longitud que comienzan en off
              conjunto. Si el parámetro es un nombre de matriz indexado suscrito por @ o
              *, el resultado son los miembros de longitud de la matriz que comienzan con
              $ {parámetro [desplazamiento]}. Se toma un desplazamiento negativo en relación con
              uno mayor que el índice máximo de la matriz especificada. Sub-
              la expansión de cadena aplicada a una matriz asociativa produce unde‐
              Resultados multados. Tenga en cuenta que un desplazamiento negativo debe estar separado
              desde el colon por al menos un espacio para evitar ser confundido
              con la: - expansión. La indexación de subcadenas está basada en cero a menos que
              se utilizan los parámetros posicionales, en cuyo caso la indexación
              comienza en 1 por defecto. Si el desplazamiento es 0, y el posicional
              se utilizan parámetros, $ 0 tiene el prefijo de la lista.
jperelli
fuente
2
Una advertencia muy importante con valores negativos como se indicó anteriormente: las expresiones aritméticas que comienzan con a - deben estar separadas por espacios en blanco de las anteriores: para distinguirse de la expansión Usar valores predeterminados. Así que para conseguir cuatro últimos caracteres de una var:${var: -4}
sshow
26

Así es como lo haría:

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

Explicación:

Específico de golpe:

Expresiones regulares (RE): _([[:digit:]]{5})_

  • _ son literales para demarcar / anclar límites coincidentes para la cadena que se está haciendo coincidir
  • () crear un grupo de captura
  • [[:digit:]] es una clase de personaje, creo que habla por sí mismo
  • {5} significa exactamente cinco del carácter anterior, clase (como en este ejemplo) o grupo debe coincidir

En inglés, puede pensar que se comporta así: la FNcadena se repite carácter por carácter hasta que veamos un _punto en el que se abre el grupo de captura e intentamos hacer coincidir cinco dígitos. Si esa coincidencia es exitosa hasta este punto, el grupo de captura guarda los cinco dígitos recorridos. Si el siguiente carácter es un _, la condición es exitosa, el grupo de captura está disponible BASH_REMATCHy la siguiente NUM=instrucción puede ejecutarse. Si alguna parte de la coincidencia falla, los detalles guardados se eliminan y el procesamiento de carácter por carácter continúa después de _. por ejemplo, si FNdónde _1 _12 _123 _1234 _12345_, habría cuatro comienzos falsos antes de encontrar una coincidencia.

nicerobot
fuente
3
Esta es una forma genérica que funciona incluso si necesita extraer más de una cosa, como lo hice yo.
zebediah49
3
Esta es la respuesta más genérica de hecho, y debe aceptarse una. Funciona para una expresión regular, no solo una cadena de caracteres en una posición fija, o entre el mismo delimitador (que habilita cut). Tampoco se basa en ejecutar un comando externo.
Dan Dascalescu
1
Esta respuesta está penalmente infravalorada.
chepner
¡Esto es genial! Adapte esto para usar diferentes dilímetros de inicio / parada (reemplace el _) y números de longitud variable (. Para {5}) para mi situación. ¿Alguien puede romper esta magia negra y explicarla?
Paul
1
@Paul agregué más detalles a mi respuesta. Espero que ayude.
nicerobot
21

Me sorprende que esta solución pura de bash no haya aparecido:

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

¡Probablemente quiera restablecer IFS a su valor anterior o unset IFSposterior!

usuario1338062
fuente
1
no es una solución pura de bash, creo que funciona en shell puro (/ bin / sh)
kayn
55
+1 Podría escribir esto de otra manera para evitar tener que desarmar IFSy parámetros posicionales:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
kojiro
2
¡Esto está sujeto a la expansión del nombre de ruta! (Entonces está roto).
gniourf_gniourf
20

Sobre la base de la respuesta de jor (que no funciona para mí):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')
PEZ
fuente
12
Regular Expressions es el verdadero negocio cuando tienes algo complicado y simplemente contar guiones bajos no cutlo hará .
Aleksandr Levchuk
12

Siguiendo los requisitos

Tengo un nombre de archivo con x número de caracteres, luego una secuencia de cinco dígitos rodeada por un solo guión bajo a cada lado y luego otro conjunto de x número de caracteres. Quiero tomar el número de 5 dígitos y ponerlo en una variable.

Encontré algunas grepformas que pueden ser útiles:

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

o mejor

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

Y luego con -Posintaxis:

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

O si desea que se ajuste exactamente a 5 caracteres:

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

Finalmente, para que se almacene en una variable solo es necesario usar la var=$(command)sintaxis.

fedorqui 'así que deja de dañar'
fuente
2
Creo que hoy en día no hay necesidad de usar egrep, el propio comando le advierte: Invocation as 'egrep' is deprecated; use 'grep -E' instead. He editado tu respuesta.
Neurotransmisor
11

Si nos centramos en el concepto de:
"Una serie de (uno o varios) dígitos"

Podríamos usar varias herramientas externas para extraer los números.
Podríamos borrar fácilmente todos los demás caracteres, ya sea sed o tr:

name='someletters_12345_moreleters.ext'

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

Pero si $ name contiene varias series de números, lo anterior fallará:

Si "name = someletters_12345_moreleters_323_end.ext", entonces:

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

Necesitamos usar expresiones regulares (regex).
Para seleccionar solo la primera ejecución (12345 no 323) en sed y perl:

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

Pero también podríamos hacerlo directamente en bash (1) :

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

Esto nos permite extraer la PRIMERA serie de dígitos de cualquier longitud
rodeados por cualquier otro texto / carácter.

Nota : regex=[^0-9]*([0-9]{5,5}).*$;solo coincidirá con ejecuciones de exactamente 5 dígitos. :-)

(1) : más rápido que llamar a una herramienta externa para cada texto breve. No más rápido que hacer todo el procesamiento dentro de sed o awk para archivos grandes.


fuente
10

Sin ningún subproceso puedes:

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

Una variante muy pequeña de esto también funcionará en ksh93.

Darron
fuente
9

Aquí hay una solución de prefijo-sufijo (similar a las soluciones proporcionadas por JB y Darron) que coincide con el primer bloque de dígitos y no depende de los guiones bajos 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
fuente
7

Me encanta sedla capacidad de tratar con grupos de expresiones regulares:

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

Una opción un poco más general sería no asumir que usted tiene un guión _que marca el inicio de su secuencia de dígitos, por lo tanto, por ejemplo despojarse de todos los no-números que se obtienen antes de que su secuencia: 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.

Más sobre esto, en caso de que no estés demasiado seguro con regexps:

  • s es para _s_ubstitute
  • [0-9]+ coincide con 1+ dígitos
  • \1 enlaces al grupo n. 1 de la salida de expresiones regulares (el grupo 0 es la coincidencia completa, el grupo 1 es la coincidencia entre paréntesis en este caso)
  • p la bandera es para _p_rinting

Todos los escapes \están ahí para hacer que sedel procesamiento regexp funcione.

Campa
fuente
6

Mi respuesta tendrá más control sobre lo que quieres de tu cadena. Aquí está el código sobre cómo puede extraer 12345de su cadena

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

Esto será más eficiente si desea extraer algo que tenga caracteres como abco caracteres especiales como _o -. Por ejemplo: si su cadena es así y desea todo lo que está después someletters_y antes _moreleters.ext:

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

Con mi código puedes mencionar exactamente lo que quieres. Explicación:

#*Eliminará la cadena anterior, incluida la clave correspondiente. Aquí la clave que mencionamos es _ %Eliminará la siguiente cadena, incluida la clave correspondiente. Aquí la clave que mencionamos es '_more *'

Haga algunos experimentos usted mismo y le parecerá interesante.

Alex Raj Kaliamoorthy
fuente
6

Dado test.txt es un archivo que contiene "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
fuente
Esto es extremadamente específico para esa entrada particular. La única solución general a la pregunta general (que el OP debería haber hecho) es usar una expresión regular .
Dan Dascalescu
3

Ok, aquí va la sustitución de parámetros pura con una cadena vacía. La advertencia es que he definido algunas letras y más letras como solo caracteres. Si son alfanuméricos, esto no funcionará como está.

filename=someletters_12345_moreletters.ext
substring=${filename//@(+([a-z])_|_+([a-z]).*)}
echo $substring
12345
morbeo
fuente
2
impresionante pero requiere al menos bash v4
olibre
2

similar a substr ('abcdefg', 2-1, 3) en php:

echo 'abcdefg'|tail -c +2|head -c 3
diyism
fuente
Esto es extremadamente específico para esa entrada. La única solución general a la pregunta general (que el OP debería haber hecho) es usar una expresión regular .
Dan Dascalescu
1

También está el comando bash builtin 'expr':

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING
jor
fuente
44
exprNo es una construcción.
gniourf_gniourf
1
Tampoco es necesario a la luz del =~operador admitido por [[.
chepner
1

Un poco tarde, pero me encontré con este problema y encontré lo siguiente:

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

Lo utilicé para obtener una resolución de milisegundos en un sistema integrado que no tiene% N para la fecha:

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

Una solución bash:

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

Esto golpeará una variable llamada x. La var xse puede cambiar a la var _.

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

fuente
1

Fin Inklusive, similar a las implementaciones JS y Java. Elimina +1 si no deseas esto.

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}}"
} 

Ejemplo:

    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

Más ejemplos de llamadas:

    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

De nada.

mmm
fuente