Skip to content

Zenith053/Posix_bash

Repository files navigation

Running the bash

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

Directory structure

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

print_basics() - Logic and Implementation

This function generates a simple shell prompt string and prints it to the console. The prompt's format is username@hostname:sysname relative_path$.

Logic:

  1. Retrieve User and System Info: It uses system calls to get the username (getenv), hostname (gethostname), and sysname (OS name) via uname.

  2. 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.

  3. 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::flush is used to ensure the prompt is immediately visible to the user.

Key Functions Used:

  • getenv(): Gets environment variable values.
  • gethostname(): Gets the machine's name.
  • uname(): Gets detailed system information.
  • getcwd(): Gets the current working directory.

cd, echo , pwd

Logic for `cd` (Change Directory)

The cd command changes the current working directory. The implementation handles two main cases:

  1. Changing to a specific directory: It takes the second argument (args[1]) and attempts to change the directory using the chdir() system call.
  2. 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 in home_address.

After a successful change, it clears a dir_words list and flushes the output buffer. If chdir() fails, it prints an error message.

Logic for echo

The echo command prints its arguments to the standard output. The logic is straightforward:

  1. It iterates through the arguments starting from the second one (args[1]).
  2. For each argument, it adds a space and writes the resulting string directly to STDOUT_FILENO using write().
  3. After printing all arguments, it adds a newline character to the output.

Logic for pwd (Print Working Directory)

The pwd command prints the absolute path of the current working directory.

  1. It declares a character array (cwd) to store the path.
  2. It uses the getcwd() system call to retrieve the current working directory path.
  3. Finally, it prints the stored path to standard output, followed by a newline.

LS

ls function is handled in ls.h

ls Command Implementation - Logic and Design

This code provides a basic implementation of the ls command, handling two common flags: -l (long format) and -a (show hidden files).

Logic for handle_ls (Main Function)

This is the primary function for the ls command. Its logic can be broken down into three main steps:

  1. 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.

  2. Directory Listing: It opens the target directory using opendir(). It then loops through each entry in the directory using readdir().

  3. Conditional Output: Inside the loop, it checks the show_hidden flag to skip files that start with a dot (.). Based on the long_format flag, it decides how to display the file:

    • If -l is present, it calls handle_long_format to print detailed file information.
    • If -l is not present, it simply prints the file's name.

Logic for handle_long_format (Utility Function)

This function is a helper for the ls -l command. It takes a file's path and name and prints its detailed metadata.

  1. 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.

  2. 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() and getgrgid() 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.

System commands

parse Function - Logic for Background Processes

Logic:

  1. Detect Background Command: The function iterates through command-line arguments (args) to check for the ampersand symbol (&).
  2. Signal and Fork: If & is found, it sets a process flag and then uses the fork() system call to create a new, separate process.
  3. Child Process (pid == 0): The new child process executes the requested command using execvp(), replacing its own process with the new program. This allows the command to run independently. If execvp() fails (e.g., the command doesn't exist), it prints an error and exits.
  4. 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.
  5. 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.

pinfo

This is handled in pinfo.h This function implements a simplified version of the pinfo command, which displays information about a process.

Logic:

  1. 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().

  2. Read Process Status: It reads data from the /proc/[pid]/stat file, which is a key part of the Linux /proc filesystem that provides real-time system information. It extracts the process state (e.g., R for running, S for sleeping) and checks if the process is in the foreground.

  3. 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

  4. Display Memory and Executable Path: It reads the /proc/[pid]/statm file to display the process's virtual memory size. It then uses readlink() on /proc/[pid]/exe to find the full path of the executable file that the process is running, as this is a symbolic link rather than a regular file.

  5. Error Handling: The function includes basic error handling for cases where the /proc files cannot be accessed.


search

This is handled in search.h This C++ function recursively searches a directory tree for a file or directory with a specific name.

Logic:

  1. Directory Access: The function first attempts to open the specified path using opendir(). If the directory cannot be opened (e.g., due to permissions or if it doesn't exist), it prints an error and returns false.

  2. Iteration and Recursion: It enters a while loop, reading each entry in the directory one by one using readdir().

    • 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 returns true, ending the search.
    • Recursive Call: If the entry is a directory (checked using stat() and S_ISDIR()), it makes a recursive call to search(), passing the new subdirectory's path. If this recursive call returns true, it means the file was found, so the current function also returns true.
  3. Final Cleanup: If the loop completes without finding a match, the function calls closedir() to close the directory and returns false, indicating the file was not found in the entire directory tree.


handle_redirect - I/O Redirection Logic

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.

Logic:

  1. 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.

  2. 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 with nullptr.

  3. Redirect I/O:

    • Input (<): If a < is found, it opens the specified file in read-only mode. It then uses the dup2() system call to duplicate the file's descriptor onto STDIN_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 uses dup2() to redirect STDOUT_FILENO to 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 uses dup2() to redirect STDOUT_FILENO to this file, so all command output overwrites the file's content.
  4. 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 restore STDIN_FILENO and STDOUT_FILENO to their default behavior, allowing the shell to continue interacting with the user normally.


handle_pipe

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.

Logic:

  1. Tokenization: The function first tokenizes the input arguments, splitting them into a vector of commands based on the | (pipe) symbol. Each inner vector holds a single command and its arguments.

  2. Pipe Creation: It then creates a series of pipes—specifically, n-1 pipes for n commands. Each pipe is a one-way communication channel with a read end and a write end.

  3. 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.
    • Pipe Cleanup: All unnecessary pipe file descriptors are immediately closed in each child process to prevent deadlocks and resource leaks.
  4. 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 using waitpid(), ensuring the entire pipeline finishes before returning control to the user.


Redirection with pipeline

  1. 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 parse function which then again looks for next most prior command which is handling redirections if it's present

Simple signals

Logic:

  • handle_sigint (Ctrl+C): This handler is triggered when a SIGINT signal is received (typically by pressing Ctrl+C). If a foreground process is running (foreground_pid > 0), the handler sends the SIGINT signal 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 a SIGTSTP signal (from Ctrl+Z). If a foreground process is active, the handler performs two actions:

    1. It adds the process's PID to a list of background processes (b_process).
    2. It sends the SIGTSTP signal 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.
  • (Ctrl+D) : This is implemented by checking for ascii value of 4 then we simple exit from the terminal

Key Elements:

  • 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 specific signal to the process with the given pid.
  • signal(sig, handler): A system call that registers a custom handler function for a given signal. This tells the operating system what to do when that signal is received.

Autocompletion Logic

handled using auto_dir.h and autocomplete.h

Overall Logic:

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.

Prefix Matching and Search Functions

  • match_prefix and match_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 the target.
  • string_search and search_dir: These functions iterate through a pre-populated list of words (e.g., commands or directory names). For each word, they call the respective match_prefix or match_dir function. The first word that matches the target prefix is returned. If no match is found, they return an empty string.

Command Autocompletion (auto_complete_list)

This function populates a global list (autocomplete_words) with the names of executable commands.

  1. It opens the /usr/bin directory, a standard location for system executables.
  2. It iterates through every entry in the directory.
  3. It adds the name of each entry (that is not a hidden file) to the autocomplete_words vector.
  4. Finally, it sorts the entire vector alphabetically, which is a good practice for efficient searching and consistent output.

Directory/File Autocompletion (auto_dir)

This function populates a global list (dir_words) with the names of files and directories in the current working directory.

  1. It gets the current working directory using getcwd().
  2. It then opens this directory.
  3. It reads each entry and adds its name to the dir_words vector, again skipping hidden files.

Tab Press - Logic and Implementation

This code snippet describes the logic for handling a tab key press (\t) to provide command and file path autocompletion in a shell.

Logic:

When the user presses the tab key, the function performs the following steps:

  1. 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.

  2. 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.

  3. Search for Matches:

    • It first attempts to find a matching file or directory from the dir_words list using search_dir. This prioritizes file path completion.
    • If no directory match is found, it then searches for a matching command in the autocomplete_words list using string_search.
  4. 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.

  5. 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_pos is updated to the end of the new string, and the output is flushed to ensure immediate display.

Command History Logic

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.

Logic for load_history()

  1. Open File: The function attempts to open the HISTORY_FILE in read mode ("r"). If the file does not exist or cannot be opened, it returns an empty vector.
  2. Read Line by Line: It uses fgets in a loop to read the file content one line at a time into a buffer. fgets is used to prevent buffer overflows.
  3. Process and Store: For each line read, it removes the trailing newline character (\n) and adds the processed command string to a std::vector<string>.
  4. Close File: After the loop, it closes the file.

Logic for save_history()

  1. Open File: This function opens the HISTORY_FILE in 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.
  2. Write Command: It uses fprintf to write the provided command string to the file, followed by a newline character. This ensures each command is saved on its own line.
  3. Close File: The file is then closed, and the new command is now part of the persistent history.
  4. Now history is called in main which loads the histroy after we run the terminal.

In Short

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.

About

This repo contains a working posix shell created as a part of AOS course assignments

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors