POSIX-ly จะนับจำนวนบรรทัดในตัวแปรสตริงได้อย่างไร


10

ฉันรู้ว่าฉันสามารถทำได้ใน Bash:

wc -l <<< "${string_variable}"

โดยพื้นฐานแล้วทุกสิ่งที่ฉันพบเกี่ยวข้องกับ<<<ตัวดำเนินการ Bash

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

คำตอบ:


11

คำตอบง่ายๆก็คือว่าwc -l <<< "${string_variable}"เป็น ksh / ทุบตี / zsh printf "%s\n" "${string_variable}" | wc -lทางลัดสำหรับ

จริงๆแล้วมีความแตกต่างในทาง<<<และไปป์ทำงาน: <<<สร้างไฟล์ชั่วคราวที่ส่งผ่านเป็นอินพุตไปยังคำสั่งในขณะที่|สร้างไปป์ ใน bash และ pdksh / mksh (แต่ไม่ใช่ ksh93 หรือ zsh) คำสั่งทางด้านขวาของไปป์นั้นจะทำงานใน subshell แต่ความแตกต่างเหล่านี้ไม่สำคัญในกรณีนี้

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

มีความแตกต่างสองประการระหว่างvar=$(somecommand); wc -l <<<"$var"และsomecommand | wc -l: การใช้การแทนที่คำสั่งและตัวแปรชั่วคราวจะลบบรรทัดว่างที่ท้ายให้ลืมว่าบรรทัดสุดท้ายของเอาต์พุตจบลงด้วยการขึ้นบรรทัดใหม่หรือไม่ และเกินจำนวนหนึ่งถ้าเอาท์พุทว่างเปล่า หากคุณต้องการที่จะรักษาผลลัพธ์และนับบรรทัดคุณสามารถทำได้โดยการต่อท้ายข้อความที่รู้จักกันและการลอกออกในตอนท้าย:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"

1
@Inian Keeping wc -lมีค่าเทียบเท่ากับต้นฉบับ: <<<$fooเพิ่มบรรทัดใหม่ให้กับค่าของ$foo(แม้ว่าจะ$fooว่างเปล่า) ฉันอธิบายในคำตอบของฉันว่าทำไมสิ่งนี้อาจไม่ใช่สิ่งที่ต้องการ แต่เป็นสิ่งที่ถูกถาม
Gilles 'หยุดความชั่วร้าย'

2

ไม่สอดคล้องกับเชลล์ในตัวโดยใช้ยูทิลิตีภายนอกเช่นgrepและawkด้วยตัวเลือกที่สอดคล้องกับ POSIX

string_variable="one
two
three
four"

ทำgrepเพื่อให้ตรงกับจุดเริ่มต้นของบรรทัด

printf '%s' "${string_variable}" | grep -c '^'
4

และด้วย awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

โปรดทราบว่าเครื่องมือ GNU บางตัวโดยเฉพาะ GNU grepไม่เคารพPOSIXLY_CORRECT=1ตัวเลือกในการเรียกใช้เครื่องมือรุ่น POSIX ในgrepพฤติกรรมเดียวที่ได้รับผลกระทบจากการตั้งค่าตัวแปรจะเป็นความแตกต่างในการประมวลผลคำสั่งของการตั้งค่าสถานะบรรทัดคำสั่ง จากเอกสาร ( grepคู่มือGNU ) ดูเหมือนว่า

POSIXLY_CORRECT

หากตั้งค่า grep จะทำงานตามที่ POSIX ต้องการ มิฉะนั้น grepจะทำงานเหมือนโปรแกรม GNU อื่น ๆ POSIX ต้องการให้ตัวเลือกที่ตามหลังชื่อไฟล์นั้นต้องได้รับการปฏิบัติเสมือนเป็นชื่อไฟล์; โดยค่าเริ่มต้นตัวเลือกดังกล่าวจะได้รับอนุญาตด้านหน้าของรายการตัวถูกดำเนินการและจะถือเป็นตัวเลือก

ดูวิธีใช้ POSIXLY_CORRECT ใน grep


2
แน่นอนwc -lยังทำงานได้ที่นี่?
Michael Homer

@MichaelHomer: จากสิ่งที่ฉันสังเกตเห็นwc -lต้องใช้สตรีมที่คั่นด้วยบรรทัดใหม่ที่เหมาะสม (มีท้าย '\ n` ท้ายเพื่อนับอย่างถูกต้อง) หนึ่งไม่สามารถใช้ง่าย FIFO ที่จะใช้กับprintfเช่นprintf '%s' "${string_variable}" | wc -lอาจจะไม่ทำงานตามที่คาดไว้ แต่<<<จะเพราะต่อท้าย\nผนวกโดย herestring
Inian

1
นั่นคือสิ่งที่printf '%s\n'กำลังทำอยู่ก่อนที่คุณจะหยิบมันออกมา ...
Michael Homer

1

ที่นี่สตริงสวยมากรุ่นหนึ่งบรรทัดของที่นี่เอกสาร<<< <<อดีตไม่ใช่คุณสมบัติมาตรฐาน แต่อย่างหลังคือ คุณสามารถใช้<<ในกรณีนี้ได้เช่นกัน สิ่งเหล่านี้ควรเทียบเท่า:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

แม้ว่าโปรดทราบว่าทั้งสองเพิ่มบรรทัดใหม่พิเศษในตอนท้ายของ$somevarเช่นพิมพ์นี้6แม้ว่าตัวแปรมีเพียงห้าบรรทัด:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

ด้วยprintfคุณสามารถตัดสินใจได้ว่าคุณต้องการขึ้นบรรทัดใหม่หรือไม่:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

แต่โปรดทราบว่าwcจะนับเฉพาะจำนวนบรรทัดทั้งหมด (หรือจำนวนอักขระบรรทัดใหม่ในสตริง) grep -c ^ควรนับส่วนของบรรทัดสุดท้ายด้วย

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(แน่นอนคุณสามารถนับจำนวนบรรทัดทั้งหมดในเชลล์โดยใช้การ${var%...}ขยายเพื่อลบทีละครั้งในลูป ... )


0

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

ตัวอย่างเช่นต่อไปนี้เป็นฟังก์ชั่นเชลล์เล็ก ๆ ที่รวมบรรทัดที่ไม่ว่างภายในอาร์กิวเมนต์ที่ให้มาทั้งหมด:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

วงเล็บไม่ใช่วงเล็บปีกกาใช้ที่นี่เพื่อจัดรูปแบบคำสั่งผสมสำหรับเนื้อความฟังก์ชัน สิ่งนี้ทำให้ฟังก์ชั่นดำเนินการใน subshell เพื่อที่จะไม่ก่อให้เกิดมลพิษ IFS ของโลกภายนอกและการตั้งค่าการขยายชื่อพา ธ ในทุกการโทร

หากคุณต้องการวนซ้ำบรรทัดที่ไม่ว่างคุณสามารถทำได้เช่นเดียวกัน:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

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

ตัวอย่างเช่นหากคุณกำลังใช้ตัวแปรเพื่อสร้างบรรทัดคำสั่งที่ซับซ้อนสำหรับบางอย่างเช่นffmpegคุณอาจต้องการรวม-vf scale=$scaleเฉพาะเมื่อตัวแปรscaleถูกตั้งค่าเป็นสิ่งที่ไม่ว่างเปล่า โดยปกติคุณสามารถทำได้ด้วย${scale:+-vf scale=$scale}แต่ถ้า IFS ไม่รวมอักขระช่องว่างตามปกติในขณะที่การขยายพารามิเตอร์เสร็จสิ้นช่องว่างระหว่าง-vfและscale=จะไม่ถูกใช้เป็นตัวคั่นคำและffmpegจะถูกส่งผ่านทั้งหมด-vf scale=$scaleเป็นอาร์กิวเมนต์เดียว ซึ่งมันจะไม่เข้าใจ

ในการแก้ไขปัญหาที่คุณจะต้องอย่างใดอย่างหนึ่งเพื่อให้แน่ใจว่าไอเอฟเอได้รับการตั้งค่าอื่น ๆ ได้ตามปกติก่อนที่จะทำในการขยายตัวหรือทำสองขยาย:${scale} ${scale:+-vf} ${scale:+scale=$scale}คำแบ่งที่เชลล์ทำในกระบวนการแยกวิเคราะห์บรรทัดคำสั่งเริ่มต้นซึ่งตรงข้ามกับการแยกในระหว่างขั้นตอนการขยายการประมวลผลบรรทัดคำสั่งเหล่านั้นไม่ได้ขึ้นอยู่กับ IFS

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

t=' '
n='
'

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

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

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

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

ในกรณีนี้readบิวด์อินเห็น IFS ตั้งค่าเป็นเพียงแท็บดังนั้นมันจะไม่แยกบรรทัดอินพุตที่อ่านบนช่องว่างด้วย แต่IFS=$t set -- $lines ไม่ได้ผล: เชลล์ขยาย$linesเมื่อสร้างsetอาร์กิวเมนต์ของบิวด์ก่อนที่จะดำเนินการคำสั่งดังนั้นการตั้งค่าชั่วคราวของ IFS ในลักษณะที่ใช้เฉพาะระหว่างการดำเนินการของบิวด์อินเองนั้นมาสายเกินไป นี่คือสาเหตุที่โค้ดขนาดเล็กที่ฉันได้ให้ไว้เหนือชุด IFS ทั้งหมดในขั้นตอนที่แยกต่างหากและสาเหตุที่พวกเขาต้องจัดการกับปัญหาของการเก็บรักษาไว้

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