Estrai la sottostringa in Bash


728

Dato un nome file nel modulo someletters_12345_moreleters.ext, voglio estrarre le 5 cifre e inserirle in una variabile.

Quindi, per enfatizzare il punto, ho un nome file con x numero di caratteri, quindi una sequenza di cinque cifre circondata da un singolo trattino basso su entrambi i lati, quindi un altro set di x numero di caratteri. Voglio prendere il numero di 5 cifre e inserirlo in una variabile.

Sono molto interessato al numero di modi diversi in cui questo può essere realizzato.


5
La risposta di JB sta chiaramente vincendo i voti: è tempo di cambiare la risposta accettata?
Jeff,

3
La maggior parte delle risposte non sembrano rispondere alla tua domanda perché la domanda è ambigua. "Ho un nome file con x numero di caratteri, quindi una sequenza di cinque cifre circondata da un singolo trattino basso su entrambi i lati e da un altro set di x numero di caratteri" . Secondo tale definizione abc_12345_def_67890_ghi_defè un input valido. Cosa vuoi che succeda? Supponiamo che ci sia solo una sequenza di 5 cifre. Hai ancora abc_def_12345_ghi_jklo 1234567_12345_1234567o 12345d_12345_12345ecome input valido in base alla tua definizione di input e la maggior parte delle risposte di seguito non lo gestirà.
Gman,

2
Questa domanda ha un input di esempio che è troppo specifico. Per questo motivo, ha ottenuto molte risposte specifiche per questo caso particolare (solo cifre, stesso _delimitatore, input che contiene la stringa di destinazione solo una volta ecc.). La risposta migliore (più generica e veloce) ha, dopo 10 anni, solo 7 voti positivi, mentre altre risposte limitate ne hanno centinaia. Mi fa perdere la fiducia negli sviluppatori 😞
Dan Dascalescu

Risposte:


692

Usa il taglio :

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

Più generico:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING

1
la risposta più generica è esattamente quello che stavo cercando, grazie
Berek Bryan

71
Il flag -f accetta indici basati su 1, anziché gli indici basati su 0 a cui un programmatore sarebbe abituato.
Matthew G,

2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $ (echo $ INPUT | cut -d'_ '-f 2) echo $ SUBSTRING
mani deepak

3
Dovresti usare correttamente le doppie virgolette attorno agli argomenti a echomeno che tu non sappia per certo che le variabili non possono contenere spazi bianchi irregolari o metacaratteri di shell. Vedi ancora stackoverflow.com/questions/10067266/...
tripleee

Il numero '2' dopo '-f' indica alla shell di estrarre la seconda serie di sottostringhe.
Sandun,

1088

Se x è costante, l'espansione del parametro seguente esegue l'estrazione della sottostringa:

b=${a:12:5}

dove 12 è l'offset (in base zero) e 5 è la lunghezza

Se i caratteri di sottolineatura attorno alle cifre sono gli unici nell'input, puoi rimuovere il prefisso e il suffisso (rispettivamente) in due passaggi:

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

Se ci sono altri caratteri di sottolineatura, è probabilmente fattibile comunque, anche se più complicato. Se qualcuno sa come eseguire entrambe le espansioni in una sola espressione, mi piacerebbe saperlo anche io.

Entrambe le soluzioni presentate sono pure bash, senza la generazione di processi, quindi molto veloci.


18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutionon my GNU bash 4.2.45.
JB.

2
@jonnyB, Qualche volta in passato ha funzionato. I miei colleghi mi hanno detto che si è fermato, e lo hanno cambiato per essere un comando sed o qualcosa del genere. Guardandolo nella storia, lo stavo eseguendo in una shsceneggiatura, che probabilmente era un trattino. A questo punto non riesco più a farlo funzionare.
Spencer Rathbun,

22
JB, dovresti chiarire che "12" è l'offset (a base zero) e "5" è la lunghezza. Inoltre, +1 per il link di @gontard che spiega tutto!
Doktor J,

1
Durante l'esecuzione all'interno di uno script come "sh run.sh", è possibile che si verifichi un errore di sostituzione errata. Per evitarlo, cambia i permessi per run.sh (chmod + x run.sh) e quindi esegui lo script come "./run.sh"
Ankur

2
Anche il parametro offset può essere negativo, BTW. Devi solo fare attenzione a non incollarlo ai due punti, o bash lo interpreterà come una :-sostituzione "Usa valori predefiniti". Quindi ${a: -12:5}restituisce i 5 caratteri 12 caratteri dalla fine e ${a: -12:-5}i 7 caratteri tra fine 12 e fine 5.
JB.

97

Soluzione generica in cui il numero può trovarsi ovunque nel nome file, utilizzando la prima di tali sequenze:

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

Un'altra soluzione per estrarre esattamente una parte di una variabile:

number=${filename:offset:length}

Se il tuo nome file ha sempre il formato stuff_digits_...puoi usare awk:

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

Ancora un'altra soluzione per rimuovere tutto tranne le cifre, usare

number=$(echo $filename | tr -cd '[[:digit:]]')

2
Cosa succede se desidero estrarre la cifra / parola dall'ultima riga del file.
A Sahra,

93

prova a usare cut -c startIndx-stopIndx


2
Esiste qualcosa come 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
Il problema è che l'input è dinamico poiché utilizzo anche la pipe per ottenerlo, quindi è sostanzialmente. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas

Questo può essere fatto con cut se line=suddiviso in due parti come git log --oneline | testa -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `ma in questo caso particolare, potrebbe essere meglio usare sed asgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179

34

Nel caso in cui qualcuno desideri informazioni più rigorose, puoi anche cercarle in man bash in questo modo

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

Risultato:

$ {Parametro: offset}
       $ {Parametro: Offset: lunghezza}
              Espansione sottostringa. Si espande fino a caratteri di lunghezza di
              parametro che inizia con il carattere specificato dall'offset. Se
              la lunghezza viene omessa, si espande alla sottostringa del parametro start‐
              ing al carattere specificato da offset. lunghezza e offset sono
              espressioni aritmetiche (vedi VALUTAZIONE ARITMETICA di seguito). Se
              offset restituisce un numero inferiore a zero, viene utilizzato il valore
              come offset dalla fine del valore del parametro. Aritmetica
              le espressioni che iniziano con un - devono essere separate da spazi bianchi
              dal precedente: da distinguere da Usa predefinito
              Espansione dei valori. Se la lunghezza corrisponde a un numero inferiore a
              zero e il parametro non è @ e non è indicizzato o associativo
              array, viene interpretato come un offset dalla fine del valore
              del parametro anziché un numero di caratteri e l'espansione
              Sion sono i caratteri tra i due offset. Se il parametro è
              @, il risultato sono i parametri posizionali della lunghezza che iniziano con off‐
              impostato. Se parametro è un nome di array indicizzato sottoscritto da @ o
              *, il risultato sono i membri di lunghezza dell'array che iniziano con
              $ {Parametro [offset]}. Viene preso un offset negativo rispetto a
              uno maggiore dell'indice massimo dell'array specificato. Sub-
              l'espansione della stringa applicata a un array associativo produce unde‐
              risultati multati. Si noti che un offset negativo deve essere separato
              dal colon da almeno uno spazio per evitare di essere confuso
              con: - espansione. L'indicizzazione del sottostringo è a base zero, a meno che
              vengono utilizzati i parametri posizionali, nel qual caso l'indicizzazione
              inizia da 1 per impostazione predefinita. Se offset è 0 e la posizione
              vengono utilizzati i parametri, $ 0 è preceduto dall'elenco.

2
Un avvertimento molto importante con valori negativi come indicato sopra: le espressioni aritmetiche che iniziano con un - devono essere separate da uno spazio bianco dal precedente: per essere distinte dall'espansione Usa valori predefiniti. Quindi, per ottenere gli ultimi quattro caratteri di un var:${var: -4}
mostra il

26

Ecco come lo farei:

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

Spiegazione:

Bash-specifica:

Espressioni regolari (RE): _([[:digit:]]{5})_

  • _ sono letterali per delimitare / ancorare i limiti di corrispondenza per la stringa da abbinare
  • () creare un gruppo di acquisizione
  • [[:digit:]] è una classe di personaggi, penso che parli da sola
  • {5} significa che devono corrispondere esattamente cinque caratteri, classe (come in questo esempio) o gruppo precedenti

In inglese, puoi pensare che si comporti in questo modo: la FNstringa viene ripetuta carattere per carattere fino a quando non vediamo un _punto in cui viene aperto il gruppo di acquisizione e proviamo a far corrispondere cinque cifre. Se tale corrispondenza ha esito positivo fino a questo punto, il gruppo di acquisizione salva le cinque cifre attraversate. Se il carattere successivo è un _, la condizione ha esito positivo, il gruppo di acquisizione viene reso disponibile BASH_REMATCHe l' NUM=istruzione successiva può essere eseguita. Se una parte della corrispondenza non riesce, i dettagli salvati vengono eliminati e l'elaborazione dei caratteri continua dopo _. ad es. se FNdove _1 _12 _123 _1234 _12345_, ci sarebbero quattro false partenze prima che trovasse una corrispondenza.


3
Questo è un modo generico che funziona anche se è necessario estrarre più di una cosa, come ho fatto io.
zebediah49,

3
Questa è davvero la risposta più generica e dovrebbe essere accettata una. Funziona per un'espressione regolare, non solo una stringa di caratteri in una posizione fissa o tra lo stesso delimitatore (che consente cut). Inoltre, non si basa sull'esecuzione di un comando esterno.
Dan Dascalescu

1
Questa risposta è criminalmente sottovalutata.
Chepner,

Questo è fantastico! L'ho adattato per utilizzare diversi dilimetri di inizio / fine (sostituire il _) e numeri di lunghezza variabile (. Per {5}) per la mia situazione. Qualcuno può abbattere questa magia nera e spiegarla?
Paul,

1
@Paul Ho aggiunto maggiori dettagli alla mia risposta. Spero che aiuti.
Nicerobot,

21

Sono sorpreso che questa pura soluzione bash non sia venuta fuori:

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

Probabilmente vuoi ripristinare IFS a quale valore era prima o unset IFSdopo!


1
non è una pura soluzione bash, penso che funzioni in pure shell (/ bin / sh)
kayn,

5
+1 Potresti scrivere in un altro modo per evitare di dover IFSIFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
impostare

2
Questo è soggetto all'espansione del percorso! (quindi è rotto).
gniourf_gniourf,

20

Basandomi sulla risposta di jor (che non funziona per me):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')

12
Le espressioni regolari sono un vero affare quando hai qualcosa di complicato e semplicemente contando i caratteri di sottolineatura no cut.
Aleksandr Levchuk,

12

Seguendo i requisiti

Ho un nome file con x numero di caratteri, quindi una sequenza di cinque cifre circondata da un singolo trattino basso su entrambi i lati, quindi un altro set di x numero di caratteri. Voglio prendere il numero di 5 cifre e inserirlo in una variabile.

Ho trovato alcuni grepmodi che potrebbero essere utili:

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

o meglio

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

E poi con la -Posintassi:

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

O se vuoi adattarlo esattamente a 5 caratteri:

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

Infine, per farlo archiviare in una variabile è sufficiente usare la var=$(command)sintassi.


2
Credo che al giorno d'oggi non v'è alcuna necessità di utilizzare egrep, il comando stesso avverte: Invocation as 'egrep' is deprecated; use 'grep -E' instead. Ho modificato la tua risposta.
Neurotrasmettitore

11

Se ci concentriamo sul concetto di:
"Una serie di (una o più) cifre"

Potremmo usare diversi strumenti esterni per estrarre i numeri.
Potremmo cancellare abbastanza facilmente tutti gli altri personaggi, sed o tr:

name='someletters_12345_moreleters.ext'

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

Ma se $ name contiene diverse serie di numeri, quanto sopra fallirà:

Se "name = someletters_12345_moreleters_323_end.ext", allora:

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

Dobbiamo usare le espressioni regolari (regex).
Per selezionare solo la prima corsa (12345 non 323) in 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";'

Ma potremmo anche farlo direttamente in bash (1) :

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

Questo ci consente di estrarre la PRIMA sequenza di cifre di qualsiasi lunghezza
circondata da qualsiasi altro testo / carattere.

Nota : regex=[^0-9]*([0-9]{5,5}).*$;corrisponderà solo esattamente a 5 cifre. :-)

(1) : più veloce di chiamare uno strumento esterno per ogni breve testo. Non più veloce di fare tutta l'elaborazione all'interno di sed o awk per file di grandi dimensioni.


10

Senza alcun processo secondario puoi:

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

Una variante molto piccola di questo funzionerà anche in ksh93.


9

Ecco una soluzione prefisso-suffisso (simile alle soluzioni fornite da JB e Darron) che corrisponde al primo blocco di cifre e non dipende dai caratteri di sottolineatura circostanti:

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

7

Adoro sedla capacità di gestire gruppi di regex:

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

Una scelta un po 'più generale sarebbe non supporre che si dispone di una sottolineatura _che segna l'inizio della sequenza cifre, quindi per esempio togliendo tutti i non-numeri si ottiene prima la sequenza: 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.

Altro su questo, nel caso in cui non sei troppo sicuro con regexps:

  • s è per _s_ubstitute
  • [0-9]+ corrisponde a 1+ cifre
  • \1 si collega al gruppo n.1 dell'output regex (il gruppo 0 è l'intera corrispondenza, in questo caso il gruppo 1 è la corrispondenza tra parentesi)
  • p flag è per _p_rinting

Tutte le escape \sono lì per far sedfunzionare l'elaborazione regexp.


6

La mia risposta avrà un maggiore controllo su ciò che desideri dalla tua stringa. Ecco il codice su come estrarre 12345la stringa

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

Questo sarà più efficace se vuoi estrarre qualcosa che ha caratteri simili abco caratteri speciali come _o -. Ad esempio: se la tua stringa è così e vuoi tutto ciò che è dopo someletters_e prima _moreleters.ext:

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

Con il mio codice puoi menzionare esattamente quello che vuoi. Spiegazione:

#*Rimuoverà la stringa precedente inclusa la chiave corrispondente. Qui la chiave che abbiamo menzionato è _ %Rimuoverà la seguente stringa inclusa la chiave corrispondente. Qui la chiave che abbiamo menzionato è '_more *'

Fai alcuni esperimenti da solo e lo troverai interessante.


6

Dato test.txt è un file contenente "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

Questo è estremamente specifico per quel particolare input. L'unica soluzione generale alla domanda generale (che l'OP avrebbe dovuto porre) è usare una regexp .
Dan Dascalescu

3

Ok, ecco la pura sostituzione dei parametri con una stringa vuota. Un avvertimento è che ho definito someletter e moreletter come solo personaggi. Se sono alfanumerici, questo non funzionerà così com'è.

filename=someletters_12345_moreletters.ext
substring=${filename//@(+([a-z])_|_+([a-z]).*)}
echo $substring
12345

2
fantastico ma richiede almeno bash v4
olibre

2

simile a substr ('abcdefg', 2-1, 3) in php:

echo 'abcdefg'|tail -c +2|head -c 3

Questo è estremamente specifico per quell'input. L'unica soluzione generale alla domanda generale (che l'OP avrebbe dovuto porre) è usare una regexp .
Dan Dascalescu

1

C'è anche il comando bash incorporato 'expr':

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING

4
exprnon è incorporato.
gniourf_gniourf,

1
Inoltre, non è necessario alla luce =~dell'operatore supportato da [[.
Chepner,

1

Un po 'tardi, ma ho riscontrato questo problema e ho trovato quanto segue:

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

L'ho usato per ottenere la risoluzione di millisecondi su un sistema incorporato che non ha% N per data:

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

1

Una soluzione bash:

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

Questo bloccherà una variabile chiamata x. Il var xpotrebbe essere cambiato nel var _.

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

1

Fine Inklusive, simile alle implementazioni JS e Java. Rimuovi +1 se non lo desideri.

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

Esempio:

    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

Altre chiamate di esempio:

    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

Prego.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.