├── tests ├── unit_tests │ ├── prop_test_files │ │ ├── with whitespace │ │ └── with multiple white │ ├── test_fuzzy_finder.c │ ├── test_tab_complete.c │ ├── test_readline.c │ ├── test_util.c │ └── test_main.c └── integration_tests │ └── autocomplete_tests.txt ├── pictures └── logo_transparent.png ├── .gitignore ├── src ├── fuzzy_finder.h ├── tab_complete.h ├── readline.h ├── colors.h ├── main.h ├── types.h ├── util.h ├── fuzzy_finder.c ├── tab_complete.c ├── readline.c ├── util.c └── main.c ├── Dockerfile ├── LICENSE.txt ├── Makefile └── README.md /tests/unit_tests/prop_test_files/with whitespace: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit_tests/prop_test_files/with multiple white: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pictures/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippRados/PShell/HEAD/pictures/logo_transparent.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/bin/ 2 | tests/unit_tests/bin 3 | log.txt 4 | user_test.txt 5 | debug.txt 6 | .psh_history 7 | .clang-format 8 | compile_flags.txt 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /src/fuzzy_finder.h: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | integer_tuple findDisplayIndices(int matching_commands_len, int cursor_diff, int index); 4 | int shiftPromptIfOverlapTest(int current_cursor_height, int fuzzy_popup_height); 5 | char* removeWhitespace(char* s1); 6 | bool updateFuzzyfinder(char** line, char c, string_array matching_commands, int* index, int* i, int max_input_len); 7 | -------------------------------------------------------------------------------- /tests/integration_tests/autocomplete_tests.txt: -------------------------------------------------------------------------------- 1 | some autocomplete 2 | uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu 3 | jjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk 4 | -------------------------------------------------------------------------------- /src/tab_complete.h: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | string_array getAllMatchingFiles(char* current_dir_sub, char* removed_sub); 4 | string_array filterMatching(char* line, const string_array PATH_BINS); 5 | tab_completion updateCompletion(autocomplete_array possible_tabcomplete, char* c, line_data* line_info, 6 | int* tab_index, token_index current_token); 7 | void removeDotFilesIfnecessary(char* current_word, autocomplete_array* possible_tabcomplete); 8 | void escapeWhitespace(string_array* arr); 9 | -------------------------------------------------------------------------------- /src/readline.h: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | line_data* lineDataConstructor(int directory_len); 4 | autocomplete_data* autocompleteDataConstructor(); 5 | history_data* historyDataConstructor(string_array* command_history, string_array global_command_history); 6 | string_array concatenateArrays(const string_array one, const string_array two); 7 | void stringToLower(char* string); 8 | bool update(line_data* line_info, autocomplete_data* autocomplete_info, history_data* history_info, 9 | coordinates terminal_size, string_array PATH_BINS, coordinates* cursor_pos); 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN apt update && \ 4 | apt install gcc make ruby tmux -y && \ 5 | gem install ttytest 6 | 7 | # have to change lib because of version 8 | ENV RUBYOPT="-KU -E utf-8:utf-8" 9 | WORKDIR "/var/lib/gems/2.5.0/gems/ttytest-0.5.0/lib/ttytest" 10 | RUN sed 's/def assert_cursor_position(x:, y:)/def assert_cursor_position(x, y)/' \ 11 | matchers.rb > \ 12 | matchers.rb.changed && \ 13 | mv matchers.rb.changed matchers.rb 14 | RUN touch /home/very_long_filename_to_test_shell_behavior 15 | RUN touch /home/short_file 16 | RUN touch /bin/very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_long_filename_to_test_shell_behavior 17 | RUN touch ~/.pshrc 18 | 19 | WORKDIR "/pshell" 20 | ENTRYPOINT [ "/bin/bash", "-l", "-c" ] 21 | -------------------------------------------------------------------------------- /src/colors.h: -------------------------------------------------------------------------------- 1 | typedef struct { 2 | int attr; 3 | int fg; 4 | int bg; 5 | } color; 6 | 7 | static const color WHITE = { 8 | .attr = 0, 9 | .fg = 37, 10 | .bg = 10, 11 | }; 12 | 13 | static const color RED = { 14 | .attr = 0, 15 | .fg = 31, 16 | .bg = 10, 17 | }; 18 | 19 | static const color GREEN = { 20 | .attr = 0, 21 | .fg = 32, 22 | .bg = 10, 23 | }; 24 | 25 | static const color CYAN = { 26 | .attr = 1, 27 | .fg = 36, 28 | .bg = 10, 29 | }; 30 | 31 | static const color HIGHLIGHT = { 32 | .attr = 1, 33 | .fg = 37, 34 | .bg = 42, 35 | }; 36 | 37 | static const color YELLOW = { 38 | .attr = 0, 39 | .fg = 33, 40 | .bg = 10, 41 | }; 42 | 43 | static const color BLUE = { 44 | .attr = 0, 45 | .fg = 34, 46 | .bg = 10, 47 | }; 48 | -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | char* getLastTwoDirs(char*); 4 | string_array removeDots(string_array* array); 5 | char* joinFilePath(char* home_dir, char* destination_file); 6 | wildcard_groups_arr expandWildcardgroups(wildcard_groups_arr wildcards); 7 | void removeWhitespaceTokens(token_index_arr* tokenized_line); 8 | bool isValidSyntax(token_index_arr tokenized_line); 9 | string_array_token splitLineIntoSimpleCommands(char* line, token_index_arr tokenized_line); 10 | string_array splitByTokens(char* line, token_index_arr token); 11 | file_redirection_data parseForRedirectionFiles(string_array simple_command, token_index_arr token); 12 | void stripRedirections(string_array* splitted_line, token_index_arr token); 13 | wildcard_groups_arr groupWildcards(char* line, token_index_arr token); 14 | void replaceLineWithWildcards(char** line, wildcard_groups_arr wildcard_matches); 15 | string_array splitString(const char*, char); 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Philipp Rados 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # -*- Makefile -*- 2 | 3 | CC = gcc 4 | DBG_CC = gcc-13 5 | CFLAGS = -g -fsanitize=address -fsanitize=leak -Wall -Wextra 6 | SOURCES = src/main.c src/util.c src/tab_complete.c src/fuzzy_finder.c src/readline.c 7 | TEST_SOURCES = tests/unit_tests/test_main.c tests/unit_tests/test_util.c tests/unit_tests/test_fuzzy_finder.c tests/unit_tests/test_tab_complete.c tests/unit_tests/test_readline.c 8 | test_target = $(basename $(notdir $(TEST_SOURCES))) 9 | CURRENT_DIR = $(shell pwd) 10 | INSTALLDIR ?= /usr/local/bin 11 | # IN_SHELL_LIST = $(shell cat /etc/shells | grep psh) 12 | 13 | .SILENT: 14 | prod: ${SOURCES} ## Compile the production shell 15 | ${CC} ${SOURCES} -o ${INSTALLDIR}/psh -ldl -lm 16 | 17 | .SILENT: 18 | debug: ${SOURCES} ## Compile the debug shell for development 19 | if [ ! -d "./src/bin" ]; then mkdir ./src/bin ; fi 20 | ${DBG_CC} ${CFLAGS} ${SOURCES} -o src/bin/psh -ldl -lm 21 | 22 | .SILENT: 23 | compile_and_run_debug: 24 | make debug 25 | ./src/bin/psh 26 | 27 | help: ## Display this help 28 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 29 | 30 | docker_shell: ${SOURCES} 31 | if [ ! -d "./src/bin" ]; then mkdir ./src/bin ; fi 32 | ${CC} ${SOURCES} -o src/bin/psh -ldl -lm 33 | 34 | run_tests: $(TEST_SOURCES) $(SOURCES) ## Run all tests (needs criterion) 35 | if [ ! -d "./tests/unit_tests/bin" ]; then mkdir ./tests/unit_tests/bin ; fi 36 | ${DBG_CC} ${CFLAGS} -o ./tests/unit_tests/bin/compiled_tests -D TEST ${TEST_SOURCES} ${SOURCES} \ 37 | -I /usr/local/Cellar/criterion/2.3.3/include/ -L/usr/local/Cellar/criterion/2.3.3/lib -lcriterion.3.1.0 38 | ./tests/unit_tests/bin/compiled_tests -l 39 | ./tests/unit_tests/bin/compiled_tests 40 | 41 | $(test_target): $(TEST_SOURCES) $(SOURCES) ## Run individual tests (needs criterion) 42 | if [ ! -d "./tests/unit_tests/bin" ]; then mkdir ./tests/unit_tests/bin ; fi 43 | if [ $@ = "test_util" -o $@ = "test_main" -o $@ = "test_readline" ]; then\ 44 | ${DBG_CC} ${CFLAGS} -o ./tests/unit_tests/bin/compiled_$(subst test_,'',$@)_tests -D TEST tests/unit_tests/$@.c $(SOURCES) -L/usr/local/Cellar/criterion/2.3.3/lib/ -I/usr/local/Cellar/criterion/2.3.3/include/ -lcriterion.3.1.0;\ 45 | else\ 46 | ${DBG_CC} ${CFLAGS} -o ./tests/unit_tests/bin/compiled_$(subst test_,'',$@)_tests -D TEST tests/unit_tests/$@.c src/util.c src/$(subst test_,'',$@).c -L/usr/local/Cellar/criterion/2.3.3/lib/ -I/usr/local/Cellar/criterion/2.3.3/include/ -lcriterion.3.1.0;\ 47 | fi 48 | ./tests/unit_tests/bin/compiled_$(subst test_,'',$@)_tests -l 49 | ./tests/unit_tests/bin/compiled_$(subst test_,'',$@)_tests 50 | 51 | integration_tests: ## have to run 'docker -t testing_container .' 52 | make clean 53 | docker run -ti --rm -v $(CURRENT_DIR):/pshell testing_container \ 54 | "make docker_shell && cat ./tests/integration_tests/autocomplete_tests.txt >> /root/.psh_history && ruby ./tests/integration_tests/test_pshell.rb" 55 | make clean 56 | 57 | clean: ## Cleans up all binary and object files 58 | rm -f -R *.o ./tests/unit_tests/bin/* ./src/bin/* 59 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | enum cursor_direction { cursor_up, cursor_down, cursor_left, cursor_right }; 4 | enum token { 5 | CMD = 1, 6 | PIPE_CMD, 7 | PIPE, 8 | AMPAMP, 9 | AMP_CMD, 10 | GREATGREAT, 11 | GREAT, 12 | LESS, 13 | AMP_GREAT, 14 | AMP_GREATGREAT, 15 | WHITESPACE, 16 | ASTERISK, 17 | QUESTION, 18 | ARG, 19 | ENUM_LEN 20 | }; 21 | 22 | typedef struct wildcard_groups { 23 | char* wildcard_arg; 24 | int start; 25 | int end; 26 | } wildcard_groups; 27 | 28 | typedef struct { 29 | int len; 30 | wildcard_groups* arr; 31 | } wildcard_groups_arr; 32 | 33 | typedef struct token_index { 34 | int start; 35 | int end; 36 | enum token token; 37 | } token_index; 38 | 39 | typedef struct token_index_arr { 40 | token_index* arr; 41 | int len; 42 | } token_index_arr; 43 | 44 | typedef struct regex_loop_struct { 45 | char fill_char; 46 | int loop_start; 47 | int token_index_inc; 48 | } regex_loop_struct; 49 | 50 | typedef struct { 51 | char* output_filename; 52 | char* input_filename; 53 | char* stderr_filename; 54 | char* merge_filename; 55 | int output_append; 56 | int stderr_append; 57 | int merge_append; 58 | } file_redirection_data; 59 | 60 | enum logger_type { 61 | integer, 62 | string, 63 | character, 64 | }; 65 | 66 | enum autocomplete_type { 67 | command, 68 | file_or_dir, 69 | }; 70 | 71 | enum color_decorations { standard = 0, bold = 1, underline = 4, reversed = 7 }; 72 | 73 | typedef struct coordinates { 74 | int x; 75 | int y; 76 | } coordinates; 77 | 78 | typedef struct { 79 | int len; 80 | char** values; 81 | } string_array; 82 | 83 | typedef struct { 84 | int len; 85 | char** values; 86 | enum token* token_arr; 87 | } string_array_token; 88 | 89 | typedef struct { 90 | string_array array; 91 | int appending_index; 92 | } autocomplete_array; 93 | 94 | typedef struct { 95 | int one; 96 | int second; 97 | } integer_tuple; 98 | 99 | typedef struct { 100 | char* removed_sub; 101 | char* current_dir; 102 | } file_string_tuple; 103 | 104 | typedef struct { 105 | int format_width; 106 | int col_size; 107 | int row_size; 108 | int cursor_height_diff; 109 | int cursor_row; 110 | int line_row_count_with_autocomplete; 111 | coordinates* cursor_pos; 112 | coordinates terminal_size; 113 | } render_objects; 114 | 115 | typedef struct { 116 | char* line; 117 | char c; 118 | int* i; 119 | int prompt_len; 120 | int cursor_row; 121 | int line_row_count_with_autocomplete; 122 | size_t size; 123 | } line_data; 124 | 125 | typedef struct { 126 | string_array sessions_command_history; 127 | string_array global_command_history; 128 | int history_index; 129 | } history_data; 130 | 131 | typedef struct { 132 | char* possible_autocomplete; 133 | int autocomplete; 134 | size_t size; 135 | } autocomplete_data; 136 | 137 | typedef struct { 138 | char* line; 139 | int shifted; 140 | } fuzzy_result; 141 | 142 | typedef struct { 143 | const char* name; 144 | void (*func)(); 145 | } function_by_name; 146 | 147 | typedef struct { 148 | int len; 149 | function_by_name* array; 150 | } builtins_array; 151 | 152 | typedef struct { 153 | int successful; 154 | int continue_loop; 155 | } tab_completion; 156 | 157 | typedef struct { 158 | char** var_names; 159 | char** values; 160 | int len; 161 | } env_var_arr; 162 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | #include "colors.h" 5 | #include "types.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define BACKSPACE 127 27 | #define CONTROL_F 6 28 | #define TAB 9 29 | #define ESCAPE '\033' 30 | #define CLEAR_LINE printf("%c[2K", 27); 31 | #define CLEAR_BELOW_CURSOR printf("%c[0J", 27); 32 | #define HIDE_CURSOR printf("\e[?25l"); 33 | #define ENABLE_CURSOR printf("\e[?25h"); 34 | #define CLEAR_SCREEN printf(" \e[1;1H\e[2J"); 35 | 36 | enum { HISTORY_SIZE = 512 }; 37 | #define BUFFER 256 38 | 39 | // ======= util.c functions ======== 40 | 41 | int getch(); 42 | void printColor(const char* string, color color, enum color_decorations color_decorations); 43 | void moveCursor(coordinates new_pos); 44 | fuzzy_result popupFuzzyFinder(const string_array all_time_command_history, const coordinates terminal_size, 45 | int current_cursor_height, int line_row_count_with_autocomplete); 46 | coordinates getTerminalSize(); 47 | string_array removeDuplicates(string_array* matching_commands); 48 | void backspaceLogic(char* line, int* i); 49 | char* removeCharAtPos(char* line, int x_pos); 50 | void logger(enum logger_type type, void* message); 51 | bool tabLoop(line_data* line_info, coordinates* cursor_pos, const string_array PATH_BINS, 52 | const coordinates terminal_size, token_index current_token); 53 | coordinates getCursorPos(); 54 | void free_string_array(string_array* arr); 55 | void insertStringAtPos(char** line, char* insert_string, int position); 56 | int isDirectory(const char* path); 57 | string_array copyStringArray(string_array arr); 58 | bool insertCharAtPos(char* line, int index, char c); 59 | bool inArray(char* value, string_array array); 60 | string_array getAllFilesInDir(string_array* directory_array); 61 | autocomplete_array fileComp(char* current_word); 62 | void fileDirArray(string_array* filtered, char* current_dir_sub); 63 | int getAppendingIndex(char* line, char delimeter); 64 | file_string_tuple getFileStrings(char* current_word, char* current_path); 65 | coordinates calculateCursorPos(coordinates terminal_size, coordinates cursor_pos, int prompt_len, int i); 66 | int calculateRowCount(coordinates terminal_size, int prompt_len, int i); 67 | char* shortenIfTooLong(char* word, int terminal_width); 68 | bool isOnlyDelimeter(const char* string, char delimeter); 69 | int firstNonDelimeterIndex(string_array splitted_line); 70 | int getLongestWordInArray(const string_array array); 71 | char* removeMultipleWhitespaces(char* string); 72 | bool isExec(char* file); 73 | token_index getCurrentToken(int line_index, token_index_arr tokenized_line); 74 | void removeEscapesString(char** string); 75 | void removeSlice(char** line, int start, int end); 76 | token_index_arr tokenizeLine(char* line); 77 | int isBuiltin(char* command, builtins_array builtins); 78 | void replaceAliasesString(char** line); 79 | char* readLine(string_array PATH_BINS, char* directories, string_array* command_history, 80 | const string_array global_command_history, builtins_array BUILTINS); 81 | void printPrompt(const char* dir, color color); 82 | 83 | #endif // !UTIL_H 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

5 |
6 | 7 | A C-shell with integrated fuzzy finder, tab completion, all standard shell features and more without using curses. 8 | --- 9 | ![](https://img.shields.io/badge/made_for-UNIX-lightgrey) 10 | ![](https://img.shields.io/github/languages/code-size/PhilippRados/PShell) 11 | ![](https://img.shields.io/github/license/PhilippRados/PShell) 12 | ![](https://img.shields.io/badge/Dependencies-Gcc,_Make-brightgreen) 13 | 14 | [![asciicast](https://asciinema.org/a/gkTmH7Km6m5C4t6Bbwxz589y3.svg)](https://asciinema.org/a/gkTmH7Km6m5C4t6Bbwxz589y3) 15 | 16 | ### Table of contents 17 | * [Features](#features) 18 | + [Syntax](#syntax) 19 | * [Setup](#setup) 20 | + [.Pshrc](#rc) 21 | * [Testing](#testing) 22 | + [Unit-tests](#unit) 23 | + [Integration-tests](#integration) 24 | * [Contribution](#contribution) 25 | * [Future Work](#future-work) 26 | 27 | 28 | ## Features 29 |
30 | Tab completion 31 | 32 |
33 |
34 | Fuzzy-finder 35 | 36 |
37 |
38 | Autocompletion 39 | 40 |
41 |
42 | Wildcard-matching 43 | 44 |
45 |
46 | Syntax highlighting 47 | 48 |
49 | 50 | ### Syntax 51 | Besides the basic `&&` to chain multiple commands, `|` to pipe one output to another, `* and ?` for wildcard matching here are some more `psh` syntax elements: 52 | ##### Input/Output Redirection 53 | ```bash 54 | > or 1> stdout redirection 55 | >> or 1>> output append 56 | 2> stderr redirection 57 | 2>> stderr append 58 | < input redirection 59 | &> merge stdout, stderr into single file 60 | &>> append merge stdout, stderr to single file 61 | 62 | ``` 63 | ##### Fuzzy-finder 64 | ```bash 65 | to open 66 | to close 67 | to choose proposal 68 | ``` 69 | ##### Quoted arguments 70 | ```bash 71 | echo 'this argument counts as single arg' # currently only single quotes supported 72 | echo you\ can\ also\ escape\ whitespace\ with\ backslash 73 | ``` 74 | 75 | ## Setup 76 | You only need **make** and **gcc** to build. Also your terminal should be capable of displaying unicode characters. 77 | ```bash 78 | $ git clone https://github.com/PhilippRados/PShell 79 | $ cd PShell 80 | $ make 81 | $ echo /usr/local/bin/psh | sudo tee -a /etc/shells 82 | ``` 83 | If you want to install it somewhere else due to permission error in default-path `make INSTALLDIR=`.
84 | When successfully executed the commands above you can change `psh` to your default shell like so: 85 | ```bash 86 | $ chsh -s /usr/local/bin/psh 87 | ``` 88 | You can use this same command to change your shell back to your original one just swap out the path. 89 | ### ~/.pshrc 90 | When first starting the shell you'll be prompted with an option to create a `~/.pshrc` file when you don't have one. This will set basic ENV-variables needed to run the shell. 91 | ```bash 92 | # ~/.pshrc 93 | PATH="some/interesting/path/$" # use $ to append to already extistant env-variable 94 | TERM="linux" # without $ any existant env-variable gets overwritten 95 | ``` 96 | ## Testing 97 | 1. Unit_tests: This requires the https://criterion.readthedocs.io/en/master/ testing lib for C. 98 | When you have installed that you can run: 99 | ```bash 100 | $ make run_tests # to run all tests 101 | $ make # to run specific tests 102 | ``` 103 | 2. Integration_tests: 104 | ```bash 105 | $ docker build -t testing_container . # have to setup docker container for tests to run in 106 | $ make integration_tests # starts running integration tests in testing_container 107 | ``` 108 | ## Contribution 109 | If you find any bugs or some other kind of issue it would be great if you can open an issue. If you want you can of course also contribute with PRs which should just pass all tests to make sure they don't break anything. 110 | 111 | ## Future Work 112 | - [ ] Implement local session ENV-variables using `export` and `echo $SOME` 113 | - [ ] Have builtin command to search through past commands' output 114 | - [ ] Allow for aliasing in ~/.pshrc 115 | - [ ] Allow for inplace variable execution 116 | - [ ] Improve fuzzy-finding algorithm 117 | - [ ] support utf-8 instead of only ascii 118 | -------------------------------------------------------------------------------- /tests/unit_tests/test_fuzzy_finder.c: -------------------------------------------------------------------------------- 1 | #include "../../src/fuzzy_finder.h" 2 | #include 3 | 4 | Test(findDisplayIndices, if_matching_commands_less_than_fuzzy_height) { 5 | int matching_commands_len = 3; 6 | int cursor_height = 7; 7 | int index = 2; 8 | 9 | integer_tuple result = findDisplayIndices(matching_commands_len, cursor_height, index); 10 | cr_expect(result.one == 0); 11 | cr_expect(result.second == 3); 12 | } 13 | 14 | Test(findDisplayIndices, if_matching_commands_more_than_fuzzy_height_but_index_less) { 15 | int matching_commands_len = 30; 16 | int cursor_height = 7; 17 | int index = 2; 18 | 19 | integer_tuple result = findDisplayIndices(matching_commands_len, cursor_height, index); 20 | cr_expect(result.one == 0); 21 | cr_expect(result.second == 7); 22 | } 23 | 24 | Test(findDisplayIndices, if_matching_commands_and_index_more_than_fuzzy_height) { 25 | int matching_commands_len = 30; 26 | int cursor_height = 7; 27 | int index = 12; 28 | 29 | integer_tuple result = findDisplayIndices(matching_commands_len, cursor_height, index); 30 | cr_expect(result.one == 6); 31 | cr_expect(result.second == 13); 32 | } 33 | 34 | Test(findDisplayIndices, if_index_equals_fuzzy_height) { 35 | int matching_commands_len = 10; 36 | int cursor_height = 7; 37 | int index = 7; 38 | 39 | integer_tuple result = findDisplayIndices(matching_commands_len, cursor_height, index); 40 | cr_expect(result.one == 1); 41 | cr_expect(result.second == 8); 42 | } 43 | 44 | Test(shift_prompt_fuzzy, shift_when_equal) { 45 | int result = shiftPromptIfOverlapTest(11, 11); 46 | 47 | cr_expect(result == 1); 48 | } 49 | 50 | Test(shift_prompt_fuzzy, dont_shift_when_cursor_higher) { 51 | int result = shiftPromptIfOverlapTest(10, 11); 52 | 53 | cr_expect(result == -1); 54 | } 55 | 56 | Test(shift_prompt_fuzzy, shift_when_cursor_lower) { 57 | int result = shiftPromptIfOverlapTest(14, 10); 58 | 59 | cr_expect(result == 5); 60 | } 61 | 62 | Test(removing_whitespace, initial_string_didnt_change) { 63 | char* s1 = calloc(12, sizeof(char)); 64 | strcpy(s1, "test ing"); 65 | 66 | char* result = calloc(12, sizeof(char)); 67 | result = removeWhitespace(s1); 68 | 69 | cr_expect(strcmp(s1, "test ing") == 0); 70 | free(s1); 71 | free(result); 72 | } 73 | 74 | Test(removing_whitespace, removing_single_whitespace) { 75 | char* s1 = calloc(12, sizeof(char)); 76 | strcpy(s1, "test ing"); 77 | 78 | char* result = calloc(12, sizeof(char)); 79 | result = removeWhitespace(s1); 80 | 81 | cr_expect(strcmp(result, "testing") == 0); 82 | free(s1); 83 | free(result); 84 | } 85 | 86 | Test(removing_whitespace, removing_multiple_whitespaces) { 87 | char* s1 = calloc(12, sizeof(char)); 88 | strcpy(s1, "test ing"); 89 | 90 | char* result = calloc(12, sizeof(char)); 91 | result = removeWhitespace(s1); 92 | 93 | cr_expect(strcmp(result, "testing") == 0); 94 | free(s1); 95 | free(result); 96 | } 97 | 98 | Test(updateFuzzyFinder, when_pressing_enter_copies_current_match_to_line) { 99 | char* one = "one"; 100 | char* two = "two"; 101 | char* addr_one[] = {one, two}; 102 | string_array arr1 = {.len = 2, .values = addr_one}; 103 | char c = '\n'; 104 | char* line = calloc(12, sizeof(char)); 105 | strcpy(line, "not this"); 106 | int* index = calloc(1, sizeof(int)); 107 | *index = 1; 108 | int* i; 109 | 110 | bool result = updateFuzzyfinder(&line, c, arr1, index, i, 12); 111 | cr_expect(result == false); 112 | cr_expect(strcmp(line, "two") == 0); 113 | free(line); 114 | free(index); 115 | } 116 | 117 | Test(updateFuzzyFinder, when_pressing_backspace_deletes_last_char) { 118 | char* one = "one"; 119 | char* two = "two"; 120 | char* addr_one[] = {one, two}; 121 | string_array arr1 = {.len = 2, .values = addr_one}; 122 | char c = BACKSPACE; 123 | char* line = calloc(12, sizeof(char)); 124 | strcpy(line, "not this"); 125 | int* index = calloc(1, sizeof(int)); 126 | *index = 1; 127 | int* i = calloc(1, sizeof(int)); 128 | *i = strlen(line); 129 | 130 | bool result = updateFuzzyfinder(&line, c, arr1, index, i, 12); 131 | cr_expect(result == true); 132 | cr_expect(strcmp(line, "not thi") == 0); 133 | cr_expect(*index == 0); 134 | cr_expect(*i == strlen("not thi")); 135 | free(line); 136 | free(index); 137 | free(i); 138 | } 139 | 140 | Test(updateFuzzyFinder, when_escape_twice_exits_finder) { 141 | int fds[2]; 142 | 143 | if (pipe(fds) == 1) { 144 | perror("pipe"); 145 | exit(1); 146 | } 147 | 148 | dup2(fds[0], 0); 149 | close(fds[0]); 150 | write(fds[1], "\033", sizeof("\033")); 151 | close(fds[1]); 152 | 153 | // Rest of the program thinks that stdin is from 154 | // pipe 155 | 156 | char* one = "one"; 157 | char* two = "two"; 158 | char* addr_one[] = {one, two}; 159 | string_array arr1 = {.len = 2, .values = addr_one}; 160 | char c = ESCAPE; 161 | char* line = calloc(12, sizeof(char)); 162 | strcpy(line, "not this"); 163 | int* index = calloc(1, sizeof(int)); 164 | *index = 1; 165 | int* i; 166 | 167 | bool result = updateFuzzyfinder(&line, c, arr1, index, i, 12); 168 | cr_expect(result == false); 169 | cr_expect(strcmp(line, "") == 0); 170 | free(line); 171 | free(index); 172 | } 173 | 174 | Test(updateFuzzyFinder, when_up_arrowpress_dec_index) { 175 | int fds[2]; 176 | 177 | if (pipe(fds) == 1) { 178 | perror("pipe"); 179 | exit(1); 180 | } 181 | 182 | dup2(fds[0], 0); 183 | close(fds[0]); 184 | write(fds[1], "ZAZAZA", sizeof("ZAZAZA")); // Z is any char that is not ESC 185 | close(fds[1]); 186 | 187 | // Rest of the program thinks that stdin is from 188 | // pipe 189 | 190 | char* one = "one"; 191 | char* two = "two"; 192 | char* addr_one[] = {one, two}; 193 | string_array arr1 = {.len = 2, .values = addr_one}; 194 | char c = ESCAPE; 195 | char* line = calloc(12, sizeof(char)); 196 | strcpy(line, "not this"); 197 | int* index = calloc(1, sizeof(int)); 198 | *index = 2; 199 | int* i; 200 | 201 | bool result1 = updateFuzzyfinder(&line, c, arr1, index, i, 12); 202 | cr_expect(result1 == true); 203 | cr_expect(strcmp(line, "not this") == 0); 204 | cr_expect(*index == 1); 205 | 206 | bool result2 = updateFuzzyfinder(&line, c, arr1, index, i, 12); 207 | cr_expect(result2 == true); 208 | cr_expect(strcmp(line, "not this") == 0); 209 | cr_expect(*index == 0); 210 | 211 | bool result3 = updateFuzzyfinder(&line, c, arr1, index, i, 12); 212 | cr_expect(result3 == true); 213 | cr_expect(strcmp(line, "not this") == 0); 214 | cr_expect(*index == 0); 215 | free(line); 216 | free(index); 217 | } 218 | 219 | Test(updateFuzzyFinder, when_up_downarrow_inc_index_if_not_bottom) { 220 | int fds[2]; 221 | 222 | if (pipe(fds) == 1) { 223 | perror("pipe"); 224 | exit(1); 225 | } 226 | 227 | dup2(fds[0], 0); 228 | close(fds[0]); 229 | write(fds[1], "ZB", sizeof("ZB")); // Z is any char that is not ESC 230 | close(fds[1]); 231 | 232 | char* one = "one"; 233 | char* two = "two"; 234 | char* three = "three"; 235 | char* addr_one[] = {one, two}; 236 | string_array arr1 = {.len = 3, .values = addr_one}; 237 | char c = ESCAPE; 238 | char* line = calloc(12, sizeof(char)); 239 | strcpy(line, "not this"); 240 | int* index = calloc(1, sizeof(int)); 241 | *index = 1; 242 | int* i; 243 | 244 | bool result1 = updateFuzzyfinder(&line, c, arr1, index, i, 12); 245 | cr_expect(result1 == true); 246 | cr_expect(strcmp(line, "not this") == 0); 247 | cr_expect(*index == 2); 248 | 249 | bool result2 = updateFuzzyfinder(&line, c, arr1, index, i, 12); 250 | cr_expect(result2 == true); 251 | cr_expect(strcmp(line, "not this") == 0); 252 | cr_expect(*index == 2); 253 | 254 | free(line); 255 | free(index); 256 | } 257 | 258 | Test(updateFuzzyFinder, when_typing_char_appends_to_line) { 259 | char* one = "one"; 260 | char* two = "two"; 261 | char* addr_one[] = {one, two}; 262 | string_array arr1 = {.len = 2, .values = addr_one}; 263 | char c = 's'; 264 | char* line = calloc(12, sizeof(char)); 265 | strcpy(line, "not this"); 266 | int* index = calloc(1, sizeof(int)); 267 | *index = 1; 268 | int* i = calloc(1, sizeof(int)); 269 | *i = strlen(line); 270 | 271 | bool result = updateFuzzyfinder(&line, c, arr1, index, i, 12); 272 | cr_expect(result == true); 273 | cr_expect(strcmp(line, "not thiss") == 0); 274 | cr_expect(*index == 0); 275 | cr_expect(*i == strlen("not thiss")); 276 | free(line); 277 | free(index); 278 | free(i); 279 | } 280 | -------------------------------------------------------------------------------- /tests/unit_tests/test_tab_complete.c: -------------------------------------------------------------------------------- 1 | #include "../../src/tab_complete.h" 2 | #include 3 | 4 | Test(getAllMatchingFiles, should_match_only_one_file) { 5 | char* current_dir_sub = "/Users/philipprados/documents/coding/c/pshell"; 6 | char* removed_sub = "Ma"; 7 | 8 | string_array result = getAllMatchingFiles(current_dir_sub, removed_sub); 9 | 10 | cr_expect(result.len == 1); 11 | cr_expect(strcmp(result.values[0], "Makefile") == 0); 12 | 13 | free_string_array(&result); 14 | } 15 | 16 | Test(updateCompletion, exit_out_if_random_letter_press) { 17 | autocomplete_array possible_tab_complete; 18 | char* c = calloc(1, sizeof(char)); 19 | *c = 'l'; 20 | char* line; 21 | int* tab_index; 22 | line_data* line_info; 23 | token_index token; 24 | 25 | tab_completion result = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 26 | cr_expect(result.continue_loop == false); 27 | free(c); 28 | } 29 | 30 | line_data* lineDataConstructor_for_testing(int directory_len) { 31 | line_data* line_info = calloc(1, sizeof(line_data)); 32 | *line_info = (line_data){ 33 | .line = calloc(BUFFER, sizeof(char)), 34 | .i = calloc(1, sizeof(int)), 35 | .prompt_len = directory_len + 4, 36 | .line_row_count_with_autocomplete = 0, 37 | .cursor_row = 0, 38 | .size = BUFFER * sizeof(char), 39 | }; 40 | *line_info->i = 0; 41 | 42 | return line_info; 43 | } 44 | 45 | Test(updateCompletion, test_tabpress_when_single_command_match) { 46 | char* one = "testing"; 47 | char* addr_one[] = {one}; 48 | 49 | string_array arr1 = {.len = 1, .values = addr_one}; 50 | autocomplete_array possible_tab_complete = {.array = arr1, .appending_index = 3}; 51 | char* c = calloc(1, sizeof(char)); 52 | *c = TAB; 53 | int* tab_index = calloc(1, sizeof(int*)); 54 | *tab_index = -1; 55 | line_data* line_info = lineDataConstructor_for_testing(4); 56 | strcpy(line_info->line, "tesuwe"); 57 | *line_info->i = 3; 58 | token_index token = { 59 | .token = CMD, 60 | .start = 0, 61 | .end = 6, 62 | }; 63 | 64 | tab_completion result = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 65 | cr_expect(result.continue_loop == false); 66 | cr_expect(strcmp(line_info->line, "testing") == 0); 67 | cr_expect(result.successful); 68 | 69 | free(tab_index); 70 | free(line_info->line); 71 | free(c); 72 | } 73 | 74 | Test(updateCompletion, resets_tabindex_when_no_more_matches) { 75 | char* two = "tree"; 76 | char* three = "trey"; 77 | char* addr_one[] = {two, three}; 78 | 79 | string_array arr1 = {.len = 2, .values = addr_one}; 80 | autocomplete_array possible_tab_complete = {.array = arr1, .appending_index = 2}; 81 | char* c = calloc(1, sizeof(char)); 82 | *c = TAB; 83 | int* tab_index = calloc(1, sizeof(int*)); 84 | *tab_index = -1; 85 | line_data* line_info = lineDataConstructor_for_testing(4); 86 | strcpy(line_info->line, "tre"); 87 | *line_info->i = strlen(line_info->line); 88 | token_index token = { 89 | .token = CMD, 90 | .start = 0, 91 | .end = 3, 92 | }; 93 | 94 | tab_completion result1 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 95 | cr_expect(result1.continue_loop == true); 96 | cr_expect(strcmp(line_info->line, "tre") == 0); 97 | cr_expect(result1.successful == false); 98 | cr_expect(*tab_index == 0); 99 | 100 | tab_completion result2 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 101 | cr_expect(*tab_index == 1); 102 | tab_completion result3 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 103 | cr_expect(*tab_index == 0); 104 | 105 | free(tab_index); 106 | free(line_info->line); 107 | free(c); 108 | } 109 | 110 | Test(updateCompletion, copies_current_tabindex_on_enter) { 111 | char* two = "tree"; 112 | char* three = "trey"; 113 | char* addr_one[] = {two, three}; 114 | 115 | string_array arr1 = {.len = 2, .values = addr_one}; 116 | autocomplete_array possible_tab_complete = {.array = arr1, .appending_index = 3}; 117 | char* c = calloc(1, sizeof(char)); 118 | *c = '\n'; 119 | int* tab_index = calloc(1, sizeof(int*)); 120 | *tab_index = 1; 121 | line_data* line_info = lineDataConstructor_for_testing(4); 122 | strcpy(line_info->line, "tre"); 123 | *line_info->i = strlen(line_info->line); 124 | token_index token = { 125 | .token = CMD, 126 | .start = 0, 127 | .end = 3, 128 | }; 129 | 130 | tab_completion result1 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 131 | cr_expect(result1.continue_loop == false); 132 | cr_expect(strcmp(line_info->line, "trey") == 0); 133 | cr_expect(result1.successful); 134 | cr_expect(*tab_index == 1); 135 | 136 | free(tab_index); 137 | free(line_info->line); 138 | free(c); 139 | } 140 | 141 | Test(updateCompletion, appends_correct_fileend_to_filecomp_on_dirs) { 142 | char* two = "tree"; 143 | char* three = "trey"; 144 | char* addr_one[] = {two, three}; 145 | 146 | string_array arr1 = {.len = 2, .values = addr_one}; 147 | autocomplete_array possible_tab_complete = {.array = arr1, .appending_index = 2}; 148 | 149 | char* c = calloc(1, sizeof(char)); 150 | *c = '\n'; 151 | int* tab_index = calloc(1, sizeof(int*)); 152 | *tab_index = 0; 153 | line_data* line_info = lineDataConstructor_for_testing(5); 154 | strcpy(line_info->line, "ls test/dir/why/tr"); 155 | *line_info->i = strlen(line_info->line); 156 | token_index token = { 157 | .token = ARG, 158 | .start = 3, 159 | .end = strlen("test/dir/why/tr"), 160 | }; 161 | 162 | tab_completion result1 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 163 | cr_expect(result1.continue_loop == false); 164 | cr_expect(strcmp(line_info->line, "ls test/dir/why/tree") == 0); 165 | cr_expect(result1.successful); 166 | cr_expect(*tab_index == 0); 167 | 168 | free(tab_index); 169 | free(line_info->line); 170 | free(c); 171 | } 172 | 173 | Test(updateCompletion, appends_correct_fileend_to_filecomp_on_dirs_when_cursor_middle) { 174 | char* two = "tree"; 175 | char* addr_one[] = {two}; 176 | 177 | string_array arr1 = {.len = 1, .values = addr_one}; 178 | autocomplete_array possible_tab_complete = {.array = arr1, .appending_index = 0}; 179 | 180 | char* c = calloc(1, sizeof(char)); 181 | *c = TAB; 182 | int* tab_index = calloc(1, sizeof(int*)); 183 | *tab_index = -1; 184 | line_data* line_info = lineDataConstructor_for_testing(4); 185 | strcpy(line_info->line, "ls test/dir/why/tr"); 186 | *line_info->i = strlen("ls test/dir/"); 187 | token_index token = { 188 | .token = ARG, 189 | .start = 3, 190 | .end = 3 + strlen("test/dir/why/tr"), 191 | }; 192 | 193 | tab_completion result1 = updateCompletion(possible_tab_complete, c, line_info, tab_index, token); 194 | cr_expect(result1.continue_loop == false); 195 | cr_expect(result1.successful); 196 | cr_expect(strcmp(line_info->line, "ls test/dir/tree") == 0); 197 | cr_expect(*tab_index == -1); 198 | 199 | free(tab_index); 200 | free(line_info->line); 201 | free(c); 202 | } 203 | 204 | Test(removeDotFilesIfnecessary, should_remove_if_no_dot_at_first_char) { 205 | char** addr_one = calloc(4, sizeof(char*)); 206 | addr_one[0] = calloc(strlen("one") + 1, 1); 207 | strcpy(addr_one[0], "one"); 208 | addr_one[1] = calloc(strlen(".two") + 1, 1); 209 | strcpy(addr_one[1], ".two"); 210 | addr_one[2] = calloc(strlen("testi.ng") + 1, 1); 211 | strcpy(addr_one[2], "testi.ng"); 212 | addr_one[3] = calloc(strlen("..") + 1, 1); 213 | strcpy(addr_one[3], ".."); 214 | string_array arr1 = {.len = 4, .values = addr_one}; 215 | autocomplete_array autocomplete = {.array = arr1, .appending_index = 0}; 216 | char* current_word = "test"; 217 | 218 | removeDotFilesIfnecessary(current_word, &autocomplete); 219 | 220 | cr_expect(strcmp(autocomplete.array.values[0], "one") == 0); 221 | cr_expect(strcmp(autocomplete.array.values[1], "testi.ng") == 0); 222 | cr_expect(autocomplete.array.len == 2); 223 | cr_expect(autocomplete.appending_index == 0); 224 | 225 | free_string_array(&autocomplete.array); 226 | } 227 | 228 | Test(getCurrentWordFromLineIndex, should_not_remove_if_current_word_starts_with_dot) { 229 | char** addr_one = calloc(4, sizeof(char*)); 230 | addr_one[0] = calloc(strlen("one") + 1, 1); 231 | strcpy(addr_one[0], "one"); 232 | addr_one[1] = calloc(strlen(".two") + 1, 1); 233 | strcpy(addr_one[1], ".two"); 234 | addr_one[2] = calloc(strlen("testi.ng") + 1, 1); 235 | strcpy(addr_one[2], "testi.ng"); 236 | addr_one[3] = calloc(strlen("..") + 1, 1); 237 | strcpy(addr_one[3], ".."); 238 | string_array arr1 = {.len = 4, .values = addr_one}; 239 | autocomplete_array autocomplete = {.array = arr1, .appending_index = 0}; 240 | char* current_word = "test/.."; 241 | 242 | removeDotFilesIfnecessary(current_word, &autocomplete); 243 | 244 | cr_expect(strcmp(autocomplete.array.values[0], "one") == 0); 245 | cr_expect(strcmp(autocomplete.array.values[1], ".two") == 0); 246 | cr_expect(strcmp(autocomplete.array.values[2], "testi.ng") == 0); 247 | cr_expect(strcmp(autocomplete.array.values[3], "..") == 0); 248 | cr_expect(autocomplete.array.len == 4); 249 | cr_expect(autocomplete.appending_index == 0); 250 | 251 | free_string_array(&autocomplete.array); 252 | } 253 | 254 | Test(escapeWhitespace, when_whitespace_in_array_gets_escaped) { 255 | char** addr_one = calloc(4, sizeof(char*)); 256 | addr_one[0] = calloc(strlen("one") + 1, 1); 257 | strcpy(addr_one[0], "one"); 258 | addr_one[1] = calloc(strlen(" two") + 1, 1); 259 | strcpy(addr_one[1], " two"); 260 | addr_one[2] = calloc(strlen("testi ng") + 1, 1); 261 | strcpy(addr_one[2], "testi ng"); 262 | string_array arr1 = {.len = 3, .values = addr_one}; 263 | 264 | escapeWhitespace(&arr1); 265 | cr_expect(strcmp(arr1.values[0], "one") == 0); 266 | cr_expect(strcmp(arr1.values[1], "\\ \\ two") == 0); 267 | cr_expect(strcmp(arr1.values[2], "testi\\ ng") == 0); 268 | } 269 | -------------------------------------------------------------------------------- /src/fuzzy_finder.c: -------------------------------------------------------------------------------- 1 | #include "fuzzy_finder.h" 2 | 3 | #define MIN3(a, b, c) ((a) < (b) ? ((a) < (c) ? (a) : (c)) : ((b) < (c) ? (b) : (c))) 4 | 5 | int levenshtein(const char* s1, char* s2, int s1_len) { 6 | if (s1_len == 0) 7 | return 0; 8 | char* s2_substring = calloc(strlen(s1) + 1, sizeof(char)); 9 | s2_substring = strncpy(s2_substring, &s2[0], strlen(s1)); 10 | 11 | unsigned int s1len, s2len, x, y, lastdiag, olddiag; 12 | s1len = strlen(s1); 13 | s2len = s1len; 14 | unsigned int column[s1len + 1]; 15 | 16 | for (y = 1; y <= s1len; y++) { 17 | column[y] = y; 18 | } 19 | 20 | for (x = 1; x <= s2len; x++) { 21 | column[0] = x; 22 | for (y = 1, lastdiag = x - 1; y <= s1len; y++) { 23 | olddiag = column[y]; 24 | column[y] = MIN3(column[y] + 1, column[y - 1] + 1, lastdiag + (s1[y - 1] == s2_substring[x - 1] ? 0 : 1)); 25 | lastdiag = olddiag; 26 | } 27 | } 28 | free(s2_substring); 29 | 30 | return column[s1len]; 31 | } 32 | 33 | char* removeWhitespace(char* s1) { 34 | char* stripped = calloc(strlen(s1) + 1, sizeof(char)); 35 | int j = 0; 36 | 37 | for (int i = 0; i < strlen(s1); i++) { 38 | if (s1[i] != ' ') { 39 | stripped[j] = s1[i]; 40 | j++; 41 | } 42 | } 43 | 44 | return stripped; 45 | } 46 | 47 | string_array filterHistory(const string_array concatenated, char* line) { 48 | char** possible_matches = calloc(concatenated.len, sizeof(char*)); 49 | int matches_num = 0; 50 | char* line_no_whitespace = removeWhitespace(line); 51 | 52 | for (int i = 0; i < concatenated.len; i++) { 53 | char* values_no_whitespace = removeWhitespace(concatenated.values[i]); 54 | 55 | if (levenshtein(line_no_whitespace, values_no_whitespace, strlen(line_no_whitespace)) < 2) { 56 | possible_matches[matches_num] = calloc(strlen(concatenated.values[i]) + 1, sizeof(char)); 57 | strcpy(possible_matches[matches_num], concatenated.values[i]); 58 | matches_num++; 59 | } 60 | free(values_no_whitespace); 61 | } 62 | free(line_no_whitespace); 63 | string_array result = {.values = possible_matches, .len = matches_num}; 64 | 65 | return result; 66 | } 67 | 68 | integer_tuple findDisplayIndices(int matching_commands_len, int cursor_diff, int index) { 69 | int start = 0; 70 | int end = (matching_commands_len < cursor_diff) ? matching_commands_len : cursor_diff; 71 | 72 | if (index >= cursor_diff) { 73 | start = index - cursor_diff + 1; 74 | end = index + 1; 75 | } 76 | 77 | integer_tuple result = { 78 | .one = start, 79 | .second = end, 80 | }; 81 | 82 | return result; 83 | } 84 | 85 | int shiftPromptIfOverlapTest(int current_cursor_height, int fuzzy_popup_height) { 86 | if (current_cursor_height < fuzzy_popup_height) 87 | return -1; 88 | int j = 0; 89 | 90 | for (int i = fuzzy_popup_height; i <= current_cursor_height; i++) { 91 | j++; 92 | } 93 | return j; 94 | } 95 | 96 | void drawPopupBox(const coordinates terminal_size, const int width, const int height) { 97 | CLEAR_SCREEN 98 | 99 | for (int row = 0; row < terminal_size.y; row++) { 100 | if (row == (height / 2) || row == (terminal_size.y - (height / 2))) { 101 | for (int i = 0; i < terminal_size.x; i++) { 102 | if (i > (width / 2) && i < (terminal_size.x - (width / 2))) { 103 | printf("\u2550"); 104 | } else if (i == (width / 2)) { 105 | if (row == (height / 2)) { 106 | printf("\u2554"); 107 | } else { 108 | printf("\u255A"); 109 | } 110 | } else if (i == (terminal_size.x - (width / 2))) { 111 | if (row == (height / 2)) { 112 | printf("\u2557"); 113 | } else { 114 | printf("\u255D"); 115 | } 116 | } else { 117 | printf(" "); 118 | } 119 | } 120 | } else if (row > (height / 2) && row < (terminal_size.y - (height / 2))) { 121 | for (int col = 0; col < terminal_size.x; col++) { 122 | if (col == (width / 2) || col == (terminal_size.x - (width / 2))) { 123 | printf("\u2551"); 124 | } else { 125 | printf(" "); 126 | } 127 | } 128 | } else { 129 | printf("\n"); 130 | } 131 | } 132 | coordinates bottom_box_pos = {.x = (width / 2) + 3, .y = terminal_size.y - (height / 2)}; 133 | moveCursor(bottom_box_pos); 134 | } 135 | 136 | void clearFuzzyWindow(coordinates initial_cursor_pos, int box_width, int box_height) { 137 | for (int rows = initial_cursor_pos.x + 2; rows < box_width; rows++) { 138 | for (int cols = initial_cursor_pos.y; cols < box_height; cols++) { 139 | coordinates cursor = {.x = rows, .y = cols}; 140 | moveCursor(cursor); 141 | printf(" "); 142 | } 143 | } 144 | 145 | moveCursor(initial_cursor_pos); 146 | } 147 | 148 | void renderMatches(string_array matching_commands, coordinates initial_cursor_pos, int index, 149 | integer_tuple start_end, int terminal_width) { 150 | int i = 0; 151 | char* complete; 152 | 153 | for (int j = start_end.one; j < start_end.second; j++) { 154 | coordinates drawing_pos = { 155 | .y = initial_cursor_pos.y + i + 1, 156 | .x = initial_cursor_pos.x + 2, 157 | }; 158 | moveCursor(drawing_pos); 159 | 160 | complete = shortenIfTooLong(matching_commands.values[j], terminal_width - 1); 161 | if (j == index) { 162 | printColor(complete, GREEN, reversed); 163 | } else { 164 | printf("%s", complete); 165 | } 166 | free(complete); 167 | i++; 168 | } 169 | } 170 | 171 | void renderFuzzyFinder(coordinates initial_cursor_pos, int terminal_width, char* line, int index, 172 | string_array matching_commands, int cursor_terminal_height_diff) { 173 | CLEAR_LINE; 174 | CLEAR_BELOW_CURSOR; 175 | coordinates end_of_line = { 176 | .x = initial_cursor_pos.x + (terminal_width - 10), 177 | .y = initial_cursor_pos.y, 178 | }; 179 | 180 | integer_tuple display_ranges = findDisplayIndices(matching_commands.len, cursor_terminal_height_diff, index); 181 | renderMatches(matching_commands, initial_cursor_pos, index, display_ranges, terminal_width); 182 | 183 | moveCursor(end_of_line); 184 | printf("%d/%d", index + 1, matching_commands.len); 185 | 186 | moveCursor(initial_cursor_pos); 187 | printf("\u2771 %s", line); 188 | } 189 | 190 | bool shiftPromptIfOverlap(int current_cursor_height, int fuzzy_popup_height, 191 | int line_row_count_with_autocomplete) { 192 | if ((current_cursor_height + line_row_count_with_autocomplete) < fuzzy_popup_height) 193 | return false; 194 | 195 | for (int i = fuzzy_popup_height; i <= (current_cursor_height + line_row_count_with_autocomplete); i++) { 196 | printf("\n"); 197 | } 198 | return true; 199 | } 200 | 201 | bool drawFuzzyPopup(int current_cursor_height, coordinates initial_cursor_pos, int terminal_width, 202 | int line_row_count_with_autocomplete) { 203 | bool shifted = 204 | shiftPromptIfOverlap(current_cursor_height, initial_cursor_pos.y - 2, line_row_count_with_autocomplete); 205 | moveCursor((coordinates){initial_cursor_pos.x, initial_cursor_pos.y - 2}); 206 | 207 | for (int i = 0; i < terminal_width - 1; i++) { 208 | printf("\u2501"); 209 | } 210 | 211 | moveCursor(initial_cursor_pos); 212 | printf("\u2771 "); 213 | 214 | return shifted; 215 | } 216 | 217 | bool updateFuzzyfinder(char** line, char c, string_array matching_commands, int* fuzzy_index, int* i, 218 | int max_input_len) { 219 | bool loop = true; 220 | if (c == '\n') { 221 | if (matching_commands.len > 0) { 222 | memset(*line, 0, strlen(*line)); 223 | if (strlen(matching_commands.values[*fuzzy_index]) > BUFFER) { 224 | *line = realloc(*line, (strlen(matching_commands.values[*fuzzy_index]) + 1) * sizeof(char)); 225 | } 226 | strcpy(*line, matching_commands.values[*fuzzy_index]); 227 | } 228 | loop = false; 229 | } else if (c == BACKSPACE) { 230 | backspaceLogic(*line, i); 231 | *fuzzy_index = 0; 232 | } else if (c == ESCAPE) { 233 | if (getch() == ESCAPE) { 234 | strcpy(*line, ""); 235 | return false; 236 | } 237 | int value = getch(); 238 | 239 | if (value == 'A') { 240 | (*fuzzy_index > 0) ? (*fuzzy_index)-- : *fuzzy_index; 241 | } else if (value == 'B') { 242 | (*fuzzy_index < matching_commands.len - 1) ? (*fuzzy_index)++ : *fuzzy_index; 243 | } 244 | } else { 245 | if (strlen(*line) < max_input_len - 1 && c > 0 && c < 127) { 246 | *fuzzy_index = 0; 247 | (*line)[*i] = c; 248 | (*i)++; 249 | } 250 | } 251 | return loop; 252 | } 253 | 254 | fuzzy_result popupFuzzyFinder(const string_array all_time_command_history, const coordinates terminal_size, 255 | int current_cursor_height, int line_row_count_with_autocomplete) { 256 | char c = -1; 257 | int* fuzzy_index = calloc(1, sizeof(int)); 258 | *fuzzy_index = 0; 259 | int* i = calloc(1, sizeof(int)); 260 | *i = 0; 261 | int max_input_len = terminal_size.x - 15; 262 | char* line = calloc(max_input_len, sizeof(char)); 263 | coordinates fuzzy_cursor_height = {.x = 2, .y = terminal_size.y * 0.85}; 264 | int cursor_terminal_height_diff = terminal_size.y - fuzzy_cursor_height.y; 265 | string_array matching_commands; 266 | matching_commands.len = 0; 267 | matching_commands.values = NULL; 268 | bool loop = true; 269 | 270 | moveCursor(terminal_size); // moving Cursor to bottom so that newline 271 | // character shifts line up 272 | bool shifted = drawFuzzyPopup(current_cursor_height, fuzzy_cursor_height, terminal_size.x, 273 | line_row_count_with_autocomplete); 274 | 275 | do { 276 | loop = updateFuzzyfinder(&line, c, matching_commands, fuzzy_index, i, max_input_len); 277 | 278 | if (loop) { 279 | string_array filtered_history = filterHistory(all_time_command_history, line); 280 | free_string_array(&matching_commands); 281 | matching_commands = removeDuplicates(&filtered_history); 282 | 283 | renderFuzzyFinder(fuzzy_cursor_height, terminal_size.x, line, *fuzzy_index, matching_commands, 284 | cursor_terminal_height_diff); 285 | } 286 | } while (loop && (c = getch())); 287 | 288 | free_string_array(&matching_commands); 289 | free(fuzzy_index); 290 | free(i); 291 | 292 | moveCursor((coordinates){0, fuzzy_cursor_height.y - 2}); 293 | CLEAR_LINE; 294 | CLEAR_BELOW_CURSOR; 295 | 296 | return (fuzzy_result){.line = line, .shifted = shifted}; 297 | } 298 | -------------------------------------------------------------------------------- /src/tab_complete.c: -------------------------------------------------------------------------------- 1 | #include "tab_complete.h" 2 | 3 | int maxWidthTerm(int width, int terminal_width) { 4 | if (width > terminal_width - 2) { 5 | width = terminal_width - 2; 6 | } 7 | return width; 8 | } 9 | 10 | void tabRender(string_array possible_tabcomplete, int tab_index, int col_size, int format_width, 11 | int terminal_width) { 12 | int j = 0; 13 | char* complete; 14 | 15 | while (j < possible_tabcomplete.len) { 16 | printf("\n"); 17 | for (int x = 0; x < col_size; x++) { 18 | if (j >= possible_tabcomplete.len) 19 | break; 20 | complete = shortenIfTooLong(possible_tabcomplete.values[j], terminal_width); 21 | 22 | int diff_len = strlen(complete) - format_width; 23 | if (tab_index == j) { 24 | 25 | printColor(complete, GREEN, reversed); 26 | printf("%-*s", diff_len, ""); 27 | } else { 28 | if (possible_tabcomplete.values[j][strlen(possible_tabcomplete.values[j]) - 1] == '/') { 29 | printColor(complete, CYAN, bold); 30 | printf("%-*s", diff_len, ""); 31 | } else { 32 | printf("%-*s", format_width, complete); 33 | } 34 | } 35 | j++; 36 | free(complete); 37 | } 38 | } 39 | } 40 | 41 | void escapeWhitespace(string_array* arr) { 42 | for (int i = 0; i < arr->len; i++) { 43 | for (int j = 0; j < strlen(arr->values[i]); j++) { 44 | if (arr->values[i][j] == ' ') { 45 | arr->values[i] = realloc(arr->values[i], (strlen(arr->values[i]) + 2) * sizeof(char)); 46 | // insert escape \\ in front of whitespace 47 | insertCharAtPos(arr->values[i], j, '\\'); 48 | j++; 49 | } 50 | } 51 | } 52 | } 53 | autocomplete_array checkForAutocomplete(char* current_word, enum token current_token, 54 | const string_array PATH_BINS) { 55 | autocomplete_array possible_autocomplete = {.array.len = 0}; 56 | 57 | if (current_token == CMD || current_token == PIPE_CMD || current_token == AMP_CMD) { // autocomplete for commands 58 | string_array filtered = filterMatching(current_word, PATH_BINS); 59 | 60 | possible_autocomplete = (autocomplete_array){ 61 | .array.values = filtered.values, .array.len = filtered.len, .appending_index = strlen(current_word)}; 62 | } else { // autocomplete for files 63 | possible_autocomplete = fileComp(current_word); 64 | escapeWhitespace(&possible_autocomplete.array); 65 | } 66 | 67 | return possible_autocomplete; 68 | } 69 | 70 | void moveCursorIfShifted(render_objects* render_data) { 71 | if (render_data->cursor_height_diff <= render_data->row_size || render_data->cursor_height_diff == 0) { 72 | render_data->cursor_pos->y -= render_data->row_size - render_data->cursor_height_diff; 73 | moveCursor(*render_data->cursor_pos); 74 | } else { 75 | moveCursor(*render_data->cursor_pos); 76 | } 77 | render_data->cursor_pos->y -= render_data->cursor_row; 78 | } 79 | 80 | void renderCompletion(autocomplete_array possible_tabcomplete, int tab_index, render_objects* render_data) { 81 | render_data->cursor_pos->y += render_data->cursor_row; 82 | int bottom_line_y = 83 | render_data->cursor_pos->y - render_data->cursor_row + render_data->line_row_count_with_autocomplete; 84 | render_data->cursor_height_diff = render_data->terminal_size.y - bottom_line_y; 85 | 86 | moveCursor((coordinates){1000, bottom_line_y}); // have to move cursor to end of 87 | // line to not cut off in middle 88 | CLEAR_BELOW_CURSOR; 89 | tabRender(possible_tabcomplete.array, tab_index, render_data->col_size, render_data->format_width, 90 | render_data->terminal_size.x); 91 | moveCursorIfShifted(render_data); 92 | } 93 | 94 | bool dontShowMatches(char answer, render_objects* render_data, autocomplete_array possible_tabcomplete) { 95 | bool result = false; 96 | 97 | if (answer != 'y') { 98 | result = true; 99 | } else if (render_data->row_size >= render_data->terminal_size.y) { 100 | renderCompletion(possible_tabcomplete, -1, render_data); 101 | printf("\n\n"); 102 | 103 | for (int i = 0; i < render_data->line_row_count_with_autocomplete; i++) { 104 | printf("\n"); 105 | } 106 | render_data->cursor_pos->y = 107 | render_data->terminal_size.y + render_data->cursor_row - render_data->line_row_count_with_autocomplete; 108 | result = true; 109 | } 110 | return result; 111 | } 112 | 113 | bool tooManyMatches(render_objects* render_data, autocomplete_array possible_tabcomplete) { 114 | char answer; 115 | char* prompt_sentence = "\nThe list of possible matches is %d lines. Do you want to print all of them? (y/n) "; 116 | int bottom_line_y = render_data->cursor_pos->y + render_data->line_row_count_with_autocomplete; 117 | 118 | moveCursor((coordinates){1000, bottom_line_y}); 119 | 120 | if (!(render_data->row_size > 10 || render_data->row_size > render_data->terminal_size.y)) 121 | return false; 122 | 123 | printf(prompt_sentence, render_data->row_size); 124 | answer = getch(); 125 | 126 | int prompt_row_count = calculateRowCount(render_data->terminal_size, 0, strlen(prompt_sentence) - 1) + 1; 127 | int diff = prompt_row_count + bottom_line_y - render_data->terminal_size.y; 128 | if (diff > 0) { 129 | render_data->cursor_pos->y -= diff; 130 | } 131 | 132 | return dontShowMatches(answer, render_data, possible_tabcomplete); 133 | } 134 | 135 | bool tabPress(autocomplete_array possible_tabcomplete, int* tab_index, line_data* line_info, 136 | token_index current_token) { 137 | if (possible_tabcomplete.array.len == 1) { 138 | removeSlice(&line_info->line, *line_info->i, current_token.end); 139 | insertStringAtPos(&line_info->line, 140 | &(possible_tabcomplete.array.values[0])[possible_tabcomplete.appending_index], 141 | *line_info->i); 142 | 143 | line_info->size = (strlen(line_info->line) + 1) * sizeof(char); 144 | return true; 145 | } else if (possible_tabcomplete.array.len > 1) { 146 | if (*tab_index < possible_tabcomplete.array.len - 1) { 147 | *tab_index += 1; 148 | } else { 149 | *tab_index = 0; 150 | } 151 | } 152 | return false; 153 | } 154 | 155 | void shiftTabPress(autocomplete_array possible_tabcomplete, int* tab_index) { 156 | getch(); 157 | if (getch() == 'Z') { // Shift-Tab 158 | if (*tab_index > 0) { 159 | *tab_index -= 1; 160 | } else { 161 | *tab_index = possible_tabcomplete.array.len - 1; 162 | } 163 | } 164 | } 165 | 166 | void enterPress(autocomplete_array possible_tabcomplete, line_data* line_info, int tab_index, 167 | token_index current_token) { 168 | removeSlice(&line_info->line, *line_info->i, current_token.end); 169 | // remove-slice is correct 170 | // have to count escapes upto line_index 171 | insertStringAtPos(&line_info->line, 172 | &(possible_tabcomplete.array.values[tab_index])[possible_tabcomplete.appending_index], 173 | *line_info->i); 174 | 175 | line_info->size = (strlen(line_info->line) + 1) * sizeof(char); 176 | } 177 | 178 | tab_completion updateCompletion(autocomplete_array possible_tabcomplete, char* c, line_data* line_info, 179 | int* tab_index, token_index current_token) { 180 | tab_completion result = {.successful = false, .continue_loop = true}; 181 | if (*c == TAB) { 182 | if (tabPress(possible_tabcomplete, tab_index, line_info, current_token)) { 183 | result.successful = true; 184 | result.continue_loop = false; 185 | } 186 | 187 | } else if (*c == ESCAPE) { 188 | shiftTabPress(possible_tabcomplete, tab_index); 189 | 190 | } else if (*c == '\n') { 191 | enterPress(possible_tabcomplete, line_info, *tab_index, current_token); 192 | result.successful = true; 193 | result.continue_loop = false; 194 | 195 | } else { 196 | result.successful = false; 197 | result.continue_loop = false; 198 | } 199 | 200 | return result; 201 | } 202 | 203 | render_objects initializeRenderObjects(coordinates terminal_size, autocomplete_array possible_tabcomplete, 204 | coordinates* cursor_pos, int cursor_row, 205 | int line_row_count_with_autocomplete) { 206 | int format_width = maxWidthTerm(getLongestWordInArray(possible_tabcomplete.array), terminal_size.x) + 2; 207 | int col_size = terminal_size.x / format_width; 208 | int row_size = ceil(possible_tabcomplete.array.len / (float)col_size); 209 | int cursor_height_diff = terminal_size.y - cursor_pos->y; 210 | 211 | return (render_objects){ 212 | .format_width = format_width, 213 | .col_size = col_size, 214 | .row_size = row_size, 215 | .cursor_height_diff = cursor_height_diff, 216 | .cursor_pos = cursor_pos, 217 | .terminal_size = terminal_size, 218 | .cursor_row = cursor_row, 219 | .line_row_count_with_autocomplete = line_row_count_with_autocomplete, 220 | }; 221 | } 222 | 223 | autocomplete_array removeDotFiles(autocomplete_array* tabcomp) { 224 | autocomplete_array new; 225 | new.array.values = calloc(tabcomp->array.len, sizeof(char*)); 226 | int j = 0; 227 | for (int i = 0; i < tabcomp->array.len; i++) { 228 | if (tabcomp->array.values[i][0] != '.') { 229 | new.array.values[j] = calloc(strlen(tabcomp->array.values[i]) + 1, sizeof(char)); 230 | strcpy(new.array.values[j], tabcomp->array.values[i]); 231 | j++; 232 | } 233 | } 234 | new.appending_index = tabcomp->appending_index; 235 | new.array.len = j; 236 | free_string_array(&tabcomp->array); 237 | return new; 238 | } 239 | 240 | void removeDotFilesIfnecessary(char* current_word, autocomplete_array* possible_tabcomplete) { 241 | int appending_index; 242 | char* removed_sub; 243 | if ((appending_index = getAppendingIndex(current_word, '/')) != -1) { 244 | removed_sub = &(current_word[strlen(current_word) - getAppendingIndex(current_word, '/')]); 245 | } else { 246 | removed_sub = current_word; 247 | } 248 | if (removed_sub[0] != '.') { 249 | *possible_tabcomplete = removeDotFiles(possible_tabcomplete); 250 | } 251 | } 252 | 253 | char* getCurrentWord(char* line, int line_index, token_index current_token) { 254 | if (current_token.start == -1 && current_token.end == -1) { 255 | return NULL; 256 | } else if (current_token.token == WHITESPACE) { 257 | char* result = calloc(2, sizeof(char)); 258 | strcpy(result, ""); 259 | return result; 260 | } else { 261 | char* result = calloc(line_index - current_token.start + 1, sizeof(char)); 262 | strncpy(result, &line[current_token.start], line_index - current_token.start); 263 | return result; 264 | } 265 | } 266 | 267 | bool tabLoop(line_data* line_info, coordinates* cursor_pos, const string_array PATH_BINS, 268 | const coordinates terminal_size, token_index current_token) { 269 | int tab_index = -1; 270 | char* current_word = getCurrentWord(line_info->line, *line_info->i, current_token); 271 | removeEscapesString(¤t_word); 272 | autocomplete_array possible_tabcomplete = checkForAutocomplete(current_word, current_token.token, PATH_BINS); 273 | removeDotFilesIfnecessary(current_word, &possible_tabcomplete); 274 | 275 | render_objects render_data = 276 | initializeRenderObjects(terminal_size, possible_tabcomplete, cursor_pos, line_info->cursor_row, 277 | line_info->line_row_count_with_autocomplete); 278 | tab_completion completion_result; 279 | 280 | if (possible_tabcomplete.array.len <= 0 || tooManyMatches(&render_data, possible_tabcomplete)) { 281 | free_string_array(&(possible_tabcomplete.array)); 282 | free(current_word); 283 | return false; 284 | } 285 | do { 286 | if ((completion_result = 287 | updateCompletion(possible_tabcomplete, &line_info->c, line_info, &tab_index, current_token)) 288 | .continue_loop) { 289 | renderCompletion(possible_tabcomplete, tab_index, &render_data); 290 | } 291 | 292 | } while (completion_result.continue_loop && (line_info->c = getch())); 293 | 294 | free_string_array(&(possible_tabcomplete.array)); 295 | free(current_word); 296 | 297 | return completion_result.successful; 298 | } 299 | -------------------------------------------------------------------------------- /tests/unit_tests/test_readline.c: -------------------------------------------------------------------------------- 1 | #include "../../src/readline.h" 2 | #include 3 | 4 | Test(stringToLower, converting_string_lowercase_in_place) { 5 | char* string = calloc(strlen("halle") + 1, sizeof(char)); 6 | strcpy(string, "HaLLe"); 7 | 8 | stringToLower(string); 9 | 10 | cr_expect(strcmp(string, "halle") == 0); 11 | free(string); 12 | } 13 | 14 | // big update tests 15 | Test(update, writing_normal_commands_works) { 16 | line_data* line_info = lineDataConstructor(4); 17 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 18 | line_info->c = 'l'; 19 | 20 | string_array arr1 = {.len = 0}; 21 | string_array global_command_history = arr1; 22 | string_array* sessions_command_history = &arr1; 23 | history_data* history_info = historyDataConstructor(sessions_command_history, global_command_history); 24 | coordinates* cursor_pos; 25 | 26 | bool result = update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, 27 | cursor_pos); 28 | 29 | cr_expect(result == true); 30 | cr_expect(strcmp(line_info->line, "l") == 0); 31 | cr_expect(*line_info->i == 1); 32 | cr_expect(autocomplete_info->autocomplete == false); 33 | 34 | line_info->c = 's'; 35 | bool result2 = update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, 36 | cursor_pos); 37 | cr_expect(result == true); 38 | cr_expect(strcmp(line_info->line, "ls") == 0); 39 | cr_expect(*line_info->i == 2); 40 | cr_expect(autocomplete_info->autocomplete == false); 41 | 42 | free(autocomplete_info->possible_autocomplete); 43 | free(autocomplete_info); 44 | free(history_info); 45 | free(line_info->line); 46 | free(line_info->i); 47 | free(line_info); 48 | } 49 | 50 | Test(update, moving_cursor_with_arrow_keys) { 51 | line_data* line_info = lineDataConstructor(8); 52 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 53 | 54 | string_array arr1 = {.len = 0}; 55 | string_array global_command_history = arr1; 56 | string_array* sessions_command_history = &arr1; 57 | history_data* history_info = historyDataConstructor(sessions_command_history, global_command_history); 58 | coordinates* cursor_pos; 59 | 60 | // type something to test 61 | line_info->c = 'l'; 62 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 63 | line_info->c = 's'; 64 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 65 | 66 | int fds[2]; 67 | if (pipe(fds) == 1) { 68 | perror("pipe"); 69 | exit(1); 70 | } 71 | dup2(fds[0], 0); 72 | close(fds[0]); 73 | write(fds[1], "ZDZDZDZC", sizeof("ZDZDZDZC")); // emulate horizontal-arrow-press 74 | close(fds[1]); 75 | 76 | line_info->c = ESCAPE; 77 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 78 | cr_expect(strcmp(line_info->line, "ls") == 0); 79 | cr_expect(*line_info->i == 1); 80 | 81 | line_info->c = ESCAPE; 82 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 83 | cr_expect(strcmp(line_info->line, "ls") == 0); 84 | cr_expect(*line_info->i == 0); 85 | 86 | line_info->c = ESCAPE; 87 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 88 | cr_expect(strcmp(line_info->line, "ls") == 0); 89 | cr_expect(*line_info->i == 0); 90 | 91 | line_info->c = ESCAPE; 92 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 93 | cr_expect(strcmp(line_info->line, "ls") == 0); 94 | cr_expect(*line_info->i == 1); 95 | 96 | line_info->c = 'e'; 97 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 98 | cr_expect(strcmp(line_info->line, "les") == 0); 99 | cr_expect(*line_info->i == 2); 100 | 101 | free(autocomplete_info->possible_autocomplete); 102 | free(autocomplete_info); 103 | free(history_info); 104 | free(line_info->line); 105 | free(line_info->i); 106 | free(line_info); 107 | } 108 | 109 | Test(update, moving_through_history_with_cursor) { 110 | line_data* line_info = lineDataConstructor(8); 111 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 112 | 113 | char** addr_one = calloc(2, sizeof(char*)); 114 | addr_one[0] = calloc(strlen("yeyeye") + 1, 1); 115 | strcpy(addr_one[0], "yeyeye"); 116 | addr_one[1] = calloc(strlen("test") + 1, 1); 117 | strcpy(addr_one[1], "test"); 118 | string_array global_command_history = (string_array){.len = 0}; 119 | string_array* sessions_command_history = calloc(1, sizeof(string_array)); 120 | sessions_command_history->values = addr_one; 121 | sessions_command_history->len = 2; 122 | history_data* history_info = historyDataConstructor(sessions_command_history, global_command_history); 123 | coordinates* cursor_pos; 124 | 125 | // type something to test 126 | line_info->c = 'l'; 127 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 128 | line_info->c = 's'; 129 | cr_expect(history_info->history_index == 0); 130 | 131 | line_info->c = ESCAPE; 132 | int fds[2]; 133 | if (pipe(fds) == 1) { 134 | perror("pipe"); 135 | exit(1); 136 | } 137 | dup2(fds[0], 0); 138 | close(fds[0]); 139 | write(fds[1], "ZAZAZAZBZB", sizeof("ZAZAZAZBZB")); // emulate key-press_sequence 140 | close(fds[1]); 141 | 142 | bool result = update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, 143 | cursor_pos); 144 | cr_expect(result == true); 145 | cr_expect(strcmp(line_info->line, "yeyeye") == 0); 146 | cr_expect(*line_info->i == strlen("yeyeye")); 147 | cr_expect(history_info->history_index == 1); 148 | 149 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 150 | cr_expect(strcmp(line_info->line, "test") == 0); 151 | cr_expect(*line_info->i == strlen("test")); 152 | cr_expect(history_info->history_index == 2); 153 | 154 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 155 | cr_expect(strcmp(line_info->line, "test") == 0); 156 | cr_expect(*line_info->i == strlen("test")); 157 | cr_expect(history_info->history_index == 2); 158 | 159 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 160 | cr_expect(strcmp(line_info->line, "yeyeye") == 0); 161 | cr_expect(*line_info->i == strlen("yeyeye")); 162 | cr_expect(history_info->history_index == 1); 163 | 164 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 165 | cr_expect(strcmp(line_info->line, "") == 0); 166 | cr_expect(*line_info->i == strlen("")); 167 | cr_expect(history_info->history_index == 0); 168 | 169 | free(autocomplete_info->possible_autocomplete); 170 | free(autocomplete_info); 171 | free_string_array(&history_info->sessions_command_history); 172 | free(history_info); 173 | free(line_info->line); 174 | free(line_info->i); 175 | free(line_info); 176 | } 177 | 178 | Test(update, moving_cursor_to_endofline_when_matching_complete_from_current_session) { 179 | line_data* line_info = lineDataConstructor(8); 180 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 181 | 182 | string_array global_command_history = (string_array){.len = 0}; 183 | string_array* sessions_command_history = calloc(1, sizeof(string_array)); 184 | char** addr_one = calloc(2, sizeof(char*)); 185 | addr_one[0] = calloc(strlen("yeyeye") + 1, 1); 186 | strcpy(addr_one[0], "yeyeye"); 187 | addr_one[1] = calloc(strlen("test") + 1, 1); 188 | strcpy(addr_one[1], "test"); 189 | sessions_command_history->values = addr_one; 190 | sessions_command_history->len = 2; 191 | history_data* history_info = historyDataConstructor(sessions_command_history, global_command_history); 192 | coordinates* cursor_pos; 193 | 194 | // type something to test 195 | line_info->c = 't'; 196 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 197 | line_info->c = 'e'; 198 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 199 | cr_expect(autocomplete_info->autocomplete == true); 200 | cr_expect(strcmp(autocomplete_info->possible_autocomplete, "test") == 0); 201 | 202 | line_info->c = ESCAPE; 203 | int fds[2]; 204 | if (pipe(fds) == 1) { 205 | perror("pipe"); 206 | exit(1); 207 | } 208 | dup2(fds[0], 0); 209 | close(fds[0]); 210 | write(fds[1], "ZC", sizeof("ZC")); // emulate left-arrow-press 211 | close(fds[1]); 212 | 213 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 214 | cr_expect(strcmp(line_info->line, "test") == 0); 215 | cr_expect(*line_info->i == strlen("test")); 216 | cr_expect(autocomplete_info->autocomplete == true); 217 | 218 | line_info->c = 'e'; 219 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 220 | cr_expect(autocomplete_info->autocomplete == false); 221 | cr_expect(strcmp(line_info->line, "teste") == 0); 222 | cr_expect(*line_info->i == strlen("teste")); 223 | 224 | free(autocomplete_info->possible_autocomplete); 225 | free(autocomplete_info); 226 | free(history_info); 227 | free(line_info->line); 228 | free(line_info->i); 229 | free(line_info); 230 | } 231 | 232 | Test(update, moving_cursor_to_endofline_when_matching_complete_from_global_history) { 233 | line_data* line_info = lineDataConstructor(8); 234 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 235 | 236 | string_array* sessions_command_history = calloc(1, sizeof(string_array)); 237 | char** addr_one = calloc(1, sizeof(char*)); 238 | char** addr_two = calloc(1, sizeof(char*)); 239 | addr_one[0] = calloc(strlen("test") + 1, 1); 240 | strcpy(addr_one[0], "test"); 241 | addr_two[0] = calloc(strlen("tig") + 1, 1); 242 | strcpy(addr_two[0], "tig"); 243 | sessions_command_history->values = addr_one; 244 | sessions_command_history->len = 1; 245 | string_array global_command_history = {.values = addr_two, .len = 1}; 246 | history_data* history_info = historyDataConstructor(sessions_command_history, global_command_history); 247 | coordinates* cursor_pos; 248 | 249 | // type something to test 250 | line_info->c = 't'; 251 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 252 | line_info->c = 'e'; 253 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 254 | cr_expect(autocomplete_info->autocomplete == true); 255 | cr_expect(strcmp(autocomplete_info->possible_autocomplete, "test") == 0); 256 | 257 | line_info->c = BACKSPACE; 258 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 259 | cr_expect(strcmp(line_info->line, "t") == 0); 260 | cr_expect(*line_info->i == strlen("t")); 261 | cr_expect(autocomplete_info->autocomplete == true); 262 | 263 | line_info->c = 'i'; 264 | update(line_info, autocomplete_info, history_info, (coordinates){20, 40}, (string_array){0, NULL}, cursor_pos); 265 | cr_expect(autocomplete_info->autocomplete == true); 266 | cr_expect(strcmp(autocomplete_info->possible_autocomplete, "tig") == 0); 267 | cr_expect(strcmp(line_info->line, "ti") == 0); 268 | cr_expect(*line_info->i == strlen("ti")); 269 | 270 | free(autocomplete_info->possible_autocomplete); 271 | free(autocomplete_info); 272 | free(history_info); 273 | free(line_info->line); 274 | free(line_info->i); 275 | free(line_info); 276 | } 277 | 278 | Test(Concatenating_command_history, check_concat_len) { 279 | char* one = "one"; 280 | char* two = "two"; 281 | char* addr_one[] = {one, two}; 282 | 283 | char* three = "three"; 284 | char* four = "four"; 285 | char* addr_two[] = {three, four}; 286 | 287 | string_array arr1 = {.len = 2, .values = addr_one}; 288 | 289 | string_array arr2 = {.len = 2, .values = addr_two}; 290 | string_array result = concatenateArrays(arr1, arr2); 291 | 292 | cr_expect(result.len == 4); 293 | } 294 | 295 | Test(Concatenating_command_history, check_concat_elements) { 296 | char* one = "one"; 297 | char* two = "two"; 298 | char* addr_one[] = {one, two}; 299 | 300 | char* three = "three"; 301 | char* four = "four"; 302 | char* addr_two[] = {three, four}; 303 | 304 | string_array arr1 = {.len = 2, .values = addr_one}; 305 | 306 | string_array arr2 = {.len = 2, .values = addr_two}; 307 | string_array result = concatenateArrays(arr1, arr2); 308 | char* concat[] = {one, two, three, four}; 309 | string_array correct = {.values = concat, .len = 4}; 310 | 311 | bool works = true; 312 | for (int i = 0; i < correct.len; i++) { 313 | if (strcmp(correct.values[i], result.values[i]) != 0) { 314 | works = false; 315 | } 316 | } 317 | cr_expect(works == true); 318 | } 319 | -------------------------------------------------------------------------------- /src/readline.c: -------------------------------------------------------------------------------- 1 | #include "readline.h" 2 | 3 | line_data* lineDataConstructor(int directory_len) { 4 | line_data* line_info = calloc(1, sizeof(line_data)); 5 | *line_info = (line_data){ 6 | .line = calloc(BUFFER, sizeof(char)), 7 | .i = calloc(1, sizeof(int)), 8 | .prompt_len = directory_len + 4, 9 | .line_row_count_with_autocomplete = 0, 10 | .cursor_row = 0, 11 | .size = BUFFER * sizeof(char), 12 | }; 13 | *line_info->i = 0; 14 | 15 | return line_info; 16 | } 17 | 18 | autocomplete_data* autocompleteDataConstructor() { 19 | autocomplete_data* autocomplete_info = calloc(1, sizeof(autocomplete_data)); 20 | *autocomplete_info = (autocomplete_data){ 21 | .possible_autocomplete = calloc(BUFFER, sizeof(char)), 22 | .autocomplete = false, 23 | .size = BUFFER * sizeof(char), 24 | }; 25 | 26 | return autocomplete_info; 27 | } 28 | 29 | history_data* historyDataConstructor(string_array* command_history, string_array global_command_history) { 30 | history_data* history_info = calloc(1, sizeof(history_data)); 31 | *history_info = (history_data){ 32 | .history_index = 0, 33 | .sessions_command_history = *command_history, 34 | .global_command_history = global_command_history, 35 | }; 36 | 37 | return history_info; 38 | } 39 | 40 | string_array concatenateArrays(const string_array one, const string_array two) { 41 | if (one.len == 0 && two.len == 0) 42 | return (string_array){.len = 0, .values = NULL}; 43 | string_array concatenated = {.values = calloc((one.len + two.len), sizeof(char*))}; 44 | int i = 0; 45 | 46 | for (int k = 0; k < one.len; k++) { 47 | concatenated.values[i] = calloc(strlen(one.values[k]) + 1, sizeof(char)); 48 | strcpy(concatenated.values[i], one.values[k]); 49 | i++; 50 | } 51 | for (int j = 0; j < two.len; j++) { 52 | concatenated.values[i] = calloc(strlen(two.values[j]) + 1, sizeof(char)); 53 | strcpy(concatenated.values[i], two.values[j]); 54 | i++; 55 | } 56 | concatenated.len = i; 57 | 58 | return concatenated; 59 | } 60 | 61 | bool wildcardInCompletion(token_index_arr token, int line_index) { 62 | int current_token_index = 0; 63 | for (int i = 0; i < token.len; i++) { 64 | if (line_index >= token.arr[i].start && line_index <= token.arr[i].end) { 65 | current_token_index = i; 66 | } 67 | } 68 | for (int j = current_token_index; j > 0; j--) { 69 | if (token.arr[j].token == WHITESPACE) { 70 | break; 71 | } else if (token.arr[j].token == ASTERISK || token.arr[j].token == QUESTION) { 72 | return true; 73 | } 74 | } 75 | return false; 76 | } 77 | 78 | int firstNonWhitespaceToken(token_index_arr token_line) { 79 | for (int i = 0; i < token_line.len; i++) { 80 | if (token_line.arr[i].token != WHITESPACE) { 81 | return token_line.arr[i].start; 82 | } 83 | } 84 | return INT_MAX; 85 | } 86 | 87 | bool tabCompBeforeFirstWord(token_index_arr tokenized_line, int line_index) { 88 | return tokenized_line.len > 0 && line_index <= firstNonWhitespaceToken(tokenized_line); 89 | } 90 | 91 | void tab(line_data* line_info, coordinates* cursor_pos, string_array PATH_BINS, coordinates terminal_size) { 92 | if (strlen(line_info->line) <= 0) 93 | return; 94 | 95 | token_index_arr tokenized_line = tokenizeLine(line_info->line); 96 | 97 | /* this should also get token when at end of word */ 98 | token_index current_token = getCurrentToken(*line_info->i, tokenized_line); 99 | if (current_token.token != CMD && wildcardInCompletion(tokenized_line, *line_info->i)) { 100 | return; 101 | } 102 | 103 | if (!tabCompBeforeFirstWord(tokenized_line, *line_info->i) && 104 | tabLoop(line_info, cursor_pos, PATH_BINS, terminal_size, current_token)) { 105 | // successful completion 106 | free(tokenized_line.arr); 107 | tokenized_line = tokenizeLine(line_info->line); 108 | current_token = 109 | getCurrentToken(*line_info->i + 1, tokenized_line); /* doesnt recognize token when at end of word*/ 110 | *line_info->i = current_token.end; 111 | line_info->c = -1; 112 | } 113 | free(tokenized_line.arr); 114 | } 115 | 116 | void upArrowPress(char* line, history_data* history_info) { 117 | if (history_info->history_index < history_info->sessions_command_history.len) { 118 | history_info->history_index += 1; 119 | memset(line, 0, strlen(line)); 120 | strcpy(line, history_info->sessions_command_history.values[history_info->history_index - 1]); 121 | }; 122 | } 123 | 124 | void downArrowPress(char* line, history_data* history_info) { 125 | if (history_info->history_index > 1) { 126 | history_info->history_index -= 1; 127 | memset(line, 0, strlen(line)); 128 | strcpy(line, history_info->sessions_command_history.values[history_info->history_index - 1]); 129 | 130 | } else if (history_info->history_index > 0) { 131 | history_info->history_index -= 1; 132 | memset(line, 0, strlen(line)); 133 | }; 134 | } 135 | 136 | bool typedLetter(line_data* line_info) { 137 | bool cursor_moved = false; 138 | if ((line_info->c < 27 || line_info->c > 127) || (strlen(line_info->line) == 0 && line_info->c == TAB)) { 139 | return false; 140 | } else if ((strlen(line_info->line) * sizeof(char) + 1) >= line_info->size) { 141 | char* tmp; 142 | if ((tmp = realloc(line_info->line, 1.5 * line_info->size)) == NULL) { 143 | perror("psh:"); 144 | } else { 145 | line_info->line = tmp; 146 | line_info->size *= 1.5; 147 | } 148 | } 149 | 150 | if (*line_info->i == strlen(line_info->line)) { 151 | (line_info->line)[*line_info->i] = line_info->c; 152 | (line_info->line)[*line_info->i + 1] = '\0'; 153 | cursor_moved = true; 154 | } else if (insertCharAtPos(line_info->line, *line_info->i, line_info->c)) { 155 | cursor_moved = true; 156 | } 157 | 158 | return cursor_moved; 159 | } 160 | 161 | void arrowPress(line_data* line_info, history_data* history_info, autocomplete_data* autocomplete_info) { 162 | getch(); 163 | int value = getch(); 164 | switch (value) { 165 | case 'A': 166 | upArrowPress(line_info->line, history_info); 167 | 168 | *line_info->i = strlen(line_info->line); 169 | break; 170 | 171 | case 'B': 172 | downArrowPress(line_info->line, history_info); 173 | 174 | *line_info->i = strlen(line_info->line); 175 | break; 176 | 177 | case 'C': { // right-arrow 178 | if (autocomplete_info->autocomplete && 179 | strncmp(line_info->line, autocomplete_info->possible_autocomplete, strlen(line_info->line)) == 0) { 180 | memset(line_info->line, 0, strlen(line_info->line)); 181 | if (((strlen(autocomplete_info->possible_autocomplete) + 1) * sizeof(char)) >= line_info->size) { 182 | line_info->line = 183 | realloc(line_info->line, (strlen(autocomplete_info->possible_autocomplete) + 1) * sizeof(char)); 184 | line_info->size = (strlen(autocomplete_info->possible_autocomplete) + 1) * sizeof(char); 185 | } 186 | strcpy(line_info->line, autocomplete_info->possible_autocomplete); 187 | *line_info->i = strlen(line_info->line); 188 | } else { 189 | *line_info->i = (*line_info->i < strlen(line_info->line)) ? *line_info->i + 1 : *line_info->i; 190 | } 191 | break; 192 | } 193 | 194 | case 'D': { // left-arrow 195 | *line_info->i = (*line_info->i > 0) ? (*line_info->i) - 1 : *line_info->i; 196 | break; 197 | } 198 | } 199 | } 200 | 201 | void ctrlFPress(string_array all_time_command_history, coordinates terminal_size, coordinates* cursor_pos, 202 | line_data* line_info) { 203 | fuzzy_result popup_result = popupFuzzyFinder(all_time_command_history, terminal_size, cursor_pos->y, 204 | line_info->line_row_count_with_autocomplete); 205 | 206 | if (strcmp(popup_result.line, "") != 0) { 207 | if (((strlen(popup_result.line) + 1) * sizeof(char)) >= line_info->size) { 208 | line_info->line = realloc(line_info->line, (strlen(popup_result.line) + 1) * sizeof(char)); 209 | line_info->size = (strlen(popup_result.line) + 1) * sizeof(char); 210 | } 211 | strcpy(line_info->line, popup_result.line); 212 | *line_info->i = strlen(line_info->line); 213 | } 214 | free(popup_result.line); 215 | 216 | if (popup_result.shifted) { 217 | cursor_pos->y = (terminal_size.y * 0.85) - 3 - line_info->line_row_count_with_autocomplete; 218 | moveCursor(*cursor_pos); 219 | } else { 220 | moveCursor(*cursor_pos); 221 | } 222 | } 223 | 224 | bool filterHistoryForMatchingAutoComplete(const string_array all_time_commands, char* line, 225 | autocomplete_data* autocomplete_info) { 226 | 227 | for (int i = 0; i < all_time_commands.len; i++) { 228 | if (strlen(line) > 0 && (strncmp(line, all_time_commands.values[i], strlen(line)) == 0)) { 229 | if (strlen(all_time_commands.values[i]) >= autocomplete_info->size) { 230 | autocomplete_info->possible_autocomplete = realloc( 231 | autocomplete_info->possible_autocomplete, (strlen(all_time_commands.values[i]) + 1) * sizeof(char)); 232 | autocomplete_info->size = strlen(all_time_commands.values[i]) + 1; 233 | } 234 | strcpy(autocomplete_info->possible_autocomplete, all_time_commands.values[i]); 235 | 236 | return true; 237 | } 238 | } 239 | 240 | return false; 241 | } 242 | 243 | bool update(line_data* line_info, autocomplete_data* autocomplete_info, history_data* history_info, 244 | coordinates terminal_size, string_array PATH_BINS, coordinates* cursor_pos) { 245 | 246 | string_array all_time_command_history = 247 | concatenateArrays(history_info->sessions_command_history, history_info->global_command_history); 248 | bool loop = true; 249 | 250 | if (line_info->c == TAB) { 251 | tab(line_info, cursor_pos, PATH_BINS, terminal_size); 252 | } 253 | if (line_info->c == BACKSPACE) { 254 | backspaceLogic(line_info->line, line_info->i); 255 | } else if (line_info->c == ESCAPE) { 256 | arrowPress(line_info, history_info, autocomplete_info); 257 | } else if (line_info->c == '\n') { 258 | free_string_array(&all_time_command_history); 259 | return false; 260 | } else if ((int)line_info->c == CONTROL_F) { 261 | ctrlFPress(all_time_command_history, terminal_size, cursor_pos, line_info); 262 | } else if (line_info->c != -1 && typedLetter(line_info)) { 263 | (*line_info->i)++; 264 | } 265 | autocomplete_info->autocomplete = 266 | filterHistoryForMatchingAutoComplete(all_time_command_history, line_info->line, autocomplete_info); 267 | int line_len = (autocomplete_info->autocomplete) ? strlen(autocomplete_info->possible_autocomplete) 268 | : strlen(line_info->line); 269 | line_info->line_row_count_with_autocomplete = calculateRowCount(terminal_size, line_info->prompt_len, line_len); 270 | line_info->cursor_row = calculateRowCount(terminal_size, line_info->prompt_len, *line_info->i); 271 | 272 | free_string_array(&all_time_command_history); 273 | 274 | return loop; 275 | } 276 | 277 | bool shiftLineIfOverlap(int current_cursor_height, int terminal_height, int line_row_count_with_autocomplete) { 278 | if ((current_cursor_height + line_row_count_with_autocomplete) < terminal_height) 279 | return false; 280 | 281 | for (int i = terminal_height; i < (current_cursor_height + line_row_count_with_autocomplete); i++) { 282 | printf("\n"); 283 | } 284 | return true; 285 | } 286 | 287 | void stringToLower(char* string) { 288 | for (int i = 0; i < strlen(string); i++) { 289 | string[i] = tolower(string[i]); 290 | } 291 | } 292 | 293 | bool isInPath(char* line, string_array PATH_BINS) { 294 | bool result = false; 295 | char* line_copy = calloc(strlen(line) + 1, sizeof(char)); 296 | strcpy(line_copy, line); 297 | stringToLower(line_copy); 298 | 299 | char* bin_copy; 300 | for (int i = 0; i < PATH_BINS.len; i++) { 301 | bin_copy = calloc(strlen(PATH_BINS.values[i]) + 1, sizeof(char)); 302 | strcpy(bin_copy, PATH_BINS.values[i]); 303 | stringToLower(bin_copy); 304 | 305 | if (strcmp(bin_copy, line_copy) == 0) { 306 | result = true; 307 | } 308 | free(bin_copy); 309 | } 310 | free(line_copy); 311 | return result; 312 | } 313 | 314 | void printTokenizedLine(line_data line_info, token_index_arr tokenized_line, builtins_array BUILTINS, 315 | string_array PATH_BINS) { 316 | for (int i = 0; i < tokenized_line.len; i++) { 317 | int token_start = tokenized_line.arr[i].start; 318 | int token_end = tokenized_line.arr[i].end; 319 | char* substring = calloc(token_end - token_start + 1, sizeof(char)); 320 | strncpy(substring, &line_info.line[token_start], token_end - token_start); 321 | 322 | switch (tokenized_line.arr[i].token) { 323 | case (AMP_CMD): 324 | case (PIPE_CMD): 325 | case (CMD): { 326 | bool in_path = isInPath(substring, PATH_BINS); 327 | bool is_builtin = isBuiltin(substring, BUILTINS) != -1 ? true : false; 328 | bool is_exec = isExec(substring); 329 | in_path || is_builtin || is_exec ? printColor(substring, GREEN, standard) : printColor(substring, RED, bold); 330 | break; 331 | } 332 | case (ARG): { 333 | char* copy_sub = calloc(strlen(substring) + 1, sizeof(char)); 334 | strcpy(copy_sub, substring); 335 | replaceAliasesString(©_sub); 336 | removeEscapesString(©_sub); 337 | 338 | autocomplete_array autocomplete = fileComp(copy_sub); 339 | if (autocomplete.array.len > 0 && !wildcardInCompletion(tokenized_line, *line_info.i)) { 340 | printColor(substring, WHITE, underline); 341 | } else if (substring[0] == '\'' && substring[strlen(substring) - 1] == '\'') { 342 | printColor(substring, YELLOW, standard); 343 | } else { 344 | printf("%s", substring); 345 | } 346 | free(copy_sub); 347 | free_string_array(&(autocomplete.array)); 348 | break; 349 | } 350 | case (WHITESPACE): { 351 | printf("%s", substring); 352 | break; 353 | } 354 | case (GREAT): 355 | case (GREATGREAT): 356 | case (LESS): 357 | case (AMP_GREAT): 358 | case (AMP_GREATGREAT): 359 | case (PIPE): 360 | case (AMPAMP): { 361 | printColor(substring, YELLOW, standard); 362 | break; 363 | } 364 | case (QUESTION): 365 | case (ASTERISK): { 366 | printColor(substring, BLUE, bold); 367 | break; 368 | } 369 | default: { 370 | fprintf(stderr, "psh: invalid input\n"); 371 | break; 372 | } 373 | } 374 | free(substring); 375 | } 376 | } 377 | 378 | void printLine(line_data line_info, builtins_array BUILTINS, string_array PATH_BINS) { 379 | token_index_arr tokenized_line = tokenizeLine(line_info.line); 380 | printTokenizedLine(line_info, tokenized_line, BUILTINS, PATH_BINS); 381 | free(tokenized_line.arr); 382 | } 383 | 384 | void render(line_data* line_info, autocomplete_data* autocomplete_info, const string_array PATH_BINS, 385 | char* directories, coordinates* starting_cursor_pos, coordinates terminal_size, 386 | builtins_array BUILTINS) { 387 | if (shiftLineIfOverlap(starting_cursor_pos->y, terminal_size.y, line_info->line_row_count_with_autocomplete)) { 388 | starting_cursor_pos->y -= 389 | (starting_cursor_pos->y + line_info->line_row_count_with_autocomplete) - terminal_size.y; 390 | } 391 | moveCursor(*starting_cursor_pos); 392 | 393 | CLEAR_LINE; 394 | CLEAR_BELOW_CURSOR; 395 | printf("\r"); 396 | printPrompt(directories, CYAN); 397 | 398 | if (strlen(line_info->line) > 0) { 399 | printLine(*line_info, BUILTINS, PATH_BINS); 400 | 401 | if (autocomplete_info->autocomplete) { 402 | printf("%s", &autocomplete_info->possible_autocomplete[strlen(line_info->line)]); 403 | } 404 | } 405 | coordinates new_cursor_pos = calculateCursorPos( 406 | terminal_size, (coordinates){.x = 0, .y = starting_cursor_pos->y}, line_info->prompt_len, *line_info->i); 407 | 408 | moveCursor(new_cursor_pos); 409 | starting_cursor_pos->x = new_cursor_pos.x; 410 | } 411 | 412 | char* readLine(string_array PATH_BINS, char* directories, string_array* command_history, 413 | const string_array global_command_history, builtins_array BUILTINS) { 414 | 415 | const coordinates terminal_size = getTerminalSize(); 416 | line_data* line_info = lineDataConstructor(strlen(directories)); 417 | autocomplete_data* autocomplete_info = autocompleteDataConstructor(); 418 | history_data* history_info = historyDataConstructor(command_history, global_command_history); 419 | coordinates* cursor_pos = calloc(1, sizeof(coordinates)); 420 | *cursor_pos = getCursorPos(); 421 | int loop = true; 422 | 423 | while (loop && (line_info->c = getch()) != -1) { 424 | loop = update(line_info, autocomplete_info, history_info, terminal_size, PATH_BINS, cursor_pos); 425 | 426 | render(line_info, autocomplete_info, PATH_BINS, directories, cursor_pos, terminal_size, BUILTINS); 427 | } 428 | 429 | moveCursor((coordinates){ 430 | 1000, cursor_pos->y + calculateRowCount(terminal_size, line_info->prompt_len, strlen(line_info->line))}); 431 | 432 | free(autocomplete_info->possible_autocomplete); 433 | free(autocomplete_info); 434 | free(history_info); 435 | free(cursor_pos); 436 | char* result = calloc(strlen(line_info->line) + 1, sizeof(char)); 437 | strcpy(result, line_info->line); 438 | free(line_info->line); 439 | free(line_info->i); 440 | free(line_info); 441 | 442 | printf("\n"); 443 | return result; 444 | } 445 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include 3 | 4 | int getch() { 5 | struct termios oldattr, newattr; 6 | int ch; 7 | tcgetattr(STDIN_FILENO, &oldattr); 8 | newattr = oldattr; 9 | newattr.c_lflag &= ~(ICANON | ECHO); 10 | tcsetattr(STDIN_FILENO, TCSANOW, &newattr); 11 | ch = getchar(); 12 | tcsetattr(STDIN_FILENO, TCSANOW, &oldattr); 13 | return ch; 14 | } 15 | 16 | void printColor(const char* string, color color, enum color_decorations color_decorations) { 17 | char command[13]; 18 | 19 | sprintf(command, "%c[%d;%d;%dm", 0x1B, color_decorations, color.fg, color.bg); 20 | printf("%s", command); 21 | printf("%s", string); 22 | 23 | sprintf(command, "%c[%d;%d;%dm", 0x1B, 0, 37, 10); 24 | printf("%s", command); 25 | } 26 | 27 | bool isOnlyDelimeter(const char* string, char delimeter) { 28 | for (int i = 0; i < strlen(string); i++) { 29 | if (string[i] != delimeter) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | 36 | char* removeMultipleWhitespaces(char* string) { 37 | int i = 0; 38 | for (; i < strlen(string) && string[i] == ' '; i++) 39 | ; 40 | char* new_string = calloc(strlen(string) + 1, sizeof(char)); 41 | strcpy(new_string, &string[i]); 42 | free(string); 43 | bool is_first = true; 44 | for (int j = 0; j < strlen(new_string);) { 45 | if (new_string[j] == ' ' && is_first) { 46 | is_first = false; 47 | j++; 48 | } else if (new_string[j] == ' ' && !is_first) { 49 | new_string = removeCharAtPos(new_string, j); 50 | } else { 51 | is_first = true; 52 | j++; 53 | } 54 | } 55 | return new_string; 56 | } 57 | 58 | void free_string_array(string_array* arr) { 59 | if (arr->values == NULL) 60 | return; 61 | for (int i = 0; i < arr->len; i++) { 62 | free(arr->values[i]); 63 | arr->values[i] = NULL; 64 | } 65 | free(arr->values); 66 | arr->values = NULL; 67 | } 68 | 69 | void moveCursor(coordinates new_pos) { printf("\033[%d;%dH", new_pos.y, new_pos.x); } 70 | 71 | coordinates getTerminalSize() { 72 | coordinates size; 73 | struct winsize w; 74 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); 75 | 76 | size.x = w.ws_col; 77 | size.y = w.ws_row; 78 | 79 | return size; 80 | } 81 | 82 | bool inArray(char* value, string_array array) { 83 | for (int i = 0; i < array.len; i++) { 84 | if (strcmp(value, array.values[i]) == 0) { 85 | return true; 86 | } 87 | } 88 | return false; 89 | } 90 | 91 | string_array removeDuplicates(string_array* matching_commands) { 92 | int j = 0; 93 | string_array no_dup_array; 94 | no_dup_array.values = calloc(matching_commands->len, sizeof(char*)); 95 | no_dup_array.len = 0; 96 | 97 | for (int i = 0; i < matching_commands->len; i++) { 98 | if (!inArray(matching_commands->values[i], no_dup_array)) { 99 | no_dup_array.values[j] = calloc(strlen(matching_commands->values[i]) + 1, sizeof(char)); 100 | strcpy(no_dup_array.values[j], matching_commands->values[i]); 101 | no_dup_array.len += 1; 102 | j++; 103 | } 104 | } 105 | free_string_array(matching_commands); 106 | 107 | return no_dup_array; 108 | } 109 | 110 | char* removeCharAtPos(char* line, int x_pos) { 111 | for (int i = x_pos - 1; i < strlen(line); i++) { 112 | line[i] = line[i + 1]; 113 | } 114 | return line; 115 | } 116 | 117 | void backspaceLogic(char* line, int* i) { 118 | if (strlen(line) > 0 && *i >= 0) { 119 | line = removeCharAtPos(line, *i); 120 | 121 | if (*i > 0) { 122 | *i -= 1; 123 | } 124 | } 125 | } 126 | 127 | void logger(enum logger_type type, void* message) { 128 | FILE* logfile = fopen("log.txt", "a"); 129 | 130 | switch (type) { 131 | case integer: { 132 | fprintf(logfile, "%d", *((int*)message)); 133 | break; 134 | } 135 | case string: { 136 | fprintf(logfile, "%s", (char*)message); 137 | break; 138 | } 139 | case character: { 140 | fprintf(logfile, "%c", *(char*)message); 141 | break; 142 | } 143 | default: { 144 | break; 145 | } 146 | } 147 | fclose(logfile); 148 | } 149 | 150 | coordinates getCursorPos() { 151 | char buf[30] = {0}; 152 | int ret, i, pow; 153 | char ch; 154 | coordinates cursor_pos = {.x = 0, .y = 0}; 155 | 156 | struct termios term, restore; 157 | 158 | tcgetattr(0, &term); 159 | tcgetattr(0, &restore); 160 | term.c_lflag &= ~(ICANON | ECHO); 161 | tcsetattr(0, TCSANOW, &term); 162 | 163 | write(1, "\033[6n", 4); 164 | 165 | for (i = 0, ch = 0; ch != 'R'; i++) { 166 | ret = read(0, &ch, 1); 167 | if (!ret) { 168 | tcsetattr(0, TCSANOW, &restore); 169 | fprintf(stderr, "psh: error parsing cursor-position\n"); 170 | return cursor_pos; 171 | } 172 | buf[i] = ch; 173 | } 174 | 175 | if (i < 2) { 176 | tcsetattr(0, TCSANOW, &restore); 177 | return cursor_pos; 178 | } 179 | 180 | for (i -= 2, pow = 1; buf[i] != ';'; i--, pow *= 10) 181 | cursor_pos.x += (buf[i] - '0') * pow; 182 | 183 | for (i--, pow = 1; buf[i] != '['; i--, pow *= 10) 184 | cursor_pos.y += (buf[i] - '0') * pow; 185 | 186 | tcsetattr(0, TCSANOW, &restore); 187 | return cursor_pos; 188 | } 189 | 190 | string_array getAllFilesInDir(string_array* directory_array) { 191 | struct dirent* file; 192 | string_array all_path_files; 193 | char** files = calloc(BUFFER, sizeof(char*)); 194 | int j = 0; 195 | size_t size = BUFFER * sizeof(char*); 196 | 197 | for (int i = 0; i < directory_array->len; i++) { 198 | if (isDirectory(directory_array->values[i])) { 199 | DIR* dr = opendir(directory_array->values[i]); 200 | 201 | while ((file = readdir(dr)) != NULL) { 202 | if ((j * sizeof(char*)) >= size) { 203 | char** tmp; 204 | if ((tmp = realloc(files, size * 1.5)) == NULL) { 205 | perror("realloc"); 206 | } else { 207 | files = tmp; 208 | size *= 1.5; 209 | } 210 | } 211 | files[j] = (char*)calloc(strlen(file->d_name) + 1, sizeof(char)); 212 | strcpy(files[j], file->d_name); 213 | j++; 214 | } 215 | closedir(dr); 216 | } 217 | } 218 | all_path_files.values = files; 219 | all_path_files.len = j; 220 | 221 | // free_string_array(directory_array); 222 | return all_path_files; 223 | } 224 | 225 | string_array filterMatching(char* line, const string_array PATH_BINS) { 226 | int buf_size = 64; 227 | size_t size = buf_size * sizeof(char*); 228 | char** matching_binaries = calloc(buf_size, sizeof(char*)); 229 | string_array result; 230 | int j = 0; 231 | 232 | for (int i = 0; i < PATH_BINS.len; i++) { 233 | if (strncmp(PATH_BINS.values[i], line, strlen(line)) == 0) { 234 | if ((j * sizeof(char*)) >= size) { 235 | char** tmp; 236 | if ((tmp = realloc(matching_binaries, size * 1.5)) == NULL) { 237 | perror("realloc"); 238 | } else { 239 | matching_binaries = tmp; 240 | size *= 1.5; 241 | } 242 | } 243 | matching_binaries[j] = calloc(strlen(PATH_BINS.values[i]) + 1, sizeof(char)); 244 | strcpy(matching_binaries[j], PATH_BINS.values[i]); 245 | j++; 246 | } 247 | } 248 | result.values = matching_binaries; 249 | result.len = j; 250 | 251 | return result; 252 | } 253 | 254 | string_array getAllMatchingFiles(char* current_dir_sub, char* removed_sub) { 255 | char* temp_sub = calloc(strlen(current_dir_sub) + 1, sizeof(char)); 256 | strcpy(temp_sub, current_dir_sub); 257 | 258 | string_array current_dir_array = {.len = 1, .values = &temp_sub}; 259 | string_array all_files_in_dir = getAllFilesInDir(¤t_dir_array); 260 | string_array filtered = filterMatching(removed_sub, all_files_in_dir); 261 | 262 | free_string_array(&all_files_in_dir); 263 | free(temp_sub); 264 | 265 | return filtered; 266 | } 267 | 268 | // doesnt allocate necessary memory 269 | bool insertCharAtPos(char* line, int index, char c) { 270 | int len = strlen(line); 271 | if (index >= 0 && index <= strlen(line)) { 272 | for (int i = strlen(line) - 1; i >= index; i--) { 273 | line[i + 1] = line[i]; 274 | } 275 | line[index] = c; 276 | line[len + 1] = '\0'; 277 | } else { 278 | return false; 279 | } 280 | return true; 281 | } 282 | 283 | void insertStringAtPos(char** line, char* insert_string, int position) { 284 | if (strcmp(insert_string, "") == 0) 285 | return; 286 | char* tmp; 287 | if ((tmp = realloc(*line, (strlen(*line) + strlen(insert_string) + 2) * sizeof(char))) == NULL) { 288 | perror("realloc"); 289 | } else { 290 | *line = tmp; 291 | } 292 | insertCharAtPos(*line, position, '%'); 293 | insertCharAtPos(*line, position + 1, 's'); 294 | 295 | char* new_line = calloc(strlen(*line) + strlen(insert_string) + 1, sizeof(char)); 296 | sprintf(new_line, *line, insert_string); 297 | strcpy(*line, new_line); 298 | free(new_line); 299 | } 300 | 301 | int isDirectory(const char* path) { 302 | struct stat statbuf; 303 | if (stat(path, &statbuf) != 0) 304 | return 0; 305 | 306 | return S_ISDIR(statbuf.st_mode); 307 | } 308 | 309 | string_array copyStringArray(string_array arr) { 310 | string_array copy = {.len = arr.len, .values = calloc(arr.len, sizeof(char*))}; 311 | for (int i = 0; i < arr.len; i++) { 312 | copy.values[i] = calloc(strlen(arr.values[i]) + 1, sizeof(char)); 313 | strcpy(copy.values[i], arr.values[i]); 314 | } 315 | 316 | return copy; 317 | } 318 | 319 | file_string_tuple getFileStrings(char* current_word, char* current_path) { 320 | char* current_dir; 321 | char* removed_sub; 322 | 323 | switch (current_word[0]) { 324 | case '/': { 325 | current_dir = current_word; 326 | if (strlen(current_dir) == 1) { 327 | removed_sub = ""; 328 | } else { 329 | removed_sub = &(current_dir[strlen(current_dir) - getAppendingIndex(current_dir, '/')]); // c_e 330 | } 331 | break; 332 | } 333 | case '~': { 334 | char* home_path = getenv("HOME"); // Users/username 335 | strcpy(current_path, home_path); 336 | 337 | current_dir = strcat(current_path, &(current_word[1])); // Users/username/documents 338 | removed_sub = &(current_dir[strlen(current_dir) - getAppendingIndex(current_dir, '/')]); // documents 339 | break; 340 | } 341 | default: { 342 | current_dir = strcat(current_path, current_word); // documents/coding/c_e 343 | removed_sub = &(current_dir[strlen(current_dir) - getAppendingIndex(current_dir, '/')]); // c_e 344 | break; 345 | } 346 | } 347 | 348 | return (file_string_tuple){.removed_sub = removed_sub, .current_dir = current_dir}; 349 | } 350 | 351 | int getAppendingIndex(char* line, char delimeter) { 352 | int j = 0; 353 | for (int i = strlen(line) - 1; i >= 0; i--) { 354 | if (line[i] == delimeter) 355 | return j; 356 | j++; 357 | } 358 | return -1; 359 | } 360 | 361 | void fileDirArray(string_array* filtered, char* current_dir_sub) { 362 | char* current_dir_sub_copy = 363 | calloc(strlen(current_dir_sub) + getLongestWordInArray(*filtered) + 2, sizeof(char)); 364 | char* temp; 365 | char copy[strlen(current_dir_sub) + getLongestWordInArray(*filtered) + 2]; 366 | strcat(current_dir_sub, "/"); 367 | 368 | for (int i = 0; i < filtered->len; i++) { 369 | strcpy(current_dir_sub_copy, current_dir_sub); 370 | 371 | temp = strcpy(copy, strcat(current_dir_sub_copy, filtered->values[i])); 372 | 373 | if (isDirectory(temp)) { 374 | filtered->values[i] = realloc(filtered->values[i], strlen(filtered->values[i]) + 2); 375 | filtered->values[i][strlen(filtered->values[i]) + 1] = '\0'; 376 | filtered->values[i][strlen(filtered->values[i])] = '/'; 377 | } 378 | memset(copy, 0, strlen(copy)); 379 | memset(temp, 0, strlen(temp)); 380 | memset(current_dir_sub_copy, 0, strlen(current_dir_sub_copy)); 381 | } 382 | free(current_dir_sub_copy); 383 | free(current_dir_sub); 384 | } 385 | 386 | int getCurrentWordPosInLine(string_array command_line, char* word) { 387 | for (int i = 0; i < command_line.len; i++) { 388 | if (strncmp(command_line.values[i], word, strlen(word)) == 0) { 389 | return i; 390 | } 391 | } 392 | 393 | return -1; 394 | } 395 | 396 | int countWhitespace(char* string) { 397 | int result = 0; 398 | for (int i = 0; i < strlen(string); i++) { 399 | if (string[i] == ' ') 400 | result++; 401 | } 402 | return result; 403 | } 404 | 405 | autocomplete_array fileComp(char* current_word) { 406 | char cd[PATH_MAX]; 407 | file_string_tuple file_strings = getFileStrings(current_word, strcat(getcwd(cd, sizeof(cd)), "/")); 408 | 409 | char* current_dir_sub = calloc(strlen(file_strings.current_dir) + 2, sizeof(char)); 410 | strncpy(current_dir_sub, file_strings.current_dir, 411 | strlen(file_strings.current_dir) - getAppendingIndex(file_strings.current_dir, '/')); 412 | string_array filtered = getAllMatchingFiles(current_dir_sub, file_strings.removed_sub); 413 | 414 | fileDirArray(&filtered, current_dir_sub); 415 | // have to take escapes into account which offset the original appending_index 416 | int offset = countWhitespace(file_strings.removed_sub); 417 | 418 | return (autocomplete_array){.array.values = filtered.values, 419 | .array.len = filtered.len, 420 | .appending_index = strlen(file_strings.removed_sub) + offset}; 421 | } 422 | 423 | coordinates calculateCursorPos(coordinates terminal_size, coordinates cursor_pos, int prompt_len, int i) { 424 | int line_pos = i + prompt_len; 425 | 426 | if (line_pos < terminal_size.x) { 427 | return (coordinates){.x = line_pos, .y = cursor_pos.y}; 428 | } else if (line_pos % terminal_size.x == 0) { 429 | return (coordinates){.x = terminal_size.x, .y = cursor_pos.y + ((line_pos - 1) / terminal_size.x)}; 430 | } else { 431 | return (coordinates){.x = line_pos % terminal_size.x, .y = cursor_pos.y + (line_pos / terminal_size.x)}; 432 | } 433 | } 434 | 435 | int calculateRowCount(coordinates terminal_size, int prompt_len, int i) { 436 | return calculateCursorPos(terminal_size, (coordinates){0, 0}, prompt_len, i).y; 437 | } 438 | 439 | char* shortenIfTooLong(char* word, int terminal_width) { 440 | char* result = calloc(strlen(word) + 1, sizeof(char)); 441 | strcpy(result, word); 442 | if (strlen(word) > terminal_width - 2) { 443 | result[terminal_width - 2] = '\0'; 444 | for (int j = terminal_width - 3; j > (terminal_width - 6); j--) { 445 | result[j] = '.'; 446 | } 447 | } 448 | return result; 449 | } 450 | 451 | int firstNonDelimeterIndex(string_array splitted_line) { 452 | for (int i = 0; i < splitted_line.len; i++) { 453 | if (strcmp(splitted_line.values[i], "") != 0) { 454 | return i; 455 | } 456 | } 457 | return 0; 458 | } 459 | 460 | int getLongestWordInArray(const string_array array) { 461 | int longest = 0; 462 | int current_len = 0; 463 | 464 | for (int i = 0; i < array.len; i++) { 465 | current_len = strlen(array.values[i]); 466 | if (current_len > longest) { 467 | longest = current_len; 468 | } 469 | } 470 | 471 | return longest; 472 | } 473 | 474 | bool isExec(char* file) { 475 | if (access(file, F_OK | X_OK) == 0 && !isDirectory(file)) { 476 | return true; 477 | } 478 | return false; 479 | } 480 | 481 | token_index getCurrentToken(int line_index, token_index_arr tokenized_line) { 482 | // when current index greater than existant line len take last element 483 | token_index result = {.start = tokenized_line.arr[tokenized_line.len - 1].start, 484 | .end = tokenized_line.arr[tokenized_line.len - 1].end}; 485 | for (int i = 0; i < tokenized_line.len; i++) { 486 | if (line_index >= tokenized_line.arr[i].start && line_index <= tokenized_line.arr[i].end) { 487 | result = tokenized_line.arr[i]; 488 | break; 489 | } 490 | } 491 | return result; 492 | } 493 | 494 | void removeEscapesString(char** string) { 495 | for (int j = 0; j < strlen(*string); j++) { 496 | if ((*string)[j] == '\\') { 497 | *string = removeCharAtPos(*string, j + 1); 498 | } 499 | } 500 | } 501 | 502 | void removeSlice(char** line, int start, int end) { 503 | for (int i = start; i < end; i++) { 504 | *line = removeCharAtPos(*line, start + 1); 505 | } 506 | } 507 | 508 | int tokenCmp(const void* a, const void* b) { 509 | token_index cast_a = *(token_index*)a; 510 | token_index cast_b = *(token_index*)b; 511 | return cast_a.start - cast_b.start; 512 | } 513 | 514 | token_index_arr tokenizeLine(char* line) { 515 | char* copy = calloc(strlen(line) + 1, sizeof(char)); 516 | strcpy(copy, line); 517 | 518 | int retval = 0; 519 | regex_t re; 520 | regmatch_t rm[ENUM_LEN]; 521 | char* quoted_args = "(\'[^\']*\')"; 522 | char* whitespace = "(\\\\[ ])|([ ]+)"; 523 | char* filenames = "([12]?>{2}|[12]?>|<|&>|&>>)[\t]*([^\n\t><]+)"; 524 | char* redirection = "([12]?>{2})|([12]?>)|(<)|(&>)|(&>>)"; 525 | char* line_token = "^[\n\t]*([^\n\t\f|&&]+)|\\|[\t\n]*([^\n\t\f|&&]+)|(\\|)|(&&)|&&[" 526 | "\t\n]*([^\n\t\f|&&]+)"; 527 | char* wildcards = "(\\*)|(\\?)"; 528 | char* only_args = "([^\t\n\f]+)"; 529 | char* regexes[] = {quoted_args, whitespace, filenames, redirection, line_token, wildcards, only_args}; 530 | regex_loop_struct regex_info[] = {{.fill_char = '\n', .loop_start = 1, .token_index_inc = 13}, // quoted_args 531 | {.fill_char = '\t', .loop_start = 1, .token_index_inc = 9}, // whitespace 532 | {.fill_char = '\n', .loop_start = 2, .token_index_inc = 12}, // filenames(args) 533 | {.fill_char = '\n', .loop_start = 1, .token_index_inc = 5}, // redirection 534 | {.fill_char = '\f', .loop_start = 1, .token_index_inc = 0}, // line-token 535 | {.fill_char = '\t', .loop_start = 1, .token_index_inc = 11}, // wildcards 536 | {.fill_char = '\t', .loop_start = 1, .token_index_inc = 13}}; // args 537 | char* start; 538 | char* end; 539 | int j = 0; 540 | 541 | token_index* result_arr = calloc(strlen(line), sizeof(token_index)); 542 | 543 | for (int k = 0; k < (sizeof(regexes) / sizeof(regexes[0])); k++) { 544 | if (regcomp(&re, regexes[k], REG_EXTENDED) != 0) { 545 | perror("Error in compiling regex\n"); 546 | } 547 | if (k == 2) { 548 | // change back escaped whitespaces 549 | for (int l = 0; l < strlen(copy); l++) { 550 | if (copy[l] == '\f') { 551 | removeCharAtPos(copy, l + 1); 552 | removeCharAtPos(copy, l + 1); 553 | insertStringAtPos(©, "\\ ", l); 554 | } 555 | } 556 | } 557 | for (; j < strlen(copy); j++) { 558 | if ((retval = regexec(&re, copy, ENUM_LEN, rm, 0)) == 0) { 559 | for (int i = regex_info[k].loop_start; i < ENUM_LEN; i++) { 560 | if (rm[i].rm_so != -1) { 561 | start = copy + rm[i].rm_so; 562 | end = copy + rm[i].rm_eo; 563 | if (k == 1 && i == 1) { 564 | // matched escape-whitespace 565 | while (start < end) { 566 | *start++ = '\f'; // overwrite with random char to recognize in next regex and change back 567 | } 568 | j--; 569 | } else { 570 | result_arr[j].start = rm[i].rm_so; 571 | result_arr[j].end = rm[i].rm_eo; 572 | result_arr[j].token = i + regex_info[k].token_index_inc; 573 | while (start < end) { 574 | *start++ = regex_info[k].fill_char; // have to overwrite matches so they dont match again 575 | } 576 | } 577 | break; 578 | } 579 | } 580 | } else { 581 | break; 582 | } 583 | } 584 | regfree(&re); 585 | } 586 | free(copy); 587 | 588 | qsort(result_arr, j, sizeof(token_index), tokenCmp); 589 | token_index_arr result = {.arr = result_arr, .len = j}; 590 | 591 | return result; 592 | } 593 | 594 | int isBuiltin(char* command, builtins_array builtins) { 595 | for (int i = 0; i < builtins.len; i++) { 596 | if (strcmp(command, builtins.array[i].name) == 0) { 597 | return i; 598 | } 599 | } 600 | return -1; 601 | } 602 | 603 | void replaceAliasesString(char** line) { 604 | for (int j = 0; j < strlen(*line); j++) { 605 | if ((*line)[j] == '~') { 606 | char* home_path = getenv("HOME"); 607 | char* prior_line = calloc(strlen(*line) + 1, sizeof(char)); 608 | strcpy(prior_line, *line); 609 | removeCharAtPos(prior_line, j + 1); 610 | *line = realloc(*line, strlen(home_path) + strlen(*line) + 10); 611 | strcpy(*line, prior_line); 612 | insertStringAtPos(line, home_path, j); 613 | free(prior_line); 614 | } 615 | } 616 | } 617 | 618 | void printPrompt(const char* dir, color color) { 619 | printColor(dir, color, bold); 620 | printf(" "); 621 | printColor("\u2771 ", GREEN, standard); 622 | } 623 | -------------------------------------------------------------------------------- /tests/unit_tests/test_util.c: -------------------------------------------------------------------------------- 1 | #include "../../src/util.h" 2 | #include 3 | 4 | Test(removeMultipleWhitespace, removes_any_extraneous_whitespace) { 5 | char* line = calloc(64, sizeof(char)); 6 | strcpy(line, " uwe ist cool"); 7 | char* result = removeMultipleWhitespaces(line); 8 | cr_expect(strcmp(result, "uwe ist cool") == 0); 9 | free(result); 10 | } 11 | 12 | Test(removeMultipleWhitespace, only_whitespace) { 13 | char* empty = calloc(5, sizeof(char)); 14 | strcpy(empty, " "); 15 | char* result = removeMultipleWhitespaces(empty); 16 | 17 | cr_expect(strcmp(result, "") == 0); 18 | free(result); 19 | } 20 | 21 | Test(removeMultipleWhitespace, when_everything_fine_nothing_changs) { 22 | char* line = calloc(64, sizeof(char)); 23 | strcpy(line, "ls ."); 24 | char* result = removeMultipleWhitespaces(line); 25 | cr_expect(strcmp(result, "ls .") == 0); 26 | free(result); 27 | } 28 | 29 | Test(insertStringAtPos, insert_string_at_end) { 30 | char* line = calloc(24, sizeof(char)); 31 | strcpy(line, "testing the waters"); 32 | char* insert_string = " here"; 33 | 34 | insertStringAtPos(&line, insert_string, strlen(line)); 35 | cr_expect(strcmp(line, "testing the waters here") == 0); 36 | 37 | free(line); 38 | } 39 | 40 | Test(insertStringAtPos, insert_string_in_middle) { 41 | char* line = calloc(24, sizeof(char)); 42 | strcpy(line, "testing the waters"); 43 | char* insert_string = "cold "; 44 | 45 | insertStringAtPos(&line, insert_string, 12); 46 | 47 | cr_expect(strcmp(line, "testing the cold waters") == 0); 48 | 49 | free(line); 50 | } 51 | 52 | Test(insertStringAtPos, insert_string_at_start) { 53 | char* line = calloc(24, sizeof(char)); 54 | strcpy(line, "~/testing"); 55 | char* insert_string = "/Users"; 56 | 57 | removeCharAtPos(line, 1); 58 | insertStringAtPos(&line, insert_string, 0); 59 | 60 | cr_expect(strcmp(line, "/Users/testing") == 0); 61 | 62 | free(line); 63 | } 64 | 65 | Test(getAppendingIndex, returns_3_if_second_word_is_len_3) { 66 | char line[64] = "make mak"; 67 | int result = getAppendingIndex(line, ' '); 68 | 69 | cr_expect(result == 3); 70 | } 71 | 72 | Test(getAppendingIndex, still_works_with_only_space) { 73 | char line[64] = "make "; 74 | int result = getAppendingIndex(line, ' '); 75 | 76 | cr_expect(result == 0); 77 | } 78 | 79 | Test(calculateCursorPos, jumps_down_if_current_line_longer_than_term) { 80 | coordinates cursor_pos = {.x = 20, .y = 2}; 81 | coordinates term_size = {.x = 20, .y = 100}; 82 | int prompt_len = 5; 83 | int i = 20; 84 | 85 | coordinates result = calculateCursorPos(term_size, cursor_pos, prompt_len, i); 86 | cr_expect(result.x == 5); 87 | cr_expect(result.y == 3); 88 | } 89 | 90 | Test(calculateCursorPos, shouldnt_change_coordinates_when_line_short) { 91 | coordinates cursor_pos = {.x = 15, .y = 2}; 92 | coordinates term_size = {.x = 20, .y = 100}; 93 | int prompt_len = 5; 94 | int i = 10; 95 | 96 | coordinates result = calculateCursorPos(term_size, cursor_pos, prompt_len, i); 97 | cr_expect(result.x == 15); 98 | cr_expect(result.y == 2); 99 | } 100 | 101 | Test(calculateCursorPos, when_on_last_row_should_still_increment) { 102 | coordinates cursor_pos = {.x = 15, .y = 100}; 103 | coordinates term_size = {.x = 20, .y = 100}; 104 | int prompt_len = 5; 105 | int i = 20; 106 | 107 | coordinates result = calculateCursorPos(term_size, cursor_pos, prompt_len, i); 108 | cr_expect(result.x == 5); 109 | cr_expect(result.y == 101); 110 | } 111 | 112 | Test(calculateCursorPos, when_line_expands_over_many_rows_and_cursor_shouldnt_jump_down_too_early) { 113 | coordinates cursor_pos = {.x = 0, .y = 1}; 114 | coordinates term_size = {.x = 45, .y = 100}; 115 | int prompt_len = 0; 116 | int i = 90; 117 | 118 | coordinates result = calculateCursorPos(term_size, cursor_pos, prompt_len, i); 119 | 120 | cr_expect(result.x == 45); 121 | cr_expect(result.y == 2); 122 | } 123 | 124 | Test(isOnlyDelimeter, true_if_just_white_space) { 125 | char* string = " "; 126 | bool result = isOnlyDelimeter(string, ' '); 127 | 128 | cr_expect(result == true); 129 | } 130 | 131 | Test(isOnlyDelimeter, false_if_even_single_char) { 132 | char* string = " l"; 133 | bool result = isOnlyDelimeter(string, ' '); 134 | 135 | cr_expect(result == false); 136 | } 137 | 138 | Test(firstNonDelimeterIndex, returns_index_of_splitted_array_where_not_delimeter) { 139 | char* one = ""; 140 | char* two = ""; 141 | char* three = "com"; 142 | char* four = "uwe"; 143 | char* addr_one[] = {one, two, three, four}; 144 | 145 | string_array arr1 = {.len = 4, .values = addr_one}; 146 | int result = firstNonDelimeterIndex(arr1); 147 | 148 | cr_expect(result == 2); 149 | } 150 | 151 | Test(firstNonDelimeterIndex, if_first_elem_not_delim_returns_zero) { 152 | char* three = "com"; 153 | char* four = "uwe"; 154 | char* addr_one[] = {three, four}; 155 | 156 | string_array arr1 = {.len = 2, .values = addr_one}; 157 | int result = firstNonDelimeterIndex(arr1); 158 | 159 | cr_expect(result == 0); 160 | } 161 | 162 | Test(insertCharAtPos, see_if_string_reference_changes) { 163 | char line[24] = "uwe tested"; 164 | 165 | insertCharAtPos(line, 3, 'i'); 166 | cr_expect(strcmp(line, "uwei tested") == 0); 167 | } 168 | 169 | Test(removeSlice, when_slice_is_complete_line_empties_string) { 170 | char* line = calloc(4, sizeof(char)); 171 | strcpy(line, "src"); 172 | removeSlice(&line, 0, 3); 173 | cr_expect(strcmp(line, "") == 0); 174 | free(line); 175 | } 176 | 177 | Test(removeSlice, remove_nothing_cursor_end_of_current_word) { 178 | char* word = calloc(52, sizeof(char)); 179 | strcpy(word, "testing if Makefile works"); 180 | int start = 19; 181 | 182 | removeSlice(&word, start, start); 183 | 184 | cr_expect(strcmp(word, "testing if Makefile works") == 0); 185 | free(word); 186 | } 187 | 188 | Test(tokenizeLine, tokenizes_simple_command) { 189 | char* line = "ls arg"; 190 | token_index_arr result = tokenizeLine(line); 191 | token_index* result_arr = result.arr; 192 | 193 | cr_expect(result.len == 3); 194 | cr_expect(result_arr[0].token == CMD); 195 | cr_expect(result_arr[0].start == 0); 196 | cr_expect(result_arr[0].end == 2); 197 | cr_expect(result_arr[1].token == WHITESPACE); 198 | cr_expect(result_arr[1].start == 2); 199 | cr_expect(result_arr[1].end == 3); 200 | cr_expect(result_arr[2].token == ARG); 201 | cr_expect(result_arr[2].start == 3); 202 | cr_expect(result_arr[2].end == 6); 203 | } 204 | 205 | Test(tokenizeLine, tokenizes_line_with_escaped_whitespace_as_single_arg) { 206 | char* line = "ls this_is_\\ one_arg"; 207 | token_index_arr result = tokenizeLine(line); 208 | token_index* result_arr = result.arr; 209 | 210 | cr_expect(result.len == 3); 211 | cr_expect(result_arr[0].token == CMD); 212 | cr_expect(result_arr[0].start == 0); 213 | cr_expect(result_arr[0].end == 2); 214 | cr_expect(result_arr[1].token == WHITESPACE); 215 | cr_expect(result_arr[1].start == 2); 216 | cr_expect(result_arr[1].end == 3); 217 | cr_expect(result_arr[2].token == ARG); 218 | cr_expect(result_arr[2].start == 3); 219 | cr_expect(result_arr[2].end == 20); 220 | } 221 | 222 | Test(tokenizeLine, tokenizes_line_with_multiple_escaped_whitespace_as_single_arg) { 223 | char* line = "ls this_is_\\ \\ \\ one_arg"; 224 | token_index_arr result = tokenizeLine(line); 225 | token_index* result_arr = result.arr; 226 | 227 | cr_expect(result.len == 3); 228 | cr_expect(result_arr[0].token == CMD); 229 | cr_expect(result_arr[0].start == 0); 230 | cr_expect(result_arr[0].end == 2); 231 | cr_expect(result_arr[1].token == WHITESPACE); 232 | cr_expect(result_arr[1].start == 2); 233 | cr_expect(result_arr[1].end == 3); 234 | cr_expect(result_arr[2].token == ARG); 235 | cr_expect(result_arr[2].start == 3); 236 | cr_expect(result_arr[2].end == 24); 237 | } 238 | 239 | Test(tokenizeLine, tokenizes_complex_command_with_escapes) { 240 | char* line = "echo this\\ is&&echo this\\ is"; 241 | token_index_arr result = tokenizeLine(line); 242 | token_index* result_arr = result.arr; 243 | 244 | cr_expect(result.len == 7); 245 | cr_expect(result_arr[0].token == CMD); 246 | cr_expect(result_arr[0].start == 0); 247 | cr_expect(result_arr[0].end == 4); 248 | cr_expect(result_arr[1].token == WHITESPACE); 249 | cr_expect(result_arr[1].start == 4); 250 | cr_expect(result_arr[1].end == 5); 251 | cr_expect(result_arr[2].token == ARG); 252 | cr_expect(result_arr[2].start == 5); 253 | cr_expect(result_arr[2].end == 13); 254 | cr_expect(result_arr[3].token == AMPAMP); 255 | cr_expect(result_arr[3].start == 13); 256 | cr_expect(result_arr[3].end == 15); 257 | cr_expect(result_arr[4].token == AMP_CMD); 258 | cr_expect(result_arr[4].start == 15); 259 | cr_expect(result_arr[4].end == 19); 260 | cr_expect(result_arr[5].token == WHITESPACE); 261 | cr_expect(result_arr[5].start == 19); 262 | cr_expect(result_arr[5].end == 20); 263 | cr_expect(result_arr[6].token == ARG); 264 | cr_expect(result_arr[6].start == 20); 265 | cr_expect(result_arr[6].end == 28); 266 | } 267 | 268 | Test(tokenizeLine, tokenizes_everything_in_quotes_as_arg) { 269 | char* line = "ls 'this is one big arg'"; 270 | token_index_arr result = tokenizeLine(line); 271 | token_index* result_arr = result.arr; 272 | 273 | cr_expect(result.len == 3); 274 | cr_expect(result_arr[0].token == CMD); 275 | cr_expect(result_arr[0].start == 0); 276 | cr_expect(result_arr[0].end == 2); 277 | cr_expect(result_arr[1].token == WHITESPACE); 278 | cr_expect(result_arr[1].start == 2); 279 | cr_expect(result_arr[1].end == 3); 280 | cr_expect(result_arr[2].token == ARG); 281 | cr_expect(result_arr[2].start == 3); 282 | cr_expect(result_arr[2].end == 24); 283 | } 284 | 285 | Test(tokenizeLine, can_seperate_multiple_quoted_args_in_same_line) { 286 | char* line = "ls 'this is one big arg'&&ls 'this_another'"; 287 | token_index_arr result = tokenizeLine(line); 288 | token_index* result_arr = result.arr; 289 | 290 | cr_expect(result.len == 7); 291 | cr_expect(result_arr[0].token == CMD); 292 | cr_expect(result_arr[0].start == 0); 293 | cr_expect(result_arr[0].end == 2); 294 | cr_expect(result_arr[1].token == WHITESPACE); 295 | cr_expect(result_arr[1].start == 2); 296 | cr_expect(result_arr[1].end == 3); 297 | cr_expect(result_arr[2].token == ARG); 298 | cr_expect(result_arr[2].start == 3); 299 | cr_expect(result_arr[2].end == 24); 300 | cr_expect(result_arr[3].token == AMPAMP); 301 | cr_expect(result_arr[3].start == 24); 302 | cr_expect(result_arr[3].end == 26); 303 | cr_expect(result_arr[4].token == AMP_CMD); 304 | cr_expect(result_arr[4].start == 26); 305 | cr_expect(result_arr[4].end == 28); 306 | cr_expect(result_arr[5].token == WHITESPACE); 307 | cr_expect(result_arr[5].start == 28); 308 | cr_expect(result_arr[5].end == 29); 309 | cr_expect(result_arr[6].token == ARG); 310 | cr_expect(result_arr[6].start == 29); 311 | cr_expect(result_arr[6].end == 43); 312 | } 313 | 314 | Test(tokenizeLine, tokenizes_command_with_too_much_whitespace) { 315 | char* line = " ls arg_s "; 316 | token_index_arr result = tokenizeLine(line); 317 | token_index* result_arr = result.arr; 318 | 319 | cr_expect(result.len == 5); 320 | cr_expect(result_arr[0].token == WHITESPACE); 321 | cr_expect(result_arr[0].start == 0); 322 | cr_expect(result_arr[0].end == 3); 323 | cr_expect(result_arr[1].token == CMD); 324 | cr_expect(result_arr[1].start == 3); 325 | cr_expect(result_arr[1].end == 5); 326 | cr_expect(result_arr[2].token == WHITESPACE); 327 | cr_expect(result_arr[2].start == 5); 328 | cr_expect(result_arr[2].end == 7); 329 | cr_expect(result_arr[3].token == ARG); 330 | cr_expect(result_arr[3].start == 7); 331 | cr_expect(result_arr[3].end == 12); 332 | cr_expect(result_arr[4].token == WHITESPACE); 333 | cr_expect(result_arr[4].start == 12); 334 | cr_expect(result_arr[4].end == 15); 335 | } 336 | 337 | Test(tokenizeLine, tokenizes_command_with_too_much_whitespace_and_pipe) { 338 | char* line = " ls arg_s | next_cmd flag=some"; 339 | token_index_arr result = tokenizeLine(line); 340 | token_index* result_arr = result.arr; 341 | 342 | cr_expect(result.len == 10); 343 | cr_expect(result_arr[0].token == WHITESPACE); 344 | cr_expect(result_arr[0].start == 0); 345 | cr_expect(result_arr[0].end == 3); 346 | cr_expect(result_arr[1].token == CMD); 347 | cr_expect(result_arr[1].start == 3); 348 | cr_expect(result_arr[1].end == 5); 349 | cr_expect(result_arr[2].token == WHITESPACE); 350 | cr_expect(result_arr[2].start == 5); 351 | cr_expect(result_arr[2].end == 7); 352 | cr_expect(result_arr[3].token == ARG); 353 | cr_expect(result_arr[3].start == 7); 354 | cr_expect(result_arr[3].end == 12); 355 | cr_expect(result_arr[4].token == WHITESPACE); 356 | cr_expect(result_arr[4].start == 12); 357 | cr_expect(result_arr[4].end == 15); 358 | cr_expect(result_arr[5].token == PIPE); 359 | cr_expect(result_arr[5].start == 15); 360 | cr_expect(result_arr[5].end == 16); 361 | cr_expect(result_arr[6].token == WHITESPACE); 362 | cr_expect(result_arr[6].start == 16); 363 | cr_expect(result_arr[6].end == 17); 364 | cr_expect(result_arr[7].token == PIPE_CMD); 365 | cr_expect(result_arr[7].start == 17); 366 | cr_expect(result_arr[7].end == 25); 367 | cr_expect(result_arr[8].token == WHITESPACE); 368 | cr_expect(result_arr[8].start == 25); 369 | cr_expect(result_arr[8].end == 27); 370 | cr_expect(result_arr[9].token == ARG); 371 | cr_expect(result_arr[9].start == 27); 372 | cr_expect(result_arr[9].end == 36); 373 | } 374 | 375 | Test(tokenizeLine, only_command_and_pipe_cmd) { 376 | char* line = "ls|next_cmd"; 377 | token_index_arr result = tokenizeLine(line); 378 | token_index* result_arr = result.arr; 379 | 380 | cr_expect(result.len == 3); 381 | cr_expect(result_arr[0].token == CMD); 382 | cr_expect(result_arr[0].start == 0); 383 | cr_expect(result_arr[0].end == 2); 384 | cr_expect(result_arr[1].token == PIPE); 385 | cr_expect(result_arr[1].start == 2); 386 | cr_expect(result_arr[1].end == 3); 387 | cr_expect(result_arr[2].token == PIPE_CMD); 388 | cr_expect(result_arr[2].start == 3); 389 | cr_expect(result_arr[2].end == 11); 390 | } 391 | 392 | Test(tokenizeLine, command_and_pipe_cmd_multiple_args) { 393 | char* line = "ls|next_cmd uwe test"; 394 | token_index_arr result = tokenizeLine(line); 395 | token_index* result_arr = result.arr; 396 | 397 | cr_expect(result.len == 7); 398 | cr_expect(result_arr[0].token == CMD); 399 | cr_expect(result_arr[0].start == 0); 400 | cr_expect(result_arr[0].end == 2); 401 | cr_expect(result_arr[1].token == PIPE); 402 | cr_expect(result_arr[1].start == 2); 403 | cr_expect(result_arr[1].end == 3); 404 | cr_expect(result_arr[2].token == PIPE_CMD); 405 | cr_expect(result_arr[2].start == 3); 406 | cr_expect(result_arr[2].end == 11); 407 | } 408 | 409 | Test(tokenizeLine, first_token_great_redirection) { 410 | char* line = ">file"; 411 | token_index_arr result = tokenizeLine(line); 412 | token_index* result_arr = result.arr; 413 | 414 | cr_expect(result.len == 2); 415 | cr_expect(result_arr[0].token == GREAT); 416 | cr_expect(result_arr[0].start == 0); 417 | cr_expect(result_arr[0].end == 1); 418 | cr_expect(result_arr[1].token == ARG); 419 | cr_expect(result_arr[1].start == 1); 420 | cr_expect(result_arr[1].end == 5); 421 | } 422 | 423 | Test(tokenizeLine, first_token_greatgreat_redirection) { 424 | char* line = ">>file"; 425 | token_index_arr result = tokenizeLine(line); 426 | token_index* result_arr = result.arr; 427 | 428 | cr_expect(result.len == 2); 429 | cr_expect(result_arr[0].token == GREATGREAT); 430 | cr_expect(result_arr[0].start == 0); 431 | cr_expect(result_arr[0].end == 2); 432 | cr_expect(result_arr[1].token == ARG); 433 | cr_expect(result_arr[1].start == 2); 434 | cr_expect(result_arr[1].end == 6); 435 | } 436 | 437 | Test(tokenizeLine, first_token_greatgreat_redirection_prefixed_with_fd) { 438 | char* line = "1>> file"; 439 | token_index_arr result = tokenizeLine(line); 440 | token_index* result_arr = result.arr; 441 | 442 | cr_expect(result.len == 3); 443 | cr_expect(result_arr[0].token == GREATGREAT); 444 | cr_expect(result_arr[0].start == 0); 445 | cr_expect(result_arr[0].end == 3); 446 | cr_expect(result_arr[1].token == WHITESPACE); 447 | cr_expect(result_arr[1].start == 3); 448 | cr_expect(result_arr[1].end == 6); 449 | cr_expect(result_arr[2].token == ARG); 450 | cr_expect(result_arr[2].start == 6); 451 | cr_expect(result_arr[2].end == 10); 452 | } 453 | 454 | Test(tokenizeLine, multiple_redirections_with_ampamp) { 455 | char* line = " fl.txt cmd arg"; 456 | token_index_arr result = tokenizeLine(line); 457 | token_index* result_arr = result.arr; 458 | 459 | cr_expect(result.len == 13); 460 | cr_expect(result_arr[0].token == LESS); 461 | cr_expect(result_arr[0].start == 0); 462 | cr_expect(result_arr[0].end == 1); 463 | cr_expect(result_arr[1].token == ARG); 464 | cr_expect(result_arr[1].start == 1); 465 | cr_expect(result_arr[1].end == 8); 466 | cr_expect(result_arr[2].token == WHITESPACE); 467 | cr_expect(result_arr[2].start == 8); 468 | cr_expect(result_arr[2].end == 11); 469 | cr_expect(result_arr[3].token == CMD); 470 | cr_expect(result_arr[3].start == 11); 471 | cr_expect(result_arr[3].end == 14); 472 | cr_expect(result_arr[4].token == AMPAMP); 473 | cr_expect(result_arr[4].start == 14); 474 | cr_expect(result_arr[4].end == 16); 475 | cr_expect(result_arr[5].token == WHITESPACE); 476 | cr_expect(result_arr[5].start == 16); 477 | cr_expect(result_arr[5].end == 17); 478 | cr_expect(result_arr[6].token == GREAT); 479 | cr_expect(result_arr[6].start == 17); 480 | cr_expect(result_arr[6].end == 18); 481 | cr_expect(result_arr[7].token == WHITESPACE); 482 | cr_expect(result_arr[7].start == 18); 483 | cr_expect(result_arr[7].end == 19); 484 | cr_expect(result_arr[8].token == ARG); 485 | cr_expect(result_arr[8].start == 19); 486 | cr_expect(result_arr[8].end == 25); 487 | cr_expect(result_arr[9].token == WHITESPACE); 488 | cr_expect(result_arr[9].start == 25); 489 | cr_expect(result_arr[9].end == 26); 490 | cr_expect(result_arr[10].token == AMP_CMD); 491 | cr_expect(result_arr[10].start == 26); 492 | cr_expect(result_arr[10].end == 29); 493 | cr_expect(result_arr[11].token == WHITESPACE); 494 | cr_expect(result_arr[11].start == 29); 495 | cr_expect(result_arr[11].end == 30); 496 | cr_expect(result_arr[12].token == ARG); 497 | cr_expect(result_arr[12].start == 30); 498 | cr_expect(result_arr[12].end == 33); 499 | } 500 | 501 | Test(tokenizeLine, tokenizes_asterisks) { 502 | char* line = "ls s* * -a"; 503 | token_index_arr result = tokenizeLine(line); 504 | token_index* result_arr = result.arr; 505 | 506 | cr_expect(result.len == 8); 507 | cr_expect(result_arr[0].token == CMD); 508 | cr_expect(result_arr[0].start == 0); 509 | cr_expect(result_arr[0].end == 2); 510 | cr_expect(result_arr[1].token == WHITESPACE); 511 | cr_expect(result_arr[1].start == 2); 512 | cr_expect(result_arr[1].end == 3); 513 | cr_expect(result_arr[2].token == ARG); 514 | cr_expect(result_arr[2].start == 3); 515 | cr_expect(result_arr[2].end == 4); 516 | cr_expect(result_arr[3].token == ASTERISK); 517 | cr_expect(result_arr[3].start == 4); 518 | cr_expect(result_arr[3].end == 5); 519 | cr_expect(result_arr[4].token == WHITESPACE); 520 | cr_expect(result_arr[4].start == 5); 521 | cr_expect(result_arr[4].end == 6); 522 | cr_expect(result_arr[5].token == ASTERISK); 523 | cr_expect(result_arr[5].start == 6); 524 | cr_expect(result_arr[5].end == 7); 525 | cr_expect(result_arr[6].token == WHITESPACE); 526 | cr_expect(result_arr[6].start == 7); 527 | cr_expect(result_arr[6].end == 8); 528 | cr_expect(result_arr[7].token == ARG); 529 | cr_expect(result_arr[7].start == 8); 530 | cr_expect(result_arr[7].end == 10); 531 | } 532 | 533 | Test(tokenizeLine, if_asterisks_in_cmd_just_includes_to_command) { 534 | char* line = "ls*op -a && so*cmd * arg*"; 535 | token_index_arr result = tokenizeLine(line); 536 | token_index* result_arr = result.arr; 537 | 538 | cr_expect(result.len == 12); 539 | cr_expect(result_arr[0].token == CMD); 540 | cr_expect(result_arr[0].start == 0); 541 | cr_expect(result_arr[0].end == 5); 542 | cr_expect(result_arr[1].token == WHITESPACE); 543 | cr_expect(result_arr[1].start == 5); 544 | cr_expect(result_arr[1].end == 6); 545 | cr_expect(result_arr[2].token == ARG); 546 | cr_expect(result_arr[2].start == 6); 547 | cr_expect(result_arr[2].end == 8); 548 | cr_expect(result_arr[3].token == WHITESPACE); 549 | cr_expect(result_arr[3].start == 8); 550 | cr_expect(result_arr[3].end == 9); 551 | cr_expect(result_arr[4].token == AMPAMP); 552 | cr_expect(result_arr[4].start == 9); 553 | cr_expect(result_arr[4].end == 11); 554 | cr_expect(result_arr[5].token == WHITESPACE); 555 | cr_expect(result_arr[5].start == 11); 556 | cr_expect(result_arr[5].end == 12); 557 | cr_expect(result_arr[6].token == AMP_CMD); 558 | cr_expect(result_arr[6].start == 12); 559 | cr_expect(result_arr[6].end == 18); 560 | cr_expect(result_arr[7].token == WHITESPACE); 561 | cr_expect(result_arr[7].start == 18); 562 | cr_expect(result_arr[7].end == 19); 563 | cr_expect(result_arr[8].token == ASTERISK); 564 | cr_expect(result_arr[8].start == 19); 565 | cr_expect(result_arr[8].end == 20); 566 | cr_expect(result_arr[10].token == ARG); 567 | cr_expect(result_arr[10].start == 22); 568 | cr_expect(result_arr[10].end == 25); 569 | cr_expect(result_arr[11].token == ASTERISK); 570 | cr_expect(result_arr[11].start == 25); 571 | cr_expect(result_arr[11].end == 26); 572 | } 573 | 574 | Test(tokenizeLine, tokenizes_question_wildcard) { 575 | char* line = calloc(512, sizeof(char)); 576 | strcpy(line, "ls Do??erfile"); 577 | token_index_arr token = tokenizeLine(line); 578 | 579 | cr_expect(token.len == 6); 580 | cr_expect(token.arr[0].token == CMD); 581 | cr_expect(token.arr[1].token == WHITESPACE); 582 | cr_expect(token.arr[2].token == ARG); 583 | cr_expect(token.arr[3].token == QUESTION); 584 | cr_expect(token.arr[4].token == QUESTION); 585 | cr_expect(token.arr[5].token == ARG); 586 | free(line); 587 | } 588 | 589 | Test(tokenizeLine, redirections_with_whitespace_filename) { 590 | char* line = calloc(512, sizeof(char)); 591 | strcpy(line, "ls 'one whole arg' > some\\ file"); 592 | token_index_arr token = tokenizeLine(line); 593 | 594 | cr_expect(token.len == 7); 595 | cr_expect(token.arr[0].token == CMD); 596 | cr_expect(token.arr[1].token == WHITESPACE); 597 | cr_expect(token.arr[2].token == ARG); 598 | cr_expect(token.arr[3].token == WHITESPACE); 599 | cr_expect(token.arr[4].token == GREAT); 600 | cr_expect(token.arr[5].token == WHITESPACE); 601 | cr_expect(token.arr[6].token == ARG); 602 | free(line); 603 | } 604 | -------------------------------------------------------------------------------- /tests/unit_tests/test_main.c: -------------------------------------------------------------------------------- 1 | #include "../../src/main.h" 2 | #include 3 | 4 | // Unit Tests 5 | Test(Parsing_directory, get_last_two_dirs) { 6 | char* last_two_dirs = getLastTwoDirs("/Users/philipprados/Documents/coding/c/pshell"); 7 | 8 | cr_expect(strcmp(last_two_dirs, "c/pshell") == 0); 9 | } 10 | 11 | Test(Split_func, split_string_at_delimeter) { 12 | string_array result = splitString("this.should.split", '.'); 13 | 14 | cr_expect(strcmp(result.values[0], "this") == 0); 15 | cr_expect(strcmp(result.values[1], "should") == 0); 16 | cr_expect(strcmp(result.values[2], "split") == 0); 17 | } 18 | 19 | Test(Split_func, single_strings_are_nullterminated) { 20 | string_array result = splitString("this.should.split", '.'); 21 | 22 | cr_expect(result.values[0][strlen(result.values[0])] == '\0', "%c", result.values[0][strlen(result.values[0])]); 23 | } 24 | 25 | Test(Split_func, quits_on_q) { 26 | string_array result = splitString("q", ' '); 27 | 28 | cr_expect(strcmp(result.values[0], "q") == 0); 29 | } 30 | 31 | Test(Split_func, end_string_split_with_NULL) { 32 | string_array result = splitString("ls -l", ' '); 33 | 34 | cr_expect_null(result.values[3]); 35 | } 36 | 37 | Test(Split_func, only_command_len) { 38 | string_array result = splitString("ma", ' '); 39 | 40 | cr_expect(result.len == 1); 41 | } 42 | 43 | Test(removeDots, removes_string_when_has_dot) { 44 | char** addr_one = calloc(4, sizeof(char*)); 45 | addr_one[0] = calloc(strlen("one") + 1, 1); 46 | strcpy(addr_one[0], "one"); 47 | addr_one[1] = calloc(strlen("two") + 1, 1); 48 | strcpy(addr_one[1], "two"); 49 | addr_one[2] = calloc(strlen("../") + 1, 1); 50 | strcpy(addr_one[2], "../"); 51 | addr_one[3] = calloc(strlen("four") + 1, 1); 52 | strcpy(addr_one[3], "four"); 53 | 54 | string_array arr1 = {.len = 4, .values = addr_one}; 55 | 56 | string_array result = removeDots(&arr1); 57 | 58 | cr_expect(strcmp(result.values[0], "one") == 0); 59 | cr_expect(strcmp(result.values[1], "two") == 0); 60 | cr_expect(strcmp(result.values[2], "four") == 0); 61 | } 62 | 63 | Test(removeDots, array_stays_same_when_no_dot) { 64 | char** addr_one = calloc(4, sizeof(char*)); 65 | addr_one[0] = calloc(strlen("one") + 1, 1); 66 | strcpy(addr_one[0], "one"); 67 | addr_one[1] = calloc(strlen("two") + 1, 1); 68 | strcpy(addr_one[1], "two"); 69 | addr_one[2] = calloc(strlen("three") + 1, 1); 70 | strcpy(addr_one[2], "three"); 71 | addr_one[3] = calloc(strlen("four") + 1, 1); 72 | strcpy(addr_one[3], "four"); 73 | 74 | string_array arr1 = {.len = 4, .values = addr_one}; 75 | 76 | string_array result = removeDots(&arr1); 77 | 78 | cr_expect(strcmp(result.values[0], "one") == 0); 79 | cr_expect(strcmp(result.values[1], "two") == 0); 80 | cr_expect(strcmp(result.values[2], "three") == 0); 81 | cr_expect(strcmp(result.values[3], "four") == 0); 82 | free_string_array(&result); 83 | } 84 | 85 | Test(removeWhitespacetokens, removes_whitetoken) { 86 | token_index arr1 = {.token = WHITESPACE, .start = 0, .end = 2}; 87 | token_index arr2 = {.token = CMD, .start = 0, .end = 2}; 88 | token_index arr[] = {arr1, arr2}; 89 | token_index_arr token = {.arr = arr, .len = 2}; 90 | 91 | removeWhitespaceTokens(&token); 92 | cr_expect(token.arr[0].token == CMD); 93 | cr_expect(token.len == 1); 94 | } 95 | 96 | Test(removeWhitespacetokens, removes_whitetoken_when_multiple_args) { 97 | token_index arr1 = {.token = CMD}; 98 | token_index arr2 = {.token = WHITESPACE}; 99 | token_index arr3 = {.token = ARG}; 100 | token_index arr4 = {.token = WHITESPACE}; 101 | token_index arr5 = {.token = ARG}; 102 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5}; 103 | token_index_arr token = {.arr = arr, .len = 5}; 104 | 105 | removeWhitespaceTokens(&token); 106 | cr_expect(token.len == 3); 107 | cr_expect(token.arr[0].token == CMD); 108 | cr_expect(token.arr[1].token == ARG); 109 | cr_expect(token.arr[2].token == ARG); 110 | cr_expect(token.len == 3); 111 | } 112 | 113 | Test(isValidSyntax, valid_when_normal_syntax) { 114 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 115 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 116 | token_index arr3 = {.token = PIPE, .start = 0, .end = 2}; 117 | token_index arr4 = {.token = PIPE_CMD, .start = 0, .end = 2}; 118 | token_index arr[] = {arr1, arr2, arr3, arr4}; 119 | token_index_arr token = {.arr = arr, .len = 4}; 120 | 121 | bool result = isValidSyntax(token); 122 | cr_expect(result == true); 123 | } 124 | 125 | Test(isValidSyntax, not_valid_when_multiple_pipes_successively) { 126 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 127 | token_index arr2 = {.token = PIPE, .start = 0, .end = 2}; 128 | token_index arr3 = {.token = PIPE, .start = 0, .end = 2}; 129 | token_index arr4 = {.token = PIPE_CMD, .start = 0, .end = 2}; 130 | token_index arr[] = {arr1, arr2, arr3, arr4}; 131 | token_index_arr token = {.arr = arr, .len = 4}; 132 | 133 | bool result = isValidSyntax(token); 134 | cr_expect(result == false); 135 | } 136 | 137 | Test(isValidSyntax, not_valid_when_last_char_pipe) { 138 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 139 | token_index arr2 = {.token = PIPE, .start = 0, .end = 2}; 140 | token_index arr[] = {arr1, arr2}; 141 | token_index_arr token = {.arr = arr, .len = 2}; 142 | 143 | bool result = isValidSyntax(token); 144 | cr_expect(result == false); 145 | } 146 | 147 | Test(isValidSyntax, not_valid_when_starts_with_pipe) { 148 | token_index arr1 = {.token = PIPE, .start = 0, .end = 2}; 149 | token_index arr2 = {.token = PIPE_CMD, .start = 0, .end = 2}; 150 | token_index arr[] = {arr1, arr2}; 151 | token_index_arr token = {.arr = arr, .len = 2}; 152 | 153 | bool result = isValidSyntax(token); 154 | cr_expect(result == false); 155 | } 156 | 157 | Test(isValidSyntax, cmd_with_args_pipe_and_ampamp) { 158 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 159 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 160 | token_index arr3 = {.token = AMPAMP, .start = 0, .end = 2}; 161 | token_index arr4 = {.token = AMP_CMD, .start = 0, .end = 2}; 162 | token_index arr5 = {.token = ARG, .start = 0, .end = 2}; 163 | token_index arr6 = {.token = PIPE, .start = 0, .end = 2}; 164 | token_index arr7 = {.token = PIPE_CMD, .start = 0, .end = 2}; 165 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6, arr7}; 166 | token_index_arr token = {.arr = arr, .len = 7}; 167 | 168 | bool result = isValidSyntax(token); 169 | cr_expect(result == true); 170 | } 171 | 172 | Test(isValidSyntax, cmd_pipe_and_ampamp) { 173 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 174 | token_index arr2 = {.token = PIPE, .start = 0, .end = 2}; 175 | token_index arr3 = {.token = PIPE_CMD, .start = 0, .end = 2}; 176 | token_index arr4 = {.token = AMPAMP, .start = 0, .end = 2}; 177 | token_index arr5 = {.token = AMP_CMD, .start = 0, .end = 2}; 178 | token_index arr6 = {.token = ARG, .start = 0, .end = 2}; 179 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6}; 180 | token_index_arr token = {.arr = arr, .len = 6}; 181 | 182 | bool result = isValidSyntax(token); 183 | cr_expect(result == true); 184 | } 185 | 186 | Test(isValidSyntax, redirectction_end_of_line) { 187 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 188 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 189 | token_index arr3 = {.token = GREAT, .start = 0, .end = 2}; 190 | token_index arr4 = {.token = ARG, .start = 0, .end = 2}; 191 | token_index arr[] = {arr1, arr2, arr3, arr4}; 192 | token_index_arr token = {.arr = arr, .len = 4}; 193 | 194 | bool result = isValidSyntax(token); 195 | cr_expect(result == true); 196 | } 197 | Test(isValidSyntax, redirectction_end_of_line_no_filename) { 198 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 199 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 200 | token_index arr3 = {.token = GREAT, .start = 0, .end = 2}; 201 | token_index arr[] = {arr1, arr2, arr3}; 202 | token_index_arr token = {.arr = arr, .len = 3}; 203 | 204 | bool result = isValidSyntax(token); 205 | cr_expect(result == false); 206 | } 207 | 208 | Test(isValidSyntax, redirection_also_pipe) { 209 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 210 | token_index arr2 = {.token = GREAT, .start = 0, .end = 2}; 211 | token_index arr3 = {.token = ARG, .start = 0, .end = 2}; 212 | token_index arr4 = {.token = PIPE, .start = 0, .end = 2}; 213 | token_index arr5 = {.token = PIPE_CMD, .start = 0, .end = 2}; 214 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5}; 215 | token_index_arr token = {.arr = arr, .len = 5}; 216 | 217 | bool result = isValidSyntax(token); 218 | cr_expect(result == true); 219 | } 220 | 221 | Test(isValidSyntax, multiple_redirection_also_pipe) { 222 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 223 | token_index arr2 = {.token = GREAT, .start = 0, .end = 2}; 224 | token_index arr3 = {.token = ARG, .start = 0, .end = 2}; 225 | token_index arr4 = {.token = GREAT, .start = 0, .end = 2}; 226 | token_index arr5 = {.token = ARG, .start = 0, .end = 2}; 227 | token_index arr6 = {.token = PIPE, .start = 0, .end = 2}; 228 | token_index arr7 = {.token = PIPE_CMD, .start = 0, .end = 2}; 229 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6, arr7}; 230 | token_index_arr token = {.arr = arr, .len = 7}; 231 | 232 | bool result = isValidSyntax(token); 233 | cr_expect(result == true); 234 | } 235 | 236 | Test(isValidSyntax, only_redirection) { 237 | token_index arr1 = {.token = GREAT, .start = 0, .end = 2}; 238 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 239 | token_index arr[] = {arr1, arr2}; 240 | token_index_arr token = {.arr = arr, .len = 2}; 241 | 242 | bool result = isValidSyntax(token); 243 | cr_expect(result == true); 244 | } 245 | 246 | Test(isValidSyntax, redirection_between_cmd_and_args) { 247 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 248 | token_index arr2 = {.token = GREAT, .start = 0, .end = 2}; 249 | token_index arr3 = {.token = ARG, .start = 0, .end = 2}; 250 | token_index arr4 = {.token = ARG, .start = 0, .end = 2}; 251 | token_index arr5 = {.token = GREAT, .start = 0, .end = 2}; 252 | token_index arr6 = {.token = ARG, .start = 0, .end = 2}; 253 | token_index arr7 = {.token = PIPE, .start = 0, .end = 2}; 254 | token_index arr8 = {.token = PIPE_CMD, .start = 0, .end = 2}; 255 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6, arr7, arr8}; 256 | token_index_arr token = {.arr = arr, .len = 8}; 257 | 258 | bool result = isValidSyntax(token); 259 | cr_expect(result == true); 260 | } 261 | 262 | Test(isValidSyntax, redirection_pipe_and_pipe_cmd) { 263 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 264 | token_index arr2 = {.token = PIPE, .start = 0, .end = 2}; 265 | token_index arr3 = {.token = GREAT, .start = 0, .end = 2}; 266 | token_index arr4 = {.token = ARG, .start = 0, .end = 2}; 267 | token_index arr5 = {.token = GREAT, .start = 0, .end = 2}; 268 | token_index arr6 = {.token = ARG, .start = 0, .end = 2}; 269 | token_index arr7 = {.token = PIPE_CMD, .start = 0, .end = 2}; 270 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6, arr7}; 271 | token_index_arr token = {.arr = arr, .len = 7}; 272 | 273 | bool result = isValidSyntax(token); 274 | cr_expect(result == true); 275 | } 276 | 277 | Test(isValidSyntax, redirection_ampamp_and_amp_cmd) { 278 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 279 | token_index arr2 = {.token = AMPAMP, .start = 0, .end = 2}; 280 | token_index arr3 = {.token = GREAT, .start = 0, .end = 2}; 281 | token_index arr4 = {.token = ARG, .start = 0, .end = 2}; 282 | token_index arr5 = {.token = AMP_CMD, .start = 0, .end = 2}; 283 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5}; 284 | token_index_arr token = {.arr = arr, .len = 5}; 285 | 286 | bool result = isValidSyntax(token); 287 | cr_expect(result == true); 288 | } 289 | 290 | Test(isValidSyntax, mulitple_redirections_without_filenames) { 291 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 292 | token_index arr2 = {.token = PIPE, .start = 0, .end = 2}; 293 | token_index arr3 = {.token = PIPE_CMD, .start = 0, .end = 2}; 294 | token_index arr4 = {.token = GREAT, .start = 0, .end = 2}; 295 | token_index arr5 = {.token = GREAT, .start = 0, .end = 2}; 296 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5}; 297 | token_index_arr token = {.arr = arr, .len = 5}; 298 | 299 | bool result = isValidSyntax(token); 300 | cr_expect(result == false); 301 | } 302 | Test(isValidSyntax, redirectction_after_pipecmd_and_arg) { 303 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 304 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 305 | token_index arr3 = {.token = PIPE, .start = 0, .end = 2}; 306 | token_index arr4 = {.token = PIPE_CMD, .start = 0, .end = 2}; 307 | token_index arr5 = {.token = GREAT, .start = 0, .end = 2}; 308 | token_index arr6 = {.token = ARG, .start = 0, .end = 2}; 309 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6}; 310 | token_index_arr token = {.arr = arr, .len = 6}; 311 | 312 | bool result = isValidSyntax(token); 313 | cr_expect(result == true); 314 | } 315 | 316 | Test(isValidSyntax, only_redirection_and_pipe_cmd) { 317 | token_index arr1 = {.token = GREAT, .start = 0, .end = 2}; 318 | token_index arr2 = {.token = ARG, .start = 0, .end = 2}; 319 | token_index arr3 = {.token = PIPE, .start = 0, .end = 2}; 320 | token_index arr4 = {.token = PIPE_CMD, .start = 0, .end = 2}; 321 | token_index arr[] = {arr1, arr2, arr3, arr4}; 322 | token_index_arr token = {.arr = arr, .len = 4}; 323 | 324 | bool result = isValidSyntax(token); 325 | cr_expect(result == false); 326 | } 327 | 328 | Test(splitLineIntoSimpleCommands, splits_at_pipe) { 329 | char* line = "ls |uwe"; 330 | token_index arr1 = {.token = CMD, .start = 0, .end = 4}; 331 | token_index arr2 = {.token = PIPE, .start = 4, .end = 5}; 332 | token_index arr3 = {.token = PIPE_CMD, .start = 5, .end = 8}; 333 | token_index arr[] = {arr1, arr2, arr3}; 334 | token_index_arr token = {.arr = arr, .len = 3}; 335 | 336 | string_array_token result = splitLineIntoSimpleCommands(line, token); 337 | cr_expect(result.len == 2); 338 | cr_expect(strcmp(result.values[0], "ls ") == 0); 339 | cr_expect(strcmp(result.values[1], "uwe") == 0); 340 | } 341 | 342 | Test(splitLineIntoSimpleCommands, tokenizes_splitted_commands_into_ampamp_or_pipe) { 343 | char* line = "ls arg&& uwe | last"; 344 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 345 | token_index arr2 = {.token = ARG, .start = 4, .end = 7}; 346 | token_index arr3 = {.token = AMPAMP, .start = 7, .end = 9}; 347 | token_index arr4 = {.token = AMP_CMD, .start = 10, .end = 13}; 348 | token_index arr5 = {.token = PIPE, .start = 14, .end = 15}; 349 | token_index arr6 = {.token = PIPE_CMD, .start = 16, .end = 20}; 350 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6}; 351 | token_index_arr token = {.arr = arr, .len = 6}; 352 | 353 | string_array_token result = splitLineIntoSimpleCommands(line, token); 354 | cr_expect(result.len == 3); 355 | cr_expect(strcmp(result.values[0], "ls arg") == 0); 356 | cr_expect(result.token_arr[0] == CMD); 357 | cr_expect(strcmp(result.values[1], "uwe ") == 0); 358 | cr_expect(result.token_arr[1] == AMP_CMD); 359 | cr_expect(strcmp(result.values[2], "last") == 0); 360 | cr_expect(result.token_arr[2] == PIPE_CMD); 361 | } 362 | 363 | Test(splitLineIntoSimpleCommands, splits_at_pipe_with_arg_and_whitespace) { 364 | char* line = "ls some_arg| uwe also|last"; 365 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 366 | token_index arr2 = {.token = ARG, .start = 3, .end = 11}; 367 | token_index arr3 = {.token = PIPE, .start = 11, .end = 12}; 368 | token_index arr4 = {.token = PIPE_CMD, .start = 13, .end = 16}; 369 | token_index arr5 = {.token = ARG, .start = 17, .end = 21}; 370 | token_index arr6 = {.token = PIPE, .start = 21, .end = 22}; 371 | token_index arr7 = {.token = PIPE_CMD, .start = 22, .end = 26}; 372 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5, arr6, arr7}; 373 | token_index_arr token = {.arr = arr, .len = 7}; 374 | 375 | string_array_token result = splitLineIntoSimpleCommands(line, token); 376 | cr_expect(result.len == 3); 377 | cr_expect(strcmp(result.values[0], "ls some_arg") == 0); 378 | cr_expect(result.token_arr[0] == CMD); 379 | cr_expect(strcmp(result.values[1], "uwe also") == 0); 380 | cr_expect(result.token_arr[1] == PIPE_CMD); 381 | cr_expect(strcmp(result.values[2], "last") == 0); 382 | cr_expect(result.token_arr[2] == PIPE_CMD); 383 | } 384 | 385 | Test(splitLineIntoSimpleCommands, splits_at_pipe_with_mutliple_whitespace) { 386 | char* line = "ls | cd|bat"; 387 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 388 | token_index arr2 = {.token = PIPE, .start = 4, .end = 5}; 389 | token_index arr3 = {.token = PIPE_CMD, .start = 7, .end = 9}; 390 | token_index arr4 = {.token = PIPE, .start = 9, .end = 10}; 391 | token_index arr5 = {.token = PIPE_CMD, .start = 10, .end = 12}; 392 | token_index arr[] = {arr1, arr2, arr3, arr4, arr5}; 393 | token_index_arr token = {.arr = arr, .len = 5}; 394 | 395 | string_array_token result = splitLineIntoSimpleCommands(line, token); 396 | cr_expect(result.len == 3); 397 | cr_expect(strcmp(result.values[0], "ls ") == 0); 398 | cr_expect(result.token_arr[0] == CMD); 399 | cr_expect(strcmp(result.values[1], "cd") == 0); 400 | cr_expect(result.token_arr[1] == PIPE_CMD); 401 | cr_expect(strcmp(result.values[2], "bat") == 0); 402 | cr_expect(result.token_arr[2] == PIPE_CMD); 403 | } 404 | 405 | Test(splitLineIntoSimpleCommands, split_with_ampamp) { 406 | char* line = "ls &&cmd"; 407 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 408 | token_index arr2 = {.token = AMPAMP, .start = 4, .end = 6}; 409 | token_index arr3 = {.token = AMP_CMD, .start = 6, .end = 9}; 410 | token_index arr[] = {arr1, arr2, arr3}; 411 | token_index_arr token = {.arr = arr, .len = 3}; 412 | 413 | string_array_token result = splitLineIntoSimpleCommands(line, token); 414 | cr_expect(result.len == 2); 415 | cr_expect(strcmp(result.values[0], "ls ") == 0); 416 | cr_expect(result.token_arr[0] == CMD); 417 | cr_expect(strcmp(result.values[1], "cmd") == 0); 418 | cr_expect(result.token_arr[1] == AMP_CMD); 419 | } 420 | 421 | Test(splitByToken, splitByWhitespaceToken) { 422 | char* line = "ls .."; 423 | token_index arr1 = {.token = CMD, .start = 0, .end = 2}; 424 | token_index arr2 = {.token = ARG, .start = 3, .end = 5}; 425 | token_index arr[] = {arr1, arr2}; 426 | token_index_arr token = {.arr = arr, .len = 2}; 427 | string_array result = splitByTokens(line, token); 428 | 429 | cr_expect(result.len == 2); 430 | cr_expect(strcmp(result.values[0], "ls") == 0); 431 | cr_expect(strcmp(result.values[1], "..") == 0); 432 | } 433 | 434 | Test(splitByToken, split_only_token_also_with_multiple_whitespace) { 435 | char* line = " bat"; 436 | token_index arr1 = {.token = CMD, .start = 4, .end = 7}; 437 | token_index arr[] = {arr1}; 438 | token_index_arr token = {.arr = arr, .len = 1}; 439 | string_array result = splitByTokens(line, token); 440 | 441 | cr_expect(result.len == 1); 442 | cr_expect(strcmp(result.values[0], "bat") == 0); 443 | } 444 | 445 | Test(parseForRedirectionFiles, removes_all_redirections_in_split) { 446 | char* simple_cmd1 = ">log.txt bat"; 447 | token_index arr1 = {.token = GREAT, .start = 0, .end = 1}; 448 | token_index arr2 = {.token = ARG, .start = 1, .end = 8}; 449 | token_index arr3 = {.token = CMD, .start = 11, .end = 14}; 450 | token_index t1[] = {arr1, arr2, arr3}; 451 | token_index_arr token1 = {.arr = t1, .len = 3}; 452 | char* simple_cmd2 = " < fl.txt cmd arg"; 453 | token_index arr4 = {.token = LESS, .start = 1, .end = 2}; 454 | token_index arr5 = {.token = ARG, .start = 3, .end = 9}; 455 | token_index arr6 = {.token = CMD, .start = 11, .end = 14}; 456 | token_index arr7 = {.token = ARG, .start = 16, .end = 19}; 457 | token_index t2[] = {arr4, arr5, arr6, arr7}; 458 | token_index_arr token2 = {.arr = t2, .len = 4}; 459 | string_array split1 = splitByTokens(simple_cmd1, token1); 460 | string_array split2 = splitByTokens(simple_cmd2, token2); 461 | 462 | file_redirection_data result1 = parseForRedirectionFiles(split1, token1); 463 | file_redirection_data result2 = parseForRedirectionFiles(split2, token2); 464 | 465 | cr_expect(strcmp(result1.output_filename, "log.txt") == 0); 466 | cr_expect_null(result1.input_filename); 467 | cr_expect(result1.output_append == false); 468 | cr_expect_null(result2.output_filename); 469 | cr_expect(strcmp(result2.input_filename, "fl.txt") == 0); 470 | cr_expect(result2.output_append == false); 471 | } 472 | 473 | Test(expandWildcardgroups, replace_wildcard_astrisk_when_single_match) { 474 | char* line = calloc(512, sizeof(char)); 475 | strcpy(line, "ls sr*"); 476 | token_index_arr token = tokenizeLine(line); 477 | 478 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 479 | wildcard_groups_arr result = expandWildcardgroups(groups); 480 | cr_expect(result.len == 1); 481 | cr_expect(strcmp(result.arr[0].wildcard_arg, "src ") == 0); 482 | free(line); 483 | } 484 | 485 | Test(expandWildcardgroups, replace_wildcard_astrisk_with_everything_if_not_after_file) { 486 | char* line = calloc(512, sizeof(char)); 487 | strcpy(line, "ls *"); 488 | token_index_arr token = tokenizeLine(line); 489 | 490 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 491 | wildcard_groups_arr result = expandWildcardgroups(groups); 492 | cr_expect(result.len == 1); 493 | cr_expect(strcmp(result.arr[0].wildcard_arg, "Dockerfile Makefile pictures tests README.md log.txt" 494 | " compile_flags.txt LICENSE.txt src ") == 0); 495 | free(line); 496 | } 497 | 498 | Test(expandWildcardgroups, replace_wildcard_astrisk_with_multiple_matches_in_dir) { 499 | char* line = calloc(512, sizeof(char)); 500 | strcpy(line, "ls tests/* some_other"); 501 | token_index_arr token = tokenizeLine(line); 502 | 503 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 504 | wildcard_groups_arr result = expandWildcardgroups(groups); 505 | cr_expect(result.len == 1); 506 | cr_expect(strcmp(result.arr[0].wildcard_arg, "tests/unit_tests tests/integration_tests ") == 0); 507 | free(line); 508 | } 509 | 510 | Test(expandWildcardgroups, if_asterisk_in_middle_of_arg) { 511 | char* line = calloc(512, sizeof(char)); 512 | strcpy(line, "ls Do*ile"); 513 | token_index_arr token = tokenizeLine(line); 514 | 515 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 516 | wildcard_groups_arr result = expandWildcardgroups(groups); 517 | cr_expect(result.len == 1); 518 | cr_expect(strcmp(result.arr[0].wildcard_arg, "Dockerfile ") == 0); 519 | free(line); 520 | } 521 | 522 | Test(expandWildcardgroups, multiple_asterisks_in_one_arg) { 523 | char* line = calloc(512, sizeof(char)); 524 | strcpy(line, "ls sr*/fuz*"); 525 | token_index_arr token = tokenizeLine(line); 526 | 527 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 528 | wildcard_groups_arr result = expandWildcardgroups(groups); 529 | cr_expect(result.len == 1); 530 | cr_expect(strcmp(result.arr[0].wildcard_arg, "src/fuzzy_finder.c src/fuzzy_finder.h ") == 0); 531 | free(line); 532 | } 533 | 534 | Test(expandWildcardgroups, multiple_asterisks_in_line) { 535 | char* line = calloc(512, sizeof(char)); 536 | strcpy(line, "ls sr*/fuz* * *file te*&&"); 537 | token_index_arr token = tokenizeLine(line); 538 | 539 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 540 | wildcard_groups_arr result = expandWildcardgroups(groups); 541 | cr_expect(result.len == 4); 542 | cr_expect(strcmp(result.arr[0].wildcard_arg, "src/fuzzy_finder.c src/fuzzy_finder.h ") == 0); 543 | 544 | cr_expect(strcmp(result.arr[1].wildcard_arg, 545 | "Dockerfile Makefile pictures tests README.md log.txt compile_flags.txt LICENSE.txt src ") == 546 | 0); 547 | cr_expect(strcmp(result.arr[2].wildcard_arg, "Dockerfile Makefile ") == 0); 548 | cr_expect(strcmp(result.arr[3].wildcard_arg, "tests ") == 0); 549 | free(line); 550 | } 551 | 552 | Test(expandWildcardgroups, escapes_whitespace_in_wildcard_match) { 553 | char* line = calloc(512, sizeof(char)); 554 | strcpy(line, "ls tests/unit_tests/prop_test_files/*"); 555 | token_index_arr token = tokenizeLine(line); 556 | 557 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 558 | wildcard_groups_arr result = expandWildcardgroups(groups); 559 | cr_expect(result.len == 1); 560 | cr_expect(strcmp(result.arr[0].wildcard_arg, "tests/unit_tests/prop_test_files/with\\ \\ multiple\\ white " 561 | "tests/unit_tests/prop_test_files/with\\ whitespace ") == 0); 562 | 563 | free(line); 564 | } 565 | 566 | Test(groupWildcards, finds_all_wildcard_groupings) { 567 | char* line = calloc(512, sizeof(char)); 568 | strcpy(line, "ls sr*/fuz* * *file te*&&"); 569 | token_index_arr token = tokenizeLine(line); 570 | 571 | wildcard_groups_arr result = groupWildcards(line, token); 572 | cr_expect(result.len == 4); 573 | cr_expect(strcmp(result.arr[0].wildcard_arg, "sr*/fuz*") == 0); 574 | cr_expect(result.arr[0].start == 3); 575 | cr_expect(result.arr[0].end == 11); 576 | cr_expect(strcmp(result.arr[1].wildcard_arg, "*") == 0); 577 | cr_expect(result.arr[1].start == 13); 578 | cr_expect(result.arr[1].end == 14); 579 | cr_expect(strcmp(result.arr[2].wildcard_arg, "*file") == 0); 580 | cr_expect(result.arr[2].start == 16); 581 | cr_expect(result.arr[2].end == 21); 582 | cr_expect(strcmp(result.arr[3].wildcard_arg, "te*") == 0); 583 | cr_expect(result.arr[3].start == 22); 584 | cr_expect(result.arr[3].end == 25); 585 | free(line); 586 | free(result.arr); 587 | } 588 | 589 | Test(groupWildcards, groups_with_question_wildcard) { 590 | char* line = calloc(512, sizeof(char)); 591 | strcpy(line, "ls Do??erfile"); 592 | token_index_arr token = tokenizeLine(line); 593 | 594 | wildcard_groups_arr result = groupWildcards(line, token); 595 | cr_expect(result.len == 1); 596 | cr_expect(strcmp(result.arr[0].wildcard_arg, "Do??erfile") == 0); 597 | cr_expect(result.arr[0].start == 3); 598 | cr_expect(result.arr[0].end == 13); 599 | free(line); 600 | free(result.arr); 601 | } 602 | 603 | Test(replaceLineWithWildcards, replaces_regular_line_with_wildcard_match) { 604 | char* line = calloc(512, sizeof(char)); 605 | strcpy(line, "ls sr*/fuz* *file te*&&"); 606 | 607 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 608 | wildcard_groups_arr wildcard_matches = expandWildcardgroups(groups); 609 | replaceLineWithWildcards(&line, wildcard_matches); 610 | 611 | cr_expect(strcmp(line, "ls src/fuzzy_finder.c src/fuzzy_finder.h Dockerfile Makefile tests &&") == 0); 612 | free(line); 613 | } 614 | 615 | Test(replaceLineWithWildcards, double_question_wildcard_in_center) { 616 | char* line = calloc(512, sizeof(char)); 617 | strcpy(line, "ls Do??erfile"); 618 | 619 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 620 | wildcard_groups_arr wildcard_matches = expandWildcardgroups(groups); 621 | replaceLineWithWildcards(&line, wildcard_matches); 622 | 623 | cr_expect(strcmp(line, "ls Dockerfile ") == 0); 624 | free(line); 625 | } 626 | Test(replaceLineWithWildcards, single_letter_between_two_asterisks) { 627 | char* line = calloc(512, sizeof(char)); 628 | strcpy(line, "ls *a*"); 629 | 630 | wildcard_groups_arr groups = groupWildcards(line, tokenizeLine(line)); 631 | wildcard_groups_arr wildcard_matches = expandWildcardgroups(groups); 632 | replaceLineWithWildcards(&line, wildcard_matches); 633 | 634 | cr_expect(strcmp(line, "ls Makefile compile_flags.txt ") == 0); 635 | free(line); 636 | } 637 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | string_array splitString(const char* string_to_split, char delimeter) { 4 | int start = 0; 5 | int j = 0; 6 | char** splitted_strings = (char**)calloc(strlen(string_to_split), sizeof(char*)); 7 | string_array result; 8 | if (isOnlyDelimeter(string_to_split, delimeter)) { 9 | splitted_strings[0] = calloc(strlen(string_to_split) + 1, sizeof(char)); 10 | strcpy(splitted_strings[0], string_to_split); 11 | return (string_array){.len = 1, .values = splitted_strings}; 12 | } 13 | 14 | for (int i = 0;; i++) { 15 | if (string_to_split[i] == delimeter || string_to_split[i] == '\0') { 16 | splitted_strings[j] = (char*)calloc(i - start + 1, sizeof(char)); 17 | memcpy(splitted_strings[j], &string_to_split[start], i - start); 18 | start = i + 1; 19 | j++; 20 | } 21 | if (string_to_split[i] == '\0') 22 | break; 23 | } 24 | result.len = j; 25 | result.values = splitted_strings; 26 | return result; 27 | } 28 | 29 | char* getLastTwoDirs(char* cwd) { 30 | int i = 1; 31 | int last_slash_pos = 0; 32 | int second_to_last_slash = 0; 33 | 34 | for (; cwd[i] != '\n' && cwd[i] != '\0'; i++) { 35 | if (cwd[i] == '/') { 36 | second_to_last_slash = last_slash_pos; 37 | last_slash_pos = i + 1; 38 | } 39 | } 40 | char* last_two_dirs = (char*)calloc(i - second_to_last_slash + 1, sizeof(char)); 41 | strncpy(last_two_dirs, &cwd[second_to_last_slash], i - second_to_last_slash); 42 | 43 | return last_two_dirs; 44 | } 45 | 46 | string_array removeDots(string_array* array) { 47 | int j = 0; 48 | bool remove_index; 49 | char* not_allowed_dots[] = {".", "..", "./", "../"}; 50 | string_array no_dots_array; 51 | no_dots_array.values = calloc(array->len, sizeof(char*)); 52 | no_dots_array.len = 0; 53 | 54 | for (int i = 0; i < array->len; i++) { 55 | remove_index = false; 56 | for (int k = 0; k < 4; k++) { 57 | if (strcmp(array->values[i], not_allowed_dots[k]) == 0) { 58 | remove_index = true; 59 | } 60 | } 61 | if (!remove_index) { 62 | no_dots_array.values[j] = calloc(strlen(array->values[i]) + 1, sizeof(char)); 63 | strcpy(no_dots_array.values[j], array->values[i]); 64 | no_dots_array.len += 1; 65 | j++; 66 | } 67 | } 68 | free_string_array(array); 69 | return no_dots_array; 70 | } 71 | 72 | char* joinFilePath(char* home_dir, char* destination_file) { 73 | char* home_dir_copied = calloc(strlen(home_dir) + strlen(destination_file) + 1, sizeof(char)); 74 | strcpy(home_dir_copied, home_dir); 75 | 76 | char* file_path = strcat(home_dir_copied, destination_file); 77 | 78 | return file_path; 79 | } 80 | 81 | void removeEscapesArr(string_array* splitted) { 82 | for (int i = 0; i < splitted->len; i++) { 83 | removeEscapesString(&splitted->values[i]); 84 | } 85 | } 86 | 87 | wildcard_groups_arr groupWildcards(char* line, token_index_arr token) { 88 | wildcard_groups* result = calloc(token.len, sizeof(wildcard_groups)); 89 | int wildcard_index = 0; 90 | 91 | for (int i = 0; i < token.len;) { 92 | if (token.arr[i].token == ASTERISK || token.arr[i].token == QUESTION) { 93 | int start = token.arr[i - 1].token == ARG ? token.arr[i - 1].start : token.arr[i].start; 94 | int j = i; 95 | 96 | for (; (j + 1) < token.len && (token.arr[j + 1].token == ARG || token.arr[j + 1].token == ASTERISK || 97 | token.arr[j + 1].token == QUESTION); 98 | j++) 99 | ; 100 | int end_index = token.arr[j].end; 101 | 102 | result[wildcard_index].wildcard_arg = calloc(end_index - start + 1, sizeof(char)); 103 | strncpy(result[wildcard_index].wildcard_arg, &line[start], end_index - start); 104 | result[wildcard_index].start = start; 105 | result[wildcard_index].end = start + strlen(result[wildcard_index].wildcard_arg); 106 | 107 | wildcard_index++; 108 | i = j + 1; 109 | } 110 | i++; 111 | } 112 | return (wildcard_groups_arr){.arr = result, .len = wildcard_index}; 113 | } 114 | 115 | bool isLastRedirectionInLine(char* line) { 116 | for (int i = 0; i < strlen(line); i++) { 117 | if (line[i] == '*' || line[i] == '?') 118 | return false; 119 | } 120 | return true; 121 | } 122 | 123 | char* createRegex(char* regex, char* start, char* end) { 124 | char* regex_start = regex; 125 | *regex++ = '^'; 126 | while (start < end) { 127 | if (*start == '*') { 128 | *regex++ = '.'; 129 | *regex++ = '*'; 130 | start++; 131 | } else if (*start == '?') { 132 | *regex++ = '.'; 133 | start++; 134 | } else if (*start == '.') { 135 | *regex++ = '\\'; 136 | *regex++ = '.'; 137 | start++; 138 | } else { 139 | *regex++ = *start++; 140 | } 141 | } 142 | *regex++ = '$'; 143 | 144 | return regex_start; 145 | } 146 | 147 | int calculateEndIndex(wildcard_groups_arr wildcard_groups, int j, int i) { 148 | int end_index = j + 1; 149 | 150 | for (; end_index < strlen(wildcard_groups.arr[i].wildcard_arg) && 151 | wildcard_groups.arr[i].wildcard_arg[end_index] != '/' && 152 | wildcard_groups.arr[i].wildcard_arg[end_index] != ' '; 153 | end_index++) 154 | ; 155 | 156 | return end_index; 157 | } 158 | 159 | void insertIfMatch(wildcard_groups_arr* wildcard_groups, char* prefix, DIR* current_dir, regex_t* re, 160 | int concat_index, bool is_dotfile, int i) { 161 | struct dirent* dir; 162 | int start_index = 0; 163 | 164 | while ((dir = readdir(current_dir)) != NULL) { 165 | if (regexec(re, dir->d_name, 0, NULL, 0) == 0) { 166 | if (dir->d_name[0] == '.' && !is_dotfile) { 167 | continue; 168 | } 169 | for (int j = 0; j < strlen(dir->d_name); j++) { 170 | if (dir->d_name[j] == ' ') { 171 | // insert escape \\ in front of whitespace 172 | insertCharAtPos(dir->d_name, j, '\\'); 173 | j++; 174 | } 175 | } 176 | char* prev_copy = calloc(strlen(prefix) + strlen(dir->d_name) + 1, sizeof(char)); 177 | strcpy(prev_copy, prefix); 178 | char* match = strcat(&prev_copy[concat_index], dir->d_name); 179 | 180 | if (strlen(wildcard_groups->arr[i].wildcard_arg) == 0) { 181 | wildcard_groups->arr[i].wildcard_arg = 182 | realloc(wildcard_groups->arr[i].wildcard_arg, (strlen(match) + 1) * sizeof(char)); 183 | strcpy(wildcard_groups->arr[i].wildcard_arg, match); 184 | } else { 185 | insertStringAtPos(&wildcard_groups->arr[i].wildcard_arg, match, start_index); 186 | } 187 | 188 | if (isLastRedirectionInLine(wildcard_groups->arr[i].wildcard_arg)) { 189 | start_index = strlen(wildcard_groups->arr[i].wildcard_arg); 190 | wildcard_groups->arr[i].wildcard_arg = 191 | realloc(wildcard_groups->arr[i].wildcard_arg, 192 | (strlen(wildcard_groups->arr[i].wildcard_arg) + 2) * sizeof(char)); 193 | insertCharAtPos(wildcard_groups->arr[i].wildcard_arg, start_index, ' '); 194 | start_index++; 195 | } 196 | free(prev_copy); 197 | } 198 | } 199 | } 200 | 201 | int calculateConcatIndex(char* prefix) { 202 | int concat_index = 0; 203 | for (; concat_index < strlen(prefix) && prefix[concat_index] != '/'; concat_index++) 204 | ; 205 | concat_index += (prefix[0] == '/') ? 0 : 1; 206 | 207 | return concat_index; 208 | } 209 | 210 | int calculatePrefixEnd(wildcard_groups_arr wildcard_groups, int j, int i) { 211 | int prefix_end = j; 212 | for (; prefix_end > 0 && wildcard_groups.arr[i].wildcard_arg[prefix_end - 1] != '/'; prefix_end--) 213 | ; 214 | return prefix_end; 215 | } 216 | 217 | void getPrefix(wildcard_groups_arr wildcard_groups, char* prefix, int i, int prefix_end) { 218 | if (wildcard_groups.arr[i].wildcard_arg[0] == '/') { 219 | strncpy(prefix, wildcard_groups.arr[i].wildcard_arg, prefix_end); 220 | } else { 221 | strcpy(prefix, "./"); 222 | strncpy(&prefix[2], wildcard_groups.arr[i].wildcard_arg, prefix_end); 223 | } 224 | } 225 | 226 | wildcard_groups_arr expandWildcardgroups(wildcard_groups_arr wildcard_groups) { 227 | for (int i = 0; i < wildcard_groups.len; i++) { 228 | for (int j = 0; j < strlen(wildcard_groups.arr[i].wildcard_arg); j++) { 229 | if (wildcard_groups.arr[i].wildcard_arg[j] == '*' || wildcard_groups.arr[i].wildcard_arg[j] == '?') { 230 | int prefix_end = calculatePrefixEnd(wildcard_groups, j, i); 231 | char* prefix = calloc(prefix_end + 3, sizeof(char)); 232 | getPrefix(wildcard_groups, prefix, i, prefix_end); 233 | 234 | char* start = &wildcard_groups.arr[i].wildcard_arg[prefix_end]; 235 | bool is_dotfile = start[0] == '.' ? true : false; 236 | 237 | int end_index = calculateEndIndex(wildcard_groups, j, i); 238 | char* end = &wildcard_groups.arr[i].wildcard_arg[end_index]; 239 | 240 | char* regex = calloc(((end - start) * 2) + 3, sizeof(char)); 241 | regex = createRegex(regex, start, end); 242 | regex_t re; 243 | if (regcomp(&re, regex, REG_EXTENDED) != 0) { 244 | perror("regex"); 245 | } 246 | 247 | removeSlice(&wildcard_groups.arr[i].wildcard_arg, 0, end_index); 248 | int concat_index = calculateConcatIndex(prefix); 249 | 250 | DIR* current_dir = opendir(prefix); 251 | if (current_dir == NULL) { 252 | // when wrong dir make wildcard empty so that foundAllWildcards is false 253 | fprintf(stderr, "psh: couldn't open directory: %s\n", prefix); 254 | strcpy(wildcard_groups.arr[0].wildcard_arg, ""); 255 | free(regex); 256 | free(prefix); 257 | regfree(&re); 258 | closedir(current_dir); 259 | return wildcard_groups; 260 | } 261 | 262 | insertIfMatch(&wildcard_groups, prefix, current_dir, &re, concat_index, is_dotfile, i); 263 | 264 | free(regex); 265 | free(prefix); 266 | regfree(&re); 267 | closedir(current_dir); 268 | } 269 | } 270 | } 271 | return wildcard_groups; 272 | } 273 | 274 | void replaceLineWithWildcards(char** line, wildcard_groups_arr wildcard_matches) { 275 | int verschoben_len = 0; 276 | for (int i = 0; i < wildcard_matches.len; i++) { 277 | removeSlice(line, wildcard_matches.arr[i].start + verschoben_len, 278 | wildcard_matches.arr[i].end + verschoben_len); 279 | insertStringAtPos(line, wildcard_matches.arr[i].wildcard_arg, wildcard_matches.arr[i].start + verschoben_len); 280 | verschoben_len += strlen(wildcard_matches.arr[i].wildcard_arg) - 281 | (wildcard_matches.arr[i].end - wildcard_matches.arr[i].start); 282 | } 283 | } 284 | 285 | void push(char* line, string_array* command_history) { 286 | if (command_history->len > 0) { 287 | for (int i = command_history->len; i > 0; i--) { 288 | if (i <= HISTORY_SIZE) { 289 | command_history->values[i] = command_history->values[i - 1]; 290 | } 291 | } 292 | } 293 | (command_history->len <= HISTORY_SIZE) ? command_history->len++ : command_history->len; 294 | command_history->values[0] = calloc(strlen(line) + 1, sizeof(char)); 295 | strcpy(command_history->values[0], line); 296 | } 297 | 298 | bool arrCmp(string_array arr1, string_array arr2) { 299 | if (arr1.len != arr2.len) { 300 | return false; 301 | } 302 | for (int i = 0; i < arr1.len; i++) { 303 | if (strcmp(arr1.values[i], arr2.values[i]) != 0) { 304 | return false; 305 | } 306 | } 307 | return true; 308 | } 309 | 310 | string_array getAllHistoryCommands() { 311 | size_t size = BUFFER * sizeof(char*); 312 | string_array result = {.len = 0, .values = calloc(BUFFER, sizeof(char*))}; 313 | char* file_path = joinFilePath(getenv("HOME"), "/.psh_history"); 314 | FILE* file_to_read = fopen(file_path, "r"); 315 | free(file_path); 316 | char* buf = NULL; 317 | int line_len; 318 | unsigned long buf_size; 319 | int i = 0; 320 | 321 | if (file_to_read == NULL) { 322 | return result; 323 | } 324 | 325 | while ((line_len = getline(&buf, &buf_size, file_to_read)) != -1) { 326 | if ((i * sizeof(char*)) >= size) { 327 | char** tmp; 328 | if ((tmp = realloc(result.values, size * 1.5)) == NULL) { 329 | perror("psh:"); 330 | } else { 331 | result.values = tmp; 332 | size *= 1.5; 333 | } 334 | } 335 | 336 | result.values[i] = calloc(strlen(buf), sizeof(char)); 337 | strncpy(result.values[i], buf, strlen(buf) - 1); 338 | i++; 339 | } 340 | result.len = i; 341 | 342 | free(buf); 343 | fclose(file_to_read); 344 | return result; 345 | } 346 | 347 | void writeCommandToGlobalHistory(char* cmd, string_array global_history) { 348 | char* file_path = joinFilePath(getenv("HOME"), "/.psh_history"); 349 | FILE* file_to_write = fopen(file_path, "a"); 350 | free(file_path); 351 | 352 | if (file_to_write == NULL) { 353 | fprintf(stderr, "psh: can't open ~/.psh_history\n"); 354 | return; 355 | } 356 | 357 | if (!inArray(cmd, global_history)) { 358 | fprintf(file_to_write, "%s\n", cmd); 359 | } 360 | 361 | fclose(file_to_write); 362 | } 363 | 364 | void cd(string_array splitted_line) { 365 | if (splitted_line.len == 2) { 366 | if (chdir(splitted_line.values[1]) == -1) { 367 | fprintf(stderr, "cd: %s: no such file or directory\n", splitted_line.values[1]); 368 | } 369 | } else if (splitted_line.len > 2) { 370 | fprintf(stderr, "cd: too many arguments\n"); 371 | } else { 372 | printf("usage: cd [path]\n"); 373 | } 374 | } 375 | 376 | bool callBuiltin(string_array splitted_line, function_by_name builtins[], int function_index) { 377 | if (strcmp(splitted_line.values[0], "exit") == 0) { 378 | return false; 379 | } 380 | (*builtins[function_index].func)(splitted_line); 381 | return true; 382 | } 383 | 384 | void pushToCommandHistory(char* line, string_array* command_history) { 385 | if (command_history->len == 0 || strcmp(command_history->values[0], line) != 0) { 386 | push(line, command_history); 387 | } 388 | } 389 | 390 | void removeWhitespaceTokens(token_index_arr* tokenized_line) { 391 | for (int i = 0; i < tokenized_line->len;) { 392 | if (tokenized_line->arr[i].token == WHITESPACE) { 393 | for (int j = i; j < tokenized_line->len - 1; j++) { 394 | tokenized_line->arr[j] = tokenized_line->arr[j + 1]; 395 | } 396 | tokenized_line->len--; 397 | } else { 398 | i++; 399 | } 400 | } 401 | } 402 | char* convertTokenToString(token_index_arr tokenized_line) { 403 | char* result = calloc(tokenized_line.len * 2 + 1, sizeof(char)); 404 | int string_index = 0; 405 | for (int i = 0; i < tokenized_line.len; i++) { 406 | // ignoring wildcards since they can be anywhere 407 | if (tokenized_line.arr[i].token != ASTERISK && tokenized_line.arr[i].token != QUESTION) { 408 | sprintf(&result[string_index], "%d", tokenized_line.arr[i].token); 409 | tokenized_line.arr[i].token >= 10 ? string_index += 2 : string_index++; 410 | } 411 | } 412 | return result; 413 | } 414 | 415 | bool isValidSyntax(token_index_arr tokenized_line) { 416 | char* string_rep = convertTokenToString(tokenized_line); 417 | bool result = false; 418 | regex_t re; 419 | regmatch_t rm[1]; 420 | // nums represent token enum values from types.h 421 | char* valid_syntax = "^((6|7|8|9|10)14)*(1((6|7|8|9|10)14)*(14)*((6|7|8|9|10)14)*((3((6|7|8|9|10)14)*2)((" 422 | "6|7|8|9|10)14)*(14)*((6|7|8|9|10)14)*|((4((6|7|8|9|10)14)*5)|(4((6|7|8|9|10)14)+))" 423 | "((6|7|8|9|10)14)*(14)*((6|7|8|9|10)14)*)*)?"; 424 | 425 | if (regcomp(&re, valid_syntax, REG_EXTENDED) != 0) { 426 | perror("psh:"); 427 | } 428 | if (regexec(&re, string_rep, 1, rm, 0) == 0 && rm->rm_eo - rm->rm_so == strlen(string_rep)) { 429 | result = true; 430 | } else { 431 | result = false; 432 | } 433 | regfree(&re); 434 | 435 | free(string_rep); 436 | return result; 437 | } 438 | 439 | string_array_token splitLineIntoSimpleCommands(char* line, token_index_arr tokenized_line) { 440 | char** line_arr = calloc(tokenized_line.len, sizeof(char*)); 441 | enum token* token_arr = calloc(tokenized_line.len, sizeof(enum token)); 442 | int j = 0; 443 | bool found_split = true; 444 | int start = 0; 445 | int i; 446 | token_arr[0] = CMD; 447 | for (i = 0; i < tokenized_line.len; i++) { 448 | if (found_split) { 449 | start = tokenized_line.arr[i].start; 450 | found_split = false; 451 | } 452 | if (tokenized_line.arr[i].token == PIPE) { 453 | int end = tokenized_line.arr[i].start; 454 | line_arr[j] = calloc(end - start + 1, sizeof(char)); 455 | strncpy(line_arr[j], &line[start], end - start); 456 | token_arr[j + 1] = PIPE_CMD; 457 | j++; 458 | found_split = true; 459 | } else if (tokenized_line.arr[i].token == AMPAMP) { 460 | int end = tokenized_line.arr[i].start; 461 | line_arr[j] = calloc(end - start + 1, sizeof(char)); 462 | strncpy(line_arr[j], &line[start], end - start); 463 | token_arr[j + 1] = AMP_CMD; 464 | j++; 465 | found_split = true; 466 | } 467 | } 468 | int end = strlen(line); 469 | line_arr[j] = calloc(end - start + 1, sizeof(char)); 470 | strncpy(line_arr[j], &line[start], end - start); 471 | 472 | string_array_token result = {.values = line_arr, .len = j + 1, .token_arr = token_arr}; 473 | return result; 474 | } 475 | 476 | string_array splitByTokens(char* line, token_index_arr token) { 477 | char** line_arr = calloc(token.len + 1, sizeof(char*)); 478 | 479 | int start; 480 | int end; 481 | for (int i = 0; i < token.len; i++) { 482 | if (token.arr[i].token == ARG && line[token.arr[i].start] == '\'' && line[token.arr[i].end - 1] == '\'') { 483 | start = token.arr[i].start + 1; 484 | end = token.arr[i].end - 1; 485 | } else { 486 | start = token.arr[i].start; 487 | end = token.arr[i].end; 488 | } 489 | line_arr[i] = calloc(end - start + 1, sizeof(char)); 490 | strncpy(line_arr[i], &line[start], end - start); 491 | } 492 | line_arr[token.len] = NULL; 493 | string_array result = {.values = line_arr, .len = token.len}; 494 | // free(token.arr); 495 | return result; 496 | } 497 | 498 | file_redirection_data parseForRedirectionFiles(string_array simple_command, token_index_arr token) { 499 | char* output_name = NULL; 500 | char* input_name = NULL; 501 | char* error_name = NULL; 502 | char* merge_name = NULL; 503 | bool output_append = false; 504 | bool error_append = false; 505 | bool merge_append = false; 506 | bool found_output = false; 507 | bool found_input = false; 508 | bool found_stderr = false; 509 | bool found_merge = false; 510 | 511 | for (int j = token.len - 2; j >= 0; j--) { 512 | if (!found_input && token.arr[j].token == LESS) { 513 | input_name = calloc(strlen(simple_command.values[j + 1]) + 1, sizeof(char)); 514 | strcpy(input_name, simple_command.values[j + 1]); 515 | removeEscapesString(&input_name); 516 | found_input = true; 517 | } 518 | if (!found_stderr && !found_output && !found_merge && 519 | (token.arr[j].token == AMP_GREAT || token.arr[j].token == AMP_GREATGREAT)) { 520 | merge_name = calloc(strlen(simple_command.values[j + 1]) + 1, sizeof(char)); 521 | strcpy(merge_name, simple_command.values[j + 1]); 522 | removeEscapesString(&merge_name); 523 | found_stderr = true; 524 | found_output = true; 525 | found_merge = true; 526 | merge_append = token.arr[j].token == AMP_GREATGREAT ? true : false; 527 | } 528 | if (token.arr[j].token == GREAT || token.arr[j].token == GREATGREAT) { 529 | if (!found_stderr && simple_command.values[j][0] == '2') { 530 | error_name = calloc(strlen(simple_command.values[j + 1]) + 1, sizeof(char)); 531 | strcpy(error_name, simple_command.values[j + 1]); 532 | removeEscapesString(&error_name); 533 | found_stderr = true; 534 | error_append = token.arr[j].token == GREATGREAT ? true : false; 535 | } 536 | if (!found_output && simple_command.values[j][0] != '2') { 537 | output_name = calloc(strlen(simple_command.values[j + 1]) + 1, sizeof(char)); 538 | strcpy(output_name, simple_command.values[j + 1]); 539 | removeEscapesString(&output_name); 540 | found_output = true; 541 | output_append = token.arr[j].token == GREATGREAT ? true : false; 542 | } 543 | } 544 | } 545 | 546 | return (file_redirection_data){.output_filename = output_name, 547 | .input_filename = input_name, 548 | .output_append = output_append, 549 | .stderr_filename = error_name, 550 | .stderr_append = error_append, 551 | .merge_filename = merge_name, 552 | .merge_append = merge_append}; 553 | } 554 | 555 | bool fileExists(char* name) { 556 | if (access(name, 0) == 0) { 557 | return true; 558 | } else { 559 | return false; 560 | } 561 | } 562 | 563 | void removeArrayElement(string_array* splitted, int pos) { 564 | for (int i = pos; i < splitted->len; i++) { 565 | splitted->values[i] = splitted->values[i + 1]; 566 | } 567 | splitted->len--; 568 | } 569 | 570 | void stripRedirections(string_array* splitted_line, token_index_arr token) { 571 | int j = 0; 572 | int len = splitted_line->len; 573 | for (int i = 0; i < len;) { 574 | if (token.arr[i].token >= GREATGREAT && token.arr[i].token <= AMP_GREATGREAT) { 575 | removeArrayElement(splitted_line, j); // removes redirection 576 | removeArrayElement(splitted_line, j); // removes filename 577 | i += 2; 578 | } else { 579 | j++; 580 | i++; 581 | } 582 | } 583 | } 584 | 585 | void replaceAliases(char** line, int len) { 586 | for (int i = 0; i < len; i++) { 587 | replaceAliasesString(&line[i]); 588 | } 589 | } 590 | 591 | void free_wildcard_groups(wildcard_groups_arr arr) { 592 | for (int i = 0; i < arr.len; i++) { 593 | free(arr.arr[i].wildcard_arg); 594 | } 595 | free(arr.arr); 596 | } 597 | 598 | bool foundAllWildcards(wildcard_groups_arr arr) { 599 | for (int i = 0; i < arr.len; i++) { 600 | if (strcmp(arr.arr[i].wildcard_arg, "") == 0) { 601 | free_wildcard_groups(arr); 602 | return false; 603 | } 604 | } 605 | free_wildcard_groups(arr); 606 | return true; 607 | } 608 | 609 | bool replaceWildcards(char** line) { 610 | token_index_arr token = tokenizeLine(*line); 611 | wildcard_groups_arr groups = groupWildcards(*line, token); 612 | wildcard_groups_arr wildcard_matches = expandWildcardgroups(groups); 613 | replaceLineWithWildcards(line, wildcard_matches); 614 | 615 | free(token.arr); 616 | 617 | return foundAllWildcards(wildcard_matches); 618 | } 619 | 620 | void free_string_array_token(string_array_token simple_commands) { 621 | for (int i = 0; i < simple_commands.len; i++) { 622 | free(simple_commands.values[i]); 623 | } 624 | free(simple_commands.values); 625 | free(simple_commands.token_arr); 626 | } 627 | 628 | void free_file_info(file_redirection_data file_info) { 629 | free(file_info.input_filename); 630 | free(file_info.output_filename); 631 | free(file_info.stderr_filename); 632 | free(file_info.merge_filename); 633 | } 634 | 635 | void resetIO(int* tmpin, int* tmpout, int* tmperr) { 636 | dup2(*tmpin, 0); 637 | dup2(*tmpout, 1); 638 | dup2(*tmperr, 2); 639 | close(*tmpin); 640 | close(*tmpout); 641 | close(*tmperr); 642 | *tmpin = dup(0); 643 | *tmpout = dup(1); 644 | *tmperr = dup(2); 645 | } 646 | 647 | void outputRedirection(file_redirection_data file_info, int pd[2], int* fdout, int* fdin, int tmpout, int i, 648 | string_array_token simple_commands_arr) { 649 | if (file_info.output_filename != NULL) { 650 | *fdout = file_info.output_append ? open(file_info.output_filename, O_RDWR | O_CREAT | O_APPEND, 0666) 651 | : open(file_info.output_filename, O_RDWR | O_CREAT | O_TRUNC, 0666); 652 | } else if (i < simple_commands_arr.len - 1 && simple_commands_arr.token_arr[i + 1] == PIPE_CMD) { 653 | pipe(pd); 654 | *fdout = pd[1]; 655 | *fdin = pd[0]; 656 | } else { 657 | *fdout = dup(tmpout); 658 | } 659 | } 660 | 661 | void errorRedirection(file_redirection_data file_info, int* fderr, int tmperr) { 662 | if (file_info.stderr_filename != NULL) { 663 | *fderr = file_info.stderr_append ? open(file_info.stderr_filename, O_RDWR | O_CREAT | O_APPEND, 0666) 664 | : open(file_info.stderr_filename, O_RDWR | O_CREAT | O_TRUNC, 0666); 665 | } else { 666 | *fderr = dup(tmperr); 667 | } 668 | } 669 | 670 | void mergeRedirection(file_redirection_data file_info, int* fdout) { 671 | *fdout = file_info.merge_append ? open(file_info.merge_filename, O_RDWR | O_CREAT | O_APPEND, 0666) 672 | : open(file_info.merge_filename, O_RDWR | O_CREAT | O_TRUNC, 0666); 673 | dup2(*fdout, STDOUT_FILENO); 674 | dup2(*fdout, STDERR_FILENO); 675 | close(*fdout); 676 | } 677 | 678 | string_array getPathBins() { 679 | string_array PATH_ARR = splitString(getenv("PATH"), ':'); 680 | string_array all_files_in_dir = getAllFilesInDir(&PATH_ARR); 681 | free_string_array(&PATH_ARR); 682 | string_array removed_dots = removeDots(&all_files_in_dir); 683 | 684 | return removeDuplicates(&removed_dots); 685 | } 686 | 687 | bool wildcardLogic(string_array_token simple_commands_arr, int* fdout, int* fderr, int tmpout, int tmperr, int i) { 688 | if (strchr(simple_commands_arr.values[i], '*') != NULL || strchr(simple_commands_arr.values[i], '?') != NULL) { 689 | if (!replaceWildcards(&simple_commands_arr.values[i])) { 690 | *fdout = dup(tmpout); 691 | *fderr = dup(tmperr); 692 | dup2(*fderr, STDERR_FILENO); 693 | close(*fderr); 694 | dup2(*fdout, STDOUT_FILENO); 695 | close(*fdout); 696 | fprintf(stderr, "psh: no wildcard matches found\n"); 697 | return false; 698 | } 699 | } 700 | return true; 701 | } 702 | 703 | bool foundBuiltin(string_array splitted_line, builtins_array BUILTINS, int* builtin_index) { 704 | return (splitted_line.len > 0 && (*builtin_index = isBuiltin(splitted_line.values[0], BUILTINS)) != -1) ? true 705 | : false; 706 | } 707 | 708 | char* parseVarName(char* buf) { 709 | char* result = calloc(strlen(buf) + 1, sizeof(char)); 710 | for (int i = 0; i < strlen(buf) && buf[i] != '='; i++) { 711 | result[i] = buf[i]; 712 | } 713 | return result; 714 | } 715 | 716 | env_var_arr parseRcFileForEnv() { 717 | char* file_path = joinFilePath(getenv("HOME"), "/.pshrc"); 718 | FILE* file_to_read = fopen(file_path, "r"); 719 | 720 | if (file_to_read == NULL) { 721 | char answer; 722 | fprintf(stderr, "psh: no ~/.pshrc file found. Want to create one? [y/n]: "); 723 | answer = getchar(); 724 | getchar(); // skip \n 725 | if (answer == 'y') { 726 | FILE* file_created = fopen(file_path, "w"); 727 | fprintf(file_created, "TERM=\"linux\"\nPATH=\"/usr/local/bin/$\"\n"); 728 | fclose(file_created); 729 | file_to_read = fopen(file_path, "r"); 730 | } else { 731 | exit(0); 732 | } 733 | } 734 | 735 | size_t len = 0; 736 | char* buf = NULL; 737 | size_t line_len; 738 | char** var_names = calloc(64, sizeof(char*)); 739 | char** values = calloc(64, sizeof(char*)); 740 | 741 | int i = 0; 742 | for (; (line_len = getline(&buf, &len, file_to_read)) != -1; i++) { 743 | var_names[i] = parseVarName(buf); 744 | values[i] = calloc(strlen(buf), sizeof(char)); 745 | strncpy(values[i], &buf[strlen(var_names[i]) + 2], strlen(buf) - (strlen(var_names[i]) + 4)); 746 | } 747 | free(file_path); 748 | fclose(file_to_read); 749 | free(buf); 750 | 751 | return (env_var_arr){.len = i, .var_names = var_names, .values = values}; 752 | } 753 | 754 | void setRcVars(env_var_arr envs) { 755 | for (int i = 0; i < envs.len; i++) { 756 | if (envs.values[i][strlen(envs.values[i]) - 1] == '$') { 757 | // $ Means concat with already existant VAR 758 | envs.values[i][strlen(envs.values[i]) - 1] = '\0'; 759 | insertCharAtPos(envs.values[i], 0, ':'); 760 | char* joined_envs = joinFilePath(getenv(envs.var_names[i]), envs.values[i]); 761 | setenv(envs.var_names[i], joined_envs, 1); 762 | free(joined_envs); 763 | } else { 764 | // can overwrite VAR 765 | setenv(envs.var_names[i], envs.values[i], 1); 766 | } 767 | } 768 | } 769 | 770 | void free_env_var_arr(env_var_arr arr) { 771 | for (int i = 0; i < arr.len; i++) { 772 | free(arr.values[i]); 773 | free(arr.var_names[i]); 774 | } 775 | free(arr.values); 776 | free(arr.var_names); 777 | } 778 | 779 | volatile sig_atomic_t ctrlc_hit = false; 780 | void ctrlCHandler(int sig) { ctrlc_hit = true; } 781 | 782 | #ifndef TEST 783 | 784 | int main(int argc, char* argv[]) { 785 | char* line; 786 | char dir[PATH_MAX]; 787 | bool loop = true; 788 | env_var_arr ENV = parseRcFileForEnv(); 789 | setRcVars(ENV); 790 | free_env_var_arr(ENV); 791 | string_array command_history = {.len = 0, .values = calloc(HISTORY_SIZE, sizeof(char*))}; 792 | string_array PATH_BINS = getPathBins(); 793 | string_array global_command_history = getAllHistoryCommands(); 794 | 795 | char* current_dir = getcwd(dir, sizeof(dir)); 796 | char* last_two_dirs = getLastTwoDirs(current_dir); 797 | function_by_name builtin_funcs[] = { 798 | {"exit", NULL}, 799 | {"cd", cd}, 800 | }; 801 | builtins_array BUILTINS = { 802 | .array = builtin_funcs, 803 | .len = sizeof(builtin_funcs) / sizeof(builtin_funcs[0]), 804 | }; 805 | 806 | CLEAR_SCREEN; 807 | 808 | signal(SIGHUP, SIG_DFL); // Stops process when terminal is closed 809 | struct sigaction sa; 810 | memset(&sa, 0, sizeof(struct sigaction)); 811 | 812 | while (loop) { 813 | ctrlc_hit = false; 814 | printf("\n"); 815 | printPrompt(last_two_dirs, CYAN); 816 | 817 | // change signal flag to exit even in when blocking func 818 | sa.sa_flags = 0; 819 | sa.sa_handler = ctrlCHandler; 820 | sigaction(SIGINT, &sa, NULL); 821 | 822 | line = readLine(PATH_BINS, last_two_dirs, &command_history, global_command_history, BUILTINS); 823 | if (ctrlc_hit) { 824 | free(line); 825 | continue; 826 | } 827 | // change signal flag to default behaviour for child process 828 | sa.sa_flags = SA_RESTART; 829 | sa.sa_handler = ctrlCHandler; 830 | sigaction(SIGINT, &sa, NULL); 831 | 832 | token_index_arr tokenized_line = tokenizeLine(line); 833 | removeWhitespaceTokens(&tokenized_line); 834 | 835 | if (tokenized_line.len > 0 && isValidSyntax(tokenized_line)) { 836 | string_array_token simple_commands_arr = splitLineIntoSimpleCommands(line, tokenized_line); 837 | replaceAliases(simple_commands_arr.values, simple_commands_arr.len); 838 | 839 | int pd[2]; 840 | int tmpin = dup(0); 841 | int tmpout = dup(1); 842 | int tmperr = dup(2); 843 | int fdout; 844 | int fderr; 845 | pid_t pid; 846 | int fdin = -1; 847 | 848 | for (int i = 0; i < simple_commands_arr.len; i++) { 849 | if (!wildcardLogic(simple_commands_arr, &fdout, &fderr, tmpout, tmperr, i)) { 850 | continue; 851 | } 852 | token_index_arr token = tokenizeLine(simple_commands_arr.values[i]); 853 | removeWhitespaceTokens(&token); 854 | string_array splitted_line = splitByTokens(simple_commands_arr.values[i], token); 855 | file_redirection_data file_info = parseForRedirectionFiles(splitted_line, token); 856 | stripRedirections(&splitted_line, token); 857 | free(token.arr); 858 | removeEscapesArr(&splitted_line); 859 | int builtin_index; 860 | 861 | if (file_info.input_filename != NULL) { 862 | if (fileExists(file_info.input_filename)) { 863 | fdin = open(file_info.input_filename, O_RDONLY); 864 | } else { 865 | fprintf(stderr, "psh: no such file %s\n", file_info.input_filename); 866 | free_string_array(&splitted_line); 867 | free_file_info(file_info); 868 | continue; 869 | } 870 | } else if (simple_commands_arr.token_arr[i] == AMP_CMD) { 871 | resetIO(&tmpin, &tmpout, &tmperr); 872 | } 873 | 874 | if (foundBuiltin(splitted_line, BUILTINS, &builtin_index)) { 875 | if (!callBuiltin(splitted_line, BUILTINS.array, builtin_index)) { 876 | free_string_array(&splitted_line); 877 | free_file_info(file_info); 878 | loop = false; 879 | break; 880 | } 881 | current_dir = getcwd(dir, sizeof(dir)); 882 | free(last_two_dirs); 883 | last_two_dirs = getLastTwoDirs(current_dir); 884 | 885 | pushToCommandHistory(line, &command_history); 886 | 887 | } else { 888 | int w_status; 889 | 890 | dup2(fdin, STDIN_FILENO); 891 | close(fdin); 892 | if (file_info.merge_filename != NULL) { 893 | mergeRedirection(file_info, &fdout); 894 | } else { 895 | outputRedirection(file_info, pd, &fdout, &fdin, tmpout, i, simple_commands_arr); 896 | errorRedirection(file_info, &fderr, tmperr); 897 | 898 | dup2(fderr, STDERR_FILENO); 899 | close(fderr); 900 | dup2(fdout, STDOUT_FILENO); 901 | close(fdout); 902 | } 903 | 904 | if (splitted_line.len > 0) { 905 | if ((pid = fork()) == 0) { 906 | int error = execvp(splitted_line.values[0], splitted_line.values); 907 | if (error) { 908 | fprintf(stderr, "psh: couldn't find command %s\n", splitted_line.values[0]); 909 | } 910 | } 911 | 912 | if (waitpid(pid, &w_status, WUNTRACED | WCONTINUED) == -1) { 913 | exit(EXIT_FAILURE); 914 | } 915 | } 916 | } 917 | free_string_array(&splitted_line); 918 | free_file_info(file_info); 919 | } 920 | dup2(tmpin, 0); 921 | dup2(tmpout, 1); 922 | dup2(tmperr, 2); 923 | close(tmpin); 924 | close(tmpout); 925 | close(tmperr); 926 | free_string_array_token(simple_commands_arr); 927 | } else { 928 | if (!isOnlyDelimeter(line, ' ')) { 929 | fprintf(stderr, "psh: syntax error\n"); 930 | } 931 | } 932 | writeCommandToGlobalHistory(line, global_command_history); 933 | pushToCommandHistory(line, &command_history); 934 | free(tokenized_line.arr); 935 | free(line); 936 | } 937 | free_string_array(&global_command_history); 938 | free_string_array(&command_history); 939 | free_string_array(&PATH_BINS); 940 | free(last_two_dirs); 941 | } 942 | 943 | #endif /* !TEST */ 944 | --------------------------------------------------------------------------------