Extraire la sous-chaîne dans Bash


729

Étant donné un nom de fichier dans le formulaire someletters_12345_moreleters.ext, je veux extraire les 5 chiffres et les mettre dans une variable.

Donc, pour souligner le point, j'ai un nom de fichier avec x nombre de caractères puis une séquence de cinq chiffres entourée d'un seul trait de soulignement de chaque côté puis un autre ensemble de x nombre de caractères. Je veux prendre le nombre à 5 chiffres et le mettre dans une variable.

Je suis très intéressé par le nombre de façons différentes dont cela peut être accompli.


5
La réponse de JB gagne clairement les votes - il est temps de changer la réponse acceptée?
Jeff

3
La plupart des réponses ne semblent pas répondre à votre question car la question est ambiguë. "J'ai un nom de fichier avec x nombre de caractères puis une séquence de cinq chiffres entourée d'un trait de soulignement unique de chaque côté puis d'un autre ensemble de x nombre de caractères" . Par cette définition abc_12345_def_67890_ghi_defest une entrée valide. Qu'est-ce que tu veux qu'il arrive? Supposons qu'il n'y ait qu'une seule séquence à 5 chiffres. Vous avez toujours abc_def_12345_ghi_jklou 1234567_12345_1234567ou 12345d_12345_12345ecomme entrée valide en fonction de votre définition de l'entrée et la plupart des réponses ci-dessous ne traiteront pas cela.
gman

2
Cette question contient un exemple d'entrée trop spécifique. Pour cette raison, il a obtenu beaucoup de réponses spécifiques pour ce cas particulier (chiffres uniquement, même _délimiteur, entrée qui ne contient la chaîne cible qu'une seule fois, etc.). La meilleure réponse (la plus générique et la plus rapide) n'a, après 10 ans, que 7 votes positifs, tandis que d'autres réponses limitées en comptent des centaines. Me fait perdre confiance dans les développeurs 😞
Dan Dascalescu

Réponses:


693

Utiliser la coupe :

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

Plus générique:

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

1
la réponse la plus générique est exactement ce que je cherchais, merci
Berek Bryan

71
L'indicateur -f prend des indices basés sur 1, plutôt que les indices basés sur 0 auxquels un programmeur serait habitué.
Matthew G

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

3
Vous devez utiliser correctement les guillemets doubles autour des arguments à echomoins que vous ne sachiez avec certitude que les variables ne peuvent pas contenir des espaces blancs irréguliers ou des métacaractères shell. Voir plus stackoverflow.com/questions/10067266/…
tripleee

Le nombre «2» après «-f» est de dire à shell d'extraire le 2e ensemble de sous-chaînes.
Sandun

1088

Si x est constant, l'expansion de paramètre suivante effectue l'extraction de la sous-chaîne:

b=${a:12:5}

12 est le décalage (basé sur zéro) et 5 est la longueur

Si les traits de soulignement autour des chiffres sont les seuls dans l'entrée, vous pouvez supprimer le préfixe et le suffixe (respectivement) en deux étapes:

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

S'il y a d'autres soulignements, c'est probablement possible de toute façon, bien que plus délicat. Si quelqu'un sait comment effectuer les deux extensions en une seule expression, j'aimerais aussi le savoir.

Les deux solutions présentées sont purement bash, sans apparition de processus, donc très rapide.


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

2
@jonnyB, Quelque temps dans le passé qui a fonctionné. Mes collègues m'ont dit que cela s'était arrêté, et ils l'ont changé pour être une commande sed ou quelque chose. En le regardant dans l'histoire, je l'exécutais dans un shscript, qui était probablement un tiret. À ce stade, je ne peux plus le faire fonctionner.
Spencer Rathbun

22
JB, vous devez préciser que "12" est le décalage (basé sur zéro) et "5" est la longueur. Aussi, +1 pour le lien de @gontard qui expose tout!
Doktor J

1
En exécutant ceci dans un script comme "sh run.sh", on pourrait obtenir une erreur de mauvaise substitution. Pour éviter cela, modifiez les autorisations pour run.sh (chmod + x run.sh) puis exécutez le script en tant que "./run.sh"
Ankur

2
Le paramètre de décalage peut également être négatif, BTW. Il vous suffit de prendre soin de ne pas le coller aux deux points, ou bash l'interprétera comme une :-substitution «Utiliser les valeurs par défaut». ${a: -12:5}Donne donc les 5 caractères 12 caractères à partir de la fin, et ${a: -12:-5}les 7 caractères entre la fin 12 et la fin 5.
JB.

97

Solution générique où le numéro peut être n'importe où dans le nom de fichier, en utilisant la première de ces séquences:

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

Une autre solution pour extraire exactement une partie d'une variable:

number=${filename:offset:length}

Si votre nom de fichier a toujours le format, stuff_digits_...vous pouvez utiliser awk:

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

Encore une autre solution pour tout supprimer sauf les chiffres, utilisez

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

2
Et si je veux extraire le chiffre / mot de la dernière ligne du fichier.
Un Sahra

93

essayez d'utiliser cut -c startIndx-stopIndx


2
Existe-t-il quelque chose comme 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
Le problème est que l'entrée est dynamique car j'utilise également le tuyau pour l'obtenir, donc c'est fondamentalement. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas

Cela peut être fait avec cut si se line=divise en deux parties comme git log --oneline | head -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `mais dans ce cas particulier, il serait préférable d'utiliser sed asgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179

34

Si quelqu'un veut des informations plus rigoureuses, vous pouvez également les rechercher dans man bash comme ceci

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

Résultat:

$ {paramètre: offset}
       $ {paramètre: offset: longueur}
              Extension de sous-chaîne. Se développe jusqu'à une longueur de caractères de
              paramètre commençant par le caractère spécifié par offset. Si
              la longueur est omise, s'étend à la sous-chaîne du paramètre start‐
              au caractère spécifié par offset. la longueur et le décalage sont
              expressions arithmétiques (voir ÉVALUATION ARITHMÉTIQUE ci-dessous). Si
              offset est évalué à un nombre inférieur à zéro, la valeur est utilisée
              comme décalage par rapport à la fin de la valeur du paramètre. Arithmétique
              les expressions commençant par un - doivent être séparées par des espaces
              du précédent: à distinguer du Use Default
              Expansion des valeurs. Si la longueur est évaluée à un nombre inférieur à
              zéro, et le paramètre n'est pas @ et n'est pas indexé ou associatif
              tableau, il est interprété comme un décalage par rapport à la fin de la valeur
              du paramètre plutôt que d'un certain nombre de caractères, et l'extension
              sion correspond aux caractères entre les deux décalages. Si le paramètre est
              @, le résultat est des paramètres positionnels de longueur commençant à off‐
              ensemble. Si le paramètre est un nom de tableau indexé souscrit par @ ou
              *, le résultat est la longueur des membres du tableau commençant par
              $ {paramètre [offset]}. Un décalage négatif est pris par rapport à
              un supérieur à l'index maximum du tableau spécifié. Sous-
              l'expansion de chaîne appliquée à un tableau associatif produit indé‐
              résultats amendés. Notez qu'un décalage négatif doit être séparé
              du côlon par au moins un espace pour éviter d'être confondu
              avec: - l'expansion. L'indexation des sous-chaînes est basée sur zéro sauf si
              les paramètres de position sont utilisés, auquel cas l'indexation
              commence à 1 par défaut. Si offset est 0 et que la position
              paramètres sont utilisés, $ 0 est préfixé à la liste.

2
Une mise en garde très importante avec des valeurs négatives comme indiqué ci-dessus: Les expressions arithmétiques commençant par un - doivent être séparées par des espaces des précédents: pour être distinguées de l'expansion Utiliser les valeurs par défaut. Donc, pour obtenir les quatre derniers caractères d'un var:${var: -4}
sshow

26

Voici comment je le ferais:

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

Explication:

Spécifique à Bash:

Expressions régulières (RE): _([[:digit:]]{5})_

  • _ sont des littéraux pour délimiter / ancrer les limites de correspondance pour la chaîne en correspondance
  • () créer un groupe de capture
  • [[:digit:]] est une classe de personnage, je pense qu'elle parle d'elle-même
  • {5} signifie exactement cinq du caractère précédent, la classe (comme dans cet exemple) ou le groupe doit correspondre

En anglais, vous pouvez penser qu'elle se comporte comme ceci: la FNchaîne est itérée caractère par caractère jusqu'à ce que nous voyions un _point auquel le groupe de capture est ouvert et nous essayons de faire correspondre cinq chiffres. Si cette correspondance réussit à ce stade, le groupe de capture enregistre les cinq chiffres parcourus. Si le caractère suivant est un _, la condition réussit, le groupe de capture est rendu disponible dans BASH_REMATCHet l' NUM=instruction suivante peut s'exécuter. Si une partie de la correspondance échoue, les détails enregistrés sont supprimés et le traitement caractère par caractère se poursuit après le _. Par exemple, si FN_1 _12 _123 _1234 _12345_, il y aurait quatre faux départs avant qu'il ne trouve une correspondance.


3
C'est une façon générique qui fonctionne même si vous devez extraire plus d'une chose, comme je l'ai fait.
zebediah49

3
Il s'agit en effet de la réponse la plus générique qui devrait être acceptée. Cela fonctionne pour une expression régulière, pas seulement une chaîne de caractères à une position fixe, ou entre le même délimiteur (ce qui permet cut). Il ne dépend pas non plus de l'exécution d'une commande externe.
Dan Dascalescu

1
Cette réponse est criminellement sous-évaluée.
chepner

C'est bien! J'ai adapté cela pour utiliser différents dilimètres start / stop (remplacer le _) et des nombres de longueur variable (. Pour {5}) pour ma situation. Quelqu'un peut-il briser cette magie noire et l'expliquer?
Paul

1
@Paul J'ai ajouté plus de détails à ma réponse. J'espère que cela pourra aider.
nicerobot

21

Je suis surpris que cette solution pure bash ne soit pas venue:

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

Vous voudrez probablement réinitialiser IFS à sa valeur avant ou unset IFSaprès!


1
ce n'est pas une solution bash pure, je pense que cela fonctionne en shell pur (/ bin / sh)
kayn

5
+1 Vous pouvez écrire ceci d'une autre manière pour éviter d'avoir à IFSIFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
désactiver les

2
Ceci est sujet à l'extension du nom de chemin! (donc c'est cassé).
gniourf_gniourf

20

S'appuyant sur la réponse de Jor (qui ne fonctionne pas pour moi):

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

12
Les expressions régulières sont la vraie affaire lorsque vous avez quelque chose de compliqué et que le simple fait de compter les traits de soulignement ne le sera pas cut.
Aleksandr Levchuk

12

Suivre les exigences

J'ai un nom de fichier avec x nombre de caractères puis une séquence de cinq chiffres entourée d'un seul trait de soulignement de chaque côté puis d'un autre ensemble de x nombre de caractères. Je veux prendre le nombre à 5 chiffres et le mettre dans une variable.

J'ai trouvé quelques grepmoyens qui peuvent être utiles:

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

ou mieux

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

Et puis avec la -Posyntaxe:

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

Ou si vous voulez que cela corresponde exactement à 5 caractères:

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

Enfin, pour le stocker dans une variable il suffit d'utiliser la var=$(command)syntaxe.


2
Je crois aujourd'hui qu'il n'y a pas besoin d'utiliser egrep, la commande elle - même vous avertit: Invocation as 'egrep' is deprecated; use 'grep -E' instead. J'ai édité votre réponse.
Neurotransmetteur

11

Si nous nous concentrons sur le concept de:
"Une série de (un ou plusieurs) chiffres"

Nous pourrions utiliser plusieurs outils externes pour extraire les chiffres.
Nous pourrions assez facilement effacer tous les autres personnages, sed ou tr:

name='someletters_12345_moreleters.ext'

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

Mais si $ name contient plusieurs séries de nombres, ce qui précède échouera:

Si "name = someletters_12345_moreleters_323_end.ext", alors:

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

Nous devons utiliser des expressions régulières (regex).
Pour sélectionner uniquement la première exécution (12345 et non 323) dans sed et perl:

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

Mais on pourrait aussi bien le faire directement en bash (1) :

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

Cela nous permet d'extraire la PREMIÈRE série de chiffres de n'importe quelle longueur
entourée de tout autre texte / caractère.

Remarque : regex=[^0-9]*([0-9]{5,5}).*$;ne correspondra qu'à exactement 5 séries de chiffres. :-)

(1) : plus rapide que d'appeler un outil externe pour chaque texte court. Pas plus rapide que de faire tout le traitement dans sed ou awk pour les gros fichiers.


10

Sans aucun sous-processus, vous pouvez:

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

Une très petite variante de ceci fonctionnera également dans ksh93.


9

Voici une solution préfixe-suffixe (similaire aux solutions proposées par JB et Darron) qui correspond au premier bloc de chiffres et ne dépend pas des traits de soulignement environnants:

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

J'adore sedla capacité de traiter avec des groupes d'expressions régulières:

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

Une option un peu plus générale serait pas supposer que vous avez un trait de soulignement _marquant le début de votre séquence de chiffres, donc , par exemple , dépouillant tous les non-chiffres que vous obtenez avant votre séquence: 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.

Plus à ce sujet, au cas où vous ne seriez pas trop confiant avec les expressions régulières:

  • s est pour _s_ubstitute
  • [0-9]+ correspond à 1+ chiffres
  • \1 liens vers le groupe n.1 de la sortie d'expression régulière (le groupe 0 est la correspondance complète, le groupe 1 est la correspondance entre parenthèses dans ce cas)
  • p le drapeau est pour _p_rinting

Toutes les échappées \sont là pour faire fonctionner sedle traitement des expressions rationnelles.


6

Ma réponse aura plus de contrôle sur ce que vous voulez sortir de votre chaîne. Voici le code sur la façon d'extraire 12345de votre chaîne

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

Ce sera plus efficace si vous voulez extraire quelque chose qui a des caractères comme abcou des caractères spéciaux comme _ou -. Par exemple: si votre chaîne est comme ça et que vous voulez tout ce qui est après someletters_et avant _moreleters.ext:

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

Avec mon code, vous pouvez mentionner exactement ce que vous voulez. Explication:

#*Il supprimera la chaîne précédente, y compris la clé correspondante. Ici, la clé que nous avons mentionnée est_ % Elle supprimera la chaîne suivante, y compris la clé correspondante. Ici, la clé que nous avons mentionnée est '_more *'

Faites vous-même des expériences et vous trouverez cela intéressant.


6

Étant donné que test.txt est un fichier contenant "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

Ceci est extrêmement spécifique à cette entrée particulière. La seule solution générale à la question générale (que l'OP aurait dû poser) est d' utiliser une expression rationnelle .
Dan Dascalescu

3

Ok, voici la substitution de paramètres pure avec une chaîne vide. La mise en garde est que j'ai défini des someletters et des moreletters comme étant uniquement des personnages. S'ils sont alphanumériques, cela ne fonctionnera pas tel quel.

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

2
génial mais nécessite au moins bash v4
olibre


1

Il y a aussi la commande bash builtin 'expr':

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

4
exprn'est pas une fonction intégrée.
gniourf_gniourf

1
Ce n'est pas non plus nécessaire compte tenu de l' =~opérateur soutenu par [[.
chepner

1

Un peu tard, mais je viens de rencontrer ce problème et j'ai trouvé ce qui suit:

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

Je l'ai utilisé pour obtenir une résolution en millisecondes sur un système embarqué qui n'a pas% N pour la date:

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

1

Une solution bash:

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

Cela encombrera une variable appelée x. Le var xpeut être changé en var _.

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

1

Fin Inklusive, similaire aux implémentations JS et Java. Supprimez +1 si vous ne le souhaitez pas.

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

Exemple:

    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

Plus d'exemples d'appels:

    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

Je vous en prie.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.