It can be done by typing the make command on terminal which automatically builds the executable
It can also be done by directly doing g++ bash.cpp and then ./a.out
project-root/
├── bash.cpp # Main source file
├── fresh.cpp # Additional source file
├── Makefile # Build automation file
├── README.md # Project documentation
Header files
├── auto_dir.h # Auto directory handling
├── autocomplete.h # Autocomplete feature
├── handle_echo.h # Handle echo commands
├── handle_other.h # Handle other commands
├── handle_pipe.h # Handle piping logic
├── ioredirect.h # Input/output redirection
├── ls.h # Custom ls implementation
├── parse.h # Command parsing
├── pinfo.h # Process info handling
├── search.h # Search functionality
Data & Output files
├── history.txt # Stores shell history
└── ouput.txt # Stores command outputs
DISPLAY REQUIREMENT
This function generates a simple shell prompt string and prints it to the console. The prompt's format is username@hostname:sysname relative_path$.
-
Retrieve User and System Info: It uses system calls to get the
username(getenv),hostname(gethostname), andsysname(OS name) viauname. -
Calculate Relative Path: The function gets the current working directory (
getcwd) and compares it to the user's home directory (getenv("HOME")). If the current directory is within the home directory, it replaces the home path with a~to make the path relative and cleaner. -
Construct and Print Prompt: All the gathered information is concatenated into a single string. The final prompt, which ends with a
$symbol, is then printed to standard output.std::flushis used to ensure the prompt is immediately visible to the user.
getenv(): Gets environment variable values.gethostname(): Gets the machine's name.uname(): Gets detailed system information.getcwd(): Gets the current working directory.
Logic for `cd` (Change Directory)
The cd command changes the current working directory. The implementation handles two main cases:
- Changing to a specific directory: It takes the second argument (
args[1]) and attempts to change the directory using thechdir()system call. - Changing to the home directory: If no argument is provided (
args[1] == nullptr) or the argument is a tilde (~), it changes the directory to the user's home directory, which is stored inhome_address.
After a successful change, it clears a dir_words list and flushes the output buffer. If chdir() fails, it prints an error message.
The echo command prints its arguments to the standard output. The logic is straightforward:
- It iterates through the arguments starting from the second one (
args[1]). - For each argument, it adds a space and writes the resulting string directly to
STDOUT_FILENOusingwrite(). - After printing all arguments, it adds a newline character to the output.
The pwd command prints the absolute path of the current working directory.
- It declares a character array (
cwd) to store the path. - It uses the
getcwd()system call to retrieve the current working directory path. - Finally, it prints the stored path to standard output, followed by a newline.
ls function is handled in ls.h
This code provides a basic implementation of the ls command, handling two common flags: -l (long format) and -a (show hidden files).
This is the primary function for the ls command. Its logic can be broken down into three main steps:
-
Argument Parsing: It iterates through the command-line arguments (
args) to detect flags (-l,-a) and the target directory. It sets boolean flags to control the output format and handles the~symbol to correctly resolve the home directory. -
Directory Listing: It opens the target directory using
opendir(). It then loops through each entry in the directory usingreaddir(). -
Conditional Output: Inside the loop, it checks the
show_hiddenflag to skip files that start with a dot (.). Based on thelong_formatflag, it decides how to display the file:- If
-lis present, it callshandle_long_formatto print detailed file information. - If
-lis not present, it simply prints the file's name.
- If
This function is a helper for the ls -l command. It takes a file's path and name and prints its detailed metadata.
-
File Metadata: It uses the
stat()system call to retrieve all metadata for the specified file, such as permissions, ownership, size, and last modified time. -
Formatting and Output: It formats this metadata into a human-readable string:
- Permissions: It checks the file's mode (
st.st_mode) to determine the file type (directory or file) and individual read, write, and execute permissions for the owner, group, and others. - Ownership: It uses
getpwuid()andgetgrgid()to convert the numeric user and group IDs into human-readable names. - Time: It formats the last modified time (
st.st_mtime) into a standard month, day, and time string. - Finally, it prints all this information followed by the file's name.
- Permissions: It checks the file's mode (
- Detect Background Command: The function iterates through command-line arguments (
args) to check for the ampersand symbol (&). - Signal and Fork: If
&is found, it sets aprocessflag and then uses thefork()system call to create a new, separate process. - Child Process (
pid == 0): The new child process executes the requested command usingexecvp(), replacing its own process with the new program. This allows the command to run independently. Ifexecvp()fails (e.g., the command doesn't exist), it prints an error and exits. - Parent Process (
else): The original parent process (the shell) prints the Process ID (pid) of the background job and continues without waiting for the child process to finish. - Signal Handling: The
signal(SIGCHLD, sigchld_handler)call is set up to asynchronously handle the termination of child processes, preventing them from becoming "zombie" processes.
This is handled in pinfo.h
This function implements a simplified version of the pinfo command, which displays information about a process.
-
Identify Process: It first determines the target process ID (PID). If a PID is provided as an argument, it's used; otherwise, the function gets its own PID using
getpid(). -
Read Process Status: It reads data from the
/proc/[pid]/statfile, which is a key part of the Linux/procfilesystem that provides real-time system information. It extracts the process state (e.g.,Rfor running,Sfor sleeping) and checks if the process is in the foreground. -
assigning values to variables: It assigns values to pid_read, comm, state, etc. It reads more variables than are immediately used to properly skip past irrelevant data fields
-
Display Memory and Executable Path: It reads the
/proc/[pid]/statmfile to display the process's virtual memory size. It then usesreadlink()on/proc/[pid]/exeto find the full path of the executable file that the process is running, as this is a symbolic link rather than a regular file. -
Error Handling: The function includes basic error handling for cases where the
/procfiles cannot be accessed.
This is handled in search.h
This C++ function recursively searches a directory tree for a file or directory with a specific name.
-
Directory Access: The function first attempts to open the specified
pathusingopendir(). If the directory cannot be opened (e.g., due to permissions or if it doesn't exist), it prints an error and returnsfalse. -
Iteration and Recursion: It enters a
whileloop, reading each entry in the directory one by one usingreaddir().- Skip Special Entries: It immediately skips the
.and..entries to prevent infinite recursion. - Direct Match: It checks if the current entry's name matches the target string
s. If so, it closes the directory and returnstrue, ending the search. - Recursive Call: If the entry is a directory (checked using
stat()andS_ISDIR()), it makes arecursivecall tosearch(), passing the new subdirectory's path. If this recursive call returnstrue, it means the file was found, so the current function also returnstrue.
- Skip Special Entries: It immediately skips the
-
Final Cleanup: If the loop completes without finding a match, the function calls
closedir()to close the directory and returnsfalse, indicating the file was not found in the entire directory tree.
This is handled in ioredirect.h
This function implements input, output, and append redirection for a shell command. It works by manipulating file descriptors before executing the command and then restoring them afterward.
-
Save Original File Descriptors: The function begins by saving the original standard input (
STDIN_FILENO) and standard output (STDOUT_FILENO) file descriptors. This is a crucial step to ensure the shell's I/O behavior can be restored after the command's execution. -
Parse Arguments: It iterates through the command's arguments to identify the redirection operators:
>>(append),>(output), and<(input). It stores the location and type of each redirection and modifies the argument list by replacing the operators and their associated filenames withnullptr. -
Redirect I/O:
- Input (
<): If a<is found, it opens the specified file in read-only mode. It then uses thedup2()system call to duplicate the file's descriptor ontoSTDIN_FILENO, causing the command to read its input from the file instead of the keyboard. - Append (
>>): If a>>is found, it opens the specified file for writing, creating it if it doesn't exist and positioning the write pointer at the end of the file. It then usesdup2()to redirectSTDOUT_FILENOto this file, so all command output is appended to the file. - Output (
>): If a>is found, it opens the specified file for writing, creating it if it doesn't exist and truncating it (deleting all existing content). It then usesdup2()to redirectSTDOUT_FILENOto this file, so all command output overwrites the file's content.
- Input (
-
Execute Command and Restore: After setting up the redirections, the function again calls a
parse()function to execute the main command but this time with lesser tokens, since those of redirections has now been handled. Once the command completes, it uses the saved original file descriptors to restoreSTDIN_FILENOandSTDOUT_FILENOto their default behavior, allowing the shell to continue interacting with the user normally.
This is handled in handle_pipe
This function implements command piping, where the output of one command is used as the input for the next. The logic involves setting up communication channels between multiple processes.
-
Tokenization: The function first tokenizes the input arguments, splitting them into a
vectorof commands based on the|(pipe) symbol. Each innervectorholds a single command and its arguments. -
Pipe Creation: It then creates a series of pipes—specifically,
n-1pipes forncommands. Each pipe is a one-way communication channel with a read end and a write end. -
Process Forking: For each command, the function
fork()s a new child process. The logic inside the child process is crucial for the piping to work:- I/O Redirection:
- If it is not the first command, the child's standard input (
STDIN_FILENO) is redirected to the read end of the previous pipe. - If it is not the last command, the child's standard output (
STDOUT_FILENO) is redirected to the write end of the current pipe.
- If it is not the first command, the child's standard input (
- Pipe Cleanup: All unnecessary pipe file descriptors are immediately closed in each child process to prevent deadlocks and resource leaks.
- I/O Redirection:
-
Execution and Waiting: After creating pipes and redirecting the read and write to pipe end it then again calls parse function with pipes now being handled for further execution while making the pipe symbol
|to nullptr . Meanwhile, the parent process waits for all child processes to complete usingwaitpid(), ensuring the entire pipeline finishes before returning control to the user.
- It is implemented as a result of how I am handling the pipes. Input and output redirections are only handled once the pipe is created using
parsefunction which then again looks for next most prior command which is handling redirections if it's present
-
handle_sigint(Ctrl+C): This handler is triggered when aSIGINTsignal is received (typically by pressingCtrl+C). If a foreground process is running (foreground_pid > 0), the handler sends theSIGINTsignal to that specific process. This effectively interrupts and terminates the foreground process, which is the standard behavior for a shell. -
handle_sigtstp(Ctrl+Z): This handler is triggered by aSIGTSTPsignal (fromCtrl+Z). If a foreground process is active, the handler performs two actions:- It adds the process's PID to a list of background processes (
b_process). - It sends the
SIGTSTPsignal to the process, pausing its execution. This moves the process to a stopped state in the background, allowing the user to resume or terminate it later.
- It adds the process's PID to a list of background processes (
-
(Ctrl+D) : This is implemented by checking for ascii value of 4 then we simple exit from the terminal
foreground_pid: A global variable that stores the Process ID of the currently running foreground job.kill(pid, signal): A system call used to send a specificsignalto the process with the givenpid.signal(sig, handler): A system call that registers a customhandlerfunction for a givensignal. This tells the operating system what to do when that signal is received.
handled using auto_dir.h and autocomplete.h
The approach is to first build a list of all possible completions (either commands from a system directory or files from the current directory) and then use helper functions to search this list for a matching prefix.
match_prefixandmatch_dir: These are identical helper functions that check if a given string (target) is a prefix of another string (a). They do this by comparing the characters of both strings up to the length of thetarget.string_searchandsearch_dir: These functions iterate through a pre-populated list of words (e.g., commands or directory names). For each word, they call the respectivematch_prefixormatch_dirfunction. The first word that matches the target prefix is returned. If no match is found, they return an empty string.
This function populates a global list (autocomplete_words) with the names of executable commands.
- It opens the
/usr/bindirectory, a standard location for system executables. - It iterates through every entry in the directory.
- It adds the name of each entry (that is not a hidden file) to the
autocomplete_wordsvector. - Finally, it sorts the entire vector alphabetically, which is a good practice for efficient searching and consistent output.
This function populates a global list (dir_words) with the names of files and directories in the current working directory.
- It gets the current working directory using
getcwd(). - It then opens this directory.
- It reads each entry and adds its name to the
dir_wordsvector, again skipping hidden files.
This code snippet describes the logic for handling a tab key press (\t) to provide command and file path autocompletion in a shell.
When the user presses the tab key, the function performs the following steps:
-
Get Current Directory Listings: It calls the
auto_dir()function to populate a list of files and directories in the current working directory. This ensures the completion list is up-to-date. -
Isolate Last Word: It identifies the last word typed by the user by finding the position of the last space in the input string (
s). This word is the prefix to be autocompleted. -
Search for Matches:
- It first attempts to find a matching file or directory from the
dir_wordslist usingsearch_dir. This prioritizes file path completion. - If no directory match is found, it then searches for a matching command in the
autocomplete_wordslist usingstring_search.
- It first attempts to find a matching file or directory from the
-
Update Input String: If a match is found in either search, the original last word is replaced with the fully completed word. The original part of the string (the "prefix") is kept intact.
-
Refresh Terminal Display:
cout << "\033[2K\r";: This is an ANSI escape code that clears the current line and moves the cursor to the beginning.print_basics();: The shell prompt is reprinted.- The updated string
s(with the completed word) is then printed to the console. - The
cursor_posis updated to the end of the new string, and the output is flushed to ensure immediate display.
This code provides the logic for managing a command history file. It includes two functions: one for loading the history from the file and one for saving new commands to it.
- Open File: The function attempts to open the
HISTORY_FILEin read mode ("r"). If the file does not exist or cannot be opened, it returns an empty vector. - Read Line by Line: It uses
fgetsin a loop to read the file content one line at a time into a buffer.fgetsis used to prevent buffer overflows. - Process and Store: For each line read, it removes the trailing newline character (
\n) and adds the processed command string to astd::vector<string>. - Close File: After the loop, it closes the file.
- Open File: This function opens the
HISTORY_FILEin append mode ("a"). This is crucial because it ensures that new commands are added to the end of the file without deleting the existing content. If the file doesn't exist, it will be created. - Write Command: It uses
fprintfto write the provided command string to the file, followed by a newline character. This ensures each command is saved on its own line. - Close File: The file is then closed, and the new command is now part of the persistent history.
- Now history is called in main which loads the histroy after we run the terminal.
what I am doing is i am reading the input char by char and special symbol like tab, backspace ,ctrl+d is initially handled and then i am appending the char to a string s. After each new line the command has to execute so to execut the command , to execute the commands i am calling parse function which takes the input string created as a result of appending chars and then sort out to different if else statement depending on the requirement and parse function is itself being called recursively by various function to further execute the commands after each time I handle a part of a command.
Dry run - lets run ls | cat < output.txt first since there is no " ; " so whole command will be stored in string s then first I handle pipes in that i make pipe which is discussed earlier and then make a 2D vector of char* which handles token of each part of pipe and then calls parse again, now the parse will look for "ls" which will be handled by ls function and then we will handle the function "cat < output.txt" which parse will send to ioredirect function which will handle the ioredirect and then again run for cat command which will be handled by execvp then the output to output.txt will happen succesfully.