সরাসরি পর্বে যান
পর্ব ২৫ · ৫৬-এর মধ্যে · মডিউল ৪

কমান্ড-লাইন আর্গুমেন্ট ও variadic ফাংশন — প্রোগ্রামকে টুল বানানো

Command-Line Arguments (argc, argv) & Variadic Functions (stdarg.h)

Taught in Bangla, this lesson covers argc and argv in memory, safe number parsing with strtol, hand-written option and flag parsing, testing a CLI, environment variables and exit status, and writing variadic functions with stdarg.h along with their type-safety dangers.

এই পর্বে যা শিখবেন

  • Shell কীভাবে একটি কমান্ড-লাইনকে শব্দে ভেঙে argv বানায়, আর মেমরিতে সেটি দেখতে কেমন
  • লেখা থেকে নিরাপদে সংখ্যা বানানো, ভুল ইনপুটে পরিষ্কার error আর সঠিক exit status দেওয়া
  • -v, -n 5, --help-এর মতো অপশন নিজে পার্স করা, আর কখন getopt ব্যবহার করবেন
  • Environment variable, exit status আর pipe দিয়ে কয়েকটি টুল জোড়া লাগানোর চিন্তা
  • নিজের variadic ফাংশন লেখা, আর তার টাইপ-ঝুঁকি ও format string-এর নিরাপত্তা-ত্রুটি চেনা

১ · প্রোগ্রাম একটি যন্ত্র — আর্গুমেন্ট তার অর্ডার ফর্ম

পর্ব ২৩-এ আমরা রহিম চাচার দোকানের খাতার জন্য একটি CLI টুল বানিয়েছিলাম। সেটি চালানোর পর মেনু দেখাত, প্রশ্ন করত, উত্তরের জন্য অপেক্ষা করত। পর্ব ২৪-এ সেই খাতা ফাইলে জমা হলো — কিন্তু ফাইলের নাম ledger.csv লেখা ছিল কোডের ভেতরেই। অন্য নামের ফাইল খুলতে চাইলে কোড বদলে আবার compile করতে হতো।

এবার একটু অন্যভাবে ভাবুন। আপনি যখন terminal-এ gcc hello.c -o hello লেখেন, gcc আপনাকে কোনো প্রশ্ন করে না। কোন ফাইল, কী নামে বানাবে — সব তথ্য আপনি চালানোর মুহূর্তেই এক লাইনে দিয়ে দেন। gcc সেই নির্দেশ পড়ে কাজ সারে, তারপর একটি সংখ্যা ফেরত দিয়ে চলে যায় — সফল হলো কি না। এই এক লাইনের নির্দেশগুলোকেই বলে command-line argument.

পাড়ার চায়ের দোকানে গিয়ে আপনি বলেন, “মামা, দুধ চা, চিনি কম, দুই কাপ।” দোকানদার আপনাকে প্রশ্ন করে সময় নষ্ট করেন না — এক বাক্যেই পুরো অর্ডার। কমান্ড-লাইনও তাই: ./marks report.txt --top 3 মানে “marks নামের যন্ত্র, report.txt ফাইলটি নাও, সেরা ৩ জন দেখাও।” প্রোগ্রাম হলো দোকানদার, আর আর্গুমেন্ট হলো অর্ডার ফর্ম। ফর্ম ভুল হলে ভালো দোকানদার পরিষ্কার বলে দেন কী ভুল — চুপচাপ ভুল চা বানিয়ে দেন না।

মেনু-ভিত্তিক প্রোগ্রাম মানুষের সাথে কথা বলার জন্য ভালো। কিন্তু আর্গুমেন্ট-ভিত্তিক টুলের একটি বড় সুবিধা আছে: অন্য প্রোগ্রামও একে চালাতে পারে। একটি script রাত ২টায় হাজারটি ফাইলের ওপর আপনার টুল চালাতে পারে, কেউ বসে থেকে মেনুতে “১” চাপার দরকার নেই। এজন্যই git, gcc, docker, ffmpeg — পেশাদার জগতের প্রায় সব শক্তিশালী টুল আর্গুমেন্ট নিয়ে চলে।

মূল ধারণা

একটি টুলের সাথে বাইরের দুনিয়ার যোগাযোগ তিন পথে: ঢোকে আর্গুমেন্ট, environment variable আর stdin দিয়ে; বেরোয় stdout (আসল ফল) আর stderr (সমস্যার কথা) দিয়ে; আর শেষে ফেরত যায় একটি exit status — সফল না ব্যর্থ। এই পর্বে এই তিনটি পথই আমরা ঠিকমতো ব্যবহার করতে শিখব।

argc আর argv নাম দুটি এসেছে একেবারে প্রথম দিকের Unix আর C থেকে — Bell Labs-এ ১৯৭০-এর দশকে। Brian Kernighan ও Dennis Ritchie-র বিখ্যাত বই “The C Programming Language” (১৯৭৮)-এ echo প্রোগ্রাম দিয়ে এই ধারণাটি শেখানো হয়েছিল, আর সেই প্রথা এখনো চলছে। নাম দুটি শুধু প্রথা — চাইলে অন্য নাম দেওয়া যায়, কিন্তু সবাই এগুলোই লেখেন, তাই আপনিও লিখুন।

২ · argc আর argv — মেমরিতে কেমন দেখায়

পর্ব ০৪-এ আমরা int main(void) লিখেছিলাম — অর্থাৎ main কিছুই নেয় না। আর্গুমেন্ট পেতে হলে main-কে লিখতে হয় এভাবে:

int main(int argc, char *argv[])   /* অথবা: char **argv — একই জিনিস */
  • argc (argument count) — মোট কয়টি শব্দ এসেছে, প্রোগ্রামের নিজের নামসহ।
  • argv (argument vector) — char pointer-এর একটি অ্যারে। প্রতিটি ঘর একটি string-এর প্রথম অক্ষরের দিকে তাক করে থাকে। পর্ব ১৮-তে দেখেছেন, ফাংশনের parameter-এ char *argv[] আর char **argv একই অর্থ — অ্যারে আসলে pointer হয়ে আসে। তাই argv একটি pointer-to-pointer.

এবার দেখি ./marks report.txt --top 3 চালালে মেমরিতে আসলে কী তৈরি হয়। “পরে” চেপে এগোন:

./marks report.txt --top 3
argv[0]pointer
argv[1]pointer
argv[2]pointer
argv[3]pointer
argv[4]NULL
"./marks\0"৮ বাইট
"report.txt\0"১১ বাইট
"--top\0"৬ বাইট
"3\0"সংখ্যা নয়, লেখা!
argc = 4NULL গোনা হয় না
  1. প্রথম শব্দ — প্রোগ্রামের নাম। Shell পুরো লাইনটি পড়ে ফাঁকা জায়গা ধরে শব্দে ভাঙে। প্রথম শব্দ ./marks দিয়ে সে বোঝে কোন প্রোগ্রাম চালাতে হবে। মজার ব্যাপার, এই নামটিও আর্গুমেন্ট হিসেবে যায় — argv[0]-এ। এর পেছনের স্ট্রিংটি শেষ হয় \0 দিয়ে, পর্ব ১৯-এর মতোই।
  2. দ্বিতীয় শব্দ — argv[1]. report.txt-এর জন্য মেমরিতে একটি স্ট্রিং তৈরি হলো, আর argv[1] ঘরে রাখা হলো তার ঠিকানা। argv-এর ঘরে নিজে লেখা থাকে না — থাকে শুধু ঠিকানা।
  3. --top — argv[2]. এটি একটি অপশন, কিন্তু shell-এর কাছে অপশন বলে কিছু নেই। তার কাছে এটি স্রেফ আরেকটি শব্দ। কোনটি অপশন, কোনটি ফাইলের নাম — সেই বিচার আপনার প্রোগ্রামকেই করতে হবে।
  4. "3" — argv[3]. খেয়াল করুন, এটি দুই বাইটের একটি স্ট্রিং: অক্ষর '3' (ASCII 51) আর \0. এটি int 3 নয়। একে সংখ্যা বানাতে আলাদা কাজ লাগবে — পরের অংশে।
  5. শেষে NULL, আর argc = 4. C standard নিশ্চয়তা দেয় যে argv[argc] সবসময় NULL pointer. তাই argv-তে মোট ৫টি ঘর, কিন্তু argc = ৪ — NULL গোনা হয় না। চাইলে argc না দেখেও NULL পর্যন্ত লুপ চালানো যায়।
  6. মূল শিক্ষা: সবই লেখা। argv-তে যা আসে সবই char * — ফাইলের নাম, অপশন, সংখ্যা, সব। প্রোগ্রামের প্রথম দায়িত্ব এই লেখাগুলো যাচাই করে সঠিক টাইপে রূপান্তর করা। আর argv[1] পড়ার আগে নিশ্চিত হতে হবে argc > 1 — না হলে আপনি NULL বা তার পরের অজানা মেমরি পড়বেন।

ফাঁকা জায়গা, quote আর wildcard

শব্দ কোথায় ভাঙবে, সেটা ঠিক করে shell — আপনার প্রোগ্রাম নয়। নামের মধ্যে ফাঁকা জায়গা থাকলে quote দিতে হয়। নিচের টেবিলে দেখুন একই রকম দেখতে কমান্ডে argv কত আলাদা হয়:

আপনি লিখলেনargcargv[1], argv[2], …
./marks Rahim Uddin 80৪"Rahim", "Uddin", "80"
./marks "Rahim Uddin" 80৩"Rahim Uddin", "80" — quote চিহ্ন নিজে আসে না
./marks ""২"" — একটি খালি স্ট্রিং, তবুও একটি আর্গুমেন্ট
./marks১কিছু নেই; argv[1] হলো NULL
./marks *.txt (Linux/Mac)ফাইল যতগুলোshell নিজেই মিলে যাওয়া সব ফাইলের নাম বসিয়ে দেয়

শেষ সারিটির নাম wildcard expansion. Linux ও Mac-এর shell *.txt-কে প্রোগ্রাম চালানোর আগেই সব মিলে যাওয়া নামে বদলে দেয়, তাই আপনার প্রোগ্রাম কখনো * চিহ্নটি দেখে না। Windows-এ ব্যাপারটি আলাদা — সেখানে প্রোগ্রাম পুরো কমান্ড-লাইন একটি লম্বা লেখা হিসেবে পায়, আর C runtime library সেটিকে ভেঙে argv বানায়। ফলাফল প্রায় একই হলেও খুঁটিনাটি নিয়মে পার্থক্য আছে।

argv[0]-কে অন্ধভাবে বিশ্বাস করবেন না। সাধারণত এতে থাকে আপনি যেভাবে প্রোগ্রামের নাম লিখেছেন, যেমন ./marks বা /usr/bin/marks. কিন্তু যে প্রোগ্রাম আপনার প্রোগ্রাম চালায়, সে চাইলে argv[0]-এ যা খুশি বসাতে পারে, এমনকি argc ০-ও হতে পারে। তাই usage মেসেজে নাম দেখানোর জন্য argv[0] ঠিক আছে, কিন্তু নিরাপত্তার কোনো সিদ্ধান্ত এর ওপর নেবেন না।

৩ · লেখা থেকে সংখ্যা — atoi নয়, strtol

--top 3-এর "3"-কে সংখ্যা বানানোর সবচেয়ে পরিচিত উপায় atoi(argv[3]). ছোট, সহজ — আর বিপজ্জনক। atoi("abc") দেয় 0, atoi("0")-ও দেয় 0. দুটোর পার্থক্য বোঝার কোনো উপায় নেই! atoi("3x") চুপচাপ 3 দেয়, আর মান খুব বড় হলে standard অনুযায়ী ফল undefined behavior. অর্থাৎ ভুল ইনপুট এলে atoi কখনো আপনাকে জানায় না।

পেশাদার বিকল্প হলো strtol (string to long), <stdlib.h>-এ। এটি তিনটি জিনিস জানায়:

  1. ফল — রূপান্তরিত সংখ্যা, টাইপ long.
  2. কোথায় থামল — আপনি একটি char *end-এর ঠিকানা দেন, strtol সেখানে লিখে রাখে কোন অক্ষরে এসে পড়া থেমেছে। একে বলে end pointer. পুরো লেখা সংখ্যা হলে *end হবে '\0'.
  3. সীমা ছাড়াল কি না — সংখ্যা long-এ না ধরলে strtol LONG_MAX বা LONG_MIN ফেরত দেয় আর errno-তে বসায় ERANGE.

একটি সূক্ষ্ম কিন্তু জরুরি নিয়ম: সফল হলে strtol errno-কে 0 করে না। আগের কোনো ভুলের পুরোনো মান থেকে যেতে পারে। তাই ডাকার ঠিক আগে নিজে errno = 0 লিখতে হয়। নিচের ডায়াগ্রামে পাঁচটি ভিন্ন ইনপুটে strtol কী করে দেখুন:

strtol("42", &end, 10)গ্রহণযোগ্য
লেখা :  4  2  \0
                ^
               end      (*end == '\0' — পুরোটা সংখ্যা)
ফল   :  42
errno:  0
strtol("42abc", &end, 10)বাতিল
লেখা :  4  2  a  b  c  \0
              ^
             end        (*end == 'a' — পেছনে আবর্জনা)
ফল   :  42              atoi এটিকেই 42 বলে মেনে নিত
errno:  0
strtol("abc", &end, 10)বাতিল
লেখা :  a  b  c  \0
        ^
       end == লেখার শুরু  (একটি অঙ্কও পড়া যায়নি)
ফল   :  0               — কিন্তু এই 0 আসল 0 নয়
errno:  0
strtol("99999999999999999999", &end, 10)বাতিল
লেখা :  20টি 9         (64-bit long-এর সীমা প্রায় 9.22 x 10^18)
end  :  '\0'-এ         (সব অঙ্কই পড়া হয়েছে)
ফল   :  LONG_MAX       (9223372036854775807)
errno:  ERANGE         — সীমা ছাড়িয়েছে
strtol("-7", &end, 10)রূপান্তর ঠিক, কিন্তু অর্থহীন
লেখা :  -  7  \0
              ^
             end        (পুরোটা সংখ্যা)
ফল   :  -7
errno:  0
আপনার নিয়ম: --top হতে হবে 1..100  →  বাতিল
  1. "42" — নিখুঁত। strtol দুটি অঙ্ক পড়ে '\0'-এ থামল। *end == '\0' আর errno == 0 — দুটো শর্তই পূরণ, তাই 42 নিশ্চিন্তে গ্রহণ করা যায়।
  2. "42abc" — পেছনে আবর্জনা। strtol 42 পড়ে 'a'-তে থেমে গেল। ফল 42 এলেও *end হলো 'a' — অর্থাৎ ব্যবহারকারী আসলে সংখ্যা দেননি, হয়তো টাইপ করতে ভুল করেছেন। atoi এখানে চুপচাপ 42 মেনে নিত।
  3. "abc" — একটি অঙ্কও নেই। ফল 0, আর end লেখার একদম শুরুতেই। end == লেখার শুরু হলে বুঝবেন কোনো রূপান্তরই হয়নি। এই যাচাই না থাকলে “abc” আর “0” আলাদা করা যায় না।
  4. ২০টি 9 — সীমা পার। Linux-এ ৬৪-বিট long-এর সর্বোচ্চ মান প্রায় ৯.২২ × ১০১৮. strtol তাই LONG_MAX ফেরত দিল আর errno-তে ERANGE বসাল। খেয়াল রাখুন, Windows-এ long মাত্র ৩২-বিট, সেখানে প্রায় ২১৪ কোটির বেশি যেকোনো সংখ্যাতেই (যেমন 3000000000) এটা ঘটে — পর্ব ০৫-এর সেই শিক্ষা: টাইপের আকার সব জায়গায় এক নয়।
  5. "-7" — রূপান্তর ঠিক, অর্থ ভুল। strtol-এর দিক থেকে সব ঠিক। কিন্তু “সেরা −৭ জন” কোনো অর্থ বহন করে না। তাই শেষ ধাপ সবসময় আপনার নিজের সীমা যাচাই। সংক্ষেপে চারটি পাহারা: end != শুরু, *end == '\0', errno != ERANGE, আর আপনার নিজের সীমা।

এই চারটি পাহারা প্রতিবার আলাদা করে লেখার বদলে একটি ছোট helper ফাংশনে রাখাই ভালো। নিচের কোডটি শুধু পড়ার জন্য — চালানোর উদাহরণ আসছে অংশ ৫-এ:

#include <errno.h>
#include <stdlib.h>

/* সফল হলে 1 ফেরত দেয় আর *out-এ মান রাখে; ব্যর্থ হলে 0 */
int parse_long(const char *s, long lo, long hi, long *out) {
    char *end;
    errno = 0;                         /* পুরোনো মান মুছে দিন */
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return 0;      /* সংখ্যাই নয়, বা পেছনে আবর্জনা */
    if (errno == ERANGE || v < lo || v > hi) return 0;  /* সীমার বাইরে */
    *out = v;
    return 1;
}

ভুল হলে কোথায় বলবেন, আর কী ফেরত দেবেন

পর্ব ১০-এ দেখেছেন, প্রতিটি প্রোগ্রামের দুটি আলাদা আউটপুট-পাইপ আছে: stdout আর stderr. নিয়মটি সহজ — আসল ফল stdout-এ, সমস্যার কথা stderr-এ। কেন এত কড়াকড়ি? ধরুন কেউ লিখলেন ./marks report.txt > top.txt. Error যদি stdout-এ যায়, সেটা চুপচাপ top.txt ফাইলের ভেতরে ঢুকে যাবে, স্ক্রিনে কিছুই দেখা যাবে না। stderr আলাদা থাকায় redirection করলেও error স্ক্রিনে আসে।

আর পর্ব ০৪-এর return 0 মনে আছে? সেটিই exit status. প্রথা অনুযায়ী:

Exit statusসাধারণ অর্থউদাহরণ
0 (EXIT_SUCCESS)সব ঠিকঠাকফাইল পড়া হলো, ফল ছাপা হলো
1 (EXIT_FAILURE)কাজ করতে গিয়ে ব্যর্থফাইল খোলা গেল না, ডেটা নষ্ট
2ভুলভাবে চালানো হয়েছে (usage error)অজানা অপশন, সংখ্যার জায়গায় লেখা
126, 127Shell নিজে দেয়প্রোগ্রাম চালানো যায় না / খুঁজে পাওয়া যায়নি

শুধু ০ আর “০ ছাড়া অন্য কিছু” — এটুকুই সব সিস্টেমে নিশ্চিত। ১ আর ২-এর পার্থক্য একটি প্রচলিত প্রথা, যা অনেক Unix টুল মানে। যেমন grep: মিল পেলে ০, কিছু না মিললে ১, আর কোনো ত্রুটি হলে ২. Error সামলানোর পুরো কৌশল আসবে পর্ব ৩০-এ।

৪ · অপশন আর ফ্ল্যাগ — নিজের হাতে পার্সিং

বেশিরভাগ টুলের আর্গুমেন্ট দুই রকম। কিছু হলো option — - বা -- দিয়ে শুরু, আচরণ বদলায়। বাকিগুলো হলো সাধারণ আর্গুমেন্ট, যেমন ফাইলের নাম। অপশনেরও দুই জাত:

সুইচ (flag)

শুধু উপস্থিত থাকলেই অর্থ — আলাদা মান লাগে না। যেমন -v মানে “বিস্তারিত দেখাও”, -h মানে “সাহায্য দেখাও”।

-v, -q, -h, --help

মানসহ অপশন

পরের শব্দটি তার মান। -n 5-এ 5 অপশনের অংশ, ফাইলের নাম নয়। অনেক টুল --top=5 লেখাও মেনে নেয়।

-n 5, -o out.txt, --top 3

পার্সিংয়ের মূল কৌশল একটি লুপ: i = 1 থেকে শুরু করে প্রতিটি argv[i] দেখা। অপশন মানসহ হলে পরের শব্দটিও “খেয়ে ফেলা” — অর্থাৎ i এক ঘর বাড়তি এগোনো। নিচে ./marks -n 3 report.txt-এর জন্য লুপটি ধাপে ধাপে চলছে:

1for (int i = 1; i < argc; i++) {2    if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {3        top = parse(argv[++i]);4    } else if (argv[i][0] == '-') {5        return usage("unknown option");6    } else {7        file = argv[i];8    }9}10printf("file=%s top=%ld\n", file, top);
ভ্যারিয়েবলের মান (argc = 4)
i
argv[i]
top
file
  1. শুরু i = 1 থেকে। argv[0] প্রোগ্রামের নাম, তাই সেটি বাদ। top-এর ডিফল্ট মান 5, file এখনো NULL. প্রথম শব্দ "-n".
  2. এটি কি -n? হ্যাঁ। কিন্তু শুধু নাম মিললেই হবে না — i + 1 < argc যাচাই করে দেখা হচ্ছে পেছনে সত্যিই একটি মান আছে কি না। কেউ শুধু ./marks -n লিখলে এই শর্ত না থাকলে argv[2] হতো NULL, আর প্রোগ্রাম ভেঙে পড়ত।
  3. ++i — পরের শব্দটি খেয়ে ফেলা। i বেড়ে 2 হলো, argv[2] = "3" যাচাইসহ সংখ্যায় বদলে top = 3. এখানে parse মানে অংশ ৩-এর strtol-পাহারা।
  4. লুপের i++ — এবার i = 3. লুপ নিজেও i বাড়াল। ফলে "3"-কে আর আলাদা আর্গুমেন্ট হিসেবে দেখা হলো না — সে -n-এর অংশ হিসেবেই শেষ। পরের শব্দ "report.txt".
  5. দুটি প্রশ্ন, দুবারই “না”। এটি -n নয়, আর - দিয়েও শুরু নয়। তাই অজানা অপশনের শাখায় যাওয়া হলো না। যদি "-x" হতো, তাহলে লাইন ৫ usage ছেপে প্রোগ্রাম থামিয়ে দিত।
  6. সাধারণ আর্গুমেন্ট — ফাইলের নাম। file এখন argv[3]-এর দিকে তাক করছে। খেয়াল করুন, লেখাটি কপি হয়নি — শুধু ঠিকানা রাখা হয়েছে। argv-এর স্ট্রিংগুলো পুরো প্রোগ্রাম চলাকালীন বেঁচে থাকে, তাই এটি নিরাপদ।
  7. i = 4, শর্ত 4 < 4 মিথ্যা। সব শব্দ দেখা শেষ, লুপ থেকে বেরিয়ে আসা। এখানে আসল প্রোগ্রাম দেখত file এখনো NULL কি না — ফাইলের নাম না দিলে usage দেখিয়ে exit 2.
  8. ফল ছাপা। file=report.txt top=3. একই লুপ ./marks report.txt -n 3 ক্রমেও ঠিক কাজ করবে — অপশন আগে না পরে, সেটা লুপের কাছে কোনো ব্যাপার নয়।

প্রচলিত নিয়মগুলো — ব্যবহারকারী যা আশা করেন

টুল যেই বানাক, ব্যবহারকারীরা কিছু আচরণ ধরে নেন। এগুলো মানলে আপনার টুল প্রথম দিনেই “চেনা” লাগবে:

  • -h ও --help: একটি usage message stdout-এ ছেপে exit 0 — কারণ সাহায্য চাওয়া কোনো ভুল নয়। কিন্তু ভুল ব্যবহারের পর usage যায় stderr-এ, exit 2.
  • Usage-এর চেহারা: usage: marks [-v] [-n N] FILE — বর্গবন্ধনী মানে ঐচ্ছিক, বড় হাতের শব্দ মানে “এখানে আপনার মান বসান”।
  • -- মানে অপশন শেষ: ./marks -- -report.txt লিখলে -report.txt-কে অপশন নয়, ফাইলের নাম ধরা হয়। একে বলে end of options চিহ্ন। - দিয়ে শুরু হওয়া নামের ফাইল মুছতে rm -- -file লিখতে হয় ঠিক এই কারণেই।
  • শুধু - মানে stdin: অনেক টুলে ফাইলের নামের জায়গায় একটি - দিলে টুলটি stdin থেকে পড়ে।
  • ছোট অপশন জোড়া দেওয়া: ls -l -a আর ls -la একই। এটি হাতে লেখা লুপে সামলানো ঝামেলার — এজন্যই আছে getopt.

getopt — চাকা নতুন করে না বানানো

Linux, Mac ও অন্যান্য POSIX সিস্টেমে অপশন পার্সিংয়ের তৈরি ফাংশন আছে: getopt. আপনি একটি ছোট লেখায় বলে দেন কোন কোন অপশন চলবে — "vn:h" মানে -v, -h সুইচ, আর -n-এর পরে : আছে বলে সেটি একটি মান নেয়। বাকি সব হিসাব getopt নিজেই করে:

#include <unistd.h>      /* POSIX — standard C নয়, Windows-এ সাধারণত নেই */

int opt;
while ((opt = getopt(argc, argv, "vn:h")) != -1) {
    switch (opt) {
    case 'v': verbose = 1; break;
    case 'n': if (!parse_long(optarg, 1, 100, &top)) return usage_error(); break;
    case 'h': print_usage(stdout); return 0;
    default:  return usage_error();      /* getopt নিজেই '?' ফেরত দেয় ও বার্তা ছাপে */
    }
}
/* অপশনের পরে বাকি নামগুলো: argv[optind] থেকে argv[argc - 1] পর্যন্ত */

optarg-এ থাকে অপশনের মান, আর optind বলে দেয় অপশন শেষে বাকি আর্গুমেন্ট কোথা থেকে শুরু। -vn 3, -n3, -- — সবই getopt ঠিকঠাক বোঝে। --help বা --top=3-এর মতো লম্বা অপশনের জন্য GNU ও BSD (Mac-সহ) সিস্টেমে আছে getopt_long, থাকে <getopt.h>-এ। বড় প্রজেক্টে নিজে লুপ না লিখে এগুলোই ব্যবহার করুন — তবে ভেতরে কী হয়, সেটা আপনি এখন জানেন।

৫ · CLI টেস্ট করা — argv-কে নকল করে বানানো

এখানে একটি বাস্তব সমস্যা আছে। এই পাতার কোড-রানার প্রোগ্রামকে কোনো আর্গুমেন্ট পাঠাতে পারে না — সে শুধু ./prog চালায়। তাহলে argv নিয়ে লেখা কোড পরীক্ষা করব কীভাবে?

সমাধানটি শুধু এখানে কাজ চালানোর কৌশল নয় — এটি পেশাদারদের unit test লেখার আসল পদ্ধতি। সব কাজ main-এ না রেখে একটি run(int argc, char **argv) ফাংশনে রাখুন, যা একটি exit status ফেরত দেয়। আসল প্রোগ্রামে main শুধু একটি লাইন: return run(argc, argv);. আর পরীক্ষার সময় main নিজেই নকল argv বানিয়ে run-কে বারবার ডাকে — একবার সঠিক ইনপুটে, একবার ভুলে — আর exit status মিলিয়ে দেখে।

গাড়ির কারখানায় নতুন ইঞ্জিন প্রথমে গাড়িতে বসিয়ে রাস্তায় চালানো হয় না। ইঞ্জিনটিকে একটি টেস্ট-বেঞ্চে বসিয়ে কৃত্রিমভাবে তেল আর বাতাস দেওয়া হয়, তারপর মাপা হয়। এখানে run হলো ইঞ্জিন, নকল argv হলো টেস্ট-বেঞ্চের তেল। ইঞ্জিন জানেই না সে আসল গাড়িতে আছে, না বেঞ্চে।

নিচের প্রোগ্রামে run একটি ছোট marks টুল: -n-এর মান যাচাই করে, অজানা অপশন ধরে, ফাইলের নাম না পেলে usage দেখায়। main দুটি নকল কমান্ড-লাইন দিয়ে একে পরীক্ষা করছে — দ্বিতীয়টিতে "3x" ইচ্ছা করে ভুল:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int usage(const char *why) { fprintf(stderr, "marks: %s\nusage: marks [-n 1..100] FILE\n", why); return 2; }
int run(int argc, char **argv) {
    long top = 5; const char *file = NULL;
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            char *end; errno = 0; top = strtol(argv[++i], &end, 10);
            if (errno != 0 || end == argv[i] || *end != '\0' || top < 1 || top > 100) return usage("bad -n value");
        } else if (argv[i][0] == '-') return usage("unknown option");
        else file = argv[i];
    }
    if (file == NULL) return usage("no file given");
    printf("file=%s top=%ld\n", file, top); return 0;
}
int main(void) {
    char *t[2][5] = { {"marks", "-n", "3", "report.txt", NULL}, {"marks", "-n", "3x", "report.txt", NULL} };
    for (int k = 0; k < 2; k++) printf("exit=%d\n", run(4, t[k]));
    return 0;
}

প্রথম নকল কমান্ড marks -n 3 report.txt ঠিকঠাক — তাই file=report.txt top=3 ছাপা হয়ে exit=0. দ্বিতীয়টিতে strtol "3x"-এর 'x'-এ থামে, *end != '\0' সত্য হয়, তাই usage stderr-এ দুই লাইনের বার্তা ছেপে 2 ফেরত দেয় — exit=2. আউটপুট বাক্সে stderr-এর লেখা আসে stdout-এর পরে, কারণ রানার দুটি পাইপ আলাদা করে সংগ্রহ করে। প্রতিটি নকল argv-এর শেষে NULL রাখা হয়েছে, যাতে আসল argv-এর মতোই argv[argc] == NULL থাকে। এবার নিজে বদলে দেখুন: "3"-এর জায়গায় "0", "-5", বা "-n"-কে সারির শেষে সরিয়ে দিন — প্রতিবার কী exit status আসে?

এই পদ্ধতির আসল শক্তি হলো test case-এর তালিকা। একটি ভালো CLI-র জন্য অন্তত এগুলো পরীক্ষা করা উচিত:

নকল argvপ্রত্যাশিত exitকী যাচাই হচ্ছে
marks2কোনো আর্গুমেন্ট নেই — argv[1] পড়ে ক্র্যাশ করছে না তো?
marks -n2অপশনের মান হারিয়ে গেছে
marks -n 0 a.txt2সীমার নিচে
marks -n 99999999999999999999 a.txt2ERANGE
marks a.txt -n 30অপশন পরে এলেও চলে
marks -x a.txt2অজানা অপশন

পর্ব ৩২-এ টেস্টিং নিয়ে বিস্তারিত দেখবেন — সেখানে এই তালিকাই একটি স্বয়ংক্রিয় test harness হয়ে উঠবে।

৬ · Environment variable আর exit status — টুল দিয়ে টুল জোড়া

Environment — প্রতিটি প্রোগ্রামের সাথে আসা পরিবেশ

আর্গুমেন্ট ছাড়াও প্রতিটি প্রোগ্রাম জন্মের সময় একটি তালিকা পায়: নাম=মান জোড়া, যেমন HOME=/home/rafi বা LANG=bn_BD.UTF-8. এদের বলে environment variable. আর্গুমেন্ট হলো “এই একবারের অর্ডার”, আর environment হলো “এই দোকানের স্থায়ী নিয়ম” — প্রতিবার বলে দিতে হয় না।

#include <stdlib.h>

const char *home = getenv("HOME");     /* না থাকলে NULL */
if (home == NULL) home = ".";          /* তাই সবসময় NULL যাচাই, তারপর ডিফল্ট */

getenv standard C-এর অংশ, তাই সব জায়গায় চলে। সবচেয়ে পরিচিত environment variable হলো PATH — কয়েকটি ফোল্ডারের তালিকা। আপনি শুধু gcc লিখলে shell এই ফোল্ডারগুলোতে একে একে gcc নামের প্রোগ্রাম খোঁজে। পর্ব ০৩-এর “gcc is not recognized” error-এর মানে আসলে এটিই: gcc-র ফোল্ডারটি PATH-এ নেই।

কিছু পুরোনো বই ও AI-এর কোডে int main(int argc, char *argv[], char *envp[]) দেখবেন। এই তৃতীয় parameter envp অনেক সিস্টেমে কাজ করে, কিন্তু C standard-এ নেই — standard শুধু একে একটি “প্রচলিত extension” হিসেবে উল্লেখ করে। বহনযোগ্য কোডে getenv-ই ব্যবহার করুন। আর একটি নিরাপত্তা-সতর্কতা: environment-এর মান ব্যবহারকারী বদলাতে পারেন, তাই আর্গুমেন্টের মতোই একেও যাচাই না করে বিশ্বাস করবেন না।

Exit status — ছোট টুল জুড়ে বড় কাজ

এখন বোঝা যাবে exit status এত জরুরি কেন। Shell প্রতিটি প্রোগ্রামের exit status মনে রাখে ($?-এ), আর সেই অনুযায়ী পরের সিদ্ধান্ত নেয়:

$ ./marks -n 3 report.txt && echo "ok"        # marks 0 দিলে তবেই echo চলবে
$ ./marks -n 3x report.txt || echo "failed"   # marks ব্যর্থ হলে তবেই echo চলবে
$ ./marks -n 3x report.txt; echo $?           # শেষ প্রোগ্রামের exit status দেখুন: 2
$ ./marks -n 10 report.txt | sort | head -3   # pipe: এক টুলের stdout পরের টুলের stdin

শেষ লাইনটি pipe. marks-এর stdout সরাসরি sort-এর stdin-এ যায়, তারপর head-এ। কোনোটিই অন্যটির কথা জানে না — প্রত্যেকে শুধু লেখা পড়ে আর লেখা দেয়। এখানেই stdout/stderr আলাদা রাখার আসল মূল্য: আপনার error যদি stdout-এ যেত, সেটা sort-এর ভেতরে ঢুকে সাজানো ডেটার মাঝে মিশে যেত। Pipe ভেতরে কীভাবে তৈরি হয় — fork, exec, pipe() — সেটা পর্ব ৪৮-এ দেখবেন।

সবচেয়ে ক্ষতিকর ভুল: ব্যর্থ হয়েও exit 0. ধরুন একটি backup script লিখল ./backup data/ && rm -rf data/. আপনার backup টুল ফাইল লিখতে ব্যর্থ হলো, স্ক্রিনে “error” লিখল — কিন্তু return 0 দিল। Shell ভাবল সব ঠিক আছে, আর পরের কমান্ড আসল ডেটা মুছে দিল। Exit status কোনো আনুষ্ঠানিকতা নয় — এটি অন্য প্রোগ্রামের সাথে আপনার চুক্তি।

৭ · Variadic ফাংশন — printf যেভাবে যতখুশি মান নেয়

এতক্ষণ দেখলাম একটি প্রোগ্রাম কীভাবে যতখুশি শব্দ নেয়। এবার একটি ফাংশন-এর একই প্রশ্ন। printf("%d", x)-এ দুটি argument, printf("%s %d %f", a, b, c)-এ চারটি। পর্ব ১৩-এর নিয়মে তো প্রতিটি ফাংশনের parameter-সংখ্যা নির্দিষ্ট! তাহলে printf এটা করে কীভাবে?

উত্তর printf-এর ঘোষণাতেই: int printf(const char *format, ...);. ওই তিনটি বিন্দু (..., ellipsis) মানে “এরপর যতগুলো খুশি, যেকোনো টাইপের argument আসতে পারে।” এমন ফাংশনকে বলে variadic function. নিজে লিখতে লাগে <stdarg.h>-এর চারটি জিনিস:

জিনিসকাজ
va_list ap;একটি “বুকমার্ক” — বাড়তি argument-গুলোর কোনটি পরের পালা, মনে রাখে
va_start(ap, last);বুকমার্ক বসায় শেষ নাম-দেওয়া parameter last-এর ঠিক পরে
va_arg(ap, int)পরের argument-টি int হিসেবে পড়ে, বুকমার্ক এগিয়ে দেয়
va_end(ap);কাজ শেষ — পরিষ্কার করা। প্রতিটি va_start-এর জোড়া

একটি গুরুত্বপূর্ণ কথা: variadic ফাংশন নিজে থেকে জানে না কয়টি argument এসেছে, বা কোনটির টাইপ কী। এই তথ্য তাকে কোনোভাবে জানাতে হয়। printf জানে format string পড়ে — যতগুলো %, ততগুলো মান। আমাদের sum জানবে প্রথম argument count থেকে। নিচে sum(4, 10, 20, 30, 40) ডাকা হলে ভেতরে কী ঘটে দেখুন:

1int sum(int count, ...) {2    va_list ap;3    va_start(ap, count);4    int total = 0;5    for (int i = 0; i < count; i++)6        total += va_arg(ap, int);7    va_end(ap);8    return total;9}
ভেতরের অবস্থা
বাড়তি argument
count
i
va_arg দিল
total
  1. ডাকা হলো। নাম-দেওয়া parameter শুধু একটি — count = 4. বাকি চারটি মান (10, 20, 30, 40) ...-এর জায়গায় এসেছে। এদের কোনো নাম নেই, তাই সরাসরি ব্যবহার করা যায় না।
  2. va_start(ap, count) — বুকমার্ক বসানো। দ্বিতীয় argument হিসেবে শেষ নাম-দেওয়া parameter-টি দিতে হয়, যাতে macro জানে বাড়তিগুলো কোথা থেকে শুরু। বর্গবন্ধনী [ ] দেখাচ্ছে বুকমার্ক এখন 10-এর ওপর।
  3. i = 0: প্রথম va_arg(ap, int). বুকমার্কের জায়গা থেকে একটি int পড়া হলো — 10. total = 10, আর বুকমার্ক নিজে থেকেই 20-এ সরে গেল। পেছনে ফেরার কোনো উপায় নেই।
  4. i = 1: পেল 20. total = 30. খেয়াল করুন, va_arg-কে আমরা প্রতিবার বলে দিচ্ছি “int হিসেবে পড়ো”। আসল মানটি double হলেও সে int হিসেবেই পড়ার চেষ্টা করত — কেউ যাচাই করে না, আর ফল হতো undefined behavior.
  5. i = 2: পেল 30. total = 60.
  6. i = 3: পেল 40. total = 100. বুকমার্ক এখন চারটির পরে — সেখানে কী আছে কেউ জানে না ([?])।
  7. i = 4: শর্ত মিথ্যা, থামা। লুপ থামল কারণ count বলেছিল ৪টি। কিন্তু কেউ যদি ভুল করে sum(5, 10, 20, 30, 40) লিখত? তাহলে পঞ্চমবার va_arg ওই [?] জায়গা থেকে পড়ত — undefined behavior. Compiler এই ভুল ধরতে পারে না।
  8. va_end, তারপর ফেরত 100. প্রতিটি va_start-এর সাথে একই ফাংশনে একটি va_end — এটা নিয়ম। অনেক সিস্টেমে va_end কিছুই করে না, কিন্তু কিছু সিস্টেমে এটি না দিলে সমস্যা হয়।

Default argument promotions — ...-এর লুকানো রূপান্তর

পর্ব ০৮-এ লুকানো টাইপ রূপান্তর দেখেছেন। ...-এর জায়গায় মান পাঠালেও একটি বিশেষ রূপান্তর ঘটে, যার নাম default argument promotions: char ও short হয়ে যায় int (integer promotion), আর float হয়ে যায় double. ফলাফল দুটি:

  • va_arg(ap, char) বা va_arg(ap, float) কখনো লিখবেন না। ওই টাইপ ... দিয়ে কখনো পৌঁছায়ই না। পড়তে হবে int বা double হিসেবে। GCC এমন কোড দেখলে warning দেয়।
  • এজন্যই printf-এ float ও double দুটোর জন্যই %f! printf কখনো float পায় না — সব double হয়ে আসে। কিন্তু scanf-এ মান নয়, ঠিকানা যায় (pointer-এর promotion হয় না), তাই সেখানে double-এর জন্য %lf লাগে। পর্ব ১০-এর সেই ধাঁধার উত্তর এটাই।

নিজের printf-এর মতো ফাংশন — vfprintf

বাস্তবে variadic ফাংশনের সবচেয়ে প্রচলিত ব্যবহার হলো printf-এর ওপর একটি মোড়ক, যেমন log লেখার ফাংশন যা প্রতিটি লাইনের আগে একটি চিহ্ন বসায়। কিন্তু সমস্যা: আপনার পাওয়া ... সরাসরি printf-কে দেওয়া যায় না। এজন্য printf-পরিবারের প্রতিটি সদস্যের একটি “v” সংস্করণ আছে, যা একটি va_list নেয়: vprintf, vfprintf, vsnprintf. নিচের প্রোগ্রামে sum আর log_msg দুটোই আছে:

#include <stdio.h>
#include <stdarg.h>
int sum(int count, ...) {
    va_list ap; int total = 0;
    va_start(ap, count);
    for (int i = 0; i < count; i++) total += va_arg(ap, int);
    va_end(ap);
    return total;
}
__attribute__((format(printf, 1, 2)))
void log_msg(const char *fmt, ...) {
    va_list ap; va_start(ap, fmt);
    printf("[log] ");
    vfprintf(stdout, fmt, ap);
    va_end(ap);
}
int main(void) {
    char grade = 'A';
    log_msg("sum = %d\n", sum(4, 10, 20, 30, 40));
    log_msg("grade %c, avg %.1f\n", grade, 76.5f);
    return 0;
}

sum ঠিক উপরের ডায়াগ্রামের মতো চারটি মান যোগ করে 100 দেয়। log_msg নিজের ... একটি va_list-এ ধরে vfprintf-এর হাতে তুলে দেয় — ফলে [log] sum = 100 আর [log] grade A, avg 76.5 ছাপা হয়। দ্বিতীয় লাইনে দুটি promotion লুকিয়ে আছে: char grade গেছে int হয়ে (তাই %c আসলে int পড়ে), আর 76.5f গেছে double হয়ে। __attribute__((format(printf, 1, 2))) লাইনটি GCC/Clang-কে বলে “১ নম্বর parameter একটি printf-format, আর বাড়তি মান শুরু ২ নম্বর থেকে।” এবার পরীক্ষা করুন: শেষ log_msg-এ %c-এর জায়গায় %s লিখে চালান — compiler সঙ্গে সঙ্গে warning দেবে।

আরও কয়েকটি তথ্য: C23-এর আগে ...-এর আগে অন্তত একটি নাম-দেওয়া parameter বাধ্যতামূলক ছিল; C23 থেকে int f(...) লেখা যায়। একই va_list দুবার ব্যবহার করতে চাইলে আগে va_copy দিয়ে কপি করতে হয়। আর অনেক variadic ফাংশন সংখ্যার বদলে একটি শেষ-চিহ্ন দিয়ে তালিকা থামায়, যেমন POSIX-এর execl(path, arg0, arg1, (char *)NULL) — argv-এর শেষে NULL রাখার মতোই চিন্তা।

৮ · Varargs-এর অন্ধকার দিক — টাইপ-যাচাই নেই

সাধারণ ফাংশনে ভুল টাইপ দিলে compiler সাথে সাথে ধরে, কারণ prototype-এ প্রতিটি parameter-এর টাইপ লেখা থাকে। কিন্তু ...-এর পরে কোনো টাইপ লেখা নেই। Compiler-এর কাছে সেখানে যা খুশি পাঠানো বৈধ। কে কী টাইপ পড়বে, সেই চুক্তি থাকে শুধু format string-এ বা আপনার মাথায়।

ভুল কোডকী হয়
printf("%d\n", 3.5);double পাঠানো হয়েছে, int পড়া হচ্ছে — আবর্জনা সংখ্যা ছাপে (undefined behavior)
printf("%s\n", 42);42-কে ঠিকানা ভেবে সেখান থেকে লেখা পড়তে যায় — সাধারণত crash
printf("%ld\n", count); (count হলো int)৬৪-বিট সিস্টেমে আকার মেলে না — কখনো ঠিক দেখায়, কখনো নয়
sum(3, 10, 20)৩টি বলে ২টি দেওয়া — তৃতীয় va_arg অজানা মেমরি পড়ে

সুখবর হলো, printf-এর জন্য GCC ও Clang বিশেষ ব্যবস্থা রেখেছে। -Wall চালু থাকলে -Wformat-ও চালু হয়, আর compiler format string পড়ে প্রতিটি মানের টাইপ মিলিয়ে দেখে। উপরের প্রথম তিনটি ভুলই warning হয়ে ধরা পড়ে। কিন্তু চতুর্থটি ধরা পড়ে না — আপনার নিজের sum-এর নিয়ম compiler জানে না। নিজের printf-মতো ফাংশনে format attribute লাগালে সেটিও printf-এর মতো যাচাই পায় — উপরের log_msg-এ ঠিক এটাই করেছি।

printf(user_input) — এক লাইনের নিরাপত্তা-ছিদ্র

এবার সবচেয়ে বিপজ্জনক ভুলটি। ব্যবহারকারীর নাম ছাপতে অনেকে লেখেন printf(argv[1]);. দেখতে নিরীহ, আর সাধারণ নামে ঠিকঠাক চলে। কিন্তু কেউ যদি নাম হিসেবে দেন "%x %x %x %x"? printf ভাববে এটি format string, আর চারটি মান খুঁজতে যাবে যা কেউ পাঠায়নি — ফলে stack-এর গোপন মেমরি স্ক্রিনে ছেপে দেবে। আরও খারাপ, %n চিহ্নটি printf-কে দিয়ে মেমরিতে লেখায়। এই আক্রমণের নাম format string vulnerability, আর ১৯৯৯–২০০০ সালের দিকে এটি ব্যাপকভাবে পরিচিত হয় — তখন wu-ftpd-এর মতো জনপ্রিয় সার্ভার সফটওয়্যারে এমন ত্রুটি দিয়ে দূর থেকে সার্ভার দখল করা গিয়েছিল।

ভুল: printf(name);
ঠিক: printf("%s", name); অথবা fputs(name, stdout);

নিয়মটি একটাই: format string সবসময় আপনার নিজের লেখা, কোডের ভেতরের একটি স্থির লেখা হবে — কখনো বাইরে থেকে আসা কিছু নয়। GCC-র -Wformat-security (কিছু Linux distribution এটি ডিফল্টে চালু রাখে) ঠিক এই ভুলটি ধরে। নিরাপত্তা-ত্রুটি কীভাবে খুঁজে বের করা ও ঠেকানো হয়, তা নিয়ে আরও গভীরে যেতে পারেন Cybersecurity কোর্সে, আর C-র দিক থেকে পর্ব ৪৬-এ।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই বিষয়ে প্রায়ই যেসব প্রশ্ন আসে:

  • ./a.out x y z চালালে argc কত? ৪ — প্রোগ্রামের নামও গোনা হয়।
  • argv[0]-এ কী থাকে, আর argv[argc]-এ? প্রথমটিতে সাধারণত প্রোগ্রামের নাম; দ্বিতীয়টি সবসময় NULL.
  • atoi আর strtol-এর পার্থক্য? strtol end pointer আর ERANGE দিয়ে ভুল ইনপুট ও সীমা পার হওয়া জানায়; atoi কিছুই জানায় না।
  • Variadic ফাংশন লিখতে কী কী লাগে? <stdarg.h>, অন্তত একটি নাম-দেওয়া parameter (C23-এর আগে), আর va_list, va_start, va_arg, va_end.
  • printf(s) কেন বিপজ্জনক? s-এ % থাকলে printf অস্তিত্বহীন argument পড়ে বা %n দিয়ে মেমরিতে লেখে — format string vulnerability.

৯ · AI-এর যুগে — CLI কোডে AI যেসব ভুল করে

“একটা C প্রোগ্রাম লেখো যা ফাইলের নাম আর একটি সংখ্যা আর্গুমেন্ট হিসেবে নেবে” — এমন অনুরোধে AI কয়েক সেকেন্ডে কোড দেয়, আর সাধারণ ইনপুটে সেটা চলেও। সমস্যা লুকিয়ে থাকে কিনারায়। AI শিখেছে লাখো টিউটোরিয়াল থেকে, আর সেগুলোর বেশিরভাগই ছোট রাখতে যাচাই বাদ দেয়। নিচের ভুলগুলো বারবার দেখা যায়:

AI-এর কোডে যা থাকেসমস্যাঠিক পদ্ধতি
FILE *f = fopen(argv[1], "r"); — আগে কোনো যাচাই নেইআর্গুমেন্ট ছাড়া চালালে argv[1] NULL — crashআগে if (argc < 2) → usage, exit 2
int n = atoi(argv[2]);“abc” → 0, “3x” → 3, কোনো সতর্কতা নেইstrtol + end pointer + errno + সীমা
printf("Error: file not found\n");error stdout-এ — pipe ও redirect-এ হারিয়ে যায়fprintf(stderr, …) বা perror
error ছাপার পরে return 0;Script ও CI ভাবে সফলreturn 1; বা EXIT_FAILURE
printf(argv[1]);format string vulnerabilityprintf("%s", argv[1]);
va_arg(ap, float) বা va_arg(ap, char)promotion-এর কারণে undefined behaviordouble / int হিসেবে পড়া
নিজের log ফাংশনে format attribute নেইটাইপের ভুল warning ছাড়াই পার পায়__attribute__((format(printf, 1, 2)))

AI-কে কাজ দেওয়ার আগে নিজের চুক্তিটি লিখে দিন — কোন অপশন, কোন সীমা, কোন exit status. শেষে test case চাইলে AI নিজের কোড নিজেই পরীক্ষার কাঠামোয় আনতে বাধ্য হয়:

C11-এ একটি কমান্ড-লাইন টুল লেখো: usage হবে "marks [-v] [-n N] FILE". নিয়ম: ১. argc যাচাই না করে কোনো argv[i] পড়বে না। ২. -n-এর মান strtol দিয়ে পড়বে (end pointer, errno = 0 আগে, ERANGE), সীমা 1..100. ৩. সব error ও usage যাবে stderr-এ; -h/--help হলে usage stdout-এ, exit 0. ৪. Exit status: সফল 0, ফাইল খুলতে ব্যর্থ 1, ভুল ব্যবহার 2. ৫. ব্যবহারকারীর কোনো লেখা format string হিসেবে printf-এ যাবে না। ৬. সব যুক্তি থাকবে int run(int argc, char **argv)-এ; main শুধু return run(argc, argv); ৭. শেষে নকল argv দিয়ে ৬টি test case দাও (কোনো আর্গুমেন্ট নেই, -n শেষে, -n 0, -n 3x, অজানা অপশন, সঠিক) আর প্রত্যেকটির প্রত্যাশিত exit status লেখো। gcc -Wall -Wextra-এ একটিও warning থাকবে না।

AI-এর CLI কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“একটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে” — Unix-এর এই দর্শন কোথা থেকে এলো, আর pipe কেন এত শক্তিশালী?

১৯৬৪ সালে Bell Labs-এর Doug McIlroy একটি ছোট অভ্যন্তরীণ নোটে লিখেছিলেন, প্রোগ্রামগুলোকে বাগানের পানির পাইপের মতো জোড়া লাগানোর উপায় থাকা উচিত — দরকার হলে আরেক টুকরো পাইপ প্যাঁচ দিয়ে লাগিয়ে দেওয়া যাবে। প্রায় এক দশক পরে, ১৯৭৩ সালে, তাঁর চাপে Ken Thompson Unix-এ pipe যোগ করেন। পরে McIlroy নিজেই দর্শনটি সংক্ষেপে বলেছিলেন: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে; প্রোগ্রামগুলো একসাথে কাজ করবে; আর সবাই লেখার প্রবাহ (text stream) পড়বে ও লিখবে, কারণ সেটাই সার্বজনীন ভাষা। একে বলে Unix philosophy.

এর শক্তি বোঝার সবচেয়ে বিখ্যাত গল্পটি ১৯৮৬ সালের। Jon Bentley-র “Programming Pearls” কলামে Donald Knuth একটি সমস্যার সমাধান দেন: একটি লেখায় সবচেয়ে বেশি ব্যবহৃত k-টি শব্দ খুঁজে বের করা। Knuth লিখলেন কয়েক পাতার একটি চমৎকার, যত্নে ব্যাখ্যা করা প্রোগ্রাম। McIlroy তার পর্যালোচনায় একই কাজ দেখালেন ছয়টি ছোট Unix কমান্ড pipe দিয়ে জুড়ে — tr, tr, sort, uniq, sort, sed. কোনোটিই এই সমস্যার জন্য বানানো হয়নি, তবুও একসাথে কাজটি হয়ে গেল।

এখানে লুকিয়ে আছে এই পর্বের সব নিয়মের কারণ। ছোট টুল জোড়া লাগাতে হলে প্রতিটিকে কয়েকটি চুক্তি মানতে হয়: আসল ডেটা শুধু stdout-এ (নইলে পরের টুলের ইনপুট নষ্ট), সমস্যার কথা stderr-এ, আর সঠিক exit status (নইলে &&, || আর script ভুল সিদ্ধান্ত নেয়)। আর্গুমেন্ট ও অপশনের প্রচলিত নিয়ম মানলে মানুষও সহজে জোড়া লাগাতে পারেন।

তবে সীমাবদ্ধতাও আছে। শুধু লেখার প্রবাহ মানে প্রতিটি টুলকে আবার নতুন করে লেখা পার্স করতে হয় — ফাঁকা জায়গাওয়ালা ফাইলের নাম নিয়ে Unix script-এর ভুল এক চিরকালের সমস্যা। তাই আধুনিক অনেক টুল JSON আউটপুটের অপশন দেয় (যেমন docker inspect), আর PowerShell লেখার বদলে object পাঠায়।

মূল উপলব্ধি: ছোট, পরিষ্কার চুক্তি মেনে চলা অংশগুলো জুড়ে যে সিস্টেম, তা যেকোনো এক বিশাল প্রোগ্রামের চেয়ে বেশি টেকে। আজকের microservice আর API-ভিত্তিক নকশাতেও একই চিন্তা চলছে।

প্র ০২একটি টুল ভুল exit status দিলে CI/CD pipeline বা deployment script-এ কী বিপদ হতে পারে?

আধুনিক সফটওয়্যার কোম্পানিতে কোড সরাসরি মানুষ server-এ তোলেন না। কেউ কোড push করলে একটি CI সিস্টেম (যেমন GitHub Actions, GitLab CI, Jenkins) পরপর কমান্ড চালায়: compile, test, security scan, তারপর deploy. প্রতিটি ধাপ সফল না ব্যর্থ, সেটা এই সিস্টেম বোঝে শুধু exit status দেখে। ০ মানে সবুজ টিক, অন্য কিছু মানে লাল ক্রস আর pipeline থেমে যায়। মানুষের মতো স্ক্রিনের লেখা পড়ে “error” শব্দ খোঁজে না।

এবার ধরুন আপনার test runner ৫টি test-এ ব্যর্থ হলো, স্ক্রিনে লাল অক্ষরে লিখল “5 FAILED” — কিন্তু return 0 দিল। CI সবুজ টিক দেখাবে, আর ভাঙা কোড সোজা production-এ চলে যাবে। হাজার হাজার মানুষের bKash বা Nagad লেনদেন যে সার্ভারে চলে, সেখানে এমন একটি ভুল মানে আসল টাকার ক্ষতি। উল্টো ভুলও আছে: সফল হয়েও অকারণে non-zero দেওয়া টুল দলকে বারবার মিথ্যা alarm দেয়, শেষে সবাই লাল ক্রস উপেক্ষা করতে শেখে — যা আরও বিপজ্জনক।

Shell script-এ আরও সূক্ষ্ম ফাঁদ আছে। ডিফল্টে bash একটি কমান্ড ব্যর্থ হলেও পরের লাইনে চলে যায়। তাই পেশাদার script শুরু হয় set -e দিয়ে (ব্যর্থ হলেই থামো)। আবার ./build | tee log.txt-এ pipe-এর exit status হয় শেষ কমান্ডের, অর্থাৎ tee-র — build ব্যর্থ হলেও ধরা পড়ে না। এজন্য bash-এ আছে set -o pipefail. Make-ও একই নিয়মে চলে: কোনো recipe non-zero দিলে build থামে (পর্ব ২৯)।

এজন্যই এই পর্বে exit status ০/১/২-এর ওপর এত জোর। আপনার ছোট C টুলটি একদিন কারও deployment pipeline-এর একটি ধাপ হতে পারে। DevOps-এর এই জগৎ নিয়ে আরও জানতে দেখুন Cloud & DevOps কোর্স।

মূল উপলব্ধি: exit status হলো যন্ত্রের সাথে যন্ত্রের ভাষা। মানুষের জন্য লেখা বার্তা যত সুন্দরই হোক, অটোমেশন বিশ্বাস করে শুধু ওই একটি সংখ্যাকে — তাই সেটি সত্যি বলতে হবে।

প্র ০৩printf-এর argument তো compiler-এর কাছে টাইপহীন — তাহলে আজ compiler কীভাবে %d-এর জায়গায় double ধরতে পারে? ইতিহাসটা কী?

C ভাষার নিয়মে compiler-এর এটি করার কোনো দায় নেই। ...-এর পরে কী আসবে, ভাষা তা বলে না। প্রথম দিকের C compiler তাই printf-এর ভুল একদমই ধরত না — ভুল ধরা পড়ত চালানোর সময়, আবর্জনা সংখ্যা বা crash হয়ে।

১৯৭০-এর দশকের শেষে Bell Labs-এ Stephen C. Johnson তৈরি করেন lint — একটি আলাদা প্রোগ্রাম, যা C কোড পড়ে সন্দেহজনক জিনিস খুঁজত, compiler যা যাচাই করত না সেগুলোও। পরের দিকের lint সংস্করণে printf-জাতীয় ফাংশনের format string মিলিয়ে দেখার ব্যবস্থা আসে। কোডে একটি বিশেষ মন্তব্য লিখে বলে দেওয়া যেত “এই ফাংশনটি printf-এর মতো”। আজও আমরা কোড-পরীক্ষার টুলকে “linter” বলি এই lint-এর নামেই।

পরে GCC এই চিন্তাটিকে compiler-এর ভেতরেই নিয়ে আসে। Compiler printf, scanf ইত্যাদি standard ফাংশনের নাম চেনে, আর format string যদি কোডের ভেতরের একটি স্থির লেখা হয়, তাহলে compile-এর সময়েই সেটি পড়ে প্রতিটি মানের টাইপ মিলিয়ে দেখে। এটিই -Wformat, যা -Wall-এর অংশ। নিজের ফাংশনের জন্য আছে format attribute, যা Clang-ও মানে। Linux kernel-এর printk-সহ বহু বড় প্রজেক্ট নিজের log ফাংশনে এটি ব্যবহার করে।

কিন্তু খেয়াল করুন সীমাবদ্ধতাটি: format string যদি চালানোর সময় তৈরি হয় (ফাইল থেকে পড়া, বা ব্যবহারকারীর দেওয়া), compiler আর কিছুই দেখতে পায় না। এজন্যই -Wformat-nonliteral আর -Wformat-security-এর মতো warning আছে, যা বলে “এখানে আমি যাচাই করতে পারছি না, সাবধান”।

মূল উপলব্ধি: ভাষা যে নিরাপত্তা দেয় না, টুলিং অনেক সময় তা পূরণ করে — কিন্তু কেবল তখনই, যখন আপনি টুলকে যথেষ্ট তথ্য দেন। Warning চালু রাখা আর format attribute লাগানো সেই তথ্য দেওয়ারই উপায়।

প্র ০৪C++ ও Rust কেন C-এর varargs-কে টাইপ-নিরাপদ বিকল্প দিয়ে বদলে দিল? এতে কী লাভ, কী দাম?

C-এর varargs-এর মূল দুর্বলতা আপনি এই পর্বে দেখেছেন: argument-এর সংখ্যা আর টাইপের তথ্য ফাংশনের কাছে পৌঁছায় না। ফাংশনকে অন্য কোনো চুক্তির ওপর ভরসা করতে হয় — format string, count, বা শেষে NULL. চুক্তি ভাঙলে undefined behavior, আর format string vulnerability-র মতো নিরাপত্তা-ত্রুটি।

C++ এই সমস্যা কয়েক ধাপে সমাধান করেছে। প্রথমে ১৯৮০-এর দশকে iostream (std::cout << x) — প্রতিটি টাইপের জন্য আলাদা overload, তাই ভুল টাইপ compile-এর সময়েই ধরা পড়ে। কিন্তু লেখা লম্বা আর বিন্যাস ঝামেলার। C++11 আনল variadic template, যেখানে যতখুশি argument নেওয়া যায় কিন্তু প্রতিটির আসল টাইপ compiler জানে। তার ওপর ভিত্তি করে C++20-এ এলো std::format আর C++23-এ std::print — দেখতে প্রায় printf-এর মতো, কিন্তু টাইপ-নিরাপদ, আর format-এর ভুল অনেক ক্ষেত্রে compile-এর সময়েই ধরা পড়ে।

Rust-এ println!("{} {}", a, b) আসলে একটি macro. Compile-এর সময় macro format string পড়ে, গুনে দেখে যতগুলো {} ততগুলো মান আছে কি না, আর প্রতিটি মান ছাপার যোগ্য কি না। ভুল হলে প্রোগ্রাম compile-ই হয় না। Java-র varargs (String... args) আবার আসলে একটি টাইপ-যুক্ত অ্যারে।

দাম কী? Template-ভিত্তিক সমাধানে compile সময় বাড়ে, আর প্রতিটি ভিন্ন টাইপ-সমন্বয়ের জন্য আলাদা কোড তৈরি হতে পারে, ফলে প্রোগ্রাম বড় হয়। C-এর printf একটিমাত্র ফাংশন, যা ছোট microcontroller-এও চলে। এজন্যই C এখনো varargs রেখে দিয়েছে, শুধু C23-এ কিছু ছোট উন্নতি এনেছে।

মূল উপলব্ধি: ভাষার বিবর্তনের একটি বড় ধারা হলো “চুক্তিকে মানুষের মাথা থেকে টাইপ-সিস্টেমে সরিয়ে আনা”। যা compiler যাচাই করতে পারে, তা আর মানুষকে মনে রাখতে হয় না।

প্র ০৫এত সুন্দর GUI থাকতে git, docker, gcc-এর মতো টুল আজও কমান্ড-লাইনে কেন? AI-এর যুগে CLI-র গুরুত্ব কি কমছে?

২০০৫ সালে Linus Torvalds যখন git বানান, সেটি ছিল পুরোপুরি কমান্ড-লাইনের টুল। আজ git-এর অনেক GUI আছে, কিন্তু প্রায় সবগুলোই ভেতরে git-এর সেই একই কমান্ড চালায়। docker, kubectl, gcc, ffmpeg, npm — একই গল্প। কারণগুলো গভীর:

  • অটোমেশন: একটি কমান্ড script-এ লেখা যায়, CI-তে চালানো যায়, রাতে হাজারবার চালানো যায়। একটি বোতাম-চাপা যায় না।
  • জোড়া লাগানো: pipe আর exit status দিয়ে কয়েকটি টুল মিলে নতুন কাজ — প্রশ্ন ০১-এর Unix দর্শন।
  • দূর থেকে কাজ: ডেটা সেন্টারের server-এ সাধারণত কোনো স্ক্রিনই নেই। SSH দিয়ে ঢুকে কয়েক বাইটের কমান্ড — ধীর ইন্টারনেটেও চলে।
  • পুনরাবৃত্তি ও প্রমাণ: “এই কমান্ডটি চালান” লিখে দেওয়া যায় documentation-এ, বাগ রিপোর্টে, Stack Overflow-এ। “তৃতীয় মেনুর দ্বিতীয় ট্যাবে ক্লিক করুন” — এটা ভঙ্গুর।
  • খরচ: একটি CLI বানানো ও বজায় রাখা একটি GUI-র চেয়ে অনেক সহজ।

AI-এর যুগে CLI-র গুরুত্ব বরং বাড়ছে। আজকের AI coding assistant ও agent-রা কোড লেখার পরে কী করে? Terminal-এ কমান্ড চালায় — compile, test, git commit — আর exit status ও stderr পড়ে বোঝে কাজ হলো কি না। একটি টুল যত পরিষ্কার usage, অনুমানযোগ্য আউটপুট আর সৎ exit status দেয়, AI তত নির্ভরযোগ্যভাবে সেটি ব্যবহার করতে পারে। অর্থাৎ ভালো CLI নকশা এখন মানুষ আর যন্ত্র দুই পাঠকের জন্যই।

GUI-র অবশ্যই জায়গা আছে — নতুন ব্যবহারকারী, দৃশ্যমান কাজ (ছবি সম্পাদনা, ডিজাইন), আর আবিষ্কারযোগ্যতায় GUI এগিয়ে। পেশাদার টুলের প্রচলিত নকশা তাই দুই স্তরের: ভেতরে একটি শক্ত CLI বা library, ওপরে ঐচ্ছিক GUI. Git-এর ইতিহাস ও ব্যবহার নিয়ে আরও জানতে দেখুন Software Engineering & Git কোর্স।

মূল উপলব্ধি: CLI হলো সফটওয়্যারের “API যা মানুষও টাইপ করতে পারে”। এই পর্বে শেখা argc/argv, strtol, stderr আর exit status — এগুলো দিয়েই আপনি এমন টুল বানাবেন, যা অন্য প্রোগ্রাম, script আর AI নির্ভয়ে ব্যবহার করতে পারে।

অনুশীলন

বেশিরভাগই ভাবা আর পর্যবেক্ষণের কাজ — শুধু শেষটিতে কোড লিখতে হবে।

  1. argv অনুমান করুন: প্রতিটি কমান্ডের জন্য argc আর argv[1] কী হবে বলুন — (ক) ./marks "Dhaka City" -n 3 (খ) ./marks -n3 (গ) ./marks '' report.txt (ঘ) ./marks -- -n

    (ক) argc = ৪; argv[1] = "Dhaka City" — quote-এর কারণে দুটি শব্দ একটি আর্গুমেন্ট, quote চিহ্ন নিজে আসে না।

    (খ) argc = ২; argv[1] = "-n3". Shell-এর কাছে এটি একটিই শব্দ। আমাদের হাতে লেখা লুপ একে অজানা অপশন ধরবে, কিন্তু getopt -n-এর মান 3 হিসেবে বুঝবে।

    (গ) argc = ৩; argv[1] = "" — খালি স্ট্রিং, কিন্তু তবুও একটি আর্গুমেন্ট। আমাদের লুপে এটি “ফাইলের নাম” হয়ে যাবে, তারপর report.txt সেটিকে বদলে দেবে — ভালো টুলে খালি নাম আলাদা করে ধরা উচিত।

    (ঘ) argc = ৩; argv[1] = "--", argv[2] = "-n". প্রচলিত নিয়মে ---এর পরের -n একটি ফাইলের নাম।

  2. Exit status পর্যবেক্ষণ: নিজের terminal-এ (Linux, Mac, বা Windows-এ Git Bash) চালান: ls; echo $?, তারপর ls no_such_file; echo $?, তারপর ls no_such_file 2> /dev/null; echo $?. তৃতীয়টিতে কী লক্ষ করলেন?

    প্রথমটিতে ০. দ্বিতীয়টিতে error বার্তা আর একটি অশূন্য সংখ্যা (GNU ls সাধারণত ২ দেয়, অন্য সিস্টেমে ১ হতে পারে)। তৃতীয়টিতে 2> /dev/null stderr-কে ফেলে দিয়েছে, তাই স্ক্রিনে কোনো বার্তা নেই — কিন্তু exit status এখনো অশূন্য! অর্থাৎ বার্তা আর exit status দুটি আলাদা পথ। Script বার্তা লুকিয়ে ফেললেও ব্যর্থতা ঠিকই জানতে পারে — এজন্যই exit status-কে সৎ রাখা এত জরুরি।

  3. AI-কোড অডিট: নিচের কোডে অন্তত চারটি সমস্যা খুঁজুন: int main(int argc, char **argv) { int n = atoi(argv[1]); if (n <= 0) { printf("bad: "); printf(argv[1]); return 0; } … }
    • argc যাচাই নেই — আর্গুমেন্ট ছাড়া চালালে argv[1] NULL, আর atoi(NULL) undefined behavior — সাধারণত crash করে।
    • atoi — “3x” মেনে নেয়, বড় মানে undefined behavior; strtol + পাহারা লাগবে।
    • error stdout-এ — fprintf(stderr, …) হওয়া উচিত।
    • printf(argv[1]) — format string vulnerability; "%s" দিয়ে ছাপতে হবে।
    • ব্যর্থ হয়েও return 0 — হওয়া উচিত 2 (ভুল ব্যবহার)।
  4. run()-কে বড় করুন (কোড লিখতে হবে): অংশ ৫-এর প্রোগ্রামে দুটি জিনিস যোগ করুন — -v সুইচ (থাকলে verbose=1 ছাপবে) আর -h/--help, যা usage stdout-এ ছেপে exit 0 দেবে। তারপর main-এ এই দুটির জন্য নকল argv যোগ করে পরীক্ষা করুন।

    লুপের প্রথম দিকে দুটি শাখা যোগ করলেই চলে — অজানা অপশনের শাখার আগে রাখতে হবে, নইলে -v-ও “unknown option” হয়ে যাবে:

    int verbose = 0;
    /* লুপের ভেতরে, -n শাখার আগে বা পরে: */
    if (strcmp(argv[i], "-v") == 0) verbose = 1;
    else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
        printf("usage: marks [-v] [-n 1..100] FILE\n");   /* সাহায্য চাওয়া ভুল নয় */
        return 0;
    }
    /* শেষে: */
    if (verbose) printf("verbose=1\n");

    পরীক্ষা: {"marks", "-v", "a.txt", NULL} → exit 0 আর verbose=1; {"marks", "--help", NULL} → usage stdout-এ, exit 0 (খেয়াল করুন, ফাইলের নাম না থাকলেও); {"marks", "-vx", "a.txt", NULL} → exit 2.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি variadic ফাংশনের ভেতরের কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. va_list ap; — বুকমার্ক ঘোষণা
  2. va_start(ap, last); — শেষ নাম-দেওয়া parameter-এর পরে বসানো
  3. va_arg(ap, int) — একে একে মান পড়া (লুপে)
  4. va_end(ap); — পরিষ্কার করা
  5. return — ফল ফেরত দেওয়া

./marks report.txt --top 3 চালালে argc কত, আর argv[3]-এ কী?

  • argc = 3, argv[3] = 3 (একটি int)
  • argc = 5, argv[3] = int 3
  • argc = 4, argv[3] = স্ট্রিং "3"
  • argc = 4, argv[3] = NULL

strtol("42abc", &end, 10) ডাকার পরে কী সত্য?

  • ফল 0, কারণ লেখাটি পুরো সংখ্যা নয়
  • ফল 42, আর *end হলো 'a'
  • ফল 42, আর errno হলো ERANGE
  • প্রোগ্রাম crash করে

একটি টুল ভুল অপশন পেলে কী করা উচিত?

  • stdout-এ "error" লিখে exit 0
  • অপশনটি উপেক্ষা করে কাজ চালিয়ে যাওয়া
  • stderr-এ বার্তা লিখে exit 0
  • stderr-এ বার্তা ও usage লিখে অশূন্য exit status (যেমন 2)

একটি variadic ফাংশনে float পাঠানো হয়েছে। ভেতরে কীভাবে পড়বেন?

  • va_arg(ap, double)
  • va_arg(ap, float)
  • va_arg(ap, int)
  • ap[0] লিখে

ব্যবহারকারীর দেওয়া নাম name ছাপার নিরাপদ উপায় কোনটি?

  • printf(name);
  • printf(name); printf("\n");
  • printf("%s\n", name);
  • printf("%d\n", name);

এই পাতার কোড-রানার argv পাঠাতে পারে না। argv-নির্ভর কোড পরীক্ষার সবচেয়ে ভালো পদ্ধতি কোনটি?

  • main-এর ভেতরে argv[1]-এ জোর করে একটি মান বসিয়ে দেওয়া
  • সব যুক্তি run(argc, argv)-এ রেখে নকল argv অ্যারে দিয়ে run-কে বারবার ডাকা
  • argv বাদ দিয়ে scanf দিয়ে ইনপুট নেওয়া
  • পরীক্ষা না করে সরাসরি ব্যবহার করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. main(int argc, char *argv[]): argc-তে শব্দ-সংখ্যা নামসহ, argv-তে স্ট্রিংয়ের ঠিকানা, আর argv[argc] সবসময় NULL.
  2. argv-এর সব কিছুই লেখা। সংখ্যা বানাতে strtol — end pointer, errno = 0 আগে, ERANGE, আর নিজের সীমা।
  3. ফল stdout-এ, সমস্যা stderr-এ; সফল হলে exit 0, ব্যর্থ হলে ১, ভুল ব্যবহারে প্রথা অনুযায়ী ২.
  4. যুক্তি রাখুন run(argc, argv)-এ — নকল argv দিয়ে টেস্ট করা যায়; বড় কাজে getopt / getopt_long.
  5. Variadic: va_list → va_start → va_arg → va_end; char/short আসে int হয়ে, float আসে double হয়ে; format string কখনো ব্যবহারকারীর লেখা নয়।

পরের পর্বে: আজ strtol, getenv, strcmp, vfprintf ব্যবহার করলাম — এগুলো সবই C-এর standard library-র অংশ। পরের পর্বে সেই তৈরি হাতিয়ারের বাক্সটি খুলে দেখব: কোন header-এ কী আছে, আর নিজে লেখার আগে কোথায় খুঁজবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান