منبع اصلی نوشتار زیر در این لینک قرار دارد

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


پرسش و پاسخ‌های رایج Bash  در Greg\'s Wiki

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

چگونه می‌توانم یک رشته را با رشته دیگری در یک متغیر، جریان داده، یک فایل، یا تمام فایلهای یک شاخه، تعویض نمایم؟

یک تعداد ابزار برای این مورد در دسترس است. که استفاده از هریک به عوامل زیادی بستگی دارد، البته مهمترین عامل آن است که چه چیزی را ویرایش می‌کنیم.

فایلها

ویرایش فایلها مستلزم دقت است. تنها ابزار استاندارد که به طور واقعی فایل را ویرایش می‌کند ed است. سایر روشها می‌توانند به کار بروند، اما آنها با فایل موقتی و mv درگیر می‌شوند (یا ابزارهای غیر استاندارد، یاملحقات POSIX).

ed ویرایشگر خط فرمانی استاندارد UNIX است. در اینجا برخی ترکیب های دستوری رایج برای تعویض رشته olddomain.com با رشته newdomain.com در فایلی به نام file آمده است. هر چهار فرمان کار یکسانی را با درجات متفاوتی از قابلیت حمل و کارایی انجام می‌دهند:

# Bash   در پوسته
ed -s file <<< $\'g/olddomain\\.com/s//newdomain.com/g\\nw\\nq\'

#  printf با وجود  Bourne  در پوسته 
printf \'%s\\n\' \'g/olddomain\\.com/s//newdomain.com/g\' w q | ed -s file

printf \'g/olddomain\\.com/s//newdomain.com/g\\nw\\nq\' | ed -s file

#  printf بدون وجود Bourne در پوسته    
ed -s file <<!
g/olddomain\\.com/s//newdomain.com/g
w
q
!

برای تعویض یک رشته در تمام فایلهای شاخه جاری، فقط یکی از موارد فوق را در حلقه قرار بدهید:

for file in ./*; do
    [[ -f $file ]] && ed -s \"$file\" <<< $\'g/old/s//new/g\\nw\\nq\'
done

برای انجام این کار به صورت بازگشتی، آسانترین راه فعال نمودن globstar در bash نگارش 4 (‎shopt -s globstar‎، یک ایده خوب، قرار دادن این دستور در فایل ‎~/.bashrc‎ خودتان است) و استفاده از این کد است:

for file in ./**/*; do
    [[ -f $file ]] && ed -s \"$file\" <<< $\'g/old/s//new/g\\nw\\nq\'
done

اگر شما bash نگارش 4 ندارید، می‌توانید از find استفاده کنید. متأسفانه، تغذیه ورودی استاندارد ed با یکایک فایلها اندکی کسل کننده است:

find . -type f -exec bash -c \'printf \"%s\\n\" \"g/old/s//new/g\" w q | ed -s \"$1\"\' _ {} \\;

اگر متغیرهای پوسته به عنوان رشته‌های جایگزینی یا جستجو به کار بروند، ed مناسب نیست. نه sed، نه هیچ ابزاری که از عبارتهای منظم استفاده ‌کند، مناسب نمی‌باشند. استفاده از کُد awk همراه با تغییر مسیرها و mv را در انتهای این پرسش و پاسخ ملاحظه کنید.

gsub_literal \"$search\" \"$rep\" < \"$file\" > tmp && mv tmp \"$file\"

1. کاربرد ابزارهای غیر استاندارد

sed ویرایشگر جریانی است، نه ویرایشگر فایل. با وجود این، اشخاص اصرار دارند آن را به طور نامناسب در هرجایی جهت ویرایش فایلها به کار ببرند. این برنامه فایلها را ویرایش نمی‌کند. sed گنو(و بعضی sedهای BSD) دارای گزینه ‎ -i‎ می‌باشند که یک کپی ایجاد می‌کند و فایل اصلی را با کپی تعویض می‌کند. یک عملیات پر خرج، اما اگر شما از کُد غیرقابل حمل، I/O بالاسری، اثرات جانبی نامساعد(از قبیل خرابی پیوندهای نمادین) لذت می‌برید، این می‌تواند یک انتخاب باشد:

sed -i    \'s/old/new/g\' ./*  # GNU
sed -i \'\' \'s/old/new/g\' ./*  # FreeBSD

افرادی از شما که پرل نگارش 5 دارند، می‌توانند همان کار را با استفاده از این کُد انجام بدهند:

perl -pi -e \'s/old/new/g\' ./*

کاربرد بازگشتی find:

find . -type f -exec perl -pi -e \'s/old/new/g\' {} \\;  # شما هنوز + ندارد find اگر
find . -type f -exec perl -pi -e \'s/old/new/g\' {} +   # اگر دارد

اگر در عوض جایگزینی می‌خواهید سطرها را حذف کنید:

 # را حذف می‌کند foo  هر سطر شامل عبارت منظم پرل   
perl -ni -e \'print unless /foo/\' ./*

برای جایگزینی تمام \"unsigned\" با \"unsigned long\" در همان مثال، در صورتی که \"unsigned int\" یا \"unsigned long\" نباشند ...:

find . -type f -exec perl -i.bak -pne \\
    \'s/\\bunsigned\\b(?!\\s+(int|short|long|char))/unsigned long/g\' {} \\;

تمام مثالهای فوق از عبارتهای منظم استفاده می‌کنند، به آن معنی که، دارای مشکلاتی همانند کُد sed قبلی می‌باشند، ایده آزمایش تعبیه متغیرهای پوسته در آنها ایده‌ای ترسناک است، و رفتار با یک کمیت انتخابی به عنوان رشته لفظی، در بهترین حالت نیز ناراحت کننده است.

علاوه براین، پرل، بدون آنکه استعداد نهانی تصادم با کاراکترهای علائم را داشته باشد، می‌تواند برای عبور دادن متغیرها به درون هم رشته‌های جستجو و هم رشته‌های جایگزین شونده به کار برود:

in=\"input (/string\" out=\"output string\" perl -pi -e $\'$quoted_in=quotemeta($ENV{\'in\'}); s/$quoted_in/$ENV{\'out\'}/g\' ./*

یا، ساده‌تر:

in=$search out=$replace perl -pi -e \'s/\\Q$ENV{\"in\"}/\\Q$env{\"out\"}/g\' ./*

متغیرها

اگر یک متغیر است، این کار می‌تواند(و باید) به طور خیلی ساده با بسط پارامتر Bash انجام بشود:

var=\'some string\'; search=some; rep=another

 # Bash در پوسته
var=${var//\"$search\"/$rep}

در POSIX خیلی دشوارتر است:

 # POSIX تابع

 #  string_rep SEARCH REPL STRING  طرز استفاده  ‎
 # تعویض می‌کند STRING  در REPL را با SEARCH تمام نمونه‌های  
string_rep() {
 # ارزش گذاری متغیرها
  in=$3
  unset out

 #  نباید خالی باشد SEARCH متغیر
  test -n \"$1\" || return

  while true; do
    #  نباشد حلقه قطع می‌شود\"$in\" دیگر در SEARCH اگر 
    case \"$in\" in
      *\"$1\"*) : ;;
      *) break;;
    esac

 # الحاق می‌کند \"$out\" را به REPL و SERCH راتا رسیدن به اولین نمونه \"$in\" محتوای 
    out=$out${in%%\"$1\"*}$2
 #  حذف می‌کند \"$in\" و هر چه قبل از آن هست را از SEARCH اولین نمونه 
    in=${in#*\"$1\"}
  done

# پیوست و نتیجه را چاپ می‌کند $out مانده است به \"$in\" در SEARCH آنچه پس از آخرین 
  printf \'%s%s\\n\' \"$out\" \"$in\"
}

var=$(string_rep \"$search\" \"$rep\" \"$var\")
#    :توجه  

POSIX راهی برای محلی نمودن متغیرها ندارد. اکثر پوسته‌ها(حتی dash وbusybox) دارند # به هرحال اگر پوسته شما پشتیبانی می‌کند، با خیال راحت آنرا انجام بدهید. اما اگر # پشتیبانی نمی‌کند، حتی اگر تابع را به صورت ‎ var=$(string_rep ...)‎ فراخوانی کنید، # باز هم تابع در یک پوسته فرعی اجرا خواهد شد و هیچ تخصیصی ایستادگی نخواهد نمود. #

در مثال bash، نقل‌قولها در اطراف ‎ \"$search\"‎ از این که با متغیرها به عنوان الگوی پوسته(که همچنین به عنوان glob نیز شناخته می‌شوند) رفتار شود، پیش‌گیری می‌کنند. البته، اگر مطابقت الگو مورد نظر است، نقل‌قولها را الحاق نکنید. اگر ‎ \"$rep\"‎ نقل‌قولی شده بود، به هرحال با نقل‌قولها به صورت لفظی رفتار می‌گردید.

بسط‌های پارامترِ مانند این، به طور مفصل‌تر در پرسش و پاسخ شماره 100 بحث شده است.

جریانها

اگر جریان است، بنابراین از stream editor(ویرایشگر جریانی sed) استفاده کنید:

some_command | sed \'s/foo/bar/g\'

sed از عبارت‌های منظم استفاده می‌کند. در مثال ما، foo و bar رشته‌های لفظی هستند. اگر آنها متغیر بودند(به عنوان مثال، ورودی کاربر)، به منظور اجتناب از خطاها، آنها می‌بایست با دقت بسیار پوشش داده می‌شدند. این کار خیلی غیر عملی است، و کوشش برای انجام آن کُد شما را بینهایت مستعد باگها می‌نماید. تعبیه متغیرهای پوسته در فرمانهای sed هرگز ایده خوبی نیست.

شما این کار را می‌توانستید در خود Bash توسط ترکیب بسط پارامتر با پرسش و پاسخ شماره 1 انجام بدهید:

search=foo; rep=bar

while IFS= read -r line; do
  printf \'%s\\n\' \"${line//\"$search\"/$rep}\"
done < <(some_command)

some_command | while IFS= read -r line; do
  printf \'%s\\n\' \"${line//\"$search\"/$rep}\"
done

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

شاید متوجه شده باشید، به هرحال آن حلقه bash بالا برای مجموعه بزرگ داده‌ها خیلی کُند است. بنابراین چطور موردی پیدا کنیم که بتواند رشته‌های لفظی را تعویض نماید؟ خوب، می‌توانستید AWK را به کار ببرید. تابع ذیل با خواندن از ورودی استاندارد و نوشتن در خروجی استاندارد، تمام نمونه‌های STR را با REP تعویض می‌کند.

# gsub_literal STR REP      : نحوه استفاده 
# stdout و نوشتن در stdin تعویض می‌کند با خواندن از REP را با STR تمام نمونه‌های 
gsub_literal() {
  # نمی‌تواند خالی باشد STR
  [[ $1 ]] || return

  # string manip needed to escape \'\'s, so awk doesn\'t expand \'\\n\' and such
  awk -v str=\"${1//\\/\\}\" -v rep=\"${2//\\/\\}\" \'
    # طول رشته جستجو را به دست می‌آورد
    BEGIN {
      len = length(str);
    }

    {
      # رشته خروجی تهی
      out = \"\";

      # ادامه حلقه تا موقعی که رشته جستجو در سطر هست
      while (i = index($0, str)) {
        # پیوست کردن هر چیز قبل از رشته جستجو و رشته جایگزین 
        out = out substr($0, 1, i-1) rep;

        # حذف اولین رشته جستجو و هر چیز قبل از آن از سطر ‎
        $0 = substr($0, i + len);
      }

      # پیوست نمودن آنچه باقی مانده است
      out = out $0;

      print out;
    }
  \'
}

some_command | gsub_literal \"$search\" \"$rep\"


# :خلاصه شده در یک سطر
some_command | awk -v s=\"${search//\\/\\}\" -v r=\"${rep//\\/\\}\" \'BEGIN {l=length(s)} {o=\"\";while (i=index($0, s)) {o=o substr($0,1,i-1) r; $0=substr($0,i+l)} print o $0}\'


CategoryShell

پرسش و پاسخ 21 (آخرین ویرایش ‎ 2012-08-12 19:11:59 ‎ توسط e36freak)