تجزیه ls - آموزش اسکریپت نویسی
X
تبلیغات
رایتل

آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

#!/bin/bash

تجزیه ls

تجزیه ls

چرا شما نباید خروجی ‎ls(1)‎ را تجزیه کنید؟

فرمان ‎ls(1)‎ برای نشان دادن صفات یک فایل منفرد به شما(حداقل در برخی حالت‌ها) نسبتاً مناسب است، اما وقتی از آن تقاضای یک لیست از فایلها را دارید، مشکل عظیمی وجود دارد: یونیکس در نام فایل تقریباً هر کاراکتری، از جمله فضای سفید، سطر جدید، علامت لوله(pipe)، و تقریباً هر مورد دیگری را که شما همیشه به عنوان جدا کننده استفاده می‌کنید، به استثنای NUL مجاز می‌داند. طرح‌های پیشنهادی برای کنار گذاشتن و اصلاح این مطلب داخل POSIX وجود دارد، اما در مواجه با موقعیت جاری، آنها کمکی نخواهند بود (همچنین صفحه چگونه به طور صحیح با نام فایلها رفتار کنیم را ببینید). اگر خروجی استاندارد ترمینال نباشد، ls در حالت پیش‌فرضِ خود، نام فایلها را با سطرجدید جدا می‌کند. تا وقتی دارای یک فایل با کاراکتر سطر جدید در نام آن نیستید این مناسب است. و چون من هیچ پیاده‌سازی از ls که به شما اجازه بدهد نام فایلها را به جای سطر جدید با کاراکترهای NUL خاتمه بدهید نمی‌شناسم، این مطلب ما را در به دست آوردن بی‌خطر لیست نام فایلها به وسیله ls، ناتوان باقی می‌گذارد.

$ touch 'a space' $'a\nnewline'
$ echo "don't taze me, bro" > a
$ ls | cat
a
a
newline
a space

این خروجی با دلالت بر اینکه ما دو فایل به نام a، یکی به نام newline و یکی به نام ‎a space‎ داریم، ظاهر می‌شود.

با کاربرد ‎ls -l‎ می‌توانیم ببینیم که به هیچوجه این صحیح نیست:

$ ls -l
total 8
-rw-r-----  1 lhunath  lhunath  19 Mar 27 10:47 a
-rw-r-----  1 lhunath  lhunath   0 Mar 27 10:47 a?newline
-rw-r-----  1 lhunath  lhunath   0 Mar 27 10:47 a space

مشکل خروجی ls آنست که، نه شما و نه کامپیوتر نمی‌توانید بگویید کدام بخشهای آن نام یک فایل را تشکیل می دهند. هر یک از کلمات است؟ خیر. هر یک از سطرهاست؟ خیر. پاسخ صحیحی برای این پرسش وجود ندارد، غیر از آنکه: شما نمی‌توانید بگویید.

همچنین توجه کنید چگونه گاهی اوقات ls نام فایل شما را تحریف می‌کند (در مثال ما، کاراکتر سطرجدید میان کلمات ‎"a"‎ و ‎"newline"‎ را به یک علامت سؤال برمی‌گرداند. برخی سیستم‌ها به جای آن یک ‎\n‎ قرار می‌دهند.). در برخی سیستم‌ها موقعی که خروجی استاندارد یک ترمینال نیست این کار انجام نمی‌شود، در سایرین همواره نام فایل را پاره می‌کند. از همه مهمتر، حقیقتاً شما نمی‌توانید و نباید به خروجی ls برای اینکه نماینده راستین نام فایلها باشد، اعتماد نمایید. چون اینطور نیست.

اکنون که مشکل را فهمیده‌ایم، بیایید روشهای مختلف کنار آمدن با آن را بشکافیم. مطابق معمول، باید با معین کردن آنکه، واقعاً می‌خواهیم چکار کنیم آغاز نماییم.

بر شمردن فایلها یا انجام اموری با فایلها

موقعی که افراد سعی می‌کنند ls را برای بدست آوردن لیست نام فایلها (تمام فایلها، یا فایلهایی که با یک glob مطابقت دارند، یا فایلهایی که به طریقی مرتب گردیده‌اند)، به کار ببرند، موارد به طور مصیبت‌باری مردود می‌شوند.

اگر فقط می‌خواهید روی تمام فایلهای دایرکتوری جاری یک عمل تکرار انجام بدهید، از یک حلقه for و یک glob استفاده کنید:

# Good!
for f in *; do
    [[ -e $f ]] || continue
    ...
done

همچنین کاربرد ‎"shopt -s nullglob"‎ را ملاحظه کنید، به طوریکه یک دایرکتوری خالی یک ‎'*'‎ لفظی به شما ارائه نخوهد نمود.

# (Bash خوب! (فقط در
shopt -s nullglob
for f in *; do
    ...
done

هرگز این موارد را انجام ندهید:

# !نامناسب! این کار را انجام ندهید‎
for f in $(ls); do
    ...
done

# !نامناسب! این کار را انجام ندهید
for f in $(find . -maxdepth 1); do # find is just as bad as ls in this context
    ...
done

# !نامناسب! این کار را انجام ندهید
arr=($(ls)) # Word-splitting and globbing here, same mistake as above
for f in "${arr[@]}"; do
    ...
done

# (!نامناسب! این کار را انجام ندهید‏(تابع خودش صحیح است‎
f() {
    local f
    for f; do
        ...
    done
}

f $(ls) # و تفکیک کلمه globbing  ،همان اشتباه فوق در اینجا

برای جزییات بیشتر BashPitfalls و DontReadLinesWithFor را ببینید.

اگر برخی مرتب‌سازی‌های ویژه را که تنها ls می‌تواند انجام بدهد لازم داشته باشید، از قبیل مرتب نمودن توسط mtime، امور دشوارتر می‌گردند. اگر قدیمی‌ترین یا جدیدترین فایل در دایرکتوری را می‌خواهید، ‎ls -t | head -1‎ را به کار نبرید -- به جای آن پرسش و پاسخ شماره ۹۹ را بخوانید. اگر به درستی لیست تمام فایلها در یک دایرکتوری را به ترتیب mtime می‌خواهید به طوری که بتوانید آنها را به ترتیب پردازش نمایید، سراغ پرل بروید، و برنامه پرل خودتان را وادار کنید، خودش دایرکتوری‌اش را باز کرده و مرتب نماید. آنوقت پردازش را در برنامه پرل انجام بدهید، یا -- بدترین حالت ممکن -- برنامه پرل را وادار کنید که نام فایلها را با جداکننده‌های NUL بیرون بدهد.

به طور رضایت‌بخش‌تر، زمان ویرایش را با قالب YYYYMMDD درون نام فایل قرار بدهید، به طوری که ترتیب glob نیز ترتیب mtime باشد. آنوقت شما به ls یا perl یا چیز دیگری احتیاج ندارید. (اکثریت وسیع موقعیت‌هایی که افراد قدیمی‌ترین یا جدیدترین فایل یک دایرکتوری را می‌خواهند، درست با انجام این کار می‌تواند رفع گردد.)

می‌توانستید ls را برای پشتیابی از گزینه ‎--null‎ تعمیر کنید و patch را به فروشنده OSتان ارائه کنید. که حدود پانزده سال قبل باید انجام شده باشد.

البته، دلیل آنکه انجام نگردید آن است که افراد بسیار کمی واقعاً به مرتب‌سازی ls در اسکریپت‌هایشان نیاز دارند. اساساً، موقعی که اشخاص لیستی از نام فایلها را می‌خواهند، به جای آن ‎find(1)‎ را به کار می‌برند، زیرا ترتیب برای آنها مهم نمی‌باشد. و برای مدت بسیار طولانی است که find در ‎BSD/GNU‎ دارای توانایی خاتمه دادن نام فایلها با کاراکترهای NUL می‌باشد.

بنابراین، به جای انجام این مورد:

# Bad!  Don't!
ls | while read filename; do
  ...
done

این مورد را امتحان کنید:

#   .متوجه باشید که این کد همان کار بالا را انجام نمی‌دهد. به طور بازگشتی پیش می‌رود‎
#  و فقط فایلهای معمولی را لیست می‌کند( پیوندهای نمادین و دایرکتوریها را خیر)‏. این‎
# .کد ممکن است در برخی حالت‌ها کار کند اما به هیچوجه یک جایگزین برای مورد فوق نیست‎
find . -type f -print0 | while IFS= read -r -d '' filename; do
  ...
done

حتی به طور بهتر، اکثر مردم واقعاً لیستی از نام فایلها را نمی‌خواهند. به جای آن، آنها می‌خواهند کارهایی با فایلها انجام بدهند. لیست فقط یک مرحله مقدماتی برای انجام هدف واقعی، از قبیل تغییر ‎www.mydomain.com‎ به ‎mydomain.com‎ در هرفایل ‎*.html‎ است. find می‌تواند به طور مستقیم نام فایلها را به یک فرمان دیگر عبور بدهد. به طور معمول نیازی به نوشتن نام فایلها به تفصیل در یک سطر مستقیم و سپس استناد به برنامه‌های دیگری برای خواندن جریان و جداسازی نامها و برگشت دادن آنها وجود ندارد.

کار کردن با فوق‌داده‌های یک فایل

اگر شما به دنبال اندازه فایل هستید، روش قابل حمل استفاده از wc به جای آن است:

# POSIX
size=$(wc -c < "$file")

به هر حال توجه نمایید که برخی پیاده‌سازی‌های wc به جای تشخیص آنکه ورودی استاندارد یک فایل معمولی است و به دست آوردن اطلاعات از ‎fstat(2)‎، فایل را به طور کامل خواهند خواند.

غالباً به دست آوردن سایر فوق‌داده‌ها به یک روش قابل حمل دشوار است. stat در هر پلاتفرم در دسترس نیست، و موقعی که هست، غالباً ترکیب دستوری کاملاً متفاوتی از شناسه‌ها را می‌پذیرد. هیچ روش استفاده از stat وجود ندارد که تا اندازه‌ای در سیستم POSIX بعدیِ اجرا‌کنندهِ اسکریپت شما، نقض نگردد. با وجود آن اگر برای شما مناسب باشد، پیاده‌سازی گنو از هر دو ابزار ‎stat(1)‎ و ‎find(1)‎ (از طریق گزینه ‎-printf‎)، بسته به آنکه برای یک فایل منفرد یا چندین فایل خواسته باشید، روشهای بسیار مناسبی جهت به دست آوردن اطلاعات فایل می‌باشند. find متعلق به AST نیز دارای گزینه ‎-printf می‌باشد، اما دوباره با قالب‌های ناسازگار، و خیلی کمتر از find گنو رایج است.

# GNU
size=$(stat -c %s -- "$file")
(( totalSize = $(find . -maxdepth 1 -type f -printf %s+)0 ))

اگر موفقیتی حاصل نشد، می‌توانید تجزیه برخی فوق‌داده‌ها از خروجی ‎ls -l‎ را امتحان کنید. دو اخطار بزرگ: ls را فقط با یک فایل در هر نوبت اجرا کنید(به خاطر داشته باشید که شما نمی‌توانید به طور قابل اعتمادی آگاه باشید که تجزیه فوق‌داده‌های فایل بعدی از کجا باید شروع بشود، چون جداکننده مناسبی وجود ندارد - و خیر، یک سطر جدید جداکننده خوبی نیست) و نشانه زمان یا بعد آن را تجزیه نکنید (نشانه زمان معمولاً در حالت بسیار وابسته به منطقه و پلاتفرم قالب‌بندی می‌شود و بدین ترتیب نمی‌تواند به طور قابل اعتمادی تجزیه بشود).

read mode links owner _ < <(ls -ld -- "$file")

توجه نمایید که رشته mode نیز غالباً مخصوص پلاتفرم است. مثلاً ‎OS X‎ یک @ برای صفات فایلها و یک+ برای اطلاعات امنیتی فایلها اضافه می‌کند.

در حالتی که ما را باور نمی‌کنید، چرای اینکه، تجزیه نشانه زمان را امتحان نکنید، در اینجاست:

# Debian unstable:
$ ls -l
-rw-r--r-- 1 wooledg wooledg       240 2007-12-07 11:44 file1
-rw-r--r-- 1 wooledg wooledg      1354 2009-03-13 12:10 file2

# OpenBSD 4.4:
$ ls -l
-rwxr-xr-x  1 greg  greg  1080 Nov 10  2006 file1
-rw-r--r--  1 greg  greg  1020 Mar 15 13:57 file2

در OpenBSD،به عنوان بیشترین نگارشهای یونیکس، ls نشانه‌های زمان را در سه فیلد نمایش می‌دهد -- ماه، روز، و سال-یا-زمان، با زمان شدن آخرین فیلد(ساعت:دقیقه) اگر عمر فایل کمتر از شش ماه باشد، یا سال شدن در صورتیکه عمر فایل بیشتر از شش ماه باشد. در دبیان ناپایدار، با یک نگارش نسبتاً جدید coreutils گنو، ls نشانه‌های زمان را در دوفیلد نمایش می‌دهد، اولی ‎Y-M-D‎(سال-ماه-روز) و دومی ‎H:M‎(ساعت:دقیقه) می‌شود، بدون توجه به آنکه عمر فایل چقدر است. بنابراین، باید تا اندازه‌ای واضح باشد که اگر ما نشانه زمان یک فایل را بخواهیم هرگز نباید خواستار تجزیه خروجی ls باشیم. اما برای فیلدهای قبل از آن، این خروجی به طور معمول نسبتاً قابل اطمینان است.

(توجه: برخی نگارشهای ls به طور پیش‌فرض گروه مالک یک فایل را چاپ نمی‌کنند، و یک نشانه ‎-g‎ برای انجام آن لازم است. سایرین به طور پیش‌فرض گروه را چاپ می‌کنند و ‎-g‎ آن را موقوف می‌سازد. به شما هشدار داده شده است.)

اگر می خواستیم فوق‌داده‌های بیش از یک فایل را در همان فرمان ls به دست آوریم، گرفتار همان مشکلی می‌شدیم که قبلاً داشتیم -- فایلها می‌توانند در نامشان شامل سطرجدید باشند، که خروجی ما را خراب می‌کند. تصور نمایید در صورتیکه ما دارای فایلی با یک سطرجدید در نام آن باشیم، چگونه کُدی مانند این شکست خواهد خورد:

# Don't do this
{ read 'perms[1]' 'links[1]' 'owner[1]' 'group[1]' _
  read 'perms[2]' 'links[2]' 'owner[2]' 'group[2]' _
} < <(ls -l "$file1" "$file2")

کُد مشابهی که دو فرمان ls جداگانه را فرامی‌خواند، احتمالاً مناسب خواهد بود، چون شروع خواندن دومین فرمان read از ابتدای خروجی یک فرمان ls، به جای محتملاً در میانه یک نام فایل، تضمین خواهد شد.

اگر تمام اینها یک کیسه شن بزرگ آزار دهندهِ شما به نظر می‌رسد، حق با شماست. احتمالاً تلاش برای طفره رفتن از تمام این کمبودهای استانداردسازی ارزش ندارد. برای برخی روشهای به‌دست آوردن فوق‌داده‌های فایل بدون هرگونه تجزیه خروجی ls پرسش و پاسخ ۸۷ را ببینید.


CategoryShell

ParsingLs (آخرین ویرایش ‎2013-07-25 12:29:11‎ توسط StephaneChazelas)