Chuyển đổi `cho tệp trong` thành` find` để tập lệnh của tôi có thể áp dụng đệ quy


7

Tôi có ý tưởng chạy tập lệnh bash để kiểm tra một số điều kiện và sử dụng ffmpegđể chuyển đổi tất cả các video trong thư mục của tôi từ bất kỳ định dạng nào sang .mkvvà nó đang hoạt động rất tốt!

Vấn đề là, tôi không biết rằng một for file invòng lặp không hoạt động đệ quy ( /programming/4638874/how-to-loop-ENC-a-directory-recursively )

Nhưng tôi hầu như không hiểu "đường ống" và đang mong chờ được xem một ví dụ và giải tỏa một số điều không chắc chắn.

Tôi có kịch bản này trong đầu mà tôi nghĩ sẽ giúp tôi rất nhiều để hiểu.

Giả sử tôi có đoạn mã bash này:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

Đối với thư mục hiện tại, tìm kiếm bất kỳ *.mkv *avi *mp4 *flv *ogg *movsau đó khai báo đầu ra để có phần mở rộng của nó .mkvsau đó xóa tệp gốc, sau đó đầu ra phải được lưu vào cùng thư mục mà video gốc nằm trong.

  1. Làm thế nào tôi có thể chuyển đổi này để chạy đệ quy? Nếu tôi sử dụng find, khai báo biến ở $fileđâu? Và bạn nên khai báo ở $targetđâu? Có phải tất cả findchỉ thực sự một lót? Tôi thực sự cần phải truyền tệp cho một biến $file, bởi vì tôi vẫn sẽ cần chạy kiểm tra điều kiện.

  2. Và, giả sử rằng (1) thành công, làm thế nào để đảm bảo rằng yêu cầu "sau đó đầu ra phải được lưu vào cùng thư mục mà video gốc đang ở" được thỏa mãn?


1
Một số shell có **toán tử hả hê để tìm kiếm các tệp đệ quy. Nó không di động nhưng nó chơi tốt với cú pháp for-loop.
hugomg

1
Cuối cùng tôi đã biến một loạt các bình luận mà tôi đã viết thành một câu trả lời của riêng mình, giải quyết các câu hỏi nhỏ của bạn như làm thế nào để khai báo vars shell.
Peter Cordes

Câu trả lời:


5

Bạn đã có mã này:

for file in *.mkv *avi *mp4 *flv *ogg *mov; do
target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

mà chạy trong thư mục hiện tại. Để biến nó thành một quá trình đệ quy, bạn có một vài lựa chọn. Đơn giản nhất (IMO) là sử dụng findnhư bạn đề xuất. Cú pháp cho findrất "không giống UNIX" nhưng nguyên tắc ở đây là mỗi đối số có thể được áp dụng với các điều kiện AND hoặc OR. Ở đây, chúng ta sẽ nói " Nếu tên tệp này khớp với HOẶC tên tệp đó khớp với Sau đó in nó ". Các mẫu tên tệp được trích dẫn để shell không thể giữ chúng (hãy nhớ rằng shell chịu trách nhiệm mở rộng tất cả các mẫu không được trích dẫn, vì vậy nếu bạn có một mẫu không được trích dẫn *.mp4và bạn có janeeyre.mp4trong thư mục hiện tại của mình, shell sẽ thay thế *.mp4bằng trận đấu, và findsẽ thấy -name janeeyre.mp4thay vì mong muốn của bạn -name *.mp4, nó sẽ tệ hơn nếu*.mp4khớp với nhiều tên ...). Các dấu ngoặc cũng được thêm tiền tố \để giữ cho vỏ không cố gắng hành động chúng dưới dạng các dấu phụ (chúng ta có thể trích dẫn các dấu ngoặc thay thế, nếu muốn '(':).

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print

Đầu ra của cái này cần được đưa vào đầu vào của một whilevòng lặp xử lý lần lượt từng tệp:

while IFS= read file    ## IFS= prevents "read" stripping whitespace
do
    target="${file%.*}.mkv"
    ffmpeg -i "$file" "$target" && rm -rf "$file"
done

Bây giờ tất cả những gì còn lại là nối hai phần lại với nhau bằng một đường ống |sao cho đầu ra của đầu ra findtrở thành đầu vào của whilevòng lặp.

Trong khi bạn đang kiểm tra mã này, tôi muốn khuyên bạn nên tiền tố cả hai ffmpegrmvới echođể bạn có thể xem những gì sẽ được thực hiện - và với các đường dẫn gì.

Đây là kết quả cuối cùng, bao gồm các echotuyên bố tôi đề xuất để thử nghiệm:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print |
    while IFS= read file    ## IFS= prevents "read" stripping whitespace
        do
            target="${file%.*}.mkv"
            echo ffmpeg -i "$file" "$target" && echo rm -rf "$file"
        done

1
Shell mở rộng công cụ đầu tiên và điều này có thể có hậu quả không mong muốn. Hãy thử echo *trong một thư mục trống. Sau đó thử nó trong một thư mục không trống. Các bản in trước *và sau thay thế ngôi sao bằng một danh sách các tập tin. Điều tương tự xảy ra với find.
Sobrique

1
@TheWolf xargscó thể là một bãi mìn. Hãy cẩn thận với điều đó.
roaima

2
@TheWolf: Nếu xargsfindkhông có -0tùy chọn, bạn vẫn gặp sự cố. Hãy để findtất cả các công việc với -execlà giải pháp tốt hơn.
cuonglm

1
@CharlesDuffy bạn được chào đón để cải thiện nó. Tôi muốn một cái gì đó đủ đơn giản để người mới bắt đầu có thể hiểu được. Nếu chúng tôi bắt đầu sử dụng -print0và các phần bổ sung chuyên nghiệp khác, bạn sẽ nhận được mã vững chắc hơn, nhưng phức tạp hơn cần được giải thích.
roaima

1
Tốt hơn là học những cách viết kịch bản an toàn ngay từ đầu, thay vì học những cách phá vỡ tên tập tin kỳ lạ. (Lên đến một điểm, dù sao IFS = đọc ... được khá tốt, và được đề xuất bởi. Mywiki.wooledge.org/BashFAQ/001 )
Peter Cordes

6

Với POSIX tìm:

find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \
          -name '*ogg' -o -name '*mov' \) -exec sh -c '
  for file do
    target="${file%.*}.mkv"
    echo ffmpeg -i "$file" "$target"
  done' sh {} +

Thay thế echobằng bất cứ lệnh nào bạn muốn sử dụng.

Nếu bạn có GNU find hoặc BSD find, bạn có thể sử dụng -regex:

find . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'

xin lỗi, nhưng tôi không thể làm theo cách thêm find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \ -name '*ogg' -o -name '*mov' \) -exec sh -c 'vào ở đầu đoạn trích sẽ được giảm xuống file. và những gì sh {} +? Cảm ơn!
arvil

Không di động nhưng ngắn hơnfind . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'
Costas

1
@TheWolf: Tôi mời bạn đọc unix.stackexchange.com/q/93324/38906
cuonglm

2

Đoạn mã ví dụ không có đường ống (giả sử bạn đang đưa đường dẫn làm đối số):

#!/bin/bash

backup_dir=/backup/

OIFS="$IFS"
IFS=$'\n'

files="$(find "$1" -type f -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv')"

for f in $files; do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir" 
done

IFS="$OIFS"

Vỏ đọc IFSbiến, được thiết lập để ( space, tab, newline) theo mặc định. Sau đó, nó nhìn vào từng nhân vật trong đầu ra của find. Vì vậy, nếu nó tìm thấy spacenó nghĩ rằng đó là phần cuối của tên tệp (tệp chứa khoảng trắng, ví dụ "Sin City.avi", được coi là hai tệp "Sin" và "City.avi"). Vì vậy, với IFS = $ '\ n', chúng tôi đang yêu cầu phân chia đầu vào newlines. Và cuối cùng chúng tôi khôi phục cũ (mặc định) IFSđược lưu trong $OIFSbiến.
Hoặc như được đề xuất trong các ý kiến ​​có thể là cách tiếp cận tốt hơn có thể là:

#!/bin/bash

backup_dir=/backup/

find "$1" -type f \( -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv' \) -print0 | while IFS= read -r -d '' f
do
    # get path
    d="${f%/*}"
    # get filename
    b="$(basename "$f")"
    ttarget="${b%.*}.mkv"

    # this is your final target
    target="$d/$ttarget"
    echo $target
    # mv $f "$backup_dir"
done

Điều này có vẻ tiện dụng, nhưng thực sự tôi không hiểu việc khai báo vars OIFSIFSở đầu và cuối kịch bản. và nếu tôi hiểu chính xác, đường dẫn được khai báo tại var $1? và backup_dirchỉ đơn thuần là một bản sao lưu để gỡ lỗi phải không?
arvil

1
IFS là một biến xác định giá trị phân tách trường của bạn sẽ là gì. IFS mặc định thường là "khoảng trắng". Vì vậy, không gian, tab, dòng mới, v.v., tất cả sẽ được công nhận là "dấu tách trường". Vì một số phim và bài hát có thể được đặt tên bằng khoảng trắng trong chúng, tập lệnh này đang nói bỏ qua khoảng trắng và bảng và sử dụng dòng mới làm dấu phân cách. Bằng cách này, thay vào đó hoặc "Kẻ hủy diệt 2.mp4" được công nhận là 2 phim, "Kẻ hủy diệt" và "2.mp4", nó được xem là 1 phim "Kẻ hủy diệt 2.mp4". Vì IFS đã được lưu dưới dạng OIFS, phần cuối của tập lệnh sẽ đặt lại làm mặc định.
Tim Kennedy

Một cách khác để làm điều này là đặt mã của bạn vào hàm shell local IFS=, vì vậy IFS trống chỉ cho ngữ cảnh của hàm của bạn và không cần phải lưu / khôi phục.
Peter Cordes

1

Chào mừng đến với Unix :)

Để trả lời một số câu hỏi nhỏ của bạn mà câu trả lời cho câu hỏi chính không bao gồm:

Shell scripting chắc chắn có một số cạnh khó khăn, vì rất nhiều thứ phá vỡ tên tệp có khoảng trắng. Và hầu hết mọi thứ đều phá vỡ tên tập tin với dòng mới (may mắn thay, không ai thực hiện những mục đích đó). Tên tập tin chứa ký tự glob thích [, ]*đôi khi một vấn đề, quá. Đôi khi, không đáng để viết mã shell khó đọc theo tiêu chuẩn của BashGuide của Wooledge , cho mục đích sử dụng của riêng bạn hoặc cho một lần mà bạn biết tên tệp của mình không lạ.

nơi để khai báo biến:

Biến Shell không cần phải khai báo. Trong bash, bạn có thể shopt -o nounsetbiến nó thành một lỗi để tham chiếu và biến unSET, nhưng điều đó không hoàn toàn giống như không được khai báo. Bỏ đặt một biến có thể hữu ích. Trong hàm shell, cách tốt nhất là khai báo tất cả các tạm thời của bạn local foo bar baz;, vì vậy bạn không xả rác môi trường shell với các biến hoặc tệ hơn là bước vào biến của người gọi cùng tên.

Tôi hầu như không hiểu "đường ống".

Khi làm việc với shell, rất nhiều dữ liệu truyền qua xảy ra bằng cách in dữ liệu ra thiết bị xuất chuẩn. Các đường ống gửi dữ liệu đó đến một chương trình khác, nó đọc nó trên stdin (và thường in một cái gì đó trên thiết bị xuất chuẩn). Bạn có thể nắm bắt đầu ra thành các biến shell bằng cách sử dụng lệnh thay thế , $(). ví dụ for i in $( locate foo | grep bar );do echo "$i"; done. (điều này sẽ phá vỡ tên tệp có khoảng trắng trong chúng, giống như rất nhiều mã shell nếu bạn không cẩn thận. Sử dụng readnếu bạn muốn viết các tập lệnh đáng tin cậy.) locatein, grepđọc và in và shell đọc đầu ra của grep. (Vỏ được đặt trên đầu ra của grepbằng cách bắt đầu grep với đầu ra của nó được kết nối với phía đầu vào của ống mà vỏ được tạo. Vỏ đọc phía đầu ra của ống.)

Một đường ống chỉ là một cách để các chương trình hoạt động giống như chúng đang ghi vào một tệp, nhưng thực ra chúng đang ghi vào một bộ đệm nhỏ. Một quá trình đọc từ một đường ống sẽ read(2)trả lại cuộc gọi hệ thống của nó khi có sẵn dữ liệu, điều này chỉ xảy ra khi một cái gì đó ghi vào đầu kia của ống.

Vỏ là |, $()và một số yếu tố cú pháp khác là làm thế nào bạn nói với vỏ làm thế nào để thiết lập hệ thống ống nước nối chương trình với nhau, và để vỏ.

Thật dễ dàng để học các thành ngữ xấu cho lập trình shell, bởi vì rất nhiều điều rõ ràng và cách làm cũ đã ẩn chứa những cạm bẫy phá vỡ tên tập tin kỳ lạ. Xem ví dụ http://mywiki.wooledge.org/BashFAQ/001 .

Tốt hơn là học các cách viết kịch bản an toàn ngay từ đầu, thay vì học các cách phá vỡ tên tệp lẻ, miễn là chúng không quá khó để gõ. :)

Rất nhiều tiện ích GNU có tùy chọn -0, để sử dụng ASCII NUL (0 byte, không thể có trong tên tệp hoặc văn bản) làm dấu tách bản ghi. Điều này cho phép bạn chuyển dữ liệu giữa findsort, ví dụ, mà không có bất kỳ khả năng nào có một "dòng" đầu ra tìm được chuyển thành nhiều dòng đầu vào sắp xếp. Điều này kết thúc không phải là siêu hữu ích khi bạn muốn đưa dữ liệu vào một biến shell, bởi vì bash không có cách nào để đọc \0các dòng giới hạn. (Tôi không nghĩ đó là một giá trị hợp lệ cho IFS.)

Dù sao, tránh việc shell xử lý dữ liệu dưới dạng mã là lý do để luôn trích dẫn hai lần mọi thứ bạn có thể, trừ khi bạn thực sự MUỐN chia tách từ. Nếu bạn muốn làm cho bộ não của bạn bị tổn thương khi nhìn vào mã shell phức tạp, chỉ cần nhìn vào mã hoàn thành bash. (Nó xử lý việc hoàn thành có thể lập trình để thực hiện những việc thông minh như hoàn thành ls --colo => --colorhoặc chỉ hoàn thành các tệp * .zip để giải nén.) set -xVà nhấn tab: P. (đặt + x để tắt theo dõi thực thi.)

re: vòng lặp for của bạn: với *.mkvmột trong các mẫu của bạn, bạn sẽ có source = Dest cho các tệp đầu vào đó. ffmpegsẽ nhắc bạn ghi đè lên tệp đầu ra cho mỗi tệp.

Ngoài ra, bạn có thực sự cần phải chuyển mã âm thanh? -c:a copycó thể là một ý tưởng tốt Tốc độ bit video thường là một thỏa thuận lớn hơn. Và bạn có thể muốn sử dụng -preset slow(hoặc slower, hoặc thậm chí veryslow) để có được chất lượng cao hơn trên mỗi bitrate, với chi phí sử dụng CPU nhiều hơn. Cũng có -crf 20(mặc định 23). https://trac.ffmpeg.org/wiki/Encode/H.264 . Bạn hy vọng đã biết điều này và bỏ nó đi vì nó không liên quan đến kịch bản bash, nhưng chỉ trong trường hợp ...: P -c:v libx264là mặc định khi xuất ra mkv, vậy thì tốt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.