Semantik untuk skrip Bash?

88

Lebih dari bahasa lain yang saya tahu, saya telah "mempelajari" Bash dengan Googling setiap kali saya membutuhkan sesuatu. Akibatnya, saya bisa menambal bersama skrip kecil yang tampaknya berfungsi. Namun, saya tidak begitu tahu apa yang terjadi, dan saya berharap untuk pengenalan yang lebih formal ke Bash sebagai bahasa pemrograman. Misalnya: Apa urutan evaluasinya? apa aturan pelingkupannya? Apa disiplin mengetik, misalnya apakah semuanya adalah string? Apa status program - apakah itu penugasan nilai-kunci dari string ke nama variabel; apakah ada lebih dari itu, misalnya tumpukan? Apakah ada tumpukan? Dan seterusnya.

Saya berpikir untuk berkonsultasi dengan manual GNU Bash untuk pemahaman semacam ini, tetapi sepertinya bukan itu yang saya inginkan; ini lebih merupakan daftar cucian gula sintaksis daripada penjelasan dari model semantik inti. Jutaan-dan-satu "tutorial bash" online hanya lebih buruk. Mungkin saya harus belajar dulu sh, dan memahami Bash sebagai gula sintaksis di atas ini? Saya tidak tahu apakah ini model yang akurat.

Ada saran?

EDIT: Saya telah diminta untuk memberikan contoh tentang apa yang idealnya saya cari. Contoh yang agak ekstrim dari apa yang saya anggap sebagai "semantik formal" adalah makalah tentang "esensi JavaScript" ini . Mungkin contoh yang sedikit kurang formal adalah laporan Haskell 2010 .

jameshfisher
sumber
3
Apakah Advanced Bash Scripting Guide salah satu dari "sejuta satu"?
choroba
2
Saya tidak yakin bahwa bash memiliki "model semantik inti" (yah, mungkin "hampir semuanya adalah string"); Saya pikir itu benar-benar gula sintaksis sampai ke bawah.
Gordon Davisson
4
Apa yang Anda sebut "daftar cucian gula sintaksis" sebenarnya adalah model ekspansi semantik - bagian yang sangat penting dari eksekusi. 90% bug dan kebingungan terjadi karena tidak memahami model perluasan.
orang lain itu
4
Saya dapat melihat mengapa seseorang mungkin berpikir ini adalah pertanyaan yang luas jika Anda membacanya seperti bagaimana cara menulis skrip shell ? Tetapi pertanyaan sebenarnya adalah apa semantik formal dan dasar untuk bahasa shell dan bash secara khusus? , dan ini adalah pertanyaan yang bagus dengan satu jawaban yang koheren. Memberi suara untuk dibuka kembali.
kojiro
1
Saya belajar sedikit di linuxcommand.org dan bahkan ada pdf gratis dari buku yang lebih mendalam di baris perintah dan menulis skrip shell
samrap

Jawaban:

107

Shell adalah antarmuka untuk sistem operasi. Ini biasanya merupakan bahasa pemrograman yang kurang lebih kuat dalam dirinya sendiri, tetapi dengan fitur yang dirancang untuk membuatnya mudah berinteraksi secara khusus dengan sistem operasi dan sistem file. Semantik POSIX shell (selanjutnya disebut hanya sebagai "shell") adalah sedikit mutt, menggabungkan beberapa fitur LISP (ekspresi-s memiliki banyak kesamaan dengan pemisahan kata shell ) dan C (banyak dari sintaks aritmatika shell semantik berasal dari C).

Akar lain dari sintaks shell berasal dari asuhannya sebagai campuran utilitas UNIX individu. Sebagian besar dari apa yang sering dibangun di dalam shell sebenarnya dapat diimplementasikan sebagai perintah eksternal. Itu melempar banyak orang baru shell untuk satu lingkaran ketika mereka menyadari bahwa /bin/[ada di banyak sistem.

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t

wat?

Ini jauh lebih masuk akal jika Anda melihat bagaimana shell diimplementasikan. Inilah implementasi yang saya lakukan sebagai latihan. Ini dengan Python, tapi saya harap itu bukan hangup untuk siapa pun. Ini tidak terlalu kuat, tetapi instruktif:

#!/usr/bin/env python

from __future__ import print_function
import os, sys

'''Hacky barebones shell.'''

try:
  input=raw_input
except NameError:
  pass

def main():
  while True:
    cmd = input('prompt> ')
    args = cmd.split()
    if not args:
      continue
    cpid = os.fork()
    if cpid == 0:
      # We're in a child process
      os.execl(args[0], *args)
    else:
      os.waitpid(cpid, 0)

if __name__ == '__main__':
  main()

Saya harap penjelasan di atas menjelaskan bahwa model eksekusi shell cukup banyak:

1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.

Ekspansi, resolusi perintah, eksekusi. Semua semantik shell terikat dalam salah satu dari tiga hal ini, meskipun jauh lebih kaya daripada implementasi yang saya tulis di atas.

Tidak semua perintah fork. Faktanya, ada beberapa perintah yang tidak masuk akal diimplementasikan sebagai eksternal (seperti yang harus mereka lakukan fork), tetapi bahkan perintah tersebut sering tersedia sebagai perintah eksternal untuk kepatuhan POSIX yang ketat.

Bash membangun di atas dasar ini dengan menambahkan fitur dan kata kunci baru untuk meningkatkan shell POSIX. Ini hampir kompatibel dengan sh, dan bash ada di mana-mana sehingga beberapa penulis skrip pergi bertahun-tahun tanpa menyadari bahwa skrip mungkin tidak benar-benar berfungsi pada sistem ketat POSIXly. (Saya juga bertanya-tanya bagaimana orang bisa begitu peduli tentang semantik dan gaya satu bahasa pemrograman, dan begitu sedikit untuk semantik dan gaya shell, tapi saya menyimpang.)

Urutan evaluasi

Ini adalah sedikit pertanyaan jebakan: Bash menafsirkan ekspresi dalam sintaks utamanya dari kiri ke kanan, tetapi dalam sintaks aritmatika, ia mengikuti diutamakan C. Ekspresi berbeda dari ekspansi . Dari EXPANSIONbagian manual bash:

Urutan ekspansi adalah: ekspansi brace; ekspansi tilde, ekspansi parameter dan variabel, ekspansi aritmatika, dan substitusi perintah (dilakukan dengan cara kiri-ke-kanan); pemisahan kata; dan perluasan nama jalur.

Jika Anda memahami pemisahan kata, perluasan nama jalur, dan perluasan parameter, Anda sedang dalam perjalanan untuk memahami sebagian besar dari apa yang dilakukan bash. Perhatikan bahwa perluasan nama jalur yang muncul setelah pemisahan kata sangat penting, karena ini memastikan bahwa file dengan spasi dalam namanya masih dapat dicocokkan oleh glob. Inilah sebabnya mengapa penggunaan ekspansi glob lebih baik daripada perintah parsing secara umum.

Cakupan

Lingkup fungsi

Sama seperti ECMAscript lama, shell memiliki cakupan dinamis kecuali Anda secara eksplisit mendeklarasikan nama dalam suatu fungsi.

$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ …

Lingkungan dan proses "ruang lingkup"

Subkulit mewarisi variabel dari cangkang induknya, tetapi jenis proses lain tidak mewarisi nama yang tidak diekspor.

$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'

$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y' # another way to transiently export a name
123

Anda dapat menggabungkan aturan pelingkupan ini:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$

Disiplin mengetik

Um, tipe. Ya. Bash benar-benar tidak memiliki tipe, dan semuanya berkembang menjadi string (atau mungkin sebuah kata akan lebih sesuai.) Tapi mari kita periksa berbagai tipe ekspansi.

String

Hampir semua hal bisa diperlakukan sebagai string. Bareword dalam bash adalah string yang artinya bergantung sepenuhnya pada ekspansi yang diterapkan padanya.

Tidak ada ekspansi

Mungkin bermanfaat untuk menunjukkan bahwa kata kosong sebenarnya hanyalah sebuah kata, dan kutipan itu tidak mengubah apa pun tentang itu.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Ekspansi substring
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Untuk lebih lanjut tentang ekspansi, baca Parameter Expansionbagian manual. Ini cukup kuat.

Bilangan bulat dan ekspresi aritmatika

Anda dapat mengilhami nama dengan atribut integer untuk memberi tahu shell untuk memperlakukan sisi kanan ekspresi tugas sebagai aritmatika. Kemudian, ketika parameter diperluas, itu akan dievaluasi sebagai matematika integer sebelum meluas ke… string.

$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
2

Array

Argumen dan Parameter Posisi

Sebelum berbicara tentang array, mungkin ada baiknya membahas parameter posisi. Argumen untuk script shell dapat diakses menggunakan parameter bernomor, $1, $2, $3, dll Anda dapat mengakses semua parameter ini sekaligus menggunakan "$@", yang ekspansi memiliki banyak kesamaan dengan array. Anda dapat mengatur dan mengubah parameter posisi menggunakan setatau shiftbuiltins, atau cukup dengan memanggil fungsi shell atau shell dengan parameter berikut:

$ bash -c 'for ((i=1;i<=$#;i++)); do
>   printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
>   local i
>   for ((i=1;i<=$#;i++)); do
>     printf '$%d => %s\n' "$i" "${@:i:1}"
>   done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
>   shift 3
>   showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy

Manual bash terkadang juga mengacu pada $0parameter posisi. Saya menemukan ini membingungkan, karena tidak memasukkannya dalam hitungan argumen $#, tetapi ini adalah parameter bernomor, jadi meh. $0adalah nama shell atau skrip shell saat ini.

Array

Sintaks array dimodelkan setelah parameter posisi, jadi sebaiknya anggap array sebagai jenis "parameter posisi eksternal", jika Anda suka. Array dapat dideklarasikan menggunakan pendekatan berikut:

$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )

Anda dapat mengakses elemen array dengan indeks:

$ echo "${foo[1]}"
element1

Anda dapat mengiris array:

$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"

Jika Anda memperlakukan sebuah array sebagai parameter normal, Anda akan mendapatkan indeks ke nol.

$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ …

Jika Anda menggunakan tanda kutip atau garis miring terbalik untuk mencegah pemisahan kata, array akan mempertahankan pemisahan kata yang ditentukan:

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

Perbedaan utama antara array dan parameter posisi adalah:

  1. Parameter posisi tidak jarang. Jika $12disetel, Anda bisa yakin $11disetel juga. (Ini bisa disetel ke string kosong, tapi $#tidak lebih kecil dari 12.) Jika "${arr[12]}"disetel, tidak ada jaminan yang "${arr[11]}"disetel, dan panjang larik bisa sekecil 1.
  2. Elemen ke-nol dari sebuah larik jelas merupakan elemen ke-nol dari larik tersebut. Dalam parameter posisi, elemen ke nol bukanlah argumen pertama , tetapi nama skrip shell atau shell.
  3. Untuk shiftsebuah array, Anda harus memotong dan menetapkannya kembali, seperti arr=( "${arr[@]:1}" ). Anda juga bisa melakukannya unset arr[0], tapi itu akan membuat elemen pertama di indeks 1.
  4. Array dapat dibagikan secara implisit di antara fungsi shell sebagai global, tetapi Anda harus secara eksplisit meneruskan parameter posisi ke fungsi shell agar dapat melihatnya.

Seringkali nyaman untuk menggunakan perluasan nama jalur untuk membuat array nama file:

$ dirs=( */ )

Perintah

Perintah adalah kuncinya, tetapi juga dibahas secara lebih mendalam daripada yang saya bisa dengan manual. Bacalah SHELL GRAMMARbagiannya. Jenis perintah yang berbeda adalah:

  1. Perintah Sederhana (mis. $ startx)
  2. Saluran pipa (misalnya $ yes | make config) (lol)
  3. Daftar (mis. $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Perintah Majemuk (mis. $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprocesses (Kompleks, tidak ada contoh)
  6. Fungsi (Perintah gabungan bernama yang dapat diperlakukan sebagai perintah sederhana)

Model Eksekusi

Model eksekusi tentu saja melibatkan heap dan stack. Ini endemik untuk semua program UNIX. Bash juga memiliki stack panggilan untuk fungsi shell, terlihat melalui penggunaan callerbuiltin yang bertingkat .

Referensi:

  1. The SHELL GRAMMARbagian bash manual
  2. The XCU Shell Command Bahasa dokumentasi
  3. The Bash Panduan di wiki Greycat ini.
  4. Pemrograman Lanjutan di Lingkungan UNIX

Tolong beri komentar jika Anda ingin saya memperluas lebih jauh ke arah tertentu.

kojiro
sumber
16
+1: Penjelasan bagus. Hargai waktu yang dihabiskan untuk menulis ini dengan contoh.
jaypal singh
1 untuk yes | make config;-) Tapi serius, artikel yang sangat bagus.
Trauma Digital
baru saja mulai membaca ini .. bagus. akan meninggalkan beberapa komentar. 1) kejutan yang lebih besar muncul ketika Anda melihatnya /bin/[dan /bin/testsering kali merupakan penerapan yang sama 2) "Asumsikan kata pertama adalah sebuah perintah." - Berharap ketika Anda melakukan tugas ...
Karoly Horvath
@KarolyHorv Ya, saya sengaja mengecualikan tugas dari demo shell saya karena variabel adalah kekacauan yang rumit. Demo shell itu tidak ditulis dengan jawaban ini - ini ditulis jauh lebih awal. Saya kira saya bisa membuatnya execledan menginterpolasi kata pertama ke dalam lingkungan, tetapi itu masih membuatnya sedikit lebih rumit.
kojiro
@kojiro: nah itu hanya akan memperumitnya, itu pasti bukan niat saya! tetapi tugas bekerja sedikit berbeda (x), dan IMHO Anda harus menyebutkannya di suatu tempat dalam teks. (x): dan sumber kebingungan ... Saya bahkan tidak bisa menghitung lagi berapa kali saya melihat orang mengeluh karena a = 1tidak bekerja).
Karoly Horvath
5

Jawaban atas pertanyaan Anda "Apa disiplin mengetik, misalnya apakah semuanya string" Variabel Bash adalah string karakter. Tapi, Bash mengizinkan operasi aritmatika dan perbandingan pada variabel ketika variabel adalah bilangan bulat. Pengecualian untuk variabel aturan Bash adalah string karakter ketika variabel tersebut diketik atau dideklarasikan sebaliknya

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

Deklarasikan arti opsi:

  • -a Variabel adalah array.
  • -f Gunakan nama fungsi saja.
  • -i Variabel akan diperlakukan sebagai integer; evaluasi aritmatika dilakukan ketika variabel diberi nilai.
  • -p Menampilkan atribut dan nilai dari setiap variabel. Saat -p digunakan, opsi tambahan diabaikan.
  • -r Membuat variabel hanya-baca. Variabel ini kemudian tidak dapat diberi nilai dengan pernyataan tugas berikutnya, juga tidak dapat tidak disetel.
  • -t Berikan atribut jejak untuk setiap variabel.
  • -x Tandai setiap variabel untuk diekspor ke perintah berikutnya melalui lingkungan.
Keith Reynolds
sumber
1

Halaman manual bash memiliki lebih banyak info daripada kebanyakan halaman manual, dan mencakup beberapa dari apa yang Anda minta. Asumsi saya setelah lebih dari satu dekade scripting bash adalah, karena 'sejarahnya sebagai perpanjangan dari sh, ia memiliki beberapa sintaks yang funky (untuk menjaga kompatibilitas ke belakang dengan sh).

FWIW, pengalaman saya seperti pengalaman Anda; walaupun berbagai buku (misalnya, O'Reilly "Learning the Bash Shell" dan sejenisnya) membantu dengan sintaks, ada banyak cara aneh untuk memecahkan berbagai masalah, dan beberapa di antaranya tidak ada dalam buku dan harus dicari di Google.

philwalk
sumber