logo
Join This Community
How Shell Executes `ls -l *.c` cover image

How Shell Executes `ls -l *.c`

Dereje Desta

Dereje Desta

7 days ago

linux
beginners
C
System programming

In this post, we'll talk about how the shell executes the command ls -l *.c. Moreover, we'll also discuss how it is implemented under the hood.

We strongly advise you to have at least fundamental knowledge of the C programming language and Linux system programming before you continue.

Note that we skipped error checks and memory management for brevity. Refer to the project implemention.

When we open a shell(users perspective) it prompts us to enter a line of command and when we enter the command it executes our command and it prompts us to enter another command.

But there is more to this story. The following are simplified versions of the steps the shell takes.

1. Getline

The shell prints the prompt and waits for input. Let us assume we entered ls -l *.c. This is performed using getline().

while(1) {
  char *line = NULL;
  size_t len = 0, nline;
  printf("$ ");
  nline = getline(&line, &len, stdin);
  if(nline == -1) return 0;
  // continues here
}

2. Tokenize

Then The shell splits this line of command into tokens. In our case ls, -land *.c. this uses strtok(command, DELIM) where DELIM is string consisting whitespace character(spaces, tabs, etc.).


// continued
const char *DELIM = " \t\a\r"
token = strtok(line, DELIM);
while(token != NULL) {
  tokens[pos++] = strup(token); // push to array of strings
  token = strtok(NULL, DELIM);
}

3. Expansion

After that, the shell looks for expansion in our case token *.c matches every file ending in .c. So tokens will become:

tokens = {"ls", "-l", "main.c", "util.c", "test.c"};

4. Alias

The shell checks if the first token is aliased. if it is it replaces it with the original expanded version. In our case ls is aliased to ls --color=tty.

ls: aliased to ls --color=tty
/usr/bin/ls
/bin/ls

5. Builtins

Then checks if the command is built-in (like exit, cd, help ... etc). ls is not built-in command.

6. PATH

If the command is not builtin command it searches the command in the PATH environmental variable. ls is found in the path /bin/ls.

7. New Process

After that Create a new process using syscall fork().

int status;
pid_t child_pid = fork();

if (child_pid == -1)
  return (1);

if (child_pid == 0) // if this is child process execute
{
  if (execve(tokens[0], tokens, __environ) == -1)
  return (1);
} else // if it is parent wait for child
  wait(&status);

8. Execution

Finally, it executes it. It uses execve(tokens[0], tokens, env) a system call. See step 7. and continues at step 1 again.

Conclusion

This is just a simple illustration of how shell goes on executing commands like `ls -l *.c`. But nowadays shells have evolved to do many complex tasks like history, conditional statements, loops, and any other things programming languages support. It was quite a journey learning shell and we believe you can benefit from implementing those concepts in yourself. You can find our implementation in C here Thanks