การสร้างสคริปต์ BASH `for` จัดการชื่อไฟล์ด้วยช่องว่าง (หรือวิธีแก้ปัญหา)


12

ในขณะที่ฉันใช้ BASH มาหลายปีประสบการณ์ของฉันกับการเขียนสคริปต์ BASH ค่อนข้าง จำกัด

รหัสของฉันเป็นดังนี้ $OUTDIRมันควรจะคว้าโครงสร้างไดเรกทอรีทั้งจากภายในไดเรกทอรีปัจจุบันและทำซ้ำมันเข้าไป

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

ปัญหาคือนี่คือตัวอย่างของโครงสร้างไฟล์ของฉัน:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

สังเกตช่องว่าง: -S และforรับพารามิเตอร์ทีละคำดังนั้นเอาต์พุตของสคริปต์ของฉันจะเป็นดังนี้:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

แต่ผมต้องการที่จะคว้าทั้งชื่อไฟล์ (หนึ่งบรรทัดในเวลา) findจากการส่งออกของ ฉันได้ลองfindใส่เครื่องหมายคำพูดคู่รอบชื่อไฟล์แต่ละรายการ แต่มันก็ไม่ได้ช่วยอะไร

for DIR in `find . -type d -printf "\"%P\"\040"`

และเอาต์พุตด้วยบรรทัดที่เปลี่ยนแปลงนี้:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

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

แก้ไข:ฉันต้องการโครงสร้างโค้ดที่จะอนุญาตให้ฉันรันโค้ดหลายบรรทัดสำหรับแต่ละไดเร็กทอรี / ไฟล์ / ลูป ขออภัยถ้าฉันไม่ชัดเจน

วิธีแก้ปัญหา:ฉันพยายามครั้งแรก:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

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

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

ซึ่งภายหลังทำให้ฉันสามารถเพิ่มตัวแปรตามเงื่อนไขภายในลูปซึ่งจะคงอยู่ในสคริปต์ต่อไป


Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko

จริงไม่ใช่ความชอบของฉัน แม้ว่าในการลบช่องว่างคุณจำเป็นต้องจัดการกับไฟล์ด้วยช่องว่างก่อน;)
Samuel Jaeschke

1
จริงๆแล้วชื่อไฟล์ควรอนุญาตให้เว้นวรรค ฉันจะอนุญาตให้ทุกอย่างยกเว้น/ตัวอักษรที่ไม่สามารถพิมพ์ได้ แต่สิ่งใดก็ตามที่ได้รับอนุญาตยกเว้น/และ\0ดังนั้นคุณต้องอนุญาต
Kevin Panko

คำตอบ:


11

คุณต้องไปป์findเป็นwhileวง

find ... | while read -r dir
do
    something with "$dir"
done

นอกจากนี้คุณไม่จำเป็นต้องใช้-printfในกรณีนี้

คุณสามารถสร้างหลักฐานนี้กับไฟล์ที่มีบรรทัดใหม่ในชื่อของพวกเขาหากคุณต้องการโดยใช้ตัวคั่น nullbyte (ซึ่งเป็นอักขระตัวเดียวที่ไม่สามารถปรากฏในพา ธ * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

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

echo "$(echo "$(echo "hello")")"

ลองทำด้วย backticks


2
นอกจากนี้แทนที่จะ"$dir"ใช้ดีกว่า"${dir}"- ง่ายที่จะบอกความแตกต่างระหว่าง $ {dir} ชื่อและ $ {dirname} แต่ $ dirname สามารถตีความได้ด้วยวิธีใดวิธีหนึ่ง
James Polley

สิ่งสำคัญที่อยู่ที่นี่คือreadอ่านทั้งบรรทัด${dir}ดังนั้น IFS จึงไม่สำคัญ
James Polley

1
ขอบคุณสำหรับการค้นหาคำว่า $ / "การจัดฟันไม่จำเป็นถ้าไม่มีอะไรตามชื่อตัวแปร
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

4
สิ่งนี้จะจัดการชื่อพา ธ ที่มีช่องว่าง (U + 0020) แต่จะยังไม่สามารถจัดการชื่อพา ธ ที่มีการป้อนบรรทัด (U + 000A) ได้อย่างถูกต้อง ฉันชอบfind … -print0 | xargs -0 …เพราะตัวคั่นที่ใช้ตรงกับตัวอักษรเท่านั้นที่ไม่ได้รับอนุญาตในชื่อพา ธ ของ POSIX: NUL (U + 0000)
344 Chris

2
ที่สมบูรณ์แบบ! สิ่งที่ฉันกำลังมองหา whileมันไม่เคยเกิดขึ้นกับผมว่าคุณอาจจะสามารถท่อ @Chris Johnsen: จริง แต่แม้กระทั่งโปรแกรมริปเพลงก็ไม่มีแนวโน้มที่จะวาง linefeeds ไว้ในชื่อไฟล์ และถ้าพวกเขาทำผมต้องการที่จะรู้ (เช่น: อะไรผิดพลาด) และได้รับการกำจัดของพวกเขาทันที ...
ซามูเอล Jaeschke

8

ดูคำตอบนี้ฉันเขียนเมื่อไม่กี่วันที่ผ่านมาเพื่อดูตัวอย่างของสคริปต์ที่จัดการชื่อไฟล์ด้วยช่องว่าง

มีวิธีที่ซับซ้อนกว่าเล็กน้อย (แต่กระชับยิ่งขึ้น) ในการบรรลุสิ่งที่คุณพยายามทำ:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0บอกให้ค้นหาแยกอาร์กิวเมนต์ด้วยค่า null -0 ถึง xargs บอกให้คาดหวังว่าอาร์กิวเมนต์คั่นด้วย nulls ซึ่งหมายความว่าจะจัดการกับช่องว่างได้ดี

-I {}บอกให้ xargs แทนสตริง{}ด้วยชื่อไฟล์ นี่ก็หมายความว่าควรใช้ชื่อไฟล์เดียวเท่านั้นต่อหนึ่งบรรทัดคำสั่ง (โดยปกติแล้ว xargs จะใช้ชื่อไฟล์มากเท่าที่จะพอดีกับบรรทัด)

ที่เหลือควรชัดเจน


ข้อเสนอแนะของเดนนิสวิลเลียมสันเป็นอย่างไร (นอกเหนือจากตัวพิมพ์) อ่านได้มากขึ้นและดีกว่าในเกือบทุกวิถีทาง
James Polley

ใช้งานได้สำหรับ mkdir แต่ขอโทษที่ฉันควรจะชัดเจนกว่า - ฉันต้องการเรียกใช้ชุดคำสั่งสำหรับแต่ละไฟล์ คุณเห็นสำหรับกิจวัตรที่คล้ายกันของฉันในภายหลังฉันต้องการสร้างชื่อไฟล์เอาต์พุตตามชื่อไฟล์อินพุต (ซึ่งเกี่ยวข้องกับการลอกนามสกุล. OG และเพิ่ม. mp3) แล้วใช้ตัวแปรหลายตัวเหล่านี้ใน pipline ของฉันเมื่อเรียกใช้ gst-launch
ซามูเอล Jaeschke

5

ปัญหาที่คุณพบคือข้อความสั่ง for กำลังตอบสนองต่อการค้นหาเป็นอาร์กิวเมนต์แยกต่างหาก ตัวคั่นพื้นที่ คุณต้องใช้ตัวแปร IFS ของ bash เพื่อไม่ให้มีการแบ่งพื้นที่

นี่คือลิงค์ที่อธิบายวิธีการทำสิ่งนี้

ตัวแปรภายในของ IFS

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

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

ตั้งค่าการค้นหาของคุณเพื่อเอาท์พุทตัวคั่นฟิลด์หลังจาก% P และตั้งค่า IFS ของคุณอย่างเหมาะสม ฉันเลือกเซมิโคลอนเนื่องจากไม่น่าจะพบได้ง่ายในชื่อไฟล์ของคุณ

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


เกิดอะไรขึ้นถ้าชื่อไฟล์มี IFS จากนั้นคุณจะต้องเลือกที่แตกต่างกัน แต่แล้วจะเกิดอะไรขึ้นถ้า ...
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

3
คุณสามารถเลือก/บน POSIX และ:บนระบบไฟล์ DOS มีอักขระที่ผิดกฎหมายสำหรับระบบไฟล์ต่าง ๆ ที่คุณสามารถเลือกสำหรับ IFS อะไรที่ซับซ้อนกว่านี้และคุณควรใช้ Perl
Darren Hall

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

นั่นก็ดูมีประโยชน์ทีเดียว ฉันได้ไปกับwhileตัวเลือกไปป์แต่นี่ยังดูใช้งานได้ค่อนข้าง ใช่ในโครงสร้างที่คล้ายกันของฉันในภายหลังฉันจำเป็นต้องทำการแยกวิเคราะห์เพิ่มเติม (ชื่อไฟล์อินพุตจะเป็น. ogg ซึ่งจะถูกส่งผ่านfilesrcใน gst ไปป์ แต่จะลงท้ายด้วย. mp3 ที่อยู่ในไดเรกทอรีออกจะถูกสร้างขึ้นและส่งผ่านไปยังไปป์ไลน์เช่นfilesinkกันและแน่นอนต้องทำเช่นนี้ สำหรับแต่ละไฟล์พร้อมกับechoผู้ใช้บางคน)
Samuel Jaeschke

4

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

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

ตรวจสอบให้แน่ใจว่าได้รวมเส้นประต่อท้าย (หรือ 'word' อื่น ๆ ) หากเชลล์มีความหลากหลายของ Bourne / POSIX (ใช้เพื่อตั้งค่า $ 0 ในสคริปต์เชลล์) นอกจากนี้ต้องใช้ความระมัดระวังด้วยการอ้างถึงเนื่องจากเชลล์สคริปต์กำลังเขียนอยู่ในสตริงที่ยกมาแทนโดยตรงที่พรอมต์


อีกแนวคิดที่น่าสนใจ ขอบคุณ - ฉันแน่ใจว่าฉันจะพบการใช้งานสำหรับการนี้ต่อมา :)
ซามูเอล Jaeschke

1

ในคำถามที่อัปเดตของคุณคุณมี

mkdir -p \"${OUTPATH}${DIR}\"

สิ่งนี้ควรเป็น

mkdir -p "${OUTPATH}${DIR}"

ขอบคุณ แก้ไขแล้ว. นอกจากนี้ยังอ่านไฟล์ชื่อแทน DIR - copy-paste: P
Samuel Jaeschke


0

หรือทำให้ทุกอย่างซับซ้อนน้อยลง:

% rsync -av --include='*/' --exclude='*' SRC DST

สิ่งนี้จำลองโครงสร้างไดเรกทอรีของ SRC เป็น DST


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

คำสั่งที่ฉันให้แก้ปัญหาที่คุณถามมันไม่สำคัญว่านี่เป็นเพียงส่วนหนึ่งของ 'ขั้นสูง' ที่อยู่ข้างคุณ สำหรับคนอื่นที่มีปัญหาตามที่อธิบายไว้ในคำถามวิธี rsync จะทำงาน ดังนั้นไม่จำเป็นต้องเสียใจกับ unclearity ศักยภาพ :)
akira

ใช่. ไม่ฉันหมายความว่าฉันจะใช้โครงสร้างที่คล้ายกันwhile... do... ในdoneภายหลังเพื่อทำการประมวลผลที่คล้ายกันจาก find ซึ่งจะต้องใช้โค้ดหลายบรรทัดในแต่ละไฟล์ (แก้ไขสตริง echo, gst-launch ฯลฯ ) ) และrsyncจะไม่บรรลุเป้าหมายนี้ นั่นเป็นเหตุผลที่ฉันระบุว่าฉันต้องสามารถเรียกใช้ชุดคำสั่งที่ซับซ้อนมากขึ้นภายในโครงสร้างที่คล้ายกัน สคริปต์ของฉันใช้โครงสร้างลูปนี้สองครั้งดังนั้นสำหรับคำถามที่ฉันโพสต์สิ่งที่มีความหยาบน้อยลงตรงกลาง
ซามูเอล Jaeschke

0

หากคุณติดตั้ง GNU Parallel http: // www.gnu.org/software/parallel/ คุณสามารถทำสิ่งนี้ได้:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

ดูวิดีโอแนะนำสำหรับ GNU Parallel เพื่อเรียนรู้เพิ่มเติม: http://www.youtube.com/watch?v=OpaiGYxkSuQ

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