Ekstrak substring di Bash

728

Diberikan nama file dalam formulir someletters_12345_moreleters.ext, saya ingin mengekstrak 5 digit dan memasukkannya ke dalam variabel.

Jadi untuk menekankan intinya, saya memiliki nama file dengan jumlah x karakter kemudian urutan lima digit dikelilingi oleh garis bawah tunggal di kedua sisi kemudian satu set x jumlah karakter. Saya ingin mengambil nomor 5 digit dan memasukkannya ke dalam variabel.

Saya sangat tertarik dengan sejumlah cara berbeda yang dapat dilakukan.

Berek Bryan
sumber
5
Jawaban JB jelas memenangkan suara - waktu untuk mengubah jawaban yang diterima?
Jeff
3
Sebagian besar jawaban tampaknya tidak menjawab pertanyaan Anda karena pertanyaannya ambigu. "Saya memiliki nama file dengan jumlah x karakter kemudian urutan lima digit yang dikelilingi oleh garis bawah tunggal di kedua sisi kemudian satu set x jumlah karakter" . Dengan definisi itu abc_12345_def_67890_ghi_defadalah input yang valid. Apa yang kamu inginkan terjadi? Mari kita asumsikan hanya ada satu urutan 5 digit. Anda masih memiliki abc_def_12345_ghi_jklatau 1234567_12345_1234567atau 12345d_12345_12345esebagai input yang valid berdasarkan definisi input Anda dan sebagian besar jawaban di bawah tidak akan menangani hal ini.
GM
2
Pertanyaan ini memiliki contoh input yang terlalu spesifik. Karena itu, ia mendapat banyak jawaban spesifik untuk kasus khusus ini (hanya digit, _pembatas yang sama , input yang berisi string target hanya sekali, dll.). Jawaban terbaik (paling umum dan tercepat) , setelah 10 tahun, hanya 7 naik, sedangkan jawaban terbatas lainnya memiliki ratusan. Membuat saya kehilangan kepercayaan pada pengembang 😞
Dan Dascalescu

Jawaban:

692

Gunakan potong :

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

Lebih umum:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING
FerranB
sumber
1
jawaban yang lebih umum adalah persis apa yang saya cari, terima kasih
Berek Bryan
71
Bendera -f mengambil indeks berbasis 1, bukan indeks berbasis 0 yang akan digunakan oleh seorang programmer.
Matthew G
2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $ (echo $ INPUT | cut -d'_ '-f 2) echo $ SUBSTRING
mani deepak
3
Anda harus menggunakan tanda kutip ganda di sekitar argumen echokecuali Anda tahu pasti bahwa variabel tidak dapat berisi spasi putih atau karakter metak shell. Lihat lebih lanjut stackoverflow.com/questions/10067266/…
tripleee
Angka '2' setelah '-f' adalah untuk memberi tahu shell untuk mengekstrak set kedua substring.
Sandun
1088

Jika x konstan, ekspansi parameter berikut melakukan ekstraksi substring:

b=${a:12:5}

di mana 12 adalah offset (berbasis nol) dan 5 adalah panjangnya

Jika garis bawah di sekitar digit adalah satu-satunya di input, Anda dapat menghapus awalan dan akhiran (masing-masing) dalam dua langkah:

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

Jika ada garis bawah lainnya, itu mungkin layak, meskipun lebih rumit. Jika ada yang tahu bagaimana melakukan kedua ekspansi dalam satu ekspresi, saya juga ingin tahu.

Kedua solusi yang disajikan adalah bash murni, tanpa melibatkan proses pemijahan, karenanya sangat cepat.

JB.
sumber
18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitutiondi GNU bash 4.2.45 saya.
JB.
2
@jonnyB, Beberapa waktu di masa lalu yang bekerja. Saya diberitahu oleh rekan kerja saya bahwa itu berhenti, dan mereka mengubahnya menjadi perintah sed atau sesuatu. Melihat itu dalam sejarah, saya menjalankannya dalam shskrip, yang mungkin putus-putus. Pada titik ini saya tidak bisa membuatnya bekerja lagi.
Spencer Rathbun
22
JB, Anda harus mengklarifikasi bahwa "12" adalah offset (berbasis nol) dan "5" adalah panjangnya. Juga, +1 untuk tautan @gontard yang menjabarkan semuanya!
Doktor J
1
Saat menjalankan ini di dalam skrip sebagai "sh run.sh", orang mungkin mendapatkan kesalahan Substitusi Buruk. Untuk menghindarinya, ubah izin untuk run.sh (chmod + x run.sh) lalu jalankan skrip sebagai "./run.sh"
Ankur
2
Parameter offset juga bisa negatif, BTW. Anda hanya harus berhati-hati untuk tidak menempelkannya ke usus besar, atau bash akan menafsirkannya sebagai :-substitusi "Gunakan Nilai Default". Jadi ${a: -12:5}menghasilkan 5 karakter 12 karakter dari akhir, dan ${a: -12:-5}7 karakter antara akhir-12 dan akhir-5.
JB.
97

Solusi generik di mana nomornya bisa di mana saja dalam nama file, menggunakan yang pertama dari urutan seperti itu:

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

Solusi lain untuk mengekstrak persis bagian dari variabel:

number=${filename:offset:length}

Jika nama file Anda selalu memiliki format, stuff_digits_...Anda dapat menggunakan awk:

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

Namun solusi lain untuk menghapus semuanya kecuali angka, gunakan

number=$(echo $filename | tr -cd '[[:digit:]]')
Johannes Schaub - litb
sumber
2
Bagaimana jika saya ingin mengekstrak digit / kata dari baris terakhir file.
A Sahra
93

coba saja gunakan cut -c startIndx-stopIndx

coklat.2179
sumber
2
Apakah ada sesuatu seperti 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
Masalahnya adalah inputnya dinamis karena saya juga menggunakan pipa untuk mendapatkannya jadi pada dasarnya. git log --oneline | head -1 | cut -c 9-(end -1)
Niklas
Ini dapat dilakukan dengan memotong jika dipecah menjadi dua bagian sebagai line=git log --oneline | kepala -1` && echo $ line | cut -c 9 - $ (($ {# line} -1)) `tetapi dalam kasus khusus ini, mungkin lebih baik menggunakan sed asgit log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
brown.2179
34

Jika seseorang menginginkan informasi yang lebih teliti, Anda juga dapat mencarinya di man bash seperti ini

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

Hasil:

$ {parameter: offset}
       $ {parameter: offset: length}
              Ekspansi Substring. Perluas hingga panjang karakter
              Parameter dimulai dari karakter yang ditentukan oleh offset. Jika
              panjang dihilangkan, memperluas ke substring dari parameter start -
              pada karakter yang ditentukan oleh offset. panjang dan offset adalah
              ekspresi aritmatika (lihat EVALUASI ARITHMETIC di bawah). Jika
              offset dievaluasi ke angka kurang dari nol, nilainya digunakan
              sebagai offset dari akhir nilai parameter. Hitung
              ekspresi yang dimulai dengan a - harus dipisahkan oleh spasi
              dari yang sebelumnya: dibedakan dari Use Default
              Nilai ekspansi. Jika panjang mengevaluasi ke angka kurang dari
              nol, dan parameter bukan @ dan bukan indeks atau asosiatif
              array, ini ditafsirkan sebagai offset dari akhir nilai
              parameter daripada sejumlah karakter, dan ekspansi
              sion adalah karakter antara dua offset. Jika parameter
              @, hasilnya adalah parameter posisi panjang mulai dari off -
              set. Jika parameter adalah nama array yang diindeks, subscripted oleh @ atau
              *, hasilnya adalah panjang anggota array yang dimulai dengan
              $ {parameter [offset]}. Offset negatif diambil relatif terhadap
              satu lebih besar dari indeks maksimum array yang ditentukan. Sub-
              ekspansi string yang diterapkan ke array asosiatif menghasilkan unde-
              hasil didenda. Perhatikan bahwa offset negatif harus dipisahkan
              dari usus besar dengan setidaknya satu ruang untuk menghindari kebingungan
              dengan: - ekspansi. Pengindeksan substring berbasis nol kecuali
              parameter posisi digunakan, dalam hal ini pengindeksan
              dimulai pada 1 secara default. Jika offset 0, dan posisional
              parameter digunakan, $ 0 diawali dengan daftar.
Jperelli
sumber
2
Peringatan yang sangat penting dengan nilai negatif seperti yang dinyatakan di atas: Ekspresi aritmatika dimulai dengan a - harus dipisahkan dengan spasi putih dari yang sebelumnya: harus dibedakan dari ekspansi Use Default Values. Jadi untuk mendapatkan empat karakter terakhir dari var:${var: -4}
sshow
26

Begini cara saya melakukannya:

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

Penjelasan:

Khusus bash:

Ekspresi Reguler (RE): _([[:digit:]]{5})_

  • _ adalah literal untuk menandai batas-batas pencocokan / jangkar untuk string yang cocok
  • () buat grup tangkap
  • [[:digit:]] adalah kelas karakter, saya pikir itu berbicara sendiri
  • {5} berarti tepat lima karakter sebelumnya, kelas (seperti dalam contoh ini), atau grup harus cocok

Dalam bahasa Inggris, Anda dapat menganggapnya berperilaku seperti ini: FNstring diulang karakter demi karakter sampai kami melihat titik _di mana grup tangkapan dibuka dan kami berusaha untuk mencocokkan lima digit. Jika pencocokan itu berhasil ke titik ini, grup tangkap menyimpan lima digit yang dilintasi. Jika karakter berikutnya adalah _, kondisinya berhasil, grup tangkap tersedia BASH_REMATCH, dan NUM=pernyataan berikutnya dapat dijalankan. Jika ada bagian dari pencocokan yang gagal, rincian yang disimpan dibuang dan karakter demi pemrosesan karakter berlanjut setelah _. mis. jika di FNmana _1 _12 _123 _1234 _12345_, akan ada empat awal yang salah sebelum menemukan kecocokan.

nicerobot
sumber
3
Ini adalah cara umum yang berfungsi bahkan jika Anda perlu mengekstrak lebih dari satu hal, seperti yang saya lakukan.
zebediah49
3
Ini memang jawaban yang paling umum, dan harus diterima. Ini berfungsi untuk ekspresi reguler, bukan hanya serangkaian karakter pada posisi tetap, atau antara pembatas yang sama (yang memungkinkan cut). Itu juga tidak bergantung pada mengeksekusi perintah eksternal.
Dan Dascalescu
1
Jawaban ini adalah undervotable secara kriminal.
chepner
Ini bagus! Saya mengadaptasi ini untuk menggunakan berbagai start / stop dilimeter (ganti _) dan angka panjang variabel (. Untuk {5}) untuk situasi saya. Bisakah seseorang menghancurkan ilmu hitam ini dan menjelaskannya?
Paul
1
@ Paul saya menambahkan rincian lebih lanjut untuk jawaban saya. Semoga itu bisa membantu.
nicerobot
21

Saya terkejut solusi bash murni ini tidak muncul:

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

Anda mungkin ingin mengatur ulang IFS ke nilai sebelumnya, atau unset IFSsesudahnya!

pengguna1338062
sumber
1
itu bukan solusi bash murni, saya pikir itu bekerja di shell murni (/ bin / sh)
kayn
5
+1 Anda dapat menulis ini dengan cara lain untuk menghindari IFSparameter yang belum disetel dan posisional:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
kojiro
2
Ini tunduk pada ekspansi pathname! (jadi rusak).
gniourf_gniourf
20

Membangun jawaban jor (yang tidak berhasil untuk saya):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')
PEZ
sumber
12
Ekspresi Reguler adalah masalah nyata ketika Anda memiliki sesuatu yang rumit dan hanya menghitung garis bawah bukan cut.
Aleksandr Levchuk
12

Mengikuti persyaratan

Saya memiliki nama file dengan jumlah x karakter kemudian urutan lima digit dikelilingi oleh garis bawah tunggal di kedua sisi kemudian satu set x jumlah karakter. Saya ingin mengambil nomor 5 digit dan memasukkannya ke dalam variabel.

Saya menemukan beberapa grepcara yang mungkin berguna:

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

atau lebih baik

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

Dan kemudian dengan -Posintaks:

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

Atau jika Anda ingin membuatnya pas dengan 5 karakter:

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

Akhirnya, untuk membuatnya disimpan dalam variabel itu hanya perlu menggunakan var=$(command)sintaks.

fedorqui 'SO berhenti merugikan'
sumber
2
Saya percaya saat ini tidak perlu menggunakan egrep, perintah itu sendiri memperingatkan Anda: Invocation as 'egrep' is deprecated; use 'grep -E' instead. Saya sudah mengedit jawaban Anda.
Neurotransmitter
11

Jika kita fokus pada konsep:
"Jumlah (satu atau beberapa) digit"

Kita bisa menggunakan beberapa alat eksternal untuk mengekstraksi angka.
Kami dapat dengan mudah menghapus semua karakter lain, baik sed atau tr:

name='someletters_12345_moreleters.ext'

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

Tetapi jika $ name berisi beberapa proses angka, hal di atas akan gagal:

Jika "name = someletters_12345_moreleters_323_end.ext", maka:

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

Kita perlu menggunakan expresi reguler (regex).
Untuk memilih hanya menjalankan pertama (12345 bukan 323) di sed dan perl:

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

Tapi kita bisa melakukannya langsung di bash (1) :

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

Ini memungkinkan kita untuk mengekstrak deretan angka pertama dengan panjang berapa pun yang
dikelilingi oleh teks / karakter lain.

Catatan : regex=[^0-9]*([0-9]{5,5}).*$;hanya akan cocok dengan berjalan 5 digit. :-)

(1) : lebih cepat daripada memanggil alat eksternal untuk setiap teks pendek. Tidak lebih cepat daripada melakukan semua pemrosesan dalam sed atau awk untuk file besar.


sumber
10

Tanpa sub-proses, Anda dapat:

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

Varian yang sangat kecil ini juga akan berfungsi di ksh93.

Darron
sumber
9

Berikut solusi akhiran-akhiran (mirip dengan solusi yang diberikan oleh JB dan Darron) yang cocok dengan blok angka pertama dan tidak bergantung pada garis bawah di sekitarnya:

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
ahli kode
sumber
7

Saya suka sedkemampuan untuk berurusan dengan grup regex:

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

Sebuah pilihan yang sedikit lebih umum akan tidak berasumsi bahwa Anda memiliki garis bawah _untuk menandai dimulainya urutan angka Anda, maka misalnya menanggalkan semua non-nomor yang Anda dapatkan sebelum urutan Anda: 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.

Lebih lanjut tentang ini, jika Anda tidak terlalu percaya diri dengan regexps:

  • s adalah untuk _s_ubstitute
  • [0-9]+ cocok dengan 1+ digit
  • \1 tautan ke grup n.1 dari output regex (grup 0 adalah seluruh kecocokan, grup 1 adalah kecocokan dalam kurung dalam kasus ini)
  • p bendera untuk _p_rinting

Semua lolos \ada untuk membuat sedpekerjaan pemrosesan regexp.

Campa
sumber
6

Jawaban saya akan memiliki kontrol lebih besar pada apa yang Anda inginkan dari string Anda. Berikut adalah kode tentang cara mengekstrak 12345string Anda

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

Ini akan lebih efisien jika Anda ingin mengekstraksi sesuatu yang memiliki karakter suka abcatau karakter khusus seperti _atau -. Misalnya: Jika string Anda seperti ini dan Anda menginginkan semua yang ada setelah someletters_dan sebelumnya _moreleters.ext:

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

Dengan kode saya, Anda dapat menyebutkan apa yang sebenarnya Anda inginkan. Penjelasan:

#*Ini akan menghapus string sebelumnya termasuk kunci yang cocok. Di sini kunci yang kami sebutkan adalah _ %Ini akan menghapus string berikut termasuk kunci yang cocok. Di sini kunci yang kami sebutkan adalah '_more *'

Lakukan beberapa percobaan sendiri dan Anda akan menemukan ini menarik.

Alex Raj Kaliamoorthy
sumber
6

Test.txt yang diberikan adalah file yang berisi "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
sumber
Ini sangat spesifik untuk input tertentu. Satu-satunya solusi umum untuk pertanyaan umum (yang seharusnya ditanyakan OP) adalah dengan menggunakan regexp .
Dan Dascalescu
3

Ok, ini dia Substitusi Parameter murni dengan string kosong. Peringatan adalah bahwa saya telah mendefinisikan someletters dan moreletters hanya sebagai karakter. Jika mereka alfanumerik, ini tidak akan berfungsi sebagaimana mestinya.

filename=someletters_12345_moreletters.ext
substring=${filename//@(+([a-z])_|_+([a-z]).*)}
echo $substring
12345
lebih buruk lagi
sumber
2
luar biasa tetapi membutuhkan setidaknya bash v4
olibre
2

mirip dengan substr ('abcdefg', 2-1, 3) di php:

echo 'abcdefg'|tail -c +2|head -c 3
diyism
sumber
Ini sangat spesifik untuk input itu. Satu-satunya solusi umum untuk pertanyaan umum (yang seharusnya ditanyakan OP) adalah dengan menggunakan regexp .
Dan Dascalescu
1

Ada juga perintah bash builtin 'expr':

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING
jor
sumber
4
exprbukan builtin.
gniourf_gniourf
1
Ini juga tidak perlu mengingat =~operator yang didukung oleh [[.
chepner
1

Sedikit terlambat, tetapi saya hanya menemukan masalah ini dan menemukan yang berikut:

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

Saya menggunakannya untuk mendapatkan resolusi milidetik pada sistem tertanam yang tidak memiliki% N untuk tanggal:

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

Solusi bash:

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

Ini akan mengalahkan variabel yang dipanggil x. Var xbisa diubah menjadi var _.

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

sumber
1

Akhir inklusif, mirip dengan implementasi JS dan Java. Hapus +1 jika Anda tidak menginginkan ini.

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

Contoh:

    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

Lebih banyak contoh panggilan:

    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

Sama sama.

mmm
sumber