Wyodrębnij podciąg w Bash

728

Biorąc pod uwagę nazwę pliku w formularzu someletters_12345_moreleters.ext, chcę wyodrębnić 5 cyfr i umieścić je w zmiennej.

Aby podkreślić ten punkt, mam nazwę pliku z x liczbą znaków, a następnie pięciocyfrową sekwencję otoczoną pojedynczym podkreśleniem po każdej stronie, a następnie innym zestawem x liczby znaków. Chcę wziąć pięciocyfrowy numer i umieścić go w zmiennej.

Jestem bardzo zainteresowany wieloma różnymi sposobami osiągnięcia tego celu.

Berek Bryan
źródło
5
Odpowiedź JB wyraźnie wygrywa głosy - czas zmienić przyjętą odpowiedź?
Jeff
3
Większość odpowiedzi wydaje się nie odpowiadać na twoje pytanie, ponieważ pytanie jest dwuznaczne. „Mam nazwę pliku zawierającą x liczby znaków, a następnie pięciocyfrową sekwencję otoczoną pojedynczym podkreśleniem po obu stronach, a następnie innym zestawem x liczby znaków” . Zgodnie z tą definicją abc_12345_def_67890_ghi_defjest to poprawny wkład. Co chcesz się wydarzyć Załóżmy, że istnieje tylko jedna 5-cyfrowa sekwencja. Nadal masz abc_def_12345_ghi_jkllub 1234567_12345_1234567lub 12345d_12345_12345ejako ważny wkład w oparciu o definicję wejścia i większość odpowiedzi poniżej nie zajmie to.
gman,
2
To pytanie zawiera przykładowe informacje, które są zbyt szczegółowe. Z tego powodu otrzymało wiele konkretnych odpowiedzi dla tego konkretnego przypadku (tylko cyfry, ten sam _separator, dane wejściowe zawierające ciąg docelowy tylko raz itp.). Odpowiedź najlepiej (najbardziej ogólny i najszybciej) ma po 10 lat, tylko 7 upvotes, podczas gdy inne ograniczone odpowiedzi setki. Sprawia, że ​​tracę wiarę w programistów 😞
Dan Dascalescu

Odpowiedzi:

691

Użyj cięcia :

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

Bardziej ogólny:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING
FerranB
źródło
1
bardziej ogólna odpowiedź jest dokładnie tym, czego szukałem, dzięki
Berek Bryan,
71
Flaga -f przyjmuje indeksy oparte na 1, a nie indeksy oparte na 0, do których przywykłby programista.
Matthew G
2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $ (echo $ INPUT | cut -d'_ '-f 2) echo $ SUBSTRING
mani deepak
3
Powinieneś właściwie używać podwójnych cudzysłowów wokół argumentów, echochyba że wiesz na pewno, że zmienne nie mogą zawierać nieregularnych białych znaków lub metaznaków powłoki. Zobacz więcej stackoverflow.com/questions/10067266/…
tripleee
Liczba „2” po „-f” oznacza, że ​​powłoka ma wyodrębnić drugi zestaw podłańcuchów.
Sandun,
1085

Jeśli x jest stałe, następujące rozwinięcie parametru wykonuje ekstrakcję podłańcucha:

b=${a:12:5}

gdzie 12 to przesunięcie (liczone od zera), a 5 to długość

Jeśli podkreślenia wokół cyfr są jedynymi na wejściu, możesz usunąć odpowiednio prefiks i sufiks w dwóch krokach:

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

Jeśli istnieją inne znaki podkreślenia, jest to prawdopodobnie wykonalne, choć trudniejsze. Jeśli ktoś wie, jak wykonać obie ekspansje w jednym wyrażeniu, też chciałbym wiedzieć.

Oba przedstawione rozwiązania to czysta gra, bez udziału procesu odradzania, a więc bardzo szybko.

JB.
źródło
18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutionna moim GNU bash 4.2.45.
JB.
2
@jonnyB, Jakiś czas w przeszłości działało. Moi współpracownicy powiedzieli mi, że to się skończyło, a oni zmienili to na rozkaz sed czy coś takiego. Patrząc na to w historii, uruchomiłem go w shskrypcie, który prawdopodobnie był kreską. W tym momencie nie mogę już tego uruchomić.
Spencer Rathbun
22
JB, powinieneś wyjaśnić, że „12” to przesunięcie (liczone od zera), a „5” to długość. Ponadto +1 za link @gontard, który to wszystko określa!
Doktor J
1
Podczas uruchamiania tego w skrypcie jako „sh run.sh”, może pojawić się błąd Bad Substitution. Aby tego uniknąć, zmień uprawnienia dla run.sh (chmod + x run.sh), a następnie uruchom skrypt jako „./run.sh”
Ankur
2
Przesunięty parametr może być również ujemny, BTW. Musisz tylko uważać, aby nie przykleić go do okrężnicy, w przeciwnym razie bash zinterpretuje to jako podstawienie :-„Użyj wartości domyślnych”. Więc ${a: -12:5}daje 5 znaków 12 znaków od końca, a ${a: -12:-5}7 znaków pomiędzy końcem 12 i koniec-5.
JB.
97

Ogólne rozwiązanie, w którym liczba może znajdować się w dowolnym miejscu w nazwie pliku, przy użyciu pierwszej z takich sekwencji:

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

Kolejne rozwiązanie, aby wyodrębnić dokładnie część zmiennej:

number=${filename:offset:length}

Jeśli twoja nazwa pliku ma zawsze format stuff_digits_..., możesz użyć awk:

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

Jeszcze jedno rozwiązanie, aby usunąć wszystko oprócz cyfr, użyj

number=$(echo $filename | tr -cd '[[:digit:]]')
Johannes Schaub - litb
źródło
2
Co jeśli chcę wyodrębnić cyfrę / słowo z ostatniego wiersza pliku.
Sahra,
93

po prostu spróbuj użyć cut -c startIndx-stopIndx

brązowy. 2179
źródło
2
Czy istnieje coś takiego jak 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))
brązowy. 2117
1
Problem polega na tym, że dane wejściowe są dynamiczne, ponieważ używam również potoku, aby je uzyskać, więc jest to w zasadzie. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas
Można to zrobić za pomocą cut, jeśli zostanie podzielony na dwie części jako line=git log --oneline | head -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `, ale w tym konkretnym przypadku lepiej użyć sed asgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179
34

W przypadku, gdy ktoś chce bardziej rygorystycznych informacji, możesz je również przeszukać w taki sposób

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

Wynik:

$ {parametr: offset}
       $ {parametr: offset: długość}
              Rozbudowa podciągów. Rozwija się do znaków o długości do
              parametr rozpoczynający się od znaku określonego przez offset. Gdyby
              długość jest pomijana, rozwija się do podłańcucha parametru start-
              ing na znak określony przez offset. długość i przesunięcie wynoszą
              wyrażenia arytmetyczne (patrz OCENA ARYTMETYCZNA poniżej). Gdyby
              Przesunięcie zwraca liczbę mniejszą niż zero, wartość jest używana
              jako przesunięcie od końca wartości parametru. Arytmetyka
              wyrażenia zaczynające się od - muszą być oddzielone spacjami
              z poprzedniego: należy odróżnić od opcji Użyj domyślnej
              Rozszerzenie wartości. Jeśli długość ma wartość mniejszą niż
              zero, a parametr nie jest @ i nie jest indeksowany ani asocjatywny
              tablica jest interpretowana jako przesunięcie od końca wartości
              parametru, a nie liczby znaków, oraz rozwinięcie
              Syjon to znaki między dwiema odsunięciami. Jeśli parametr to
              @, wynikiem są parametry pozycyjne długości zaczynające się od
              zestaw. Jeśli parametr to indeksowana nazwa tablicy indeksowana przez @ lub
              *, wynikiem są elementy długości tablicy zaczynające się od
              $ {parametr [przesunięcie]}. Przesunięcie ujemne przyjmuje się względem
              jeden większy niż maksymalny indeks określonej tablicy. Pod-
              rozwinięcie łańcucha zastosowane do tablicy asocjacyjnej powoduje, że
              grzywny wyniki. Pamiętaj, że przesunięcie ujemne musi być oddzielone
              od okrężnicy o co najmniej jedno pole, aby uniknąć pomyłki
              z: - rozszerzeniem. Indeksowanie podciągów jest zerowane, chyba że
              używane są parametry pozycyjne, w którym to przypadku indeksowane
              domyślnie zaczyna się od 1. Jeśli offset wynosi 0, a pozycja
              parametry są używane, $ 0 jest prefiksem na liście.
jperelli
źródło
2
Bardzo ważne zastrzeżenie o ujemnych wartościach jak podano powyżej: Wyrażenia arytmetyczne rozpoczynające się od - należy oddzielić białymi spacjami od poprzedniego: aby odróżnić je od rozszerzenia Użyj wartości domyślnych. Aby uzyskać cztery ostatnie znaki var:${var: -4}
pokaż
26

Oto jak bym to zrobił:

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

Wyjaśnienie:

Specyficzne dla Bash:

Wyrażenia regularne (RE): _([[:digit:]]{5})_

  • _ są literałami wyznaczającymi / kotwiczącymi granice dopasowania dla dopasowanego łańcucha
  • () utwórz grupę przechwytywania
  • [[:digit:]] to klasa postaci, myślę, że mówi sama za siebie
  • {5} oznacza dokładnie pięć z poprzedniego znaku, klasy (jak w tym przykładzie) lub grupy musi pasować

Po angielsku możesz pomyśleć, że zachowuje się tak: FNciąg jest iterowany znak po znaku, dopóki nie zobaczymy, _w którym momencie grupa przechwytywania jest otwarta i próbujemy dopasować pięć cyfr. Jeśli to dopasowanie zakończy się pomyślnie do tego momentu, grupa przechwytywania zapisuje pięć cyfr, po których następuje przejście. Jeśli następnym znakiem jest an _, warunek się powiódł, grupa przechwytywania jest dostępna w BASH_REMATCHi NUM=można wykonać następną instrukcję. Jeśli którakolwiek część dopasowania nie powiedzie się, zapisane szczegóły są usuwane, a przetwarzanie znak po znaku jest kontynuowane po _. np. jeśli FNgdzie _1 _12 _123 _1234 _12345_, będą cztery fałszywe starty, zanim znajdzie dopasowanie.

nicerobot
źródło
3
Jest to ogólny sposób, który działa, nawet jeśli musisz wyodrębnić więcej niż jedną rzecz, tak jak ja.
zebediah49
3
Jest to rzeczywiście najbardziej ogólna odpowiedź i należy ją przyjąć. Działa dla wyrażenia regularnego, a nie tylko ciągu znaków w ustalonej pozycji lub między tym samym ogranicznikiem (co umożliwia cut). Nie polega również na wykonaniu zewnętrznego polecenia.
Dan Dascalescu,
1
Ta odpowiedź jest niedoceniana w sprawach karnych.
chepner
To jest świetne! Zaadaptowałem to do używania różnych rozcieńczalników start / stop (zamień _) i liczb o zmiennej długości (. Dla {5}) w mojej sytuacji. Czy ktoś może rozbić tę czarną magię i ją wyjaśnić?
Paul
1
@Paul Do mojej odpowiedzi dodałem więcej szczegółów. Mam nadzieję, że to pomaga.
nicerobot
21

Dziwię się, że nie pojawiło się to czyste rozwiązanie bash:

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

Prawdopodobnie chcesz zresetować IFS do wartości, która była wcześniej lub unset IFSpóźniej!

użytkownik1338062
źródło
1
to nie jest czyste rozwiązanie, myślę, że działa w czystej powłoce (/ bin / sh)
kayn
5
+1 Możesz napisać w inny sposób, aby uniknąć konieczności IFSIFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
rozbrajania
2
Zależy to od rozszerzenia nazwy ścieżki! (więc jest zepsuty).
gniourf_gniourf
20

Opierając się na odpowiedzi jora (która nie działa dla mnie):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')
PEZ
źródło
12
Wyrażenia regularne to prawdziwa okazja, gdy masz coś skomplikowanego, a po prostu liczenie podkreślników, cutprawda.
Aleksandr Levchuk
12

Zgodnie z wymogami

Mam nazwę pliku z x liczbą znaków, a następnie pięciocyfrową sekwencję otoczoną pojedynczym podkreśleniem po obu stronach, a następnie innym zestawem x liczby znaków. Chcę wziąć pięciocyfrowy numer i umieścić go w zmiennej.

Znalazłem kilka grepsposobów, które mogą być przydatne:

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

albo lepiej

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

A potem ze -Poskładnią:

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

Lub jeśli chcesz dopasować dokładnie 5 znaków:

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

Wreszcie, aby zapisać go w zmiennej, wystarczy użyć var=$(command)składni.

fedorqui „SO przestań szkodzić”
źródło
2
Wierzę, że w dzisiejszych czasach nie ma potrzeby korzystania z egrep sama komenda ostrzega: Invocation as 'egrep' is deprecated; use 'grep -E' instead. Zredagowałem twoją odpowiedź.
Neurotransmitter
11

Jeśli skupimy się na koncepcji:
„Ciąg (jedna lub kilka) cyfr”

Możemy użyć kilku zewnętrznych narzędzi do wyodrębnienia liczb.
Możemy dość łatwo usunąć wszystkie inne postacie, sed lub tr:

name='someletters_12345_moreleters.ext'

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

Ale jeśli $ name zawiera kilka serii liczb, powyższe nie powiedzie się:

Jeśli „name = someletters_12345_moreleters_323_end.ext”, to:

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

Musimy używać regularnych wyrażeń (regex).
Aby wybrać tylko pierwszy przebieg (12345 nie 323) w sed i perl:

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

Ale równie dobrze moglibyśmy to zrobić bezpośrednio w bash (1) :

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

To pozwala nam wyodrębnić PIERWSZY ciąg cyfr o dowolnej długości
otoczony dowolnymi innymi tekstami / znakami.

Uwaga : regex=[^0-9]*([0-9]{5,5}).*$;dopasuje tylko dokładnie 5-cyfrowy przebieg. :-)

(1) : szybciej niż wywołanie zewnętrznego narzędzia dla każdego krótkiego tekstu. Nie szybciej niż całe przetwarzanie w sed lub awk dla dużych plików.


źródło
10

Bez żadnych podprocesów możesz:

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

Bardzo mały wariant tego działa również w ksh93.

Darron
źródło
9

Oto rozwiązanie przedrostka-sufiksu (podobne do rozwiązań podanych przez JB i Darrona), które pasuje do pierwszego bloku cyfr i nie zależy od otaczających podkreślników:

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
kodista
źródło
7

Uwielbiam sedzdolność radzenia sobie z grupami wyrażeń regularnych:

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

Nieco bardziej ogólnego rozwiązaniem byłoby nie do przyjęcia, że masz podkreślenia _oznakowanie rozpoczęcia swojej sekwencji cyfr, stąd na przykład odciągnięciu wszystkich nie-cyfr dostaniesz przed kolejności: 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.

Więcej informacji na ten temat, na wypadek gdybyś nie był zbyt pewny wyrażeń regularnych:

  • s jest dla _s_ubstitute
  • [0-9]+ dopasowuje 1+ cyfry
  • \1 linki do grupy nr 1 wyniku wyrażenia regularnego (grupa 0 to całe dopasowanie, grupa 1 to dopasowanie w nawiasach w tym przypadku)
  • p Flaga służy do _p_rinting

Wszystkie sekwencje specjalne \są dostępne, aby sedprzetwarzanie wyrażeń regularnych działało.

Campa
źródło
6

Moja odpowiedź będzie miała większą kontrolę nad tym, co chcesz ze swojego łańcucha. Oto kod, w jaki sposób możesz wyodrębnić 12345ciąg

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

Będzie to bardziej wydajne, jeśli chcesz wyodrębnić coś, co ma dowolne znaki abclub znaki specjalne, takie jak _lub -. Na przykład: jeśli Twój ciąg znaków jest taki i chcesz mieć wszystko, co jest po someletters_i przed _moreleters.ext:

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

Za pomocą mojego kodu możesz wspomnieć, czego dokładnie chcesz. Wyjaśnienie:

#*Usunie poprzedzający ciąg wraz z pasującym kluczem. Klucz, o którym wspominaliśmy _ %, usunie następujący ciąg, w tym pasujący klucz. Klucz, o którym wspominaliśmy, to „_more *”

Zrób kilka eksperymentów sam, a okaże się to interesujące.

Alex Raj Kaliamoorthy
źródło
6

Podany test.txt to plik zawierający „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
źródło
Jest to wyjątkowo specyficzne dla tego konkretnego wkładu. Jedynym ogólnym rozwiązaniem ogólnego pytania (które OP powinien był zadać) jest użycie wyrażenia regularnego .
Dan Dascalescu,
3

Ok, tutaj jest czysta zamiana parametrów z pustym ciągiem. Zastrzeżenie polega na tym, że zdefiniowałem somethters i moreletters jako tylko postacie. Jeśli są alfanumeryczne, nie zadziała tak jak jest.

filename=someletters_12345_moreletters.ext
substring=${filename//@(+([a-z])_|_+([a-z]).*)}
echo $substring
12345
Morbeo
źródło
2
niesamowite, ale wymaga przynajmniej bash v4
olibre
2

podobny do substr ('abcdefg', 2-1, 3) w php:

echo 'abcdefg'|tail -c +2|head -c 3
diyizm
źródło
Jest to wyjątkowo specyficzne dla tego wejścia. Jedynym ogólnym rozwiązaniem ogólnego pytania (które OP powinien był zadać) jest użycie wyrażenia regularnego .
Dan Dascalescu,
1

Istnieje również wbudowane polecenie bash „expr”:

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING
jor
źródło
4
exprnie jest wbudowany.
gniourf_gniourf
1
Nie jest to również konieczne w świetle =~operatora obsługiwanego przez [[.
chepner
1

Trochę późno, ale właśnie natknąłem się na ten problem i znalazłem:

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

Użyłem go, aby uzyskać milisekundową rozdzielczość w systemie osadzonym, który nie ma% N dla daty:

set `grep "now at" /proc/timer_list`
nano=$3
fraction=`expr $nano : '.*\(...\)......'`
$debug nano is $nano, fraction is $fraction
Russell
źródło
1

Rozwiązanie bash:

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

Spowoduje to zablokowanie zmiennej o nazwie x. Zmienna xmoże być zmieniona na zmienną _.

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

źródło
1

Niewygodny koniec, podobny do implementacji JS i Java. Usuń +1, jeśli tego nie chcesz.

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

Przykład:

    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

Więcej przykładowych połączeń:

    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

Zapraszamy.

mmm
źródło