เชลล์: ขึ้นบรรทัดใหม่ต่อท้าย ('\ n') ในการทดแทนคำสั่ง


14

ฉันต้องการที่จะสามารถที่จะจับเอาท์พุทแน่นอนของแทนคำสั่งรวมทั้งตัวอักษรต่อท้ายบรรทัดใหม่

ฉันรู้ว่าพวกเขาถูกปล้นโดยค่าเริ่มต้นดังนั้นการจัดการบางอย่างอาจจำเป็นต้องเก็บไว้และฉันต้องการเก็บรหัสทางออกดั้งเดิมไว้

ตัวอย่างเช่นกำหนดคำสั่งด้วยจำนวนตัวแปรของการขึ้นบรรทัดใหม่และรหัสออก:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

ฉันต้องการเรียกใช้บางสิ่งเช่น:

exact_output f

และมีผลลัพธ์เป็น:

Output: $'\n\n'
Exit: 5

ฉันสนใจทั้งในbashและ shPOSIX


1
ขึ้นบรรทัดใหม่เป็นส่วนหนึ่ง$IFSดังนั้นจะไม่ถูกบันทึกเป็นอาร์กิวเมนต์
Deathgrip

4
@Deathgrip มันมีอะไรจะทำอย่างไรกับIFS(ลอง( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" )เฉพาะการขึ้นบรรทัดใหม่ได้รับการปล้น.. \tและ `` ไม่ได้และIFSไม่ส่งผลกระทบมัน.
PSkocik



คำตอบ:


17

เปลือกหอย POSIX

เคล็ดลับ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ) เพื่อให้ได้ stdout ที่สมบูรณ์ของคำสั่งคือ:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

.\nความคิดที่จะเพิ่มและเสริม คำสั่งเปลี่ยนตัวจะตัดเฉพาะที่ \nและคุณตัดกับ.${output%.}

โปรดทราบว่าในเชลล์อื่น ๆ นอกเหนือจากzshนั้นจะยังคงใช้งานไม่ได้หากเอาต์พุตมี NUL ไบต์ ด้วยyashนั่นจะไม่ทำงานหากผลลัพธ์ไม่ใช่ข้อความ

นอกจากนี้โปรดทราบว่าในบางสถานที่มันมีความสำคัญต่ออักขระที่คุณใช้เพื่อแทรกในตอนท้าย .โดยทั่วไปควรจะดี แต่บางคนอาจไม่ ตัวอย่างเช่นx(ตามที่ใช้ในคำตอบอื่น ๆ ) หรือ@จะไม่ทำงานในภาษาที่ใช้ชุดอักขระ BIG5, GB18030 หรือ BIG5HKSCS ในชุดอักขระเหล่านั้นการเข้ารหัสของจำนวนอักขระจะสิ้นสุดในไบต์เดียวกับการเข้ารหัสของxหรือ@(0x78, 0x40)

ตัวอย่างเช่นūใน BIG5HKSCS คือ 0x88 0x78 (และx0x78 เหมือนกับใน ASCII ชุดอักขระทั้งหมดในระบบต้องมีการเข้ารหัสเหมือนกันสำหรับอักขระทั้งหมดของชุดอักขระแบบพกพาซึ่งรวมถึงตัวอักษรภาษาอังกฤษ@และ.) ดังนั้นถ้าcmdเป็นprintf '\x88'และเราแทรกxหลังจากที่มัน${output%x}จะล้มเหลวที่จะตัดที่xเป็นจริงจะมี$outputū

ใช้.แทนอาจนำไปสู่ปัญหาเดียวกันในทางทฤษฎีหากมีตัวละครที่มีการเข้ารหัสสิ้นสุดลงในการเข้ารหัสเดียวกันเป็น.แต่สำหรับการตรวจสอบบางเวลาที่ผ่านมาฉันสามารถบอกได้ว่าไม่มีตัวอักษรที่อาจจะพร้อมใช้งานในสถานที่ ระบบ Debian, FreeBSD หรือ Solaris มีตัวละครที่ดีพอสำหรับฉัน (และทำไมฉันถึงตัดสินด้วย.ซึ่งเป็นสัญลักษณ์ในการทำเครื่องหมายจุดสิ้นสุดของประโยคเป็นภาษาอังกฤษดังนั้นจึงดูเหมือนว่าเหมาะสม)

วิธีที่ถูกต้องมากขึ้นตามที่ได้กล่าวไว้โดย @Arrowจะเป็นการเปลี่ยนโลแคลเป็น C สำหรับการลอกอักขระตัวสุดท้าย ( ${output%.}) ซึ่งจะทำให้แน่ใจว่ามีเพียงหนึ่งไบต์ที่ถูกถอดออก แต่จะทำให้รหัสซับซ้อนและอาจทำให้เกิดปัญหาความเข้ากันได้ มันเอง

bash / zsh ทางเลือก

ด้วยbashและzshสมมติว่าเอาต์พุตไม่มี NUL คุณสามารถทำสิ่งต่อไปนี้

IFS= read -rd '' output < <(cmd)

เพื่อให้ได้ออกจากสถานะของcmdคุณสามารถทำwait "$!"; ret=$?ในแต่ไม่ได้อยู่ในbashzsh

RC / ES / akanaga

เพื่อความสมบูรณ์โปรดทราบว่าrc/ es/ akangaมีตัวดำเนินการสำหรับสิ่งนั้น ในพวกเขาทดแทนคำสั่งแสดงเป็น`cmd(หรือ`{cmd}สำหรับคำสั่งที่ซับซ้อนมากขึ้น) ส่งกลับรายการ (โดยแยกใน$ifsพื้นที่แท็บขึ้นบรรทัดใหม่ตามค่าเริ่มต้น) ในเชลล์เหล่านั้น (ตรงข้ามกับเชลล์เหมือนบอร์น) การลอกบรรทัดขึ้นบรรทัดใหม่จะทำได้ก็ต่อเมื่อเป็นส่วนหนึ่งของการ$ifsแยกเท่านั้น ดังนั้นคุณสามารถว่าง$ifsหรือใช้``(seps){cmd}แบบฟอร์มที่คุณระบุตัวคั่น:

ifs = ''; output = `cmd

หรือ:

output = ``()cmd

ไม่ว่าในกรณีใดสถานะการออกของคำสั่งจะหายไป คุณจะต้องฝังไว้ในผลลัพธ์และแยกออกหลังจากนั้นซึ่งจะน่าเกลียด

ปลา

ในปลาการแทนที่คำสั่งอยู่ด้วย(cmd)และไม่เกี่ยวข้องกับ subshell

set var (cmd)

สร้าง$varอาร์เรย์ที่มีบรรทัดทั้งหมดในเอาต์พุตของcmdif หาก$IFSไม่ว่างเปล่าหรือมีเอาต์พุตของสcmdไทรพด์สูงสุดหนึ่งบรรทัด(ตรงข้ามกับเชลล์ทั้งหมดในเชลล์อื่น ๆ ส่วนใหญ่) หาก$IFSว่างเปล่า

ดังนั้นยังคงมีปัญหาในการที่(printf 'a\nb')และขยายตัวออกไปในสิ่งเดียวกันแม้จะมีที่ว่างเปล่า(printf 'a\nb\n')$IFS

ในการหลีกเลี่ยงสิ่งที่ดีที่สุดที่ฉันสามารถทำได้คือ:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

อีกทางเลือกหนึ่งคือ:

read -z output < (begin; cmd; set ret $status; end | psub)

เชลล์เป้าหมาย

เชลล์เป้าหมายไม่สนับสนุน$(...)รูปแบบหรือตัว${var%pattern}ดำเนินการดังนั้นจึงค่อนข้างยากที่จะบรรลุเป้าหมาย วิธีหนึ่งคือใช้ eval และ quoting:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

ที่นี่เรากำลังสร้าง

output='output of cmd
with the single quotes escaped as '\''
';ret=X

evalจะถูกส่งผ่านไปยัง สำหรับวิธีการ POSIX ถ้า'เป็นหนึ่งในตัวละครที่ผู้ที่มีการเข้ารหัสสามารถพบได้ในตอนท้ายของตัวละครอื่น ๆ ที่เราต้องการมีปัญหา (กมากยิ่งหนึ่งที่มันจะกลายเป็นช่องโหว่ในการฉีดคำสั่ง) แต่โชคดีเช่น., มันไม่ใช่หนึ่งในนั้นและเทคนิคการอ้างถึงนั้นเป็นเทคนิคที่ใช้โดยอะไรก็ตามที่อ้างถึงรหัสเชลล์ (โปรดทราบว่า\มีปัญหาดังนั้นไม่ควรใช้ (ยกเว้น"..."ภายในซึ่งคุณต้องใช้แบ็กสแลชสำหรับอักขระบางตัว) ที่นี่เราจะใช้มันหลังจาก'ที่ตกลง)

tcsh

ดูtcsh รักษาบรรทัดใหม่ในการแทนที่คำสั่ง `... '

(ไม่ได้ดูแลสถานะการออกซึ่งคุณสามารถที่อยู่ได้โดยการบันทึกไว้ในไฟล์ชั่วคราว ( echo $status > $tempfile:qหลังจากคำสั่ง)


ขอบคุณ - และโดยเฉพาะอย่างยิ่งสำหรับเบาะแสเกี่ยวกับชุดอักขระที่แตกต่างกัน หากzshสามารถเก็บไว้NULในตัวแปรทำไมจะไม่IFS= read -rd '' output < <(cmd)ทำงาน มันจะต้องสามารถเก็บความยาวของสตริง ... มันเข้ารหัส''เป็นสตริง 1 ไบต์\0แทนที่จะเป็นสตริง 0 ไบต์หรือไม่
Tom Hale

1
@ TomHale ใช่read -d ''ได้รับการปฏิบัติเหมือนread -d $'\0'(ในbashเหมือนกันทุกที่$'\0'เหมือน''กัน)
Stéphane Chazelas

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

@Arrow ใช่var=value command evalเคล็ดลับถูกกล่าวถึงที่นี่ ( เช่น ) และในรายชื่อผู้รับจดหมายกลุ่มออสตินก่อน คุณจะพบว่ามันไม่ใช่แบบพกพา (และค่อนข้างชัดเจนเมื่อคุณลองทำสิ่งที่ชอบa=1 command eval 'unset a; a=2'หรือแย่กว่านั้นคือไม่ได้ใช้แบบนั้น) เช่นเดียวกันกับสิ่งsavedVAR=$VAR;...;VAR=$savedVARที่ไม่ได้ทำในสิ่งที่คุณต้องการเมื่อไม่$VARได้รับการตั้งค่าเริ่มต้น หากนั่นคือการหลีกเลี่ยงปัญหาทางทฤษฎีเท่านั้น (ข้อผิดพลาดที่ไม่สามารถตีได้ในทางปฏิบัติ) IMO จะไม่คุ้มค่าที่จะต้องรำคาญ ยังฉันจะสนับสนุนคุณสำหรับการลอง
Stéphane Chazelas

คุณมีลิงค์ไปยังที่ที่คุณยกเลิกและในที่สุดก็ยกเลิกการใช้งานLANG=Cเพื่อลบไบต์ออกจากสตริงหรือไม่? คุณกำลังเพิ่มข้อกังวลรอบจุดที่แท้จริงทั้งหมดนี้ง่ายต่อการแก้ไข (1) ไม่มีการใช้งาน (2) ทดสอบตัวแปรก่อนที่จะทำการเปลี่ยนแปลง @ StéphaneChazelas
ไอแซค

3

สำหรับคำถามใหม่สคริปต์นี้ใช้งานได้:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

ในการดำเนินการ:

Output:$'\n\n\n'
Exit :25
Done

คำอธิบายที่ยาว

ภูมิปัญญาปกติสำหรับ POSIX เชลล์ที่จะจัดการกับการลบ\nคือ:

เพิ่ม x

s=$(printf "%s" "${1}x"); s=${s%?}

ที่จำเป็นต้องมีเพราะสุดท้ายบรรทัดใหม่ ( S ) จะถูกลบออกจากการขยายคำสั่งต่อข้อกำหนด POSIX :

การลบลำดับของอักขระหนึ่งตัวขึ้นไปเมื่อสิ้นสุดการแทนที่


xเกี่ยวกับต่อท้าย

มันได้รับการกล่าวในคำถามนี้ว่าxอาจจะสับสนกับไบต์ต่อท้ายของตัวละครในการเข้ารหัส แต่เราจะเดาได้อย่างไรว่าตัวอักษรใดหรือดีกว่าในบางภาษาในการเข้ารหัสที่เป็นไปได้นั่นเป็นข้อเสนอที่ยากที่จะพูดน้อยที่สุด

อย่างไรก็ตาม; ที่เป็นเพียงที่ไม่ถูกต้อง

เพียงกฎที่เราต้องติดตามคือการเพิ่มว่าสิ่งที่เราลบ

มันควรจะง่ายต่อการเข้าใจว่าถ้าเราเพิ่มบางสิ่งบางอย่างในสตริงที่มีอยู่ (หรือลำดับไบต์) และหลังจากนั้นเราลบสิ่งเดียวกันแน่นอนสตริงเดิม (หรือลำดับไบต์) จะต้องเหมือนกัน

เราไปผิดที่ไหน เมื่อเราผสม ตัวอักษรและไบต์

ถ้าเราเพิ่มไบต์ที่เราต้องเอาไบต์ถ้าเราเพิ่มตัวอักษรที่เราต้องเอาตัวอักษรเดียวกันแน่นอน

ตัวเลือกที่สองการเพิ่มตัวอักษร (และลบตัวอักษรที่เหมือนกันในภายหลัง) อาจกลายเป็นซับซ้อนและซับซ้อนและใช่โค้ดเพจและการเข้ารหัสอาจเข้ามาขวางทาง

อย่างไรก็ตามตัวเลือกแรกนั้นเป็นไปได้มากและหลังจากอธิบายแล้วมันจะกลายเป็นเรื่องง่าย

ให้เพิ่มไบต์, ASCII ไบต์ (<127), และเพื่อให้สิ่งต่าง ๆ มีความซับซ้อนน้อยที่สุดเท่าที่เป็นไปได้, สมมุติว่าตัวอักษร ASCII อยู่ในช่วงของ az หรือที่เราควรจะบอกว่ามันเป็นไบต์ในช่วงฐานสิบหก-0x61 0x7aเลือกอย่างใดอย่างหนึ่งอาจจะเป็น x (เป็นมูลค่า0x78จริง ๆ ) เราสามารถเพิ่มไบต์ดังกล่าวด้วยการเชื่อม x เข้ากับสตริง (สมมติว่ามีé):

$ a
$ b=${a}x

ถ้าเราดูสตริงเป็นลำดับของไบต์เราจะเห็นว่า:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

ลำดับของสตริงที่ลงท้ายด้วย x

หากเราลบ x (ค่าไบต์0x78) เราจะได้รับ:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

มันทำงานได้โดยไม่มีปัญหา

ตัวอย่างที่ยากขึ้นอีกหน่อย

ให้บอกว่าสตริงที่เราสนใจลงท้ายด้วย byte 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

และให้เพิ่มไบต์ของค่า 0xa9

$ b=$a$'\xa9'

สตริงกลายเป็นสิ่งนี้ทันที:

$ echo "$b"
a test string é

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

หากเราลบอักขระสตริงเดิมจะถูกเปลี่ยน แต่นั่นไม่ใช่สิ่งที่เราเพิ่มเราเพิ่มค่าไบต์ซึ่งเกิดขึ้นกับการเขียนเป็น x แต่เป็นไบต์ต่อไป

สิ่งที่เราต้องหลีกเลี่ยงการตีความผิดไบต์เป็นตัวละคร 0xa9สิ่งที่เราต้องการคือการกระทำที่เอาไบต์ที่เราใช้ อันที่จริง, Ash, ทุบตี, lksh และ mksh ทั้งหมดดูเหมือนว่า:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

แต่ไม่ใช่ ksh หรือ zsh

อย่างไรก็ตามนั่นแก้ง่ายมากให้บอกshell ทั้งหมดว่าต้องทำการลบ byte:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

นั่นคือมันเชลล์ที่ทดสอบทั้งหมดทำงาน (ยกเว้น yash) (สำหรับส่วนสุดท้ายของสตริง):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

เพียงแค่ว่าง่ายบอกเปลือกเพื่อลบ LC_ALL = C ตัวละครซึ่งเป็นสิ่งหนึ่งไบต์ค่าไบต์ทั้งหมดจากการ0x000xff

โซลูชั่นสำหรับความคิดเห็น:

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

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

ที่จะลบปัญหาการเข้ารหัส


ดีใจที่รู้ว่าอาจมีการลบบรรทัดขึ้นบรรทัดใหม่มากกว่าหนึ่งบรรทัด
Tom Hale


ฉันยอมรับว่าการแก้ไขโลแคลเป็น C เพื่อให้แน่ใจว่า${var%?}แถบหนึ่งไบต์นั้นถูกต้องมากกว่าในทางทฤษฎี แต่: 1- LC_ALLและการLC_CTYPEแทนที่$LANGดังนั้นคุณจะต้องตั้งค่าLC_ALL=C2- คุณไม่สามารถทำvar=${var%?}ใน subshell เนื่องจากการเปลี่ยนแปลงจะ จะหายไปดังนั้นคุณจะต้องบันทึกและคืนค่าและสถานะของLC_ALL(หรือlocalเปลี่ยนเป็นคุณสมบัติขอบเขตที่ไม่ใช่ POSIX ) 3- การเปลี่ยนโลแคลตรงกลางผ่านสคริปต์ไม่ได้รับการสนับสนุนอย่างเต็มที่ในเชลล์บางตัวเช่น yash ในทางกลับกันในทางปฏิบัติ.ไม่มีปัญหาในชุดอักขระในชีวิตจริงดังนั้นการใช้มันจึงหลีกเลี่ยงการผสมกับ LC_ALL
Stéphane Chazelas

2

คุณสามารถออกอักขระหลังจากเอาท์พุทปกติแล้วตัดมัน:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

นี่เป็นโซลูชันที่สอดคล้องกับ POSIX


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