รับชื่อไฟล์โดยไม่มีนามสกุลใน Bash


6

ฉันมีดังต่อไปนี้ for วนเป็นรายบุคคล sort ไฟล์ข้อความทั้งหมดที่อยู่ในโฟลเดอร์ (เช่นสร้างไฟล์เอาต์พุตที่เรียงลำดับสำหรับแต่ละไฟล์)

for file in *.txt; 
do
   printf 'Processing %s\n' "$file"
   LC_ALL=C sort -u "$file" > "./${file}_sorted"  
done

มันเกือบจะสมบูรณ์แบบยกเว้นว่ามันจะส่งออกไฟล์ในรูปแบบของ:

originalfile.txt_sorted

... ในขณะที่ฉันต้องการส่งออกไฟล์ในรูปแบบ:

originalfile_sorted.txt 

นี่เป็นเพราะ ${file} ตัวแปรมีชื่อไฟล์รวมถึงส่วนขยาย ฉันใช้ Cygwin บน Windows ฉันไม่แน่ใจว่าสิ่งนี้จะทำงานได้อย่างไรในสภาพแวดล้อม Linux จริง แต่ใน Windows ส่วนขยายที่เปลี่ยนไปนี้ทำให้ Windows Explorer ไม่สามารถเข้าถึงไฟล์ได้

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

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

คำตอบ:


19

โซลูชันเหล่านี้ที่คุณเชื่อมโยงไปนั้นดีมาก คำตอบบางคำอาจขาดคำอธิบายดังนั้นลองเรียงลำดับออกมาเพิ่มอีกหน่อย

สายของคุณนี้

for file in *.txt

บ่งชี้ว่าส่วนขยายเป็นที่รู้จักกันล่วงหน้า (หมายเหตุ: สภาพแวดล้อมที่สอดคล้องกับ POSIX เป็นกรณี ๆ ไป *.txt จะไม่ตรงกัน FOO.TXT ) ในกรณีเช่นนี้

basename -s .txt "$file"

ควรคืนชื่อโดยไม่มีนามสกุล ( basename ยังลบเส้นทางไดเรกทอรี: /directory/path/filename & amp; rightarrow; filename; ในกรณีของคุณมันไม่สำคัญเพราะ $file ไม่มีเส้นทางดังกล่าว) ในการใช้เครื่องมือในรหัสของคุณคุณต้องทดแทนคำสั่งที่มีลักษณะดังนี้: $(some_command). การทดแทนคำสั่งใช้เอาต์พุตของ some_commandถือว่าเป็นสตริงและวางไว้ที่ใด $(…) คือ. การเปลี่ยนเส้นทางเฉพาะของคุณจะเป็น

… > "./$(basename -s .txt "$file")_sorted.txt"
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this

คำพูดซ้อนกันอยู่ที่นี่เพราะ Bash ฉลาดพอที่จะรู้คำพูดได้ $(…) ถูกจับคู่เข้าด้วยกัน

สิ่งนี้สามารถปรับปรุงได้ บันทึก basename เป็นไฟล์ปฏิบัติการแยกต่างหากไม่ใช่เชลล์ในตัว (ใน Bash run type basename, เปรียบเทียบกับ type cd ) การวางกระบวนการพิเศษใด ๆ เป็นค่าใช้จ่ายมันต้องใช้ทรัพยากรและเวลา การวางไข่แบบวนซ้ำมักจะทำงานได้ไม่ดี ดังนั้นคุณควรใช้อะไรก็ตามที่เชลล์เสนอให้คุณเพื่อหลีกเลี่ยงกระบวนการพิเศษ ในกรณีนี้การแก้ปัญหาคือ:

… > "./${file%.txt}_sorted.txt"

ไวยากรณ์อธิบายไว้ด้านล่างสำหรับกรณีทั่วไปที่มากกว่า


ในกรณีที่คุณไม่รู้จักส่วนขยาย:

… > "./${file%.*}_sorted.${file##*.}"

ไวยากรณ์อธิบาย:

  • ${file#*.} - $fileแต่การจับคู่สตริงที่สั้นที่สุด *. ถูกลบออกจากด้านหน้า
  • ${file##*.} - $fileแต่การจับคู่สตริงที่ยาวที่สุด *. ถูกลบออกจากด้านหน้า ใช้มันเพื่อรับส่วนขยาย
  • ${file%.*} - $fileแต่การจับคู่สตริงที่สั้นที่สุด .* จะถูกลบออกจากจุดสิ้นสุด; ใช้มันเพื่อรับทุกอย่างยกเว้นการขยาย
  • ${file%%.*} - $fileแต่ด้วยการจับคู่สตริงที่ยาวที่สุด .* จะถูกลบออกจากจุดสิ้นสุด;

การจับคู่รูปแบบเหมือนกลมไม่ใช่ regex ซึ่งหมายความว่า * เป็นสัญลักษณ์แทนสำหรับศูนย์หรือมากกว่าตัวอักษร ? เป็นอักขระตัวแทนสำหรับอักขระหนึ่งตัว (เราไม่ต้องการ ? ในกรณีของคุณแม้ว่า) เมื่อคุณวิงวอน ls *.txt หรือ for file in *.txt; คุณกำลังใช้กลไกการจับคู่รูปแบบเดียวกัน อนุญาตให้ใช้รูปแบบที่ไม่มีอักขระแทน เราได้ใช้ไปแล้ว ${file%.txt} ที่ไหน .txt เป็นรูปแบบ

ตัวอย่าง:

$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name

แต่ระวัง:

$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless

ด้วยเหตุนี้การคุมกำเนิดต่อไปนี้ อาจ มีประโยชน์ (แต่ไม่ใช่คำอธิบายด้านล่าง):

${file#${file%.*}}

มันทำงานได้โดยระบุทุกอย่างยกเว้นส่วนขยาย ( ${file%.*} ) จากนั้นลบสิ่งนี้ออกจากสตริงทั้งหมด ผลลัพธ์เป็นดังนี้:

$ file=name.name2.name3.ext
$ echo "${file#${file%.*}}"
.ext
$ file=extensionless
$ echo "${file#${file%.*}}"

$   # empty output above

หมายเหตุ . รวมอยู่ในเวลานี้ คุณอาจได้รับผลลัพธ์ที่ไม่คาดคิดถ้า $file มีตัวอักษร * หรือ ?; แต่ Windows (ในกรณีที่ส่วนขยายสำคัญ) ไม่อนุญาต ตัวละครเหล่านี้ในชื่อไฟล์อย่างไรก็ตามคุณอาจไม่สนใจ อย่างไรก็ตาม […] หรือ {…}หากมีอยู่อาจเปิดใช้รูปแบบการจับคู่รูปแบบของตนเองและทำลายโซลูชัน!

การเปลี่ยนเส้นทาง "ที่ดีขึ้น" ของคุณจะเป็น:

… > "./${file%.*}_sorted${file#${file%.*}}"

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

การเปลี่ยนเส้นทางที่ดีขึ้นจริงๆ:

… > "./${file%.*}_sorted${file#"${file%.*}"}"

การอ้างอิงสองครั้งทำให้ ${file%.*} ไม่ทำหน้าที่เป็นรูปแบบ! Bash นั้นฉลาดพอที่จะบอกราคาจากภายในและภายนอกได้เพราะราคาด้านในฝังอยู่ด้านนอก ${…} วากยสัมพันธ์ ฉันคิดว่านี่เป็นวิธีที่ถูกต้อง .

อีกโซลูชัน (ไม่สมบูรณ์) ลองวิเคราะห์ด้วยเหตุผลทางการศึกษา:

${file/./_sorted.}

มันแทนที่ก่อน . กับ _sorted.. มันจะทำงานได้ดีถ้าคุณมีจุดมากที่สุดหนึ่งจุด $file. มีไวยากรณ์ที่คล้ายกัน ${file//./_sorted.} ที่แทนที่ทุกจุด เท่าที่ฉันรู้ไม่มีตัวแปรที่จะแทนที่ สุดท้าย จุดเท่านั้น

ยังคงเป็นโซลูชั่นเริ่มต้นสำหรับไฟล์ด้วย . ดูแข็งแกร่ง ทางออกสำหรับการขยาย $file ไม่สำคัญ: ${file}_sorted. ตอนนี้สิ่งที่เราต้องการคือวิธีบอกสองกรณี นี่มันคือ:

[[ "$file" == *?.* ]]

จะส่งกลับสถานะการออก 0 (จริง) ถ้าหากเนื้อหาของ $file ตัวแปรที่ตรงกับรูปแบบด้านขวามือ รูปแบบกล่าวว่า "มีจุดหลังจากตัวละครอย่างน้อยหนึ่งตัว" หรือเทียบเท่า "มีจุดที่ไม่ใช่จุดเริ่มต้น" ประเด็นคือการจัดการกับไฟล์ที่ซ่อนอยู่ของ Linux (เช่น .bashrc ) เป็นส่วนขยายเว้นแต่จะมี อื่น จุดที่ใดที่หนึ่ง

หมายเหตุที่เราต้องการ [[ ที่นี่ไม่ [. อดีตมีพลังมากขึ้น แต่น่าเสียดาย ไม่พกพา ; หลังมีขนาดเล็ก แต่ จำกัด สำหรับเรา

ตรรกะตอนนี้จะเป็นดังนี้:

[[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"

หลังจากนี้, $file1 มีชื่อที่ต้องการดังนั้นการเปลี่ยนเส้นทางของคุณควรเป็น

… > "./$file1"

และข้อมูลโค้ดทั้งหมด ( *.txt แทนที่ด้วย * เพื่อระบุว่าเราทำงานกับส่วนขยายใด ๆ หรือไม่มีส่วนขยาย):

for file in *; 
do
   printf 'Processing %s\n' "$file"
   [[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"
   LC_ALL=C sort -u "$file" > "./$file1"  
done

นี่จะพยายามประมวลผลไดเรกทอรี (ถ้ามี) เช่นกัน คุณรู้อยู่แล้วว่า สิ่งที่ต้องทำ เพื่อแก้ไข


อีกครั้งคำตอบที่ยอดเยี่ยมขอบคุณ ฉันอยู่ไกลจากการทำความเข้าใจทุกอย่างแน่นอน แต่สำหรับตอนนี้ฉันจะจากไปด้านหนึ่งและอ่านข้อมูลเพิ่มเติมเกี่ยวกับการทดแทนคำสั่งเมื่อฉันมีเวลา ฉันมีคำถามหนึ่งข้อ: คุณพูดถึงเรื่องนั้น … > "./${file%.txt}_sorted.txt" "หลีกเลี่ยงกระบวนการพิเศษ" - เป็นเพราะเราใช้ basename ใน $file ตัวแปรภายนอก for วนที่นี่: basename -s .txt "$file"... หรือฉันเข้าใจผิด?
Hashim

@Hashim … > "./${file%.txt}_sorted.txt" เป็นการเปลี่ยนแปลงเพียงอย่างเดียวที่คุณต้องทำกับสคริปต์ของคุณ (จุดไข่ปลา เพียงระบุทุกสิ่งที่คุณมีมาก่อน >มันเป็น ไม่ อักขระจริงที่คุณควรใส่ไว้ในสคริปต์ของคุณ แทนที่ > และส่วนที่เหลือของบรรทัดด้วย > "./${file%.txt}_sorted.txt" ) มันหลีกเลี่ยงกระบวนการพิเศษเพราะตอนนี้เราไม่ได้ใช้ basename เลย ; เปลือกเวทมนตร์ทำโดยตัวของมันเองขอบคุณ ${file%.txt} วากยสัมพันธ์ หมายเหตุด้านข้าง: แต่เพียงผู้เดียว basename -s .txt "$file" แค่พิมพ์บางอย่าง หากคุณคิดว่ามันเปลี่ยนแปลงตัวแปรคุณผิด
Kamil Maciorowski

อาดังนั้นการแทนที่คำสั่งจะถูกใช้แทน basename มากกว่าอยู่ข้างๆ ฉันเห็น. ขอบคุณอีกครั้งสำหรับความช่วยเหลือของคุณ
Hashim

1
@Hashim ไม่มาก ส่วนนี้ > "./$(basename -s .txt "$file")_sorted.txt" ใช้การทดแทนคำสั่งคำสั่งคือ basename …. คุณสามารถใช้สิ่งนี้หรือ > "./${file%.txt}_sorted.txt" ซึ่งไม่ใช้การทดแทนคำสั่ง มันคือ (การทดแทนคำสั่ง + basename ) xor การขยายตัวของตัวแปรแฟนซี ${file%.txt} โดยไม่มีการทดแทนคำสั่ง
Kamil Maciorowski

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