چگونه تفاوت بین دو فایل را سریع و دقیق پیدا کنیم؟ با ابزار خط فرمان diff و همتایان آن آشنا شوید.
فرض کن دو نسخه از یک کد داری و میخواهی ببینی چه تفاوتی بین آنها وجود دارد. یک راه این است که هر دو فایل را کنار هم در ویرایشگر باز کنی و خطبهخط آنها را با چشم مقایسه کنی.
اما این روش بسیار خطاپذیر است. مغز انسان برای تشخیص تغییرات جزئی در میان حجم زیادی از متن، چندان قابل اعتماد نیست. بهراحتی ممکن است یک تفاوت کوچک را نادیده بگیری و بعداً دچار سردرگمی شوی.
💡 هرچه فایل بزرگتر باشد، مقایسه دستی دشوارتر و اشتباهآمیزتر میشود. خوشبختانه ابزارهای خودکاری وجود دارند که این کار را برای ما انجام میدهند.
ابزار خط فرمان diff یک راه حل عالی برای مقایسه خودکار فایلهاست. این دستور میتواند تفاوت بین دو فایل یا حتی دو پوشه را در قالبهای مختلف نمایش دهد.
با استفاده از diff، دیگر نیازی نیست خودت به دنبال تغییرات بگردی؛ ابزار دقیقاً به تو نشان میدهد کدام خطها تغییر کردهاند، کدام خطها حذف شدهاند و کدام خطها اضافه شدهاند.
⚡ diff یکی از پرکاربردترین دستورات در خط فرمان است و در کنار Git و سایر سیستمهای کنترل نسخه، به شما کمک میکند تغییرات را بهتر درک کنید.
بیایید با یک مثال ساده شروع کنیم. دو فایل به نامهای rearrange1.py و rearrange2.py داریم که هر دو شامل یک تابع برای مرتبسازی نام هستند. ابتدا با دستور cat محتوای آنها را میبینیم:
$ cat rearrange1.py
#!/usr/bin/env python3
import re
def rearrange_name(name):
result = re.search(r"^([\w .]*), ([\w .]*)$", name)
if result == None:
return name
return "{} {}".format(result[2], result[1])
$ cat rearrange2.py
#!/usr/bin/env python3
import re
def rearrange_name(name):
result = re.search(r"^([\w .-]*), ([\w .-]*)$", name)
if result == None:
return name
return "{} {}".format(result[2], result[1])
اگر دقت کنی، تفاوت در الگوی عبارت منظم (regex) است. در فایل اول از [\w .]* استفاده شده، اما در فایل دوم [\w .-]* به کار رفته که کاراکتر - را هم پوشش میدهد. شاید با چشم این تفاوت را ببینی، اما در فایلهای بزرگتر پیدا کردن چنین تغییراتی سخت است.
حالا بیاییم از دستور diff استفاده کنیم تا تفاوت را دقیقاً ببینیم:
$ diff rearrange1.py rearrange2.py
6c6
< result = re.search(r"^([\w .]*), ([\w .]*)$", name)
---
> result = re.search(r"^([\w .-]*), ([\w .-]*)$", name)
خروجی فقط خطهای متفاوت را نشان میدهد. در اینجا:
< نشاندهنده خطی است که در فایل اول وجود داشته و حذف شده است.
> نشاندهنده خطی است که در فایل دوم اضافه شده است.
6c6 یعنی در هر دو فایل، خط شماره ۶ تغییر کرده است (حرف c مخفف changed).
بنابراین میفهمیم که دقیقاً یک خط با خط دیگری جایگزین شده است. این نوع تغییر در کدنویسی بسیار رایج است.
📌 خروجی diff بهخوبی نشان میدهد که کدام بخشها دستخوش تغییر شدهاند و به ما کمک میکند تا بدون سردرگمی، تفاوتها را درک کنیم.
بیایید مثال دیگری را بررسی کنیم. دو فایل validations1.py و validations2.py که در آن تغییرات بیشتری وجود دارد.
$ diff validations1.py validations2.py
5c5,6
< assert (type(username) == str), "username must be a string"
---
> if type(username) != str:
> raise TypeError("username must be a string")
11a13,15
> return False
> # Usernames can't begin with a number
> if username[0].isnumeric():
خروجی این بار دو بخش دارد:
5c5,6 یعنی خط ۵ در فایل اول با دو خط (۵ و ۶) در فایل دوم جایگزین شده است.
11a13,15 یعنی بعد از خط ۱۱ در فایل اول، سه خط جدید (۱۳ تا ۱۵) در فایل دوم اضافه شده است. حرف a مخفف added است.
اما ممکن است بخش دوم کمی گنگ به نظر برسد؛ چرا که فقط خطوط اضافه شده را نشان میدهد، بدون اینکه بگوید کدام شرط مربوط به کدام بخش کد است.
در خروجی diff، بخشها با شماره خطوط و یک حرف مشخص میشوند:
c به معنای تغییر (change) است – یک یا چند خط با خطوط دیگر عوض شدهاند.
a به معنای اضافه (add) است – خطهای جدیدی بعد از یک خط مشخص اضافه شدهاند.
d به معنای حذف (delete) است – یک یا چند خط حذف شدهاند (در این مثال نداشتیم).
در مثال دوم، بخش 11a13,15 به این معناست که بعد از خط ۱۱ فایل اول، خطوط ۱۳ تا ۱۵ فایل دوم اضافه شدهاند. اما چون زمینه (context) کافی نداریم، ممکن است متوجه نشویم که این اضافهها مربوط به چه بخشی از کد هستند.
💡 برای درک بهتر تغییرات، میتوانیم از حالت یکپارچه (unified) استفاده کنیم که زمینه بیشتری را نشان میدهد.
با اضافه کردن پرچم -u به دستور diff، خروجی را به صورت یکپارچه (unified) دریافت میکنیم:
$ diff -u validations1.py validations2.py
--- validations1.py 2019-06-06 14:28:49.639209499 +0200
+++ validations2.py 2019-06-06 14:30:48.019360890 +0200
@@ -2,7 +2,8 @@
def validate_user(username, minlen):
- assert type(username) == str, "username must be a string"
+ if type(username) != str:
+ raise TypeError("username must be a string")
if minlen < 1:
raise ValueError("minlen must be at least 1")
@@ -10,5 +11,8 @@
return False
if not username.isalnum():
return False
+ # Usernames can't begin with a number
+ if username[0].isnumeric():
+ return False
return True
در این حالت:
- (منفی) مشخص شدهاند، از فایل اول حذف شدهاند.
+ (مثبت) مشخص شدهاند، به فایل دوم اضافه شدهاند.
این خروجی بسیار واضحتر است و به راحتی میتوان دید که در فایل دوم، یک بلاک if جدید اضافه شده که بخشی از زنجیره شرطهاست.
⚠️ حالت unified به ویژه وقتی تغییرات زیاد هستند یا به هم مرتبطاند، بسیار مفید است و در ابزارهایی مثل Git نیز از همین قالب استفاده میشود.
diff محبوبترین ابزار مقایسه فایلهاست، اما تنها گزینه نیست. برخی ابزارها تفاوتها را به شکل دیگری نمایش میدهند:
این ابزارها با ارائه زمینه بصری بیشتر، درک تغییرات را آسانتر میکنند، مخصوصاً وقتی با کدهای بزرگ یا چندین نسخه مواجه هستیم.
🚀 جمعبندی: diff و ابزارهای مشابه به ما کمک میکنند تا تغییرات بین فایلها را سریع، دقیق و بدون خطای انسانی تشخیص دهیم. در جلسهی بعدی خواهیم دید که چگونه از این تفاوتها برای اعمال تغییرات (patch) استفاده کنیم.
🎉 با اتمام این جلسه، شما با ابزار قدرتمند diff برای مقایسهی فایلها آشنا شدید و آمادهی یادگیری patch هستید.