What happens when you type
ls -l *.c and hit Enter in a Shell?
This thread is made with the main purpose of trying to explain, with many details as possible, how does the command “ls -l *.c” is processed in a Shell (Linux) after pressing Enter.
It’s important to note that the following explanation is COMPLETELY BASED on this particular project, meaning that, the process of executing the command above via Simple Shell won’t be the same as in the original Shell.
Having said that, you can take this information as a simple reference for your own development of a simulated Shell, but please don’t consider it as the ultimate example. #DYOR
After running the Simple Shell in the Interactive Mode, you type in the Command Prompt
ls -l *.c and press
The previous action will first store the argument typed in the
*argv variable from the main.c program, which then will be passed as a parameter in the
shellLoop() from the shell_loop.c program.
Within the body of the
shellLoop() , the pointers which will be used are first initialized to
NULLbecause of Good Practices, and also a variable called
getline is set wtih type ssize_t, to return a value that could either be a positive integer (including 0), or a NEGATIVE VALUE (-1) to indicate an ERROR.
Then, the first thing the
shellLoop()function will do is to create a
linked list of the PATH directories by using another function called
listpath() which parameter will be the memory address of
pathcopy pointer .
Now, in the body of
listpath() 2 functions are called.
The first one is
_getenv("PATH") which will look for the “PATH” environment variable and will return the address of the result without including the ‘=’ symbol if found (otherwise, it will return
The second one is
_getpathdir(getEnv, pathCopy) which first will make a copy of the result returned by
_getenv(), then it will allocate enough memory for the Node, then it will execute
strtok(*pathCopy, ":") with the parameter ‘
:’ to tokenize the copy, then it will add the first
tokento the field str of the linked list, then it will add the ‘next’ token as the new
head, then it will assign the memory allocated to that
head , then it will repeat the previous process inside a while loop to fill the linked list with the rest of the string until the last token to add is
Finally, it will return the Node to
listpath() , which then will return that Node to
shellLoop() , the linked list of PATH directories will be useful when the Simple Shell needs to find the corresponding executable binary of the command passed.
Cool, now here comes the
while (TRUE)loop will be responsible for keeping the exection of more commands until the user decides to kill the Simple Shell by sending the syscall
exitor until there’s no more memory RAM to allocate stuff.
Now it will execute
fflush(stdout) to clean the buffer of the stdout.
Then, it will execute
readLine(&buffer, &tokens) from the parser_tools.c program.
readLine() will first initialize its stuff , then it will store the result of executing
_getline(buffer, &bufferSize, stdin) in a variable called
gl, such previous mentioned function belongs to the allowed_library.c program.
_getline()will be responsible for keeping the exectuion of
_getchar(), which will be a function that reads 1 char from the
EOFif it reaches the End of File (i.e. no more data to read from the stdin).
(*buff)if it reads the char successfully.
_getchar()will kill the Simple Shell if an error occurs when trying to execute (e.g Signal Interruption).
_getline() will first try to allocate 120 bytes of memory for then executing
_getchar() until it returns
EOF (End of file), then if the
*n buffer gets filled up, it will increase its size.
Then it will add 1 to the
n_read counter , for then adding the char read to the
[n_read — 1]array at the previous position, then will evaluate if the current char it’s a line break ‘
\n’ (ASCII number 10) to break the while loop.
Then it will evaluate if the current char is an
EOF to avoid an eternal loop cycle after executing the first command passed, if true it will return
shellLoop() , otherwise it will add
NULL) to the
[n_read]array at the last position.
Finally it will return the number of char
Obviously, for this case
gl will be greater than 0 and
**buffer will not end up with a line break
\n , then the Simple Shell will proceed to execute the function
replaceNewLine(buffer) (which is btw some lines above
This function will simply replace the
\n char in the buffer by a
lenTokens(gl, buffer) shall be executed, the returned variable shall be stored in the variable
This function will first initialize its stuff, it’s important to note the pointer
delim will be pointing to line break
\nand a tab
\t , then it will allocate enough memory for copying the buffer, then it will execute another function called
_strcpy(copyBuffer, *buffer); to copy the buffer’s content, being
copyBuffer the destiny and
*buffer the origin.
Then it will execute
strtok(copyBuffer, delim); for tokenizing the copy and save the result in
tempToken, then it will iterate over the tokenized copy until it reaches
NULL , then it will clean the variable
copyBuffer, and then it will finally return the number of tokens (words) the command passed by the user had.
processTokens(tokens, buffer, countToken) shall be executed.
This function will just update the content of tokens variable by first allocating enough memory for
tokens, then it will execute
strtok(*buffer, delim); for tokenizing the buffer’s content and save the result in
token , then it will iterate over
token until it reaches
NULL , saving each
token current position inside the
tokens variable, then it will execute
strtok again with the the arguments of the previous
strtok in order to add the
NULL as the last value inside the
All right, now we’re back at the body of
readLine() , the next functions to execute will be
isBasicExit(tokens, countToken, &gl) and
isEnv(tokens, countToken) , however, as the command passed by the user for this case was “
ls -l *.c” these functions won’t be executed at all.
Finally, at the end of
readLine() body it will return
glis not equal to EOF in this case, it won’t break this loop.
Then, the next function to execute will be
addPath(&tokens, path) from the parser_tools.c program.
addPath() will do is to create binary file paths by concatenating each one of the tokens of the linked list of the PATH directories with the first element pointed by
*tokens (which would be the first word of the command passed by the user) and store them in a variable called
copyPath , but not without first allocating the right amount of memory for such process, of course.
For then evaluating each one of those combinations with a function called
stat(copyPath, &st) , which is a function that returns 0 if it can list the properties of a file identified by the path provided (
copyPath) and then dumping those properties into the stat type struct (
st), it returns -1 otherwise.
free(copyPath) is executed, cleaning the content of that parameter, and then redoing the previous process until
/bin/ls -l *.c is created and validated.
Once found, it will return the right
copyPath value (i.e.
/bin/ls -l *.c ).
All right we are close to finish 🙂…
Now, the final 2 functions to execute will be
isPath(&tokens, &fullPath, argv, &counter, &errorShowed) and
executeLine(&buffer, &tokens, fullPath), both from the executer_tools.c program.
isPath()function will verify with
access()if the first word of the command passed by the user actually exists in the binary files or can be executed by the user.
If true, it will return 0 and make
*pathpoints to a copy of that first word of the command.
Else, in case there isn’t any binary file associated with that word, it will return -1, which then will print to the stderr a message indicating that such command wasn’t found.
Else, in case such command DO EXIST, but the current user DOESN’T HAVE the necessary execution permissions, it will print to the stdout that the user doesn’t have the permissions.
Also, just in case you are wondering…
Tests whether the file exists.
Tests whether the file can be accessed for reading.
Tests whether the file can be accessed for writing.
Tests whether the file can be accessed for execution.
In our case, the
ls command is a very common and fundamental command on Linux, so it will just return 0 and continue to the last function 😄.
executeLine() is maybe the most important function for the user, because this is the only function that executes and prints the result of executing the command to the stdout, delivering what the user is asking for.
executeLine() will evaluate if the user has just entered a Tab (
\n ) as a command without anything else or if
NULL as value. If true, it cleans
free() and return 1.
Else, it will execute the function
fork() and assign its return to a variable called
fork() will try to create a sub-process in which the execution of
ls -l *.c command will be done, it also uses the function
wait() to make sure that it will wait enough time to create a sub-process.
fork() will return -1 if it couldn’t create a sub-process, which then will notify to the user with
perror() and return -1.
fork() will return 0 if it managed to create a sub-process successfully, then it will execute the command of the user using
execve(fullPath, *tokens, environ) , which btw will also return -1 if for some particular reason it couldn’t execute the command, notifying the user with the message “Exec -1 \n” via
printf() , it will also notify the command that failed the exectuion via
perror() , and will also clean the contents of
*buffer and finally executing the command
exit(99) killing the Simple Shell.
If the execution of the command in a sub-process was a success, its result will be printed to the stdout, and will also clean the contents of
fullPath and return 1.
This will be the final result:
Once done, the Simple Shell will be ready to receive more commands from the user, repeating the previous big process described, until the user type
exit and press
Enter, or there’s no more memory RAM to allocate, creating a
Segmentation fault, whatever happens first ¯\_(ツ)_/¯.