Fungsi bash stateful

16

Saya ingin menerapkan fungsi dalam Bash yang meningkatkan (dan mengembalikan) hitungan dengan setiap panggilan. Sayangnya ini tampaknya non-sepele karena saya menjalankan fungsi di dalam subkulit dan akibatnya tidak dapat memodifikasi variabel shell induknya.

Inilah usaha saya:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

Ini akan digunakan sebagai berikut (dan karenanya kebutuhan saya untuk memanggil fungsi dari subkulit):

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

Dengan begitu, saya akan memiliki prompt multi-line bernomor:

> echo 'this
1   is
2   a
3   test'

Imut. Tetapi karena keterbatasan yang disebutkan di atas tidak berhasil.

Solusi yang tidak berfungsi adalah menulis hitungan ke file alih-alih variabel. Namun, ini akan menciptakan konflik antara beberapa sesi, secara bersamaan menjalankan sesi. Saya bisa menambahkan ID proses dari shell ke nama file, tentu saja. Tapi saya berharap ada solusi yang lebih baik yang tidak akan mengacaukan sistem saya dengan banyak file.

Konrad Rudolph
sumber
Tabrakan WRT menggunakan file simpanan lihat man 1 mktemp.
goldilocks
Anda akan melihat hasil edit saya - saya pikir Anda akan menyukainya.
mikeserv

Jawaban:

14

masukkan deskripsi gambar di sini

Untuk mendapatkan hasil yang sama seperti yang Anda catat dalam pertanyaan Anda, yang diperlukan hanyalah ini:

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

Anda tidak perlu memutarbalikkan. Kedua baris tersebut akan melakukan semuanya dalam shell yang berpura-pura mendekati kompatibilitas POSIX.

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

Tapi saya suka ini. Dan saya ingin menunjukkan dasar-dasar apa yang membuat pekerjaan ini sedikit lebih baik. Jadi saya sedikit mengedit ini. Saya memasukkannya /tmpuntuk saat ini tetapi saya pikir saya akan menyimpannya sendiri juga. Itu disini:

cat /tmp/prompt

PROMPT SCRIPT:

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

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

Catatan: setelah baru-baru ini belajar tentang yash , saya membangunnya kemarin. Untuk alasan apa pun itu tidak mencetak byte pertama dari setiap argumen dengan %cstring - meskipun dokumen khusus tentang ekstensi wide-char untuk format itu dan jadi itu mungkin terkait - tetapi tidak apa-apa dengan%.1s

Itu semuanya. Ada dua hal utama yang terjadi di sana. Dan seperti inilah tampilannya:

/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

Setiap kali $PS1dievaluasi itu diurai dan dicetak $PWDuntuk menambah prompt. Tetapi saya tidak suka seluruh $PWDkerumunan layar saya, jadi saya ingin hanya huruf pertama dari setiap remah roti di jalur saat ini ke direktori saat ini, yang ingin saya lihat secara penuh. Seperti ini:

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

Ada beberapa langkah di sini:

IFS=/

kita harus membagi arus $PWDdan cara yang paling dapat diandalkan untuk melakukannya adalah dengan $IFSmembagi /. Tidak perlu repot dengan itu sama sekali sesudahnya - semua pemisahan dari sini keluar akan ditentukan oleh $@array parameter posisi shell di perintah berikutnya seperti:

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

Jadi yang satu ini sedikit rumit, tetapi hal utama adalah bahwa kita membelah $PWDpada /simbol. Saya juga menggunakan ekspansi parameter untuk menetapkan $lastsegalanya setelah nilai apa pun yang terjadi antara /slash paling kiri dan paling kanan . Dengan cara ini saya tahu bahwa jika saya hanya /punya dan hanya punya satu /maka $lastakan tetap sama dengan keseluruhan $PWDdan $1akan kosong. Ini penting. Saya juga melepas $lastdari ujung ekor $PWDsebelum menugaskan $@.

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

Jadi di sini - selama ${1+is set}kita printfadalah karakter pertama %cdari setiap argumen shell kita - yang baru saja kita atur untuk setiap direktori saat ini $PWD- dikurangi direktori teratas - terpecah /. Jadi pada dasarnya kita hanya mencetak karakter pertama dari setiap direktori $PWDtetapi yang paling atas. Penting untuk menyadari bahwa ini hanya terjadi jika $1disetel sama sekali, yang tidak akan terjadi pada saat root /atau dihilangkan dari /seperti pada /etc.

printf "$last > "

$lastadalah variabel yang baru saja saya ditugaskan ke direktori teratas kami. Jadi sekarang ini adalah direktori teratas kami. Ini mencetak apakah pernyataan terakhir lakukan atau tidak. Dan butuh sedikit yang rapi >untuk mengukur baik.

TAPI APA TENTANG PENINGKATAN?

Dan kemudian ada masalah $PS2kondisional. Saya telah menunjukkan sebelumnya bagaimana hal ini dapat dilakukan yang masih dapat Anda temukan di bawah - ini pada dasarnya masalah ruang lingkup. Tapi ada sedikit lebih dari itu kecuali jika Anda ingin mulai melakukan banyak printf \bruang dan kemudian mencoba untuk menyeimbangkan jumlah karakter mereka ... ugh. Jadi saya melakukan ini:

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

Sekali lagi, ${parameter##expansion}hematlah hari itu. Agak aneh di sini - kita sebenarnya mengatur variabel sementara kita menghapusnya sendiri. Kami menggunakan nilai barunya - setel strip tengah - sebagai gumpalan yang darinya kami strip. Kamu melihat? Kami ##*menghapus semua dari kepala variabel kenaikan kami ke karakter terakhir yang dapat berupa apa saja [$((PS2c=0))-9]. Kami dijamin dengan cara ini untuk tidak menampilkan nilai, namun kami masih menetapkannya. Ini keren - saya belum pernah melakukannya sebelumnya. Tetapi POSIX juga menjamin kita bahwa ini adalah cara paling portabel yang bisa dilakukan.

Dan terima kasih kepada POSIX yang ditentukan ${parameter} $((expansion))yang membuat definisi ini di shell saat ini tanpa mengharuskan kita mengaturnya dalam subkulit yang terpisah, terlepas dari di mana kita mengevaluasinya. Dan inilah mengapa ini bekerja dashdan shsama baiknya dengan di bashdan zsh. Kami tidak menggunakan shell / terminal dependent escapes dan kami membiarkan variabel menguji sendiri. Itulah yang membuat kode portabel cepat.

Sisanya cukup sederhana - cukup tambahkan penghitung kami untuk setiap kali $PS2dievaluasi sampai $PS1sekali lagi atur ulang. Seperti ini:

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

Jadi sekarang saya bisa:

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

Ia bekerja sama di bashatau 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

Seperti yang saya katakan di atas, masalah utama adalah Anda harus mempertimbangkan di mana Anda melakukan perhitungan. Anda tidak mendapatkan status di shell induk - jadi Anda tidak menghitung di sana. Anda mendapatkan status dalam subkulit - jadi di situlah Anda menghitung. Tapi Anda melakukan definisi di shell induk.

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 >
mikeserv
sumber
1
@ mikeserv Kami berputar-putar. Saya tahu semua ini. Tetapi bagaimana saya menggunakan ini dalam definisi saya PS2? Ini bagian yang sulit. Saya tidak berpikir solusi Anda dapat diterapkan di sini. Jika Anda berpikir sebaliknya, tolong tunjukkan saya caranya.
Konrad Rudolph
1
@ mikeserv Tidak, itu tidak berhubungan, maaf. Lihat pertanyaan saya untuk detailnya. PS1dan PS2merupakan variabel khusus dalam shell yang dicetak sebagai command prompt (coba dengan menetapkan PS1ke nilai yang berbeda di jendela shell baru), sehingga mereka digunakan sangat berbeda dari kode Anda. Berikut ini beberapa info lebih lanjut tentang penggunaannya: linuxconfig.org/bash-prompt-basics
Konrad Rudolph
1
@KonradRudolph apa yang menghentikan Anda mendefinisikan mereka dua kali? Yang mana hal orisinal saya lakukan ... Saya harus melihat jawaban Anda ... Ini selalu dilakukan.
mikeserv
1
@ mikeserv Ketik echo 'thispada prompt, lalu jelaskan cara memperbarui nilai PS2sebelum mengetikkan kutipan tunggal penutup.
chepner
1
Oke, jawaban ini sekarang luar biasa secara resmi. Saya juga suka remah roti, meskipun saya tidak akan mengadopsinya karena saya mencetak path lengkap dalam baris yang terpisah: i.imgur.com/xmqrVxL.png
Konrad Rudolph
8

Dengan pendekatan ini (fungsi berjalan dalam sebuah subkulit) Anda tidak akan dapat memperbarui status proses master shell tanpa melalui contortions. Alih-alih, atur agar fungsi dijalankan dalam proses master.

Nilai PROMPT_COMMANDvariabel ditafsirkan sebagai perintah yang dijalankan sebelum mencetak PS1prompt.

Sebab PS2, tidak ada yang sebanding. Tetapi Anda dapat menggunakan trik sebagai gantinya: karena semua yang ingin Anda lakukan adalah operasi aritmatika, Anda dapat menggunakan ekspansi aritmatika, yang tidak melibatkan subkulit.

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

Hasil perhitungan aritmatika berakhir di prompt. Jika Anda ingin menyembunyikannya, Anda bisa meneruskannya sebagai subscript array yang tidak ada.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '
Gilles 'SANGAT berhenti menjadi jahat'
sumber
4

Agak intensif I / O, tetapi Anda harus menggunakan file sementara untuk menyimpan nilai hitungan.

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

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

Jika Anda khawatir tentang perlu file terpisah per sesi shell (yang tampaknya seperti masalah kecil; apakah Anda benar-benar akan mengetik perintah multi-line dalam dua shell yang berbeda pada saat yang sama?), Anda harus menggunakan mktempuntuk membuat file baru untuk setiap menggunakan.

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"
}
chepner
sumber
+1 I / O mungkin tidak terlalu signifikan karena jika file tersebut kecil dan sering diakses, itu akan di-cache, yaitu pada dasarnya berfungsi sebagai memori bersama.
goldilocks
1

Anda tidak dapat menggunakan variabel shell dengan cara ini dan Anda sudah mengerti mengapa. Subkulit mewarisi variabel dengan cara yang sama persis dengan proses mewarisi lingkungannya: setiap perubahan yang dibuat hanya berlaku untuk itu dan anak-anaknya dan tidak untuk proses leluhur.

Seperti jawaban lain, hal termudah untuk dilakukan adalah menyimpan data itu dalam file.

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

Dll

goldilocks
sumber
Tentu saja Anda dapat mengatur variabel dengan cara ini. Anda tidak perlu file temp. Anda mengatur variabel dalam subkulit dan mencetak nilainya ke shell induk di mana Anda menyerap nilai itu. Anda mendapatkan semua status yang Anda butuhkan untuk menghitung nilainya dalam subkulit sehingga di situlah Anda melakukannya.
mikeserv
1
@ mikeserv Itu bukan hal yang sama, itulah mengapa OP mengatakan solusi seperti itu tidak akan berfungsi (walaupun ini seharusnya dibuat lebih jelas dalam pertanyaan). Apa yang Anda maksudkan adalah memberikan nilai ke proses lain melalui IPC sehingga dapat menetapkan nilai itu untuk apa pun. Apa yang OP inginkan / perlu lakukan adalah memengaruhi nilai variabel global yang dibagikan oleh sejumlah proses, dan Anda tidak bisa melakukannya melalui lingkungan; itu tidak terlalu berguna untuk IPC.
goldilocks
Sobat, entah saya benar-benar salah mengerti apa yang dibutuhkan di sini, atau yang dimiliki semua orang. Tampaknya sangat sederhana bagi saya. Anda melihat hasil edit saya? Apakah ada yang salah?
mikeserv
@ mikeserv Saya tidak berpikir Anda salah paham dan bersikap adil, apa yang Anda miliki adalah bentuk IPC dan bisa bekerja. Tidak jelas bagi saya mengapa Konrad tidak menyukainya, tetapi jika tidak cukup fleksibel, maka simpanan file cukup mudah (dan demikian juga cara untuk menghindari tabrakan, misalnya mktemp).
goldilocks
2
@ mikeserv Fungsi yang dimaksud dipanggil ketika nilai PS2diperluas oleh shell. Anda tidak memiliki kesempatan untuk memperbarui nilai variabel di shell induk pada saat itu.
chepner
0

Untuk referensi, inilah solusi saya menggunakan file sementara, yang unik untuk setiap proses shell, dan dihapus sesegera mungkin (untuk menghindari kekacauan, seperti disinggung dalam pertanyaan):

# 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"
}
Konrad Rudolph
sumber