50.005 Computer System Engineering
Information Systems Technology and Design
Singapore University of Technology and Design
Natalie Agus (Summer 2024)
Shell .rc
file
The “rc” file
A shell “rc” file, where “rc” stands for “run commands,” is a script that runs every time a shell starts.
These files are used to configure the shell environment, including setting environment variables, aliases, and functions, as well as running commands and scripts that should execute with every new shell instance. The specific rc file and its location can vary depending on the shell and the operating system.
Here are some of the common shell rc files:
~/.bashrc
: The bashrc file is executed for interactive non-login shells. On Linux systems, when you open a new terminal window, the commands in this file run. For macOS, bash was the default shell until Catalina, after which zsh became the default.~/.zshrc
: This is the configuration file for Zsh, executed for interactive shells. It’s similar to~/.bashrc
but for Zsh.~/.bash_profile
or~/.profile
or~/.bash_login
: These files are executed for login shells (e.g., when you log in from the console or remotely). Typically,~/.bash_profile
will source~/.bashrc
to ensure consistent behavior between login and non-login shells.~/.vimrc
: Allows users to customize Vim, a highly configurable CLI text editor to their liking, automating many aspects, setting global preferences, and including custom commands and key mappings.
For this part of the assignment, we’re tasked with implementing functionality to read and interpret a configuration file named .cseshellrc
. This file should be placed in the root project directory.
This functionality enhances the shell by providing a simple but powerful way to automate startup tasks and configure the shell environment automatically upon launch, making it more flexible and personalized to the user’s needs.
.cseshellrc
This .cseshellrc
file allows for initial setup actions to be performed automatically each time your custom shell starts. The requirements for processing .cseshellrc
are straightforward and involve two main actions based on the content of each line in the file: setting PATH
, and executing commands.
Setting the PATH Environment Variable
If a line in .cseshellrc starts with the keyword PATH, the rest of the line should be interpreted as the value for the PATH environment variable. This involves setting the PATH environment variable of your shell process to the specified value in .cseshellrc
. The PATH variable is crucial as it tells the shell where to look for the executables corresponding to the command names that are entered.
With this, we are able to execute any other system program in the system, and not only those at [PROJECT_DIR]/bin
.
Executing Commands
For all other lines that do not start with “PATH”, the shell should treat these lines as commands to be executed. Each line represents a separate command that should be run by the shell, just as if it were typed in by the user at the command prompt. You will still need to execute this command in the child process. This allows for automatic execution of a series of commands at the startup of your shell, enabling tasks such as setting aliases, defining functions, or performing initial setup actions.
Tips
The processing of
.cseshellrc
should be carried out before the user is prompted for the first time. Additionally, it should be done only once at the very beginning of the cseshell process.
Sample Output and Implementation Notes
Given the following .cseshellrc
:
PATH=/Users/natalie_agus/Desktop/pa1-2024/bin:/usr/bin
clear
cal
Your shell must be able to process it the first time it launches:
Notice how the console is cleared, and calendar is printed out before the first prompt is presented. We are also able to access system program clear
residing at /usr/bin
, which indicates that the PATH
is registered correctly. We can also check that using the env
command implemented earlier and inspect the value of the PATH variable.
If .cseshellrc
contains invalid commands or empty spaces:
PATH=/Users/natalie_agus/Desktop/pa1-2024/bin:/usr/bin
clear
test
something
else
cal
It should still be able to execute the valid commands, while indicating that the previous commands don’t exist.
Afterwards, your shell must also be able to execute any system program set in PATH
using .cseshellrc
at all times, even when we cd
. Failure to do this results in -1% penalty.
Use execvp
instead of execv
execv
and execvp
are two of the exec family functions in Unix-like operating systems, which are used to replace the current process image with a new process image. These functions are typically used in system programming to execute external programs from within a C program. However, execv
requires a full path, while execvp
can work with just the program name or a relative path, automatically searching the PATH environment variable.
// using execvp
#include <stdio.h>
#include <unistd.h>
int main() {
// Arguments array
char *args[] = {"ls", "-l", NULL}; // Lists in long format
// Execute command
execvp(args[0], args);
// If execvp returns, it must have failed
perror("execvp failed");
return 1;
}
// using execv
#include <stdio.h>
#include <unistd.h>
int main() {
// Arguments array
char *args[] = {"ls", "-l", NULL}; // Lists in long format
// Execute command - assuming the full path to 'ls' is known
execv("/bin/ls", args);
// If execv returns, it must have failed
perror("execv failed");
return 1;
}
Therefore, all you need to do while processing the .rc file is to set the PATH value as the environment variable for the process. You don’t have to append anything, simply replace the existing PATH
with the new PATH
specified in the .rc file.
Use _exit
instead of exit
in Child Process
In the child process, if execvp
or execv
fails, you should use _exit(0)
instead of exit(0)
to terminate the child process so that it does not interfere with parent process’ resources.
_exit()
is a system call that terminates the process immediately, without performing any clean-up operations mentioned above. It ensures that the child exits quickly, without interfering with any shared resources (like standard I/O buffers) that the parent process might be using.
It is likely that you would open the .rc
file with fopen
and then read its content line by line using fgets
or getline
, and then immediately process each line by fork
and executing the line as a command. However, it is likely that you will meet an infinite loop as you combine fgets
or getline
with fork
. Read this post here if you’re interested. You can also try this compiling and running this program:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_LINE_LENGTH 1024
int main()
{
const char *filePath = "[PATH TO YOUR TEXT FILE]"; // Specify the path to your text file
FILE *file = fopen(filePath, "r");
if (file == NULL)
{
perror("Failed to open file");
return EXIT_FAILURE;
}
char line[MAX_LINE_LENGTH];
while (fgets(line, sizeof(line), file))
{
printf("Line [%s]\n", line); // Print the line read from the file
// fflush(0);
pid_t pid = fork();
if (pid == -1)
{
// If fork() returns -1, an error occurred
perror("fork failed");
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
// Child process
printf("Hello from child\n");
exit(EXIT_SUCCESS); // Exit child process, should've used _exit(EXIT_SUCCESS) instead
}
else
{
// Parent process
int status;
waitpid(pid, &status, 0); // Wait for child process to finish
}
}
fclose(file); // Close the file
return EXIT_SUCCESS;
}
Text file data, e.g ./test.txt
:
hello
from
the
other
side
It’s likely that this is your output (you’d have to terminate the program with ctrl+c):
The Cause: Shared file descriptor and resources between parent and child
Open file descriptors, along with other resources are inherited (shared) between parent and child. To this end, file descriptors need very careful handling around
fork()
. If you useexit()
in the child process whenexecvp
fails to execute (and therefore the child process hasn’t been replaced by a new process),exit
will perform clean-up operations before terminating the process. These operations include flushing buffered data to output streams, closing all open file descriptors, and executing functions registered withatexit
.
You should use _exit
(or its synonym _Exit
) to abort the child program when the execv
fails, because in this situation, the child process may interfere with the parent process’ external data (files) by calling its atexit handlers, calling its signal handlers, and/or flushing buffers.
For the same reason, you should also use _exit
in any child process that does not do an exec, but those are rare.
When to use
exit()
?Typically,
exit()
is used in the parent process because the clean-up operations are necessary to ensure a clean and orderly shutdown of the program.