C: الإدخال/الإخراج المتقدم وسطر الأوامر

وسيطات سطر الأوامر كالطلب في مطعم — تخبر البرنامج بما تريد (argv)، ويحسب البرنامج عدد العناصر المطلوبة (argc)، ثم يُعد طلبك.

1. وسيطات سطر الأوامر

(1) argc و argv

يمكن للدالة main أن تستقبل معاملين:

TEXT 📖 للعرض فقط
int main(int argc, char *argv[])
C
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Argument count: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

التشغيل:

BASH
./program hello world 123
TEXT 📖 للعرض فقط
Argument count: 4
argv[0] = ./program
argv[1] = hello
argv[2] = world
argv[3] = 123
💡 نصيحة: وسيطات سطر الأوامر كلها سلاسل نصية! إذا احتجت أرقامًا، يجب تحويلها باستخدام atoi أو atol أو strtol.

(2) تحليل الوسيطات عمليًا

في المشاريع الفعلية، تستخدم وسيطات سطر الأوامر عادةً بادئة - للخيارات:

TEXT 📖 للعرض فقط
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    int verbose = 0;
    int count = 1;
    const char *filename = NULL;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
            verbose = 1;
        } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            count = atoi(argv[++i]);
        } else if (argv[i][0] != '-') {
            filename = argv[i];
        } else {
            fprintf(stderr, "Unknown option: %s\n", argv[i]);
            return 1;
        }
    }

    if (filename == NULL) {
        fprintf(stderr, "Usage: %s [-v] [-n count] filename\n", argv[0]);
        return 1;
    }

    printf("File: %s, Count: %d, Verbose: %s\n",
           filename, count, verbose ? "on" : "off");
    return 0;
}
⚠️ ملاحظة: التحليل اليدوي للوسيطات عرضة للأخطاء. يمكن للمشاريع المعقدة استخدام دالة getopt التي تتعامل مع تركيبات الخيارات القصيرة والطويلة.


2. متغيرات البيئة

(1) getenv

C
char *getenv(const char *name);

تُرجع قيمة متغير البيئة، أو NULL إذا لم يكن موجودًا.

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = getenv("PATH");
    if (path != NULL) {
        printf("PATH = %s\n", path);
    }

    const char *home = getenv("HOME");
    if (home != NULL) {
        printf("HOME = %s\n", home);
    }
    return 0;
}

(2) المتغير الخارجي environ

تعرّف C متغيرًا شاملًا environ يشير إلى مصفوفة من سلاسل متغيرات البيئة جميعها:

C
#include <stdio.h>

extern char **environ;

int main(void) {
    char **env = environ;
    while (*env) {
        printf("%s\n", *env);
        env++;
    }
    return 0;
}
💡 نصيحة: تُستخدم متغيرات البيئة شائعًا لتهيئة سلوك البرنامج، مثل سلاسل اتصال قاعدة البيانات ومستويات التسجيل. هذا أكثر مرونة بكثير من تضمين القيم مباشرة في الكود المصدري.


3. معالجة الأخطاء بـ errno

(1) آلية errno

عندما تواجه دوال المكتبة القياسية أخطاء، لا تنهار عادةً مباشرة. بل تضبط المتغير الشامل errno وتُرجع قيمة خطأ:

C
#include <errno.h>

لا يتم تصفير errno تلقائيًا بعد استدعاء ناجح لدالة مكتبة! يجب فحصه فقط بعد فشل استدعاء دالة.

(2) perror / strerror

C
void perror(const char *s);
char *strerror(int errnum);

تُخرج perror s: رسالة الخطأ إلى stderr. تُرجع strerror الوصف النصي المقابل لرمز الخطأ.

TEXT 📖 للعرض فقط
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    FILE *fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        printf("Error code: %d, Error message: %s\n", errno, strerror(errno));
        return 1;
    }
    fclose(fp);
    return 0;
}
TEXT 📖 للعرض فقط
fopen: No such file or directory
Error code: 2, Error message: No such file or directory

▶ مثال

نمط معالجة الأخطاء الكامل:

C
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s filename\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        fprintf(stderr, "Cannot open '%s': %s\n", argv[1], strerror(errno));
        return 1;
    }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    if (ferror(fp)) {
        fprintf(stderr, "Read error occurred: %s\n", strerror(errno));
        fclose(fp);
        return 1;
    }

    fclose(fp);
    return 0;
}
▶ جرّب الكود
⚠️ ملاحظة: ferror يتحقق مما إذا تعرض تدفق لخطأ قراءة/كتابة، وهذا يختلف عن feof (الذي يتحقق من نهاية الملف).


4. التدفقات القياسية

تفتح C ثلاثة تدفقات تلقائيًا عند بدء البرنامج:

التدفق الاسم الجهاز الافتراضي واصف الملف
stdin الإدخال القياسي لوحة المفاتيح 0
stdout الإخراج القياسي الشاشة 1
stderr الخطأ القياسي الشاشة 2

الفرق بين stdout وstderr: stdout مُخزّن مؤقتًا، وstderr غير مُخزّن. عند إعادة التوجيه، لا يؤثر أحدهما على الآخر.

C
#include <stdio.h>

int main(void) {
    fprintf(stdout, "This is normal output\n");
    fprintf(stderr, "This is error output\n");
    return 0;
}
BASH
./program > output.txt

عند التشغيل بهذه الطريقة، يذهب "الإخراج الطبيعي" إلى الملف، بينما يظل "إخراج الخطأ" ظاهرًا على الشاشة.

(1) مبادئ إعادة التوجيه

في سطر الأوامر:

BASH
./program 2>&1 all.log

هذا الأمر يدمج stderr في stdout ويكتب كليهما في all.log.


5. الملفات المؤقتة

(1) tmpfile

C
FILE *tmpfile(void);

ينشئ ملفًا مؤقتًا مفتوحًا بالوضع "wb+". يُحذف الملف تلقائيًا عند إغلاقه أو انتهاء البرنامج.

C
#include <stdio.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    fprintf(tmp, "Temporary data %d\n", 42);
    rewind(tmp);

    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        printf("%s", buf);
    }

    fclose(tmp);
    return 0;
}
TEXT 📖 للعرض فقط
Temporary data 42

(2) tmpnam

C
char *tmpnam(char *s);

يُنشئ اسم ملف مؤقت لا يتعارض مع الملفات الموجودة. لكن هناك شرط سباق — بين إنشاء الاسم وإنشاء الملف، قد ينشئ برنامج آخر ملفًا بنفس الاسم.

⚠️ ملاحظة: فضّل tmpfile — فهو ينشئ الملف ويفتحه بشكل ذري، مما يجعله أكثر أمانًا. tmpnam ليس آمنًا في البيئات متعددة الخيوط.

▶ مثال

استخدام ملف مؤقت لمعالجة البيانات الوسيطة:

C 📖 للعرض فقط
#include <stdio.h>
#include <string.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    char *lines[] = {"banana", "apple", "orange", "grape"};
    int n = 4;

    for (int i = 0; i < n; i++) {
        fprintf(tmp, "%s\n", lines[i]);
    }

    rewind(tmp);
    printf("Before sorting:\n");
    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    rewind(tmp);
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            char a[64], b[64];
            long pos = ftell(tmp);
            fgets(a, sizeof(a), tmp);
            fgets(b, sizeof(b), tmp);
            a[strcspn(a, "\n")] = '\0';
            b[strcspn(b, "\n")] = '\0';

            if (strcmp(a, b) > 0) {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", b, a);
            } else {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", a, b);
            }
            fseek(tmp, pos + strlen(a) + strlen(b) + 2, SEEK_SET);
        }
        rewind(tmp);
    }

    rewind(tmp);
    printf("After sorting:\n");
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    fclose(tmp);
    return 0;
}
47 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
TEXT 📖 للعرض فقط
Before sorting:
  banana
  apple
  orange
  grape
After sorting:
  apple
  grape
  orange
  banana

6. الإخراج المنسق المتقدم

(1) snprintf

C
int snprintf(char *str, size_t size, const char *format, ...);

مقارنة بـ sprintf، يضيف snprintf معامل size لتحديد طول الكتابة، مما يمنع تجاوز المخزن المؤقت.

C
#include <stdio.h>

int main(void) {
    char buf[10];
    int n = snprintf(buf, sizeof(buf), "Hello, %s!", "World");
    printf("buf = \"%s\", required length = %d\n", buf, n);
    return 0;
}
TEXT 📖 للعرض فقط
buf = "Hello, Wo", required length = 13
💡 نصيحة: القيمة المُرجعة من snprintf هي الطول الذي يجب أن يكون عليه الإخراج المنسق، وليس عدد البايتات الفعلي المكتوب. إذا كانت القيمة المُرجعة أكبر من size-1، فهذا يعني أن الإخراج قُطع.


❓ أسئلة شائعة

س ما ترتيب الوسيطات في argv؟
ج argv[0] هو اسم البرنامج نفسه، وargv[1] فصاعدًا هي الوسيطات التي قدمها المستخدم، بنفس الترتيب في سطر الأوامر.
س لماذا تُخرج perror إلى stderr بدلاً من stdout؟
ج لأن رسائل الخطأ يجب ألا تضيع عند إعادة توجيه stdout إلى ملف. stderr غير مُخزّن ومستقل عن stdout، مما يضمن ظهور رسائل الخطأ دائمًا.
س متى يُصفّر errno إلى صفر؟
ج لا يُصفّر تلقائيًا أبدًا. يجب ضبط errno = 0 يدويًا قبل استدعاء دالة قد تفشل، ثم فحصها بعد أن تُرجع الدالة قيمة خطأ.

📖 ملخص

📝 تمارين

  1. اكتب برنامج سطر أوامر يقبل -c لعدّ الأحرف، و-l لعدّ الأسطر، و-w لعدّ الكلمات في ملف
  2. اكتب برنامجًا يستخدم tmpfile لعكس محتويات ملف (اكتب كل شيء في ملف مؤقت، ثم اقرأه بالترتيب العكسي)
  3. اكتب برنامجًا يوضح الفرق بين إعادة توجيه stdout وstderr، باستخدام > و2> لالتقاط كل نوع من الإخراج على حدة
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%