ฟังก์ชันทุบตี stateful


16

ฉันต้องการใช้ฟังก์ชั่นใน Bash ซึ่งจะเพิ่มจำนวน (และคืน) จำนวนการโทรทุกครั้ง น่าเสียดายที่นี่ดูไม่สำคัญเพราะฉันเรียกใช้ฟังก์ชั่นภายใน subshell และดังนั้นจึงไม่สามารถแก้ไขตัวแปรของ parent shell ได้

นี่คือความพยายามของฉัน:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

นี้จะใช้ดังนี้ (และด้วยเหตุนี้ฉันต้องเรียกใช้ฟังก์ชันจาก subshell):

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

ด้วยวิธีนี้ฉันจะได้รับพรอมต์หลายบรรทัดที่มีหมายเลข:

> echo 'this
1   is
2   a
3   test'

น่ารัก แต่เนื่องจากข้อ จำกัด ดังกล่าวไม่ทำงาน

โซลูชันที่ไม่ทำงานจะเขียนจำนวนลงในไฟล์แทนตัวแปร อย่างไรก็ตามสิ่งนี้จะสร้างความขัดแย้งระหว่างเซสชันหลายเซสชันพร้อมกัน ฉันสามารถต่อท้าย ID กระบวนการของเชลล์กับชื่อไฟล์ได้แน่นอน แต่ฉันหวังว่าจะมีวิธีแก้ปัญหาที่ดีกว่าซึ่งจะไม่ทำให้ระบบยุ่งเหยิงด้วยไฟล์จำนวนมาก


ชน WRT man 1 mktempใช้ดูไฟล์ที่ซ่อน
goldilocks

คุณควรเห็นการแก้ไขของฉัน - ฉันคิดว่าคุณจะชอบมัน
mikeserv

คำตอบ:


14

ป้อนคำอธิบายรูปภาพที่นี่

เพื่อให้ได้ผลลัพธ์เดียวกันกับที่คุณบันทึกในคำถามของคุณสิ่งที่จำเป็นต้องมีคือ:

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

คุณไม่จำเป็นต้องบิด ทั้งสองบรรทัดนี้จะทำทุกอย่างในเชลล์ที่อ้างว่าใกล้เคียงกับ POSIX

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

แต่ฉันชอบสิ่งนี้ และฉันต้องการแสดงให้เห็นพื้นฐานของสิ่งที่ทำให้งานนี้ดีขึ้นเล็กน้อย ดังนั้นฉันจึงแก้ไขสิ่งนี้เล็กน้อย ฉันติดมันใน/tmpตอนนี้ แต่ฉันคิดว่าฉันจะเก็บมันไว้เพื่อตัวเองเช่นกัน ที่นี่:

cat /tmp/prompt

พรอมต์สคริปต์:

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

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

หมายเหตุ: เมื่อเร็ว ๆ นี้ได้เรียนรู้เกี่ยวกับyashฉันสร้างมันเมื่อวาน ไม่ว่าจะด้วยเหตุผลใดก็ตามมันจะไม่พิมพ์ไบต์แรกของอาร์กิวเมนต์ทุกตัวด้วย%cสตริง - แม้ว่าเอกสารนั้นจะมีความเฉพาะเจาะจงเกี่ยวกับส่วนขยายแบบกว้างสำหรับรูปแบบนั้นและอาจเกี่ยวข้องกัน - แต่มันก็ใช้ได้ดี%.1s

นั่นคือสิ่งทั้งหมด มีสองสิ่งหลักเกิดขึ้นที่นั่น และนี่คือสิ่งที่ดูเหมือนว่า:

/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 >

วจีวิภาค $PWD

ทุกครั้งที่$PS1มีการประเมินผลจะแยกวิเคราะห์และพิมพ์$PWDเพื่อเพิ่มไปยังพรอมต์ แต่ฉันไม่ชอบ$PWDหน้าจอที่เบียดเสียดของฉันดังนั้นฉันต้องการแค่ตัวอักษรตัวแรกของ breadcrumb ทุกอันในเส้นทางปัจจุบันจนถึงไดเรกทอรีปัจจุบันซึ่งฉันต้องการเห็นเต็ม แบบนี้:

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

มีไม่กี่ขั้นตอนที่นี่:

IFS=/

เรากำลังจะมีการแยกในปัจจุบัน$PWDและวิธีที่เชื่อถือได้มากที่สุดที่จะทำคือมีแยก$IFS /ไม่จำเป็นต้องยุ่งกับมันเลย - การแยกทั้งหมดจากที่นี่ออกมาจะถูกกำหนดโดย$@อาร์เรย์พารามิเตอร์ของเชลล์ในคำสั่งถัดไปเช่น:

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

ดังนั้นหนึ่งนี้หากินเล็ก ๆ น้อย ๆ แต่สิ่งที่สำคัญคือการที่เราแยกกำลัง$PWDบน/สัญลักษณ์ ฉันยังใช้การขยายพารามิเตอร์เพื่อกำหนดให้กับ$lastทุกสิ่งหลังจากค่าใด ๆ ที่เกิดขึ้นระหว่าง/เครื่องหมายสแลชซ้ายและขวาสุด ด้วยวิธีนี้ฉันรู้ว่าถ้าฉันแค่/และมีเพียงคนเดียว/ก็$lastจะยังคงเท่ากับทั้งหมด$PWDและ$1จะว่างเปล่า เรื่องนี้ ฉันยังตัด$lastจากปลายหางของก่อนที่จะกำหนดมัน$PWD$@

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

ดังนั้นที่นี่ - ตราบใดที่${1+is set}เราprintfแรก%character ของการขัดแย้งแต่ละเปลือกของเรา - ซึ่งเราได้ตั้งเพียงเพื่อไดเรกทอรีในปัจจุบันของเราในแต่ละ$PWD- น้อยไดเรกทอรีด้านบน - /แยก ดังนั้นโดยพื้นฐานแล้วเราแค่พิมพ์ตัวอักษรแรกของทุก ๆ ตัวในไดเรกทอรี$PWDแต่ตัวแรกสุด มันเป็นสิ่งสำคัญที่จะตระหนักถึงแม้ว่านี้จะเกิดขึ้นหาก$1ได้รับการกำหนดที่ทุกคนซึ่งจะไม่เกิดขึ้นที่ราก/หรืออย่างใดอย่างหนึ่งลบออกจากเช่นใน//etc

printf "$last > "

$lastเป็นตัวแปรที่ฉันเพิ่งกำหนดให้กับสารบบอันดับต้น ๆ ของเรา ดังนั้นตอนนี้นี่คือไดเรกทอรียอดนิยมของเรา มันพิมพ์ว่าคำสั่งสุดท้ายได้หรือไม่ และใช้เวลาเล็กน้อย>ในการวัดที่ดี

แต่สิ่งที่เกี่ยวกับการบุกรุก?

แล้วก็มีเรื่องของ$PS2เงื่อนไข ฉันแสดงให้เห็นก่อนหน้านี้ว่าวิธีนี้สามารถทำได้ซึ่งคุณยังสามารถหาได้ด้านล่าง - นี่คือปัญหาของขอบเขต แต่มีอะไรเพิ่มเติมอีกนิดหน่อยถ้าคุณไม่ต้องการที่จะเริ่มทำสprintf \bปอตและพยายามสร้างความสมดุลให้กับตัวละครของพวกเขา ดังนั้นฉันทำสิ่งนี้:

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

อีกครั้ง${parameter##expansion}บันทึกวัน มันแปลกเล็กน้อยที่นี่ - เราตั้งค่าตัวแปรในขณะที่เราถอดมันเอง เราใช้ค่าใหม่ - ตั้งแถบกลาง - เป็นกลมจากที่เราตัด เห็นไหม เราตัดทั้งหมดออกจากหัวของตัวแปรที่เพิ่มขึ้นของเรากับตัวละครที่ผ่านมาซึ่งได้อะไรจาก##* [$((PS2c=0))-9]เรารับประกันด้วยวิธีนี้ไม่ให้ส่งออกค่าและเรายังคงกำหนดมัน มันค่อนข้างเท่ห์ - ฉันไม่เคยทำมาก่อน แต่ POSIX ยังรับประกันเราว่านี่เป็นวิธีพกพาที่สุดที่สามารถทำได้

และต้องขอบคุณ POSIX ที่ระบุ${parameter} $((expansion))ซึ่งเก็บคำจำกัดความเหล่านี้ไว้ในเชลล์ปัจจุบันโดยไม่ต้องการให้เราตั้งค่าไว้ในเชลล์ย่อยที่แยกจากกันโดยไม่คำนึงถึงตำแหน่งที่เราประเมินพวกมัน และนี่คือเหตุผลที่มันทำงานในdashและshก็เช่นกันเช่นเดียวกับในและbash zshเราไม่ใช้การหลีกเลี่ยงเชลล์ / เทอร์มินัลและเราปล่อยให้ตัวแปรทดสอบตัวเอง นั่นคือสิ่งที่ทำให้โค้ดพกพารวดเร็ว

ส่วนที่เหลือค่อนข้างง่าย - เพียงแค่เพิ่มเคาน์เตอร์ของเราทุกครั้งที่$PS2มีการประเมินจนกว่าจะ$PS1รีเซ็ตอีกครั้ง แบบนี้:

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

ดังนั้นตอนนี้ฉันสามารถ:

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

มันทำงานเหมือนกันในbashหรือ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

ดังที่ฉันได้กล่าวไปแล้วปัญหาหลักคือคุณต้องพิจารณาว่าคุณทำการคำนวณของคุณที่ไหน คุณไม่ได้สถานะในพาเรนต์เชลล์ดังนั้นคุณจึงไม่ต้องคำนวณ คุณได้รับสถานะใน subshell - นั่นคือที่ที่คุณคำนวณ แต่คุณทำนิยามในเชลล์พาเรนต์

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
@mikeserv เรากำลังเปลี่ยนเป็นวงกลม ฉันรู้ทั้งหมดนี้แล้ว แต่ฉันจะใช้สิ่งนี้ในคำจำกัดความของฉันได้PS2อย่างไร นี่คือส่วนที่ยุ่งยาก ฉันไม่คิดว่าวิธีแก้ปัญหาของคุณสามารถใช้ได้ที่นี่ หากคุณคิดเป็นอย่างอื่นโปรดแสดงให้ฉันเห็นว่า
Konrad Rudolph

1
@mikeserv ไม่ไม่เกี่ยวข้องขอโทษ ดูคำถามของฉันสำหรับรายละเอียด PS1และPS2เป็นตัวแปรพิเศษในเชลล์ที่พิมพ์ออกมาเป็นพรอมต์คำสั่ง (ลองโดยการตั้งค่าPS1เป็นค่าที่แตกต่างกันในหน้าต่างเชลล์ใหม่) ดังนั้นมันจึงถูกใช้แตกต่างจากโค้ดของคุณมาก นี่คือข้อมูลเพิ่มเติมเกี่ยวกับการใช้งานของพวกเขา: linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KradradRudolph จะหยุดคุณจากการกำหนดพวกเขาสองครั้งอะไร? สิ่งที่เป็นต้นฉบับดั้งเดิมของฉันคืออะไร ... ฉันต้องดูคำตอบของคุณ ... เสร็จแล้วตลอดเวลา
mikeserv

1
@mikeserv พิมพ์echo 'thisพรอมต์จากนั้นอธิบายวิธีอัปเดตค่าPS2ก่อนพิมพ์คำพูดปิดเดี่ยว
chepner

1
ตกลงคำตอบนี้เป็นที่น่าอัศจรรย์อย่างเป็นทางการแล้ว ฉันชอบ breadcrumbs ด้วยแม้ว่าฉันจะไม่ยอมรับมันตั้งแต่ฉันพิมพ์เส้นทางแบบเต็มในบรรทัดแยกต่อไป: i.imgur.com/xmqrVxL.png
Konrad Rudolph

8

ด้วยวิธีนี้ (ฟังก์ชั่นที่ทำงานใน subshell) คุณจะไม่สามารถอัปเดตสถานะของกระบวนการของเชลล์หลักได้โดยไม่ต้องผ่านความขัดแย้ง ให้จัดเรียงฟังก์ชันให้ทำงานในกระบวนการหลักแทน

ค่าของPROMPT_COMMANDตัวแปรนั้นถูกอินเตอร์รัปต์เป็นคำสั่งที่ดำเนินการก่อนพิมพ์PS1พรอมต์

สำหรับPS2ไม่มีอะไรเทียบได้ แต่คุณสามารถใช้เคล็ดลับแทน: เนื่องจากสิ่งที่คุณต้องทำคือการดำเนินการทางคณิตศาสตร์คุณสามารถใช้การขยายเลขคณิตซึ่งไม่เกี่ยวข้องกับ subshell

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

ผลลัพธ์ของการคำนวณทางคณิตศาสตร์สิ้นสุดลงในพร้อมต์ หากคุณต้องการซ่อนคุณสามารถส่งเป็นตัวห้อยอาร์เรย์ที่ไม่มีอยู่ได้

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

4

เป็นบิต I / O มาก แต่คุณจะต้องใช้ไฟล์ชั่วคราวเพื่อเก็บค่าของการนับ

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

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

หากคุณกังวลเกี่ยวกับการต้องการไฟล์แยกต่างหากต่อเชลล์เซสชั่น (ซึ่งดูเหมือนว่าจะเป็นข้อกังวลเล็กน้อยคุณจะพิมพ์คำสั่งหลายบรรทัดในเชลล์ที่แตกต่างกันสองอันพร้อมกันหรือไม่?) คุณควรใช้mktempเพื่อสร้างไฟล์ใหม่สำหรับแต่ละไฟล์ ใช้.

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 อาจไม่สำคัญมากเนื่องจากหากไฟล์มีขนาดเล็กและมีการเข้าถึงบ่อยๆก็จะถูกแคชนั่นคือมันทำงานเป็นหน่วยความจำที่ใช้ร่วมกันเป็นหลัก
goldilocks

1

คุณไม่สามารถใช้ตัวแปรหอยด้วยวิธีนี้และคุณเข้าใจแล้วว่าทำไม subshell ตัวแปรสืบทอดตรงเช่นเดียวกับกระบวนการสืบทอดสภาพแวดล้อม: การเปลี่ยนแปลงใด ๆ ที่ทำใช้เฉพาะกับมันและเด็กและไม่ให้กระบวนการบรรพบุรุษใด ๆ

ตามคำตอบอื่น ๆ สิ่งที่ง่ายที่สุดที่จะทำคือเก็บข้อมูลนั้นไว้ในไฟล์

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

เป็นต้น


แน่นอนคุณสามารถตั้งค่าตัวแปรด้วยวิธีนี้ คุณไม่ต้องการไฟล์ชั่วคราว คุณตั้งค่าในตัวแปรในเชลล์ย่อยและพิมพ์ค่าลงในพาเรนต์เชลล์ซึ่งคุณดูดซับค่านั้น คุณได้รับสถานะทั้งหมดที่คุณต้องการคำนวณค่าของมันใน subshell เพื่อที่คุณจะทำมัน
mikeserv

1
@mikeserv นั่นไม่ใช่สิ่งเดียวกันซึ่งเป็นสาเหตุที่ OP ได้กล่าวถึงวิธีการแก้ปัญหาดังกล่าวจะไม่ทำงาน (แม้ว่าสิ่งนี้จะทำให้ชัดเจนขึ้นในคำถาม) สิ่งที่คุณอ้างถึงคือการส่งค่าไปยังกระบวนการอื่นผ่านIPCเพื่อให้สามารถกำหนดค่านั้นให้กับสิ่งใดก็ได้ สิ่งที่ OP ต้องการ / จำเป็นต้องทำคือส่งผลต่อค่าของตัวแปรโกลบอลที่แบ่งใช้โดยกระบวนการจำนวนหนึ่งและคุณไม่สามารถทำสิ่งนั้นผ่านสภาวะแวดล้อม มันไม่ได้มีประโยชน์มากสำหรับ IPC
goldilocks

ชายทั้งที่ฉันเข้าใจผิดว่าอะไรคือสิ่งที่ต้องการที่นี่หรือคนอื่นมี ดูเหมือนง่ายมากสำหรับฉัน คุณเห็นการแก้ไขของฉัน? มีอะไรผิดปกติกับมัน?
mikeserv

@mikeserv ฉันไม่คิดว่าคุณเข้าใจผิดและยุติธรรมสิ่งที่คุณมีคือ IPC และสามารถทำงานได้ มันไม่ชัดเจนให้ฉันทำไมคอนราดไม่ชอบมัน แต่ถ้ามันไม่ได้มีความยืดหยุ่นเพียงพอแล้วซ่อนไฟล์ตรงไปตรงสวย (และเพื่อให้มีวิธีการที่จะหลีกเลี่ยงการชนเช่นmktemp)
goldilocks

2
@mikeserv ฟังก์ชันที่ตั้งใจถูกเรียกใช้เมื่อค่าของPS2ถูกขยายโดยเชลล์ คุณไม่มีโอกาสอัปเดตค่าของตัวแปรในเชลล์พาเรนต์ในขณะนั้น
chepner

0

สำหรับการอ้างอิงต่อไปนี้เป็นวิธีแก้ปัญหาของฉันโดยใช้ไฟล์ชั่วคราวซึ่งไม่ซ้ำกันต่อกระบวนการเชลล์และลบโดยเร็วที่สุด (เพื่อหลีกเลี่ยงความยุ่งเหยิงดังที่กล่าวถึงในคำถาม):

# 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"
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.