Teilzeichenfolge in Bash extrahieren


728

Bei einem Dateinamen im Formular someletters_12345_moreleters.extmöchte ich die 5 Ziffern extrahieren und in eine Variable einfügen.

Um den Punkt hervorzuheben, habe ich einen Dateinamen mit x Zeichenanzahl, dann eine fünfstellige Folge, die auf beiden Seiten von einem einzelnen Unterstrich umgeben ist, und dann einen weiteren Satz von x Zeichenanzahl. Ich möchte die 5-stellige Zahl nehmen und in eine Variable einfügen.

Ich bin sehr interessiert an der Anzahl der verschiedenen Möglichkeiten, wie dies erreicht werden kann.


5
Die Antwort von JB ist eindeutig, die Stimmen zu gewinnen - Zeit, die akzeptierte Antwort zu ändern?
Jeff

3
Die meisten Antworten scheinen Ihre Frage nicht zu beantworten, da die Frage nicht eindeutig ist. „Ich habe einen Dateinamen mit x Anzahl der Zeichen , dann wird eine fünfstellige Sequenz auf beiden Seiten von einem einzigen Unterstrich umgab dann ein weiterer Satz von x Anzahl der Zeichen“ . Nach dieser Definition abc_12345_def_67890_ghi_defist eine gültige Eingabe. Was willst du passieren? Nehmen wir an, es gibt nur eine 5-stellige Sequenz. Sie haben noch abc_def_12345_ghi_jkloder 1234567_12345_1234567oder 12345d_12345_12345eals gültige Eingabe basierend auf Ihrer Definition der Eingabe und die meisten der folgenden Antworten werden dies nicht behandeln.
Gman

2
Diese Frage enthält eine zu spezifische Beispieleingabe. Aus diesem Grund erhielt es viele spezifische Antworten für diesen speziellen Fall (nur Ziffern, dasselbe _Trennzeichen, Eingabe, die die Zielzeichenfolge nur einmal enthält usw.). Die beste (allgemeinste und schnellste) Antwort hat nach 10 Jahren nur 7 positive Stimmen, während andere begrenzte Antworten Hunderte haben. Lässt mich das Vertrauen in Entwickler verlieren
Dan

Antworten:


692

Verwenden Sie Schnitt :

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

Allgemeiner:

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

1
Die allgemeinere Antwort ist genau das, wonach ich gesucht habe, danke
Berek Bryan

71
Das Flag -f verwendet 1-basierte Indizes anstelle der 0-basierten Indizes, an die ein Programmierer gewöhnt wäre.
Matthew G

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

3
Sie sollten die Argumente ordnungsgemäß in doppelte Anführungszeichen setzen, es echosei denn, Sie wissen sicher, dass die Variablen keine unregelmäßigen Leerzeichen oder Shell-Metazeichen enthalten können. Siehe weitere stackoverflow.com/questions/10067266/…
Tripleee

Die Zahl '2' nach '-f' gibt der Shell an, den zweiten Teil der Teilzeichenfolge zu extrahieren.
Sandun

1087

Wenn x konstant ist, führt die folgende Parametererweiterung eine Teilzeichenfolgenextraktion durch:

b=${a:12:5}

Dabei ist 12 der Versatz (nullbasiert) und 5 die Länge

Wenn die Unterstriche um die Ziffern die einzigen in der Eingabe sind, können Sie das Präfix bzw. das Suffix in zwei Schritten entfernen:

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

Wenn es andere Unterstriche gibt, ist es wahrscheinlich sowieso machbar, wenn auch schwieriger. Wenn jemand weiß, wie man beide Erweiterungen in einem einzigen Ausdruck ausführt, würde ich es auch gerne wissen.

Beide vorgestellten Lösungen sind reine Bashs, ohne dass Prozesslaichen erforderlich sind, daher sehr schnell.


18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutionauf meiner GNU-Bash 4.2.45.
JB.

2
@jonnyB, Irgendwann in der Vergangenheit hat das funktioniert. Meine Kollegen haben mir gesagt, dass es aufgehört hat, und sie haben es in einen sed-Befehl oder so geändert. Als ich es in der Geschichte betrachtete, führte ich es in einem shSkript aus, das wahrscheinlich ein Strich war. Zu diesem Zeitpunkt kann ich es nicht mehr zum Laufen bringen.
Spencer Rathbun

22
JB, Sie sollten klarstellen, dass "12" der Versatz (nullbasiert) und "5" die Länge ist. Außerdem +1 für den Link von @gontard, der alles beschreibt!
Doktor J

1
Wenn Sie dies in einem Skript als "sh run.sh" ausführen, wird möglicherweise der Fehler "Bad Substitution" angezeigt. Um dies zu vermeiden, ändern Sie die Berechtigungen für run.sh (chmod + x run.sh) und führen Sie das Skript dann als "./run.sh" aus
Ankur

2
Der Offset-Parameter kann übrigens auch negativ sein. Sie müssen nur darauf achten, dass es nicht auf den Doppelpunkt geklebt wird. Andernfalls wird es von bash als :-Ersatz für "Standardwerte verwenden" interpretiert . So ${a: -12:5}ergeben sich die 5 Zeichen 12 Zeichen vom Ende und ${a: -12:-5}die 7 Zeichen zwischen Ende-12 und Ende-5.
JB.

97

Generische Lösung, bei der die Nummer an einer beliebigen Stelle im Dateinamen stehen kann, wobei die erste dieser Sequenzen verwendet wird:

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

Eine andere Lösung, um genau einen Teil einer Variablen zu extrahieren:

number=${filename:offset:length}

Wenn Ihr Dateiname immer das Format stuff_digits_...hat, können Sie awk verwenden:

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

Verwenden Sie noch eine andere Lösung, um alles außer Ziffern zu entfernen

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

2
Was ist, wenn ich die Ziffer / das Wort aus der letzten Zeile der Datei extrahieren möchte.
Eine Sahra

93

versuche es einfach zu benutzen cut -c startIndx-stopIndx


2
Gibt es so etwas wie 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))
braun.2179

1
Das Problem ist, dass die Eingabe dynamisch ist, da ich auch die Pipe verwende, um sie zu erhalten, also ist es im Grunde. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas

Dies kann mit cut erfolgen, wenn in zwei Teile als line=git log --oneline | gebrochen wird head -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `aber in diesem speziellen Fall ist es möglicherweise besser, sed alsgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179

34

Falls jemand strengere Informationen wünscht, können Sie diese auch in Man Bash wie diesen suchen

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

Ergebnis:

$ {Parameter: Offset}
       $ {Parameter: Offset: Länge}
              Teilstringerweiterung. Erweitert sich auf bis zu lange Zeichen von
              Parameter beginnend mit dem durch Offset angegebenen Zeichen. Wenn
              Länge wird weggelassen, erweitert sich auf den Teilstring des Parameters start‐
              bei dem durch Offset angegebenen Zeichen. Länge und Versatz sind
              arithmetische Ausdrücke (siehe ARITHMETISCHE BEWERTUNG unten). Wenn
              Offset ergibt eine Zahl kleiner als Null, der Wert wird verwendet
              als Versatz vom Ende des Parameterwerts. Arithmetik
              Ausdrücke, die mit einem - beginnen, müssen durch Leerzeichen getrennt werden
              vom vorhergehenden: zu unterscheiden vom Use Default
              Werteerweiterung. Wenn die Länge eine Zahl kleiner als ergibt
              Null, und der Parameter ist nicht @ und kein indizierter oder assoziativer Parameter
              Array wird als Versatz vom Ende des Werts interpretiert
              von Parameter statt einer Anzahl von Zeichen, und die Erweiterung
              sion sind die Zeichen zwischen den beiden Offsets. Wenn Parameter ist
              @, das Ergebnis sind Längenpositionsparameter, die bei off‐ beginnen
              einstellen. Wenn der Parameter ein indizierter Array-Name ist, der durch @ oder tiefgestellt ist
              *, das Ergebnis sind die Längenelemente des Arrays, die mit beginnen
              $ {Parameter [Offset]}. Ein negativer Versatz wird relativ zu genommen
              Eins größer als der maximale Index des angegebenen Arrays. Sub‐
              Die auf ein assoziatives Array angewendete String-Erweiterung erzeugt Unde‐
              Geldstrafe Ergebnisse. Beachten Sie, dass ein negativer Versatz getrennt werden muss
              vom Doppelpunkt um mindestens ein Leerzeichen, um Verwirrung zu vermeiden
              mit der: - Erweiterung. Die Teilstring-Indizierung basiert auf Null, es sei denn
              Die Positionsparameter werden verwendet, in diesem Fall die Indizierung
              beginnt standardmäßig bei 1. Wenn der Versatz 0 ist und die Position
              Wenn Parameter verwendet werden, wird der Liste $ 0 vorangestellt.

2
Eine sehr wichtige Einschränkung bei negativen Werten wie oben angegeben: Arithmetische Ausdrücke, die mit a - beginnen, müssen durch Leerzeichen vom vorhergehenden getrennt werden: um von der Erweiterung "Standardwerte verwenden" unterschieden zu werden. Also, um die letzten vier Zeichen einer Var zu bekommen:${var: -4}
Show

26

So würde ich es machen:

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

Erläuterung:

Bash-spezifisch:

Reguläre Ausdrücke (RE): _([[:digit:]]{5})_

  • _ sind Literale, um Übereinstimmungsgrenzen für die übereinstimmende Zeichenfolge abzugrenzen / zu verankern
  • () Erstellen Sie eine Erfassungsgruppe
  • [[:digit:]] ist eine Charakterklasse, ich denke sie spricht für sich
  • {5} bedeutet, dass genau fünf der vorherigen Zeichen, Klassen (wie in diesem Beispiel) oder Gruppen übereinstimmen müssen

Auf Englisch können Sie sich vorstellen, dass es sich so verhält: Die FNZeichenfolge wird zeichenweise wiederholt, bis wir sehen, _an welchem ​​Punkt die Erfassungsgruppe geöffnet wird, und wir versuchen, fünf Ziffern zuzuordnen. Wenn dieser Abgleich bis zu diesem Punkt erfolgreich ist, speichert die Erfassungsgruppe die fünf durchquerten Ziffern. Wenn das nächste Zeichen ein ist _, ist die Bedingung erfolgreich, die Erfassungsgruppe wird in verfügbar gemacht BASH_REMATCHund die nächste NUM=Anweisung kann ausgeführt werden. Wenn ein Teil des Abgleichs fehlschlägt, werden gespeicherte Details entsorgt und die zeichenweise Verarbeitung wird nach dem fortgesetzt _. zB wenn FNwo _1 _12 _123 _1234 _12345_, würde es vier Fehlstarts geben, bevor es eine Übereinstimmung gefunden hat.


3
Dies ist eine generische Methode, die auch dann funktioniert, wenn Sie wie ich mehr als eine Sache extrahieren müssen.
Zebediah49

3
Dies ist in der Tat die allgemeinste Antwort und sollte akzeptiert werden. Es funktioniert für einen regulären Ausdruck, nicht nur für eine Zeichenfolge an einer festen Position oder zwischen demselben Trennzeichen (was aktiviert cut). Es ist auch nicht auf die Ausführung eines externen Befehls angewiesen.
Dan Dascalescu

1
Diese Antwort wird strafrechtlich unterbewertet.
Chepper

Das ist toll! Ich habe dies angepasst, um für meine Situation unterschiedliche Start / Stopp-Dilimeter (ersetzen Sie das _) und variable Längenzahlen (. Für {5}) zu verwenden. Kann jemand diese schwarze Magie zerlegen und erklären?
Paul

1
@ Paul Ich habe meiner Antwort weitere Details hinzugefügt. Ich hoffe, das hilft.
Nicerobot

21

Ich bin überrascht, dass diese reine Bash-Lösung nicht aufgetaucht ist:

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

Sie möchten IFS wahrscheinlich auf den Wert zurücksetzen, der vorher oder unset IFSnachher war!


1
Es ist keine reine Bash-Lösung, ich denke, es funktioniert in reiner Shell (/ bin / sh)
Kayn

5
+1 Sie könnten dies auf eine andere Weise schreiben, um zu vermeiden, dass IFSParameter nicht gesetzt und positioniert werden müssen:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
Kojiro

2
Dies unterliegt der Pfadnamenerweiterung! (Also ist es kaputt).
gniourf_gniourf

20

Aufbauend auf Jors Antwort (was bei mir nicht funktioniert):

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

12
Reguläre Ausdrücke sind die eigentliche Behandlung , wenn man etwas kompliziert haben und einfach zu zählen Unterstrichen wird nicht cutes.
Aleksandr Levchuk

12

Den Anforderungen folgen

Ich habe einen Dateinamen mit x Zeichenanzahl, dann eine fünfstellige Folge, umgeben von einem einzelnen Unterstrich auf beiden Seiten, dann einen weiteren Satz von x Zeichenanzahl. Ich möchte die 5-stellige Zahl nehmen und in eine Variable einfügen.

Ich habe einige grepMöglichkeiten gefunden, die nützlich sein können:

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

oder besser

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

Und dann mit -PoSyntax:

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

Oder wenn Sie möchten, dass es genau 5 Zeichen passt:

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

Um es in einer Variablen zu speichern, muss nur die var=$(command)Syntax verwendet werden.


2
Ich glaube, heutzutage besteht keine Notwendigkeit, egrep zu verwenden. Der Befehl selbst warnt Sie : Invocation as 'egrep' is deprecated; use 'grep -E' instead. Ich habe deine Antwort bearbeitet.
Neurotransmitter

11

Wenn wir uns auf das Konzept konzentrieren:
"Eine Folge von (einer oder mehreren) Ziffern"

Wir könnten mehrere externe Tools verwenden, um die Zahlen zu extrahieren.
Wir könnten ganz leicht alle anderen Zeichen löschen, entweder sed oder tr:

name='someletters_12345_moreleters.ext'

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

Wenn $ name jedoch mehrere Zahlenläufe enthält, schlägt Folgendes fehl:

Wenn "name = someletters_12345_moreleters_323_end.ext", dann:

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

Wir müssen regelmäßige Ausdrücke (Regex) verwenden.
So wählen Sie nur den ersten Lauf (12345, nicht 323) in sed und perl aus:

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

Aber wir könnten es genauso gut direkt in bash (1) tun :

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

Auf diese Weise können wir den ERSTEN Ziffernlauf beliebiger Länge extrahieren, der
von anderen Texten / Zeichen umgeben ist.

Hinweis : regex=[^0-9]*([0-9]{5,5}).*$;Entspricht nur genau 5-stelligen Läufen. :-)

(1) : schneller als das Aufrufen eines externen Tools für jeden Kurztext. Nicht schneller als die gesamte Verarbeitung in sed oder awk für große Dateien.


10

Ohne Unterprozesse können Sie:

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

Eine sehr kleine Variante davon funktioniert auch in ksh93.


9

Hier ist eine Präfix-Suffix-Lösung (ähnlich den von JB und Darron angegebenen Lösungen), die mit dem ersten Ziffernblock übereinstimmt und nicht von den umgebenden Unterstrichen abhängt:

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

Ich liebe seddie Fähigkeit, mit Regex-Gruppen umzugehen:

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

Eine etwas allgemeinere Option wäre, nicht anzunehmen, dass Sie einen Unterstrich haben _, der den Beginn Ihrer Ziffernfolge markiert, und beispielsweise alle Nicht-Zahlen, die Sie vor Ihrer Sequenz erhalten, entfernen : 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.

Mehr dazu, falls Sie mit regulären Ausdrücken nicht allzu sicher sind:

  • s ist für _s_ubstitute
  • [0-9]+ entspricht 1+ Ziffern
  • \1 Links zur Gruppe Nr. 1 der Regex-Ausgabe (Gruppe 0 ist die gesamte Übereinstimmung, Gruppe 1 ist in diesem Fall die Übereinstimmung in Klammern)
  • p Flag ist für _p_rinting

Alle Escapezeichen \sind dazu da, dass seddie Regexp-Verarbeitung funktioniert.


6

Meine Antwort hat mehr Kontrolle darüber, was Sie von Ihrem String erwarten. Hier ist der Code, wie Sie 12345aus Ihrer Zeichenfolge extrahieren können

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

Dies ist effizienter, wenn Sie etwas extrahieren möchten, das Zeichen wie abcoder Sonderzeichen wie _oder enthält -. Zum Beispiel: Wenn Ihre Zeichenfolge so ist und Sie alles wollen, was nach someletters_und vor ist _moreleters.ext:

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

Mit meinem Code können Sie genau angeben, was Sie wollen. Erläuterung:

#*Die vorhergehende Zeichenfolge einschließlich des passenden Schlüssels wird entfernt. Hier ist der Schlüssel, den wir erwähnt haben: _ %Er entfernt die folgende Zeichenfolge einschließlich des passenden Schlüssels. Hier ist der Schlüssel, den wir erwähnt haben, '_more *'

Machen Sie selbst einige Experimente und Sie würden dies interessant finden.


6

Gegeben ist test.txt eine Datei, die "ABCDEFGHIJKLMNOPQRSTUVWXYZ" enthält.

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

Dies ist äußerst spezifisch für diesen bestimmten Eingang. Die einzige allgemeine Lösung für die allgemeine Frage (die das OP hätte stellen sollen) ist die Verwendung eines regulären Ausdrucks .
Dan Dascalescu

3

Ok, hier geht reine Parameterersetzung mit einer leeren Zeichenfolge. Vorbehalt ist, dass ich Someletters und Moreletters als einzige Charaktere definiert habe . Wenn sie alphanumerisch sind, funktioniert dies nicht so wie es ist.

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

2
genial, erfordert aber mindestens bash v4
olibre


1

Es gibt auch den eingebauten Befehl 'expr' von bash:

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

4
exprist kein eingebauter.
gniourf_gniourf

1
Dies ist auch angesichts des von =~unterstützten Betreibers nicht erforderlich [[.
Chepper

1

Ein bisschen spät, aber ich bin gerade auf dieses Problem gestoßen und habe Folgendes festgestellt:

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

Ich habe es verwendet, um eine Millisekundenauflösung auf einem eingebetteten System zu erhalten, das für das Datum nicht% N enthält:

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

1

Eine Bash-Lösung:

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

Dadurch wird eine aufgerufene Variable blockiert x. Die Var xkönnte in die Var geändert werden _.

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

1

Inklusive Ende, ähnlich wie bei JS- und Java-Implementierungen. Entfernen Sie +1, wenn Sie dies nicht wünschen.

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

Beispiel:

    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

Weitere Beispielaufrufe:

    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

Bitte schön.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.