Ngữ nghĩa cho các tập lệnh Bash?


87

Hơn bất kỳ ngôn ngữ nào khác mà tôi biết, tôi đã "học" Bash bằng Googling mỗi khi tôi cần một số việc nhỏ. Do đó, tôi có thể chắp vá các tập lệnh nhỏ có vẻ hoạt động với nhau. Tuy nhiên, tôi không thực sự biết chuyện gì đang xảy ra và tôi đã hy vọng có một bài giới thiệu chính thức hơn về Bash như một ngôn ngữ lập trình. Ví dụ: Thứ tự đánh giá là gì? các quy tắc xác định phạm vi là gì? Kỷ luật gõ là gì, ví dụ: mọi thứ có phải là một chuỗi không? Trạng thái của chương trình là gì - nó có phải là sự gán khóa-giá trị của các chuỗi cho các tên biến không; có nhiều hơn thế không, ví dụ như ngăn xếp? Có một đống? Và như thế.

Tôi đã nghĩ đến việc tham khảo hướng dẫn sử dụng GNU Bash để có cái nhìn sâu sắc này, nhưng có vẻ như nó không phải là những gì tôi muốn; nó giống như một danh sách giặt giũ của đường cú pháp hơn là một lời giải thích về mô hình ngữ nghĩa cốt lõi. Hàng triệu lẻ "hướng dẫn cơ bản" trên mạng chỉ tệ hơn. Có lẽ trước tiên tôi nên nghiên cứu shvà hiểu Bash như một cú pháp đường trên đầu trang này? Tuy nhiên, tôi không biết liệu đây có phải là một mô hình chính xác hay không.

Bất kỳ đề xuất?

CHỈNH SỬA: Tôi đã được yêu cầu cung cấp các ví dụ về những gì lý tưởng mà tôi đang tìm kiếm. Một ví dụ khá tiêu cực về những gì tôi muốn coi là "ngữ nghĩa chính thức" là bài báo này về "bản chất của JavaScript" . Có lẽ một ví dụ ít chính thức hơn một chút là báo cáo Haskell 2010 .


3
Có phải Hướng dẫn Viết kịch bản Bash Nâng cao là một trong "triệu lẻ một" không?
choroba

2
Tôi không tin rằng bash có một "mô hình ngữ nghĩa cốt lõi" (tốt, có thể "hầu hết mọi thứ đều là một chuỗi"); tôi nghĩ nó thực sự là đường cú pháp.
Gordon Davisson

4
Cái mà bạn gọi là "danh sách giặt là đường cú pháp" thực ra là mô hình mở rộng ngữ nghĩa - một phần cực kỳ quan trọng của quá trình thực thi. 90% lỗi và nhầm lẫn là do không hiểu mô hình mở rộng.
anh chàng kia

4
Tôi có thể hiểu tại sao ai đó có thể nghĩ rằng đây là một câu hỏi rộng nếu bạn đọc nó giống như cách tôi viết script shell ? Nhưng câu hỏi thực sự là ngữ nghĩa chính thức và cơ sở cho ngôn ngữ shell và bash nói riêng là gì? , và đó là một câu hỏi hay với một câu trả lời mạch lạc. Bỏ phiếu để mở lại.
kojiro

1
Tôi đã học được khá nhiều trên linuxcommand.org và thậm chí còn có một pdf miễn phí của một sâu hơn cuốn sách trên dòng lệnh viết kịch bản shell
samrap

Câu trả lời:


107

Vỏ là một giao diện cho hệ điều hành. Nó thường là một ngôn ngữ lập trình mạnh mẽ hơn hoặc ít hơn theo đúng nghĩa của nó, nhưng với các tính năng được thiết kế để dễ dàng tương tác cụ thể với hệ điều hành và hệ thống tệp. Ngữ nghĩa của shell POSIX (sau đây được gọi là "shell") có một chút đột biến, kết hợp một số tính năng của LISP (các biểu thức s có rất nhiều điểm chung với tách từ shell ) và C (phần lớn cú pháp số học của shell ngữ nghĩa xuất phát từ C).

Phần gốc khác của cú pháp shell đến từ việc nuôi dưỡng nó như một hỗn hợp các tiện ích UNIX riêng lẻ. Hầu hết những gì thường là nội trang trong shell thực sự có thể được thực hiện dưới dạng các lệnh bên ngoài. Nó ném nhiều neophytes trong một vòng lặp khi chúng nhận ra rằng nó /bin/[tồn tại trên nhiều hệ thống.

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

w?

Điều này có ý nghĩa hơn nhiều nếu bạn nhìn vào cách một trình bao được thực hiện. Đây là một triển khai tôi đã làm như một bài tập. Nó bằng Python, nhưng tôi hy vọng đó không phải là sự cố treo máy đối với bất kỳ ai. Nó không quá mạnh, nhưng nó có tính hướng dẫn:

#!/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()

Tôi hy vọng phần trên làm rõ rằng mô hình thực thi của một shell là khá nhiều:

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

Mở rộng, phân giải lệnh, thực thi. Tất cả ngữ nghĩa của shell đều gắn liền với một trong ba điều này, mặc dù chúng phong phú hơn nhiều so với cách triển khai mà tôi đã viết ở trên.

Không phải tất cả các lệnh fork. Trên thực tế, có một số lệnh không có ý nghĩa gì khi được triển khai dưới dạng các lệnh bên ngoài (chẳng hạn như chúng sẽ phải làm như vậy fork), nhưng ngay cả những lệnh đó thường có sẵn dưới dạng lệnh bên ngoài để tuân thủ nghiêm ngặt POSIX.

Bash xây dựng dựa trên cơ sở này bằng cách thêm các tính năng và từ khóa mới để nâng cao trình bao POSIX. Nó gần như tương thích với sh, và bash phổ biến đến mức một số tác giả kịch bản mất nhiều năm mà không nhận ra rằng một tập lệnh có thể không thực sự hoạt động trên một hệ thống nghiêm ngặt POSIXly. (Tôi cũng tự hỏi làm thế nào mọi người có thể quan tâm nhiều đến ngữ nghĩa và phong cách của một ngôn ngữ lập trình, và quá ít đối với ngữ nghĩa và phong cách của trình bao, nhưng tôi lại phân biệt.)

Thứ tự đánh giá

Đây là một câu hỏi hơi khó: Bash diễn giải các biểu thức theo cú pháp chính từ trái sang phải, nhưng trong cú pháp số học, nó tuân theo C. ưu tiên. Tuy nhiên, biểu thức khác với mở rộng . Từ EXPANSIONphần của sổ tay bash:

Thứ tự của các khai triển là: mở rộng dấu ngoặc nhọn; khai triển dấu ngã, mở rộng tham số và biến, khai triển số học và thay thế lệnh (thực hiện theo kiểu từ trái sang phải); tách từ; và mở rộng tên đường dẫn.

Nếu bạn hiểu phân loại chữ, mở rộng tên đường dẫn và mở rộng tham số, bạn đang trên đường hiểu hầu hết những gì bash làm. Lưu ý rằng việc mở rộng tên đường dẫn đến sau wordsplitting là rất quan trọng, vì nó đảm bảo rằng một tệp có khoảng trắng trong tên của nó vẫn có thể được khớp với một hình cầu. Đây là lý do tại sao việc sử dụng tốt các mở rộng toàn cầu sẽ tốt hơn so với các lệnh phân tích cú pháp nói chung.

Phạm vi

Phạm vi chức năng

Giống như ECMAscript cũ, shell có phạm vi động trừ khi bạn khai báo rõ ràng các tên trong một hàm.

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

$ bar

$ x=123
$ foo
123
$ bar

$ …

Môi trường và "phạm vi" quy trình

Các biểu mẫu con kế thừa các biến của vỏ mẹ của chúng, nhưng các loại quy trình khác không kế thừa các tên chưa được báo cáo.

$ 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

Bạn có thể kết hợp các quy tắc xác định phạm vi sau:

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

$

Kỷ luật đánh máy

Ừm, các kiểu. Vâng. Bash thực sự không có các kiểu và mọi thứ đều mở rộng thành một chuỗi (hoặc có lẽ một từ sẽ thích hợp hơn.) Nhưng chúng ta hãy kiểm tra các kiểu mở rộng khác nhau.

Dây

Khá nhiều thứ có thể được coi là một chuỗi. Barewords trong bash là những chuỗi mà ý nghĩa của nó phụ thuộc hoàn toàn vào sự mở rộng được áp dụng cho nó.

Không mở rộng

Có thể đáng giá để chứng minh rằng một từ trần trụi thực sự chỉ là một từ, và những câu trích dẫn không thay đổi gì về điều đó.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Mở rộng chuỗi con
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Để biết thêm về các bản mở rộng, hãy đọc Parameter Expansionphần của sách hướng dẫn. Nó khá mạnh mẽ.

Số nguyên và biểu thức số học

Bạn có thể nhập các tên với thuộc tính số nguyên để yêu cầu trình bao xử lý phía bên phải của các biểu thức gán là số học. Sau đó, khi tham số mở rộng, nó sẽ được đánh giá là số nguyên toán học trước khi mở rộng thành… một chuỗi.

$ 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

Mảng

Đối số và tham số vị trí

Trước khi nói về mảng, có thể cần thảo luận về các tham số vị trí. Những lập luận để một kịch bản shell có thể được truy xuất thông qua các thông số đánh số, $1, $2, $3, vv Bạn có thể truy cập tất cả các thông số này cùng một lúc sử dụng "$@", trong đó mở rộng có nhiều điểm tương đồng với mảng. Bạn có thể đặt và thay đổi các tham số vị trí bằng cách sử dụng sethoặc shiftnội trang, hoặc đơn giản bằng cách gọi shell hoặc một hàm shell với các tham số sau:

$ 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

Hướng dẫn sử dụng bash đôi khi cũng đề cập đến $0như một tham số vị trí. Tôi thấy điều này khó hiểu, vì nó không bao gồm nó trong số đối số $#, nhưng nó là một tham số được đánh số, vì vậy meh. $0là tên của shell hoặc script shell hiện tại.

Mảng

Cú pháp của mảng được mô hình hóa dựa trên các tham số vị trí, vì vậy, hầu hết bạn nên coi mảng như một loại "tham số vị trí bên ngoài" được đặt tên, nếu bạn muốn. Mảng có thể được khai báo bằng cách sử dụng các phương pháp sau:

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

Bạn có thể truy cập các phần tử mảng theo chỉ mục:

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

Bạn có thể cắt các mảng:

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

Nếu bạn coi một mảng như một tham số bình thường, bạn sẽ nhận được chỉ số thứ không.

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

$ …

Nếu bạn sử dụng dấu ngoặc kép hoặc dấu gạch chéo ngược để ngăn phân tách từ, mảng sẽ duy trì phân tách từ được chỉ định:

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

Sự khác biệt chính giữa mảng và các tham số vị trí là:

  1. Tham số vị trí không thưa thớt. Nếu $12được đặt, bạn cũng có thể chắc chắn $11là đã được đặt. (Nó có thể được đặt thành chuỗi trống, nhưng $#sẽ không nhỏ hơn 12.) Nếu "${arr[12]}"được đặt, không có gì đảm bảo rằng nó "${arr[11]}"được đặt và độ dài của mảng có thể nhỏ hơn 1.
  2. Phần tử thứ 0 của một mảng rõ ràng là phần tử thứ 0 của mảng đó. Trong các tham số vị trí, phần tử thứ 0 không phải là đối số đầu tiên mà là tên của tập lệnh shell hoặc shell.
  3. Đối với shiftmột mảng, bạn phải cắt và gán lại nó, chẳng hạn như arr=( "${arr[@]:1}" ). Bạn cũng có thể làm được unset arr[0], nhưng điều đó sẽ làm cho phần tử đầu tiên ở chỉ mục 1.
  4. Mảng có thể được chia sẻ ngầm giữa các hàm shell dưới dạng toàn cầu, nhưng bạn phải chuyển rõ ràng các tham số vị trí cho một hàm shell để nó nhìn thấy chúng.

Việc sử dụng mở rộng tên đường dẫn để tạo mảng tên tệp thường rất tiện lợi:

$ dirs=( */ )

Lệnh

Các lệnh là quan trọng, nhưng chúng cũng được đề cập sâu hơn so với hướng dẫn sử dụng của tôi. Đọc SHELL GRAMMARphần. Các loại lệnh khác nhau là:

  1. Lệnh đơn giản (ví dụ $ startx)
  2. Đường ống (ví dụ $ yes | make config) (lol)
  3. Danh sách (ví dụ $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Lệnh ghép (ví dụ $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprocesses (Phức tạp, không có ví dụ)
  6. Hàm (Một lệnh ghép được đặt tên có thể được coi như một lệnh đơn giản)

Mô hình thực thi

Tất nhiên, mô hình thực thi liên quan đến cả một đống và một ngăn xếp. Đây là loài đặc hữu của tất cả các chương trình UNIX. Bash cũng có một ngăn xếp cuộc gọi cho các hàm shell, có thể nhìn thấy thông qua việc sử dụng callernội trang lồng nhau .

Người giới thiệu:

  1. Các SHELL GRAMMARphần của cuốn cẩm nang bash
  2. Các XCU Shell Command Language tài liệu
  3. Các Bash Hướng dẫn trên wiki Greycat của.
  4. Lập trình nâng cao trong môi trường UNIX

Hãy đóng góp ý kiến ​​nếu bạn muốn tôi mở rộng thêm theo một hướng cụ thể.


16
+1: Lời giải thích tuyệt vời. Đánh giá cao thời gian dành cho việc viết bài này với các ví dụ.
jaypal singh

+1 cho yes | make config;-) Nhưng nghiêm túc mà nói, một bài viết rất tốt.
Digital Trauma

mới bắt đầu đọc cái này .. hay quá. sẽ để lại một số bình luận. 1) bất ngờ còn lớn hơn khi bạn thấy điều đó /bin/[/bin/testthường là cùng một ứng dụng 2) "Giả sử từ đầu tiên là một lệnh." - mong đợi khi bạn làm nhiệm vụ ...
Karoly Horvath

@KarolyHorvath Có, tôi đã cố ý loại trừ nhiệm vụ khỏi trình bao demo của mình vì các biến là một mớ hỗn độn phức tạp. Trình bao demo đó không được viết với câu trả lời này - nó đã được viết trước đó nhiều. Tôi cho rằng tôi có thể tạo ra nó execlevà nội suy các từ đầu tiên vào môi trường, nhưng điều đó vẫn sẽ khiến nó phức tạp hơn một chút.
kojiro

@kojiro: nah điều đó sẽ làm phức tạp quá mức, đó chắc chắn không phải là ý định của tôi! nhưng nhiệm vụ hoạt động hơi khác (x), và IMHO bạn nên đề cập nó ở đâu đó trong văn bản. (x): và một nguồn gốc của một số nhầm lẫn ... Tôi thậm chí không thể đếm được nữa tôi đã thấy mọi người phàn nàn về việc a = 1không hoạt động bao nhiêu lần ).
Karoly Horvath

5

Câu trả lời cho câu hỏi của bạn "Kỷ luật gõ là gì, ví dụ: mọi thứ đều là một chuỗi" Các biến Bash là các chuỗi ký tự. Nhưng, Bash cho phép các phép toán số học và so sánh trên các biến khi các biến là số nguyên. Ngoại lệ đối với quy tắc Các biến Bash là các chuỗi ký tự là khi các biến đã nói là bộ định kiểu hoặc được khai báo khác

$ 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

Khai báo ý nghĩa tùy chọn:

  • -a Biến là một mảng.
  • -f Chỉ sử dụng tên hàm.
  • -i Biến được coi là một số nguyên; Đánh giá số học được thực hiện khi biến được gán một giá trị.
  • -p Hiển thị các thuộc tính và giá trị của từng biến. Khi -p được sử dụng, các tùy chọn bổ sung sẽ bị bỏ qua.
  • -r Đặt biến ở chế độ chỉ đọc. Sau đó, các biến này không thể được gán giá trị bằng các câu lệnh gán tiếp theo, cũng như không thể bỏ đặt chúng.
  • -t Cung cấp cho mỗi biến thuộc tính dấu vết.
  • -x Đánh dấu từng biến để xuất sang các lệnh tiếp theo thông qua môi trường.

1

Trang chủ bash có khá nhiều thông tin hơn hầu hết các trang và bao gồm một số thông tin bạn đang yêu cầu. Giả định của tôi sau hơn một thập kỷ viết kịch bản bash là, do 'lịch sử là một phần mở rộng của sh, nó có một số cú pháp thú vị (để duy trì khả năng tương thích ngược với sh).

FWIW, trải nghiệm của tôi cũng giống như của bạn; mặc dù các cuốn sách khác nhau (ví dụ: O'Reilly "Learning the Bash Shell" và tương tự) có trợ giúp về cú pháp, có rất nhiều cách kỳ lạ để giải quyết các vấn đề khác nhau và một số trong số chúng không có trong sách và phải được đưa lên Google.

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.