برخی توزیعهای گنو-لینوکس دارای فرمان rename(1) هستند، که میتوانید برای این کار استفاده کنید، به هرحال نحوه ترکیب دستوری(syntax) آن از یک توزیع به دیگری تفاوت دارد، بنابراین پاسخ قابل حملی نیست....
اگر میخواهید یاد بگیرید که چگونه از فرمان renameخود استفاده کنید، در صورتی که یک نمونه از آن را دارید، صفحات man در سیستم خود را کنکاش کنید. غالباً، برای انجام تغییر نامهای یکبارهِ محاورهای کاملاً مناسب است، نه در اسکریپتهای قابل حمل. ما در اینجا مثالی برای rename نمیآوریم زیرا خیلی مغشوش میشود -- دو نگارش رایج از این برنامه وجود دارد و آنها کاملاً با یکدیگر ناسازگارند.
احتمالاً میتوانید تغییر نام انبوه غیر بازگشتی را با استفاده از یک حلقه و برخی بسط پارامترها انجام بدهید، مانند این :
# POSIX
# تغییر نام میدهد *.bar را به *.foo تمام
for f in *.foo; do mv -- "$f" "${f%.foo}.bar"; done
# POSIX
# را از تمام فایلها حذف میکند .zip پسوند
for file in ./*.zip; do mv "$file" "${file%.zip}"; done
"--" و "./*" برای محافظت ازنام فایلهای حیرتآوری که با "-" شروع میشوند، هستند. فقط به یکی از آنها نیاز دارید بنابراین مطلوب خود را به کار ببرید.
دراینجا مثالهای مشابهی ، با کاربرد بسط پارامترهای مخصوص Bash آمده است:
# Bash
# تمام فاصلهها با خط زیر تعویض میشوند
for f in *\ *; do mv -- "$f" "${f// /_}"; done
# Bash
# تغییر میکنند "bar" ها به "foo" همه
for file in ./*foo*; do mv "$file" "${file//foo/bar}"; done
تمام مثالهای فوق فرمان خارجی mv(1) را یکبار برای هر فایل احضار میکنند، بنابراین شاید آنها به کارآمدی بعضی پیادهسازیهای rename نباشند.
اگر میخواهید به طور بازگشتی فایلها را تغییر نام بدهید، آنوقت خیلی بیشتر چالشانگیز میشود. این مثال *.foo را به *.bar تغییر نام میدهد:
# Bash در پوسته
# نیاز دارد BSD گنو یا find(1) همچنین به
# تغییر میکنند *.bar به *.foo به طور بازگشتی تمام فایلهای
find . -type f -name '*.foo' -print0 | while IFS= read -r -d '' f; do
mv -- "$f" "${f%.foo}.bar"
done
این مثال به جای به کار بردن find گنو از globstar نگارش Bash 4 استفاده میکند:
# فعال شده باشد، قابل حمل نیست globstar که لازم است گزینه Bash 4 در
# تغییر نام میدهد "bar" را به "foo" به طور بازگشتی تمام فایلهای
# در نام دایرکتوری نباید ظاهر شود "foo"
shopt -s globstar; for file in /path/to/**/*foo*; do mv -- "$file" "${file//foo/bar}"; done
برای کنترل آن که خروجی فرمان فوق چه خواهد شد، میتوانید یک فرمان echo قبل از mv اضافه کنید به این ترتیب ایدهای از آن به دست میآورید.
جهت تکنیکهای بیشتر برای کار با فایلهایی که کاراکترهای نامناسب در نام خود دارند پرسش و پاسخ شماره 20 را ببینید.
مهارت آمیزترین قسمت تغییر نام بازگشتی، آن است که مطمئن بشوید دایرکتوری تشکیل دهنده نام مسیر را تغییر ندهید، زیرا موردی مشابه این محکوم به شکست است:
mv "./FOO/BAR/FILE.TXT" "./foo/bar/file.txt"
بنابراین، هر فرمان تغییر نام بازگشتی، فقط باید نام فایلهای تشکیل دهنده هرمسیر را تغییر بدهد. اگر نیاز دارید که نام دایرکتوریها را هم تغییر بدهید، این کار باید به طور جداگانه انجام شود. گذشته از این، تغییر نام بازگشتی دایرکتوریها یا باید اول از عمق انجام شود(فقط تغییر نام آخرین جزء تشکیل دهنده نام دایرکتوری در هر نمونه)، یا در چند نوبت. تغییر اول از عمق در حالت کلی بهتر کار میکند.
این هم یک اسکریپت نمونه که بازگشت از عمق به سطح را(فاصله ها در نام را به خطزیر تغییر میدهد، اما فقط لازم است شما تابع ren() را برای آنچه که میخواهید انجام بدهد، تغییر بدهید.) برای تغییر نام هم فایلها هم دایرکتوریها به کار میبرد(از طرف دیگر، ویرایش آن برای آنکه فقط روی فایلها یا فقط روی دایرکتوریها عمل کند، یا فقط بر فایلهایی با پسوند معین عمل کند، با اجتناب از رونویسی فایلها یا اجبار به انجام آن، وغیره، آسان است.):
# Bash
ren() {
local newname
newname=${1// /_}
[ "$1" != "$newname" ] && mv -- "$1" "$newname"
}
traverse() {
local i
cd -- "$1" || exit 1
for i in *; do
[ -d "$i" ] && traverse "$i"
ren "$i"
done
cd .. || exit 1
}
# main program
shopt -s nullglob
traverse /path/to/startdir
این هم روش دیگر تغییرنام بازگشتی تمام شاخهها و فایلهایی که نام آنها دارای فاصله است:
find . -depth -name "* *" -exec bash -c 'dir=${1%/*} base=${1##*/}; mv "$1" "$dir/${base// /_}"' _ {} \;
یا، اگر نگارش find شما آن را قبول میکند، این روش، که به جای اجرای یک bash برای هرفایل، جهت چندین فایل، یک bash اجرا میکند، بیشتر کارآمد است:
find . -depth -name "* *" -exec bash -c 'for f; do dir=${f%/*} base=${f##*/}; mv "$f" "$dir/${base// /_}"; done' _ {} +برای تبدیل نام فایلها به حروف کوچک، اگر شما برنامه سودمند mmv(1) را در سیستم خود دارید، میتوانید این مورد را انجام بدهید:
# تمام نام فایلها را به حروف کوچک تبدیل میکتد mmv "*" "#l1"
در غیر اینصورت، به چیزی احتیاج دارید که نام فایلهای با حروف مختلط را به عنوان ورودی پذیرفته و برگردان حالت حروف کوچک را به عنوان خروجی باز گرداند. در Bash 4 و بالاتر، بسط پارامتری وجود دارد که میتواند آن را انجام بدهد:
# Bash 4
for f in *[[:upper:]]*; do mv -- "$f" "${f,,*}"; done
در غیر اینصورت، شاید tr(1) سودمند باشد:
# نام فایلها را به حروف کوچک تبدیل میکند
# POSIX
for file in "$@"
do
[ -f "$file" ] || continue # صرفنظر از نامهای غیر موجود
newname=$(echo "$file" | tr '[:upper:]' '[:lower:]') # حالت کوچک
[ "$file" = "$newname" ] && continue # کاری انجام نمیشود
[ -f "$newname" ] && continue # فایهای موجود رونویسی نشوند
mv -- "$file" "$newname"
done
ما از نماد دامنه به صورت فوق استفاده نمودیم، زیرا برنامه tr، هنگامی که در برخی مناطق از دامنه به صورت A-Z استفاده میشود، میتواند به طور بسیار عجیبی رفتار نماید:
imadev:~$ echo Hello | tr A-Z a-z hÉMMÓ
برای حصول اطمینان از این که موقع استفاده از tr همراه با دامنهها، شگفتزده نشوید، یا از کلاس دامنهها استفاده کنید، یا منطقه را به C تنظیم کنید.
imadev:~$ echo Hello | LC_ALL=C tr A-Z a-z hello imadev:~$ echo Hello | tr '[:upper:]' '[:lower:]' hello # Either way is fine here.
این روش همچنین برای تعویض تمام کاراکترهای ناخواسته در نام یک فایل، به عنوان مثال با '_'(خط زیر) میتواند به کار برود. همان اسکریپت بالا، فقط با سطر تغییر یافتهٔ "newname=...".
# تغیر نام فایلهایی که در نام خود شامل کاراکتر غیر عادی هستند
# POSIX
for file in "$@"
do
[ -f "$file" ] || continue # چشمپوشی از فایلهای غیرمعمولی، وغیره
newname=$(echo "$file" | sed 's/[^[:alnum:]_.]/_/g')
[ "$file" = "$newname" ] && continue # nothing to do
[ -f "$newname" ] && continue # فایلهای موجود رونویسی نشوند
mv -- "$file" "$newname"
done
کلاس کاراکتر در [] شامل تمام کاراکترهایی است که میخواهیم حفظ (بعد از ^)، و در صورت لزوم ویرایش کنیم. دامنه [:alnum:] عهدهدار تمام حروف و ارقام منطقه جاری است.
در اینجا مثالی هست که همان کار را انجام میدهد، اما این دفعه به جای sed از بسط پارامتر استفاده میشود:
# تغییر نام فایلها(نگارش کارآمدتر، کمتر قابل حمل)
# Bash
for file in "$@"; do
[ -f "$file" ] || continue
newname=${f//[^[:alnum:]_.]/_}
[ "$file" = "$newname" ] && continue
[ -f "$newname" ] && continue
mv -- "$file" "$newname"
done
باید توجه بشود که تمام این مثالها شامل یک وضعیت مسابقه هستند-- و یک فایل موجود در صورتی که در فاصله بین فرمانهای [ -f "$newname" ... و mv "$file" ... ایجاد گردد، میتواند رونویسی بشود. به هرحال حل این موضوع فراتر از محدوده این صفحه میباشد.
یک مطلب نهایی در مورد تغییر حالت نام فایلها: در بسیاری از سیستم فایلها، موقع استفاده از mv گنو، کوشش برای تغییرنام فایل به حالت حروف کوچک یا بزرگ معادل آن، ناموفق میشود. (این شامل Cygwin در سیستمهای DOS/Windows با سیستم فایل FAT یا NTFS و mv گنو در سیستمهای Mac OS X با HFS+ در وضعیت غیرحساس به حالت حروف میشود، همچنین در سیستمهای لینوکس که فایلسیستمهای متصل شده Windows/Mac دارند، و بسیاری تنظیمهای احتمالی دیگر.) فرمان mv گنو قبل از تلاش برای تغییرنام، مقصد هر دو نام را کنترل میکند، و به سبب مسیردهی سیستمفایل، گمان میکند که فایل هماکنون موجود است:
mv README Readme # ناموفق است FAT گنو در فایلسیستم mv با دستور
رفع و رجوع این مورد، دوبار تغییرنام فایل است: اول به یک نام موقتی که کاملاً متمایز از نام اولیه است، سپس به نام مورد نظر.
mv README tempfilename && mv tempfilename Readme
پرسش و پاسخ 30 (آخرین ویرایش 2012-02-21 19:16:53 توسط GreyCat)
فرمان غیر استاندارد خارجی readlink(1) میتواند برای نمایش مقصد لینک نمادین استفاده شود:
$ readlink /bin/sh bash
اگر شما برنامه readlink را ندارید، میتوانید از Perl استفاده کنید:
perl -e 'print readlink "/bin/sh", "\n"'
همچنین میتوانید دستور find -printf %l گنو را به کار ببرید، که مخصوصاً اگر به واکاوی گروهی پیوندها نیاز دارید، مفید است:
$ find /bin/ -type l -printf '%p points to %l\n' /bin/sh points to bash /bin/bunzip2 points to bzip2 ...
اگر سیستم شما فاقد هر دو برنامه readlink و Perl میباشد، میتوانید از چنین تابعی استفاده کنید:
# Bash
readlink() {
local path=$1 ll
if [ -L "$path" ]; then
ll=$(LC_ALL=C ls -l "$path" 2>/dev/null) &&
printf '%s\n' "${ll#* -> }"
else
return 1
fi
}
گر چه، در صورتی که پیوند نمادین در نام خود شامل " -> " باشد، این تابع میتواند با شکست مواجه شود.
پرسش و پاسخ 29 (آخرین ویرایش 2011-04-16 04:04:51 توسط pool-72-81-225-126)
این موضوع به طور مکرر طرح میشود. این پاسخ فقط عبارت به کار رفته فوق («فایلهای پیکربندی») را پوشش نمیدهد، بلکه چند حالت مختلف را شامل میشود. اگر شما به اینجا هدایت شدهاید، لطفاً قبل از ترک کردن آن، تمام پاسخ را بخوانید.
این پرسش پیچیدهایست، زیرا برای آن یک پاسخ صحیح منفرد وجود ندارد. حتی وخیمتر: پیدا کردن محل اسکریپت به طور قابل اعتماد برای 100% تمام حالتها، امکانپذیر نیست. تمام روشهای یافتن محل اسکریپت وابسته به نام اسکریپت است، به طوری که در متغیر پیشتعریف شده $0 دیده شده است. اما فراهم نمودن نام اسکریپت در $0 فقط یک قرارداد(بسیار رایج) است، نه یک التزام.
پاسخ مورد تردید این است: «در برخی شلها $0 همیشه، حتی اگر شما اسکریپت را با نام مسیر نسبی، یا بدون هرگونه نام مسیر فراخوانی نمایید»، محتوی نام مسیر مطلق است. اما این در تمام پوستهها قابل اتکا نیست، بعضی از آنها(از جمله BASH) به جای نام مسیر کاملاً واجد شرایط، فرمان واقعی تایپ شده توسط کاربر را باز میگردانند. و این فقط نوک کوه یخ شناور است!
اسکریپت شما ممکن است در حقیقت به هیچ وجه در دیسک قابل دسترسی در محل قرار نداشته باشد. این مورد را ملاحظه کنید:
ssh remotehost bash < ./myscript
پوستهِ در حال اجرا در میزبان راه دور، فرمان خود را از یک لوله دریافت میکند. اسکریپتی در جایی روی دیسک، که bash بتواند ببیند وجود ندارد.
علاوه براین، حتی اگر اسکریپت شما در دیسک محلی ذخیره و اجرا شده باشد، میتواند انتقال یابد. شخصی میتواند در فاصله بین زمانی که شما فرمان را تایپ میکنید و زمان بررسی $0 اسکریپت را به محل دیگری mv نماید. یا کسی میتواند در مدت همان روزنهِ زمانی، پیوند اسکریپت را قطع کند، به طوری که دیگر به طور واقعی ارتباطی با سیستم فایل نداشته باشد.
حتی در حالتهایی که اسکریپت در یک مکان ثابت روی دیسک محلی قرار گرفته است، رویکرد $0 باز هم اشکالات عمدهای دارد. مهمتر از همه آنست که نام اسکریپ(به طوری که در $0 دیده شده) شاید نسبت به دایرکتوری کاری نباشد، بلکه وابسته به شاخهای از برنامه جستجوی مسیر $PATH باشد(این مورد اغلب در KornShell دیده شده است). یا(و این مشکل به مراتب محتملتر است) شاید به اسکریپت پیوندهای چندگانه از مکانهای مختلف وجود داشته باشد، که یکی از آنها پیوند نمادین سادهای به یک شاخه معمول در PATH مانند /usr/local/bin باشد، که اسکریپت به واسطه آن فراخوانی میشود. اسکریپت شما شاید در /opt/foobar/bin/script باشد، اما رویکرد ساده خواندن $0 آنرا به شما نخواهد گفت -- ممکن است به جای آن این آدرس را /usr/local/bin/script بگوید.
(برای بحث جامعتر در خصوص فایل سیستم یونیکس و چگونگی تأثیر پیوندهای نمادین بر توانایی شما در مورد دانستن جایی که در یک لحظه معین قرار دارید، این طرح ۹ صفحهای را ببینید.)
با تمام آنچه گفته شد، اگر بازهم میخواهید به طرف مفروضات ساده چرخش کنید، و تمام آنچه میخواهید، نگارش کاملاً واجد شرایط $0 است، میتوانید از موردی مشابه این ترکیب دستوری استفاده کنید (BASH):
[[ $0 == /* ]] && echo "$0" || echo "${PWD}/${0#./}"
یا نگارش پوسته Bourne:
case $0 in /*) echo "$0";; *) echo "`pwd`/$0";; esac
یا نوع مستقل از پوسته(به readlink(1) با پشتیبانی از -f نیاز دارد،به هرحال وابسته به سیستم عامل است):
readlink -f "$0"
در Bash، نگارش 4.1.7(1)-release، در لینوکس، به نظر میرسد همیشه اسکریپت با توصیفگرفایل 255 باز میشود، بنابراین میتوانید دقیقاً اینطور انجام بدهید:
HOME="$(dirname "$(readlink /proc/$$/fd/255)")"
اگر بخواهیم تمام وضعیتهایی را بشماریم که در آنها ممکن است نام مسیر نسبی اسکریپت(در $0) به جای دایرکتوری کاری جاری(به طوری که در فوق اشاره گردید)،نسبت به اجزاء متشکله $PATH باشد، میتوانیم جستجو برای اسکریپت در تمام شاخههای $PATH را آزمایش کنیم.
اسکریپت پایین چگونگی انجام آن را نشان میدهد:
1 #!/bin/bash
2
3 myname=$0
4 if [[ -s "$myname" ]] && [[ -x "$myname" ]]; then
5 # از قبل نام فایل معتبری است $myname
6
7 mypath=$myname
8 else
9 case "$myname" in
10 /*) exit 1;; # جستجو نمیشود PATH نام مسیر مطلق،
11 *)
12 # مراقب تفسیر ،PATH جستجوی تمام شاخههای متغیر
13 # کاراکتر ":" ابتدا و انتهای مسیر به معنی دایرکتوری
14 # نیز همین PATH جاری باشید در مورد "::" در میان
15 # .مطلب صادق است
16
17 # p تعویض : ابتدای مسیر با . و ذخیره در
18 p=${PATH/#:/.:}
19 # تعویض : انتها با .
20 p=${p//%:/:.}
21 # تعویض :: با :.:
22 p=${p//::/:.:}
23 # جداکننده فیلد موقتی، پرسش و پاسخ شماره ۱ را ببینید
24 OFS=$IFS IFS=:
25 # تفکیک مسیر نسبت به کولن و تکرار حلقه روی آنها
26 for dir in $p; do
27 [[ -f "$dir/$myname" ]] || continue # فایل نیست
28 [[ -x "$dir/$myname" ]] || continue # قابل اجرا نیست
29 mypath=$dir/$myname
30 break # فقط اولین انطباق فایل را برمیگرداند
31 done
32 # بازیابی جداکننده فیلد اولیه
33 IFS=$OFS
34 ;;
35 esac
36 fi
37
38 if [[ ! -f "$mypath" ]]; then
39 echo >&2 "cannot find full path name: $myname"
40 exit 1
41 fi
42
43 echo >&2 "path of this script: $mypath"
توجه کنید که $mypath ضرورتاً یک نام مسیر مطلق نیست. باز هم میتواند شامل نام مسیرهای نسبی مانند ../bin/myscript باشد،زیرا $PATH میتوانست شامل آنها باشد. اگر شما میخواهید فقط دایرکتوری را از آن رشته به دست آورید، پرسش و پاسخ شماره 73 را بررسی کنید.
آیا متوجه میشوید که چطور این مشکل به طور مسخرهای پیچیده میشود؟ و این هنوز درست سادهترین حالتی است که در آن، مفروضات زیادی در مورد انتقال نیافتن اسکریپت، و در لوله نبودن آن ایجاد نمودهایم!
به طور کلی، ذخیره فایلها در همان شاخه برنامههایشان عادت نامناسبی است. طرحبندی فایل سیستم یونیکس فرض میکند که فایلهای موجود در یک محل(به عنوان مثال /bin) برنامههای اجرایی هستند، در حالیکه فایلها در محل دیگر(به عنوان مثال /etc) فایلهای داده هستند. (تصور کنید برای یک لحظه از این ارثیه سیستمهای یونیکس، با قرار دادن برنامه ها در /etc صرفنظر کنیم، میتوانیم....)
در اینجا، به جای تلاش در انجام امر غیر ممکن، برخی جایگزینهای عقل سلیم را ملاحظه خواهید نمود:
واقعاً نگهداری پیکربندی اسکریپت خودتان در محل منفرد و ثابت از قبیل /etc/foobar.conf بیش از همه مفهوم واقع میشود.
اگر لازم است که چند فایل پیکربندی تعیین کنید، آنوقت میتوانید یک دایرکتوری (مثلاً /var/lib/foobar/ یا /usr/local/lib/foobar/) داشته باشید، و مکان آن دایرکتوری را از یک محل ثابت مانند /etc/foobar.conf بخوانید.
حتی اگر نمیخواهید که زیاد hard-coded(مترجم: نوشتن دادهها یا رفتار به طور مستقم در برنامه، احتمالاً در چند محل به طوری که به آسانی نمیتواند اصلاح شود) بشود، میتوانید محل foobar.conf(یاخود دایرکتوری پیکربندی) را به عنوان پارامتر به اسکریپت بدهید.
اگر نیاز دارید اسکریپت در صورت فقدان /etc/foobar.conf، پیشفرض معینی در نظر بگیرد، میتوانید پیش فرضها را در خود اسکریپت قرار بدهید، یا اگر /etc/foobar.conf غایب است، به موردی مانند $HOME/.foobar.conf بازگشت بدهید.
BASH_SOURCE احتمالاً ایده بسیار بهتری از $0 است، نتایج بهتری ارائه میکند و بهتر تعریف میشود. شاید این مقاله باید با در نظر داشتن BASH_SOURCE نوشته میشد. --Lhunath
پرسش و پاسخ 28 (آخرین ویرایش 2011-08-22 18:38:21 توسط GreyCat)
دو فرآیند نا مرتبط نمیتوانند از شناسهها، محیط یا stdin/stdout برای مراوده استفاده کنند، برخی اشکال ارتباط درون فرآیندی(IPC) لازم است.
پردازش A در یک فایل مینویسد، و پردازش B فایل را میخواند. این شیوه همگام شده نمیباشد، بنابراین اگر در حالیکه A در فایل مینویسد، B بتواند آنرا بخواند، یک روش مطمئن نیست. شاید قفل دایرکتوری، یا سیگنال بتواند کمک کند.
mkdir میتواند برای بررسی وجود یک شاخه و ایجاد آن در یک عملیات تجزیه ناپذیر به کار برود، بدین ترتیب میتواند به عنوان یک قفل به کار برود، گرچه روش خیلی مؤثری نیست.
اسکریپت A:
until mkdir /tmp/dir;do # تا بتوانیم شاخه را ایجاد نماییم منتظر میماند
sleep 1
done
echo foo > file # نوشتن در فایل، این بخش حساس است
rmdir /tmp/dir # پاک کردن قفل
اسکریپت B:
until mkdir /tmp/dir;do #انتظار تا بتوانیم دایرکتوری را ایجاد کنیم
sleep 1
done
read var < file # خواندن فایل، این بخش حساس است
echo "$var" # نمیتواند در فایل بنویسد A اسکریپت
rmdir /tmp/dir # حذف قفل
برای مثالهای بیشتر قفل دایرکتوری، پرسش و پاسخ شماره 45 و mutex را ببینید.
سیگنالها احتمالاً سادهترین شکل IPC(مترجم: مخفف InterProcess Communication یا ارتباط درون فرآیندی) میباشند:
اسکریپت A:
trap 'flag=go' USR1 # USR1 تنظیم اداره کننده سیگنال برای سیگنال
# echo $$ > /tmp/ScriptA.pid # را در یک فایل ذخیره کنیم pid اگر بخواهیم
flag=""
while [[ $flag != go ]]; do # B انتظار برای چراغ سبز از اسکریپت
sleep 1;
done
echo we received the signal
شما باید pid اسکریپت دیگر را برای ارسال یک سیگنال جهت کشتن آن، بدانید یا پیداکنید :
#kill all the pkill -USR1 -f ScriptA # خود را در فایل ذخیره کرده باشد pid شماره A اگر اسکریپت kill -USR1 $(</var/run/ScriptA.pid) # :فرزند باشد A اگر اسکریپت ScriptA & pid=$! kill -USR1 $pid
دو روش اول ضد گلوله نیستند و اگر شما بیش از یک نمونه از اسکریپت A را اجرا نمایید موجب دردسر خواهند شد.
لولههای با نام شکل بیشتر توانمند IPC هستند. آنها در صفحه خودشان تشریح شدهاند: NamedPipes.
پرسش و پاسخ 27 (آخرین ویرایش 2010-05-11 05:25:30 توسط p5B09615D)
یک راهکار برای تصادفی نمودن سطرها در یک فایل چنین است. این روش، تولید یک عدد تصادفی است که پیشوند هر سطر میشود ، سپس سطرهای حاصل، مرتب میگردند و اعداد حذف میشوند.
#bash
randomize() {
while IFS='' read -r l ; do printf '%d\t%s\n' "$RANDOM" "$l"; done |
sort -n |
cut -f2-
}
RANDOM توسط BASH و KornShell پشتیبانی میشود، اما در posix تعریف نگردیده است.
این روش نیز همان ایده است(چاپ اعداد تصادفی در ابتدای سطر، و مرتب نمودن سطرها نسبت به آن ستون) با استفاده از برنامههای دیگر:
# Bourne
awk '
BEGIN { srand() }
{ print rand() "\t" $0 }
' |
sort -n | # مرتب نمودن عددی نسبت به اولین ستون(عدد تصادفی)
cut -f2- # حذف ستون مرتبسازی
این روش(احتمالاً ) سریعتر از راه حل قبلی است، اما در نگارشهای خیلی قدیمی AWK کار نمیکند("nawk" یا "gawk"، یا اگر موجود است /usr/xpg4/bin/awk را امتحان کنید). (توجه نمایید که awk زمان epoch(مترجم: تعداد ثانیههای سپری شده از نیمه شب اول ژانویه سال ۱۹۷۰ که برای یونیکس به عنوان مبداء زمان تلقی میشود) را برای تابع srand() استفاده میکند، که شاید برای شما به اندازه کافی تصادفی نباشد)
نگارش تعمیم یافته این پرسش میتواند, چگونه میتوانم عناصر یک آرایه را بُر بزنم(درهم و برهم نمایم)؟ باشد. اگر نخواهیم از رویکرد تا اندازهای بدترکیبِ مرتبسازی سطرها استفاده کنیم، در واقع این کار پیچیدهتر از آنست که به نظر میرسد. یک روش خام نتایج جهتدار بدشکل به ما ارائه خواهد نمود. الگوریتم پیچیدهتر(و صحیح) مشابه این مورد میباشد:
# استفاده از متغیر آرایه سراسری. بایدتوپُر باشد(نه آرایه پراکنده)
# Bash syntax.
shuffle() {
local i tmp size max rand
# جهت دار است $RANDOM به علت محدودیت دامنه $RANDOM % (i+1)
# .با استفاده از دامنهای که ضریبی از تعداد عناصر آرایه است اصلاح میشود
size=${#array[*]}
max=$(( 32768 / size * size ))
for ((i=size-1; i>0; i--)); do
while (( (rand=$RANDOM) >= max )); do :; done
rand=$(( rand % (i+1) ))
tmp=${array[i]} array[i]=${array[rand]} array[rand]=$tmp
done
}
این تابع عناصر یک آرایه را با استفاده از الگوریتم بُر زنی Knuth-Fisher-Yates در جا بُر میزند.
پرسش دیگری که من بارها دیدهام، چگونه میتوانم سطر تصادفی فایلی را چاپ نمایم؟ میباشد. در اینجا مشکل آنست که، لازم است شما از قبل بدانید فایل شامل چند سطر است. بدون این آگاهی، باید شما یکبار تمام فایل را برای شمارش سطرها بخوانید -- یا، تمام فایل را به داخل آرایه جذب کنید. اجازه بدهید هر دو رویکرد رابررسی کنیم.
# Bash
n=$(wc -l < "$file") # شمارش تعداد سطرها
r=$((RANDOM % n + 1)) # عدد تصادفی از یک تا n
sed -n "$r{p;q;}" "$file" # r چاپ سطر شماره
#posix with awk
awk -v n="$(wc -l<"$file")" 'BEGIN{srand();l=int((rand()*n)+1)} NR==l{print;exit}' "$file"
( برای اطلاعات بیشتر در مورد چاپ سطر شماره n این پرسش و پاسخ را ببینید.)
مثال بعد تمامی فایل را به داخل حافظه جذب میکند. این رویکرد زمان بازکردن مجدد فایل را صرفهجویی میکند، اما به وضوح، حافظه بیشتری مصرف میکند. (احتمالاً: در سیستمهایی با حافظه کافی و نهانگاه دیسک مؤثر، با روشهای قبلی تمام فایل را به حافظه خواندهاید، مگر اینکه حافظه کافی برای انجام آن موجود نباشد،که در چنین حالتی نمیتوانید، که میباید ثابت میشد.)
# Bash
unset lines i
while IFS= read -r 'lines[i++]'; do :; done < "$file" # See FAQ 5
n=${#lines[@]}
r=$((RANDOM % n)) # see below
echo "${lines[r]}"
توجه نمایید که ما در این مثال 1 را به عدد تصادفی اضافه نمیکنیم، زیرا آرایه سطرها از صفر شماره گذاری میشود.
همچنین، بعضی اشخاص میخواهند یک فایل اتفاقی از یک دایرکتوری را انتخاب کنند(برای امضاء یک e-mail، یا انتخاب یک فایل صوتی اتفاقی برای اجرا، یا یک تصویر تصادفی برای نمایش، و غیره). تکنیک مشابهی که میتواند به کار برود:
# Bash
files=(*.ogg) # Or *.gif, or *
n=${#files[@]} # برای زیبایی گرایی
xmms -- "${files[RANDOM % n]}" # انتخاب یک عضو تصادفی
توجه کنید که این چند نمونه اخیر از یک ضریب ساده متغیر RANDOM استفاده میکنند، بنابر این، نتایج جهتدار است. اگر این مشکلی برای برنامه شما میباشد، از تکنیکهای ضد جهت مثال Knuth-Fisher-Yates فوق استفاده کنید.
دیگر برنامههای غیرقابل حمل:
shuf در Coreutils گنو(در نسخههای اخیر coreutils)
در خصوص coreutils گنو، از نگارش 6.9 برنامه sort گنو دارای یک گزینه -R (مستعار --random-sort)میباشد. به طور شگفتانگیز، فقط بامناطق عمومی کار میکند:
LC_ALL=C sort -R file # خروج سطرها به طور تصادفی
LC_ALL=POSIX sort -R file # خروج سطرها به طور تصادفی
LC_ALL=en_US sort -R file # صرفنظر میکند -R به طور مؤثر از گزینه
برای جزئیات بیشتر، info coreutils sort یا یک راهنمای معادل را ببینید.
> http://lists.gnu.org/archive/html/bug-bash/2010-01/msg00042.html به یک دام شگفتآور مرتبط با استفاده از RANDOM بدون مقدم نمودن $ در زمینههای محاسباتی معین، اشاره میکند. (خلاصه: تقریباً در هر وضعیتی باید n=$((...math...)); ((array[n]++)) را بر ((array[...math...]++)) ترجیح بدهید.)
رفتار تشریح شده، در نگارشهای جاری mksh، ksh93، Bash، و Zsh به طور معکوس ظاهر میشود. بازهم موردی برای به خاطر سپردن به واسطه ماترک. -ormaaj
پرسش و پاسخ 26 (آخرین ویرایش 2012-11-27 14:25:08 توسط geirha)