Trích xuất chuỗi con trong Bash


728

Cho một tên tệp trong biểu mẫu someletters_12345_moreleters.ext, tôi muốn trích xuất 5 chữ số và đặt chúng vào một biến.

Vì vậy, để nhấn mạnh điểm, tôi có một tên tệp với x số ký tự sau đó một chuỗi năm chữ số được bao quanh bởi một dấu gạch dưới ở hai bên sau đó là một bộ x số ký tự khác. Tôi muốn lấy số có 5 chữ số và đặt nó vào một biến.

Tôi rất quan tâm đến số lượng các cách khác nhau mà điều này có thể được thực hiện.


5
Câu trả lời của JB rõ ràng là giành được phiếu bầu - thời gian để thay đổi câu trả lời được chấp nhận?
Jeff

3
Hầu hết các câu trả lời dường như không trả lời câu hỏi của bạn vì câu hỏi không rõ ràng. "Tôi có một tên tệp với x số ký tự, sau đó một chuỗi năm chữ số được bao quanh bởi một dấu gạch dưới duy nhất ở hai bên sau đó là một bộ x số ký tự khác" . Theo định nghĩa đó abc_12345_def_67890_ghi_deflà một đầu vào hợp lệ. Bạn muốn điều gì xảy ra? Giả sử chỉ có một chuỗi 5 chữ số. Bạn vẫn có abc_def_12345_ghi_jklhoặc 1234567_12345_1234567hoặc 12345d_12345_12345elà đầu vào hợp lệ dựa trên định nghĩa đầu vào của bạn và hầu hết các câu trả lời dưới đây sẽ không xử lý việc này.
gman

2
Câu hỏi này có một ví dụ đầu vào quá cụ thể. Do đó, nó đã nhận được rất nhiều câu trả lời cụ thể cho trường hợp cụ thể này (chỉ các chữ số, cùng một _dấu phân cách, đầu vào chỉ chứa chuỗi đích một lần, v.v.). Các câu trả lời tốt nhất (generic nhất và nhanh nhất) có, sau 10 năm, chỉ 7 upvotes, trong khi câu trả lời hạn chế khác có hàng trăm. Khiến tôi mất niềm tin vào các nhà phát triển
Dan Dascalescu

Câu trả lời:


692

Sử dụng cắt :

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

Chung hơn:

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

1
câu trả lời chung chung hơn chính xác là những gì tôi đang tìm kiếm, cảm ơn
Berek Bryan

71
Cờ -f lấy các chỉ mục dựa trên 1, thay vì các chỉ mục dựa trên 0 mà lập trình viên sẽ sử dụng.
Matthew G

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

3
Bạn nên sử dụng đúng dấu ngoặc kép xung quanh các đối số để echotrừ khi bạn biết chắc chắn rằng các biến không thể chứa khoảng trắng không đều hoặc ký tự đại diện hệ vỏ. Xem thêm stackoverflow.com/questions/10067266/
Mạnh

Số '2' sau '-f' là để báo cho shell trích xuất bộ chuỗi con thứ 2.
Sandun

1088

Nếu x là hằng số, việc mở rộng tham số sau đây thực hiện trích xuất chuỗi con:

b=${a:12:5}

trong đó 12 là phần bù (dựa trên zero) và 5 là độ dài

Nếu dấu gạch dưới xung quanh các chữ số là số duy nhất trong đầu vào, bạn có thể loại bỏ tiền tố và hậu tố (tương ứng) theo hai bước:

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

Nếu có các dấu gạch dưới khác, dù sao thì nó cũng có thể khả thi, mặc dù khó khăn hơn. Nếu bất cứ ai biết cách thực hiện cả hai lần mở rộng trong một biểu thức, tôi cũng muốn biết.

Cả hai giải pháp được trình bày là bash thuần túy, không có quá trình sinh sản liên quan, do đó rất nhanh.


18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutiontrên bash GNU 4.2,45 của tôi.
JB.

2
@jonnyB, Một thời gian trong quá khứ mà làm việc. Tôi được các đồng nghiệp của tôi nói rằng nó đã dừng lại và họ đã thay đổi nó thành một lệnh sed hoặc một cái gì đó. Nhìn vào nó trong lịch sử, tôi đã chạy nó trong một shkịch bản, có lẽ là dấu gạch ngang. Tại thời điểm này tôi không thể làm cho nó hoạt động được nữa.
Spencer Rathbun

22
JB, bạn nên làm rõ rằng "12" là phần bù (dựa trên zero) và "5" là độ dài. Ngoài ra, +1 cho liên kết của @gontard sẽ đưa ra tất cả!
Doktor J

1
Trong khi chạy tập lệnh này bên trong tập lệnh là "sh run.sh", người ta có thể gặp lỗi Thay thế xấu. Để tránh điều đó, hãy thay đổi quyền cho run.sh (chmod + x run.sh) và sau đó chạy tập lệnh dưới dạng "./run.sh"
Ankur

2
Thông số bù có thể âm quá, BTW. Bạn chỉ cần lưu ý không dán nó vào dấu hai chấm, hoặc bash sẽ diễn giải nó như là một :-thay thế sử dụng giá trị mặc định. Vì vậy, ${a: -12:5}mang lại 5 ký tự 12 ký tự từ cuối và ${a: -12:-5}7 ký tự giữa cuối 12 và cuối 5.
JB.

97

Giải pháp chung trong đó số có thể ở bất kỳ đâu trong tên tệp, sử dụng chuỗi đầu tiên như vậy:

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

Một giải pháp khác để trích xuất chính xác một phần của biến:

number=${filename:offset:length}

Nếu tên tệp của bạn luôn có định dạng, stuff_digits_...bạn có thể sử dụng awk:

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

Một giải pháp khác để loại bỏ mọi thứ trừ chữ số, sử dụng

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

2
Điều gì xảy ra nếu tôi muốn trích xuất chữ số / từ từ dòng cuối cùng của tệp.
Một Sahra

93

chỉ cần cố gắng sử dụng cut -c startIndx-stopIndx


2
Có cái gì đó giống như start Index-last Index - 1 không?
Niklas

1
@Niklas Trong bash, startIndx-$((lastIndx-1))
proly

3
start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1))
màu nâu.2179

1
Vấn đề là đầu vào là động vì tôi cũng sử dụng đường ống để có được nó nên về cơ bản. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas

Điều này có thể được thực hiện với cắt nếu chia thành hai phần là line=git log --oneline | đầu -1` && echo $ line | cắt -c 9 - $ (($ {# line} -1)) `nhưng trong trường hợp cụ thể này, có thể tốt hơn để sử dụng sed nhưgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179

34

Trong trường hợp ai đó muốn thông tin nghiêm ngặt hơn, bạn cũng có thể tìm kiếm nó trong man bash như thế này

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

Kết quả:

$ {tham số: offset}
       $ {tham số: offset: length}
              Mở rộng chuỗi con. Mở rộng tối đa các ký tự của
              tham số bắt đầu từ ký tự được chỉ định bởi offset. Nếu
              độ dài bị bỏ qua, mở rộng đến chuỗi con của tham số start
              ing tại ký tự được chỉ định bởi offset. chiều dài và bù đắp là
              biểu thức số học (xem ĐÁNH GIÁ ARITHMETIC bên dưới). Nếu
              offset ước tính thành một số nhỏ hơn 0, giá trị được sử dụng
              như một phần bù từ cuối giá trị của tham số. Môn số học
              các biểu thức bắt đầu bằng a - phải được phân tách bằng khoảng trắng
              từ trước: được phân biệt với Mặc định sử dụng
              Mở rộng giá trị. Nếu độ dài ước tính thành một số nhỏ hơn
              không, và tham số không phải là @ và không phải là chỉ mục hoặc liên kết
              mảng, nó được hiểu là phần bù từ cuối giá trị
              của tham số chứ không phải là một số ký tự và mở rộng
              Sion là các nhân vật giữa hai độ lệch. Nếu tham số là
              @, kết quả là độ dài tham số vị trí bắt đầu từ off‐
              bộ. Nếu tham số là tên mảng được lập chỉ mục được đăng ký bởi @ hoặc
              *, kết quả là các thành viên có độ dài của mảng bắt đầu bằng
              $ {tham số [offset]}. Một phần bù âm được lấy tương đối so với
              một lớn hơn chỉ số tối đa của mảng được chỉ định. Sub
              mở rộng chuỗi được áp dụng cho một mảng kết hợp tạo ra unde‐
              kết quả bị phạt. Lưu ý rằng phần bù âm phải được tách riêng
              từ đại tràng bằng ít nhất một không gian để tránh bị nhầm lẫn
              với: - mở rộng. Lập chỉ mục chuỗi con là không dựa trên trừ khi
              các tham số vị trí được sử dụng, trong trường hợp đó là lập chỉ mục
              bắt đầu từ 1 theo mặc định. Nếu độ lệch bằng 0 và vị trí
              các tham số được sử dụng, $ 0 được thêm tiền tố vào danh sách.

2
Một cảnh báo rất quan trọng với các giá trị âm như đã nêu ở trên: Các biểu thức số học bắt đầu bằng một - phải được phân tách bằng khoảng trắng từ trước: để được phân biệt với mở rộng Sử dụng Giá trị Mặc định. Vì vậy, để có được bốn ký tự cuối cùng của một var:${var: -4}
sshow 27/07/17

26

Đây là cách tôi sẽ làm điều đó:

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

Giải trình:

Bash-cụ thể:

Biểu thức chính quy (RE): _([[:digit:]]{5})_

  • _ là những chữ để phân định ranh giới / neo khớp cho chuỗi được khớp
  • () tạo một nhóm chụp
  • [[:digit:]] là một lớp nhân vật, tôi nghĩ nó nói cho chính nó
  • {5} có nghĩa là chính xác năm trong số các ký tự, lớp trước (như trong ví dụ này) hoặc nhóm phải khớp

Trong tiếng Anh, bạn có thể nghĩ về nó hoạt động như thế này: FNchuỗi được lặp theo từng ký tự cho đến khi chúng ta thấy một _điểm mà nhóm chụp được mở và chúng ta cố gắng khớp năm chữ số. Nếu kết hợp đó thành công đến thời điểm này, nhóm chụp sẽ lưu năm chữ số đi qua. Nếu ký tự tiếp theo là một _, điều kiện thành công, nhóm chụp được tạo sẵn BASH_REMATCHNUM=câu lệnh tiếp theo có thể thực thi. Nếu bất kỳ phần nào của kết hợp không thành công, các chi tiết đã lưu sẽ được xử lý và ký tự bằng cách xử lý ký tự tiếp tục sau _. ví dụ: nếu FNở đâu _1 _12 _123 _1234 _12345_, sẽ có bốn bắt đầu sai trước khi tìm thấy kết quả khớp.


3
Đây là một cách chung hoạt động ngay cả khi bạn cần trích xuất nhiều hơn một điều, như tôi đã làm.
zebediah49

3
Đây thực sự là câu trả lời chung chung nhất và nên được chấp nhận. Nó hoạt động cho một biểu thức chính quy, không chỉ là một chuỗi các ký tự ở một vị trí cố định hoặc giữa cùng một dấu phân cách (cho phép cut). Nó cũng không dựa vào việc thực hiện một lệnh bên ngoài.
Dan Dascalescu

1
Câu trả lời này không được đánh giá cao.
chepner

Điều đó thật tuyệt! Tôi đã điều chỉnh điều này để sử dụng các số đo độ khởi động / dừng khác nhau (thay thế số _) và số có độ dài thay đổi (. Cho {5}) cho tình huống của tôi. Ai đó có thể phá vỡ ma thuật đen này và giải thích nó?
Paul

1
@Paul Tôi đã thêm chi tiết vào câu trả lời của tôi. Mong rằng sẽ giúp.
nicerobot

21

Tôi ngạc nhiên khi giải pháp bash tinh khiết này không xuất hiện:

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

Bạn có thể muốn đặt lại IFS về giá trị trước unset IFSđó hoặc sau đó!


1
Đó không phải là giải pháp bash thuần túy, tôi nghĩ rằng nó hoạt động trong vỏ nguyên chất (/ bin / sh)
kayn

5
+1 Bạn có thể viết theo cách khác để tránh phải bỏ đặt IFSvà tham số vị trí:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
kojiro

2
Đây là chủ đề để mở rộng tên đường dẫn! (vì vậy nó bị hỏng).
gniourf_gniourf

20

Dựa trên câu trả lời của jor (không phù hợp với tôi):

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

12
Biểu thức chính quy là thỏa thuận thực sự khi bạn có một cái gì đó phức tạp và chỉ đơn giản là đếm phần dưới sẽ không cut.
Alexanderr Levchuk

12

Theo yêu cầu

Tôi có một tên tệp với x số ký tự, sau đó một chuỗi năm chữ số được bao quanh bởi một dấu gạch dưới duy nhất ở hai bên sau đó là một bộ x số ký tự khác. Tôi muốn lấy số có 5 chữ số và đặt nó vào một biến.

Tôi tìm thấy một số grepcách có thể hữu ích:

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

hoặc tốt hơn

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

Và sau đó với -Pocú pháp:

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

Hoặc nếu bạn muốn làm cho nó phù hợp với chính xác 5 ký tự:

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

Cuối cùng, để làm cho nó được lưu trữ trong một biến, nó chỉ cần sử dụng var=$(command)cú pháp.


2
Tôi tin rằng ngày nay không cần sử dụng egrep, chính lệnh này cảnh báo bạn : Invocation as 'egrep' is deprecated; use 'grep -E' instead. Tôi đã chỉnh sửa câu trả lời của bạn.
Chất dẫn truyền thần kinh

11

Nếu chúng ta tập trung vào khái niệm:
"Chạy một (một hoặc vài) chữ số"

Chúng ta có thể sử dụng một số công cụ bên ngoài để trích xuất các con số.
Chúng tôi có thể dễ dàng xóa tất cả các nhân vật khác, dù là sed hay tr:

name='someletters_12345_moreleters.ext'

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

Nhưng nếu $ name chứa một vài lần chạy số, điều trên sẽ thất bại:

Nếu "name = someletters_12345_moreleter_323_end.ext", thì:

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

Chúng ta cần sử dụng expresions thường xuyên (regex).
Để chỉ chọn lần chạy đầu tiên (12345 chứ không phải 323) trong sed và perl:

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

Nhưng chúng ta cũng có thể làm điều đó trực tiếp trong bash (1) :

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

Điều này cho phép chúng tôi trích xuất chuỗi chữ số đầu tiên có độ dài bất kỳ được
bao quanh bởi bất kỳ văn bản / ký tự nào khác.

Lưu ý : regex=[^0-9]*([0-9]{5,5}).*$;sẽ chỉ khớp chính xác 5 chữ số chạy. :-)

(1) : nhanh hơn gọi một công cụ bên ngoài cho mỗi văn bản ngắn. Không nhanh hơn thực hiện tất cả xử lý bên trong sed hoặc awk cho các tệp lớn.


10

Nếu không có bất kỳ quy trình phụ nào bạn có thể:

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

Một biến thể rất nhỏ của điều này cũng sẽ hoạt động trong ksh93.


9

Đây là một giải pháp tiền tố hậu tố (tương tự như các giải pháp được đưa ra bởi JB và Darron) khớp với khối chữ số đầu tiên và không phụ thuộc vào các dấu gạch dưới xung quanh:

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

Tôi thích sedkhả năng đối phó với các nhóm regex:

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

Tùy chọn tổng quát hơn một chút sẽ không giả sử rằng bạn có dấu gạch dưới _đánh dấu bắt đầu chuỗi chữ số của bạn, do đó, ví dụ như tước bỏ tất cả các số không bạn nhận được trước chuỗi : 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.

Thêm về điều này, trong trường hợp bạn không quá tự tin với biểu thức chính quy:

  • s dành cho _s_ubstolarship
  • [0-9]+ khớp với hơn 1 chữ số
  • \1 liên kết đến nhóm n.1 của đầu ra regex (nhóm 0 là toàn bộ khớp, nhóm 1 là khớp trong ngoặc đơn trong trường hợp này)
  • p cờ dành cho _p_rinting

Tất cả các lối thoát \đều ở đó để thực hiện sedcông việc xử lý regrec.


6

Câu trả lời của tôi sẽ có nhiều quyền kiểm soát hơn đối với những gì bạn muốn từ chuỗi của bạn. Đây là mã về cách bạn có thể trích xuất 12345ra khỏi chuỗi của mình

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

Điều này sẽ hiệu quả hơn nếu bạn muốn trích xuất một cái gì đó có bất kỳ ký tự như abchoặc bất kỳ ký tự đặc biệt nào thích _hoặc -. Ví dụ: Nếu chuỗi của bạn giống như thế này và bạn muốn mọi thứ nằm sau someletters_và trước _moreleters.ext:

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

Với mã của tôi, bạn có thể đề cập chính xác những gì bạn muốn. Giải trình:

#*Nó sẽ loại bỏ chuỗi trước bao gồm cả khóa khớp. Ở đây khóa chúng tôi đã đề cập là _ %Nó sẽ loại bỏ chuỗi sau bao gồm cả khóa khớp. Ở đây, khóa chúng tôi đã đề cập là '_more *'

Tự làm một số thí nghiệm và bạn sẽ thấy điều này thú vị.


6

Đã cho test.txt là một tệp chứa "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

Điều này là cực kỳ cụ thể cho đầu vào cụ thể đó. Giải pháp chung duy nhất cho câu hỏi chung (mà OP nên hỏi) là sử dụng biểu thức chính quy .
Dan Dascalescu

3

Ok, ở đây đi thay thế Parameter thuần túy với một chuỗi rỗng. Hãy cẩn thận là tôi đã định nghĩa somelettersmoreletters chỉ là các nhân vật. Nếu chúng là chữ và số, điều này sẽ không hoạt động như vậy.

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

2
tuyệt vời nhưng yêu cầu ít nhất bash v4
olibre


1

Ngoài ra còn có lệnh bash dựng sẵn 'expr':

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

4
exprkhông phải là nội dung.
gniourf_gniourf

1
Nó cũng không cần thiết trong ánh sáng của =~nhà điều hành được hỗ trợ bởi [[.
chepner

1

Hơi muộn một chút, nhưng tôi chỉ chạy qua vấn đề này và thấy như sau:

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

Tôi đã sử dụng nó để có được độ phân giải mili giây trên một hệ thống nhúng không có% N cho ngày:

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

1

Một giải pháp bash:

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

Điều này sẽ ghi đè một biến được gọi là x. Các var xcó thể được thay đổi thành var _.

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

1

Kết thúc bằng mực, tương tự như triển khai JS và Java. Xóa +1 nếu bạn không muốn điều này.

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

Thí dụ:

    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

Các ví dụ khác gọi:

    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

Không có gì.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.