การทดแทนคำสั่ง: การแยกบรรทัดใหม่ แต่ไม่ใช่ที่ว่าง


30

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

ฉันมีไฟล์ที่มีเนื้อหาเช่น

AAA
B C DDD
FOO BAR

โดยที่ฉันหมายความว่ามันมีหลายบรรทัดเท่านั้นและแต่ละบรรทัดอาจมีหรือไม่มีช่องว่างก็ได้ ฉันต้องการเรียกใช้คำสั่งเช่น

cmd AAA "B C DDD" "FOO BAR"

ถ้าฉันใช้cmd $(< file)ฉันจะได้รับ

cmd AAA B C DDD FOO BAR

และถ้าฉันใช้cmd "$(< file)"ฉันจะได้รับ

cmd "AAA B C DDD FOO BAR"

ฉันจะให้แต่ละบรรทัดปฏิบัติกับพารามิเตอร์หนึ่งตัวได้อย่างไร


คำตอบ:


26

portably:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

หรือใช้เชลล์ย่อยเพื่อทำให้IFSตัวเลือกและเปลี่ยนภายใน:

( set -f; IFS='
'; exec cmd $(cat <file) )

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

มีไม่มากที่จะได้รับจากการสร้าง bash หรือ ksh คุณสามารถทำให้IFSท้องถิ่นเพื่อฟังก์ชั่น set -fแต่ไม่ได้

ใน bash หรือ ksh93 คุณสามารถจัดเก็บเขตข้อมูลในอาร์เรย์ได้หากคุณต้องการส่งผ่านไปยังหลายคำสั่ง คุณต้องควบคุมการขยายตัวในเวลาที่คุณสร้างอาร์เรย์ จากนั้น"${a[@]}"ขยายไปยังองค์ประกอบของอาร์เรย์หนึ่งรายการต่อคำ

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

คุณสามารถทำได้ด้วยอาร์เรย์ชั่วคราว

ติดตั้ง:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

เติมอาร์เรย์:

$ IFS=$'\n'; set -f; foo=($(<input))

ใช้อาร์เรย์:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

ไม่สามารถหาวิธีในการทำเช่นนั้นได้โดยไม่มีตัวแปรชั่วคราว - ยกเว้นการIFSเปลี่ยนแปลงที่ไม่สำคัญสำหรับcmdกรณีนี้:

$ IFS=$'\n'; set -f; cmd $(<input) 

ควรทำมัน


IFSทำให้ฉันสับสนอยู่เสมอ IFS=$'\n' cmd $(<input)ไม่ทำงาน IFS=$'\n'; cmd $(<input); unset IFSทำงานได้ดี ทำไม? ฉันเดาว่าฉันจะใช้(IFS=$'\n'; cmd $(<input))
โปรเก่า

6
@OldPro IFS=$'\n' cmd $(<input)ไม่ทำงานเพราะมันชุดเท่านั้นในสภาพแวดล้อมของIFS ถูกขยายเพื่อจัดรูปแบบคำสั่งก่อนที่จะทำการมอบหมายให้ทำ cmd$(<input)IFS
Gilles 'หยุดความชั่วร้าย' ใน

8

ดูเหมือนว่าวิธีการที่เป็นที่ยอมรับในการทำเช่นนี้bashเป็นสิ่งที่ต้องการ

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

หรือถ้า bash เวอร์ชันของคุณมีmapfile:

mapfile -t args < filename
cmd "${args[@]}"

ความแตกต่างเพียงอย่างเดียวที่ฉันสามารถหาได้ระหว่าง mapfile และลูปแบบอ่านขณะเปรียบเทียบกับแบบหนึ่งบรรทัด

(set -f; IFS=$'\n'; cmd $(<file))

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

ฉันจะใช้IFS=$'\n' cmd $(<file)แต่มันใช้งานไม่ได้เพราะ$(<file)ถูกตีความให้เป็นบรรทัดคำสั่งก่อนที่IFS=$'\n'จะมีผล

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

find / -name '*.config' -print0 | xargs -0 md5

ฟีดรายการชื่อไฟล์ที่ผ่านการรับรองโดยสมบูรณ์เป็นอาร์กิวเมนต์สำหรับ md5 โดยไม่มีการวนซ้ำหรือการสอดแทรกหรืออะไรก็ตาม ที่นำไปสู่การแก้ปัญหาที่ไม่ได้อยู่ในตัว

tr "\n" "\000" <file | xargs -0 cmd

ถึงแม้ว่าสิ่งนี้จะละเว้นบรรทัดว่างแม้ว่ามันจะจับเส้นที่มีพื้นที่ว่างเท่านั้น


การใช้cmd $(<file)ค่าที่ไม่มีข้อความ (การใช้ความสามารถในการทุบคำเพื่อแยกคำ) เป็นการเดิมพันที่มีความเสี่ยงเสมอ หากบรรทัดใด ๆ*มันจะถูกขยายโดยเชลล์ไปยังรายการไฟล์

3

คุณสามารถใช้ bash ในตัวmapfileเพื่ออ่านไฟล์ในอาเรย์

mapfile -t foo < filename
cmd "${foo[@]}"

หรือไม่ได้ทดสอบxargsอาจทำ

xargs cmd < filename

จากเอกสาร mapfile: "mapfile ไม่ใช่คุณสมบัติทั่วไปหรือพกพาเชลล์" และมันไม่รองรับในระบบของฉัน xargsไม่ช่วยเช่นกัน
โปรเก่า

คุณจะต้องการxargs -dหรือxargs -L
James Youngman

@ James ไม่ฉันไม่มี-dตัวเลือกและxargs -L 1เรียกใช้คำสั่งหนึ่งครั้งต่อบรรทัด แต่ก็ยังแยก args บน whitespace
โปรรุ่นเก่า

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

mapfileมีประโยชน์มากสำหรับฉันเพราะมันจับเส้นว่างเป็นรายการอาร์เรย์ซึ่งIFSวิธีการไม่ทำ IFSปฏิบัติต่อบรรทัดใหม่ที่ต่อเนื่องกันเป็นตัวคั่นเดียว ... ขอบคุณที่แสดงมันเนื่องจากฉันไม่ทราบคำสั่ง (แม้ว่าขึ้นอยู่กับข้อมูลอินพุตของ OP และบรรทัดคำสั่งที่คาดไว้ดูเหมือนว่าจริง ๆ แล้วเขาต้องการละเว้นบรรทัดว่าง)
Peter.O

0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

วิธีที่ดีที่สุดที่ฉันสามารถหา เพิ่งได้ผล


และทำไมมันถึงใช้งานได้ถ้าคุณตั้งIFSให้มีที่ว่าง แต่คำถามคือไม่ต้องแยกออกจากที่ว่าง?
RalfFriedl

0

ไฟล์

การวนรอบขั้นพื้นฐาน (พกพา) เพื่อแยกไฟล์บนบรรทัดใหม่คือ:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

สิ่งที่จะพิมพ์:

$ ./script
<AAA><A B C><DE F>

ใช่กับไอเอฟเอเริ่มต้นspacetabnewline=

ทำไมมันถึงได้ผล

  • IFS จะถูกใช้โดยเชลล์เพื่อแยกอินพุตเป็นตัวแปรหลายตัว เนื่องจากมีเพียงตัวแปรเดียวจึงไม่มีการแยกโดยเชลล์ ดังนั้นไม่IFSจำเป็นต้องเปลี่ยน
  • ใช่ช่องว่าง / แท็บนำหน้าและต่อท้ายกำลังถูกลบออกไป แต่ดูเหมือนจะไม่มีปัญหาในกรณีนี้
  • ไม่มีglobbingจะทำอย่างที่ไม่มีการขยายตัวจะunquoted ดังนั้นไม่set -fจำเป็น
  • อาเรย์เดียวที่ใช้ (หรือจำเป็น) คือพารามิเตอร์ตำแหน่งเหมือนอาเรย์
  • -r(ดิบ) ตัวเลือกคือการหลีกเลี่ยงการกำจัดของทับขวามากที่สุด

สิ่งนี้จะไม่ทำงานหากจำเป็นต้องแยกและ / หรือกลมกลืน ในกรณีเช่นนี้จำเป็นต้องมีโครงสร้างที่ซับซ้อนมากขึ้น

หากคุณต้องการ (ยังพกพา) ไปที่:

  • หลีกเลี่ยงการลบช่องว่าง / แท็บนำหน้าและส่วนท้ายใช้: IFS= read -r line
  • สายแยก vars IFS=':' read -r a b cตัวละครบางใช้:

แบ่งไฟล์ออกเป็นอักขระอื่น ๆ (ไม่ใช่แบบพกพาใช้งานได้กับ ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

การขยายตัว

แน่นอนชื่อคำถามของคุณเกี่ยวกับการแบ่งคำสั่งในการขึ้นบรรทัดใหม่โดยหลีกเลี่ยงการแบ่งช่องว่าง

วิธีเดียวที่จะได้รับการแยกออกจากเปลือกคือการปล่อยให้ขยายโดยไม่มีคำพูด:

echo $(< file)

ที่ถูกควบคุมโดยค่าของ IFS และในการขยายที่ไม่ได้กล่าวถึงนั้นจะมีการใช้การวนรอบด้วย หากต้องการ mkae ที่ทำงานคุณต้อง:

  • ตั้งค่า IFS เป็นบรรทัดใหม่เท่านั้นเพื่อแยกบรรทัดใหม่เท่านั้น
  • ยกเลิกการตั้งค่าตัวเลือก globbing shell set +f:

    set + f IFS = '' cmd $ (<ไฟล์)

แน่นอนว่าการเปลี่ยนแปลงค่าของ IFS และการโค้งไปตามส่วนที่เหลือของสคริปต์

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