آموزش‌های خط فرمانی

این وبلاگ تلاش می‌کند گامی در حد بضاعت در جهت آموزش خط فرمان و اسکریپت‌نویسی پوسته گنو-لینوکس بردارد.

آموزش‌های خط فرمانی

این وبلاگ تلاش می‌کند گامی در حد بضاعت در جهت آموزش خط فرمان و اسکریپت‌نویسی پوسته گنو-لینوکس بردارد.

پرسش و پاسخ شماره ۳۱



تفاوت بین test و ‎[‎ و ‎ [[‎ چیست؟

[‎(فرمان test) و ‎ [[‎ (فرمان جدید test) برای ارزیابی عبارتها به کار می‌روند. ‎ [[‎ فقط در پوسته Bash و Korn کار می‌کند، و قدرتمندتر می‌باشد،‎ [‎ و test در پوسته‌های POSIX معتبر هستند. چند مثال در اینجا آمده است:

    if [ -z "$variable" ]
    then
        echo "variable is empty!"
    fi

    if [ ! -f "$filename" ]
    then
        echo "not a valid, existing file name: $filename"
    fi

و

    if [[ ! -e $file ]]
    then
        echo "directory entry does not exist: $file"
    fi

    if [[ $file0 -nt $file1 ]]
    then
        echo "file $file0 is newer than $file1"
    fi

برای کوتاهی گفتار: test ترکیب قدیمی و قابل حمل فرمان را اجرا می‌کند. تقریباً در تمام پوسته‌ها(قدیمی‌ترین پوسته‌های Bourne استثنا هستند)، ‎[‎ مترادف test است(اما به یک شناسه انتهایی‎ ] ‎ احتیاج دارد). هر چند تمام پوسته‌های مدرن دارای پیاده‌سازی داخلی ‎[‎ می‌باشند، به طور معمول بازهم یک برنامه اجرایی خارجی با آن نام وجود دارد، به عنوان مثال ‎ /bin/[‎. استاندارد POSIX یک مجموعه ویژگی الزامی برای ‎ [‎ تعریف نموده است، اما تقریباً هر پوسته‌ای ملحقاتی را با آن ارائه می‌کند. بنابراین، اگر کُد قابل حمل می‌خواهید، باید مراقب باشید که آن ملحقات را به کار نبرید.

[[‎ نگارش بهبودیافته‌ای از آن است، و یک کلمه کلیدی است، نه یک برنامه. این موضوع استفاده از آن را به طوری که در ذیل نشان داده شده آسانتر می‌سازد. ‎[[‎ توسط پوسته Korn و BASH (به عنوان مثال نگارش 2.03) شناخته می‌شود، اما توسط پوسته‌های قدیمی‌تر POSIX یا پوسته Bourne خیر.

اگر چه ‎ [‎ و ‎[[‎ اشتراک فراوانی دارند، و در عملگرهای بسیاری مانند ‎ "-f", "-s", "-n", "-z"‎ با هم سهیم می‌باشند، تفاوتهای قابل توجهی نیز وجود دارد. این هم یک لیست مقایسه‌ای:

ویژگی تست جدید [[ تست قدیمی [ مثال

مقایسه رشته‌ای

>

\> (*)

[[ a > b ]] || echo "a does not come before b"

<

\< (*)

[[ az < za ]] && echo "az comes before za"

= یا ==

=

[[ a == a ]] && echo "a equals a"

!=

!=

[[ a != b ]] && echo "a is not equal to b"

مقایسه عددی

-gt

-gt

[[ 5 -gt 10 ]] || echo "5 is not bigger than 10"

-lt

-lt

[[ 8 -lt 9 ]] && echo "8 is less than 9"

-ge

-ge

[[ 3 -ge 3 ]] && echo "3 is greater than or equal to 3"

-le

-le

[[ 3 -le 8 ]] && echo "3 is less than or equal to 8"

-eq

-eq

[[ 5 -eq 05 ]] && echo "5 equals 05"

-ne

-ne

[[ 6 -ne 20 ]] && echo "6 is not equal to 20"

ارزیابی شرطی

&&

-a (**)

[[ -n $var && -f $var ]] && echo "$var is a file"

||

-o (**)

[[ -b $var || -c $var ]] && echo "$var is a device"

گروه‌بندی عبارت

(...)

\( ... \) (**)

[[ $var = img* && ($var = *.png || $var = *.jpg) ]] &&
echo "$var starts with img and ends with .jpg or .png"

انطباق الگو

= یا ==

نا معتبر

[[ $name = a* ]] || echo "name does not start with an 'a': $name"

انطباق عبارت منظم

=~

نا معتبر

[[ $(date) =~ ^Fri\ ...\ 13 ]] && echo "It's Friday the 13th!"

(*) این یک پیوست به POSIX استاندارد است، برخی پوسته‌ها شاید آن را داشته باشند و برخی نداشته باشند.

(**) عملگرهای ‎ -a‎ و ‎-o‎، و ‎ گروه‌بندی ( ... )‎، در POSIX تعریف شده‌اند اما فقط برای وضعیت های شدیداً محدود. استفاده از اینها تشویق نمی‌شود، شما باید به جای آن از چندین ‎ [‎ استفاده کنید:

  • if [ "$a" = a ] && [ "$b" = b ]; then ...

  • if { [ "$a" = a ] || [ "$b" = b ] ; } && [ "$c" = c ]; then ...

گزینه‌های اولیه‌ای که در ‎[[‎ تعریف شده، اما شاید ‎ [‎ فاقد آن باشد(بسته به پیاده‌سازی):

شرح

گزینه‌ها

مثال

ورودی(فایل یا شاخه) موجود است

-e

[[ -e $config ]] && echo "config file exists: $config"

فایل جدیدتر یا قدیمی‌تر از دیگری است

-nt یا -ot

[[ $file0 -nt $file1 ]] && echo "$file0 is newer than $file1"

هر دو فایل یکسان هستند

-ef

[[ $input -ef $output ]] && { echo "will not overwrite input file: $input"; exit 1; }

نفی کردن

!

[[ ! -u $file ]] && echo "$file is not a setuid file"

لیکن تفاوتهای ظریف بیشتری وجود دارد.

  • تفکیک کلمه یا بسط glob بواسطه ‎[[‎ انجام نخواهد شد(و بنابراین بسیاری از شناسه‌ها نیازی به نقل‌قولی شدن ندارند):

     file="file name"
     [[ -f $file ]] && echo "$file is a file"

    با وجود اینکه ‎ $file‎ نقل‌قولی نشده و شامل فضای سفید می‌باشد، کار می‌کند. با ‎ [‎ لازم است متغیر نقل‌قولی بشود:

     file="file name"
     [ -f "$file" ] && echo "$file is a file"

    این امر استفاده از ‎ [[‎ را آسانتر و استعداد خطا را کمتر می‌کند.

  • در ‎ [[‎ نیازی به پوشش دادن(با کاراکتر گریز) پرانتزها نیست:

     [[ -f $file1 && ( -d $dir1 || -d $dir2) ]]
     [ -f "$file1" -a \( -d "$dir1" -o -d "$dir2" \) ]
  • از bash نگارش 4.1 مقایسه رشته با استفاده از ‎ <‎ یا ‎>‎ وقتی در ‎[[‎ انجام می‌شود منطقه جاری لحاظ می‌شود، اما در ‎[‎ یا test لحاظ نمی‌شود. در حقیقت، ‎[‎ و test، حتی اگر در صفحات man گفته باشد انجام می‌دهد، هرگز مرتب‌سازی منطقه‌ای به کار نبرده‌اند. در نگارشهای Bash قبل از 4.1 نیز ‎[[‎ مرتب‌سازی نسبت به منطقه به کار نمی‌برد.

به عنوان یک قاعده کلی، ‎ [[‎ برای رشته‌ها و فایلها به کار می‌رود. اگر می‌خواهید اعداد را مقایسه کنید، از یک عبارت محاسباتی استفاده کنید، به عنوان نمونه

# Bash
i=0
while ((i<10)); do ...

چه وقت باید فرمان تست جدید ‎ [[‎ به کار برود، و چه موقع فرمان قدیمی ‎ [‎؟ اگر قابلیت حمل به BourneShell اهمیت دارد، باید ترکیب قدیمی استفاده گردد. از طرف دیگر ، اگر اسکریپت به BASH یا KornShell احتیاج دارد، ترکیب جدید خیلی بیشتر قابل انعطاف است.

فصل شرط ها و بررسی‌ها در راهنمای Bash را ببینید.

اصول نظری

نظریه پشت تمام این مطالب آنست که ‎ [‎ فرمان ساده‌ایست، در جاییکه ‎[[‎ فرمانی مرکب است. ‎[‎ شناسه‌هایش را مانند هر فرمان دیگری دریافت می‌کند، اما اکثر فرمانهای مرکب زمینه تجزیه خاصی را ارائه می‌کنند که قبل از هرگونه پردازش دیگری انجام می‌شود. به طورنوعی این مرحله کلمات رزرو شده یا عملگرهای کنترلی مخصوص هر فرمان مرکب که آن را به قسمتهایی تجزیه می‌کند یا بر روند کنترل اثر می‌گذارد را جستجو می‌کند. عملگرهای and و or بررسی منطقی عبارت می‌توانند دور زده شوند، زیرا در این روش آنها خاص هستند(به عنوان مثال مانند ;;، elif، و else هستند). بر خلاف عبارت حسابی، که به طور معمول در آن تمام بسط‌ها از چپ به راست، با رشته نتیجه‌ای که به عنوان موضوع محاسباتی تفسیر می‌شود، انجام می‌گردند.

  • فرمان مرکب محاسباتی عملگرهای ویژه‌ای ندارد. فقط یک متن ارزیابی، یعنی یک عبارت محاسباتی منفرد دارد. عبارتهای محاسباتی نیز دارای عملگر ها هستند، که بعضی از آنها در ضمن مرحله ارزیابی حسابی(که آخر از همه رخ می‌دهد) بر روند کنترل اثر می‌گذارند .
     # Bash
     (( 1 + 1 == 2 ? 1 : $(echo ‎"‏آنچه شما فکر می‌کنید را انجام نمی‌دهد‏"‎ >&2; echo 1) ))
  • از طرف دیگر، عبارتهای تست، عملگرها را به عنوان بخشی از ترکیب دستوری خودشان دارند، که در انتهای دیگر طیف قرار می‌گیرند (اول ارزیابی شده).
     # Bash
     [[ '1 + 1' -eq 2 && $(echo ‎"‏اما این احتمالاً آنچه را انتظار دارید انجام می‌دهد‏‎" >&2) ]]
  • تست های سَبکِ قدیم راهی برای کنترل ارزیابی ندارند، زیرا شناسه‌های آنها ویژه نیستند.
        # Bash
        [ $((1 + 1)) -eq 2 -o $(echo 'No short-circuit' >&2) ]
  • احتمالاً قبل از انجام بسط ها، با جستجوی نشانه‌های ویژه فرمان مرکب، مدیریت خطای متفاوتی امکان پذیر می‌شود. ‎[[‎ می‌تواند حضور بسط‌هایی که یک کلمه نتیجه نمی‌دهند ولی بازهم اگر مشخص نشوند یک خطا می‌سازند را تشخیص بدهد. دستورات معمولی نمی‌توانند.

     # Bash 
     ( set -- $(echo 'بسط‌های غیر نقل‌قولی تهی پارامترهای تهی را نتیجه نمی‌دهند'‎ >&2); echo $# )
     [[ -z $(:) ]] && echo ' به عنوان یک شناسه فراهم شده و تهی ارزیابی نمی‌شود-z ‎'
     [ -z ] && echo ' به عنوان یک شناسه فراهم نشده و خطایی گزارش نمی‌کند-z‎'
      #را مجبور به تعیین یک شناسه نماید نیست Bash در اینجا راهی برای آنکه بتواند
     [[ -z ]]  #.این موجب یک خطا خواهد شد که دستورات معمولی نمی‌توانند تشخیص بدهند
  • به دلیل بسیار مشابهی، زیرا عملگرهای ‎ [‎ درست مثل شناسه‌ها هستند، برخلاف ‎ [[‎، شما می‌توانید برای فرمان معمولیtest عملگرها را به عنوان پارامترها تعیین نمایید. ممکن است این یک محدودیت برای ‎ [[‎ به نظر برسد، اما تقریباً همیشه بخش زیرین نسبتاً می‌چربد.

      # ksh93
    
      args=('0' '-gt' '1')
     
      (( $(print '0 > 1') )) # دستور معتبر، مطابق انتظار،  وضعیت خروج ۱ است 
      [ "${args[@]}" ]       #   این هم با ۱ خارج می‌شود
      [[ ${args[@]} ]]       # دستور معتبر، اما گمراه کننده. وضعیت خروج صفر است‎
      # می‌باشد [[ -n '0 -gt 1 ]] آشکار می‌کند که فرمان حاصل از بسط، set -x دستور 
  • به خاطر داشته باشید که کدام عملگرها متعلق به کدام ساختارهای پوسته هستند. ترتیب بسط ها می‌تواند باعث نتایج شگفت‌انگیز گردد، مخصوصاً موقع اختلاط و تودرتویی زمینه‌های متفاوت ارزیابی!
    # ksh93
    typeset -i x=0
        
    ( print "$(( ++x, ${ x+=1; print $x >&2;}1, x ))"      ) #  را چاپ می‌کند‎ 1,2
    ( print "$(( $((++x)), ${ x+=1; print $x >&2;}1, x ))" ) # را چاپ می‌کند ‎ 2,2 
                                               # به دلیل آنکه اول بسط انجام می‌شود 


CategoryShell

پرسش و پاسخ 31 (آخرین ویرایش ‎ 2012-05-27 19:22:57 ‎ توسط cpc18-rdng20-2-0-cust501)


پرسش و پاسخ شماره ۳۰



چگونه می‌توانم تمام فایل‌های ‎ *.foo‎ را به ‎*.bar‎ تبدیل نمایم، یا فاصله‌ها را به خط زیر تبدیل کنم، و یا حروف بزرگ نام فایلها را به حروف کوچک تبدیل کنم؟

برخی توزیع‌های گنو-لینوکس دارای فرمان ‎ 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


CategoryShell

پرسش و پاسخ 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
}

گر چه، در صورتی که پیوند نمادین در نام خود شامل ‎" -> "‎ باشد، این تابع می‌تواند با شکست مواجه شود.


CategoryShell

پرسش و پاسخ 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


CategoryShell

پرسش و پاسخ 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.


CategoryShell

پرسش و پاسخ 27 (آخرین ویرایش ‎ 2010-05-11 05:25:30 ‎ توسط p5B09615D)