فواصل الأسطر CRLF وLF: ما الذي يتعطل فعليًا
الفرق بين CRLF وLF بايت واحد. فـLF محرف واحد \n (0x0A)، وهو فاصل السطر على Linux وmacOS، أما CRLF فبايتان، \r\n (0x0D 0x0A)، وبهما ينهي Windows أسطره.
وهنا الجزء الذي تخطئ فيه معظم المقالات: سكربت الصدفة (shell) ذو نهايات CRLF لا يفشل في العادة. بل يعمل ويطبع ما تتوقّعه ويخرج بالرمز 0. ما يتعطّل فعلاً هو قيمة متغيّر، لأن عملية الإسناد احتفظت بـ\r في آخرها دون أن يشتكي منها أحد.
والسلوك الذي قِسناه ينقسم إلى أربعة أنماط، مرتّبة بحسب صعوبة ملاحظتها:
- نجاح صامت. المخرجات تبدو صحيحة، ومتغيّر يحمل
\rغير مرئي، ورمز الخروج 0. - خطأ لا يغيّر شيئاً. رسالة
: command not foundعند سطر فارغ، والسكربت يواصل حتى نهايته، ورمز الخروج 0. - خطأ صياغي. تنكسر
ifوforوتعريفات الدوال، ورمز الخروج 2. bad interpreter. سطر shebang يحمل\r، ورمز الخروج 126.
وقبل أي شيء آخر، شغّل file yourfile. سطر واحد من المخرجات يحسم أيّهما لديك: CRLF أم LF.
كل ما يلي مقيس على macOS (Darwin arm64) باستخدام bash 3.2.57(1)-release وzsh 5.9 وdash وgit 2.55.0 وnode v26.7.0 وPython 3.14.6، ومُتحقَّق منه تقاطعياً على Linux عبر
docker run --rm bash:5، وهو GNU bash 5.3.15(1)-release (aarch64-unknown-linux-musl).
1. CRLF وLF وCR: ما هي البايتات فعلياً
الاختصار CRLF يعني carriage return line feed (إرجاع العربة ثم تغذية السطر)، وهو حرفياً محرفان ملتصقان:
| الاسم | رمز الهروب | البايت |
|---|---|---|
| إرجاع العربة (carriage return) | \r | 0x0D |
| تغذية السطر (line feed) | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
وكل منصّة تكتب واحداً منها:
| المنصّة | فاصل السطر |
|---|---|
| Windows | CRLF (\r\n) |
| Linux وmacOS الحديث | LF (\n) |
| Mac Classic وOS 9 وما قبله | CR (\r) وحده |
والأسماء ميكانيكية الأصل. ففي آلة التلكس (teletype)، كان «إرجاع العربة» يعيد رأس الطباعة إلى الهامش الأيسر، وكانت «تغذية السطر» تلفّ الورقة صفاً واحداً إلى أعلى. احتفظ Windows بالحركتين معاً في بايتين، واكتفى Unix بواحدة. ولا يزال CR المنفرد يظهر في التصديرات القديمة، والملف المليء به يبدو لمعظم أدوات Unix سطراً واحداً طويلاً.
أمر واحد يخبرك أيّهما لديك
يقرأ file البايتات ويسمّي فاصل السطر:
$ file d1.txt
d1.txt: ASCII text, with CRLF line terminators
$ file d2.txt
d2.txt: ASCII text ← no suffix means pure LF
$ file d3.txt
d3.txt: ASCII text, with CRLF, LF line terminators ← mixed, file lists both
والحالة الثالثة هي التي تحتاج انتباهاً. فالملف ذو النهايات المختلطة يعني أن أداتين بإعدادات مختلفة كتبتا فيه بالتناوب: محرّر حفظ بـLF، وسكربت ألحق بـCRLF، ودمج جمع بين نسختين. والأمر od -c يُظهر الانقسام بايتاً بايتاً:
$ od -c d3.txt
0000000 a \r \n b \n
0000005
وفواصل الأسطر مسألة بايتات، تجاور ترميز المحارف. ودليل ترميز UTF-8 وUTF-16 يغطّي ما يجري في الطبقة الأعلى منها مباشرة.
2. أنماط الفشل الأربعة، من النجاح الصامت إلى الخروج بالرمز 126
الملف نفسه بنهايات \r\n، وخمس نتائج مختلفة بحسب ما يحتويه السطر:
| محتوى السكربت | ما يحدث فعلياً | الخروج |
|---|---|---|
echo hello وسائر الأوامر البسيطة | نجاح صامت، والمخرجات صحيحة | 0 |
إسناد X=abc | نجاح صامت، لكن القيمة تنتهي بـ\r | 0 |
الأسطر الفارغة وأسطر المتابعة المنتهية بـ\ | : command not found، والسكربت يواصل حتى النهاية | 0 |
if/fi وfor/do/done وf() { | syntax error near unexpected token | 2 |
سطر shebang يحمل \r | bad interpreter: No such file or directory | 126 |
والصفّان الأولان هما ما يغيب عن معظم الشروح. فعبارة «CRLF يسبّب command not found» تتكرّر في كل مكان، وليست هي ما يحدث. الأوامر البسيطة لا تشتكي إطلاقاً.
النجاح الصامت هو الأخطر
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
سطران دخلاً وسطران خرجاً، والخروج بالرمز 0. ليس هناك ما تصحّحه، والسجل خالٍ ممّا تبحث عنه بـgrep. لكن أعطِ السكربت نفسه مقارنةً ينفّذها، فينقلب الحال:
$ printf 'V=1.2.3\r\ntest "$V" = "1.2.3" && echo MATCH || echo NO-MATCH\r\n' > v.sh
$ bash v.sh
NO-MATCH
[exit=0]
المتغيّر $V يحمل 1.2.3\r لا 1.2.3. والنتيجة متطابقة على macOS وLinux. وبوّابات الإصدار، وif [ "$ENV" = "prod" ]، ورايات المزايا (feature flags) المقروءة من ملف، كلّها تسلك الفرع الخاطئ بهدوء وتخرج بالرمز 0. وهذه أصعب علل فواصل الأسطر اكتشافاً داخل CI، لأن البناء أخضر والسجل نظيف.
الخطأ الذي لا يوقف شيئاً
السطر الذي لا يحتوي إلا على \r هو الشكل الذي يتّخذه السطر الفارغ داخل ملف بنهايات CRLF، والصدفة تعامله كأمر يجب تنفيذه. فيفشل، ويطبع رسالة، ثم ينتقل السكربت إلى السطر التالي وينتهي برمز الخروج 0. أما الصياغة الدقيقة للرسالة فتختلف من صدفة إلى أخرى.
وتنكسر أسطر المتابعة المنتهية بشرطة مائلة عكسية بالطريقة نفسها. فالمحرف \r يقع بين الشرطة المائلة العكسية وفاصل السطر، فتبطل المتابعة، ويُنفَّذ السطر التالي وحده.
أخطاء الصياغة، وسبب أن fi ليست fi
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
وbash 5.3.15 على Linux يقول عن الملف نفسه أكثر من ذلك:
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
أما الحلقات وتعريفات الدوال فتفشل عند السطر 1 بدلاً من ذلك:
c_for.sh: line 1: syntax error near unexpected token `do'
c_for.sh: line 1: `for i in 1 2; do'
c_func.sh: line 1: syntax error near unexpected token `{'
c_func.sh: line 1: `f() {'
وسبب هذا الصنف كلّه واحد، ولهذا يسهل توقّعه. فـbash يقرأ الكلمة الختامية بوصفها fi\r لا fi. وfi\r ليست الكلمة المفتاحية fi، فلا تُغلق كتلة if أبداً، ويظلّ bash يقرأ حتى ينفد الملف، ولهذا يشير الخطأ إلى السطر الأخير بدل السطر المعطوب.
bad interpreter والخروج بالرمز 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
يطبع macOS وLinux النص نفسه، ويخرج كلاهما بالرمز 126. اقرأ المسار في الرسالة: /bin/bash^M. فالنواة تأخذ كل ما بعد #! حتى فاصل السطر بوصفه مسار المفسّر، والمحرف \r جزء منه. ولا وجود لملف بهذا الاسم، فيفشل التنفيذ قبل أن يعمل سطر واحد من سكربتك.
ورمز الخروج 126 يغطّي أيضاً حالة «موجود لكنه غير قابل للتنفيذ»، فالسكربت الذي يرفض الإقلاع ليس بالضرورة مشكلة فواصل أسطر؛ إذ تُنتج أذونات الملفات إخفاقات من العائلة نفسها. والمحرف ^M داخل المسار هو ما يفرّق بين الحالتين.
لماذا لا ترى \r أبداً
قراءة المخرجات لا تنفع، لأن هذا البايت لا يترك أثراً تراه على الشاشة. عليك أن تُجبره على الظهور:
$ bash d.sh # script: HOST=example.com / echo "connecting to $HOST:8080"
connecting to example.com:8080 ← what you get
$ bash d.sh | cat -v
connecting to example.com^M:8080^M ← what is actually there
$ bash d.sh | od -c
0000000 c o n n e c t i n g t o e x
0000020 a m p l e . c o m \r : 8 0 8 0 \r
0000040 \n
بايتان من \r، غير مرئيين في المخرجات العادية، وكلاهما داخل سلسلة على وشك أن تُستخدم اسمَ مضيف. وقد ينتقل البايت إلى داخل وسيط أمر، فيظهر الخطأ باسم أداة لا ذنب لها:
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
الأداة head تتصرّف تصرّفاً سليماً تماماً. فالسكربت مرّر إليها 2\r. وأي رسالة خطأ يظهر فيها \r شارد داخل القيمة المقتبَسة هي العلّة نفسها وقد جاءت باسم أداة أخرى.
3. لماذا لا تشبه رسالة خطئك تلك التي تجدها على الإنترنت
ملف واحد، هو printf 'echo a\r\n\r\necho b\r\n' وسطره الثاني سطر فارغ لا يحوي إلا \r، مُشغَّلاً تحت أربع صَدَفات:
| الصدفة | الإصدار | نص الرسالة بالضبط |
|---|---|---|
| bash (المرفق مع macOS) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash (على توزيعات Linux السائدة) | 5.3.15(1)-release | t.sh: line 2: $'\r': command not found |
| zsh | 5.9 | s_blank.sh:2: command not found: ^M |
| dash | — | s_blank.sh: 2: : not found |
تكاد كل نتائج البحث تقتبس الصفّ الثاني. فالإصداران 4 و5 من bash يطبعان المحارف غير القابلة للطباعة باقتباس ANSI-C، وهو ما يحوّل «إرجاع العربة» إلى $'\r'. أما bash المرفق مع macOS فإصداره 3.2 ولا يفعل ذلك، فتحصل على نقطتين رأسيتين ومسافة ولا شيء بينهما. وzsh يطبع ^M. وdash يُسقط كلمة command كلياً.
عطب واحد وأربع رسائل. فإن لصقت رسالة خطئك الحرفية في صندوق بحث ولم تعد بشيء مفيد، فهذا هو السبب. والرسالة العارية : command not found هي العلّة نفسها التي في الرسالة الشهيرة.
4. Git: ما الذي يخزّنه core.autocrlf فعلياً في مستودعك
تتوقّف معظم شروح git autocrlf عند التعريفات، بينما ما يهمّك عملياً هو ما ينتهي إليه الملف داخل الإيداع (commit)، وما الذي يحصل عليه زملاؤك عند السحب. قِسنا ذلك بـgit cat-file -p HEAD:f.txt للكائن المخزَّن، وبـrm f.txt && git checkout -- f.txt لنسخة العمل:
core.autocrlf | الملف المصدر | الكائن في المستودع | نسخة العمل بعد السحب |
|---|---|---|---|
true | CRLF | LF | CRLF |
true | LF | LF | CRLF |
input | CRLF | LF | LF |
input | LF | LF | LF |
false | CRLF | CRLF | CRLF |
false | LF | LF | LF |
ويقول هذا الجدول ثلاثة أشياء:
- القيمتان
trueوinputتضمنان كلتاهما LF في المستودع. والفارق الوحيد عند السحب:trueتحوّل عائدةً إلى CRLF، وinputتترك الملف كما هو. - القيمة
falseوحدها هي التي تُدخل CRLF في الإيداع. فحين يسأل أحدهم مَن أودع محارف «إرجاع العربة»، فهذا الصفّ هو الجواب. - الصفّ الثاني هو المفاجأة. فتحت
true، الملف الذي كان LF على القرص يعود CRLF بعد السحب.
وGit يعلن عن إعادة الكتابة قبل حدوثها، بإحدى صيغتين:
warning: in the working copy of 'f.txt', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'f.txt', CRLF will be replaced by LF the next time Git touches it
«لم أغيّر شيئاً، لكن git يقول إن الملف كلّه تغيّر»
الصفّ الثاني مرة أخرى. فمع git config core.autocrlf true، يعيد السحب كتابة ملفات LF إلى CRLF في دليل العمل. وصار كل سطر يختلف عن الكائن المخزَّن ببايت واحد، فيبلّغ git diff عن كل سطر بوصفه معدَّلاً، ويُظهر طلب السحب ملفاً لم يمسّه أحد وكأنه أُعيدت كتابته بالكامل. مرشِّح السحب هو الفاعل.
والصورة المعكوسة تنتج الضجيج نفسه: زميل يعمل بـfalse يودع CRLF، وأنت على input، فتظهر في فروقك (diff) ملفات لم تفتحها قط.
5. .gitattributes هو الجواب على مستوى الفريق
الإعداد core.autocrlf محلي على جهازك وحده، ولا يراه غيرك. أما .gitattributes فملف داخل المستودع، ينتقل مع كل استنساخ. وحين يختلف الاثنان، تفوز الخصائص. جرّبنا الصيغ الأربع كلها:
.gitattributes | core.autocrlf | المصدر | الكائن | بعد السحب | الفائز |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | الخصائص |
* text eol=crlf | input | LF | LF | CRLF | الخصائص |
* -text | true | CRLF | CRLF | CRLF | الخصائص |
* text eol=lf | true | CRLF | LF | LF | الخصائص |
والصفّ الثالث يستحقّ الحفظ: الخاصية -text تعطّل التحويل تماماً، وتتغلّب رغم ذلك على core.autocrlf=true. وبها تحمي الملفات التي يجب أن تبقى بايتاتها كما هي.
ملف .gitattributes جاهز للنسخ
* text=auto
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png -text
*.jpg -text
*.pdf -text
*.zip -text
تُطبِّع text=auto كل ما يكتشفه Git بوصفه نصاً إلى LF داخل المستودع. وأسطر eol=lf الصريحة تغطّي الملفات التي يجب أن تكون LF أياً كان كاتبها، لأن ملف .sh بنهايات CRLF خروجٌ بالرمز 126 ينتظر وقوعه. وتأخذ أنواع سكربتات Windows القيمة eol=crlf للسبب المعاكس، وتأخذ أنماط الملفات الثنائية -text كي لا يُحوَّل فيها شيء البتة.
core.safecrlf وcore.eol
إعدادان يظهران بجوار core.autocrlf ويفعلان شيئاً مختلفاً:
core.safecrlfحارس لا محوِّل. فحين لا يكون التحويل قابلاً للعودة إلى أصله، كما في ملف مختلط يفقد التطبيع فيه معلومات، ترفضtrueالعملية، وتمرّرهاwarnمع تحذير. وهو لا يغيّر أبداً أيّ البايتات تُخزَّن؛ إنما يرفض أن ينفّذ العمليات المُفقِدة للمعلومات في صمت.core.eolيختار أيّ فاصل سطر يكتبه Git في دليل العمل للملفات الموسومة بـtext، حين تكونcore.autocrlfبقيمةfalse. وقيمهlfوcrlfوnative. ويتجاوزهcore.autocrlf، ولهذا يبدو ضبطcore.eolعلى جهاز ما زال autocrlf مفعّلاً فيه وكأنه لم يفعل شيئاً.
تغيير .gitattributes لا يصلح الملفات المُودَعة سلفاً
تُطبَّق الخصائص حين يكتب Git ملفاً أو يقرأه، فيبقى المحتوى القائم على حاله حتى يعيد شيء ما كتابته. أجرِ هذا المرور بنفسك:
$ git add --renormalize .
$ git commit -m "Normalize line endings"
توقّع فرقاً ضخماً واحداً، وهذا هو المقصود. نفّذه على فرع مستقلّ وادمجه في إيداع واحد، ونبّه الفريق قبل أن يصل إليهم.
6. تحويل CRLF إلى LF، والعكس
أربع طرق، تحقّقنا من أن جميعها يزيل \r على Darwin:
| الأمر | النتيجة |
|---|---|
tr -d '\r' < f > f.out | يعمل |
perl -pi -e 's/\r\n/\n/g' f | يعمل |
sed -i '' -e 's/\r$//' f (صيغة BSD) | يعمل |
sed -i -e 's/\r$//' f (صيغة GNU، مُشغَّلة على macOS) | يعمل، ويخلّف وراءه ملفاً زائداً |
فخّ sed -i بصيغة GNU على macOS
تشترط sed في BSD أن يتبع -i لاحقةُ نسخة احتياطية. انسخ درساً من Linux حرفياً، فتبتلع sed في BSD الوسيط -e بوصفه تلك اللاحقة. التحرير يقع فعلاً، لكنه يترك وراءه هذا:
a5.txt
a5.txt-e
الملف a5.txt-e نسخة احتياطية، أُنشئت لأن sed قرأت -e بوصفها اللاحقة التي طلبتها. والصيغة الصحيحة على macOS تمرّر سلسلة فارغة صريحة: sed -i '' -e 's/\r$//' f. والمستودع الذي أُودعت فيه ملفات تنتهي بـ-e هو مستودع شغّل فيه أحدهم سطراً أحادياً من GNU على جهاز Mac.
لا يوجد dos2unix في macOS
لا يعيد command -v dos2unix شيئاً على نظام macOS خارج الصندوق؛ فالملف التنفيذي يأتي من brew install dos2unix. ولهذا يفشل أكثر الأجوبة تداولاً على الإنترنت حين يجرّبها كثير من المطوّرين على أجهزتهم. أما tr -d '\r' فلا يحتاج تثبيتاً ويؤدّي المهمة نفسها.
وفي الاتجاه المعاكس، يعاني unix2dos مشكلة التوافر نفسها، ويصلح sed -e 's/$/\r/' بديلاً عنه.
وحين يعود الملف إلى LF نظيف، يصير آمناً أن تعامله قائمةَ أسطر من جديد. وهذا مهمّ لكل ما يقارن الأسطر بوصفها سلاسل كاملة: فأداتا ترتيب أسطر النص وحذف الأسطر المكرّرة تقرآن value\r وvalue سطرين مختلفين، فيكفي محرف «إرجاع عربة» شارد واحد ليُفسد إزالة التكرار في صمت.
داخل محرّرك
يعرض VS Code فاصل السطر للملف الحالي بوصفه CRLF أو LF في شريط الحالة أسفل يمين النافذة، والنقر عليه يبدّل الملف. ويتحكّم الإعداد files.eol في القيمة الافتراضية للملفات الجديدة، وضبطه على مستوى مساحة العمل يُبقي الفريق المختلط متّسقاً. وتوفّر المحرّرات الأخرى الضابطين نفسيهما بأسماء أخرى. وما يغفل عنه كثيرون أن مؤشّر الملف الواحد والإعداد الافتراضي منفصلان: تغيير أحدهما لا يمسّ الآخر.
7. التعامل مع فواصل الأسطر داخل الشيفرة
ملف واحد محتواه line1\r\nline2\r\n، مقروءاً عبر سبع نقاط دخول:
| نقطة الدخول | ما تحصل عليه | \r |
|---|---|---|
Node — fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | محفوظ |
Node، القيمة نفسها + .split("\n") | ["line1\r","line2\r",""] | محفوظ في كل سطر |
Node — readline مع crlfDelay:Infinity | ["line1","line2"] | مُزال |
Python — open(f) بالوضع الافتراضي | 'line1\nline2\n' | مُحوَّل |
Python — open(f).readlines() | ['line1\n','line2\n'] | مُحوَّل |
Python — open(f, newline="") | 'line1\r\nline2\r\n' | محفوظ |
Python — open(f,"rb") | b'line1\r\nline2\r\n' | محفوظ |
هذا الجدول يحسم بلاغ علّة مألوفاً: «يعمل في Python وينكسر في Node». فالوضع النصي الافتراضي في Python يطبّق «الأسطر الجديدة العامة» (universal newlines) ويترجم \r\n إلى \n قبل أن تراها أصلاً. أما Node فيناولك البايتات كما هي. ولا أحد منهما مخطئ، لكنّ الفارق يقع لحظة قراءة الملف نفسه.
rstrip("\n") يترك \r وراءه
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
تزيل rstrip("\n") المحارف التي أدرجتها بالضبط، و\r لم يكن ضمن القائمة. فتصير النتيجة غير مساوية لكل ما يفترض أن تطابقه، وهذا تفسير «أزلتُه ومع ذلك لا يتساوى». استعمل strip()، أو rstrip() بلا وسيط، فتذهب كل المسافات البيضاء اللاحقة بما فيها محرف «إرجاع العربة».
تقسيم الأسطر بأمان على المنصّتين
في JavaScript، قسّم على نمط يتحمّل الفاصلين معاً: text.split(/\r?\n/). وفي Python، إما أن تبقى في الوضع النصي الافتراضي وتدع «الأسطر الجديدة العامة» تتولّى الأمر، وإما أن تستدعي splitlines() التي تتعامل مع \r\n و\n و\r المنفرد.
والكتابة هي النصف الآخر. فـNode يكتب البايتات التي تعطيه إياها، فابنِ سلاسلك بـ\n ودع .gitattributes يقرّر ما يهبط على القرص. أما open(path, "w") في Python فيترجم \n إلى فاصل المنصّة ما لم تمرّر newline=""، وهي الراية التي تطلبها كاتبات CSV لهذا السبب بالذات.
محرف «إرجاع العربة» في آخر السطر وعلامة ترتيب البايتات (BOM) في أول الملف هما الصنف نفسه من العلل من طرفين متقابلين: بايت غير مرئي ينجو من النسخ واللصق ويكسر اختبار التساوي. ودليل استكشاف أخطاء علامة BOM في UTF-8 يغطّي ذلك الطرف.
8. أين تؤذيك فواصل الأسطر أيضاً
CSV وExcel. يجعل المعيار RFC 4180 فاصل CRLF حدّاً بين السجلات، ما يجعل CSV أحد المواضع القليلة التي يكون فيها CRLF صواباً لا عيباً. والمحلّلات المكتوبة لمدخلات LF وحدها تترك \r في آخر حقل من كل صفّ، فإن بدت القيم الناتجة من تحويل CSV إلى JSON صحيحة لكنها لا تتساوى عند المقارنة، فافحص ذلك البايت أولاً.
Docker. ملف .sh بنهايات CRLF يُنسخ داخل صورة هو نمط الفشل الرابع. فالتعليمة COPY تحفظ البايتات، ويحتفظ سطر shebang بـ\r، وتخرج الحاوية بالرمز 126. وسطر واحد *.sh text eol=lf في .gitattributes يمنع هذا الصنف كلّه.
الفروق وطلبات السحب. الملف الموسوم بأنه تغيّر بالكامل دون تحرير مرئي هو آلية القسم 4 وقد وصلت إلى مراجعة الشيفرة. ومقارنة النسختين بأداة مقارنة النصوص تؤكّد ذلك في ثوانٍ، ودليل مقارنة النصوص يشرح كيف تقرأ النتيجة.
الملفات المختلطة. الناتج ASCII text, with CRLF, LF line terminators يعني كاتبين بإعدادات مختلفة. طبّع الملف كلّه بدل الأسطر التي صادف أنك حرّرتها، وإلا كان الفرق التالي بالضجيج نفسه.
الأسئلة الشائعة
ما الفرق بين CRLF وLF؟
CRLF بايتان، \r\n (0x0D 0x0A). وLF بايت واحد، \n (0x0A). يكتب Windows فاصل CRLF، ويكتب Linux وmacOS فاصل LF، وكلاهما يعلّم نهاية السطر. والنص يبدو متطابقاً في المحرّر؛ ولا يظهر الفرق إلا في البايتات ومقارنات السلاسل والفروق.
سكربتي بنهايات CRLF، فلماذا لا يظهر أي خطأ؟
لأن الأوامر البسيطة تنجو من البايت الزائد. فـecho hello مع \r لاحق يعمل ويخرج بالرمز 0. ولا تظهر الأخطاء إلا حيث يهتمّ المحلّل: سطر فارغ، أو كلمة مفتاحية مثل fi، أو سطر shebang. أما عمليات الإسناد فهي الحالة الخطرة، لأنها تنجح وتخزّن \r داخل المتغيّر.
ماذا تعني $'\r': command not found، ولماذا لا أراها؟
تعني أن الصدفة حاولت تنفيذ سطر لا يحوي إلا محرف «إرجاع العربة». والإصداران 4 و5 من bash يطبعان ذلك المحرف باقتباس ANSI-C، فينتج $'\r'. أما bash 3.2 المرفق مع macOS فلا يطبع شيئاً بين النقطتين، وzsh يطبع ^M بدلاً منه. عطب واحد وثلاث رسائل مختلفة.
هل ينبغي أن تكون core.autocrlf بقيمة true أم input أم false؟
استعمل input على Linux وmacOS، وtrue على Windows، وفضّل .gitattributes على كليهما. وبالقياس، تخزّن true وinput كلتاهما LF في المستودع، وfalse وحدها تسمح بدخول CRLF إلى الإيداع. وتزيد true بأنها تعيد كتابة ملفات LF إلى CRLF في نسخة عملك عند السحب.
إذا اختلف .gitattributes مع core.autocrlf، فأيّهما يفوز؟
يفوز .gitattributes. فالصيغ الأربع المختبَرة كلها (* text=auto و* text eol=crlf و* -text و* text eol=lf) تجاوزت قيمة core.autocrlf المحلية. وهذه هي الحجّة لاستعماله: الخصائص تُودَع في المستودع وتسري على الجميع، بينما core.autocrlf إعداد خاص بكل جهاز لا تستطيع رؤيته ولا فرضه.
كيف أتحقّق ممّا إذا كان الملف يستعمل CRLF أم LF؟
شغّل file yourfile. فمع CRLF يطبع ASCII text, with CRLF line terminators، ومع LF الخالص يطبع ASCII text بلا أي لاحقة إطلاقاً، ومع الملف المختلط يطبع with CRLF, LF line terminators. وللتيقّن على مستوى البايتات، شغّل od -c وابحث عن \r قبل كل \n.
متى ينبغي أن تستعمل CRLF فعلاً؟
حين يشترطه تنسيق أو بروتوكول. فالمعيار RFC 4180 يعرّف CRLF فاصلاً بين سجلات CSV، وترويسات HTTP وبروتوكول SMTP يفعلان الشيء نفسه على السلك. وملفات Windows batch وPowerShell أسلم بنهايات CRLF أيضاً. وفي كل ما عدا ذلك، من شيفرة مصدرية وسكربتات صدفة وملفات إعداد، استعمل LF.