এই post পড়ার আগে Part 2: readdir(), dirent, closedir() পড়ে নাও — সেখানে directory থেকে file list করা শেখানো হয়েছে।

Part 2 এর শেষে একটা জিনিস notice করেছিলাম — আমাদের myls চালালে output এ . আর .. দেখাচ্ছে, কিন্তু real ls দেখায় না। তখন বলেছিলাম "এটা পরে fix করব।" এই part এ সেই কাজটাই করব।

কিন্তু শুধু fix করব না — আরেকটু গভীরে যাব। কেন এই behavior? এটা কি intentional নাকি accidental? আর এই জায়গাটায় Unix এর একটা মজার ইতিহাস আছে।

Hidden Files — একটা Accidental Feature এর জন্ম

Unix এর শুরুর দিকে, ls প্রোগ্রামে . (current directory) আর .. (parent directory) দেখাত। এগুলো দেখতে annoying লাগত কারণ প্রতিটা directory list এ এরা থাকতই। তখন কেউ একজন — ধারণা করা হয় Ken Thompson বা Dennis Ritchie — এই দুটো entry skip করার জন্য একটা quick fix লিখলেন:

if (name[0] == '.') continue;

এটা লেখার কথা ছিল এরকম:

if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;

কিন্তু সেটা না লিখে shorter version লেখা হলো — "যদি নামের প্রথম character . হয়, skip করো।" Bug! এতে শুধু . আর .. না, যেকোনো dot দিয়ে শুরু হওয়া file skip হয়ে গেল।

কিন্তু তখন কেউ এটাকে bug মনে করল না — বরং এটাকে feature হিসেবে ব্যবহার শুরু করে দিল। .bashrc, .gitignore, .ssh — এই convention টা সেই একটা lazy shortcut থেকেই জন্ম নিয়েছে।

আজকে Unix/Linux এর hidden file মানেই dot দিয়ে শুরু — এটা কোনো official specification না, একটা accidental convention।

আমাদের সমস্যা

Part 2 এর myls এর output এ সমস্যাটা দেখি:

$ ./myls
.             32 bytes
..            32 bytes
myls          32 bytes
myls.c        32 bytes

Real ls দিলে:

$ ls
myls  myls.c

দুটো পার্থক্য:

  1. . ও .. দেখাচ্ছে — দেখানো উচিত না by default
  2. Hidden files (যেকোনো dot file) দেখাচ্ছে — -a flag ছাড়া দেখানো উচিত না

Fix টা simple:

if (entry->d_name[0] == '.') continue;

কিন্তু আমরা চাই real ls এর মতো behavior — ./myls -a দিলে সব দেখাবে, না দিলে hide থাকবে। এজন্য command-line argument handle করতে হবে।

argc ও argv — Program কিভাবে Argument পায়

main function এর signature টা এতদিন ignore করে এসেছি:

int main(int argc, char **argv)

এই দুটো parameter দিয়ে program তার command-line argument গুলো পায়।

  • argc (argument count): কতগুলো argument আছে। Program এর নিজের নামও count হয়, তাই minimum value সবসময় 1।
  • argv (argument values): সব argument এর array। argv[0] সবসময় program এর নাম।

উদাহরণ — ./myls -a /tmp run করলে:

argc    = 3
argv[0] = "./myls"
argv[1] = "-a"
argv[2] = "/tmp"

Memory তে argv দেখতে কেমন?

argv আসলে একটা pointer এর pointer — char **। প্রতিটা element একটা string এর শুরুর address। শেষে NULL থাকে sentinel হিসেবে।

    argv
     │
     ▼
   ┌─────────┐
   │  ptr 0  │───────▶  "./myls\0"
   ├─────────┤
   │  ptr 1  │───────▶  "-a\0"
   ├─────────┤
   │  ptr 2  │───────▶  "/tmp\0"
   ├─────────┤
   │  NULL   │
   └─────────┘

Live experiment

নিজে হাতে দেখতে চাইলে এই ছোট্ট program টা লিখে run করো:

// argc_test.c
#include <stdio.h>

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

Compile করে চালাও:

$ gcc -o argc_test argc_test.c
$ ./argc_test hello world -a
argc = 4
argv[0] = ./argc_test
argv[1] = hello
argv[2] = world
argv[3] = -a

লক্ষ্য করো — argv[0] এ program এর নাম automatically চলে এসেছে, যদিও আমরা সেটা type করিনি। এটা shell এর কাজ — program execute করার সময় নামটাও argument হিসেবে পাঠায়।

String Compare — == কেন কাজ করে না

Flag check করতে গেলে argv[1] == "-a" লিখতে ইচ্ছে করে। কিন্তু C তে এটা কাজ করবে না।

C তে string হলো character array এর pointer। == দিয়ে দুটো pointer compare করলে দেখে দুটো একই memory address কিনা — দুটো string এর content একই কিনা সেটা না।

এজন্য strcmp() ব্যবহার করতে হয়:

#include <string.h>

strcmp("hello", "hello")  // returns 0  (equal)
strcmp("abc", "xyz")      // returns negative (abc comes before xyz)
strcmp("xyz", "abc")      // returns positive

0 মানে equal। তাই flag check করতে:

if (strcmp(argv[i], "-a") == 0) {
    show_hidden = 1;
}

Updated myls.c

এখন সব মিলিয়ে updated code লিখি:

// myls.c

#include <dirent.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
  // -a flag আছে কিনা check করি
  int show_hidden = 0;
  for (int i = 1; i < argc; i++) {
    if (strcmp(argv[i], "-a") == 0) {
      show_hidden = 1;
    }
  }

  DIR *dir_stream;
  struct dirent *dir_read;

  dir_stream = opendir(".");

  if (dir_stream == NULL) {
    perror("Error while trying to open directory");
    return 1;
  }

  while ((dir_read = readdir(dir_stream)) != NULL) {
    // show_hidden না থাকলে dot file skip করি
    if (!show_hidden && dir_read->d_name[0] == '.') {
      continue;
    }
    printf("%s\n", dir_read->d_name);
  }

  if (closedir(dir_stream) == -1) {
    perror("Can't close the dir");
    return -1;
  }

  return 0;
}

কোডটা ধাপে ধাপে কী করছে

1. Flag scan:

int show_hidden = 0;
for (int i = 1; i < argc; i++) {
  if (strcmp(argv[i], "-a") == 0) show_hidden = 1;
}

i = 1 থেকে শুরু — argv[0] হলো program এর নাম, সেটা skip করি। সব argument check করি, -a পেলে show_hidden = 1 করি।

2. Hidden file filter:

if (!show_hidden && dir_read->d_name[0] == '.') {
  continue;
}

দুটো condition একসাথে:

  • show_hidden false (মানে -a দেওয়া হয়নি)
  • নামের প্রথম character .

দুটোই true হলে continue — এই entry skip করে পরেরটায় যাও।

Output Comparison

Compile করে test করি:

gcc -Wall -Werror -g myls.c -o myls

তিনটা version পাশাপাশি দেখি — real ls, আমাদের ./myls, আর ./myls -a:

┌────────────────┬────────────────┬─────────────────┐
│    $ ls        │   $ ./myls     │   $ ./myls -a   │
├────────────────┼────────────────┼─────────────────┤
│  myls          │  myls          │  .              │
│  myls.c        │  myls.c        │  ..             │
│                │                │  myls           │
│                │                │  myls.c         │
└────────────────┴────────────────┴─────────────────┘
  • ls আর ./myls — identical। Hidden files hide হচ্ছে।
  • ./myls -a — সব দেখাচ্ছে, . ও .. সহ।

Real ls এর মতো behavior!

GDB দিয়ে argv দেখা

কৌতূহল থামাতে পারছি না — GDB দিয়ে দেখি argv memory তে আসলে কী আছে:

GDB session টা দেখতে এরকম হবে:

$ gdb ./myls
(gdb) b main
Breakpoint 1 at 0x11a9: file myls.c, line 6.

(gdb) run -a
Starting program: /work/myls -a

Breakpoint 1, main (argc=2, argv=0x7fffffffe328) at myls.c:6

(gdb) p argc
$1 = 2

(gdb) p argv[0]
$2 = 0x7fffffffe5e4 "./myls"

(gdb) p argv[1]
$3 = 0x7fffffffe5eb "-a"

(gdb) p argv
$4 = (char **) 0x7fffffffe328

(gdb) p *argv
$5 = 0x7fffffffe5e4 "./myls"

Output এ দেখবে:

  • argc = 2
  • argv[0] = "./myls" — program এর নাম
  • argv[1] = "-a" — আমাদের flag
  • argv নিজে একটা memory address — কারণ এটা pointer

একটা interesting জিনিস: p argv দিলে একটা address দেখাবে, p *argv দিলে argv[0] দেখাবে। কারণ argv হলো char** — pointer এর pointer।

আজকে কী শিখলাম

বিষয়কী শিখলাম
Hidden filesUnix convention — dot দিয়ে শুরু হলে hidden। একটা accidental bug থেকে জন্ম নিয়েছে।
argcCommand-line argument এর count। Program নিজেও count হয়।
argvArgument এর array। argv[0] = program name।
strcmp()String compare এর সঠিক উপায়। == দিয়ে string compare হয় না C তে।
continueLoop এর current iteration skip করে পরেরটায় যায়।
Boolean flag patternint show_hidden = 0 → argument loop → if (!show_hidden && ...)

উপসংহার

একটা ছোট্ট if check আর argc/argv — এটুকু দিয়েই আমাদের myls এখন real ls এর মতো hidden files handle করতে পারছে। আর সেই accidental bug এর গল্পটা — এটা আমার কাছে reminder যে অনেক "design decision" আসলে কেউ একদিন lazy ছিল বলে।

পরের part এ argv আরো গভীরে দেখব — path argument নেওয়া, যাতে ./myls /tmp এর মতো যেকোনো directory list করা যায়।


সম্পূর্ণ সিরিজ

Partবিষয়
Part 1opendir(), DIR structure, GDB দিয়ে debug
Part 2readdir(), dirent struct, closedir()
Part 3 (এই post)Hidden files, argc/argv, -a flag
Part 4argv deep dive, char**, path argument