Chức năng bash nhà nước


16

Tôi muốn triển khai một hàm trong Bash làm tăng (và trả về) số đếm cho mỗi cuộc gọi. Thật không may, điều này có vẻ không tầm thường vì tôi đang gọi hàm bên trong một lớp con và do đó nó không thể sửa đổi các biến của lớp vỏ của nó.

Đây là nỗ lực của tôi:

PS_COUNT=0

ps_count_inc() {
    let PS_COUNT=PS_COUNT+1
    echo $PS_COUNT
}

ps_count_reset() {
    let PS_COUNT=0
}

Điều này sẽ được sử dụng như sau (và do đó tôi cần phải gọi các hàm từ một lớp con):

PS1='$(ps_count_reset)> '
PS2='$(ps_count_inc)   '

Bằng cách đó, tôi có một dấu nhắc nhiều dòng được đánh số:

> echo 'this
1   is
2   a
3   test'

Dễ thương. Nhưng do giới hạn nêu trên không hoạt động.

Một giải pháp không hoạt động sẽ là ghi số đếm vào một tệp thay vì một biến. Tuy nhiên, điều này sẽ tạo ra một cuộc xung đột giữa nhiều phiên, chạy đồng thời. Dĩ nhiên, tôi có thể nối ID tiến trình của shell vào tên tệp. Nhưng tôi hy vọng có một giải pháp tốt hơn sẽ không làm lộn xộn hệ thống của tôi với nhiều tệp.


Va chạm WRT bằng cách sử dụng tệp stash xem man 1 mktemp.
goldilocks

Bạn sẽ thấy chỉnh sửa của tôi - tôi nghĩ bạn sẽ thích nó.
mikeerv

Câu trả lời:


14

nhập mô tả hình ảnh ở đây

Để có được đầu ra giống như bạn lưu ý trong câu hỏi của bạn, tất cả những gì cần thiết là:

PS1='${PS2c##*[$((PS2c=0))-9]}- > '
PS2='$((PS2c=PS2c+1)) > '

Bạn không cần phải vặn vẹo. Hai dòng này sẽ làm tất cả trong bất kỳ shell nào giả vờ bất cứ thứ gì gần với khả năng tương thích POSIX.

- > cat <<HD
1 >     line 1
2 >     line $((PS2c-1))
3 > HD
    line 1
    line 2
- > echo $PS2c
0

Nhưng tôi thích điều này. Và tôi muốn chứng minh các nguyên tắc cơ bản của những gì làm cho công việc này tốt hơn một chút. Vì vậy, tôi chỉnh sửa điều này một chút. Bây giờ tôi đã nhét nó vào /tmpnhưng tôi nghĩ tôi cũng sẽ giữ nó cho riêng mình. Nó ở đây:

cat /tmp/prompt

KHUYẾN MÃI:

ps1() { IFS=/
    set -- ${PWD%"${last=${PWD##/*/}}"}
    printf "${1+%c/}" "$@" 
    printf "$last > "
}

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
PS2='$((PS2c=PS2c+1)) > '

Lưu ý: gần đây đã biết về yash , tôi đã xây dựng nó ngày hôm qua. Vì bất kỳ lý do gì, nó không in byte đầu tiên của mọi đối số với %cchuỗi - mặc dù các tài liệu cụ thể về các phần mở rộng char cho định dạng đó và vì vậy nó có thể liên quan - nhưng nó chỉ tốt với%.1s

Đó là toàn bộ. Có hai điều chính đang diễn ra ở đó. Và đây là những gì nó trông giống như:

/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 >

PARSING $PWD

Mỗi lần $PS1được đánh giá, nó phân tích cú pháp và in $PWDđể thêm vào dấu nhắc. Nhưng tôi không thích toàn bộ $PWDmàn hình của mình, vì vậy tôi chỉ muốn chữ cái đầu tiên của mỗi mẩu bánh mì trong đường dẫn hiện tại xuống thư mục hiện tại mà tôi muốn xem đầy đủ. Như thế này:

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cd /
/ > cd ~
/h/mikeserv > 

Có một vài bước ở đây:

IFS=/

chúng ta sẽ phải phân chia hiện tại $PWDvà cách đáng tin cậy nhất để làm điều đó là $IFSchia tách /. Không cần phải bận tâm với nó sau đó - tất cả việc phân tách từ đây trở đi sẽ được xác định bởi $@mảng tham số vị trí của shell trong lệnh tiếp theo như:

set -- ${PWD%"${last=${PWD##/*/}}"}

Vì vậy, cái này là một chút khó khăn, nhưng điều chủ yếu là chúng ta tách đang $PWDtrên /các biểu tượng. Tôi cũng sử dụng mở rộng tham số để gán cho $lastmọi thứ sau khi bất kỳ giá trị nào xảy ra giữa /dấu gạch chéo trái và phải nhất . Bằng cách này, tôi biết rằng nếu tôi chỉ ở /và chỉ có một /thì $lastsẽ vẫn bằng nhau $PWD$1sẽ trống rỗng. Vấn đề này Tôi cũng thoát $lastkhỏi đầu đuôi $PWDtrước khi gán nó cho $@.

printf "${1+%c/}" "$@"

Vì vậy, ở đây - miễn là ${1+is set}chúng tôi printf%ckẻ gây rối đầu tiên cho mỗi đối số của shell - mà chúng ta vừa đặt cho mỗi thư mục trong hiện tại $PWD- trừ thư mục trên cùng - tách ra /. Vì vậy, về cơ bản chúng tôi chỉ in ký tự đầu tiên của mọi thư mục trong $PWDnhưng thư mục trên cùng. Mặc dù điều quan trọng là nhận ra điều này chỉ xảy ra nếu $1được đặt ở tất cả, điều này sẽ không xảy ra ở gốc /hoặc tại một lần bị xóa khỏi /như trong /etc.

printf "$last > "

$lastlà biến tôi vừa gán cho thư mục hàng đầu của chúng tôi. Vì vậy, bây giờ đây là thư mục hàng đầu của chúng tôi. Nó in ra hay không tuyên bố cuối cùng đã làm. Và nó cần một chút gọn gàng >cho các biện pháp tốt.

NHƯNG GÌ VỀ SỰ TĂNG CƯỜNG?

Và sau đó là vấn đề của $PS2điều kiện. Tôi đã chỉ ra trước đây làm thế nào điều này có thể được thực hiện mà bạn vẫn có thể tìm thấy bên dưới - đây về cơ bản là một vấn đề về phạm vi. Nhưng có thêm một chút nữa trừ khi bạn muốn bắt đầu thực hiện một loạt các không printf \bgian và sau đó cố gắng cân bằng số lượng nhân vật của họ ... ugh. Vì vậy, tôi làm điều này:

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'

Một lần nữa, ${parameter##expansion}tiết kiệm trong ngày. Mặc dù có một điều lạ ở đây - chúng tôi thực sự đã đặt biến trong khi chúng tôi tự loại bỏ biến đó. Chúng tôi sử dụng giá trị mới của nó - đặt dải giữa - như toàn cầu mà chúng tôi tách. Bạn thấy sao? Chúng tôi ##*tách tất cả từ phần đầu của biến tăng dần sang ký tự cuối cùng có thể là bất cứ thứ gì từ đó [$((PS2c=0))-9]. Chúng tôi đảm bảo theo cách này không tạo ra giá trị, nhưng chúng tôi vẫn chỉ định nó. Nó thật tuyệt - Tôi chưa bao giờ làm điều đó trước đây. Nhưng POSIX cũng đảm bảo với chúng tôi rằng đây là cách di động nhất có thể được thực hiện.

Và đó là nhờ POSIX quy định ${parameter} $((expansion))giữ các định nghĩa này trong vỏ hiện tại mà không yêu cầu chúng tôi đặt chúng trong một khung con riêng biệt, bất kể chúng tôi đánh giá chúng ở đâu. Và đây là lý do tại sao nó hoạt động trong dashshcũng như nó làm trong bashzsh. Chúng tôi không sử dụng thoát thoát phụ thuộc shell / terminal và chúng tôi để các biến tự kiểm tra. Đó là những gì làm cho mã di động nhanh chóng.

Phần còn lại khá đơn giản - chỉ cần tăng bộ đếm của chúng tôi cho mỗi lần $PS2được đánh giá cho đến khi $PS1một lần nữa đặt lại nó. Như thế này:

PS2='$((PS2c=PS2c+1)) > '

Vì vậy, bây giờ tôi có thể:

DASH DEMO

ENV=/tmp/prompt dash -i

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 > printf '\t%s\n' "$PS1" "$PS2" "$PS2c"
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
    0
/u/s/m/man3 > cd ~
/h/mikeserv >

SH DEMO

Nó hoạt động tương tự trong bashhoặc sh:

ENV=/tmp/prompt sh -i

/h/mikeserv > cat <<HEREDOC
1 >     $( echo $PS2c )
2 >     $( echo $PS1 )
3 >     $( echo $PS2 )
4 > HEREDOC
    4
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
/h/mikeserv > echo $PS2c ; cd /
0
/ > cd /usr/share
/u/share > cd ~
/h/mikeserv > exit

Như tôi đã nói ở trên, vấn đề chính là bạn cần xem xét nơi bạn thực hiện tính toán của mình. Bạn không nhận được trạng thái trong vỏ cha mẹ - vì vậy bạn không tính toán ở đó. Bạn có trạng thái trong subshell - vì vậy đó là nơi bạn tính toán. Nhưng bạn làm định nghĩa trong vỏ cha.

ENV=/dev/fd/3 sh -i  3<<\PROMPT
    ps1() { printf '$((PS2c=0)) > ' ; }
    ps2() { printf '$((PS2c=PS2c+1)) > ' ; }
    PS1=$(ps1)
    PS2=$(ps2)
PROMPT

0 > cat <<MULTI_LINE
1 > $(echo this will be line 1)
2 > $(echo and this line 2)
3 > $(echo here is line 3)
4 > MULTI_LINE
this will be line 1
and this line 2
here is line 3
0 >

1
@mikeerv Chúng tôi đang quay vòng tròn. Tôi biết tất cả điều này. Nhưng làm thế nào để tôi sử dụng điều này trong định nghĩa của tôi về PS2? Đây là phần khó khăn. Tôi không nghĩ rằng giải pháp của bạn có thể được áp dụng ở đây. Nếu bạn nghĩ khác xin vui lòng chỉ cho tôi làm thế nào.
Konrad Rudolph

1
@mikeerv Không, điều đó không liên quan, xin lỗi. Xem câu hỏi của tôi để biết chi tiết. PS1PS2là các biến đặc biệt trong shell được in dưới dạng dấu nhắc lệnh (thử nó bằng cách đặt PS1thành một giá trị khác trong cửa sổ shell mới), do đó chúng được sử dụng rất khác với mã của bạn. Dưới đây là một số thông tin về cách sử dụng của họ: linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KonradRudolph điều gì ngăn bạn xác định chúng hai lần? Đó là những gì ban đầu của tôi đã làm ... Tôi phải xem câu trả lời của bạn ... Điều này được thực hiện tất cả thời gian.
mikeerv

1
@mikeerv Nhập echo 'thisvào một dấu nhắc, sau đó giải thích cách cập nhật giá trị PS2trước khi nhập dấu ngoặc đơn đóng.
chepner

1
Được rồi, câu trả lời này bây giờ chính thức tuyệt vời. Tôi cũng thích mẩu bánh mì, mặc dù tôi sẽ không chấp nhận nó vì tôi vẫn in đường dẫn đầy đủ trong một dòng riêng biệt: i.imgur.com/xmqrVxL.png
Konrad Rudolph

8

Với cách tiếp cận này (chức năng chạy trong một lớp con), bạn sẽ không thể cập nhật trạng thái của quy trình vỏ chính mà không phải trải qua các mâu thuẫn. Thay vào đó, sắp xếp cho chức năng chạy trong quy trình tổng thể.

Giá trị của PROMPT_COMMANDbiến được xen kẽ như một lệnh được thực thi trước khi in PS1dấu nhắc.

Đối với PS2, không có gì có thể so sánh. Nhưng thay vào đó, bạn có thể sử dụng một mẹo: vì tất cả những gì bạn muốn làm là một phép toán số học, bạn có thể sử dụng mở rộng số học, không liên quan đến một lớp con.

PROMPT_COMMAND='PS_COUNT=0'
PS2='$((++PS_COUNT))  '

Kết quả tính toán số học kết thúc trong dấu nhắc. Nếu bạn muốn ẩn nó, bạn có thể vượt qua nó dưới dạng một mảng con không tồn tại.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '

4

Đó là một chút I / O chuyên sâu, nhưng bạn sẽ cần sử dụng một tệp tạm thời để giữ giá trị của số đếm.

ps_count_inc () {
   read ps_count < ~/.prompt_num
   echo $((++ps_count)) | tee ~/.prompt_num
}

ps_count_reset () {
   echo 0 > ~/.prompt_num
}

Nếu bạn lo lắng về việc cần một tệp riêng cho mỗi phiên shell (có vẻ như là một mối quan tâm nhỏ; bạn có thực sự sẽ gõ các lệnh nhiều dòng trong hai shell khác nhau cùng một lúc không?), Bạn nên sử dụng mktempđể tạo một tệp mới cho mỗi tệp sử dụng.

ps_count_reset () {
    rm -f "$prompt_count"
    prompt_count=$(mktemp)
    echo 0 > "$prompt_count"
}

ps_count_inc () {
    read ps_count < "$prompt_count"
    echo $((++ps_count)) | tee "$prompt_count"
}

+1 I / O có lẽ không đáng kể lắm vì nếu tệp nhỏ và thường xuyên được truy cập, nó sẽ được lưu vào bộ nhớ cache, nghĩa là về cơ bản nó hoạt động như bộ nhớ dùng chung.
goldilocks

1

Bạn không thể sử dụng biến shell theo cách này và bạn đã hiểu tại sao. Một lớp con kế thừa các biến chính xác giống như cách một quá trình kế thừa môi trường của nó: mọi thay đổi được thực hiện chỉ áp dụng cho nó và các con của nó chứ không áp dụng cho bất kỳ quy trình tổ tiên nào.

Theo các câu trả lời khác, điều dễ nhất để làm là bỏ dữ liệu đó vào một tệp.

echo $count > file
count=$(<file)

Vân vân.


Tất nhiên bạn có thể đặt một biến theo cách này. Bạn không cần một tập tin tạm thời. Bạn đặt biến trong vỏ con và in giá trị của nó sang vỏ cha nơi bạn hấp thụ giá trị đó. Bạn nhận được tất cả trạng thái bạn cần để tính giá trị của nó trong lớp con để bạn thực hiện nó.
mikeerv

1
@mikeerv Đó không phải là điều tương tự, đó là lý do tại sao OP đã nói rằng một giải pháp như vậy sẽ không hoạt động (mặc dù điều này cần được làm rõ hơn trong câu hỏi). Những gì bạn đang đề cập là chuyển một giá trị cho một quá trình khác thông qua IPC để nó có thể gán giá trị đó cho bất cứ điều gì. Những gì OP muốn / cần làm là ảnh hưởng đến giá trị của biến toàn cục được chia sẻ bởi một số quy trình và bạn không thể thực hiện điều đó thông qua môi trường; Nó không hữu ích cho IPC.
goldilocks

Man, hoặc tôi đã hoàn toàn hiểu sai những gì cần thiết ở đây, hoặc những người khác có. Nó có vẻ thực sự đơn giản với tôi. Bạn thấy chỉnh sửa của tôi? Có gì sai với nó?
mikeerv

@mikeerv Tôi không nghĩ bạn đã hiểu lầm và công bằng mà nói, những gì bạn có một dạng IPC và có thể hoạt động. Tôi không rõ tại sao Konrad không thích nó, nhưng nếu nó không đủ linh hoạt, thì việc lưu trữ tệp khá đơn giản (và đó là cách để tránh va chạm, ví dụ mktemp).
goldilocks

2
@mikeerv Hàm dự định được gọi khi giá trị của PS2được mở rộng bằng shell. Bạn không có cơ hội cập nhật giá trị của biến trong vỏ cha mẹ tại thời điểm đó.
chepner

0

Để tham khảo, đây là giải pháp của tôi bằng cách sử dụng các tệp tạm thời, duy nhất cho mỗi quy trình shell và xóa càng sớm càng tốt (để tránh lộn xộn, như được đề cập trong câu hỏi):

# Yes, I actually need this to work across my systems. :-/
_mktemp() {
    local tmpfile="${TMPDIR-/tmp}/psfile-$$.XXX"
    local bin="$(command -v mktemp || echo echo)"
    local file="$($bin "$tmpfile")"
    rm -f "$file"
    echo "$file"
}

PS_COUNT_FILE="$(_mktemp)"

ps_count_inc() {
    local PS_COUNT
    if [[ -f "$PS_COUNT_FILE" ]]; then
        let PS_COUNT=$(<"$PS_COUNT_FILE")+1
    else
        PS_COUNT=1
    fi

    echo $PS_COUNT | tee "$PS_COUNT_FILE"
}

ps_count_reset() {
    rm -f "$PS_COUNT_FILE"
}
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.