├── clash.gif ├── words.py ├── .gitignore ├── Makefile ├── src ├── proc-util.cc ├── clash.h ├── pipeline.cc ├── job.cc ├── clash.cc ├── pipeline.h ├── job.h ├── string-util.h ├── string-util.cc ├── proc-util.h ├── log.h ├── shell.cc ├── environment.cc ├── command.h ├── arguments.cc ├── shell.h ├── environment.h ├── command.cc ├── file-util.h ├── arguments.h ├── job-parser.h ├── file-util.cc └── job-parser.cc ├── README.md ├── failures_list_reasons ├── test.out ├── test.py └── test-fixed.py /clash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/clash/HEAD/clash.gif -------------------------------------------------------------------------------- /words.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Prints out its argument words (except for the first one, containing the 4 | # program name). Used for testing clash. 5 | 6 | import sys 7 | 8 | i = 0; 9 | for word in sys.argv: 10 | if (i != 0): 11 | print("Word %d: %s" %(i, word)) 12 | i += 1 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built files 2 | clash 3 | 4 | # Test runner files 5 | __stdout 6 | __test/ 7 | 8 | # Prerequisites 9 | *.d 10 | 11 | # OS files 12 | .DS_Store 13 | 14 | # Compiled Object files 15 | *.slo 16 | *.lo 17 | *.o 18 | *.obj 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Compiled Dynamic libraries 25 | *.so 26 | *.dylib 27 | *.dll 28 | 29 | # Fortran module files 30 | *.mod 31 | *.smod 32 | 33 | # Compiled Static libraries 34 | *.lai 35 | *.la 36 | *.a 37 | *.lib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # use the C++ compiler 2 | CC = clang++ 3 | 4 | # Program name 5 | TARGET ?= clash 6 | 7 | # Source code folder 8 | SRC_DIR ?= ./src 9 | 10 | SRCS := $(shell find $(SRC_DIR) -name *.cc -or -name *.c -or -name *.s) 11 | OBJS := $(addsuffix .o,$(basename $(SRCS))) 12 | DEPS := $(OBJS:.o=.d) 13 | 14 | INC_DIRS := $(shell find $(SRC_DIR) -type d) 15 | INC_FLAGS := $(addprefix -I,$(INC_DIRS)) 16 | 17 | # compiler flags: 18 | # -MMD lists user header files used by the source program 19 | # -MP emits dummy dependency rules (use with -MMD) 20 | # -Wall turns on most, but not all, compiler warnings 21 | # -std=c++17 use C++17 dialect 22 | CPPFLAGS ?= $(INC_FLAGS) -MMD -MP -Wall -std=c++17 23 | 24 | LDFLAGS ?= -lreadline 25 | 26 | $(TARGET): $(OBJS) 27 | $(CC) $(LDFLAGS) $(OBJS) -o $@ $(LOADLIBES) $(LDLIBS) 28 | 29 | .PHONY: clean 30 | clean: 31 | $(RM) $(TARGET) $(OBJS) $(DEPS) 32 | 33 | -include $(DEPS) -------------------------------------------------------------------------------- /src/proc-util.cc: -------------------------------------------------------------------------------- 1 | #include "proc-util.h" 2 | 3 | pid_t ProcUtil::CreateProcess() { 4 | pid_t pid = fork(); 5 | if (pid == -1) { 6 | throw ProcException("Unable to create new process"); 7 | } 8 | return pid; 9 | } 10 | 11 | void ProcUtil::SetCurrentWorkingDirectory(const string& new_cwd) { 12 | if (chdir(new_cwd.c_str()) == -1) { 13 | throw ProcException(new_cwd + ": No such file or directory"); 14 | } 15 | } 16 | 17 | string ProcUtil::GetCurrentWorkingDirectory() { 18 | char cwd[PATH_MAX]; 19 | if (getcwd(cwd, sizeof(cwd)) == NULL) { 20 | throw ProcException(cwd + string(": No such file or directory")); 21 | } 22 | return string(cwd); 23 | } 24 | 25 | string ProcUtil::GetUserHomeDirectory(const string& user) { 26 | passwd * pw = getpwnam(user.c_str()); 27 | 28 | if (pw == NULL) { 29 | return ""; 30 | } 31 | 32 | char * home_dir = pw->pw_dir; 33 | return string(home_dir); 34 | } 35 | -------------------------------------------------------------------------------- /src/clash.h: -------------------------------------------------------------------------------- 1 | /** 2 | * The entry point into the Clash program. 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | #include "arguments.h" 11 | #include "log.h" 12 | #include "shell.h" 13 | 14 | using namespace std; 15 | 16 | /** 17 | * Help text for the ./clash command line program. 18 | */ 19 | static const string INTRO_TEXT = 20 | R"(Clash - A Simple Bash-Like Shell 21 | 22 | Usage: 23 | ./clash [options] 24 | 25 | If no arguments are present, then an interactive REPL is started. If a single 26 | file name argument is provided, the commands are read from that file. If a "-c" 27 | argument is provided, then commands are read from a string. 28 | 29 | Examples: 30 | Start an interactive REPL. 31 | ./clash 32 | 33 | Read and execute commands from a file. 34 | ./clash shell-script.sh 35 | 36 | Read and excute commands from a string. 37 | ./clash -c "echo hello world" 38 | 39 | Read commands from stdin. 40 | echo "echo hello from stdin" | ./clash 41 | )"; 42 | -------------------------------------------------------------------------------- /src/pipeline.cc: -------------------------------------------------------------------------------- 1 | #include "pipeline.h" 2 | 3 | void Pipeline::RunAndWait(int pipeline_source, int pipeline_sink) { 4 | if (commands.size() == 0) { 5 | return; 6 | } 7 | 8 | if (commands.size() == 1) { 9 | commands[0].Run(pipeline_source, pipeline_sink); 10 | commands[0].Wait(); 11 | return; 12 | } 13 | 14 | vector fds = FileUtil::CreatePipe(); 15 | int command_sink = fds[1]; 16 | int command_source = fds[0]; 17 | 18 | commands[0].Run(pipeline_source, command_sink); 19 | FileUtil::CloseDescriptor(command_sink); 20 | 21 | for (size_t i = 1; i < commands.size() - 1; i++) { 22 | command_source = fds[0]; 23 | fds = FileUtil::CreatePipe(); 24 | command_sink = fds[1]; 25 | 26 | commands[i].Run(command_source, command_sink); 27 | 28 | FileUtil::CloseDescriptor(command_source); 29 | FileUtil::CloseDescriptor(command_sink); 30 | } 31 | 32 | command_source = fds[0]; 33 | commands[commands.size() - 1].Run(command_source, pipeline_sink); 34 | FileUtil::CloseDescriptor(command_source); 35 | 36 | for (Command& command : commands) { 37 | command.Wait(); 38 | } 39 | } 40 | 41 | string Pipeline::ToString() { 42 | string result = "Pipeline:"; 43 | for (Command& command : commands) { 44 | result += "\n " + command.ToString(); 45 | } 46 | return result; 47 | } 48 | -------------------------------------------------------------------------------- /src/job.cc: -------------------------------------------------------------------------------- 1 | #include "job.h" 2 | 3 | Job::Job(ParsedJob& parsed_job, Environment& env) : env(env) { 4 | for (ParsedPipeline& parsed_pipeline : parsed_job.pipelines) { 5 | vector commands; 6 | 7 | for (ParsedCommand& parsed_command : parsed_pipeline.commands) { 8 | Command command(parsed_command, env); 9 | commands.push_back(command); 10 | } 11 | 12 | Pipeline pipeline(commands); 13 | pipelines.push_back(pipeline); 14 | parsed_pipelines.push_back(parsed_pipeline); 15 | } 16 | } 17 | 18 | void Job::RunAndWait(int job_source, int job_sink) { 19 | // ParsedPipeline pipeline = pipelines[0]; 20 | // for (int i = 0; i < pipelines.size(); i++) { 21 | // pipeline.RunAndWait(job_source, job_sink); 22 | // Job updated_job = job_parser.Parse(pipeline.remaining_job_str, env); 23 | // pipeline = updated_job[0]; //ugly hack to get it to use variable 24 | // //assignment from the first command run. 25 | // } 26 | if (pipelines.size() == 0) return; 27 | pipelines[0].RunAndWait(job_source, job_sink); 28 | //assumes follow will work, because it already did the first time & 29 | // string is identical 30 | ParsedJob latter = job_parser.Parse(parsed_pipelines[0].remaining_job_str, env); 31 | Job updated_job(latter, env); 32 | updated_job.RunAndWait(job_source, job_sink); 33 | //even grosser recursive implementation! 34 | } 35 | 36 | string Job::ToString() { 37 | string result = "Job:"; 38 | for (Pipeline& pipeline : pipelines) { 39 | result += "\n " + pipeline.ToString(); 40 | } 41 | return result; 42 | } 43 | -------------------------------------------------------------------------------- /src/clash.cc: -------------------------------------------------------------------------------- 1 | #include "clash.h" 2 | 3 | LogType LOG_LEVEL = INFO; 4 | 5 | int main(int argc, char* argv[]) { 6 | Arguments args(INTRO_TEXT); 7 | args.RegisterBool("help", "Print help message"); 8 | args.RegisterAlias('h', "help"); 9 | 10 | args.RegisterBool("verbose", "Show debug logs"); 11 | args.RegisterAlias('v', "verbose"); 12 | 13 | args.RegisterBool("quiet", "Hide all logs except errors"); 14 | args.RegisterAlias('q', "quiet"); 15 | 16 | args.RegisterString("command", "Run command"); 17 | args.RegisterAlias('c', "command"); 18 | 19 | try { 20 | args.Parse(argc, argv); 21 | } catch (ArgumentsException& err) { 22 | error("%s", err.what()); 23 | return 2; 24 | } 25 | 26 | if (args.GetBool("quiet")) { 27 | LOG_LEVEL = ERROR; 28 | } else if (args.GetBool("verbose")) { 29 | LOG_LEVEL = DEBUG; 30 | } 31 | 32 | if (args.GetBool("help")) { 33 | printf("%s\n", args.GetHelpText().c_str()); 34 | return EXIT_SUCCESS; 35 | } 36 | 37 | Shell shell(argc, argv); 38 | 39 | string command = args.GetString("command"); 40 | if (!command.empty()) { 41 | if (!shell.ParseString(command)) { 42 | return -1; 43 | } 44 | return shell.RunJobsAndWait() ? EXIT_SUCCESS : -1; 45 | } 46 | 47 | vector unnamed_args = args.GetUnnamed(); 48 | if (unnamed_args.size() > 0) { 49 | const string& file_path = unnamed_args[0]; 50 | if (!shell.ParseFile(file_path)) { 51 | return 127; 52 | } 53 | return shell.RunJobsAndWait() ? EXIT_SUCCESS : -1; 54 | } 55 | 56 | return shell.StartRepl(); 57 | } 58 | -------------------------------------------------------------------------------- /src/pipeline.h: -------------------------------------------------------------------------------- 1 | /** 2 | * A set of one or more "commands", where the input from one command flows into 3 | * the next command. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include "command.h" 12 | #include "file-util.h" 13 | #include "log.h" 14 | 15 | using namespace std; 16 | 17 | class Pipeline { 18 | public: 19 | /** 20 | * Create a new pipeline of commands. Input from one command flows into 21 | * the next command. 22 | */ 23 | Pipeline(vector commands) : commands(commands) {} 24 | 25 | /** 26 | * Run the pipeline and wait for all the commands contained within to 27 | * finish running. This runs all the commands that make up the pipeline 28 | * simultaneously and blocks until all of them finish running. 29 | * 30 | * The given arguments, pipeline_source and pipeline_sink, are used to 31 | * configure the stdin of the first process and the stdout of the last 32 | * process, respectively. 33 | * 34 | * @param pipeline_source The file descriptor to use as stdin to the first 35 | * command in the pipeline 36 | * @param pipeline_sink The file descriptor to use as the stdout of the 37 | * final command in the pipeline 38 | */ 39 | void RunAndWait(int pipeline_source = STDIN_FILENO, int pipeline_sink = STDOUT_FILENO); 40 | 41 | /** 42 | * Returns a readable string representation of the pipeline, including 43 | * the contained commands. 44 | */ 45 | string ToString(); 46 | private: 47 | /** 48 | * List of all the commands that make up this pipeline. 49 | */ 50 | vector commands; 51 | }; 52 | -------------------------------------------------------------------------------- /src/job.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Job class which represents a job to be run by the terminal. Offers 3 | * functionality for parsing a user-entered string. Handles jobs that are 4 | * composed of many pipelines and many individual commands. 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | #include "command.h" 13 | #include "environment.h" 14 | #include "job-parser.h" 15 | #include "log.h" 16 | #include "pipeline.h" 17 | 18 | using namespace std; 19 | 20 | class Job { 21 | public: 22 | /* 23 | * Construct a new job based on the text entered on the terminal by the 24 | * user. This job can be "run", meaning that the pipelines and commands 25 | * contained within will be executed. The job is parsed and executed 26 | * in a Bash-like manner, obeying the rules of Bash as much as possible. 27 | * 28 | * @param job_str The line (or lines) of text entered on the terminal 29 | * by the user. 30 | */ 31 | Job(ParsedJob& job, Environment& env); 32 | 33 | /** 34 | * Run the job, including all pipelines and commands contained within. 35 | * Blocks until the entire job finishes running. 36 | * 37 | * May throw an exception if the job string is invalid or if there is 38 | * an error running the job for any reason (e.g. the program is missing, 39 | * the system ran out of file descriptors, etc.) 40 | */ 41 | void RunAndWait(int job_source = STDIN_FILENO, int job_sink = STDOUT_FILENO); 42 | 43 | /** 44 | * Returns a readable string representation of the job, including the 45 | * contained pipelines and commands. 46 | */ 47 | string ToString(); 48 | 49 | private: 50 | /** 51 | * The line (or lines) of text entered on the terminal by the user 52 | */ 53 | string original_job_str; 54 | 55 | /** 56 | * Instance of the job parser; used to actually parse the string 57 | * entered by the user. 58 | */ 59 | JobParser job_parser; 60 | 61 | /** 62 | * A job is composed of one or more pipelines, which are stored here 63 | * after the job string has been parsed. 64 | */ 65 | vector pipelines; 66 | vector parsed_pipelines; 67 | 68 | Environment& env; 69 | }; 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Clash logo 4 |
5 |
6 | Clash - A Simple Bash-Like Shell 7 |
8 |
9 |

10 | 11 | ## What is Clash? 12 | 13 | A simple Bash-like shell. Assignment instructions are [here](https://web.stanford.edu/~ouster/cgi-bin/cs190-winter19/clash.php). 14 | 15 | ## What is Bash? 16 | 17 | Bash is a Unix shell and command language written by Brian Fox for the GNU 18 | Project as a free software replacement for the Bourne shell. First 19 | released in 1989, it has been distributed widely as the default login shell 20 | for most Linux distributions and Apple's macOS (formerly OS X). A version is 21 | also available for Windows 10. It is also the default user shell in Solaris 22 | 11. 23 | 24 | Bash is a command processor that typically runs in a text window where the user 25 | types commands that cause actions. Bash can also read and execute commands from 26 | a file, called a shell script. Like all Unix shells, it supports filename 27 | globbing (wildcard matching), piping, here documents, command substitution, 28 | variables, and control structures for condition-testing and iteration. The 29 | keywords, syntax and other basic features of the language are all copied from 30 | sh. Other features, e.g., history, are copied from csh and ksh. Bash is a 31 | POSIX-compliant shell, but with a number of extensions. 32 | 33 | The shell's name is an acronym for Bourne-again shell, a pun on the name of the 34 | Bourne shell that it replaces and on the common term "born again". 35 | 36 | (From [Wikipedia – The Free Encyclopedia](https://en.wikipedia.org/wiki/Bash_(Unix_shell))). 37 | 38 | ## Quick start 39 | 40 | Get started super quickly: 41 | 42 | ```bash 43 | make 44 | ./clash 45 | ``` 46 | 47 | ## Full usage instructions 48 | 49 | ```bash 50 | $ ./clash --help 51 | Clash - A Simple Bash-Like Shell 52 | 53 | Usage: 54 | ./clash [options] 55 | 56 | If no arguments are present, then an interactive REPL is started. If a single 57 | file name argument is provided, the commands are read from that file. If a "-c" 58 | argument is provided, then commands are read from a string. 59 | 60 | Examples: 61 | Start an interactive REPL. 62 | ./clash 63 | 64 | Read and execute commands from a file. 65 | ./clash shell-script.sh 66 | 67 | Read and excute commands from a string. 68 | ./clash -c "echo hello world" 69 | 70 | Read commands from stdin. 71 | echo "echo hello from stdin" | ./clash 72 | ``` 73 | 74 | # License 75 | 76 | Copyright (c) Feross Aboukhadijeh and Jake McKinnon 77 | -------------------------------------------------------------------------------- /src/string-util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * String utility functions. 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "log.h" 15 | 16 | using namespace std; 17 | 18 | class StringUtil { 19 | public: 20 | /** 21 | * Split the given string str into a vector of strings using the given 22 | * single-character delimiter delim. 23 | * 24 | * Example: 25 | * Split("12.34.56.78:9000", ":") 26 | * 27 | * Returns a vector with two elements: "12.34.56.78", "9000" 28 | * 29 | * @param str The string to split 30 | * @param delim The delimiter to search for 31 | * @return Vector of result strings 32 | */ 33 | static vector Split(const string &str, const string &delim); 34 | static vector Split(const char * str, const string &delim); 35 | 36 | /** 37 | * Return a new string with space to the right of the given string str 38 | * so that it is at least size characters wide. 39 | * 40 | * @param str The string to pad 41 | * @param size The size to ensure the string is padded to 42 | * @return The result string 43 | */ 44 | static string PadRight(string const& str, size_t size); 45 | 46 | /** 47 | * Return a new string with space to the left of the given string str so 48 | * that it is at least size characters wide. 49 | * 50 | * @param str The string to pad 51 | * @param size The size to ensure the string is padded to 52 | * @return The result string 53 | */ 54 | static string PadLeft(string const& str, size_t size); 55 | 56 | /** 57 | * Return a new string with whitespace trimmed from the start and end of 58 | * the given string, str. 59 | * 60 | * @param str String to trim 61 | */ 62 | static string Trim(std::string &str); 63 | 64 | /** 65 | * Return a new string with whitespace trimmed from the start of the 66 | * given string, str. 67 | * 68 | * @param str String to "left trim" 69 | */ 70 | static string TrimLeft(string &str); 71 | 72 | /** 73 | * Return a new string with whitespace trimmed from the end of the 74 | * given string, str. 75 | * 76 | * @param str String to "right trim" 77 | */ 78 | static string TrimRight(string &str); 79 | }; 80 | -------------------------------------------------------------------------------- /src/string-util.cc: -------------------------------------------------------------------------------- 1 | #include "string-util.h" 2 | 3 | vector StringUtil::Split(const string &str, const string &delim) { 4 | vector result; 5 | 6 | size_t pos = 0; 7 | size_t last_seen = 0; 8 | while ((pos = str.find(delim, last_seen)) != string::npos) { 9 | string token = str.substr(last_seen, pos - last_seen); 10 | result.push_back(token); 11 | last_seen = pos + delim.length(); 12 | } 13 | result.push_back(str.substr(last_seen)); 14 | 15 | return result; 16 | } 17 | 18 | 19 | // const vector StringUtil::MatchNextInSet(const string &str, const string &delims) { 20 | // vector result; 21 | 22 | // size_t pos = 0; 23 | // size_t last_seen = 0; 24 | // while ((pos = str.find(delim, last_seen)) != string::npos) { 25 | // string token = str.substr(last_seen, pos - last_seen); 26 | // result.push_back(token); 27 | // last_seen = pos + delim.length(); 28 | // } 29 | // result.push_back(str.substr(last_seen)); 30 | 31 | // for 32 | 33 | // return result; 34 | 35 | // for (int i = 1; i < split_command.size(); i++) { 36 | // int start_pos = split_command[i].find_first_not_of(' '); 37 | // int end_pos = split_command[i].find_first_of(' ', start_pos); 38 | // command.outputFile = split_command[i].substr(start_pos, end_pos); 39 | // if (end_pos < split_command[i].size()) { 40 | // command_str.push_back(' '); 41 | // command_str.append(split_command[i].substr(end_pos)); 42 | // } 43 | // } 44 | // } 45 | 46 | 47 | vector StringUtil::Split(const char * str, const string &delim) { 48 | string s(str); 49 | return Split(s, delim); 50 | } 51 | 52 | 53 | string StringUtil::PadRight(string const& str, size_t size) { 54 | if (str.size() < size) { 55 | return str + string(size - str.size(), ' '); 56 | } else { 57 | return str; 58 | } 59 | } 60 | 61 | string StringUtil::PadLeft(string const& str, size_t size) { 62 | if (str.size() < size) { 63 | return string(size - str.size(), ' ') + str; 64 | } else { 65 | return str; 66 | } 67 | } 68 | 69 | string StringUtil::Trim(std::string &str) { 70 | string s = TrimRight(str); 71 | s = TrimLeft(s); 72 | return s; 73 | } 74 | 75 | string StringUtil::TrimLeft(string &str) { 76 | string s = str; 77 | s.erase(s.begin(), find_if(s.begin(), s.end(), [](int ch) { 78 | return !isspace(ch); 79 | })); 80 | return s; 81 | } 82 | 83 | string StringUtil::TrimRight(string &str) { 84 | string s = str; 85 | s.erase(find_if(s.rbegin(), s.rend(), [](int ch) { 86 | return !isspace(ch); 87 | }).base(), s.end()); 88 | return s; 89 | } 90 | -------------------------------------------------------------------------------- /src/proc-util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Process utility functions. 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std; 14 | 15 | class ProcUtil { 16 | public: 17 | /** 18 | * Creates a new process by duplicating the calling process. The new 19 | * process is referred to as the child process. The calling process is 20 | * referred to as the parent process. The child process and the parent 21 | * process run in separate memory spaces. 22 | * 23 | * This function effective "returns" twice, once in the parent process 24 | * and once again in the child process. In the parent process, the 25 | * return value is the pid of the child process. In the child process, 26 | * the return value is the sentinel value 0 which indicates that 27 | * execution is in the child process. 28 | * 29 | * Throws a ProcException if the new process cannot be created. 30 | * 31 | * @return pid of the child process, or zero if in the child process 32 | */ 33 | static pid_t CreateProcess(); 34 | 35 | /** 36 | * Changes the current working directory of this process to the 37 | * directory specified in new_cwd. 38 | * 39 | * Throws a ProcException if the current working directory cannot be 40 | * changed. 41 | * 42 | * @param new_cwd The directory to change the current working directory 43 | * to. 44 | */ 45 | static void SetCurrentWorkingDirectory(const string& new_cwd); 46 | 47 | /** 48 | * Get the current working directory of this process as a string. 49 | * Throws a ProcException if the current working directory cannot be 50 | * retrieved. 51 | * 52 | * @return String representing the current working directory of the 53 | * current running process. 54 | */ 55 | static string GetCurrentWorkingDirectory(); 56 | 57 | /** 58 | * Get the home direcotry of the given user. If the given user does not 59 | * exist on the system, then an empty string is returned instead of 60 | * a home directory path.. 61 | * 62 | * @param user The login username of the user whose home directory will 63 | * be looked up. 64 | * @return A string representing a home directory path, or an empty 65 | * string if the user does not exist on the system. 66 | */ 67 | static string GetUserHomeDirectory(const string& user); 68 | }; 69 | 70 | class ProcException : public exception { 71 | public: 72 | ProcException(const string& message): message(message) {} 73 | ProcException(const char* message): message(message) {} 74 | const char* what() const noexcept { return message.c_str(); } 75 | private: 76 | string message; 77 | }; 78 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | /** 2 | * A logger designed to standardize logging across an entire project, ensures 3 | * that logging is thread-safe, and offers support to different "log levels". 4 | * 5 | * Users of this logger should set the desired logging level using the extern 6 | * LOG_LEVEL variable. All log messages are categorized into one of four levels, 7 | * so only the desired logs can be included when running a program. 8 | * 9 | * Log levels, from most verbose to least verbose: 10 | * - DEBUG (verbose, protocol information, variable values, etc.) 11 | * - INFO (important user-facing information) 12 | * - WARN (non-fatal warnings) 13 | * - ERROR (fatal errors) 14 | * 15 | * Example: 16 | * LogType LOG_LEVEL = INFO; // Exclude logs at DEBUG level 17 | * 18 | * int main(int argc, char* argv[]) { 19 | * debug("%s", "This is a debug message"); // Not printed! 20 | * info("%s", "This is some useful information"); 21 | * warn("%s", "This is a warning!"); 22 | * error("Unexpected error: %s", error_message); 23 | * } 24 | */ 25 | 26 | #pragma once 27 | 28 | #include 29 | #include 30 | 31 | enum LogType { DEBUG, INFO, WARN, ERROR }; 32 | static const std::string LogTypeStrings[] = { "DEBUG", "INFO", "WARN", "ERROR" }; 33 | static std::mutex log_mutex; 34 | 35 | extern LogType LOG_LEVEL; 36 | 37 | /** 38 | * Print a debug message, accepting printf-style arguments. 39 | * 40 | * @param format C string that contains the text to be written. 41 | * @param ... (additional arguments) 42 | */ 43 | template 44 | void debug(const char* format, Args... args) { 45 | log(DEBUG, format, args...); 46 | } 47 | 48 | /** 49 | * Print an informational message, accepting printf-style arguments. 50 | * 51 | * @param format C string that contains the text to be written. 52 | * @param ... (additional arguments) 53 | */ 54 | template 55 | void info(const char* format, Args... args) { 56 | log(INFO, format, args...); 57 | } 58 | 59 | /** 60 | * Print an informational message, accepting printf-style arguments. 61 | * 62 | * @param format C string that contains the text to be written. 63 | * @param ... (additional arguments) 64 | */ 65 | template 66 | void warn(const char* format, Args... args) { 67 | log(WARN, format, args...); 68 | } 69 | 70 | /** 71 | * Print an informational message, accepting printf-style arguments. 72 | * 73 | * @param format C string that contains the text to be written. 74 | * @param ... (additional arguments) 75 | */ 76 | template 77 | void error(const char* format, Args... args) { 78 | log(ERROR, format, args...); 79 | } 80 | 81 | /** 82 | * Internal function. Print to stdout using the given log level, in a 83 | * thread-safe manner. 84 | */ 85 | template 86 | static void log(LogType level, const char* format, Args... args) { 87 | std::lock_guard lock(log_mutex); 88 | if (level < LOG_LEVEL) return; 89 | printf("[%s] ", LogTypeStrings[level].c_str()); 90 | printf(format, args...); 91 | printf("\n"); 92 | } -------------------------------------------------------------------------------- /failures_list_reasons: -------------------------------------------------------------------------------- 1 | Failures list (reasons, to fix): 2 | 3 | - glob expansion 4 | 5 | 6 | rebuildPathMap 7 | 8 | parse_commands_no_word 9 | 10 | 11 | 12 | First 3: 13 | Glob doesn't handle escaped, and we don't track escaped 14 | 15 | ---- 16 | 17 | '__test/script a b | 3 | a b c\n' != '__test/script a b | 3 | a b c\n' 18 | -> $* 19 | -> easy fix when building %*, probably 20 | 21 | /bin/echo $# '|' $0 $1 '|' $*\n 22 | -> same as above 23 | 24 | ---- 25 | 26 | 27 | "/bin/echo 'foo\nbar'\n/bin/echo second command" 28 | we don't run second command b/c command separator \n that we ignore 29 | 30 | -> maybe fix by treating the file s.t. instead of being one job, 31 | we split on newlines & treat like repl commands 32 | 33 | 34 | 35 | "/bin/echo command output\nexit 32\n \n" 36 | 37 | same as above, doesn't run & should b/c \n is command separator 38 | 39 | --- 40 | 41 | 42 | 'Word 1: .abc\nWord 2: def\nWord 3: x\nWord 4: y.\n' != 'Word 1: .abc def\tx\ny.\n 43 | 44 | not splitting on whitespace inside 45 | 46 | 47 | similarly: 48 | run("x='*.c'; /bin/echo \"__test/*.c\"")) 49 | 50 | I return l 51 | 52 | It's because I return a string, rather than rewinding the job_copy_str 53 | 54 | 55 | --------- 56 | Traceback (most recent call last): 57 | File "test-fixed.py", line 355, in test_doSubs_commands 58 | self.assertEqual("abc\n", run("x=abc; /bin/echo `/bin/echo $x`")) 59 | AssertionError: 'abc\n' != '-clash: basic_string\n' 60 | -------- 61 | 62 | 63 | 64 | 65 | 66 | # Feross passes this, I don't for some reason 67 | test_rebuildPathMap_default 68 | 69 | 70 | 71 | -------- 72 | 73 | run("x='~ ~'; /bin/echo ~ $x \"~\" '~'")) 74 | AssertionError: '/Users/jakemck ~ ~ ~ ~\n' != '/Users/jakemck /Users/jakemck /Users/jakemck ~ ~\n' 75 | 76 | 77 | parsing variables as tildes 78 | 79 | 80 | ----------- 81 | 82 | Say what we're looking for when we're incomplete 83 | 84 | FAIL: test_eval_errors (__main__.TestParser) 85 | ---------------------------------------------------------------------- 86 | Traceback (most recent call last): 87 | File "test-fixed.py", line 331, in test_eval_errors 88 | run("/bin/echo 'a b c")) 89 | AssertionError: "unexpected EOF while looking for matching `''" not found in '-clash: syntax error: unexpected end of file\n' 90 | 91 | ====================================================================== 92 | FAIL: test_parse_backslashes (__main__.TestParser) 93 | ---------------------------------------------------------------------- 94 | Traceback (most recent call last): 95 | File "test-fixed.py", line 436, in test_parse_backslashes 96 | run("/bin/echo \\")) 97 | AssertionError: 'Unexpected EOF while parsing backslash' not found in '-clash: syntax error: unexpected end of file\n' 98 | 99 | 100 | 101 | ------- 102 | 103 | 104 | test_rebuildPathMap_basics 105 | 106 | '__test/child/a\n__test/b\n' != '__test/child/a\n-clash: /Users/jakemck/Desktop/proj3-5/__test/child/b: permission denied\n 107 | 108 | -> spaces from command substituions, should be no spaces 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/shell.cc: -------------------------------------------------------------------------------- 1 | #include "shell.h" 2 | 3 | Shell::Shell(int argc, char* argv[]) { 4 | env.SetVariable("0", argv[0]); 5 | env.SetVariable("?", "0"); 6 | 7 | int command_flag_index = -1; 8 | for (int i = 0; i < argc; i++) { 9 | if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--command") == 0) { 10 | command_flag_index = i; 11 | break; 12 | } 13 | } 14 | 15 | int vars_start = command_flag_index + 2; 16 | int total_vars = argc - vars_start - 1; 17 | if (total_vars < 0) { 18 | total_vars = 0; 19 | } 20 | env.SetVariable("#", to_string(total_vars)); 21 | 22 | string all_args; 23 | for (int i = 0; i < argc - vars_start; i++) { 24 | string argument = string(argv[vars_start + i]); 25 | env.SetVariable(to_string(i), argument); 26 | if (i != 1) all_args.append(" "); 27 | if (i != 0) all_args.append(argument); 28 | } 29 | env.SetVariable("*", all_args); 30 | } 31 | 32 | bool Shell::ParseString(string& job_str) { 33 | try { 34 | if (job_parser.IsPartialJob(job_str, env)) { 35 | // Return failure if this is an incomplete job 36 | return false; 37 | } else { 38 | ParsedJob parsed_job = job_parser.Parse(job_str, env); 39 | jobs.push_back(Job(parsed_job, env)); 40 | } 41 | } catch (exception& e) { 42 | // Parsed the complete job, but it was invalid 43 | printf("-clash: %s\n", e.what()); 44 | return false; 45 | } 46 | 47 | // Parsed the complete job. It was valid and successfully added to the list 48 | // of pending jobs to run. 49 | return true; 50 | } 51 | 52 | bool Shell::ParseFile(const string& file_path) { 53 | string job_str; 54 | ifstream file(file_path); 55 | if (!file.is_open()) { 56 | printf("-clash: %s: No such file or directory\n", file_path.c_str()); 57 | return false; 58 | } 59 | 60 | string line; 61 | while (getline(file, line)) { 62 | job_str += line + "\n"; 63 | } 64 | 65 | if (file.bad()) { 66 | printf("-clash: %s: Error reading file\n", file_path.c_str()); 67 | return false; 68 | } 69 | 70 | return ParseString(job_str); 71 | } 72 | 73 | bool Shell::RunJobsAndWait() { 74 | for (Job& job : jobs) { 75 | try { 76 | debug("%s", job.ToString().c_str()); 77 | job.RunAndWait(); 78 | } catch (exception& err) { 79 | printf("-clash: %s\n", err.what()); 80 | jobs.clear(); 81 | return false; 82 | } 83 | } 84 | jobs.clear(); 85 | return true; 86 | } 87 | 88 | int Shell::StartRepl() { 89 | bool isTTY = isatty(STDIN_FILENO); 90 | debug("isTTY: %d", isTTY); 91 | string remaining_job_str; 92 | while (true) { 93 | char * prompt = NULL; 94 | if (isTTY) { 95 | prompt = (char *) (remaining_job_str.length() == 0 ? "% " : "> "); 96 | } 97 | 98 | char* line = readline(prompt); 99 | if (line == NULL) { 100 | break; 101 | } 102 | 103 | remaining_job_str.append(line); 104 | remaining_job_str.append("\n"); 105 | free(line); 106 | 107 | try { 108 | if (job_parser.IsPartialJob(remaining_job_str, env)) { 109 | // Incomplete job, so get more input from user 110 | continue; 111 | } else { 112 | ParsedJob parsed_job = job_parser.Parse(remaining_job_str, env); 113 | jobs.push_back(Job(parsed_job, env)); 114 | } 115 | } catch (exception& e) { 116 | // Parsed the complete job, but it was invalid 117 | printf("-clash: %s\n", e.what()); 118 | } 119 | 120 | RunJobsAndWait(); 121 | remaining_job_str = string(); 122 | } 123 | 124 | if (!remaining_job_str.empty()) { 125 | printf("-clash: syntax error: unexpected end of file\n"); 126 | return 2; 127 | } 128 | 129 | return stoi(env.GetVariable("?")); 130 | } 131 | -------------------------------------------------------------------------------- /src/environment.cc: -------------------------------------------------------------------------------- 1 | #include "environment.h" 2 | 3 | extern char **environ; 4 | 5 | static const string DEFAULT_ENV_VARIABLE_VALUE = ""; 6 | 7 | Environment::Environment() { 8 | // Inherit environment variables from parent process 9 | int i = 0; 10 | for (char * var = environ[i]; var != NULL; i += 1, var = environ[i]) { 11 | vector split = StringUtil::Split(var, "="); 12 | string name = split[0]; 13 | string value = split[1]; 14 | SetVariable(name, value); 15 | ExportVariable(name); 16 | } 17 | 18 | PopulatePathCache(); 19 | } 20 | 21 | const string& Environment::GetVariable(const string& name) { 22 | if (variables.count(name)) { 23 | return variables[name]; 24 | } else { 25 | return DEFAULT_ENV_VARIABLE_VALUE; 26 | } 27 | } 28 | 29 | void Environment::SetVariable(const string& name, const string& value) { 30 | variables[name] = value; 31 | if (name == "PATH") { 32 | PopulatePathCache(); 33 | } 34 | } 35 | 36 | void Environment::UnsetVariable(const string& name) { 37 | variables.erase(name); 38 | export_variables.erase(name); 39 | if (name == "PATH") { 40 | PopulatePathCache(); 41 | } 42 | } 43 | 44 | void Environment::ExportVariable(const string& name) { 45 | if (variables.count(name)) { 46 | export_variables.insert(name); 47 | } 48 | } 49 | 50 | vector Environment::GetExportVariableStrings() { 51 | vector export_variable_strings; 52 | for (const string& name : export_variables) { 53 | export_variable_strings.push_back(name + "=" + GetVariable(name)); 54 | } 55 | return export_variable_strings; 56 | } 57 | 58 | // If no matching executable file can be found, return the first 59 | // matching non-executable file. If nothing matched, empty string is 60 | // returned. 61 | string Environment::FindProgramPath(string& program_name) { 62 | // If the program name contains a "/" character, then it is already a 63 | // path to an exutable so use it as-is. 64 | if (program_name.find("/") != string::npos) { 65 | return program_name; 66 | } 67 | 68 | // Fast path. If the program name is in the cache, return it. 69 | if (path_cache.count(program_name)) { 70 | return path_cache[program_name]; 71 | } 72 | 73 | string first_program_path; 74 | 75 | // If PATH is missing, use a reasonable default 76 | string path = variables.count("PATH") 77 | ? variables["PATH"] 78 | : DEFAULT_PATH_VAR; 79 | 80 | vector search_paths = StringUtil::Split(path, ":"); 81 | 82 | for (string search_path : search_paths) { 83 | vector entries = FileUtil::GetDirectoryEntries(search_path); 84 | for (string& entry : entries) { 85 | if (entry != program_name) { 86 | continue; 87 | } 88 | 89 | string program_path = search_path + "/" + program_name; 90 | if (first_program_path.empty()) { 91 | first_program_path = program_path; 92 | } 93 | 94 | if (FileUtil::IsExecutableFile(program_path)) { 95 | return program_path; 96 | } 97 | } 98 | } 99 | 100 | // May return empty string, indicating no file was matched 101 | return first_program_path; 102 | } 103 | 104 | void Environment::PopulatePathCache() { 105 | path_cache.clear(); 106 | 107 | string path = variables.count("PATH") 108 | ? variables["PATH"] 109 | : DEFAULT_PATH_VAR; 110 | 111 | vector search_paths = StringUtil::Split(path, ":"); 112 | 113 | for (string search_path : search_paths) { 114 | vector entries = FileUtil::GetDirectoryEntries(search_path); 115 | for (string& entry : entries) { 116 | if (!path_cache.count(entry)) { 117 | // Earlier path entries take priority over later ones, so 118 | // never overwrite a cache entry. 119 | string program_path = search_path + "/" + entry; 120 | path_cache[entry] = program_path; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/command.h: -------------------------------------------------------------------------------- 1 | /** 2 | * An individual progam to be run, along with its arguments and optional input 3 | * and output redirection files. Commands can also be shell built-ins like "cd", 4 | * "set", "unset", "printenv", "export", "exit", "pwd", etc. 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "environment.h" 16 | #include "job-parser.h" 17 | #include "file-util.h" 18 | #include "log.h" 19 | #include "proc-util.h" 20 | 21 | using namespace std; 22 | 23 | class Command { 24 | public: 25 | /** 26 | * Create a new command, which may be a prgoram 27 | * 28 | * @param parsed_command Struct which describes all the relevant 29 | * information about the command 30 | * @param env The shell environment 31 | */ 32 | Command(ParsedCommand parsed_command, Environment& env); 33 | 34 | /** 35 | * Run the given command, which may be a program to run or a shell 36 | * builtin, using the given file descriptors as a source and sink from 37 | * which to read and write stdin and stdout/stderr to. 38 | * 39 | * This function never blocks or waits for the process to finish running. 40 | * 41 | * @param source The file descriptor from which to read stdin 42 | * @param sink The file descriptor to write stdout/stderr to 43 | */ 44 | void Run(int source, int sink); 45 | 46 | /** 47 | * Block until the command has finished running. 48 | */ 49 | void Wait(); 50 | 51 | /** 52 | * Returns a readable string representation of the command, including 53 | * the contained arguments and input and output redirection files 54 | * (if present). 55 | */ 56 | string ToString(); 57 | 58 | private: 59 | /** 60 | * Attempts to run the command as a shell builtin. If the command is 61 | * recognized as a shell builtin, then the command is run as such. Many 62 | * shell builtins are supported including "cd", "set", "unset", 63 | * "printenv", "export", "exit", "pwd", etc. 64 | * 65 | * Throws any number of various exception types if the builtin cannot 66 | * be run because of invalid arguments or system call failure. 67 | * 68 | * @return True if the command was recognized and executed as a shell 69 | * builtin. 70 | */ 71 | bool RunBuiltin(); 72 | 73 | /** 74 | * Attempts to run the command as a program, setting up the programs 75 | * stdin and stdout/stderr to redirect to the given source and sink file 76 | * descriptors. The program's environment is determined by the state 77 | * of the passed in Environment instance. 78 | * 79 | * Throws any number of various exception types if the program cannot 80 | * be run because there is no such file, the program is not executable, 81 | * or system call failure. 82 | * 83 | * @param source The source file descriptor, where stdin comes from 84 | * @param sink The sink file descrptor, where stdout/stderr goes 85 | */ 86 | void RunProgram(int source, int sink); 87 | 88 | /** 89 | * List of all the command arguments. 90 | */ 91 | vector words; 92 | 93 | /** 94 | * An optional input file to redirect from. 95 | */ 96 | string input_file; 97 | 98 | /** 99 | * An optional output file to redirect from. 100 | */ 101 | string output_file; 102 | 103 | /** 104 | * Boolean indicating whether to also redirect stderr to the sink 105 | * file descriptor. 106 | */ 107 | bool redirect_stderr; 108 | 109 | /** 110 | * The process id of the started process. 111 | */ 112 | pid_t pid; 113 | 114 | /** 115 | * The shell environment. 116 | */ 117 | Environment& env; 118 | }; 119 | -------------------------------------------------------------------------------- /src/arguments.cc: -------------------------------------------------------------------------------- 1 | #include "arguments.h" 2 | 3 | void Arguments::RegisterBool(const string name, const string description) { 4 | bool_args[name] = false; 5 | descriptions[name] = description; 6 | } 7 | 8 | void Arguments::RegisterInt(const string name, const string description) { 9 | int_args[name] = -1; 10 | descriptions[name] = description; 11 | } 12 | 13 | void Arguments::RegisterString(const string name, const string description) { 14 | string_args[name] = ""; 15 | descriptions[name] = description; 16 | } 17 | 18 | void Arguments::RegisterAlias(const char alias, const string name) { 19 | alias_to_name[string(1, alias)] = name; 20 | name_to_alias[name] = alias; 21 | } 22 | 23 | void Arguments::Parse(int argc, char* argv[]) { 24 | vector arguments(argv + 1, argv + argc); 25 | for (int i = 0; i < arguments.size(); i++) { 26 | string arg = arguments[i]; 27 | string name; 28 | 29 | // Named arguments are prefixed with two dashes (e.g. --help) 30 | string named_prefix("--"); 31 | // Aliased arguments are prefixed with one dash (e.g. -h) 32 | string alias_prefix("-"); 33 | 34 | if (arg.substr(0, named_prefix.size()) == named_prefix) { 35 | name = arg.substr(named_prefix.size()); 36 | } else if (arg.substr(0, alias_prefix.size()) == alias_prefix) { 37 | string alias = arg.substr(alias_prefix.size()); 38 | if (alias_to_name.count(alias)) { 39 | name = alias_to_name[alias]; 40 | } else { 41 | throw ArgumentsException(arg + " option was unexpected"); 42 | } 43 | } 44 | 45 | if (!name.empty()) { 46 | if (bool_args.count(name)) { 47 | bool_args[name] = true; 48 | } else if (int_args.count(name) || string_args.count(name)) { 49 | // Integer and string arguments consume the next token to 50 | // determine the argument value 51 | i += 1; 52 | if (i >= arguments.size()) { 53 | throw ArgumentsException(arg + " option requires an argument"); 54 | } 55 | string value = arguments[i]; 56 | if (int_args.count(name)) { 57 | int_args[name] = stoi(value); 58 | } else { 59 | string_args[name] = value; 60 | } 61 | } else { 62 | throw ArgumentsException(arg + " option was unexpected"); 63 | } 64 | } else { 65 | // Unnamed arguments have no dash prefix 66 | unnamed_args.push_back(arg); 67 | } 68 | } 69 | } 70 | 71 | bool Arguments::GetBool(const string name) { 72 | if (!bool_args.count(name)) { 73 | throw ArgumentsException("Missing argument " + name); 74 | } 75 | return bool_args[name]; 76 | } 77 | 78 | int Arguments::GetInt(const string name) { 79 | if (!int_args.count(name)) { 80 | throw ArgumentsException("Missing argument " + name); 81 | } 82 | return int_args[name]; 83 | } 84 | 85 | const string& Arguments::GetString(const string name) { 86 | if (!string_args.count(name)) { 87 | throw ArgumentsException("Missing argument " + name); 88 | } 89 | return string_args[name]; 90 | } 91 | 92 | const vector& Arguments::GetUnnamed() { 93 | return unnamed_args; 94 | } 95 | 96 | string Arguments::GetHelpText() { 97 | string result; 98 | 99 | if (intro.size()) result += intro + "\n"; 100 | result += "Usage:"; 101 | 102 | unsigned long max_name_len = 0; 103 | unsigned long max_description_len = 0; 104 | for (auto const& [name, description] : descriptions) { 105 | max_name_len = max(max_name_len, name.size()); 106 | max_description_len = max(max_description_len, description.size()); 107 | } 108 | 109 | for (auto const& [name, description] : descriptions) { 110 | string type; 111 | if (bool_args.count(name)) { 112 | type = "bool"; 113 | } else if (int_args.count(name)) { 114 | type = "int"; 115 | } else { 116 | type = "string"; 117 | } 118 | 119 | string alias; 120 | if (name_to_alias.count(name)) { 121 | alias = "-" + name_to_alias[name] + ", "; 122 | } 123 | 124 | result += "\n "; 125 | result += StringUtil::PadRight(alias, 1); 126 | result += "--" + StringUtil::PadRight(name, max_name_len) + " "; 127 | result += StringUtil::PadRight(description, max_description_len); 128 | result += " [" + type + "]"; 129 | } 130 | return result; 131 | } 132 | -------------------------------------------------------------------------------- /src/shell.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Shell is the top-level class that represents an instance of the Clash 3 | * command line program. It's primary purpose is to process user input and run 4 | * it as a series of jobs. 5 | 6 | * In our system, we use the following terminology: 7 | * 8 | * "Command" - An individual progam to be run, along with its arguments and 9 | * optional input and output redirection files. 10 | * 11 | * "Pipeline" - A set of one or more "commands", where the input from one 12 | * command flows into the next command. 13 | * 14 | * "Job" - A set of one or more "pipelines" to be run sequentially, one 15 | * after the other. 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "environment.h" 28 | #include "job.h" 29 | #include "job-parser.h" 30 | 31 | using namespace std; 32 | 33 | class Shell { 34 | public: 35 | /** 36 | * Create a new shell instance. 37 | * 38 | * @param argc The number of arguments that the shell was started with 39 | * @param argv Array of strings representing arguments the shell was started with 40 | */ 41 | Shell(int argc, char* argv[]); 42 | 43 | /** 44 | * Parse a job string, which is a set of one or more "pipelines" to be 45 | * run sequentially, one after the other, separated by semicolons or 46 | * newlines. The parsed job is added to the list of jobs to be executed 47 | * when RunJobsAndWait() is called. 48 | * 49 | * Returns a boolean indicating whether the given job string 50 | * constitutes a valid and complete job. Jobs which end without closting 51 | * all quotes and properly finishing all sequences of special commands 52 | * like output file redirections are considered invalid. 53 | * 54 | * @param job_str The string that defines the sequence of commands to run 55 | * @return True if the job was valid and parsed completely 56 | * False otherwise. 57 | */ 58 | bool ParseString(string& job_str); 59 | 60 | /** 61 | * Read and parse a shell script file, which is a file with a newline- 62 | * separated list of jobs to execute. The parsed job is added to the 63 | * list of jobs to be executed when RunJobsAndWait() is called. 64 | * 65 | * @param file_path Filesystem path to the shell script to run 66 | * @return True if the job was valid and parsed completely 67 | * False otherwise. 68 | */ 69 | bool ParseFile(const string& file_path); 70 | 71 | /** 72 | * Run all the jobs which have been previously parsed, sequentially, one 73 | * after the other. Blocks until all the jobs are finished running. 74 | * After the jobs are finished running, the list of pending jobs is 75 | * cleared. 76 | * 77 | * @return True if the jobs in the pending job queue were all executed 78 | * without errors. 79 | */ 80 | bool RunJobsAndWait(); 81 | 82 | /** 83 | * Start a Read-Eval-Print loop which continually prompts the user for 84 | * jobs from stdin. If stdin is a TTY, then a helpful prompt is printed 85 | * a incomplete job strings are detected and the user is helpfully 86 | * prompted to complete their original command on a new line. 87 | * 88 | * If stdin is not a TTY, then no prompt is printed and lines are 89 | * continuously read from stdin until EOF is reached. If there is in 90 | * an incomplete job when EOF is reached, then an error is printed. 91 | * 92 | * @return the exit code of last command int he 93 | */ 94 | int StartRepl(); 95 | private: 96 | /** 97 | * The local shell environment. Stores environment variables and the 98 | * PATH cache. 99 | */ 100 | Environment env; 101 | 102 | /** 103 | * Job string parser. Aids us in the complex task of parsing the user's 104 | * job strings into a useful structured form. 105 | */ 106 | JobParser job_parser; 107 | 108 | /** 109 | * List of pending jobs to run when RunJobsAndWait() is called. 110 | */ 111 | vector jobs; 112 | }; 113 | 114 | class ShellException : public exception { 115 | public: 116 | ShellException(const string& message): message(message) {} 117 | ShellException(const char* message): message(message) {} 118 | const char* what() const noexcept { return message.c_str(); } 119 | private: 120 | string message; 121 | }; 122 | -------------------------------------------------------------------------------- /src/environment.h: -------------------------------------------------------------------------------- 1 | /** 2 | * The local shell environment. Provides an abstraction over the concept of 3 | * environment variables and looks up program names efficiently using the 4 | * current PATH environment variable, using an internal cache whenever possible. 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "file-util.h" 14 | #include "string-util.h" 15 | 16 | using namespace std; 17 | 18 | const string DEFAULT_PATH_VAR = 19 | "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"; 20 | 21 | class Environment { 22 | public: 23 | /** 24 | * Create a new shell environment. By default, this environment inherits 25 | * environment variables from the parent process, exports them all, and 26 | * warms the PATH cache using the default PATH inherited from the parent 27 | * process, or if there was no PATH variable set in parent process then 28 | * a reasonable default is used. 29 | */ 30 | Environment(); 31 | 32 | /** 33 | * Get the environment variable with the given name. If no variable with 34 | * the given name exists, then an empty string is returned. 35 | * 36 | * @param name The environment variable name 37 | * @return The current value of the environment variable 38 | */ 39 | const string& GetVariable(const string& name); 40 | 41 | /** 42 | * Set the environment variable with the given name to the given value. 43 | * If the variable name is "PATH", then the internal PATH cache is 44 | * re-populated. 45 | * 46 | * @param name The environment variable name 47 | * @param value The new value for the environment variable 48 | */ 49 | void SetVariable(const string& name, const string& value); 50 | 51 | /** 52 | * Delete the environment variable with the given name. If the variable 53 | * name is "PATH", then the internal PATH cache is re-populated with 54 | * a reasonable default value. 55 | * 56 | * @param name The environment variable name 57 | */ 58 | void UnsetVariable(const string& name); 59 | 60 | /** 61 | * Mark the variable with the given name to be exported. This means that 62 | * subprocesses started from this environment should contain this 63 | * variable. 64 | * 65 | * @param name The environment variable name 66 | */ 67 | void ExportVariable(const string& name); 68 | 69 | /** 70 | * Return a vector of environment variable names and values, where each 71 | * entry in the vector represents a single name and value pair, 72 | * separated by an equals sign. 73 | * 74 | * Example: ["PATH=/bin:/usr/bin:/usr/local/bin","HOME=/home/user"] 75 | * 76 | * @return Vector of environment variable strings 77 | */ 78 | vector GetExportVariableStrings(); 79 | 80 | /** 81 | * Looks for a program with the given program_name in the locations 82 | * listed in the PATH environment variable. Returns an absolute path to 83 | * the given program. If no suitable executable file could be found, 84 | * then the first matching non-executable file is returned, or an empty 85 | * string if no matching programs could be found. 86 | * 87 | * The search process is very fast in most cases since an internal cache 88 | * of all the binaries included in all the directories in the PATH is 89 | * created at initialization time and anytime the PATH variable changes. 90 | * However, if the contents of any of the directories in the PATH 91 | * changes without the PATH variable value changing, then the new 92 | * binaries will not be in the cache. 93 | * 94 | * If the given program_name is not in the cache, it will still be found 95 | * because the PATH is always searched directly when there is a cache 96 | * miss. 97 | * 98 | * @param program_name The program name to search for. 99 | * @return An absolute path to the given program, or an 100 | * empty string if no matching program could be 101 | * found. 102 | */ 103 | string FindProgramPath(string& program_name); 104 | private: 105 | /** 106 | * Clear the contents of the PATH cache and rebuild it from scratch 107 | * using the latest contents of the directories included in the PATH 108 | * environment variable. 109 | */ 110 | void PopulatePathCache(); 111 | 112 | /** 113 | * Map of environment variable name to value. 114 | */ 115 | map variables; 116 | 117 | /** 118 | * Set of variable names which are marked as exported. 119 | */ 120 | set export_variables; 121 | 122 | /** 123 | * The PATH cache. Map of program names to absolute paths where the 124 | * given programs are located. 125 | */ 126 | map path_cache; 127 | }; 128 | -------------------------------------------------------------------------------- /src/command.cc: -------------------------------------------------------------------------------- 1 | #include "command.h" 2 | 3 | Command::Command(ParsedCommand parsed_command, Environment& env) : 4 | pid(0), env(env) { 5 | 6 | words = parsed_command.words; 7 | input_file = parsed_command.input_file; 8 | output_file = parsed_command.output_file; 9 | redirect_stderr = parsed_command.redirect_stderr; 10 | debug("command words: %d", words.size()); 11 | } 12 | 13 | void Command::Run(int source, int sink) { 14 | bool isBuiltin = RunBuiltin(); 15 | if (!isBuiltin) { 16 | RunProgram(source, sink); 17 | } 18 | } 19 | 20 | void Command::Wait() { 21 | int status; 22 | waitpid(pid, &status, 0); 23 | env.SetVariable("?", to_string(WEXITSTATUS(status))); 24 | } 25 | 26 | string Command::ToString() { 27 | string result = "Command:"; 28 | for (string word : words) { 29 | result += " " + word; 30 | } 31 | if (!input_file.empty()) { 32 | result += " [input_file: " + input_file + "]"; 33 | } 34 | if (!output_file.empty()) { 35 | result += " [output_file: " + output_file + "]"; 36 | } 37 | return result; 38 | } 39 | 40 | bool Command::RunBuiltin() { 41 | string& program = words[0]; 42 | 43 | if (program == "cd") { 44 | if (words.size() == 1) { 45 | const string& home_directory = env.GetVariable("HOME"); 46 | if (!home_directory.empty()) { 47 | ProcUtil::SetCurrentWorkingDirectory(home_directory); 48 | } else { 49 | printf("cd: HOME not set"); 50 | } 51 | } else if (words.size() == 2) { 52 | ProcUtil::SetCurrentWorkingDirectory(words[1]); 53 | } else { 54 | printf("cd: Too many arguments\n"); 55 | } 56 | return true; 57 | } 58 | 59 | if (program == "pwd") { 60 | if (words.size() == 1) { 61 | printf("%s\n", ProcUtil::GetCurrentWorkingDirectory().c_str()); 62 | } else { 63 | printf("pwd: Too many arguments\n"); 64 | } 65 | return true; 66 | } 67 | 68 | if (program == "exit") { 69 | if (words.size() == 1) { 70 | exit(0); 71 | } else if (words.size() >= 2) { 72 | int status; 73 | try { 74 | status = stoi(words[1]); 75 | } catch (const invalid_argument& err) { 76 | printf("exit: %s: Numeric argument required\n", words[1].c_str()); 77 | status = 2; 78 | } 79 | if (words.size() != 2) { 80 | printf("exit: Too many arguments"); 81 | } 82 | exit(status); 83 | } else { 84 | } 85 | return true; 86 | } 87 | 88 | if (program == "printenv") { 89 | vector variable_strings = env.GetExportVariableStrings(); 90 | for (string& variable_string : variable_strings) { 91 | printf("%s\n", variable_string.c_str()); 92 | } 93 | return true; 94 | } 95 | 96 | if (program == "set") { 97 | if (words.size() == 3) { 98 | env.SetVariable(words[1], words[2]); 99 | } else if (words.size() < 3) { 100 | printf("set: Not enough arguments"); 101 | } else { 102 | printf("set: Too many arguments"); 103 | } 104 | return true; 105 | } 106 | 107 | if (program.find("=") != string::npos) { 108 | vector res = StringUtil::Split(program, "="); 109 | env.SetVariable(res[0], res[1]); 110 | return true; 111 | } 112 | 113 | if (program == "unset") { 114 | if (words.size() == 1) { 115 | printf("unset: Not enough arguments\n"); 116 | } else { 117 | vector names(words.begin() + 1, words.end()); 118 | for (string& name : names) { 119 | env.UnsetVariable(name); 120 | } 121 | } 122 | return true; 123 | } 124 | 125 | if (program == "export") { 126 | if (words.size() == 1) { 127 | printf("export: Not enough arguments\n"); 128 | } else { 129 | vector names(words.begin() + 1, words.end()); 130 | for (string& name : names) { 131 | env.ExportVariable(name); 132 | } 133 | } 134 | return true; 135 | } 136 | 137 | return false; 138 | } 139 | 140 | void Command::RunProgram(int source, int sink) { 141 | pid = ProcUtil::CreateProcess(); 142 | if (pid != 0) { 143 | return; 144 | } 145 | 146 | if (!input_file.empty()) { 147 | source = FileUtil::OpenFile(input_file); 148 | } 149 | 150 | if (!output_file.empty()) { 151 | sink = FileUtil::OpenFile(output_file, O_WRONLY | O_CREAT | O_TRUNC); 152 | } 153 | 154 | FileUtil::DuplicateDescriptor(source, STDIN_FILENO); 155 | FileUtil::DuplicateDescriptor(sink, STDOUT_FILENO); 156 | if (redirect_stderr) { 157 | FileUtil::DuplicateDescriptor(sink, STDERR_FILENO); 158 | } 159 | 160 | string program_path = env.FindProgramPath(words[0]); 161 | if (program_path.empty()) { 162 | fprintf(stderr, "-clash: %s: command not found\n", words[0].c_str()); 163 | exit(0); 164 | } 165 | words[0] = program_path; 166 | 167 | // Build argument array 168 | char * argv[words.size() + 1]; 169 | for (size_t i = 0; i < words.size(); i++) { 170 | argv[i] = const_cast(words[i].c_str()); 171 | } 172 | argv[words.size()] = NULL; 173 | 174 | // Build environment variable array 175 | vector variable_strings = env.GetExportVariableStrings(); 176 | char * envp[variable_strings.size() + 1]; 177 | for (size_t i = 0; i < variable_strings.size(); i++) { 178 | envp[i] = const_cast(variable_strings[i].c_str()); 179 | } 180 | envp[variable_strings.size()] = NULL; 181 | 182 | execve(argv[0], argv, envp); 183 | fprintf(stderr, "-clash: %s: No such file or directory\n", argv[0]); 184 | exit(0); 185 | } 186 | -------------------------------------------------------------------------------- /src/file-util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * File descriptor utility functions. 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "log.h" 20 | #include "string-util.h" 21 | 22 | using namespace std; 23 | 24 | class FileUtil { 25 | public: 26 | /** 27 | * Creates a pipe, a unidirectional data channel that can be used for 28 | * interprocess communication and returns the two newly-allocated file 29 | * descriptors in a vector. The pipe is configured with O_CLOEXEC so it 30 | * will be automatically closed when execve() is called. 31 | * 32 | * Throws a FileException if the pipe cannot be created or configured 33 | * correctly. 34 | * 35 | * @return vector that contains exactly two file descriptors 36 | */ 37 | static vector CreatePipe(); 38 | 39 | /** 40 | * Closes a file descriptor, so that it no longer refers to any file and 41 | * may be reused. 42 | * 43 | * Throws a FileException if the file cannnot be closed for any reason. 44 | * 45 | * @param fd Integer representing the file descriptor to close 46 | */ 47 | static void CloseDescriptor(int fd); 48 | 49 | /** 50 | * Creates a copy of the file descriptor old_fd, using the descriptor 51 | * number specified in new_fd. If the file descriptor new_fd was 52 | * previously open, it is silently closed before being reused. After a 53 | * successful return, the old and new file descriptors may be used 54 | * interchangeably. They literally refer to the same open file 55 | * description. 56 | * 57 | * Throws a FileException if the file descriptor cannot be duplicated. 58 | * 59 | * @param new_fd The file descriptor to be closed and overwritten 60 | * @param old_fd The file descriptor to be duplicated 61 | */ 62 | static void DuplicateDescriptor(int new_fd, int old_fd); 63 | 64 | /** 65 | * Reads the complete contents of the given file descriptor fd and 66 | * returns it as a string. 67 | * 68 | * @param fd The file descriptor to read from 69 | * @return File contents as a string 70 | */ 71 | static string ReadFileDescriptor(int fd); 72 | 73 | /** 74 | * Open the file specified by file_path. The return value is a file 75 | * descriptor, a small, nonnegative integer that is used in subsequent 76 | * system calls to refer to the open file. The file descriptor returned 77 | * by a successful call will be the lowest-numbered file descriptor not 78 | * currently open for the process. 79 | * 80 | * Optionally, flags and a mode can be specified which change the 81 | * behavior of the opened file, e.g. opening it for reading vs. writing 82 | * or setting the permissions on a newly-created file. See the standard 83 | * C documentation for open() to learn more about these options. 84 | * 85 | * Throws a FileException if the file cannot be opened. 86 | * 87 | * @param file_path Filesystem path to the file to open 88 | * @param flags Standard open() flags to configure the file descriptor 89 | * @param mode Standard open() flags to configure the file's mode 90 | * @return The newly-opened file descriptor 91 | */ 92 | static int OpenFile(string& file_path, int flags = O_RDONLY, 93 | mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 94 | 95 | /** 96 | * Return a vector of all directory entries in the given directory path. 97 | * Skips over trivial directory entries like "." and "..". 98 | * 99 | * Throws a FileException if the directory cannot be opened or the 100 | * stream of directory entries cannot be read. 101 | * 102 | * @param path Filesystem path to the directory to read entries from 103 | * @return Vector of strings representing directory entries 104 | */ 105 | static vector GetDirectoryEntries(const string& path); 106 | 107 | /** 108 | * Get all files or diretories which match the given "glob" pattern. 109 | * Glob patterns are quite similar to regular expressions but they are 110 | * not quite the same. The rules for glob matching are too complex to 111 | * list here, but complete information can be found online from various 112 | * resources such as http://tldp.org/LDP/abs/html/globbingref.html. 113 | * 114 | * The search starts in the current working directory. Throws a 115 | * FileException if the given pattern is invalid, e.g. it has 116 | * unclosed character class. 117 | * 118 | * Returns a vector of all the files or directories which match the 119 | * given glob pattern. If no files or directories are matched, then 120 | * vector containing a single string representing the original pattern 121 | * is returned. 122 | * 123 | * @param pattern The glob pattern to match against 124 | * @return The vector of strings that represent matched files and directories 125 | */ 126 | static vector GetGlobMatches(const string& pattern); 127 | 128 | /** 129 | * Returns true if the given file or directory has the executable 130 | * permission bit set. 131 | * 132 | * @param Filesystem path to the file to check 133 | * @return True if file is executable 134 | */ 135 | static bool IsExecutableFile(const string& path); 136 | 137 | /** 138 | * Returns true if the given path represents a file (as opposed to a 139 | * directory). 140 | 141 | * @param path Filesystem path to the entry to check 142 | * @return True if the given path represents a file 143 | */ 144 | static bool IsDirectory(const string& path); 145 | 146 | private: 147 | /** 148 | * Returns true if the given pattern matches the given candidate file 149 | * or directory name. 150 | * 151 | * @param pattern A glob pattern 152 | * @param name The candidate file or directory name 153 | * @return True if the pattern matches the name 154 | */ 155 | static bool GlobMatch(const string& pattern, const string& name); 156 | }; 157 | 158 | class FileException : public exception { 159 | public: 160 | FileException(const string& message): message(message) {} 161 | FileException(const char* message): message(message) {} 162 | const char* what() const noexcept { return message.c_str(); } 163 | private: 164 | string message; 165 | }; 166 | -------------------------------------------------------------------------------- /src/arguments.h: -------------------------------------------------------------------------------- 1 | /** 2 | * This class exposes a friendly interface for parsing a command line 3 | * argument string (i.e. char* argv[]) into a more useful structure. The 4 | * user must specify expected argument names, argument descriptions, and 5 | * expected argument types (bool, int, or string). 6 | * 7 | * Example: 8 | * 9 | * int main(int argc, char* argv[]) { 10 | * Arguments args("Hello World - A hello world CLI program"); 11 | * args.RegisterBool("help", "Print help message"); 12 | * try { 13 | * args.Parse(argc, argv); 14 | * } catch (exception& err) { 15 | * printf("Error: %s\n", err.what()); 16 | * return 1; 17 | * } 18 | * if (args.GetBool("help")) { 19 | * printf("%s\n", args.GetHelpText().c_str()); 20 | * return 0; 21 | * } 22 | * } 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "string-util.h" 34 | 35 | using namespace std; 36 | 37 | class Arguments { 38 | public: 39 | /** 40 | * Construct a command line argument parser. 41 | * 42 | * @param intro Help text to describe the purpose of the program 43 | */ 44 | Arguments(string intro) : intro(intro) {}; 45 | 46 | /** 47 | * Register a named boolean command line argument with the given name 48 | * and description. Defaults to false. 49 | * 50 | * Boolean argument flags appear alone with no following value 51 | * (e.g. "--mybool"). 52 | * 53 | * @param name The full name (e.g. --mybool) of the argument 54 | * @param description Friendly description of the argument's purpose 55 | */ 56 | void RegisterBool(const string name, const string description); 57 | 58 | /** 59 | * Register a named integer command line argument with the given name 60 | * and description. Dsefaults to -1. 61 | * 62 | * Integer argument flags must be followed immediately by a space and 63 | * number (e.g. "--myint 42"). 64 | * 65 | * @param name The full name (e.g. --myint) of the argument 66 | * @param description Friendly description of the argument's purpose 67 | */ 68 | void RegisterInt(const string name, const string description); 69 | 70 | /** 71 | * Register a named string command line argument with the given name 72 | * and description. Defaults to "". 73 | * 74 | * String arguments must be followed immediately by a space and a string 75 | * (e.g. "--mystring hello"). 76 | * 77 | * @param name The full name (e.g. --mystring) of the argument 78 | * @param description Friendly description of the argument's purpose 79 | */ 80 | void RegisterString(const string name, const string description); 81 | 82 | /** 83 | * Register a one character command alias with the given name. For 84 | * example, the command alias "-h" could be registered as a shortcut for 85 | * the full command name "--help". 86 | * 87 | * @param alias The one-character alias (e.g. 'h') 88 | * @param name The full command name that the alias maps to (e.g "help") 89 | */ 90 | void RegisterAlias(const char alias, const string name); 91 | 92 | /** 93 | * Parse the user-provided command line argument string, usually 94 | * obtained directly from the arguments to main(). 95 | * 96 | * May throw an exception if the command line argument string is 97 | * malformed, or missing a required value (e.g. for a named string or 98 | * integer argument which requires a value). 99 | * 100 | * @throw ArgumentsException 101 | * 102 | * @param argc Number of command line arguments 103 | * @param argv Array of command line argument strings 104 | */ 105 | void Parse(int argc, char* argv[]); 106 | 107 | /** 108 | * Return the value of the boolean argument with the given name. 109 | * 110 | * @throw ArgumentsException 111 | * 112 | * @param argument name 113 | * @return argument value 114 | */ 115 | bool GetBool(const string name); 116 | 117 | /** 118 | * Return the value of the integer argument with the given name. 119 | * 120 | * @throw ArgumentsException 121 | * 122 | * @param argument name 123 | * @return argument value 124 | */ 125 | int GetInt(const string name); 126 | 127 | /** 128 | * Return the value of the string argument with the given name. 129 | * 130 | * @throw ArgumentsException 131 | * 132 | * @param argument name 133 | * @return argument value 134 | */ 135 | const string& GetString(const string name); 136 | 137 | /** 138 | * Return a vector of the unnamed "extra" arguments included in the 139 | * command line argument string. 140 | * 141 | * Unnamed arguments are useful for command line programs which accept 142 | * an unbounded number of command line arguments, often as the last 143 | * arguments to a program. 144 | * 145 | * Unnamed arguments lack the two dash prefix (--) which distinguishes 146 | * named arguments (e.g. --mybool) and are not associated with named 147 | * arguments as their value (e.g. in "--myint 42", the "42" is not an 148 | * unnamed argument). 149 | * 150 | * Example of 3 unnamed arguments: 151 | * 152 | * "./program --mybool --myint 42 unnamed1 unnamed2 unnamed3" 153 | * 154 | * @return vector of unnamed argument strings 155 | */ 156 | const vector& GetUnnamed(); 157 | 158 | /** 159 | * Returns the program's help text, including the "intro" string 160 | * specified in the constructor, as well as a generated list of 161 | * argument names, descriptions, and types. 162 | * 163 | * @return string of command line help text 164 | */ 165 | string GetHelpText(); 166 | 167 | private: 168 | /** 169 | * Argument descriptions. Maps argument names to string descriptions 170 | * that describe the purpose of the arguments. 171 | */ 172 | map descriptions; 173 | 174 | /** 175 | * Argument value map. Maps arguments of each type (bool, int, string) 176 | * to their actual values after the argument information (i.e. argc and 177 | * argv) has been parsed. 178 | */ 179 | map bool_args; 180 | map int_args; 181 | map string_args; 182 | 183 | /** 184 | * Maps from command alias to full command name and vide versa. Used to 185 | * translate a command alias like e.g. "-h" to a full command name like 186 | * e.g. "--help" and vice versa. 187 | */ 188 | map alias_to_name; 189 | map name_to_alias; 190 | 191 | /** 192 | * Vector of the unnamed "extra" arguments included in the command line 193 | * argument string. 194 | */ 195 | vector unnamed_args; 196 | 197 | /** 198 | * Text to describe the purpose of the program. Used to generate the 199 | * program's help text. 200 | */ 201 | string intro; 202 | }; 203 | 204 | class ArgumentsException : public exception { 205 | public: 206 | ArgumentsException(const string& message): message(message) {} 207 | ArgumentsException(const char* message): message(message) {} 208 | const char* what() const noexcept { return message.c_str(); } 209 | private: 210 | string message; 211 | }; 212 | -------------------------------------------------------------------------------- /test.out: -------------------------------------------------------------------------------- 1 | ....................FF...F....F.F...F...F..F..F..FF....F......F.FF....FF....... 2 | ====================================================================== 3 | FAIL: test_matchString_asterisk (__main__.TestExpand) 4 | ---------------------------------------------------------------------- 5 | Traceback (most recent call last): 6 | File "test-fixed.py", line 221, in test_matchString_asterisk 7 | self.assertEqual("__tes?/x*z\n", run('/bin/echo __tes?/x\*z')) 8 | AssertionError: '__tes?/x*z\n' != '__test/xyz\n' 9 | 10 | ====================================================================== 11 | FAIL: test_matchString_brackets (__main__.TestExpand) 12 | ---------------------------------------------------------------------- 13 | Traceback (most recent call last): 14 | File "test-fixed.py", line 228, in test_matchString_brackets 15 | self.assertEqual("__tes?/x[y]z\n", run('/bin/echo __tes?/x\[y]z')) 16 | AssertionError: '__tes?/x[y]z\n' != '__test/xyz\n' 17 | 18 | ====================================================================== 19 | FAIL: test_matchString_question_mark (__main__.TestExpand) 20 | ---------------------------------------------------------------------- 21 | Traceback (most recent call last): 22 | File "test-fixed.py", line 212, in test_matchString_question_mark 23 | self.assertEqual("__tes?/x?z\n", run('/bin/echo __tes?/x\?z')) 24 | AssertionError: '__tes?/x?z\n' != '__test/xyz\n' 25 | 26 | ====================================================================== 27 | FAIL: test_main_script_file_basics (__main__.TestMain) 28 | ---------------------------------------------------------------------- 29 | Traceback (most recent call last): 30 | File "test-fixed.py", line 290, in test_main_script_file_basics 31 | runWithArgs("__test/script")) 32 | AssertionError: 'foo\nbar\nsecond command\n' != 'foo\nbar /bin/echo second command\n' 33 | 34 | ====================================================================== 35 | FAIL: test_main_script_file_exit_status (__main__.TestMain) 36 | ---------------------------------------------------------------------- 37 | Traceback (most recent call last): 38 | File "test-fixed.py", line 301, in test_main_script_file_exit_status 39 | self.assertEqual("command output\n", runWithArgs("__test/script")) 40 | AssertionError: 'command output\n' != 'command output exit 32\n' 41 | 42 | ====================================================================== 43 | FAIL: test_doSubs_commands (__main__.TestParser) 44 | ---------------------------------------------------------------------- 45 | Traceback (most recent call last): 46 | File "test-fixed.py", line 355, in test_doSubs_commands 47 | self.assertEqual("abc\n", run("x=abc; /bin/echo `/bin/echo $x`")) 48 | AssertionError: 'abc\n' != '-clash: basic_string\n' 49 | 50 | ====================================================================== 51 | FAIL: test_doSubs_tildes_first (__main__.TestParser) 52 | ---------------------------------------------------------------------- 53 | Traceback (most recent call last): 54 | File "test-fixed.py", line 341, in test_doSubs_tildes_first 55 | run("x='~ ~'; /bin/echo ~ $x \"~\" '~'")) 56 | AssertionError: '/Users/feross ~ ~ ~ ~\n' != '/Users/feross /Users/feross /Users/feross ~ ~\n' 57 | 58 | ====================================================================== 59 | FAIL: test_eval_errors (__main__.TestParser) 60 | ---------------------------------------------------------------------- 61 | Traceback (most recent call last): 62 | File "test-fixed.py", line 331, in test_eval_errors 63 | run("/bin/echo 'a b c")) 64 | AssertionError: "unexpected EOF while looking for matching `''" not found in '-clash: syntax error: unexpected end of file\n' 65 | 66 | ====================================================================== 67 | FAIL: test_parse_backslashes (__main__.TestParser) 68 | ---------------------------------------------------------------------- 69 | Traceback (most recent call last): 70 | File "test-fixed.py", line 436, in test_parse_backslashes 71 | run("/bin/echo \\")) 72 | AssertionError: 'Unexpected EOF while parsing backslash' not found in '-clash: syntax error: unexpected end of file\n' 73 | 74 | ====================================================================== 75 | FAIL: test_parse_commands_basics (__main__.TestParser) 76 | ---------------------------------------------------------------------- 77 | Traceback (most recent call last): 78 | File "test-fixed.py", line 419, in test_parse_commands_basics 79 | run("x='$y'; y=abc; /bin/echo `./words.py $x`")) 80 | AssertionError: 'Word 1: $y\n' != '-clash: basic_string\n' 81 | 82 | ====================================================================== 83 | FAIL: test_parse_commands_errors (__main__.TestParser) 84 | ---------------------------------------------------------------------- 85 | Traceback (most recent call last): 86 | File "test-fixed.py", line 430, in test_parse_commands_errors 87 | run("/bin/echo `foo bar")) 88 | AssertionError: "Unexpected EOF while looking for matching ``'" not found in '-clash: syntax error: unexpected end of file\n' 89 | 90 | ====================================================================== 91 | FAIL: test_parse_variables (__main__.TestParser) 92 | ---------------------------------------------------------------------- 93 | Traceback (most recent call last): 94 | File "test-fixed.py", line 407, in test_parse_variables 95 | run("/bin/echo ${xxx")) 96 | AssertionError: "unexpected EOF while looking for `}'" not found in '-clash: syntax error: unexpected end of file\n' 97 | 98 | ====================================================================== 99 | FAIL: test_rebuildPathMap_basics (__main__.TestPipeline) 100 | ---------------------------------------------------------------------- 101 | Traceback (most recent call last): 102 | File "test-fixed.py", line 517, in test_rebuildPathMap_basics 103 | run("PATH=\"`/bin/pwd`/__test/child:`/bin/pwd`/__test\"; a; b")) 104 | AssertionError: '__test/child/a\n__test/b\n' != '__test/child/a\n-clash: /Users/feross/stanford/CS190/proj3-5/__test/child/b: No such file or directory\n' 105 | 106 | ====================================================================== 107 | FAIL: test_run_ambiguous_input_redirection (__main__.TestPipeline) 108 | ---------------------------------------------------------------------- 109 | Traceback (most recent call last): 110 | File "test-fixed.py", line 530, in test_run_ambiguous_input_redirection 111 | run("x='a b'; /bin/cat <$x")) 112 | AssertionError: 'Ambiguous input redirection' not found in '-clash: no file given for input redirection\n' 113 | 114 | ====================================================================== 115 | FAIL: test_run_ambiguous_output_redirection (__main__.TestPipeline) 116 | ---------------------------------------------------------------------- 117 | Traceback (most recent call last): 118 | File "test-fixed.py", line 539, in test_run_ambiguous_output_redirection 119 | run("x='a b'; /bin/echo foo bar >$x")) 120 | AssertionError: 'Ambiguous output redirection' not found in '-clash: no file given for output redirection\n' 121 | 122 | ====================================================================== 123 | FAIL: test_run_rebuild_path_cache_path_changed (__main__.TestPipeline) 124 | ---------------------------------------------------------------------- 125 | Traceback (most recent call last): 126 | File "test-fixed.py", line 560, in test_run_rebuild_path_cache_path_changed 127 | run("x; PATH=\"/bin:`pwd`/__test\"; x")) 128 | AssertionError: ' x: command not found\n__test/x' not found in '-clash: x: command not found\n/Users/feross/Documents/Stanford/CS190/proj3-5\n/Users/feross/Documents/Stanford/CS190/proj3-5\n/Users/feross/Documents/Stanford/CS190/proj3-5\n-clash: Unable to open directory /__test\n' 129 | 130 | ====================================================================== 131 | FAIL: test_run_rebuild_path_cache_to_discover_new_file (__main__.TestPipeline) 132 | ---------------------------------------------------------------------- 133 | Traceback (most recent call last): 134 | File "test-fixed.py", line 553, in test_run_rebuild_path_cache_to_discover_new_file 135 | run("PATH=\"/bin:`pwd`/__test\"; x; chmod +x __test/x; x")) 136 | AssertionError: ' x: command not found\n__test/x' not found in '/Users/feross/Documents/Stanford/CS190/proj3-5\n-clash: Unable to open directory /__test\n' 137 | 138 | ---------------------------------------------------------------------- 139 | Ran 79 tests in 2.854s 140 | 141 | FAILED (failures=17) 142 | -------------------------------------------------------------------------------- /src/job-parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "string-util.h" 4 | #include "environment.h" 5 | #include "proc-util.h" 6 | 7 | using namespace std; 8 | 9 | 10 | /** 11 | * Set of structs containing the information necessary to spawn a job 12 | */ 13 | 14 | /** 15 | * Parsed Command describes where a command, and whether it shoudl redirect 16 | * input or output, potentially including stderr 17 | */ 18 | struct ParsedCommand { 19 | ParsedCommand() : redirect_stderr(false) {} 20 | vector words; 21 | string input_file; 22 | string output_file; 23 | bool redirect_stderr; 24 | void clear() { 25 | words.clear(); 26 | input_file.clear(); 27 | output_file.clear(); 28 | }; 29 | }; 30 | 31 | /** 32 | * Parsed Pipeline describes where a Pipeline, and any remaining 33 | * job string that will need to be reparsed after we run this pipeline 34 | * (e.g. variables that may now be set) 35 | */ 36 | struct ParsedPipeline { 37 | vector commands; 38 | string remaining_job_str; //after we run, any later pipelines must be reparsed 39 | void clear() { 40 | commands.clear(); 41 | remaining_job_str.clear(); 42 | }; 43 | }; 44 | 45 | /** 46 | * Parsed Job describes a job, whether it is completely cnstructed, and 47 | * contains a useful utility for printing the content of a job. 48 | */ 49 | struct ParsedJob { 50 | vector pipelines; 51 | bool complete = true; //TODO: for cases where we have multiple lines, e.g. \\n, unmatched quotes, etc 52 | void clear() { 53 | pipelines.clear(); 54 | complete = true; //TODO: false in future 55 | }; 56 | void print() { 57 | debug("%s", "job:"); 58 | for (ParsedPipeline& pipeline : pipelines) { 59 | debug("%s", " pipeline:"); 60 | for (ParsedCommand& command : pipeline.commands) { 61 | debug("%s", " command:"); 62 | debug(" input_file:%s", command.input_file.c_str()); 63 | debug(" output_file:%s", command.output_file.c_str()); 64 | for (string& command_str : command.words) { 65 | debug(" word: %s", command_str.c_str()); 66 | } 67 | } 68 | } 69 | } 70 | }; 71 | 72 | class IncompleteParseException : public exception { 73 | public: 74 | IncompleteParseException() {} 75 | IncompleteParseException(const string& message, char ch): message(message), 76 | problem_char(ch) {} 77 | IncompleteParseException(const char* message): message(message) {} 78 | const char* what() const noexcept { return message.c_str(); } 79 | char unmatched_char() const noexcept { return problem_char; } 80 | private: 81 | string message; 82 | char problem_char; 83 | }; 84 | 85 | class FatalParseException : public exception { 86 | public: 87 | FatalParseException() {} 88 | FatalParseException(const string& message): message(message) {} 89 | FatalParseException(const char* message): message(message) {} 90 | const char* what() const noexcept { return message.c_str(); } 91 | private: 92 | string message; 93 | }; 94 | 95 | class SyntaxErrorParseException : public FatalParseException { 96 | public: 97 | SyntaxErrorParseException(const string& message): message(message) {} 98 | SyntaxErrorParseException(const char* message): message(message) {} 99 | SyntaxErrorParseException(const char ch) { 100 | message = ("syntax error near unexpected token \'" + 101 | string(1, ch) + "\'").c_str(); 102 | } 103 | const char* what() const noexcept { return message.c_str(); } 104 | private: 105 | string message; 106 | }; 107 | 108 | class JobParser { 109 | /** 110 | * Static parsing class, primarily using the call stack to keep track of 111 | * any matching requirements for characters. Due to this design, we 112 | * throw on parsing errors, and generally require a valid command for 113 | * successful parsing (we throw helpful exceptions to describe why parsing 114 | * failed - including exceptions to specify paritally-complete jobs v.s. 115 | * invalid jobs) 116 | */ 117 | public: 118 | /** 119 | * Attempts to determine whether a job_str fully describes a job 120 | * or whether more input is needed before a complete job 121 | * can be constructed. 122 | * 123 | * @param job_str - string describing job, to be parsed 124 | * @param env - the environment which has variables we may substitute 125 | * 126 | * @return bool - whether this is a partial job 127 | * throws - FatalParseException if this job is syntactically invalid 128 | */ 129 | static bool IsPartialJob(string& job_str, Environment& env); 130 | 131 | /** 132 | * Parse method will completely parse a command, including any command 133 | * substitutions that must be run to determine the command. 134 | * Should use IsPartialJob first to determine if this job is valid, 135 | * to avoid running command substitutions. 136 | * 137 | * @param job_str - string describing job, to be parsed 138 | * @param env - the environment which has variables we may substitute 139 | * 140 | * @return ParsedJob - the fully parsed job content 141 | * throws - FatalParseException if this job is syntactically invalid 142 | * throws - IncompletePraseException if this job incomplete 143 | */ 144 | static ParsedJob Parse(string& job_str, Environment& env); 145 | 146 | private: 147 | /** 148 | * Internal parse method that takes a boolean determining whether to 149 | * execute command substitutions or not. 150 | * 151 | * @param job_str - string describing job, to be parsed 152 | * @param env - the environment which has variables we may substitute 153 | * @return ParsedJob - the fully parsed job content 154 | * 155 | * throws - FatalParseException if this job is syntactically invalid 156 | * throws - IncompletePraseException if this job incomplete 157 | */ 158 | static ParsedJob Parse(string& job_str, Environment& env, 159 | bool should_execute); 160 | 161 | /** 162 | * Parses an individual pipeline from command. Was originally intended 163 | * to allow parsing of individual commands to be run before parsing 164 | * subsequent commands (since this may affect variable substitutions), 165 | * but experimentation with bash suggests that parsing is approximately 166 | * checked up-front (except subcommands). 167 | */ 168 | static ParsedPipeline ParsePipeline(string& job_str_copy, 169 | Environment& env, bool should_execute); 170 | /** 171 | * Will attempt to consume input until a valid closing quote is found. 172 | * This includes performing any interal substitutions necessary, though 173 | * will only attempt command substitutions if we know we are planning 174 | * on running the full job. 175 | * 176 | * @param job_str_copy - a reference to a copy of the string we are 177 | * parsing over. Consumes job_string 178 | * @param env - the environment which has variables we may substitute 179 | * @return - string content inside the quotes 180 | */ 181 | static string ParseDoubleQuote(string& job_str_copy, Environment& env, 182 | bool should_execute); 183 | /** 184 | * Will attempt to consume input until a valid closing backtick is found 185 | * If should_execute is true, we will actually run the subcommand (which 186 | * is part of parsing - it's generating the input to be parsed). The 187 | * output is pushed back on the front of the string we are parsing, to 188 | * be "reparsed from scratch" including things that may change the 189 | * behavior & interpretation of characters outside the backticks 190 | * 191 | * @param job_str_copy - a reference to a copy of the string we are 192 | * parsing over. Consumes job_string 193 | * @param env - the environment which has variables we may substitute 194 | * @return - Returns an empty string, since all output must be reparsed 195 | * from scratch. 196 | */ 197 | static string ParseBacktick(string& job_str_copy, Environment& env, 198 | bool should_execute); 199 | static string ParseSingleQuote(string& job_str_copy); 200 | static string ParseBackslash(string& job_str_copy, char mode = ' '); 201 | static string ParseVariable(string& job_str_copy, Environment& env); 202 | static string ParseTilde(string& job_str_copy, Environment& env); 203 | 204 | }; 205 | -------------------------------------------------------------------------------- /src/file-util.cc: -------------------------------------------------------------------------------- 1 | #include "file-util.h" 2 | 3 | vector FileUtil::CreatePipe() { 4 | int fds[2]; 5 | #ifdef _GNU_SOURCE 6 | if (pipe2(fds, O_CLOEXEC) == -1) { 7 | throw FileException("Failed to create pipe"); 8 | } 9 | #else 10 | if (pipe(fds) == -1) { 11 | throw FileException("Failed to create pipe"); 12 | } 13 | 14 | if (fcntl(fds[0], F_SETFD, FD_CLOEXEC) == -1 || 15 | fcntl(fds[1], F_SETFD, FD_CLOEXEC) == -1) { 16 | throw FileException("Failed to configure pipe"); 17 | } 18 | #endif 19 | 20 | vector fds_vec; 21 | fds_vec.assign(fds, fds + 2); 22 | return fds_vec; 23 | } 24 | 25 | void FileUtil::CloseDescriptor(int fd) { 26 | if (close(fd) != 0) { 27 | throw FileException("Unable to close descriptor " + to_string(fd)); 28 | } 29 | } 30 | 31 | void FileUtil::DuplicateDescriptor(int new_fd, int old_fd) { 32 | if (dup2(new_fd, old_fd) != old_fd) { 33 | throw FileException("Unable to duplicate descriptor " + 34 | to_string(new_fd)); 35 | } 36 | } 37 | 38 | int FileUtil::OpenFile(string& file_path, int flags, mode_t mode) { 39 | int fd = open(file_path.c_str(), flags | O_CLOEXEC, mode); 40 | if (fd == -1) { 41 | throw FileException("No such file or directory: " + file_path); 42 | } 43 | return fd; 44 | } 45 | 46 | string FileUtil::ReadFileDescriptor(int fd) { 47 | string contents = string(); 48 | char buf[1024 + 1]; 49 | int read_bytes; 50 | while ((read_bytes = read(fd, buf, 1024)) != 0) { 51 | buf[read_bytes] = '\0'; 52 | contents.append(buf); 53 | } 54 | return contents; 55 | } 56 | 57 | vector FileUtil::GetDirectoryEntries(const string& path) { 58 | vector entries; 59 | 60 | DIR * dirp = opendir(path == "" ? "." : path.c_str()); 61 | if (dirp == NULL) { 62 | throw FileException("Unable to open directory " + path); 63 | } 64 | 65 | while (true) { 66 | errno = 0; 67 | dirent * dir = readdir(dirp); 68 | 69 | // read error 70 | if (dir == NULL && errno != 0) { 71 | throw FileException(strerror(errno)); 72 | } 73 | 74 | // end of stream 75 | if (dir == NULL && errno == 0) { 76 | break; 77 | } 78 | 79 | const char * entry = dir->d_name; 80 | 81 | if (strcmp(entry, ".") != 0 && strcmp(entry, "..") != 0) { 82 | entries.push_back(entry); 83 | } 84 | } 85 | 86 | if (closedir(dirp) != 0) { 87 | throw FileException("Unable to close directory " + path); 88 | } 89 | 90 | return entries; 91 | } 92 | 93 | vector FileUtil::GetGlobMatches(const string& glob_pattern) { 94 | vector current_matches; 95 | if (glob_pattern.length() == 0) { 96 | return current_matches; 97 | } 98 | 99 | vector pattern_segments = StringUtil::Split(glob_pattern, "/"); 100 | if (pattern_segments.front() == "") { 101 | pattern_segments.erase(pattern_segments.begin()); 102 | current_matches.push_back("/"); 103 | } else { 104 | current_matches.push_back(""); 105 | } 106 | 107 | for (size_t i = 0; i < pattern_segments.size(); i++) { 108 | string pattern_segment = pattern_segments[i]; 109 | 110 | // Ensure that repeated path separators (i.e '//') are ignored, except 111 | // when they appear after a path segment that ends in an alphanumeric 112 | // character. Matches bash behavior. 113 | if (pattern_segment == "") { 114 | if (i > 0 && isalnum(pattern_segments[i - 1].back())) { 115 | for (string& current_match : current_matches) { 116 | current_match += "/"; 117 | } 118 | } 119 | continue; 120 | } 121 | 122 | vector next_matches; 123 | for (string& current_match : current_matches) { 124 | vector entries = GetDirectoryEntries(current_match); 125 | 126 | for (string& entry : entries) { 127 | if (GlobMatch(pattern_segment, entry)) { 128 | string next_match; 129 | if (current_match == "") { 130 | next_match = entry; 131 | } else if (current_match == "/") { 132 | next_match = "/" + entry; 133 | } else { 134 | next_match = current_match + "/" + entry; 135 | } 136 | 137 | // Ensure that files are only added when examining the last 138 | // path segment. Otherwise, skip them since they cannot 139 | // match if there are further segments to examine later. 140 | if (IsDirectory(next_match) || i == pattern_segments.size() - 1) { 141 | next_matches.push_back(next_match); 142 | } 143 | } 144 | } 145 | } 146 | current_matches = next_matches; 147 | } 148 | 149 | // If pattern matched no files, return the pattern as-is. 150 | if (current_matches.size() == 0) { 151 | current_matches.push_back(glob_pattern); 152 | } 153 | 154 | return current_matches; 155 | } 156 | 157 | bool FileUtil::GlobMatch(const string& pattern, const string& name) { 158 | int px = 0; 159 | int nx = 0; 160 | int nextPx = 0; 161 | int nextNx = 0; 162 | 163 | vector charClass; 164 | int inCharClass = false; 165 | bool inRange = false; 166 | char rangeStart = '\0'; 167 | bool negateCharClass = false; 168 | 169 | while (px < pattern.length() || nx < name.length()) { 170 | if (px < pattern.length()) { 171 | char c = pattern[px]; 172 | switch (c) { 173 | case '?': { // Single-character wildcard 174 | if (nx < name.length() 175 | && (nx > 0 || c != '.')) { // Don't match leading dot in hidden file name 176 | px++; 177 | nx++; 178 | continue; 179 | } 180 | break; 181 | } 182 | case '*': { // Zero-or-more-character wildcard 183 | if (nx > 0 || c != '.') { // Don't match leading dot in hidden file name 184 | // Try to match at nx. If that doesn't work out, restart 185 | // at nx+1 next. 186 | nextPx = px; 187 | nextNx = nx + 1; 188 | px++; 189 | continue; 190 | } 191 | break; 192 | } 193 | case '[': { // Start of character class 194 | if (inCharClass) { 195 | // Appears within character class, treat as literal 196 | goto default_case; 197 | } 198 | inCharClass = true; 199 | px++; 200 | continue; 201 | } 202 | case ']': { // End of character class 203 | if (!inCharClass) { 204 | // appears outside of character class, treat as literal 205 | goto default_case; 206 | } 207 | if (inRange) { 208 | throw FileException("End of character class without ending range"); 209 | } 210 | if (nx < name.length()) { 211 | bool matched = find(charClass.begin(), charClass.end(), 212 | name[nx]) != charClass.end(); 213 | if (matched != negateCharClass) { 214 | px++; 215 | nx++; 216 | charClass.clear(); 217 | inCharClass = false; 218 | negateCharClass = false; 219 | continue; 220 | } 221 | } 222 | break; 223 | } 224 | case '-': { // Chracter class range character 225 | if (!inCharClass) { 226 | // Appears outside of character class, treat as literal 227 | goto default_case; 228 | } 229 | if (charClass.size() == 0) { 230 | // Appears at start of character class, treat as literal 231 | goto default_case; 232 | } 233 | inRange = true; 234 | rangeStart = charClass.back(); 235 | charClass.pop_back(); 236 | px++; 237 | continue; 238 | } 239 | case '^': { // Character class negation character 240 | if (!inCharClass) { 241 | // Appears outside of character class, treat as literal 242 | goto default_case; 243 | } 244 | if (charClass.size() > 0) { 245 | // Not first character of the character class (i.e. [a^b]) 246 | // so treat as literal 247 | goto default_case; 248 | } 249 | negateCharClass = true; 250 | px++; 251 | continue; 252 | } 253 | default: 254 | default_case: { 255 | if (inRange) { 256 | char rangeEnd = c; 257 | for (char ch = rangeStart; ch <= rangeEnd; ch++) { 258 | charClass.push_back(ch); 259 | } 260 | px++; 261 | inRange = false; 262 | continue; 263 | } 264 | if (inCharClass) { 265 | charClass.push_back(c); 266 | px++; 267 | continue; 268 | } 269 | 270 | // Ordinary character 271 | if (nx < name.length() && name[nx] == c) { 272 | px++; 273 | nx++; 274 | continue; 275 | } 276 | break; 277 | } 278 | } 279 | } 280 | 281 | // Mismatch. Maybe restart. 282 | // For more information about this strategy which allows star matching 283 | // to run in real-time, see this blog post: https://research.swtch.com/glob 284 | if (nextNx > 0 && nextNx <= name.length()) { 285 | px = nextPx; 286 | nx = nextNx; 287 | charClass.clear(); 288 | inCharClass = false; 289 | inRange = false; 290 | negateCharClass = false; 291 | continue; 292 | } 293 | return false; 294 | } 295 | // Matched all of pattern to all of name. Success. 296 | return true; 297 | } 298 | 299 | bool FileUtil::IsExecutableFile(const string& path) { 300 | return access(path.c_str(), X_OK) == 0; 301 | } 302 | 303 | bool FileUtil::IsDirectory(const string& path) { 304 | struct stat stat_result; 305 | if (stat(path.c_str(), &stat_result) != 0) { 306 | throw FileException(strerror(errno)); 307 | } 308 | return stat_result.st_mode & S_IFDIR; 309 | } 310 | -------------------------------------------------------------------------------- /src/job-parser.cc: -------------------------------------------------------------------------------- 1 | #include "job-parser.h" 2 | #include "job.h" 3 | 4 | #include 5 | #include 6 | 7 | bool JobParser::IsPartialJob(string& job_str, Environment& env) { 8 | try { 9 | Parse(job_str, env, false); 10 | } catch (IncompleteParseException& ipe) { 11 | //incomplete job given, need more lines 12 | return true; 13 | } //intentionally allow fatal errors through 14 | return false; 15 | } 16 | 17 | ParsedJob JobParser::Parse(string& job_str, Environment& env) { 18 | return Parse(job_str, env, true); 19 | } 20 | 21 | //overview: will parse individual pipelines... and keep continuing until whole 22 | //string is consumed (throwing if last pipeline is incomplete) 23 | 24 | ParsedJob JobParser::Parse(string& job_str, Environment& env, bool should_execute) { 25 | string job_str_copy(job_str); 26 | ParsedJob job; 27 | while(!job_str_copy.empty()) { 28 | ParsedPipeline pipeline = ParsePipeline(job_str_copy, env, should_execute); 29 | pipeline.remaining_job_str = string(job_str_copy); 30 | if (pipeline.commands.size() > 0) job.pipelines.push_back(pipeline); 31 | } 32 | return job; 33 | } 34 | 35 | 36 | ParsedPipeline JobParser::ParsePipeline(string& job_str_copy, Environment& env, bool should_execute) { 37 | 38 | ParsedCommand command; 39 | ParsedPipeline pipeline; 40 | 41 | string partial_word = string(); 42 | bool next_word_redirects_out = false; 43 | bool next_word_redirects_in = false; 44 | bool quote_word = false; 45 | bool glob_current_word = false; 46 | 47 | while(true) { 48 | int match_index = strcspn(job_str_copy.c_str(), " \t\n;|<>~$'`\"\\*?[&"); 49 | if (match_index != 0) partial_word.append(job_str_copy.substr(0,match_index)); 50 | 51 | //word breaking 52 | if (match_index == job_str_copy.size() || 53 | string("\t\n ;|<>").find(job_str_copy[match_index]) != string::npos) { 54 | if (partial_word.size() > 0 || quote_word) { //word exists 55 | 56 | vector words_to_add; 57 | if (glob_current_word) { 58 | glob_current_word = false; 59 | words_to_add = FileUtil::GetGlobMatches(partial_word); 60 | //always at least one word 61 | } else words_to_add.push_back(partial_word); 62 | if (next_word_redirects_in) { 63 | command.input_file = words_to_add[0]; 64 | next_word_redirects_in = false; 65 | } else if (next_word_redirects_out) { 66 | command.output_file = words_to_add[0]; 67 | next_word_redirects_out = false; 68 | } else command.words.push_back(words_to_add[0]); 69 | 70 | for (int i = 1; i < words_to_add.size(); i++) { 71 | command.words.push_back(words_to_add[i]); 72 | } 73 | partial_word = string(); 74 | quote_word = false; 75 | } 76 | } 77 | 78 | //command breaking 79 | if (match_index == job_str_copy.size()) { 80 | if (command.words.empty() && !pipeline.commands.empty()) { 81 | throw IncompleteParseException("Incomplete job given, no command break", '|'); 82 | } 83 | if (!command.words.empty()) pipeline.commands.push_back(command); 84 | // technically code duplication, but the implementation that avoids 85 | // this is significantly less clear. 86 | } else if (string(";|").find(job_str_copy[match_index]) != string::npos) { 87 | if (command.words.empty()) { 88 | throw SyntaxErrorParseException(job_str_copy[match_index]); 89 | } 90 | pipeline.commands.push_back(command); 91 | command.clear(); 92 | } 93 | 94 | //redirected words 95 | if (next_word_redirects_in || next_word_redirects_out) { 96 | if (match_index == job_str_copy.size()) { 97 | throw SyntaxErrorParseException("syntax error near unexpected newline"); 98 | } else if (string("\n;|<>").find(job_str_copy[match_index]) != string::npos) { 99 | if (next_word_redirects_in) { 100 | throw SyntaxErrorParseException("no file given for input redirection"); 101 | } else if (next_word_redirects_out) { 102 | throw SyntaxErrorParseException("no file given for output redirection"); 103 | } 104 | throw SyntaxErrorParseException(job_str_copy[match_index]); 105 | } 106 | } 107 | 108 | //fully consumed string 109 | if (match_index == job_str_copy.size()) { 110 | job_str_copy = string(); //technically job_str_copy.substr(match_index); 111 | break; 112 | } 113 | char matched = job_str_copy[match_index]; 114 | job_str_copy = job_str_copy.substr(match_index + 1); 115 | if (matched == ';') { 116 | break; //because can't break within switch 117 | } 118 | switch(matched) { 119 | case '&': 120 | if (partial_word.empty() && pipeline.commands.size() > 0) { 121 | pipeline.commands.back().redirect_stderr = true; 122 | } else { 123 | partial_word.append(1, matched); 124 | } 125 | continue; 126 | case '*': 127 | case '?': 128 | case '[': { 129 | glob_current_word = true; 130 | partial_word.append(1, matched); 131 | continue; 132 | } 133 | case '\t': 134 | case ' ': 135 | case '\n': 136 | case '|': { 137 | debug("whitespace, prev_word:%s", partial_word.c_str()); 138 | continue; 139 | } 140 | case '<': { 141 | next_word_redirects_in = true; 142 | continue; 143 | } 144 | case '>': { 145 | next_word_redirects_out = true; 146 | continue; 147 | } 148 | case '~': { 149 | if (partial_word.empty() && !quote_word) { 150 | partial_word.append(ParseTilde(job_str_copy, env)); 151 | } else { 152 | partial_word.append("~"); 153 | } 154 | continue; 155 | } 156 | case '\"': { 157 | partial_word.append(ParseDoubleQuote(job_str_copy, env, should_execute)); 158 | quote_word = true; 159 | continue; 160 | } 161 | case '\'': { 162 | partial_word.append(ParseSingleQuote(job_str_copy)); 163 | quote_word = true; 164 | continue; 165 | } 166 | case '`': { 167 | partial_word.append(ParseBacktick(job_str_copy, env, should_execute)); 168 | continue; 169 | } 170 | case '\\': { 171 | partial_word.append(ParseBackslash(job_str_copy)); 172 | continue; 173 | } 174 | case '$': { 175 | //to word break, places variable to parse back on job_str_copy 176 | string nonparse_output = ParseVariable(job_str_copy, env); 177 | if (nonparse_output == string("ambiguous if redirect")) { 178 | if (next_word_redirects_in) { 179 | throw FatalParseException("Ambiguous input redirection"); 180 | } else if (next_word_redirects_out) { 181 | throw FatalParseException("Ambiguous output redirection"); 182 | } 183 | nonparse_output = string(); 184 | } 185 | partial_word.append(nonparse_output); 186 | continue; 187 | } 188 | default : { 189 | throw IncompleteParseException("Matched Unknown character", '?'); 190 | } 191 | } 192 | } 193 | return pipeline; 194 | } 195 | 196 | 197 | string JobParser::ParseDoubleQuote(string& job_str_copy, Environment& env, 198 | bool should_execute) { 199 | string quoted = string(); 200 | int match_index; 201 | while((match_index = strcspn(job_str_copy.c_str(), "\"`$\\")) != job_str_copy.size()) { 202 | char matched = job_str_copy[match_index]; 203 | quoted.append(job_str_copy.substr(0,match_index)); 204 | job_str_copy = job_str_copy.substr(match_index + 1); 205 | switch(matched) { 206 | case '\"': { 207 | return quoted; 208 | } 209 | case '`': { 210 | quoted.append(ParseBacktick(job_str_copy, env, should_execute)); 211 | continue; 212 | } 213 | case '\\': { 214 | quoted.append(ParseBackslash(job_str_copy, '\"')); 215 | continue; 216 | } 217 | case '$': { 218 | string nonparsed_output = ParseVariable(job_str_copy, env); 219 | if (nonparsed_output != string("ambiguous if redirect")) { 220 | quoted.append(nonparsed_output); 221 | } 222 | continue; 223 | } 224 | } 225 | } 226 | // unmatched " 227 | throw IncompleteParseException("Incomplete job given, no valid closing quote (\")", '\"'); 228 | } 229 | 230 | string JobParser::ParseSingleQuote(string& job_str_copy) { 231 | int match_index = strcspn(job_str_copy.c_str(), "\'"); 232 | string quoted = job_str_copy.substr(0,match_index); 233 | if (match_index != job_str_copy.size()) { 234 | job_str_copy = job_str_copy.substr(match_index + 1); 235 | return quoted; 236 | } 237 | // unmatched ' 238 | throw IncompleteParseException("Incomplete job given, no valid closing quote (')", '\''); 239 | } 240 | 241 | string JobParser::ParseBackslash(string& job_str_copy, char mode) { 242 | string quoted = job_str_copy.substr(0,1); 243 | if (mode == ' ') { 244 | job_str_copy = job_str_copy.substr(1); 245 | if (quoted != "\n") return quoted; 246 | else { 247 | if (job_str_copy.empty()) { 248 | throw IncompleteParseException("Incomplete job given, no valid closing (\\)", '\\'); 249 | } else { 250 | return string(); 251 | } 252 | } 253 | } 254 | //"$", "`" (backquote), double-quote, backslash, or newline; 255 | if (mode == '\"') { 256 | string valid_matches("$`\"\\\n"); 257 | if (valid_matches.find(quoted) == string::npos) { 258 | string unmodified("\\"); 259 | return unmodified; 260 | } 261 | job_str_copy = job_str_copy.substr(1); 262 | return quoted; 263 | } 264 | //only backtick (inside, will parse as new job) 265 | if (mode == '`') { 266 | string valid_matches("`"); 267 | if (valid_matches.find(quoted) == string::npos) { 268 | string unmodified("\\"); 269 | return unmodified; 270 | } 271 | job_str_copy = job_str_copy.substr(1); 272 | return quoted; 273 | } 274 | throw IncompleteParseException("Unknown backslash mode", '\\'); 275 | } 276 | 277 | string JobParser::ParseVariable(string& job_str_copy, Environment& env) { 278 | char first_var_char = job_str_copy[0]; 279 | string variable_name; 280 | if (string("*?#").find(first_var_char) != string::npos || 281 | isdigit(first_var_char)) { 282 | variable_name = string(1, first_var_char); 283 | job_str_copy = job_str_copy.substr(1); 284 | } else if (first_var_char =='{') { 285 | int match_index = job_str_copy.find_first_of("}"); 286 | if (match_index == string::npos) { 287 | throw IncompleteParseException("Incomplete job given, no valid closing (})", '}'); 288 | } 289 | variable_name = job_str_copy.substr(1,match_index-1); //skip first { 290 | job_str_copy = job_str_copy.substr(match_index + 1); 291 | } else if (isalpha(first_var_char)) { 292 | int match_index = job_str_copy.find_first_not_of( 293 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); 294 | variable_name = job_str_copy.substr(0,match_index); 295 | job_str_copy = job_str_copy.substr(match_index); 296 | } else { 297 | string unmodified("$"); 298 | return unmodified; 299 | } 300 | string var_value = env.GetVariable(variable_name); 301 | job_str_copy = var_value + job_str_copy; 302 | 303 | /** 304 | * Bash for some reason is not OK with redirect to a variable with a space, 305 | * even though it's supposed to parse into words... and it's fine with you 306 | * giving the same thing as multiple words. 307 | * Better handling option: 308 | * return word containing full variable, and parse in the returning context 309 | * for spaces only (would be relatively easy, even just split - TODO) 310 | */ 311 | if (var_value.find(" \t\n") != string::npos) { 312 | return string("ambiguous if redirect"); 313 | } 314 | return string(); 315 | } 316 | 317 | 318 | string JobParser::ParseBacktick(string& job_str_copy, Environment& env, 319 | bool should_execute) { 320 | string quoted = string(); 321 | int match_index; 322 | while((match_index = strcspn(job_str_copy.c_str(), "`\\")) != job_str_copy.size()) { 323 | char matched = job_str_copy[match_index]; 324 | debug("strcspn loc str:%s, char:%c", job_str_copy.c_str() + match_index, matched); 325 | quoted.append(job_str_copy.substr(0,match_index)); 326 | job_str_copy = job_str_copy.substr(match_index + 1); 327 | if (matched == '`') { 328 | if (should_execute) { 329 | string command_output_str; 330 | try { 331 | vector fds = FileUtil::CreatePipe(); 332 | int read = fds[0]; 333 | int write = fds[1]; 334 | ParsedJob parsed_job = Parse(quoted, env); 335 | Job(parsed_job, env).RunAndWait(STDIN_FILENO, write); 336 | FileUtil::CloseDescriptor(write); 337 | command_output_str = FileUtil::ReadFileDescriptor(read); 338 | FileUtil::CloseDescriptor(read); 339 | //bash special cases this (compare bash printf v.s. echo 340 | //in command subs - should be different, isn't) 341 | int end_pos = command_output_str.size() - 1; 342 | if (command_output_str[end_pos] == '\n') { 343 | command_output_str = command_output_str.substr(0, end_pos); 344 | } 345 | } catch (IncompleteParseException& ipe) { 346 | string subcommand_err_message("command substitution: "); 347 | subcommand_err_message.append(ipe.what()); 348 | throw FatalParseException(subcommand_err_message); 349 | } catch (FatalParseException& fpe) { 350 | string subcommand_err_message("command substitution: "); 351 | subcommand_err_message.append(fpe.what()); 352 | throw FatalParseException(subcommand_err_message); 353 | } 354 | job_str_copy = command_output_str + job_str_copy; 355 | debug("new string:%s\n", job_str_copy.c_str()); 356 | } 357 | return string(); 358 | } else { 359 | quoted.append(ParseBackslash(job_str_copy, '`')); 360 | } 361 | } 362 | throw IncompleteParseException("Incomplete job given, no valid closing backtick (`)", '`'); 363 | } 364 | 365 | string JobParser::ParseTilde(string& job_str_copy, Environment& env) { 366 | int match_index = job_str_copy.find_first_of("/\t\n ;|<>"); 367 | string matched_str = job_str_copy.substr(0,match_index); 368 | if (matched_str.size() == 0) { 369 | //just expand tilde w/o username. As per spec, this is default var 370 | return env.GetVariable("HOME"); 371 | } 372 | string home_dir = ProcUtil::GetUserHomeDirectory(matched_str); 373 | if (home_dir == "") { 374 | //no user found, just use literal tilde & consume no input 375 | return string("~"); 376 | } 377 | job_str_copy = job_str_copy.substr(match_index); 378 | return home_dir; 379 | } 380 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # A test suite for clash. Designed as unit tests for John Ousterhout's 4 | # implementation, but implemented at a high enough level that it can be 5 | # applied to other implementations as well. 6 | 7 | import os 8 | import shutil 9 | import subprocess 10 | import sys 11 | import time 12 | import unittest 13 | 14 | # The location of the shell to be tested. You can change this to 15 | # /bin/bash to see if Bash passes this test suite. 16 | shell = "./clash" 17 | #shell = "/bin/bash" 18 | 19 | # This variable will hold the exit status of the most recent command 20 | # executed by "run" 21 | status = 0 22 | 23 | def readFile(name): 24 | """ Return the contents of a file. """ 25 | f = open(name, "r") 26 | result = f.read() 27 | f.close() 28 | return result 29 | 30 | def writeFile(name, contents="xyzzy"): 31 | """ Create a file with particular contents. """ 32 | f = open(name, "w") 33 | f.write(contents) 34 | f.close() 35 | 36 | def run(cmd): 37 | """ Invoke the test shell with the given command and return any 38 | output generated. 39 | """ 40 | 41 | global status 42 | writeFile("__stdin", cmd) 43 | stdin = open("__stdin", "r") 44 | 45 | # This sometime fails under Cygwin, so try again when that happens 46 | try: 47 | stdout = open("__stdout", "w") 48 | except: 49 | time.sleep(0.01) 50 | stdout = open("__stdout", "w") 51 | 52 | status = subprocess.call(shell, stdin=stdin, stdout=stdout, 53 | stderr=subprocess.STDOUT) 54 | result = readFile("__stdout") 55 | stdin.close() 56 | stdout.close() 57 | os.remove("__stdin") 58 | 59 | # This sometime fails under Cygwin, so try again when that happens 60 | try: 61 | os.remove("__stdout") 62 | except: 63 | time.sleep(0.01) 64 | os.remove("__stdout") 65 | 66 | return result 67 | 68 | def runWithArgs(*args): 69 | """ Invoke the test shell with the given set of arguments, and 70 | return any output generated. 71 | """ 72 | 73 | global status 74 | 75 | # This sometime fails under Cygwin, so try again when that happens 76 | try: 77 | stdout = open("__stdout", "w") 78 | except: 79 | time.sleep(0.01) 80 | stdout = open("__stdout", "w") 81 | 82 | fullArgs = [] 83 | fullArgs.append(shell) 84 | fullArgs.extend(args) 85 | status = subprocess.call(fullArgs, stdout=stdout, 86 | stderr=subprocess.STDOUT) 87 | result = readFile("__stdout") 88 | stdout.close() 89 | 90 | # This sometime fails under Cygwin, so try again when that happens 91 | try: 92 | os.remove("__stdout") 93 | except: 94 | time.sleep(0.01) 95 | os.remove("__stdout") 96 | 97 | return result 98 | 99 | class TestBuiltin(unittest.TestCase): 100 | def tearDown(self): 101 | if os.path.exists("__test"): 102 | shutil.rmtree("__test") 103 | 104 | def test_cd_no_args(self): 105 | self.assertEqual(run("cd ~; pwd"), run("cd; pwd")) 106 | 107 | def test_cd_no_HOME(self): 108 | self.assertIn("cd: HOME not set", run("unset HOME; cd")) 109 | 110 | def test_cd_HOME_bad_path(self): 111 | self.assertIn("__bogus/foo: No such file or directory", 112 | run("HOME=__bogus/foo; cd")) 113 | 114 | def test_cd_too_many_args(self): 115 | self.assertIn("cd: Too many arguments", run("cd a b")) 116 | 117 | def test_cd_bad_path(self): 118 | self.assertIn("__bogus/foo: No such file or directory", 119 | run("cd __bogus/foo")) 120 | 121 | def test_cd_success(self): 122 | os.makedirs("__test") 123 | writeFile("__test/foo", "abc def"); 124 | self.assertEqual("abc def", 125 | run("cd __test; /bin/cat foo")) 126 | 127 | def test_exit_no_args(self): 128 | self.assertEqual("", run("exit; echo foo")) 129 | self.assertEqual(0, status) 130 | 131 | def test_exit_with_arg(self): 132 | self.assertEqual("", run("exit 14; echo foo")) 133 | self.assertEqual(14, status) 134 | 135 | def test_exit_non_numeric_arg(self): 136 | self.assertIn("jkl: Numeric argument required", 137 | run("exit jkl; echo foo")) 138 | self.assertEqual(2, status) 139 | 140 | def test_exit_too_many_arguments(self): 141 | self.assertIn("exit: Too many arguments", run("exit 1 2 3; echo foo")) 142 | self.assertEqual(1, status) 143 | 144 | class TestExpand(unittest.TestCase): 145 | def tearDown(self): 146 | if os.path.exists("__test"): 147 | shutil.rmtree("__test") 148 | 149 | def test_expandPath_no_wildcards(self): 150 | os.makedirs("__test/foo") 151 | self.assertEqual("abc def\n", run("/bin/echo abc def")) 152 | self.assertEqual("__test/[a]*\n", run('/bin/echo __test/"[a]*"')) 153 | 154 | def test_expandPath_file_matched(self): 155 | os.makedirs("__test/foo") 156 | self.assertEqual("__test/foo\n", run('/bin/echo __test/[f]*')) 157 | 158 | def test_expandPath_no_file_matched(self): 159 | os.makedirs("__test/foo"); 160 | self.assertEqual("__test/x*\n", run('/bin/echo __test/x*')) 161 | 162 | def test_expandPath_multiple_files_matched(self): 163 | os.makedirs("__test/foo") 164 | writeFile("__test/foo/a.c") 165 | writeFile("__test/foo/b.c") 166 | writeFile("__test/foo/c.cc") 167 | result = run('/bin/echo __test/foo/*.c') 168 | self.assertIn("__test/foo/a.c", result) 169 | self.assertIn("__test/foo/b.c", result) 170 | 171 | def test_expandTilde(self): 172 | self.assertEqual("/home/ouster\n", 173 | run("PATH=/home/ouster; /bin/echo ~\n")) 174 | self.assertEqual("/home/ouster/xyz\n", run("/bin/echo ~/xyz\n")) 175 | self.assertEqual("/home/ouster\n", run("/bin/echo ~ouster\n")) 176 | self.assertEqual("/home/ouster/xyz\n", run("/bin/echo ~ouster/xyz\n")) 177 | self.assertEqual("~__bogus__/xyz\n", run("/bin/echo ~__bogus__/xyz\n")) 178 | 179 | def test_matchFiles_bad_directory(self): 180 | self.assertEqual("__test/bogus/*\n", run('/bin/echo __test/bogus/*')) 181 | 182 | def test_matchFiles_no_match_in_directory(self): 183 | os.makedirs("__test") 184 | self.assertEqual("__test/*.c\n", run('/bin/echo __test/*.c')) 185 | 186 | def test_matchFiles_repeated_separators(self): 187 | os.makedirs("__test/foo/bar") 188 | self.assertEqual("__test/foo\n", run('/bin/echo __t*//foo')) 189 | self.assertEqual("__test/foo/bar\n", run('/bin/echo __t*//f*//bar')) 190 | self.assertEqual("__test//foo/bar\n", run('/bin/echo __test//f*//bar')) 191 | 192 | def test_matchFiles_multiple_levels_of_matching(self): 193 | os.makedirs("__test/x1") 194 | os.makedirs("__test/x2") 195 | writeFile("__test/x1/a.c") 196 | writeFile("__test/x2/b.c") 197 | writeFile("__test/x2/c.c") 198 | result = run('/bin/echo __test/x?/*.c') 199 | self.assertIn("__test/x1/a.c", result) 200 | self.assertIn("__test/x2/b.c", result) 201 | self.assertIn("__test/x2/c.c", result) 202 | 203 | def test_matchString_fail_end_of_string(self): 204 | os.makedirs("__test") 205 | writeFile("__test/xyz") 206 | self.assertEqual("__tes?/xyzq\n", run('/bin/echo __tes?/xyzq')) 207 | 208 | def test_matchString_question_mark(self): 209 | os.makedirs("__test") 210 | writeFile("__test/xyz") 211 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x?z')) 212 | self.assertEqual("__tes?/x?z\n", run('/bin/echo __tes?/x\?z')) 213 | 214 | def test_matchString_asterisk(self): 215 | os.makedirs("__test") 216 | writeFile("__test/xyz") 217 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/*z')) 218 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x*z')) 219 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x*')) 220 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x****yz')) 221 | self.assertEqual("__tes?/x*z\n", run('/bin/echo __tes?/x\*z')) 222 | 223 | def test_matchString_brackets(self): 224 | os.makedirs("__test") 225 | writeFile("__test/xyz") 226 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x[ayql]z')) 227 | self.assertEqual("__tes?/x[abc]z\n", run('/bin/echo __tes?/x[abc]z')) 228 | self.assertEqual("__tes?/x[y]z\n", run('/bin/echo __tes?/x\[y]z')) 229 | 230 | def test_matchString_character_mismatch(self): 231 | os.makedirs("__test") 232 | writeFile("__test/xyz") 233 | self.assertEqual("__tes?/xa*\n", run('/bin/echo __tes?/xa*')) 234 | 235 | def test_matchString_pattern_ends_before_string(self): 236 | os.makedirs("__test") 237 | writeFile("__test/xyz") 238 | self.assertEqual("__tes?/xy\n", run('/bin/echo __tes?/xy')) 239 | 240 | def test_matchBrackets(self): 241 | os.makedirs("__test") 242 | writeFile("__test/testFile") 243 | writeFile("__test/te[st") 244 | self.assertEqual("__test/testFile\n", 245 | run('/bin/echo __test/te[qrstu]tFile')) 246 | self.assertEqual("__test/testFile\n", 247 | run('/bin/echo __test/te[s-u]tFile')) 248 | self.assertEqual("__test/testFile\n", 249 | run('/bin/echo __test/te[c-u]tFile')) 250 | self.assertEqual("__test/testFile\n", 251 | run('/bin/echo __test/te[c-s]tFile')) 252 | self.assertEqual("__test/testFile\n", 253 | run('/bin/echo __test/te[xa-el-u]tFile')) 254 | self.assertEqual("__test/te[^q-u]tFile\n", 255 | run('/bin/echo __test/te[^q-u]tFile')) 256 | self.assertEqual("__test/testFile\n", 257 | run('/bin/echo __test/te[^q-r]tFile')) 258 | self.assertEqual("__test/te[st\n", 259 | run('/bin/echo __test/te[[]*')) 260 | 261 | class TestMain(unittest.TestCase): 262 | def tearDown(self): 263 | if os.path.exists("__test"): 264 | shutil.rmtree("__test") 265 | 266 | def test_loadArgs(self): 267 | os.makedirs("__test"); 268 | writeFile("__test/script", "/bin/echo $0 $1 $2 '|' $# '|' $*") 269 | self.assertEqual("__test/script a b | 3 | a b c\n", 270 | runWithArgs("__test/script", "a", "b", "c")) 271 | self.assertEqual("0 | |\n", 272 | run("/bin/echo $# '|' $0 $1 '|' $*")) 273 | 274 | def test_main_c_option_basics(self): 275 | self.assertEqual("foo bar\n", runWithArgs("-c", "/bin/echo foo bar")) 276 | 277 | def test_main_c_option_missing_arg(self): 278 | self.assertIn("option requires an argument", runWithArgs("-c")) 279 | self.assertEqual(2, status) 280 | 281 | def test_main_c_option_exit_code(self): 282 | self.assertEqual("", runWithArgs("-c", "exit 44")) 283 | self.assertEqual(44, status) 284 | 285 | def test_main_script_file_basics(self): 286 | os.makedirs("__test"); 287 | writeFile("__test/script", "/bin/echo 'foo\nbar'\n/bin/echo second command") 288 | self.assertEqual("foo\nbar\nsecond command\n", 289 | runWithArgs("__test/script")) 290 | 291 | def test_main_script_file_nonexistent_file(self): 292 | self.assertIn("_bogus_/xyzzy: No such file or directory\n", 293 | runWithArgs("_bogus_/xyzzy")) 294 | self.assertEqual(127, status) 295 | 296 | def test_main_script_file_exit_status(self): 297 | os.makedirs("__test"); 298 | writeFile("__test/script", "/bin/echo command output\nexit 32\n \n") 299 | self.assertEqual("command output\n", runWithArgs("__test/script")) 300 | self.assertEqual(32, status) 301 | 302 | def test_main_script_file_command_line_args(self): 303 | os.makedirs("__test"); 304 | writeFile("__test/script", "/bin/echo $# '|' $0 $1 '|' $*\n") 305 | self.assertEqual("4 | __test/script a b | a b c d e\n", 306 | runWithArgs("__test/script", "a b", "c", "d", "e")) 307 | self.assertEqual(0, status) 308 | 309 | class TestParser(unittest.TestCase): 310 | def tearDown(self): 311 | if os.path.exists("__test"): 312 | shutil.rmtree("__test") 313 | 314 | def test_eval_basics(self): 315 | self.assertEqual("foo abc $y\n", run("x=foo; /bin/echo $x abc '$y'")) 316 | 317 | def test_eval_input_file_subs(self): 318 | os.makedirs("__test") 319 | writeFile("__test/foo", "abcde"); 320 | self.assertEqual("abcde", run("x=foo; /bin/cat <__test/$x")) 321 | 322 | def test_eval_output_file_subs(self): 323 | os.makedirs("__test") 324 | self.assertEqual("", run("x=foo; /bin/echo foo bar >__test/$x")) 325 | self.assertEqual("foo bar\n", readFile("__test/foo")) 326 | 327 | def test_eval_errors(self): 328 | self.assertIn("unexpected EOF while looking for matching `''", 329 | run("/bin/echo 'a b c")) 330 | self.assertIn("${}: bad substitution", run("/bin/echo a b ${}")) 331 | output = run("/bin/echo start; echo ${}") 332 | self.assertIn("${}: bad substitution", output) 333 | self.assertIn("start\n", output) 334 | 335 | def test_doSubs_tildes_first(self): 336 | home = run("/bin/echo ~")[:-1] 337 | self.assertNotIn("~", home) 338 | self.assertEqual("%s ~ ~ ~ ~\n"% (home), 339 | run("x='~ ~'; /bin/echo ~ $x \"~\" '~'")) 340 | 341 | def test_doSubs_variables(self): 342 | self.assertEqual("Word 1: foo\nWord 2: a\n" 343 | + "Word 3: b\nWord 4: a b\n", 344 | run("x=foo; y='a b'; ./words.py $x $y \"$y\"")) 345 | self.assertEqual("a\nb\n", run("x='a\nb'; /bin/echo \"$x\"")) 346 | self.assertEqual("Word 1: a\nWord 2: b\nWord 3: c\n", 347 | run("x='a b c '; ./words.py $x")) 348 | self.assertEqual("", run("x=''; ./words.py $x $x")) 349 | self.assertEqual("Word 1: .\nWord 2: a\nWord 3: b\nWord 4: .\n", 350 | run("x=' a b '; ./words.py .$x.")) 351 | 352 | def test_doSubs_commands(self): 353 | self.assertEqual("abc\n", run("x=abc; /bin/echo `/bin/echo $x`")) 354 | self.assertEqual("$abc\n", run("x='$abc'; /bin/echo `/bin/echo $x`")) 355 | 356 | def test_doSubs_backslashes(self): 357 | self.assertEqual("$x \"$x\n", run("x=abc; /bin/echo \$x \"\\\"\\$x\"")) 358 | 359 | def test_doSubs_double_quotes(self): 360 | self.assertEqual("Word 1: a x y z b\n", 361 | run('./words.py "a `/bin/echo x y z` b"')) 362 | self.assertEqual("Word 1: \nWord 2: \nWord 3: \n", 363 | run("x=\"\"; ./words.py $x\"\" \"\" \"$x\"")) 364 | 365 | def test_doSubs_single_quotes(self): 366 | self.assertEqual("Word 1: a $x `echo foo`\n", 367 | run("x=abc; ./words.py 'a $x `echo foo`'")) 368 | self.assertEqual("Word 1: \nWord 2: \nWord 3: \n", 369 | run("x=''; ./words.py $x'' ''$x ''")) 370 | 371 | def test_doSubs_path_expansion(self): 372 | os.makedirs("__test") 373 | writeFile("__test/a.c") 374 | writeFile("__test/b.c") 375 | result = run("x='*.c'; /bin/echo __test/*.c") 376 | self.assertIn("__test/a.c", result) 377 | self.assertIn("__test/b.c", result) 378 | result = run("x='*.c'; /bin/echo __test/$x") 379 | self.assertIn("__test/a.c", result) 380 | self.assertIn("__test/b.c", result) 381 | self.assertEqual("__test/*.c\n", 382 | run("x='*.c'; /bin/echo \"__test/*.c\"")) 383 | 384 | def test_parse_leading_separators(self): 385 | self.assertEqual("a b c\n", run("/bin/echo a b c ")) 386 | 387 | def test_parse_single_quotes(self): 388 | self.assertEqual("Word 1: xyz \\\nWord 2: abc\n", 389 | run("./words.py 'xyz \\' abc")) 390 | 391 | def test_parse_double_quotes(self): 392 | self.assertEqual("Word 1: a b c\n", 393 | run("./words.py \"a b c\"")) 394 | self.assertEqual("Word 1: a b\n", 395 | run("x='a b'; ./words.py \"$x\"")) 396 | self.assertEqual("Word 1: foo bar\n", 397 | run("./words.py \"`/bin/echo foo bar`\"")) 398 | self.assertEqual("Word 1: a\\b$x\n", 399 | run("x=abc; ./words.py \"a\\b\$x\"")) 400 | self.assertEqual("\"\n", run("/bin/echo '\"'")) 401 | 402 | def test_parse_variables(self): 403 | self.assertEqual("abcyyy\n", run("xxx=abc; /bin/echo ${xxx}yyy")) 404 | self.assertIn("unexpected EOF while looking for `}'", 405 | run("/bin/echo ${xxx")) 406 | self.assertEqual("abc.z\n", run("x0yz4=abc; /bin/echo $x0yz4.z")) 407 | self.assertEqual("a55b\n", run("/bin/bash -c 'exit 55'; /bin/echo a$?b")) 408 | self.assertIn("${}: bad substitution\n", run("/bin/echo ${}")) 409 | self.assertEqual("Word 1: a\nb\nc\n", 410 | run("x='a\nb\nc'; ./words.py \"$x\"")) 411 | self.assertEqual("$ $x $\n", run("x0yz4=abc; /bin/echo $ \"$\"x $")) 412 | self.assertEqual("arg12xy\n", 413 | runWithArgs("-c", "echo $12xy", "arg0", "arg1", "arg2")) 414 | 415 | def test_parse_commands_basics(self): 416 | self.assertEqual("Word 1: $y\n", 417 | run("x='$y'; y=abc; /bin/echo `./words.py $x`")) 418 | self.assertEqual("Word 1: a b c Word 2: x y\n", 419 | run("/bin/echo `./words.py \"a b c\" 'x y'`")) 420 | self.assertEqual("`\n", run("/bin/echo '`'")) 421 | 422 | def test_parse_commands_no_word_breaks(self): 423 | self.assertEqual("Word 1: a b c\n", 424 | run("./words.py \"`/bin/echo a b c`\"")) 425 | 426 | def test_parse_commands_errors(self): 427 | self.assertIn("Unexpected EOF while looking for matching ``'", 428 | run("/bin/echo `foo bar")) 429 | 430 | def test_parse_backslashes(self): 431 | self.assertEqual("aa$x`echo foo`\n", 432 | run("x=99; /bin/echo a\\a\\$x\\`echo foo\\`")) 433 | self.assertIn("Unexpected EOF while parsing backslash", 434 | run("/bin/echo \\")) 435 | self.assertIn("Unexpected EOF after backslash-newline", 436 | run("/bin/echo \\\n")) 437 | self.assertEqual("Word 1: axyz\n", run("./words.py a\\\nxyz")) 438 | 439 | def test_parse_backslashes_in_quotes(self): 440 | self.assertEqual("a$x`b\"c\\d\ne\\a\n", 441 | run("x=99; /bin/echo \"a\\$x\\`b\\\"c\\\\d\\\ne\\a\"")) 442 | 443 | def test_parse_backslashes_meaningless(self): 444 | self.assertEqual("a\\b\n", run("/bin/echo 'a\\b'")) 445 | 446 | def test_split_basics(self): 447 | os.makedirs("__test") 448 | self.assertEqual("abc def\n", run("/bin/echo abc def")) 449 | self.assertEqual("", run("/bin/echo abc def > __test/foo")) 450 | self.assertEqual("", run("/bin/echo abc def>__test/foo")) 451 | self.assertEqual("abc def\n", readFile("__test/foo")) 452 | self.assertEqual("abc def\n", run("/bin/cat < __test/foo")) 453 | self.assertEqual("abc def\n", run("/bin/cat<__test/foo")) 454 | 455 | def test_split_empty_first_word(self): 456 | os.makedirs("__test") 457 | self.assertEqual("", run("> __test/foo /bin/echo abc")) 458 | self.assertEqual("abc\n", readFile("__test/foo")) 459 | 460 | def test_split_missing_input_file(self): 461 | os.makedirs("__test") 462 | self.assertEqual("-clash: no file given for input redirection\n", 463 | run("/bin/echo abc < >__test/foo")) 464 | 465 | def test_split_missing_output_file(self): 466 | os.makedirs("__test") 467 | self.assertEqual("-clash: no file given for output redirection\n", 468 | run("/bin/echo abc >;")) 469 | 470 | def test_split_pipeline(self): 471 | os.makedirs("__test") 472 | self.assertEqual("abc def\n", run("/bin/echo abc def | /bin/cat")) 473 | self.assertEqual("abc def\n", run("/bin/echo abc def|/bin/cat")) 474 | self.assertEqual(" 1\t 1\tabc def\n", 475 | run("/bin/echo abc def | /bin/cat -n | /bin/cat -n")) 476 | 477 | def test_split_multiple_pipelines(self): 478 | os.makedirs("__test") 479 | self.assertEqual("xyz\n", 480 | run("/bin/echo abc>__test/out1;/bin/echo def > __test/out2;" 481 | + "/bin/echo xyz")) 482 | self.assertEqual("abc\n", readFile("__test/out1")) 483 | self.assertEqual("def\n", readFile("__test/out2")) 484 | self.assertEqual("xyz\n", 485 | run("/bin/echo abc>__test/out1;/bin/echo def > __test/out2 ;" 486 | + "/bin/echo xyz")) 487 | self.assertEqual("abc\n", readFile("__test/out1")) 488 | self.assertEqual("def\n", readFile("__test/out2")) 489 | 490 | def test_breakAndAppend(self): 491 | self.assertEqual("Word 1: .abc\nWord 2: def\nWord 3: x\nWord 4: y.\n", 492 | run("x='abc def\tx\ny'; ./words.py .$x.")) 493 | self.assertEqual("Word 1: .\nWord 2: a\nWord 3: b\nWord 4: .\n", 494 | run("x=' \t\n a b \t\n'; ./words.py .$x.")) 495 | 496 | class TestPipeline(unittest.TestCase): 497 | # Much of the code in this class was already tested by other 498 | # tests, such as those for Parser. 499 | def tearDown(self): 500 | if os.path.exists("__test"): 501 | shutil.rmtree("__test") 502 | 503 | def test_rebuildPathMap_basics(self): 504 | os.makedirs("__test") 505 | os.makedirs("__test/child") 506 | writeFile("__test/a", "#!/bin/sh\n/bin/echo __test/a") 507 | os.chmod("__test/a", 0o777) 508 | writeFile("__test/b", "#!/bin/sh\n/bin/echo __test/b") 509 | os.chmod("__test/b", 0o777) 510 | writeFile("__test/child/a", "#!/bin/sh\n/bin/echo __test/child/a") 511 | os.chmod("__test/child/a", 0o777) 512 | writeFile("__test/child/b", "#!/bin/sh\n/bin/echo __test/child/b") 513 | self.assertEqual("__test/child/a\n__test/b\n", 514 | run("PATH=\"`/bin/pwd`/__test/child:`/bin/pwd`/__test\"; a; b")) 515 | 516 | def test_rebuildPathMap_default(self): 517 | self.assertEqual("a b\n", run("unset PATH; echo a b")) 518 | 519 | def test_run_redirection_basics(self): 520 | os.makedirs("__test") 521 | self.assertEqual("a b c\n", 522 | run("/bin/echo a b c > __test/foo; /bin/cat __test/foo")) 523 | self.assertEqual("a b c\n", run("/bin/cat < __test/foo")) 524 | 525 | def test_run_ambiguous_input_redirection(self): 526 | self.assertIn("Ambiguous input redirection", 527 | run("x='a b'; /bin/cat <$x")) 528 | 529 | def test_run_bad_input_file(self): 530 | os.makedirs("__test") 531 | self.assertIn("No such file or directory", 532 | run("cat < __test/bogus")) 533 | 534 | def test_run_ambiguous_output_redirection(self): 535 | self.assertIn("Ambiguous output redirection", 536 | run("x='a b'; /bin/echo foo bar >$x")) 537 | 538 | def test_run_pipeline(self): 539 | self.assertIn("x y z\n", run("/bin/echo x y z | cat")) 540 | 541 | def test_run_bad_output_file(self): 542 | os.makedirs("__test") 543 | self.assertIn("No such file or directory", 544 | run("/bin/echo abc > __test/_bogus/xyz")) 545 | 546 | def test_run_rebuild_path_cache_to_discover_new_file(self): 547 | os.makedirs("__test") 548 | writeFile("__test/x", "#!/bin/sh\n/bin/echo __test/x") 549 | self.assertIn(" x: command not found\n__test/x", 550 | run("PATH=\"/bin:`pwd`/__test\"; x; chmod +x __test/x; x")) 551 | 552 | def test_run_rebuild_path_cache_path_changed(self): 553 | os.makedirs("__test") 554 | writeFile("__test/x", "#!/bin/sh\n/bin/echo __test/x") 555 | os.chmod("__test/x", 0o777) 556 | self.assertIn(" x: command not found\n__test/x", 557 | run("x; PATH=\"/bin:`pwd`/__test\"; x")) 558 | 559 | def test_run_no_such_executable(self): 560 | os.makedirs("__test") 561 | self.assertIn("No such file or directory", run("__test/bogus foo bar")) 562 | self.assertIn("No such file or directory", 563 | run("__test/bogus foo bar | /bin/echo foo")) 564 | self.assertNotIn("No such file or directory", 565 | run("__test/bogus foo bar |& /bin/echo foo")) 566 | self.assertEqual("", 567 | run("__test/bogus foo bar |& /bin/cat > __test/out")) 568 | self.assertIn("No such file or directory", 569 | readFile("__test/out")) 570 | 571 | def test_run_set_status_variable(self): 572 | os.makedirs("__test") 573 | self.assertEqual("0\n", run("/bin/true; /bin/echo $?")) 574 | self.assertEqual("44\n", run("/bin/bash -c \"exit 44\"; /bin/echo $?")) 575 | 576 | class TestVariables(unittest.TestCase): 577 | def test_set(self): 578 | self.assertEqual("50 100\n", run("x=99; y=100; x=50; /bin/echo $x $y")) 579 | self.assertIn("x=100", 580 | run("x=99; export x; /bin/echo foo; x=100; /usr/bin/printenv")) 581 | 582 | def test_setFromEnviron(self): 583 | self.assertNotEqual("\n", run("/bin/echo $HOME")) 584 | self.assertNotEqual("\n", run("/bin/echo $SHELL")) 585 | self.assertIn("SHELL=xyzzy", run("SHELL=xyzzy; /usr/bin/printenv")) 586 | 587 | def test_set(self): 588 | self.assertEqual(".99. ..\n", run("x=99; /bin/echo .$x. .$y.")) 589 | 590 | def test_getEnviron(self): 591 | result = run("x=99; export x; y=100; /usr/bin/printenv") 592 | self.assertIn("x=99", result) 593 | self.assertNotIn("y=", result) 594 | self.assertIn("HOME=", result) 595 | self.assertIn("SHELL=", result) 596 | 597 | def test_unset(self): 598 | self.assertEqual("~99~\n~~\n", 599 | run("x=99; /bin/echo ~$x~; unset x; /bin/echo ~$x~")) 600 | result = run("x=99; export x; echo x:$x; unset x; /usr/bin/printenv") 601 | self.assertIn("x:99", result); 602 | self.assertNotIn("x=99", result); 603 | 604 | def test_markExported(self): 605 | self.assertNotIn("x=99", run("x=99; /usr/bin/printenv")) 606 | self.assertIn("x=99", run("x=99; /bin/echo foo bar; export x; " 607 | "/usr/bin/printenv")) 608 | 609 | if __name__ == '__main__': 610 | unittest.main() 611 | -------------------------------------------------------------------------------- /test-fixed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # A test suite for clash. Designed as unit tests for John Ousterhout's 4 | # implementation, but implemented at a high enough level that it can be 5 | # applied to other implementations as well. 6 | 7 | import os 8 | import shutil 9 | import subprocess 10 | import sys 11 | import time 12 | import unittest 13 | 14 | # The location of the shell to be tested. You can change this to 15 | # /bin/bash to see if Bash passes this test suite. 16 | shell = "./clash" 17 | # shell = "/bin/bash" 18 | 19 | # This variable will hold the exit status of the most recent command 20 | # executed by "run" 21 | status = 0 22 | 23 | def readFile(name): 24 | """ Return the contents of a file. """ 25 | f = open(name, "r") 26 | result = f.read() 27 | f.close() 28 | return result 29 | 30 | def writeFile(name, contents="xyzzy"): 31 | """ Create a file with particular contents. """ 32 | f = open(name, "w") 33 | f.write(contents) 34 | f.close() 35 | 36 | def run(cmd): 37 | """ Invoke the test shell with the given command and return any 38 | output generated. 39 | """ 40 | 41 | global status 42 | writeFile("__stdin", cmd) 43 | stdin = open("__stdin", "r") 44 | 45 | # This sometime fails under Cygwin, so try again when that happens 46 | try: 47 | stdout = open("__stdout", "w") 48 | except: 49 | time.sleep(0.01) 50 | stdout = open("__stdout", "w") 51 | 52 | status = subprocess.call(shell, stdin=stdin, stdout=stdout, 53 | stderr=subprocess.STDOUT) 54 | result = readFile("__stdout") 55 | stdin.close() 56 | stdout.close() 57 | os.remove("__stdin") 58 | 59 | # This sometime fails under Cygwin, so try again when that happens 60 | try: 61 | os.remove("__stdout") 62 | except: 63 | time.sleep(0.01) 64 | os.remove("__stdout") 65 | 66 | return result 67 | 68 | def runWithArgs(*args): 69 | """ Invoke the test shell with the given set of arguments, and 70 | return any output generated. 71 | """ 72 | 73 | global status 74 | 75 | # This sometime fails under Cygwin, so try again when that happens 76 | try: 77 | stdout = open("__stdout", "w") 78 | except: 79 | time.sleep(0.01) 80 | stdout = open("__stdout", "w") 81 | 82 | fullArgs = [] 83 | fullArgs.append(shell) 84 | fullArgs.extend(args) 85 | status = subprocess.call(fullArgs, stdout=stdout, 86 | stderr=subprocess.STDOUT) 87 | result = readFile("__stdout") 88 | stdout.close() 89 | 90 | # This sometime fails under Cygwin, so try again when that happens 91 | try: 92 | os.remove("__stdout") 93 | except: 94 | time.sleep(0.01) 95 | os.remove("__stdout") 96 | 97 | return result 98 | 99 | class TestBuiltin(unittest.TestCase): 100 | def tearDown(self): 101 | if os.path.exists("__test"): 102 | shutil.rmtree("__test") 103 | 104 | def test_cd_no_args(self): 105 | self.assertEqual(run("cd ~; pwd"), run("cd; pwd")) 106 | 107 | def test_cd_no_HOME(self): 108 | self.assertIn("cd: HOME not set", run("unset HOME; cd")) 109 | 110 | def test_cd_HOME_bad_path(self): 111 | self.assertIn("__bogus/foo: No such file or directory", 112 | run("HOME=__bogus/foo; cd")) 113 | 114 | def test_cd_too_many_args(self): 115 | self.assertIn("cd: Too many arguments", run("cd a b")) 116 | 117 | def test_cd_bad_path(self): 118 | self.assertIn("__bogus/foo: No such file or directory", 119 | run("cd __bogus/foo")) 120 | 121 | def test_cd_success(self): 122 | os.makedirs("__test") 123 | writeFile("__test/foo", "abc def"); 124 | self.assertEqual("abc def", 125 | run("cd __test; /bin/cat foo")) 126 | 127 | def test_exit_no_args(self): 128 | self.assertEqual("", run("exit; echo foo")) 129 | self.assertEqual(0, status) 130 | 131 | def test_exit_with_arg(self): 132 | self.assertEqual("", run("exit 14; echo foo")) 133 | self.assertEqual(14, status) 134 | 135 | def test_exit_non_numeric_arg(self): 136 | self.assertIn("jkl: Numeric argument required", 137 | run("exit jkl; echo foo")) 138 | self.assertEqual(2, status) 139 | 140 | def test_exit_too_many_arguments(self): 141 | self.assertIn("exit: Too many arguments", run("exit 1 2 3; echo foo")) 142 | self.assertEqual(1, status) 143 | 144 | class TestExpand(unittest.TestCase): 145 | def tearDown(self): 146 | if os.path.exists("__test"): 147 | shutil.rmtree("__test") 148 | 149 | def test_expandPath_no_wildcards(self): 150 | os.makedirs("__test/foo") 151 | self.assertEqual("abc def\n", run("/bin/echo abc def")) 152 | self.assertEqual("__test/[a]*\n", run('/bin/echo __test/"[a]*"')) 153 | 154 | def test_expandPath_file_matched(self): 155 | os.makedirs("__test/foo") 156 | self.assertEqual("__test/foo\n", run('/bin/echo __test/[f]*')) 157 | 158 | def test_expandPath_no_file_matched(self): 159 | os.makedirs("__test/foo"); 160 | self.assertEqual("__test/x*\n", run('/bin/echo __test/x*')) 161 | 162 | def test_expandPath_multiple_files_matched(self): 163 | os.makedirs("__test/foo") 164 | writeFile("__test/foo/a.c") 165 | writeFile("__test/foo/b.c") 166 | writeFile("__test/foo/c.cc") 167 | result = run('/bin/echo __test/foo/*.c') 168 | self.assertIn("__test/foo/a.c", result) 169 | self.assertIn("__test/foo/b.c", result) 170 | 171 | def test_expandTilde(self): 172 | self.assertEqual("/home/ouster\n", 173 | run("HOME=/home/ouster; /bin/echo ~\n")) 174 | self.assertEqual("/home/ouster/xyz\n", run("HOME=/home/ouster; /bin/echo ~/xyz\n")) 175 | self.assertEqual("/var/root\n", run("/bin/echo ~root\n")) 176 | self.assertEqual("/var/root/xyz\n", run("/bin/echo ~root/xyz\n")) 177 | self.assertEqual("~__bogus__/xyz\n", run("HOME=/home/ouster; /bin/echo ~__bogus__/xyz\n")) 178 | 179 | def test_matchFiles_bad_directory(self): 180 | self.assertEqual("__test/bogus/*\n", run('/bin/echo __test/bogus/*')) 181 | 182 | def test_matchFiles_no_match_in_directory(self): 183 | os.makedirs("__test") 184 | self.assertEqual("__test/*.c\n", run('/bin/echo __test/*.c')) 185 | 186 | def test_matchFiles_repeated_separators(self): 187 | os.makedirs("__test/foo/bar") 188 | self.assertEqual("__test/foo\n", run('/bin/echo __t*//foo')) 189 | self.assertEqual("__test/foo/bar\n", run('/bin/echo __t*//f*//bar')) 190 | self.assertEqual("__test//foo/bar\n", run('/bin/echo __test//f*//bar')) 191 | 192 | def test_matchFiles_multiple_levels_of_matching(self): 193 | os.makedirs("__test/x1") 194 | os.makedirs("__test/x2") 195 | writeFile("__test/x1/a.c") 196 | writeFile("__test/x2/b.c") 197 | writeFile("__test/x2/c.c") 198 | result = run('/bin/echo __test/x?/*.c') 199 | self.assertIn("__test/x1/a.c", result) 200 | self.assertIn("__test/x2/b.c", result) 201 | self.assertIn("__test/x2/c.c", result) 202 | 203 | def test_matchString_fail_end_of_string(self): 204 | os.makedirs("__test") 205 | writeFile("__test/xyz") 206 | self.assertEqual("__tes?/xyzq\n", run('/bin/echo __tes?/xyzq')) 207 | 208 | def test_matchString_question_mark(self): 209 | os.makedirs("__test") 210 | writeFile("__test/xyz") 211 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x?z')) 212 | self.assertEqual("__tes?/x?z\n", run('/bin/echo __tes?/x\?z')) 213 | 214 | def test_matchString_asterisk(self): 215 | os.makedirs("__test") 216 | writeFile("__test/xyz") 217 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/*z')) 218 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x*z')) 219 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x*')) 220 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x****yz')) 221 | self.assertEqual("__tes?/x*z\n", run('/bin/echo __tes?/x\*z')) 222 | 223 | def test_matchString_brackets(self): 224 | os.makedirs("__test") 225 | writeFile("__test/xyz") 226 | self.assertEqual("__test/xyz\n", run('/bin/echo __tes?/x[ayql]z')) 227 | self.assertEqual("__tes?/x[abc]z\n", run('/bin/echo __tes?/x[abc]z')) 228 | self.assertEqual("__tes?/x[y]z\n", run('/bin/echo __tes?/x\[y]z')) 229 | 230 | def test_matchString_character_mismatch(self): 231 | os.makedirs("__test") 232 | writeFile("__test/xyz") 233 | self.assertEqual("__tes?/xa*\n", run('/bin/echo __tes?/xa*')) 234 | 235 | def test_matchString_pattern_ends_before_string(self): 236 | os.makedirs("__test") 237 | writeFile("__test/xyz") 238 | self.assertEqual("__tes?/xy\n", run('/bin/echo __tes?/xy')) 239 | 240 | def test_matchBrackets(self): 241 | os.makedirs("__test") 242 | writeFile("__test/testFile") 243 | writeFile("__test/te[st") 244 | self.assertEqual("__test/testFile\n", 245 | run('/bin/echo __test/te[qrstu]tFile')) 246 | self.assertEqual("__test/testFile\n", 247 | run('/bin/echo __test/te[s-u]tFile')) 248 | self.assertEqual("__test/testFile\n", 249 | run('/bin/echo __test/te[c-u]tFile')) 250 | self.assertEqual("__test/testFile\n", 251 | run('/bin/echo __test/te[c-s]tFile')) 252 | self.assertEqual("__test/testFile\n", 253 | run('/bin/echo __test/te[xa-el-u]tFile')) 254 | self.assertEqual("__test/te[^q-u]tFile\n", 255 | run('/bin/echo __test/te[^q-u]tFile')) 256 | self.assertEqual("__test/testFile\n", 257 | run('/bin/echo __test/te[^q-r]tFile')) 258 | self.assertEqual("__test/te[st\n", 259 | run('/bin/echo __test/te[[]*')) 260 | 261 | class TestMain(unittest.TestCase): 262 | def tearDown(self): 263 | if os.path.exists("__test"): 264 | shutil.rmtree("__test") 265 | 266 | def test_loadArgs(self): 267 | os.makedirs("__test"); 268 | writeFile("__test/script", "/bin/echo $0 $1 $2 '|' $# '|' $*") 269 | self.assertEqual("__test/script a b | 3 | a b c\n", 270 | runWithArgs("__test/script", "a", "b", "c")) 271 | self.assertEqual("0 | " + shell + " |\n", 272 | run("/bin/echo $# '|' $0 $1 '|' $*")) 273 | 274 | def test_main_c_option_basics(self): 275 | self.assertEqual("foo bar\n", runWithArgs("-c", "/bin/echo foo bar")) 276 | 277 | def test_main_c_option_missing_arg(self): 278 | self.assertIn("option requires an argument", runWithArgs("-c")) 279 | self.assertEqual(2, status) 280 | 281 | def test_main_c_option_exit_code(self): 282 | self.assertEqual("", runWithArgs("-c", "exit 44")) 283 | self.assertEqual(44, status) 284 | 285 | def test_main_script_file_basics(self): 286 | os.makedirs("__test"); 287 | writeFile("__test/script", "/bin/echo 'foo\nbar'\n/bin/echo second command") 288 | self.assertEqual("foo\nbar\nsecond command\n", 289 | runWithArgs("__test/script")) 290 | 291 | def test_main_script_file_nonexistent_file(self): 292 | self.assertIn("_bogus_/xyzzy: No such file or directory\n", 293 | runWithArgs("_bogus_/xyzzy")) 294 | self.assertEqual(127, status) 295 | 296 | def test_main_script_file_exit_status(self): 297 | os.makedirs("__test"); 298 | writeFile("__test/script", "/bin/echo command output\nexit 32\n \n") 299 | self.assertEqual("command output\n", runWithArgs("__test/script")) 300 | self.assertEqual(32, status) 301 | 302 | def test_main_script_file_command_line_args(self): 303 | os.makedirs("__test"); 304 | writeFile("__test/script", "/bin/echo $# '|' $0 $1 '|' $*\n") 305 | self.assertEqual("4 | __test/script a b | a b c d e\n", 306 | runWithArgs("__test/script", "a b", "c", "d", "e")) 307 | self.assertEqual(0, status) 308 | 309 | class TestParser(unittest.TestCase): 310 | def tearDown(self): 311 | if os.path.exists("__test"): 312 | shutil.rmtree("__test") 313 | 314 | def test_eval_basics(self): 315 | self.assertEqual("foo abc $y\n", run("x=foo; /bin/echo $x abc '$y'")) 316 | 317 | def test_eval_input_file_subs(self): 318 | os.makedirs("__test") 319 | writeFile("__test/foo", "abcde"); 320 | self.assertEqual("abcde", run("x=foo; /bin/cat <__test/$x")) 321 | 322 | def test_eval_output_file_subs(self): 323 | os.makedirs("__test") 324 | self.assertEqual("", run("x=foo; /bin/echo foo bar >__test/$x")) 325 | self.assertEqual("foo bar\n", readFile("__test/foo")) 326 | 327 | def test_eval_errors(self): 328 | self.assertIn("unexpected EOF while looking for matching `''", 329 | run("/bin/echo 'a b c")) 330 | self.assertIn("${}: bad substitution", run("/bin/echo a b ${}")) 331 | output = run("/bin/echo start; echo ${}") 332 | self.assertIn("${}: bad substitution", output) 333 | self.assertIn("start\n", output) 334 | 335 | def test_doSubs_tildes_first(self): 336 | home = run("/bin/echo ~")[:-1] 337 | self.assertNotIn("~", home) 338 | self.assertEqual("%s ~ ~ ~ ~\n"% (home), 339 | run("x='~ ~'; /bin/echo ~ $x \"~\" '~'")) 340 | 341 | def test_doSubs_variables(self): 342 | self.assertEqual("Word 1: foo\nWord 2: a\n" 343 | + "Word 3: b\nWord 4: a b\n", 344 | run("x=foo; y='a b'; ./words.py $x $y \"$y\"")) 345 | self.assertEqual("a\nb\n", run("x='a\nb'; /bin/echo \"$x\"")) 346 | self.assertEqual("Word 1: a\nWord 2: b\nWord 3: c\n", 347 | run("x='a b c '; ./words.py $x")) 348 | self.assertEqual("", run("x=''; ./words.py $x $x")) 349 | self.assertEqual("Word 1: .\nWord 2: a\nWord 3: b\nWord 4: .\n", 350 | run("x=' a b '; ./words.py .$x.")) 351 | 352 | def test_doSubs_commands(self): 353 | self.assertEqual("abc\n", run("x=abc; /bin/echo `/bin/echo $x`")) 354 | self.assertEqual("$abc\n", run("x='$abc'; /bin/echo `/bin/echo $x`")) 355 | 356 | def test_doSubs_backslashes(self): 357 | self.assertEqual("$x \"$x\n", run("x=abc; /bin/echo \$x \"\\\"\\$x\"")) 358 | 359 | def test_doSubs_double_quotes(self): 360 | self.assertEqual("Word 1: a x y z b\n", 361 | run('./words.py "a `/bin/echo x y z` b"')) 362 | self.assertEqual("Word 1: \nWord 2: \nWord 3: \n", 363 | run("x=\"\"; ./words.py $x\"\" \"\" \"$x\"")) 364 | 365 | def test_doSubs_single_quotes(self): 366 | self.assertEqual("Word 1: a $x `echo foo`\n", 367 | run("x=abc; ./words.py 'a $x `echo foo`'")) 368 | self.assertEqual("Word 1: \nWord 2: \nWord 3: \n", 369 | run("x=''; ./words.py $x'' ''$x ''")) 370 | 371 | def test_doSubs_path_expansion(self): 372 | os.makedirs("__test") 373 | writeFile("__test/a.c") 374 | writeFile("__test/b.c") 375 | result = run("x='*.c'; /bin/echo __test/*.c") 376 | self.assertIn("__test/a.c", result) 377 | self.assertIn("__test/b.c", result) 378 | result = run("x='*.c'; /bin/echo __test/$x") 379 | self.assertIn("__test/a.c", result) 380 | self.assertIn("__test/b.c", result) 381 | self.assertEqual("__test/*.c\n", 382 | run("x='*.c'; /bin/echo \"__test/*.c\"")) 383 | 384 | def test_parse_leading_separators(self): 385 | self.assertEqual("a b c\n", run("/bin/echo a b c ")) 386 | 387 | def test_parse_single_quotes(self): 388 | self.assertEqual("Word 1: xyz \\\nWord 2: abc\n", 389 | run("./words.py 'xyz \\' abc")) 390 | 391 | def test_parse_double_quotes(self): 392 | self.assertEqual("Word 1: a b c\n", 393 | run("./words.py \"a b c\"")) 394 | self.assertEqual("Word 1: a b\n", 395 | run("x='a b'; ./words.py \"$x\"")) 396 | self.assertEqual("Word 1: foo bar\n", 397 | run("./words.py \"`/bin/echo foo bar`\"")) 398 | self.assertEqual("Word 1: a\\b$x\n", 399 | run("x=abc; ./words.py \"a\\b\$x\"")) 400 | self.assertEqual("\"\n", run("/bin/echo '\"'")) 401 | 402 | def test_parse_variables(self): 403 | self.assertEqual("abcyyy\n", run("xxx=abc; /bin/echo ${xxx}yyy")) 404 | self.assertIn("unexpected EOF while looking for `}'", 405 | run("/bin/echo ${xxx")) 406 | self.assertEqual("abc.z\n", run("x0yz4=abc; /bin/echo $x0yz4.z")) 407 | self.assertEqual("a55b\n", run("/bin/bash -c 'exit 55'; /bin/echo a$?b")) 408 | self.assertIn("${}: bad substitution\n", run("/bin/echo ${}")) 409 | self.assertEqual("Word 1: a\nb\nc\n", 410 | run("x='a\nb\nc'; ./words.py \"$x\"")) 411 | self.assertEqual("$ $x $\n", run("x0yz4=abc; /bin/echo $ \"$\"x $")) 412 | self.assertEqual("arg12xy\n", 413 | runWithArgs("-c", "echo $12xy", "arg0", "arg1", "arg2")) 414 | 415 | def test_parse_commands_basics(self): 416 | self.assertEqual("Word 1: $y\n", 417 | run("x='$y'; y=abc; /bin/echo `./words.py $x`")) 418 | self.assertEqual("Word 1: a b c Word 2: x y\n", 419 | run("/bin/echo `./words.py \"a b c\" 'x y'`")) 420 | self.assertEqual("`\n", run("/bin/echo '`'")) 421 | 422 | def test_parse_commands_no_word_breaks(self): 423 | self.assertEqual("Word 1: a b c\n", 424 | run("./words.py \"`/bin/echo a b c`\"")) 425 | 426 | def test_parse_commands_errors(self): 427 | self.assertIn("Unexpected EOF while looking for matching ``'", 428 | run("/bin/echo `foo bar")) 429 | 430 | def test_parse_backslashes(self): 431 | self.assertEqual("aa$x`echo foo`\n", 432 | run("x=99; /bin/echo a\\a\\$x\\`echo foo\\`")) 433 | self.assertIn("Unexpected EOF while parsing backslash", 434 | run("/bin/echo \\")) 435 | self.assertIn("Unexpected EOF after backslash-newline", 436 | run("/bin/echo \\\n")) 437 | self.assertEqual("Word 1: axyz\n", run("./words.py a\\\nxyz")) 438 | 439 | def test_parse_backslashes_in_quotes(self): 440 | self.assertEqual("a$x`b\"c\\d\ne\\a\n", 441 | run("x=99; /bin/echo \"a\\$x\\`b\\\"c\\\\d\\\ne\\a\"")) 442 | 443 | def test_parse_backslashes_meaningless(self): 444 | self.assertEqual("a\\b\n", run("/bin/echo 'a\\b'")) 445 | 446 | def test_split_basics(self): 447 | os.makedirs("__test") 448 | self.assertEqual("abc def\n", run("/bin/echo abc def")) 449 | self.assertEqual("", run("/bin/echo abc def > __test/foo")) 450 | self.assertEqual("", run("/bin/echo abc def>__test/foo")) 451 | self.assertEqual("abc def\n", readFile("__test/foo")) 452 | self.assertEqual("abc def\n", run("/bin/cat < __test/foo")) 453 | self.assertEqual("abc def\n", run("/bin/cat<__test/foo")) 454 | 455 | def test_split_empty_first_word(self): 456 | os.makedirs("__test") 457 | self.assertEqual("", run("> __test/foo /bin/echo abc")) 458 | self.assertEqual("abc\n", readFile("__test/foo")) 459 | 460 | def test_split_missing_input_file(self): 461 | os.makedirs("__test") 462 | self.assertEqual("-clash: no file given for input redirection\n", 463 | run("/bin/echo abc < >__test/foo")) 464 | 465 | def test_split_missing_output_file(self): 466 | os.makedirs("__test") 467 | self.assertEqual("-clash: no file given for output redirection\n", 468 | run("/bin/echo abc >;")) 469 | 470 | def test_split_pipeline(self): 471 | os.makedirs("__test") 472 | self.assertEqual("abc def\n", run("/bin/echo abc def | /bin/cat")) 473 | self.assertEqual("abc def\n", run("/bin/echo abc def|/bin/cat")) 474 | self.assertEqual(" 1\t 1\tabc def\n", 475 | run("/bin/echo abc def | /bin/cat -n | /bin/cat -n")) 476 | 477 | def test_split_multiple_pipelines(self): 478 | os.makedirs("__test") 479 | self.assertEqual("xyz\n", 480 | run("/bin/echo abc>__test/out1;/bin/echo def > __test/out2;" 481 | + "/bin/echo xyz")) 482 | self.assertEqual("abc\n", readFile("__test/out1")) 483 | self.assertEqual("def\n", readFile("__test/out2")) 484 | self.assertEqual("xyz\n", 485 | run("/bin/echo abc>__test/out1;/bin/echo def > __test/out2 ;" 486 | + "/bin/echo xyz")) 487 | self.assertEqual("abc\n", readFile("__test/out1")) 488 | self.assertEqual("def\n", readFile("__test/out2")) 489 | 490 | def test_breakAndAppend(self): 491 | self.assertEqual("Word 1: .abc\nWord 2: def\nWord 3: x\nWord 4: y.\n", 492 | run("x='abc def\tx\ny'; ./words.py .$x.")) 493 | self.assertEqual("Word 1: .\nWord 2: a\nWord 3: b\nWord 4: .\n", 494 | run("x=' \t\n a b \t\n'; ./words.py .$x.")) 495 | 496 | class TestPipeline(unittest.TestCase): 497 | # Much of the code in this class was already tested by other 498 | # tests, such as those for Parser. 499 | def tearDown(self): 500 | if os.path.exists("__test"): 501 | shutil.rmtree("__test") 502 | 503 | def test_rebuildPathMap_basics(self): 504 | os.makedirs("__test") 505 | os.makedirs("__test/child") 506 | writeFile("__test/a", "#!/bin/sh\n/bin/echo __test/a") 507 | os.chmod("__test/a", 0o777) 508 | writeFile("__test/b", "#!/bin/sh\n/bin/echo __test/b") 509 | os.chmod("__test/b", 0o777) 510 | writeFile("__test/child/a", "#!/bin/sh\n/bin/echo __test/child/a") 511 | os.chmod("__test/child/a", 0o777) 512 | writeFile("__test/child/b", "#!/bin/sh\n/bin/echo __test/child/b") 513 | self.assertEqual("__test/child/a\n__test/b\n", 514 | run("PATH=\"`/bin/pwd`/__test/child:`/bin/pwd`/__test\"; a; b")) 515 | 516 | def test_rebuildPathMap_default(self): 517 | self.assertEqual("a b\n", run("unset PATH; echo a b")) 518 | 519 | def test_run_redirection_basics(self): 520 | os.makedirs("__test") 521 | self.assertEqual("a b c\n", 522 | run("/bin/echo a b c > __test/foo; /bin/cat __test/foo")) 523 | self.assertEqual("a b c\n", run("/bin/cat < __test/foo")) 524 | 525 | def test_run_ambiguous_input_redirection(self): 526 | self.assertIn("Ambiguous input redirection", 527 | run("x='a b'; /bin/cat <$x")) 528 | 529 | def test_run_bad_input_file(self): 530 | os.makedirs("__test") 531 | self.assertIn("No such file or directory", 532 | run("cat < __test/bogus")) 533 | 534 | def test_run_ambiguous_output_redirection(self): 535 | self.assertIn("Ambiguous output redirection", 536 | run("x='a b'; /bin/echo foo bar >$x")) 537 | 538 | def test_run_pipeline(self): 539 | self.assertIn("x y z\n", run("/bin/echo x y z | cat")) 540 | 541 | def test_run_bad_output_file(self): 542 | os.makedirs("__test") 543 | self.assertIn("No such file or directory", 544 | run("/bin/echo abc > __test/_bogus/xyz")) 545 | 546 | def test_run_rebuild_path_cache_to_discover_new_file(self): 547 | os.makedirs("__test") 548 | writeFile("__test/x", "#!/bin/sh\n/bin/echo __test/x") 549 | self.assertIn(" x: command not found\n__test/x", 550 | run("PATH=\"/bin:`pwd`/__test\"; x; chmod +x __test/x; x")) 551 | 552 | def test_run_rebuild_path_cache_path_changed(self): 553 | os.makedirs("__test") 554 | writeFile("__test/x", "#!/bin/sh\n/bin/echo __test/x") 555 | os.chmod("__test/x", 0o777) 556 | self.assertIn(" x: command not found\n__test/x", 557 | run("x; PATH=\"/bin:`pwd`/__test\"; x")) 558 | 559 | def test_run_no_such_executable(self): 560 | os.makedirs("__test") 561 | self.assertIn("No such file or directory", run("__test/bogus foo bar")) 562 | self.assertIn("No such file or directory", 563 | run("__test/bogus foo bar | /bin/echo foo")) 564 | self.assertNotIn("No such file or directory", 565 | run("__test/bogus foo bar |& /bin/echo foo")) 566 | self.assertEqual("", 567 | run("__test/bogus foo bar |& /bin/cat > __test/out")) 568 | self.assertIn("No such file or directory", 569 | readFile("__test/out")) 570 | 571 | def test_run_set_status_variable(self): 572 | os.makedirs("__test") 573 | self.assertEqual("0\n", run("/usr/bin/true; /bin/echo $?")) 574 | self.assertEqual("44\n", run("/bin/bash -c \"exit 44\"; /bin/echo $?")) 575 | 576 | class TestVariables(unittest.TestCase): 577 | def test_set(self): 578 | self.assertEqual("50 100\n", run("x=99; y=100; x=50; /bin/echo $x $y")) 579 | self.assertIn("x=100", 580 | run("x=99; export x; /bin/echo foo; x=100; /usr/bin/printenv")) 581 | 582 | def test_setFromEnviron(self): 583 | self.assertNotEqual("\n", run("/bin/echo $HOME")) 584 | self.assertNotEqual("\n", run("/bin/echo $SHELL")) 585 | self.assertIn("SHELL=xyzzy", run("SHELL=xyzzy; /usr/bin/printenv")) 586 | 587 | def test_set(self): 588 | self.assertEqual(".99. ..\n", run("x=99; /bin/echo .$x. .$y.")) 589 | 590 | def test_getEnviron(self): 591 | result = run("x=99; export x; y=100; /usr/bin/printenv") 592 | self.assertIn("x=99", result) 593 | self.assertNotIn("y=", result) 594 | self.assertIn("HOME=", result) 595 | self.assertIn("SHELL=", result) 596 | 597 | def test_unset(self): 598 | self.assertEqual("~99~\n~~\n", 599 | run("x=99; /bin/echo ~$x~; unset x; /bin/echo ~$x~")) 600 | result = run("x=99; export x; echo x:$x; unset x; /usr/bin/printenv") 601 | self.assertIn("x:99", result); 602 | self.assertNotIn("x=99", result); 603 | 604 | def test_markExported(self): 605 | self.assertNotIn("x=99", run("x=99; /usr/bin/printenv")) 606 | self.assertIn("x=99", run("x=99; /bin/echo foo bar; export x; " 607 | "/usr/bin/printenv")) 608 | 609 | if __name__ == '__main__': 610 | unittest.main() 611 | --------------------------------------------------------------------------------