এই post পড়ার আগে Part 3: Hidden files, argc/argv, -a flag পড়ে নাও — সেখানে 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
  • ✅ -a flag যেকোনো 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 checkArray index access এর আগে argc > 1 check বাধ্যতামূলক
Undefined behaviorCrash না হলেই ঠিক না — invisible bug এর জন্ম
perror(path)Custom prefix সহ informative error message
GDB x commandRaw 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 1opendir(), DIR structure, GDB দিয়ে debug
Part 2readdir(), dirent struct, closedir()
Part 3Hidden files, argc/argv, -a flag
Part 4 (এই post)char**, argv memory layout, path argument
Part 5stat() system call, file metadata