এই post পড়ার আগে Part 3: Hidden files, argc/argv,
-aflag পড়ে নাও — সেখানেargc/argvএর সাথে প্রথম পরিচয় হয়েছে।
Part 3 এ argc আর argv এর সাথে চটপট পরিচয় করেছি — -a flag detect করার জন্য যতটুকু দরকার, ততটুকু। কিন্তু real ls শুধু flag না, path ও নেয়:
$ ls /tmp
$ ls /etc
$ ls -la /var/log
আমাদের myls এখনো hardcoded — শুধু current directory (.) এ কাজ করে। চাই এমন: ./myls /tmp লিখলে /tmp এর content দেখাবে, ./myls -a /etc লিখলে /etc এর hidden file সহ দেখাবে।
এটা করতে গেলে argv এর real structure বুঝতে হবে। Part 3 এ আমরা argv[0], argv[1] access করেছি — কিন্তু এই array টা memory তে আসলে কেমন? char ** type টা দেখলে ভয় লাগে কেন? argv[100] access করলে কী হয়?
এই part এ সেই গভীরে যাব।
char ** — Type টা ভেঙে বুঝি
main এর signature:
int main(int argc, char **argv)
char **argv পড়লে পড়ে — "pointer to pointer to char"। প্রথমবার দেখলে মাথা ঘোরে। কিন্তু ধাপে ধাপে বুঝলে সহজ।
Step 1 — Single string:
C তে string হলো character এর array। আর array এর নাম মানে প্রথম element এর address। তাই:
char *name = "nazrul";
name একটা pointer — 'n' character টার address ধরে রাখে। Memory তে:
name ──▶ ['n']['a']['z']['r']['u']['l']['\0']
Step 2 — Multiple strings:
একাধিক string রাখতে চাইলে?
char *names[] = { "nazrul", "islam", "dhaka" };
names একটা array, প্রতিটা element একটা char *। Array এর নাম মানে প্রথম element এর address — যেটা এখানে char * এর pointer। মানে char **।
names ──▶ ┌─────────┐
│ char * │───▶ "nazrul\0"
├─────────┤
│ char * │───▶ "islam\0"
├─────────┤
│ char * │───▶ "dhaka\0"
└─────────┘
argv exactly এটাই — program কে pass করা string গুলোর array।
Memory তে argv এর পূর্ণ চেহারা
ধরো আমরা run করলাম:
./myls -a /tmp
Memory তে এই সময় যা হচ্ছে:
Stack frame (main): Process memory (args area):
┌──────────┐
│ argc = 3 │
├──────────┤
│ argv │──────┐
└──────────┘ │
▼
┌─────────┐
│ ptr 0 │─────▶ "./myls\0"
├─────────┤
│ ptr 1 │─────▶ "-a\0"
├─────────┤
│ ptr 2 │─────▶ "/tmp\0"
├─────────┤
│ NULL │ ◀── sentinel
└─────────┘
গুরুত্বপূর্ণ পয়েন্ট:
argvনিজে একটা pointer — একটা array কে point করে।- সেই array এর প্রতিটা element আবার একটা pointer — একটা null-terminated string কে point করে।
- Array এর শেষে
NULL— এটা C standard এর guarantee (C99 section 5.1.2.2.1)।
মজার ব্যাপার — সেই NULL দিয়ে argc ছাড়াও iterate করা যায়:
for (char **p = argv; *p != NULL; p++) {
printf("%s\n", *p);
}
এটা C idiomatic pattern। কিন্তু আমরা argc দিয়েই করব — beginner-friendly।
GDB দিয়ে argv Inspect করা
এবার actual memory তে দেখি। Docker container এ ঢুকে একটা ছোট্ট inspection program বানাই:
// argv_inspect.c
#include <stdio.h>
int main(int argc, char **argv) {
printf("inspect me with gdb\n");
return 0;
}
Compile and debug:
gcc -Wall -g argv_inspect.c -o argv_inspect
gdb ./argv_inspect
GDB session:
(gdb) b main
Breakpoint 1 at 0x1149: file argv_inspect.c, line 4.
(gdb) run -a /tmp hello
Breakpoint 1, main (argc=4, argv=0x7fffffffe328) at argv_inspect.c:4
(gdb) p argc
$1 = 4
(gdb) p argv
$2 = (char **) 0x7fffffffe328
(gdb) p argv[0]
$3 = 0x7fffffffe5a0 "./argv_inspect"
(gdb) p argv[1]
$4 = 0x7fffffffe5b0 "-a"
(gdb) p argv[2]
$5 = 0x7fffffffe5b3 "/tmp"
(gdb) p argv[3]
$6 = 0x7fffffffe5b8 "hello"
(gdb) p argv[4]
$7 = 0x0
কয়েকটা জিনিস লক্ষ্য করার মতো:
১. argv[4] হলো 0x0 — মানে NULL। C standard এর সেই sentinel টা আসলেই আছে। দেখলে?
২. String গুলো consecutive memory তে: 0x...e5a0, ...e5b0, ...e5b3, ...e5b8 — এরা পাশাপাশি বসে আছে। কারণ shell program টা launch করার সময় সব argument একটা single block এ copy করে।
৩. argv নিজে একটা address (0x7fffffffe328) — যেখানে pointer array টা বসে আছে।
আরো deeper দেখতে চাইলে x command দিয়ে raw memory inspect করা যায়:
(gdb) x/5xg argv
0x7fffffffe328: 0x00007fffffffe5a0 0x00007fffffffe5b0
0x7fffffffe338: 0x00007fffffffe5b3 0x00007fffffffe5b8
0x7fffffffe348: 0x0000000000000000
x/5xg মানে — "examine 5 giant (8-byte) words in hex"। পরপর 5 টা pointer দেখাচ্ছে, শেষটা NULL (0x0000...)।
এই একটা command দিয়ে পুরো argv array এর memory layout চোখের সামনে।
argv[i][0] — এই syntax কেন কাজ করে?
Path detect করতে আমাদের flag (- দিয়ে শুরু) বাদ দিতে হবে। তার মানে প্রতিটা argument এর প্রথম character check করব।
if (argv[i][0] != '-') { ... }
এই double indexing প্রথমবার confusing। কিন্তু ভেঙে দেখলে simple। C তে p[i] আসলে *(p + i) এর syntactic sugar। তাই:
argv[i]=*(argv + i)= i-তম pointer (ie একটাchar *)argv[i][0]=*(argv[i] + 0)= সেই string এর প্রথম character
Pointer arithmetic দুই ধাপে:
argv[i][0]
│ │ │
│ │ └─▶ string এর 0-তম char
│ └────▶ i-তম pointer (string এর শুরু)
└─────────▶ pointer array এর start
Double indexing দেখে ভয় পেয়ো না — গভীরে এটা শুধু pointer arithmetic।
Path Argument নেওয়া
এবার আমাদের myls upgrade করি। Logic simple:
- User যদি flag-না এমন argument দেয়, সেটা path হিসেবে নাও।
- না দিলে, default
.(current directory)।
char *path = "."; // default
for (int i = 1; i < argc; i++) {
if (argv[i][0] != '-') {
path = argv[i];
break;
}
}
i = 1 থেকে শুরু — argv[0] = program name, skip। প্রথম non-flag argument পেলে সেটা path, loop থেকে বের হই।
Safe Access — Bounds check কেন বাধ্যতামূলক
Dangerous code:
char *path = argv[1]; // ❌ argc = 1 হলে ভয়ংকর
User যদি শুধু ./myls লিখে — argc = 1, তাই argv[1] হলো সেই NULL sentinel। তারপর argv[1][0] access করলে NULL pointer dereference — segmentation fault।
সঠিক code:
if (argc > 1 && argv[1][0] != '-') {
path = argv[1];
}
এটাই undefined behavior এর classic case। C তে undefined behavior মানে — compiler কোনো guarantee দেয় না কী হবে:
- কখনো crash হয় (লাকি)
- কখনো garbage data পায় (unlucky)
- কখনো "ঠিকঠাক চলছে" মনে হয়, কিন্তু পরে অন্য কোথাও weird bug — এটাই সবচেয়ে ভয়ংকর
Lesson: Array index access এর আগে সবসময় bounds check।
Updated myls.c
সব মিলিয়ে:
// myls.c
#include <dirent.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
int show_hidden = 0;
char *path = "."; // default: current directory
// argument parse — flag ও path একসাথে handle করি
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
show_hidden = 1;
} else if (argv[i][0] != '-') {
path = argv[i];
}
}
DIR *dir_stream = opendir(path);
if (dir_stream == NULL) {
perror(path); // path নাম সহ error
return 1;
}
struct dirent *entry;
while ((entry = readdir(dir_stream)) != NULL) {
if (!show_hidden && entry->d_name[0] == '.') continue;
printf("%s\n", entry->d_name);
}
if (closedir(dir_stream) == -1) {
perror("closedir");
return 1;
}
return 0;
}
কোডে কী কী বদলালো
Part 3 এর সাথে তুলনা করলে দুটো মূল পরিবর্তন:
১. Path variable:
char *path = ".";
Default . — Part 3 এর hardcoded opendir(".") এর বদলে এখন variable।
২. Argument loop এ else if:
} else if (argv[i][0] != '-') {
path = argv[i];
}
Flag না হলে path। - দিয়ে শুরু হলে flag (পরে আরো flag যোগ হবে — -l, -R)।
৩. perror(path):
Error হলে path নাম সহ message — informative। "/nonexistent: No such file or directory" অনেক ভালো "Error opening directory" এর চেয়ে।
Test করি
$ gcc -Wall -Werror -g myls.c -o myls
চারটা case একসাথে দেখি:
┌─────────────────────────────────┬───────────────────────────┐
│ Command │ Behavior │
├─────────────────────────────────┼───────────────────────────┤
│ $ ./myls │ current dir, no hidden │
│ $ ./myls /tmp │ /tmp, no hidden │
│ $ ./myls -a /tmp │ /tmp, with hidden │
│ $ ./myls /nonexistent │ error with path name │
└─────────────────────────────────┴───────────────────────────┘
Actual output:
$ ./myls /tmp
com.apple.launchd.xxxx
powerlog
tmp.xxxxx
$ ./myls -a /tmp
.
..
.DS_Store
com.apple.launchd.xxxx
tmp.xxxxx
$ ./myls /nonexistent
/nonexistent: No such file or directory
Error case এ perror(path) কীভাবে path নাম সহ message বানাচ্ছে দেখলে? Context দেয় — user বুঝতে পারে exactly কোন path টা fail করল।
Real ls এর আরো অনেক কিছু — যা এখনো নাই
Real ls অনেক জটিল parsing করে:
$ ls /tmp /etc # একসাথে একাধিক path
$ ls -la /tmp # combined flags (-l এবং -a)
$ ls -l -a /tmp # separate flags, same meaning
$ ls /tmp -a # flag argument এর পরেও
আমাদের myls এখন handle করছে:
- ✅ একটা path argument
- ✅
-aflag যেকোনো position এ - ❌ একসাথে একাধিক path
- ❌ combined flags (
-la)
এগুলো future work। এখনকার foundation টা solid — উপরে আরো build করা যাবে।
আজকে কী শিখলাম
| বিষয় | কী শিখলাম |
|---|---|
char ** | Pointer to pointer — array of strings এর standard pattern |
argv[argc] | সবসময় NULL — C standard guarantee (sentinel) |
argv[i][0] | Double indexing — string এর i-তম element এর 0-তম char |
p[i] এর মানে | *(p + i) — pointer arithmetic এর syntactic sugar |
| Bounds check | Array index access এর আগে argc > 1 check বাধ্যতামূলক |
| Undefined behavior | Crash না হলেই ঠিক না — invisible bug এর জন্ম |
perror(path) | Custom prefix সহ informative error message |
GDB x command | Raw memory inspect — x/5xg (5 giant hex words) |
উপসংহার
এতদিন argv দেখলে মনে হতো — "ওটা magic, main এ এমনি থাকে।" কিন্তু আজকে দেখলাম — shell program launch করার সময় আসলেই একটা pointer array বানায়, সেই array এর pointer আমাদের main কে পাঠায়, শেষে NULL দিয়ে mark করে। Simple mechanism, কিন্তু এটা বুঝলে C তে argument parsing আর অজানা লাগে না।
এবার একটা পরিবর্তন আসতে যাচ্ছে। পরের part এ আমরা নতুন world এ ঢুকব — stat() system call। এতদিন শুধু library function ব্যবহার করেছি (opendir, readdir, closedir)। এবার সরাসরি kernel এর সাথে কথা বলব — file এর size, permission, owner, modification time পাব। এটা ls -l flag বানানোর প্রথম ধাপ।
System call আর library function — দুটোই function call দেখায় বাইরে থেকে, কিন্তু ভেতরে পুরো আলাদা জগৎ। Kernel mode, context switch, syscall table — সব সামনে আসবে।
সাথে থাকো।
সম্পূর্ণ সিরিজ
| Part | বিষয় |
|---|---|
| Part 1 | opendir(), DIR structure, GDB দিয়ে debug |
| Part 2 | readdir(), dirent struct, closedir() |
| Part 3 | Hidden files, argc/argv, -a flag |
| Part 4 (এই post) | char**, argv memory layout, path argument |
| Part 5 | stat() system call, file metadata |

