diff --git a/mshell-go/Evaluator.go b/mshell-go/Evaluator.go index 2ea9256..9ba5ce3 100644 --- a/mshell-go/Evaluator.go +++ b/mshell-go/Evaluator.go @@ -1,1593 +1,1606 @@ package main import ( - "io" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "sync" - "bufio" - "bytes" - "path/filepath" + "bufio" + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" ) type MShellStack []MShellObject func (objList *MShellStack) Peek() (MShellObject, error) { - if len(*objList) == 0 { - return nil, fmt.Errorf("Empty stack") - } - return (*objList)[len(*objList) - 1], nil + if len(*objList) == 0 { + return nil, fmt.Errorf("Empty stack") + } + return (*objList)[len(*objList)-1], nil } func (objList *MShellStack) Pop() (MShellObject, error) { - if len(*objList) == 0 { - return nil, fmt.Errorf("Empty stack") - } - popped := (*objList)[len(*objList) - 1] - *objList = (*objList)[:len(*objList) - 1] - return popped, nil + if len(*objList) == 0 { + return nil, fmt.Errorf("Empty stack") + } + popped := (*objList)[len(*objList)-1] + *objList = (*objList)[:len(*objList)-1] + return popped, nil } func (objList *MShellStack) Push(obj MShellObject) { - *objList = append(*objList, obj) + *objList = append(*objList, obj) } func (objList *MShellStack) String() string { - var builder strings.Builder - builder.WriteString("Stack contents:\n") - for i, obj := range *objList { - builder.WriteString(fmt.Sprintf("%d: %s\n", i, obj.DebugString())) - } - builder.WriteString("End of stack contents\n") - return builder.String() + var builder strings.Builder + builder.WriteString("Stack contents:\n") + for i, obj := range *objList { + builder.WriteString(fmt.Sprintf("%d: %s\n", i, obj.DebugString())) + } + builder.WriteString("End of stack contents\n") + return builder.String() } -type EvalState struct { - PositionalArgs[] string - LoopDepth int - Variables map[string]MShellObject +type EvalState struct { + PositionalArgs []string + LoopDepth int + Variables map[string]MShellObject - StopOnError bool + StopOnError bool } type EvalResult struct { - Success bool - BreakNum int - ExitCode int + Success bool + BreakNum int + ExitCode int } type ExecuteContext struct { - StandardInput io.Reader - StandardOutput io.Writer + StandardInput io.Reader + StandardOutput io.Writer } func SimpleSuccess() EvalResult { - return EvalResult { true, -1, 0 } + return EvalResult{true, -1, 0} } func FailWithMessage(message string) EvalResult { - // Log message to stderr - fmt.Fprintf(os.Stderr, message) - return EvalResult { false, -1, 1 } + // Log message to stderr + fmt.Fprintf(os.Stderr, message) + return EvalResult{false, -1, 1} } func (state *EvalState) Evaluate(objects []MShellParseItem, stack *MShellStack, context ExecuteContext, definitions []MShellDefinition) EvalResult { - index := 0 - - MainLoop: - for index < len(objects) { - t := objects[index] - index++ - - switch t.(type) { - case *MShellParseList: - // Evaluate the list - list := t.(*MShellParseList) - var listStack MShellStack - listStack = []MShellObject{} - - result := state.Evaluate(list.Items, &listStack, context, definitions) - - if !result.Success { - fmt.Fprintf(os.Stderr, "Failed to evaluate list.\n") - return result - } - - if result.BreakNum > 0 { - return FailWithMessage("Encountered break within list.\n") - } - - stack.Push(&MShellList { Items: listStack, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE }) - case *MShellParseQuote: - parseQuote := t.(*MShellParseQuote) - q := MShellQuotation { Tokens: parseQuote.Items, StandardInputFile: "", StandardOutputFile: "", StandardErrorFile: "" } - stack.Push(&q) - case Token: - t := t.(Token) - - if t.Type == EOF { - return SimpleSuccess() - } else if t.Type == LITERAL { - - // Check for definitions - for _, definition := range definitions { - if definition.Name == t.Lexeme { - // Evaluate the definition - result := state.Evaluate(definition.Items, stack, context, definitions) - if !result.Success || result.BreakNum > 0 { - return result - } - continue MainLoop - } - } - - if t.Lexeme == ".s" { - // Print current stack - fmt.Fprintf(os.Stderr, stack.String()) - } else if t.Lexeme == "dup" { - top, err := stack.Peek() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot duplicate an empty stack.\n", t.Line, t.Column)) - } - stack.Push(top) - } else if t.Lexeme == "over" { - stackLen := len(*stack) - if stackLen < 2 { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'over' operation on a stack with less than two items.\n", t.Line, t.Column)) - } - - obj := (*stack)[stackLen - 2] - stack.Push(obj) - } else if t.Lexeme == "swap" { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'swap' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'swap' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - stack.Push(obj1) - stack.Push(obj2) - } else if t.Lexeme == "drop" { - _, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot drop an empty stack.\n", t.Line, t.Column)) - } - } else if t.Lexeme == "rot" { - // Check that there are at least 3 items on the stack - stackLen := len(*stack) - if stackLen < 3 { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'rot' operation on a stack with less than three items.\n", t.Line, t.Column)) - } - top, _ := stack.Pop() - second, _ := stack.Pop() - third, _ := stack.Pop() - stack.Push(second) - stack.Push(top) - stack.Push(third) - } else if t.Lexeme == "-rot" { - // Check that there are at least 3 items on the stack - if len(*stack) < 3 { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'rot' operation on a stack with less than three items.\n", t.Line, t.Column)) - } - top, _ := stack.Pop() - second, _ := stack.Pop() - third, _ := stack.Pop() - stack.Push(top) - stack.Push(third) - stack.Push(second) - } else if t.Lexeme == "nip" { - if len(*stack) < 2 { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nip' operation on a stack with less than two items.\n", t.Line, t.Column)) - } - top, _ := stack.Pop() - _, _ = stack.Pop() - stack.Push(top) - } else if t.Lexeme == "glob" { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'glob' operation on an empty stack.\n", t.Line, t.Column)) - } - - // Can be a string or literal - var globStr string - switch obj1.(type) { - case *MShellString: - globStr = obj1.(*MShellString).Content - case *MShellLiteral: - globStr = obj1.(*MShellLiteral).LiteralText - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot glob a %s.\n", t.Line, t.Column, obj1.TypeName())) - } - - files, err := filepath.Glob(globStr) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Malformed glob pattern: %s\n", t.Line, t.Column, err.Error())) - } - - newList := &MShellList { Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE } - for _, file := range files { - newList.Items = append(newList.Items, &MShellString { file }) - } - stack.Push(newList) - } else if t.Lexeme == "stdin" { - // Dump all of current stdin onto the stack as a string - var buffer bytes.Buffer - _, err := buffer.ReadFrom(context.StandardInput) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) - } - stack.Push(&MShellString { buffer.String() }) - } else if t.Lexeme == "append" { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'append' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'append' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - // Can do append with list and object in either order. If two lists, append obj1 into obj2 - switch obj1.(type) { - case *MShellList: - switch obj2.(type) { - case *MShellList: - obj2.(*MShellList).Items = append(obj2.(*MShellList).Items, obj1) - stack.Push(obj2) - default: - obj1.(*MShellList).Items = append(obj1.(*MShellList).Items, obj2) - stack.Push(obj1) - } - default: - switch obj2.(type) { - case *MShellList: - obj2.(*MShellList).Items = append(obj2.(*MShellList).Items, obj1) - stack.Push(obj2) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot append a %s to a %s.\n", t.Line, t.Column, obj1.TypeName(), obj2.TypeName())) - } - } - } else if t.Lexeme == "args" { - // Dump the positional arguments onto the stack as a list of strings - newList := &MShellList { Items: make([]MShellObject, len(state.PositionalArgs)), StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE } - for i, arg := range state.PositionalArgs { - newList.Items[i] = &MShellString { arg } - } - stack.Push(newList) - } else if t.Lexeme == "len" { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'len' operation on an empty stack.\n", t.Line, t.Column)) - } - - switch obj.(type) { - case *MShellList: - stack.Push(&MShellInt { len(obj.(*MShellList).Items) }) - case *MShellString: - stack.Push(&MShellInt { len(obj.(*MShellString).Content) }) - case *MShellLiteral: - stack.Push(&MShellInt { len(obj.(*MShellLiteral).LiteralText) }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot get length of a %s.\n", t.Line, t.Column, obj.TypeName())) - } - } else if t.Lexeme == "nth" { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - int1, ok := obj1.(*MShellInt) - if ok { - result, err := obj2.Index(int1.Value) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: %s\n", t.Line, t.Column, err.Error())) - } - stack.Push(result) - } else { - int2, ok := obj2.(*MShellInt) - if ok { - result, err := obj1.Index(int2.Value) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: %s\n", t.Line, t.Column, err.Error())) - } - stack.Push(result) - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' with a %s and a %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) - } - } - } else if t.Lexeme == "pick" { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' operation on an empty stack.\n", t.Line, t.Column)) - } - // Check that obj1 is an integer - int1, ok := obj1.(*MShellInt) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' with a %s.\n", t.Line, t.Column, obj1.TypeName())) - } - - // Check that int is greater than or equal to 1 - if int1.Value < 1 { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' with a value less than 1.\n", t.Line, t.Column)) - } - - // Check that the stack has enough items - if int1.Value > len(*stack) { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' on a stack with less than %d items.\n", t.Line, t.Column, int1.Value + 1)) - } - - // Duplicate the nth item on the stack - // a b c 2 -> a b c b - stack.Push((*stack)[len(*stack) - int1.Value]) - } else if t.Lexeme == "w" || t.Lexeme == "wl" || t.Lexeme == "we" || t.Lexeme == "wle" { - // Print the top of the stack to the console. - top, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot write an empty stack.\n", t.Line, t.Column)) - } - - var writer io.Writer - if t.Lexeme == "we" || t.Lexeme == "wle" { - writer = os.Stderr - } else { - writer = os.Stdout - } - - switch top.(type) { - case *MShellLiteral: - fmt.Fprintf(writer, "%s", top.(*MShellLiteral).LiteralText) - case *MShellString: - fmt.Fprintf(writer, "%s", top.(*MShellString).Content) - case *MShellInt: - fmt.Fprintf(writer, "%d", top.(*MShellInt).Value) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot write a %s.\n", t.Line, t.Column, top.TypeName())) - } - - if t.Lexeme == "wl" || t.Lexeme == "wle" { - fmt.Fprintf(writer, "\n") - } - } else if t.Lexeme == "findReplace" { - // Do simple find replace with the top three strings on stack - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - obj3, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on a stack with only two items.\n", t.Line, t.Column)) - } - - switch obj1.(type) { - case *MShellString: - switch obj2.(type) { - case *MShellString: - switch obj3.(type) { - case *MShellString: - stack.Push(&MShellString { strings.Replace(obj3.(*MShellString).Content, obj2.(*MShellString).Content, obj1.(*MShellString).Content, -1) }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj3.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj1.TypeName())) - } - - } else if t.Lexeme == "split" { - delimiter, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'split' operation on an empty stack.\n", t.Line, t.Column)) - } - - strLiteral, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'split' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - - var delimiterStr string - var strToSplit string - - switch delimiter.(type) { - case *MShellString: - delimiterStr = delimiter.(*MShellString).Content - case *MShellLiteral: - delimiterStr = delimiter.(*MShellLiteral).LiteralText - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot split with a %s.\n", t.Line, t.Column, delimiter.TypeName())) - } - - switch strLiteral.(type) { - case *MShellString: - strToSplit = strLiteral.(*MShellString).Content - case *MShellLiteral: - strToSplit = strLiteral.(*MShellLiteral).LiteralText - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot split a %s.\n", t.Line, t.Column, strLiteral.TypeName())) - } - - split := strings.Split(strToSplit, delimiterStr) - newList := &MShellList { Items: make([]MShellObject, len(split)), StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE } - for i, item := range split { - newList.Items[i] = &MShellString { item } - } - stack.Push(newList) - } else if t.Lexeme == "join" { - delimiter, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'join' operation on an empty stack.\n", t.Line, t.Column)) - } - - list, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'join' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - var delimiterStr string - var listItems []string - - switch delimiter.(type) { - case *MShellString: - delimiterStr = delimiter.(*MShellString).Content - case *MShellLiteral: - delimiterStr = delimiter.(*MShellLiteral).LiteralText - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join with a %s.\n", t.Line, t.Column, delimiter.TypeName())) - } - - switch list.(type) { - case *MShellList: - for _, item := range list.(*MShellList).Items { - switch item.(type) { - case *MShellString: - listItems = append(listItems, item.(*MShellString).Content) - case *MShellLiteral: - listItems = append(listItems, item.(*MShellLiteral).LiteralText) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join a list with a %s inside (%s).\n", t.Line, t.Column, item.TypeName(), item.DebugString())) - } - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join a %s (%s).\n", t.Line, t.Column, list.TypeName(), list.DebugString())) - } - - stack.Push(&MShellString { strings.Join(listItems, delimiterStr) }) - } else if t.Lexeme == "lines" { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot evaluate 'lines' on an empty stack.\n", t.Line, t.Column)) - } - - s1, ok := obj.(*MShellString) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot evaluate 'lines' on a %s.\n", t.Line, t.Column, obj.TypeName())) - } - - // TODO: Maybe reuse a scanner? - scanner := bufio.NewScanner(strings.NewReader(s1.Content)) - newList := &MShellList { Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE } - for scanner.Scan() { - newList.Items = append(newList.Items, &MShellString { scanner.Text() }) - } - - stack.Push(newList) - } else if t.Lexeme == "~" || strings.HasPrefix(t.Lexeme, "~/") { - // Only do tilde expansion - homeDir, err := os.UserHomeDir() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error expanding ~: %s\n", t.Line, t.Column, err.Error())) - } - - var tildeExpanded string - if t.Lexeme == "~" { - tildeExpanded = homeDir - } else { - tildeExpanded = filepath.Join(homeDir, t.Lexeme[2:]) - } - - stack.Push(&MShellString { tildeExpanded }) - } else { - stack.Push(&MShellLiteral { t.Lexeme }) - } - } else if t.Type == LEFT_SQUARE_BRACKET { - return FailWithMessage(fmt.Sprintf("%d:%d: Found unexpected left square bracket.\n", t.Line, t.Column)) - } else if t.Type == LEFT_PAREN { - return FailWithMessage(fmt.Sprintf("%d:%d: Found unexpected left parenthesis.\n", t.Line, t.Column)) - } else if t.Type == EXECUTE || t.Type == QUESTION { - top, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot execute an empty stack.\n", t.Line, t.Column)) - } - - // Switch on type - var result EvalResult - var exitCode int - var stdout string - - switch top.(type) { - case *MShellList: - result, exitCode, stdout = RunProcess(*top.(*MShellList), context, state) - case *MShellPipe: - result, exitCode = state.RunPipeline(*top.(*MShellPipe), context, stack) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot execute a non-list object. Found %s %s\n", t.Line, t.Column, top.TypeName(), top.DebugString())) - } - - if state.StopOnError && exitCode != 0 { - // Exit completely, with that exit code, don't need to print a different message. Usually the command itself will have printed an error. - return EvalResult { false, -1, exitCode } - } - - if !result.Success { - return result - } - - asList, ok := top.(*MShellList) - if ok { - if asList.StdoutBehavior == STDOUT_LINES { - newMShellList := &MShellList { Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE } - var scanner *bufio.Scanner - scanner = bufio.NewScanner(strings.NewReader(stdout)) - for scanner.Scan() { - newMShellList.Items = append(newMShellList.Items, &MShellString { scanner.Text() }) - } - stack.Push(newMShellList) - } else if asList.StdoutBehavior == STDOUT_STRIPPED { - stripped := strings.TrimSpace(stdout) - stack.Push(&MShellString { stripped }) - } else if asList.StdoutBehavior == STDOUT_COMPLETE { - stack.Push(&MShellString { stdout }) - } - } - - // Push the exit code onto the stack if a question was used to execute - if t.Type == QUESTION { - stack.Push(&MShellInt { exitCode }) - } - } else if t.Type == TRUE { - stack.Push(&MShellBool { true }) - } else if t.Type == FALSE { - stack.Push(&MShellBool { false }) - } else if t.Type == INTEGER { - intVal, err := strconv.Atoi(t.Lexeme) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing integer: %s\n", t.Line, t.Column, err.Error())) - } - - stack.Push(&MShellInt { intVal}) - } else if t.Type == STRING { - parsedString, err := ParseRawString(t.Lexeme) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing string: %s\n", t.Line, t.Column, err.Error())) - } - stack.Push(&MShellString { parsedString }) - } else if t.Type == SINGLEQUOTESTRING { - stack.Push(&MShellString { t.Lexeme[1:len(t.Lexeme) - 1] }) - } else if t.Type == IF { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do an 'if' on an empty stack.\n", t.Line, t.Column)) - } - - list, ok := obj.(*MShellList) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Argument for if expected to be a list of quoations, received a %s\n", t.Line, t.Column, obj.TypeName())) - } - - if len(list.Items) < 2 { - return FailWithMessage(fmt.Sprintf("%d:%d: If statement requires a list with at least 2 items. Found %d.\n", t.Line, t.Column, len(list.Items))) - } - - // Check that all items are quotations - for i, item := range list.Items { - if _, ok := item.(*MShellQuotation); !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Item %d in if statement is not a quotation.\n", t.Line, t.Column, i)) - } - } - - trueIndex := -1 - - ListLoop: - for i := 0; i < len(list.Items) - 1; i += 2 { - quotation := list.Items[i].(*MShellQuotation) - result := state.Evaluate(quotation.Tokens, stack, context, definitions) - - if !result.Success { - return result - } - - if result.BreakNum > 0 { - return FailWithMessage("Encountered break within if statement.\n") - } - - top, err := stack.Pop() - if err != nil { - conditionNum := i / 2 + 1 - return FailWithMessage(fmt.Sprintf("%d:%d: Found an empty stack when evaluating condition #%d .\n", t.Line, t.Column, conditionNum)) - } - - // Check for either integer or boolean - switch top.(type) { - case *MShellInt: - if top.(*MShellInt).Value == 0 { - trueIndex = i - break ListLoop - } - case *MShellBool: - if top.(*MShellBool).Value { - trueIndex = i - break ListLoop - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Expected an integer or boolean for condition #%d, received a %s.\n", t.Line, t.Column, i / 2 + 1, top.TypeName())) - } - } - - if trueIndex > -1 { - quotation := list.Items[trueIndex + 1].(*MShellQuotation) - result := state.Evaluate(quotation.Tokens, stack, context, definitions) - - // If we encounter a break, we should return it up the stack - if !result.Success || result.BreakNum != -1 { - return result - } - } else if len(list.Items) % 2 == 1 { // Try to find a final else statement, will be the last item in the list if odd number of items - quotation := list.Items[len(list.Items) - 1].(*MShellQuotation) - result := state.Evaluate(quotation.Tokens, stack, context, definitions) - - if !result.Success || result.BreakNum != -1 { - return result - } - } - } else if t.Type == PLUS { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '+' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '+' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - switch obj1.(type) { - case *MShellInt: - switch obj2.(type) { - case *MShellInt: - stack.Push(&MShellInt { obj2.(*MShellInt).Value + obj1.(*MShellInt).Value }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add an integer to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - case *MShellString: - switch obj2.(type) { - case *MShellString: - stack.Push(&MShellString { obj2.(*MShellString).Content + obj1.(*MShellString).Content }) - case *MShellLiteral: - stack.Push(&MShellString { obj2.(*MShellLiteral).LiteralText + obj1.(*MShellString).Content }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add a string to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - case *MShellLiteral: - switch obj2.(type) { - case *MShellString: - stack.Push(&MShellString { obj2.(*MShellString).Content + obj1.(*MShellLiteral).LiteralText }) - case *MShellLiteral: - stack.Push(&MShellString { obj2.(*MShellLiteral).LiteralText + obj1.(*MShellLiteral).LiteralText }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add a literal to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '+' to a %s to a %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) - } - } else if t.Type == MINUS { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '-' operation on an empty stack.\n", t.Line, t.Column)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '-' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - switch obj1.(type) { - case *MShellInt: - switch obj2.(type) { - case *MShellInt: - stack.Push(&MShellInt { obj2.(*MShellInt).Value - obj1.(*MShellInt).Value }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot subtract an integer from a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '-' to a %s and %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) - } - } else if t.Type == AND || t.Type == OR { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) - } - - switch obj1.(type) { - case *MShellBool: - switch obj2.(type) { - case *MShellBool: - if t.Type == AND { - stack.Push(&MShellBool { obj2.(*MShellBool).Value && obj1.(*MShellBool).Value }) - } else { - stack.Push(&MShellBool { obj2.(*MShellBool).Value || obj1.(*MShellBool).Value }) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) - } - } else if t.Type == NOT { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) - } - - switch obj.(type) { - case *MShellBool: - stack.Push(&MShellBool { !obj.(*MShellBool).Value }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s.\n", t.Line, t.Column, t.Lexeme, obj.TypeName())) - } - } else if t.Type == GREATERTHANOREQUAL || t.Type == LESSTHANOREQUAL { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) - } - - if obj1.IsNumeric() && obj2.IsNumeric() { - if t.Type == GREATERTHANOREQUAL { - stack.Push(&MShellBool { obj2.FloatNumeric() >= obj1.FloatNumeric() }) - } else { - stack.Push(&MShellBool { obj2.FloatNumeric() <= obj1.FloatNumeric() }) - } - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and a %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) - } - } else if t.Type == GREATERTHAN || t.Type == LESSTHAN { - // This can either be normal comparison for numerics, or it's a redirect on a list or quotation. - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) - } - - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) - } - - if obj1.IsNumeric() && obj2.IsNumeric() { - if t.Type == GREATERTHAN { - stack.Push(&MShellBool { obj2.FloatNumeric() > obj1.FloatNumeric() }) - } else { - stack.Push(&MShellBool { obj2.FloatNumeric() < obj1.FloatNumeric() }) - } - } else { - switch obj1.(type) { - case *MShellString: - switch obj2.(type) { - case *MShellList: - if t.Type == GREATERTHAN { - obj2.(*MShellList).StandardOutputFile = obj1.(*MShellString).Content - } else { - obj2.(*MShellList).StandardInputFile = obj1.(*MShellString).Content - } - stack.Push(obj2) - case *MShellQuotation: - if t.Type == GREATERTHAN { - obj2.(*MShellQuotation).StandardOutputFile = obj1.(*MShellString).Content - } else { - obj2.(*MShellQuotation).StandardInputFile = obj1.(*MShellString).Content - } - stack.Push(obj2) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect a string to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - case *MShellLiteral: - switch obj2.(type) { - case *MShellList: - if t.Type == GREATERTHAN { - obj2.(*MShellList).StandardOutputFile = obj1.(*MShellLiteral).LiteralText - } else { - obj2.(*MShellList).StandardInputFile = obj1.(*MShellLiteral).LiteralText - } - stack.Push(obj2) - case *MShellQuotation: - if t.Type == GREATERTHAN { - obj2.(*MShellQuotation).StandardOutputFile = obj1.(*MShellLiteral).LiteralText - } else { - obj2.(*MShellQuotation).StandardInputFile = obj1.(*MShellLiteral).LiteralText - } - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect a %s to a %s.\n", t.Line, t.Column, obj1.TypeName(), obj2.TypeName())) - } - } - } else if t.Type == STDERRREDIRECT { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr on an empty stack.\n", t.Line, t.Column)) - } - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr on a stack with only one item.\n", t.Line, t.Column)) - } - - switch obj1.(type) { - case *MShellString: - switch obj2.(type) { - case *MShellList: - obj2.(*MShellList).StandardErrorFile = obj1.(*MShellString).Content - stack.Push(obj2) - case *MShellQuotation: - obj2.(*MShellQuotation).StandardErrorFile = obj1.(*MShellString).Content - stack.Push(obj2) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - case *MShellLiteral: - switch obj2.(type) { - case *MShellList: - obj2.(*MShellList).StandardErrorFile = obj1.(*MShellLiteral).LiteralText - stack.Push(obj2) - case *MShellQuotation: - obj2.(*MShellQuotation).StandardErrorFile = obj1.(*MShellLiteral).LiteralText - stack.Push(obj2) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj1.TypeName())) - } - } else if t.Type == VARSTORE { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Nothing on stack to store into variable %s.\n", t.Line, t.Column, t.Lexeme[1:])) - } - - state.Variables[t.Lexeme[1:]] = obj - } else if t.Type == VARRETRIEVE { - name := t.Lexeme[:len(t.Lexeme) - 1] - obj, found_mshell_variable := state.Variables[name] - if found_mshell_variable { - stack.Push(obj) - } else { - // Check current environment variables - envValue, ok := os.LookupEnv(name) - if ok { - stack.Push(&MShellString { envValue }) - } else { - var message strings.Builder - message.WriteString(fmt.Sprintf("%d:%d: Variable %s not found.\n", t.Line, t.Column, name)) - message.WriteString("Variables:\n") - for key := range state.Variables { - message.WriteString(fmt.Sprintf(" %s\n", key)) - } - return FailWithMessage(message.String()) - } - } - } else if t.Type == LOOP { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do a loop on an empty stack.\n", t.Line, t.Column)) - } - - quotation, ok := obj.(*MShellQuotation) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Argument for loop expected to be a quotation, received a %s\n", t.Line, t.Column, obj.TypeName())) - } - - if len(quotation.Tokens) == 0 { - return FailWithMessage(fmt.Sprintf("%d:%d: Loop quotation needs a minimum of one token.\n", t.Line, t.Column)) - } - - context := ExecuteContext { - StandardInput: nil, - StandardOutput: nil, - } - - if quotation.StandardInputFile != "" { - file, err := os.Open(quotation.StandardInputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for reading: %s\n", t.Line, t.Column, quotation.StandardInputFile, err.Error())) - } - context.StandardInput = file - defer file.Close() - } - - if quotation.StandardOutputFile != "" { - file, err := os.Create(quotation.StandardOutputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for writing: %s\n", t.Line, t.Column, quotation.StandardOutputFile, err.Error())) - } - context.StandardOutput = file - defer file.Close() - } - - maxLoops := 15000 - loopCount := 0 - state.LoopDepth++ - - breakDiff := 0 - - for loopCount < maxLoops { - result := state.Evaluate(quotation.Tokens, stack, context, definitions) - - if !result.Success { - return result - } - - if result.BreakNum >= 0 { - breakDiff = state.LoopDepth - result.BreakNum - if breakDiff >= 0 { - break - } - } - - loopCount++ - } - - if loopCount == maxLoops { - return FailWithMessage(fmt.Sprintf("%d:%d: Loop exceeded maximum number of iterations.\n", t.Line, t.Column)) - } - - state.LoopDepth-- - - if breakDiff > 0 { - return EvalResult { true, breakDiff - 1, 0 } - } - } else if t.Type == BREAK { - return EvalResult { true, 1, 0 } - } else if t.Type == EQUALS { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '=' operation on an empty stack.\n", t.Line, t.Column)) - } - obj2, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '=' operation on a stack with only one item.\n", t.Line, t.Column)) - } - - // Implement for integers right now. - switch obj1.(type) { - case *MShellInt: - switch obj2.(type) { - case *MShellInt: - stack.Push(&MShellBool { obj2.(*MShellInt).Value == obj1.(*MShellInt).Value }) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot compare an integer to a %s.\n", t.Line, t.Column, obj2.TypeName())) - } - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot complete %s with a %s to a %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) - } - } else if t.Type == INTERPRET { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot interpret an empty stack.\n", t.Line, t.Column)) - } - - quotation, ok := obj.(*MShellQuotation) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Argument for interpret expected to be a quotation, received a %s\n", t.Line, t.Column, obj.TypeName())) - } - - quoteContext := ExecuteContext { - StandardInput: nil, - StandardOutput: nil, - } - - if quotation.StandardInputFile != "" { - file, err := os.Open(quotation.StandardInputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for reading: %s\n", t.Line, t.Column, quotation.StandardInputFile, err.Error())) - } - quoteContext.StandardInput = file - defer file.Close() - } else if context.StandardInput != nil { - quoteContext.StandardInput = context.StandardInput - } else { - // Default to stdin of this process itself - quoteContext.StandardInput = os.Stdin - } - - if quotation.StandardOutputFile != "" { - file, err := os.Create(quotation.StandardOutputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for writing: %s\n", t.Line, t.Column, quotation.StandardOutputFile, err.Error())) - } - quoteContext.StandardOutput = file - defer file.Close() - } else if context.StandardOutput != nil { - quoteContext.StandardOutput = context.StandardOutput - } else { - // Default to stdout of this process itself - quoteContext.StandardOutput = os.Stdout - } - - result := state.Evaluate(quotation.Tokens, stack, quoteContext, definitions) - if !result.Success { - return result - } - - if result.BreakNum > 0 { - return result - } - } else if t.Type == POSITIONAL { - posNum := t.Lexeme[1:] - posIndex, err := strconv.Atoi(posNum) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing positional argument number: %s\n", t.Line, t.Column, err.Error())) - } - - if posIndex == 0 { - return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument are 1-based, first argument is $1, not $0.\n", t.Line, t.Column)) - } - - if posIndex < 0 { - return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument numbers must be positive.\n", t.Line, t.Column)) - } - - if posIndex > len(state.PositionalArgs) { - return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument %s is greater than the number of arguments provided.\n", t.Line, t.Column, t.Lexeme)) - } - - stack.Push(&MShellString { state.PositionalArgs[posIndex - 1] }) - } else if t.Type == PIPE { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) - } - - // obj1 should be a list - list, ok := obj1.(*MShellList) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot pipe a %s.\n", t.Line, t.Column, obj1.TypeName())) - } - - stack.Push( &MShellPipe { *list } ) - } else if t.Type == READ { - var reader io.Reader - // Check if what we are reading from is seekable. If so, we can do a buffered read and reset the position. - // Else, we have to read byte by byte. - - isSeekable := false - - if context.StandardInput == nil { - reader = os.Stdin - _, err := reader.(*os.File).Seek(0, io.SeekCurrent) - isSeekable = err == nil - - } else { - reader = context.StandardInput - _, err := reader.(*os.File).Seek(0, io.SeekCurrent) - isSeekable = err == nil - } - - if isSeekable { - // Do a buffered read - bufferedReader := bufio.NewReader(reader) - line, err := bufferedReader.ReadString('\n') - if err != nil { - if err == io.EOF { - stack.Push(&MShellString { "" }) - stack.Push(&MShellBool { false }) - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) - } - } else { - // Check if the last character is a '\r' and remove it if it is. Also remove the '\n' itself - if len(line) > 0 && line[len(line) - 1] == '\n' { - line = line[:len(line) - 1] - } - if len(line) > 0 && line[len(line) - 1] == '\r' { - line = line[:len(line) - 1] - } - - stack.Push(&MShellString { line }) - stack.Push(&MShellBool { true }) - } - - // Reset the position of the reader to the position after the read - offset, err := reader.(*os.File).Seek(0, io.SeekCurrent) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error resetting position of reader: %s\n", t.Line, t.Column, err.Error())) - } - remainingInBuffer := bufferedReader.Buffered() - // fmt.Fprintf(os.Stderr, "Offset: %d, Remaining in buffer: %d\n", offset, remainingInBuffer) - newPosition := offset - int64(remainingInBuffer) - // fmt.Fprintf(os.Stderr, "New position: %d\n", newPosition) - _, err = reader.(*os.File).Seek(newPosition, io.SeekStart) - } else { - // Do a byte by byte read - var line strings.Builder - for { - b := make([]byte, 1) - _, err := reader.Read(b) - if err != nil { - if err == io.EOF { - // If nothing in line, then this was the end of the file - if line.Len() == 0 { - stack.Push(&MShellString { "" }) - stack.Push(&MShellBool { false }) - } else { - // Else, we have a final that wasn't terminated by a newline. Still try to remove '\r' if it's there - builderStr := line.String() - if len(builderStr) > 0 && builderStr[len(builderStr) - 1] == '\r' { - builderStr = builderStr[:len(builderStr) - 1] - } - stack.Push(&MShellString { builderStr }) - stack.Push(&MShellBool { true }) - } - break - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) - } - } - - if b[0] == '\n' { - builderStr := line.String() - - // Check if the last character is a '\r' and remove it if it is - if len(builderStr) > 0 && builderStr[len(builderStr) - 1] == '\r' { - builderStr = builderStr[:len(builderStr) - 1] - } - - stack.Push(&MShellString { builderStr }) - stack.Push(&MShellBool { true }) - break - } else { - line.WriteByte(b[0]) - } - } - } - } else if t.Type == STR { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot convert an empty stack to a string.\n", t.Line, t.Column)) - } - - stack.Push(&MShellString { obj.ToString() }) - } else if t.Type == INDEXER { - obj1, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot index an empty stack.\n", t.Line, t.Column)) - } - - // Indexer is a digit between ':' and ':'. Remove ends and parse the number - indexStr := t.Lexeme[1:len(t.Lexeme) - 1] - index, err := strconv.Atoi(indexStr) - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing index: %s\n", t.Line, t.Column, err.Error())) - } - - result, err := obj1.Index(index) - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: %s", t.Line, t.Column, err.Error())) } - stack.Push(result) - } else if t.Type == ENDINDEXER || t.Type == STARTINDEXER { - obj1, err := stack.Pop() - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: Cannot end index an empty stack.\n", t.Line, t.Column)) } - - var indexStr string - // Parse the index value - if t.Type == ENDINDEXER { - indexStr = t.Lexeme[1:] - } else { indexStr = t.Lexeme[:len(t.Lexeme) - 1] } - - index, err := strconv.Atoi(indexStr) - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing index: %s\n", t.Line, t.Column, err.Error())) } - - var result MShellObject - if t.Type == ENDINDEXER { - result, err = obj1.SliceEnd(index) - } else { result, err = obj1.SliceStart(index) } - - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: %s", t.Line, t.Column, err.Error())) } - stack.Push(result) - } else if t.Type == SLICEINDEXER { - obj1, err := stack.Pop() - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: Cannot slice index an empty stack.\n", t.Line, t.Column)) } - - // StartInc:EndExc - parts := strings.Split(t.Lexeme, ":") - startInt, err := strconv.Atoi(parts[0]) - endInt, err2 := strconv.Atoi(parts[1]) - - if err != nil || err2 != nil { return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing slice indexes: %s\n", t.Line, t.Column, err.Error())) } - - result, err := obj1.Slice(startInt, endInt) - if err != nil { return FailWithMessage(fmt.Sprintf("%d:%d: Cannot slice index a %s.\n", t.Line, t.Column, obj1.TypeName())) } - - stack.Push(result) - } else if t.Type == STDOUTLINES || t.Type == STDOUTSTRIPPED || t.Type == STDOUTCOMPLETE { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot set stdout behavior to lines on an empty stack.\n", t.Line, t.Column)) - } - - list, ok := obj.(*MShellList) - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot set stdout behavior to lines on a %s.\n", t.Line, t.Column, obj.TypeName())) - } - - if t.Type == STDOUTLINES { - list.StdoutBehavior = STDOUT_LINES - } else if t.Type == STDOUTSTRIPPED { - list.StdoutBehavior = STDOUT_STRIPPED - } else if t.Type == STDOUTCOMPLETE { - list.StdoutBehavior = STDOUT_COMPLETE - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: We haven't implemented the token type '%s' yet.\n", t.Line, t.Column, t.Type)) - } - stack.Push(list) - } else if t.Type == EXPORT { - obj, err := stack.Pop() - if err != nil { - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export an empty stack.\n", t.Line, t.Column)) - } - - var varName string - switch obj.(type) { - case *MShellString: - varName = obj.(*MShellString).Content - case *MShellLiteral: - varName = obj.(*MShellLiteral).LiteralText - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export a %s.\n", t.Line, t.Column, obj.TypeName())) - } - - // Check that varName is in state.Variables, and varName is string or literal - varValue, ok := state.Variables[varName] - if !ok { - return FailWithMessage(fmt.Sprintf("%d:%d: Variable %s not found in state.Variables.\n", t.Line, t.Column, varName)) - } - - switch varValue.(type) { - case *MShellString: - os.Setenv(varName, varValue.(*MShellString).Content) - case *MShellLiteral: - os.Setenv(varName, varValue.(*MShellLiteral).LiteralText) - default: - return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export a %s as an environment variable.\n", t.Line, t.Column, varValue.TypeName())) - } - } else if t.Type == STOP_ON_ERROR { - state.StopOnError = true - } else { - return FailWithMessage(fmt.Sprintf("%d:%d: We haven't implemented the token type '%s' yet.\n", t.Line, t.Column, t.Type)) - } - - default: - return FailWithMessage(fmt.Sprintf("We haven't implemented the type '%T' yet.\n", t)) - } - - - } - - return EvalResult { true, -1, 0 } + index := 0 + +MainLoop: + for index < len(objects) { + t := objects[index] + index++ + + switch t.(type) { + case *MShellParseList: + // Evaluate the list + list := t.(*MShellParseList) + var listStack MShellStack + listStack = []MShellObject{} + + result := state.Evaluate(list.Items, &listStack, context, definitions) + + if !result.Success { + fmt.Fprintf(os.Stderr, "Failed to evaluate list.\n") + return result + } + + if result.BreakNum > 0 { + return FailWithMessage("Encountered break within list.\n") + } + + stack.Push(&MShellList{Items: listStack, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE}) + case *MShellParseQuote: + parseQuote := t.(*MShellParseQuote) + q := MShellQuotation{Tokens: parseQuote.Items, StandardInputFile: "", StandardOutputFile: "", StandardErrorFile: ""} + stack.Push(&q) + case Token: + t := t.(Token) + + if t.Type == EOF { + return SimpleSuccess() + } else if t.Type == LITERAL { + + // Check for definitions + for _, definition := range definitions { + if definition.Name == t.Lexeme { + // Evaluate the definition + result := state.Evaluate(definition.Items, stack, context, definitions) + if !result.Success || result.BreakNum > 0 { + return result + } + continue MainLoop + } + } + + if t.Lexeme == ".s" { + // Print current stack + fmt.Fprintf(os.Stderr, stack.String()) + } else if t.Lexeme == "dup" { + top, err := stack.Peek() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot duplicate an empty stack.\n", t.Line, t.Column)) + } + stack.Push(top) + } else if t.Lexeme == "over" { + stackLen := len(*stack) + if stackLen < 2 { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'over' operation on a stack with less than two items.\n", t.Line, t.Column)) + } + + obj := (*stack)[stackLen-2] + stack.Push(obj) + } else if t.Lexeme == "swap" { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'swap' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'swap' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + stack.Push(obj1) + stack.Push(obj2) + } else if t.Lexeme == "drop" { + _, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot drop an empty stack.\n", t.Line, t.Column)) + } + } else if t.Lexeme == "rot" { + // Check that there are at least 3 items on the stack + stackLen := len(*stack) + if stackLen < 3 { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'rot' operation on a stack with less than three items.\n", t.Line, t.Column)) + } + top, _ := stack.Pop() + second, _ := stack.Pop() + third, _ := stack.Pop() + stack.Push(second) + stack.Push(top) + stack.Push(third) + } else if t.Lexeme == "-rot" { + // Check that there are at least 3 items on the stack + if len(*stack) < 3 { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'rot' operation on a stack with less than three items.\n", t.Line, t.Column)) + } + top, _ := stack.Pop() + second, _ := stack.Pop() + third, _ := stack.Pop() + stack.Push(top) + stack.Push(third) + stack.Push(second) + } else if t.Lexeme == "nip" { + if len(*stack) < 2 { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nip' operation on a stack with less than two items.\n", t.Line, t.Column)) + } + top, _ := stack.Pop() + _, _ = stack.Pop() + stack.Push(top) + } else if t.Lexeme == "glob" { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'glob' operation on an empty stack.\n", t.Line, t.Column)) + } + + // Can be a string or literal + var globStr string + switch obj1.(type) { + case *MShellString: + globStr = obj1.(*MShellString).Content + case *MShellLiteral: + globStr = obj1.(*MShellLiteral).LiteralText + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot glob a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + + files, err := filepath.Glob(globStr) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Malformed glob pattern: %s\n", t.Line, t.Column, err.Error())) + } + + newList := &MShellList{Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE} + for _, file := range files { + newList.Items = append(newList.Items, &MShellString{file}) + } + stack.Push(newList) + } else if t.Lexeme == "stdin" { + // Dump all of current stdin onto the stack as a string + var buffer bytes.Buffer + _, err := buffer.ReadFrom(context.StandardInput) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(&MShellString{buffer.String()}) + } else if t.Lexeme == "append" { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'append' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'append' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + // Can do append with list and object in either order. If two lists, append obj1 into obj2 + switch obj1.(type) { + case *MShellList: + switch obj2.(type) { + case *MShellList: + obj2.(*MShellList).Items = append(obj2.(*MShellList).Items, obj1) + stack.Push(obj2) + default: + obj1.(*MShellList).Items = append(obj1.(*MShellList).Items, obj2) + stack.Push(obj1) + } + default: + switch obj2.(type) { + case *MShellList: + obj2.(*MShellList).Items = append(obj2.(*MShellList).Items, obj1) + stack.Push(obj2) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot append a %s to a %s.\n", t.Line, t.Column, obj1.TypeName(), obj2.TypeName())) + } + } + } else if t.Lexeme == "args" { + // Dump the positional arguments onto the stack as a list of strings + newList := &MShellList{Items: make([]MShellObject, len(state.PositionalArgs)), StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE} + for i, arg := range state.PositionalArgs { + newList.Items[i] = &MShellString{arg} + } + stack.Push(newList) + } else if t.Lexeme == "len" { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'len' operation on an empty stack.\n", t.Line, t.Column)) + } + + switch obj.(type) { + case *MShellList: + stack.Push(&MShellInt{len(obj.(*MShellList).Items)}) + case *MShellString: + stack.Push(&MShellInt{len(obj.(*MShellString).Content)}) + case *MShellLiteral: + stack.Push(&MShellInt{len(obj.(*MShellLiteral).LiteralText)}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot get length of a %s.\n", t.Line, t.Column, obj.TypeName())) + } + } else if t.Lexeme == "nth" { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + int1, ok := obj1.(*MShellInt) + if ok { + result, err := obj2.Index(int1.Value) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(result) + } else { + int2, ok := obj2.(*MShellInt) + if ok { + result, err := obj1.Index(int2.Value) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(result) + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'nth' with a %s and a %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) + } + } + } else if t.Lexeme == "pick" { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' operation on an empty stack.\n", t.Line, t.Column)) + } + // Check that obj1 is an integer + int1, ok := obj1.(*MShellInt) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' with a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + + // Check that int is greater than or equal to 1 + if int1.Value < 1 { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' with a value less than 1.\n", t.Line, t.Column)) + } + + // Check that the stack has enough items + if int1.Value > len(*stack) { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'pick' on a stack with less than %d items.\n", t.Line, t.Column, int1.Value+1)) + } + + // Duplicate the nth item on the stack + // a b c 2 -> a b c b + stack.Push((*stack)[len(*stack)-int1.Value]) + } else if t.Lexeme == "w" || t.Lexeme == "wl" || t.Lexeme == "we" || t.Lexeme == "wle" { + // Print the top of the stack to the console. + top, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot write an empty stack.\n", t.Line, t.Column)) + } + + var writer io.Writer + if t.Lexeme == "we" || t.Lexeme == "wle" { + writer = os.Stderr + } else { + writer = os.Stdout + } + + switch top.(type) { + case *MShellLiteral: + fmt.Fprintf(writer, "%s", top.(*MShellLiteral).LiteralText) + case *MShellString: + fmt.Fprintf(writer, "%s", top.(*MShellString).Content) + case *MShellInt: + fmt.Fprintf(writer, "%d", top.(*MShellInt).Value) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot write a %s.\n", t.Line, t.Column, top.TypeName())) + } + + if t.Lexeme == "wl" || t.Lexeme == "wle" { + fmt.Fprintf(writer, "\n") + } + } else if t.Lexeme == "findReplace" { + // Do simple find replace with the top three strings on stack + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + obj3, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'find-replace' operation on a stack with only two items.\n", t.Line, t.Column)) + } + + switch obj1.(type) { + case *MShellString: + switch obj2.(type) { + case *MShellString: + switch obj3.(type) { + case *MShellString: + stack.Push(&MShellString{strings.Replace(obj3.(*MShellString).Content, obj2.(*MShellString).Content, obj1.(*MShellString).Content, -1)}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj3.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot find-replace a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + + } else if t.Lexeme == "split" { + delimiter, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'split' operation on an empty stack.\n", t.Line, t.Column)) + } + + strLiteral, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'split' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + var delimiterStr string + var strToSplit string + + switch delimiter.(type) { + case *MShellString: + delimiterStr = delimiter.(*MShellString).Content + case *MShellLiteral: + delimiterStr = delimiter.(*MShellLiteral).LiteralText + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot split with a %s.\n", t.Line, t.Column, delimiter.TypeName())) + } + + switch strLiteral.(type) { + case *MShellString: + strToSplit = strLiteral.(*MShellString).Content + case *MShellLiteral: + strToSplit = strLiteral.(*MShellLiteral).LiteralText + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot split a %s.\n", t.Line, t.Column, strLiteral.TypeName())) + } + + split := strings.Split(strToSplit, delimiterStr) + newList := &MShellList{Items: make([]MShellObject, len(split)), StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE} + for i, item := range split { + newList.Items[i] = &MShellString{item} + } + stack.Push(newList) + } else if t.Lexeme == "join" { + delimiter, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'join' operation on an empty stack.\n", t.Line, t.Column)) + } + + list, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do 'join' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + var delimiterStr string + var listItems []string + + switch delimiter.(type) { + case *MShellString: + delimiterStr = delimiter.(*MShellString).Content + case *MShellLiteral: + delimiterStr = delimiter.(*MShellLiteral).LiteralText + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join with a %s.\n", t.Line, t.Column, delimiter.TypeName())) + } + + switch list.(type) { + case *MShellList: + for _, item := range list.(*MShellList).Items { + switch item.(type) { + case *MShellString: + listItems = append(listItems, item.(*MShellString).Content) + case *MShellLiteral: + listItems = append(listItems, item.(*MShellLiteral).LiteralText) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join a list with a %s inside (%s).\n", t.Line, t.Column, item.TypeName(), item.DebugString())) + } + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot join a %s (%s).\n", t.Line, t.Column, list.TypeName(), list.DebugString())) + } + + stack.Push(&MShellString{strings.Join(listItems, delimiterStr)}) + } else if t.Lexeme == "lines" { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot evaluate 'lines' on an empty stack.\n", t.Line, t.Column)) + } + + s1, ok := obj.(*MShellString) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot evaluate 'lines' on a %s.\n", t.Line, t.Column, obj.TypeName())) + } + + // TODO: Maybe reuse a scanner? + scanner := bufio.NewScanner(strings.NewReader(s1.Content)) + newList := &MShellList{Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE} + for scanner.Scan() { + newList.Items = append(newList.Items, &MShellString{scanner.Text()}) + } + + stack.Push(newList) + } else if t.Lexeme == "~" || strings.HasPrefix(t.Lexeme, "~/") { + // Only do tilde expansion + homeDir, err := os.UserHomeDir() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error expanding ~: %s\n", t.Line, t.Column, err.Error())) + } + + var tildeExpanded string + if t.Lexeme == "~" { + tildeExpanded = homeDir + } else { + tildeExpanded = filepath.Join(homeDir, t.Lexeme[2:]) + } + + stack.Push(&MShellString{tildeExpanded}) + } else { + stack.Push(&MShellLiteral{t.Lexeme}) + } + } else if t.Type == LEFT_SQUARE_BRACKET { + return FailWithMessage(fmt.Sprintf("%d:%d: Found unexpected left square bracket.\n", t.Line, t.Column)) + } else if t.Type == LEFT_PAREN { + return FailWithMessage(fmt.Sprintf("%d:%d: Found unexpected left parenthesis.\n", t.Line, t.Column)) + } else if t.Type == EXECUTE || t.Type == QUESTION { + top, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot execute an empty stack.\n", t.Line, t.Column)) + } + + // Switch on type + var result EvalResult + var exitCode int + var stdout string + + switch top.(type) { + case *MShellList: + result, exitCode, stdout = RunProcess(*top.(*MShellList), context, state) + case *MShellPipe: + result, exitCode = state.RunPipeline(*top.(*MShellPipe), context, stack) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot execute a non-list object. Found %s %s\n", t.Line, t.Column, top.TypeName(), top.DebugString())) + } + + if state.StopOnError && exitCode != 0 { + // Exit completely, with that exit code, don't need to print a different message. Usually the command itself will have printed an error. + return EvalResult{false, -1, exitCode} + } + + if !result.Success { + return result + } + + asList, ok := top.(*MShellList) + if ok { + if asList.StdoutBehavior == STDOUT_LINES { + newMShellList := &MShellList{Items: []MShellObject{}, StandardInputFile: "", StandardOutputFile: "", StdoutBehavior: STDOUT_NONE} + var scanner *bufio.Scanner + scanner = bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + newMShellList.Items = append(newMShellList.Items, &MShellString{scanner.Text()}) + } + stack.Push(newMShellList) + } else if asList.StdoutBehavior == STDOUT_STRIPPED { + stripped := strings.TrimSpace(stdout) + stack.Push(&MShellString{stripped}) + } else if asList.StdoutBehavior == STDOUT_COMPLETE { + stack.Push(&MShellString{stdout}) + } + } + + // Push the exit code onto the stack if a question was used to execute + if t.Type == QUESTION { + stack.Push(&MShellInt{exitCode}) + } + } else if t.Type == TRUE { + stack.Push(&MShellBool{true}) + } else if t.Type == FALSE { + stack.Push(&MShellBool{false}) + } else if t.Type == INTEGER { + intVal, err := strconv.Atoi(t.Lexeme) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing integer: %s\n", t.Line, t.Column, err.Error())) + } + + stack.Push(&MShellInt{intVal}) + } else if t.Type == STRING { + parsedString, err := ParseRawString(t.Lexeme) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing string: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(&MShellString{parsedString}) + } else if t.Type == SINGLEQUOTESTRING { + stack.Push(&MShellString{t.Lexeme[1 : len(t.Lexeme)-1]}) + } else if t.Type == IF { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do an 'if' on an empty stack.\n", t.Line, t.Column)) + } + + list, ok := obj.(*MShellList) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Argument for if expected to be a list of quoations, received a %s\n", t.Line, t.Column, obj.TypeName())) + } + + if len(list.Items) < 2 { + return FailWithMessage(fmt.Sprintf("%d:%d: If statement requires a list with at least 2 items. Found %d.\n", t.Line, t.Column, len(list.Items))) + } + + // Check that all items are quotations + for i, item := range list.Items { + if _, ok := item.(*MShellQuotation); !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Item %d in if statement is not a quotation.\n", t.Line, t.Column, i)) + } + } + + trueIndex := -1 + + ListLoop: + for i := 0; i < len(list.Items)-1; i += 2 { + quotation := list.Items[i].(*MShellQuotation) + result := state.Evaluate(quotation.Tokens, stack, context, definitions) + + if !result.Success { + return result + } + + if result.BreakNum > 0 { + return FailWithMessage("Encountered break within if statement.\n") + } + + top, err := stack.Pop() + if err != nil { + conditionNum := i/2 + 1 + return FailWithMessage(fmt.Sprintf("%d:%d: Found an empty stack when evaluating condition #%d .\n", t.Line, t.Column, conditionNum)) + } + + // Check for either integer or boolean + switch top.(type) { + case *MShellInt: + if top.(*MShellInt).Value == 0 { + trueIndex = i + break ListLoop + } + case *MShellBool: + if top.(*MShellBool).Value { + trueIndex = i + break ListLoop + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Expected an integer or boolean for condition #%d, received a %s.\n", t.Line, t.Column, i/2+1, top.TypeName())) + } + } + + if trueIndex > -1 { + quotation := list.Items[trueIndex+1].(*MShellQuotation) + result := state.Evaluate(quotation.Tokens, stack, context, definitions) + + // If we encounter a break, we should return it up the stack + if !result.Success || result.BreakNum != -1 { + return result + } + } else if len(list.Items)%2 == 1 { // Try to find a final else statement, will be the last item in the list if odd number of items + quotation := list.Items[len(list.Items)-1].(*MShellQuotation) + result := state.Evaluate(quotation.Tokens, stack, context, definitions) + + if !result.Success || result.BreakNum != -1 { + return result + } + } + } else if t.Type == PLUS { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '+' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '+' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + switch obj1.(type) { + case *MShellInt: + switch obj2.(type) { + case *MShellInt: + stack.Push(&MShellInt{obj2.(*MShellInt).Value + obj1.(*MShellInt).Value}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add an integer to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + case *MShellString: + switch obj2.(type) { + case *MShellString: + stack.Push(&MShellString{obj2.(*MShellString).Content + obj1.(*MShellString).Content}) + case *MShellLiteral: + stack.Push(&MShellString{obj2.(*MShellLiteral).LiteralText + obj1.(*MShellString).Content}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add a string to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + case *MShellLiteral: + switch obj2.(type) { + case *MShellString: + stack.Push(&MShellString{obj2.(*MShellString).Content + obj1.(*MShellLiteral).LiteralText}) + case *MShellLiteral: + stack.Push(&MShellString{obj2.(*MShellLiteral).LiteralText + obj1.(*MShellLiteral).LiteralText}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot add a literal to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '+' to a %s to a %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) + } + } else if t.Type == MINUS { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '-' operation on an empty stack.\n", t.Line, t.Column)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '-' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + switch obj1.(type) { + case *MShellInt: + switch obj2.(type) { + case *MShellInt: + stack.Push(&MShellInt{obj2.(*MShellInt).Value - obj1.(*MShellInt).Value}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot subtract an integer from a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '-' to a %s and %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName())) + } + } else if t.Type == AND || t.Type == OR { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) + } + + switch obj1.(type) { + case *MShellBool: + switch obj2.(type) { + case *MShellBool: + if t.Type == AND { + stack.Push(&MShellBool{obj2.(*MShellBool).Value && obj1.(*MShellBool).Value}) + } else { + stack.Push(&MShellBool{obj2.(*MShellBool).Value || obj1.(*MShellBool).Value}) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) + } + } else if t.Type == NOT { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) + } + + switch obj.(type) { + case *MShellBool: + stack.Push(&MShellBool{!obj.(*MShellBool).Value}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s.\n", t.Line, t.Column, t.Lexeme, obj.TypeName())) + } + } else if t.Type == GREATERTHANOREQUAL || t.Type == LESSTHANOREQUAL { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) + } + + if obj1.IsNumeric() && obj2.IsNumeric() { + if t.Type == GREATERTHANOREQUAL { + stack.Push(&MShellBool{obj2.FloatNumeric() >= obj1.FloatNumeric()}) + } else { + stack.Push(&MShellBool{obj2.FloatNumeric() <= obj1.FloatNumeric()}) + } + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and a %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) + } + } else if t.Type == GREATERTHAN || t.Type == LESSTHAN { + // This can either be normal comparison for numerics, or it's a redirect on a list or quotation. + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) + } + + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme)) + } + + if obj1.IsNumeric() && obj2.IsNumeric() { + if t.Type == GREATERTHAN { + stack.Push(&MShellBool{obj2.FloatNumeric() > obj1.FloatNumeric()}) + } else { + stack.Push(&MShellBool{obj2.FloatNumeric() < obj1.FloatNumeric()}) + } + } else { + switch obj1.(type) { + case *MShellString: + switch obj2.(type) { + case *MShellList: + if t.Type == GREATERTHAN { + obj2.(*MShellList).StandardOutputFile = obj1.(*MShellString).Content + } else { + obj2.(*MShellList).StandardInputFile = obj1.(*MShellString).Content + } + stack.Push(obj2) + case *MShellQuotation: + if t.Type == GREATERTHAN { + obj2.(*MShellQuotation).StandardOutputFile = obj1.(*MShellString).Content + } else { + obj2.(*MShellQuotation).StandardInputFile = obj1.(*MShellString).Content + } + stack.Push(obj2) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect a string to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + case *MShellLiteral: + switch obj2.(type) { + case *MShellList: + if t.Type == GREATERTHAN { + obj2.(*MShellList).StandardOutputFile = obj1.(*MShellLiteral).LiteralText + } else { + obj2.(*MShellList).StandardInputFile = obj1.(*MShellLiteral).LiteralText + } + stack.Push(obj2) + case *MShellQuotation: + if t.Type == GREATERTHAN { + obj2.(*MShellQuotation).StandardOutputFile = obj1.(*MShellLiteral).LiteralText + } else { + obj2.(*MShellQuotation).StandardInputFile = obj1.(*MShellLiteral).LiteralText + } + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect a %s to a %s.\n", t.Line, t.Column, obj1.TypeName(), obj2.TypeName())) + } + } + } else if t.Type == STDERRREDIRECT { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr on an empty stack.\n", t.Line, t.Column)) + } + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr on a stack with only one item.\n", t.Line, t.Column)) + } + + switch obj1.(type) { + case *MShellString: + switch obj2.(type) { + case *MShellList: + obj2.(*MShellList).StandardErrorFile = obj1.(*MShellString).Content + stack.Push(obj2) + case *MShellQuotation: + obj2.(*MShellQuotation).StandardErrorFile = obj1.(*MShellString).Content + stack.Push(obj2) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + case *MShellLiteral: + switch obj2.(type) { + case *MShellList: + obj2.(*MShellList).StandardErrorFile = obj1.(*MShellLiteral).LiteralText + stack.Push(obj2) + case *MShellQuotation: + obj2.(*MShellQuotation).StandardErrorFile = obj1.(*MShellLiteral).LiteralText + stack.Push(obj2) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot redirect stderr to a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + } else if t.Type == VARSTORE { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Nothing on stack to store into variable %s.\n", t.Line, t.Column, t.Lexeme[1:])) + } + + state.Variables[t.Lexeme[1:]] = obj + } else if t.Type == VARRETRIEVE { + name := t.Lexeme[:len(t.Lexeme)-1] + obj, found_mshell_variable := state.Variables[name] + if found_mshell_variable { + stack.Push(obj) + } else { + // Check current environment variables + envValue, ok := os.LookupEnv(name) + if ok { + stack.Push(&MShellString{envValue}) + } else { + var message strings.Builder + message.WriteString(fmt.Sprintf("%d:%d: Variable %s not found.\n", t.Line, t.Column, name)) + message.WriteString("Variables:\n") + for key := range state.Variables { + message.WriteString(fmt.Sprintf(" %s\n", key)) + } + return FailWithMessage(message.String()) + } + } + } else if t.Type == LOOP { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do a loop on an empty stack.\n", t.Line, t.Column)) + } + + quotation, ok := obj.(*MShellQuotation) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Argument for loop expected to be a quotation, received a %s\n", t.Line, t.Column, obj.TypeName())) + } + + if len(quotation.Tokens) == 0 { + return FailWithMessage(fmt.Sprintf("%d:%d: Loop quotation needs a minimum of one token.\n", t.Line, t.Column)) + } + + context := ExecuteContext{ + StandardInput: nil, + StandardOutput: nil, + } + + if quotation.StandardInputFile != "" { + file, err := os.Open(quotation.StandardInputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for reading: %s\n", t.Line, t.Column, quotation.StandardInputFile, err.Error())) + } + context.StandardInput = file + defer file.Close() + } + + if quotation.StandardOutputFile != "" { + file, err := os.Create(quotation.StandardOutputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for writing: %s\n", t.Line, t.Column, quotation.StandardOutputFile, err.Error())) + } + context.StandardOutput = file + defer file.Close() + } + + maxLoops := 15000 + loopCount := 0 + state.LoopDepth++ + + breakDiff := 0 + + for loopCount < maxLoops { + result := state.Evaluate(quotation.Tokens, stack, context, definitions) + + if !result.Success { + return result + } + + if result.BreakNum >= 0 { + breakDiff = state.LoopDepth - result.BreakNum + if breakDiff >= 0 { + break + } + } + + loopCount++ + } + + if loopCount == maxLoops { + return FailWithMessage(fmt.Sprintf("%d:%d: Loop exceeded maximum number of iterations.\n", t.Line, t.Column)) + } + + state.LoopDepth-- + + if breakDiff > 0 { + return EvalResult{true, breakDiff - 1, 0} + } + } else if t.Type == BREAK { + return EvalResult{true, 1, 0} + } else if t.Type == EQUALS { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '=' operation on an empty stack.\n", t.Line, t.Column)) + } + obj2, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '=' operation on a stack with only one item.\n", t.Line, t.Column)) + } + + // Implement for integers right now. + switch obj1.(type) { + case *MShellInt: + switch obj2.(type) { + case *MShellInt: + stack.Push(&MShellBool{obj2.(*MShellInt).Value == obj1.(*MShellInt).Value}) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot compare an integer to a %s.\n", t.Line, t.Column, obj2.TypeName())) + } + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot complete %s with a %s to a %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName())) + } + } else if t.Type == INTERPRET { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot interpret an empty stack.\n", t.Line, t.Column)) + } + + quotation, ok := obj.(*MShellQuotation) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Argument for interpret expected to be a quotation, received a %s\n", t.Line, t.Column, obj.TypeName())) + } + + quoteContext := ExecuteContext{ + StandardInput: nil, + StandardOutput: nil, + } + + if quotation.StandardInputFile != "" { + file, err := os.Open(quotation.StandardInputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for reading: %s\n", t.Line, t.Column, quotation.StandardInputFile, err.Error())) + } + quoteContext.StandardInput = file + defer file.Close() + } else if context.StandardInput != nil { + quoteContext.StandardInput = context.StandardInput + } else { + // Default to stdin of this process itself + quoteContext.StandardInput = os.Stdin + } + + if quotation.StandardOutputFile != "" { + file, err := os.Create(quotation.StandardOutputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error opening file %s for writing: %s\n", t.Line, t.Column, quotation.StandardOutputFile, err.Error())) + } + quoteContext.StandardOutput = file + defer file.Close() + } else if context.StandardOutput != nil { + quoteContext.StandardOutput = context.StandardOutput + } else { + // Default to stdout of this process itself + quoteContext.StandardOutput = os.Stdout + } + + result := state.Evaluate(quotation.Tokens, stack, quoteContext, definitions) + if !result.Success { + return result + } + + if result.BreakNum > 0 { + return result + } + } else if t.Type == POSITIONAL { + posNum := t.Lexeme[1:] + posIndex, err := strconv.Atoi(posNum) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing positional argument number: %s\n", t.Line, t.Column, err.Error())) + } + + if posIndex == 0 { + return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument are 1-based, first argument is $1, not $0.\n", t.Line, t.Column)) + } + + if posIndex < 0 { + return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument numbers must be positive.\n", t.Line, t.Column)) + } + + if posIndex > len(state.PositionalArgs) { + return FailWithMessage(fmt.Sprintf("%d:%d: Positional argument %s is greater than the number of arguments provided.\n", t.Line, t.Column, t.Lexeme)) + } + + stack.Push(&MShellString{state.PositionalArgs[posIndex-1]}) + } else if t.Type == PIPE { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme)) + } + + // obj1 should be a list + list, ok := obj1.(*MShellList) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot pipe a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + + stack.Push(&MShellPipe{*list}) + } else if t.Type == READ { + var reader io.Reader + // Check if what we are reading from is seekable. If so, we can do a buffered read and reset the position. + // Else, we have to read byte by byte. + + isSeekable := false + + if context.StandardInput == nil { + reader = os.Stdin + _, err := reader.(*os.File).Seek(0, io.SeekCurrent) + isSeekable = err == nil + + } else { + reader = context.StandardInput + _, err := reader.(*os.File).Seek(0, io.SeekCurrent) + isSeekable = err == nil + } + + if isSeekable { + // Do a buffered read + bufferedReader := bufio.NewReader(reader) + line, err := bufferedReader.ReadString('\n') + if err != nil { + if err == io.EOF { + stack.Push(&MShellString{""}) + stack.Push(&MShellBool{false}) + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) + } + } else { + // Check if the last character is a '\r' and remove it if it is. Also remove the '\n' itself + if len(line) > 0 && line[len(line)-1] == '\n' { + line = line[:len(line)-1] + } + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + + stack.Push(&MShellString{line}) + stack.Push(&MShellBool{true}) + } + + // Reset the position of the reader to the position after the read + offset, err := reader.(*os.File).Seek(0, io.SeekCurrent) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error resetting position of reader: %s\n", t.Line, t.Column, err.Error())) + } + remainingInBuffer := bufferedReader.Buffered() + // fmt.Fprintf(os.Stderr, "Offset: %d, Remaining in buffer: %d\n", offset, remainingInBuffer) + newPosition := offset - int64(remainingInBuffer) + // fmt.Fprintf(os.Stderr, "New position: %d\n", newPosition) + _, err = reader.(*os.File).Seek(newPosition, io.SeekStart) + } else { + // Do a byte by byte read + var line strings.Builder + for { + b := make([]byte, 1) + _, err := reader.Read(b) + if err != nil { + if err == io.EOF { + // If nothing in line, then this was the end of the file + if line.Len() == 0 { + stack.Push(&MShellString{""}) + stack.Push(&MShellBool{false}) + } else { + // Else, we have a final that wasn't terminated by a newline. Still try to remove '\r' if it's there + builderStr := line.String() + if len(builderStr) > 0 && builderStr[len(builderStr)-1] == '\r' { + builderStr = builderStr[:len(builderStr)-1] + } + stack.Push(&MShellString{builderStr}) + stack.Push(&MShellBool{true}) + } + break + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: Error reading from stdin: %s\n", t.Line, t.Column, err.Error())) + } + } + + if b[0] == '\n' { + builderStr := line.String() + + // Check if the last character is a '\r' and remove it if it is + if len(builderStr) > 0 && builderStr[len(builderStr)-1] == '\r' { + builderStr = builderStr[:len(builderStr)-1] + } + + stack.Push(&MShellString{builderStr}) + stack.Push(&MShellBool{true}) + break + } else { + line.WriteByte(b[0]) + } + } + } + } else if t.Type == STR { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot convert an empty stack to a string.\n", t.Line, t.Column)) + } + + stack.Push(&MShellString{obj.ToString()}) + } else if t.Type == INDEXER { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot index an empty stack.\n", t.Line, t.Column)) + } + + // Indexer is a digit between ':' and ':'. Remove ends and parse the number + indexStr := t.Lexeme[1 : len(t.Lexeme)-1] + index, err := strconv.Atoi(indexStr) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing index: %s\n", t.Line, t.Column, err.Error())) + } + + result, err := obj1.Index(index) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: %s", t.Line, t.Column, err.Error())) + } + stack.Push(result) + } else if t.Type == ENDINDEXER || t.Type == STARTINDEXER { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot end index an empty stack.\n", t.Line, t.Column)) + } + + var indexStr string + // Parse the index value + if t.Type == ENDINDEXER { + indexStr = t.Lexeme[1:] + } else { + indexStr = t.Lexeme[:len(t.Lexeme)-1] + } + + index, err := strconv.Atoi(indexStr) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing index: %s\n", t.Line, t.Column, err.Error())) + } + + var result MShellObject + if t.Type == ENDINDEXER { + result, err = obj1.SliceEnd(index) + } else { + result, err = obj1.SliceStart(index) + } + + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: %s", t.Line, t.Column, err.Error())) + } + stack.Push(result) + } else if t.Type == SLICEINDEXER { + obj1, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot slice index an empty stack.\n", t.Line, t.Column)) + } + + // StartInc:EndExc + parts := strings.Split(t.Lexeme, ":") + startInt, err := strconv.Atoi(parts[0]) + endInt, err2 := strconv.Atoi(parts[1]) + + if err != nil || err2 != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Error parsing slice indexes: %s\n", t.Line, t.Column, err.Error())) + } + + result, err := obj1.Slice(startInt, endInt) + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot slice index a %s.\n", t.Line, t.Column, obj1.TypeName())) + } + + stack.Push(result) + } else if t.Type == STDOUTLINES || t.Type == STDOUTSTRIPPED || t.Type == STDOUTCOMPLETE { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot set stdout behavior to lines on an empty stack.\n", t.Line, t.Column)) + } + + list, ok := obj.(*MShellList) + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot set stdout behavior to lines on a %s.\n", t.Line, t.Column, obj.TypeName())) + } + + if t.Type == STDOUTLINES { + list.StdoutBehavior = STDOUT_LINES + } else if t.Type == STDOUTSTRIPPED { + list.StdoutBehavior = STDOUT_STRIPPED + } else if t.Type == STDOUTCOMPLETE { + list.StdoutBehavior = STDOUT_COMPLETE + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: We haven't implemented the token type '%s' yet.\n", t.Line, t.Column, t.Type)) + } + stack.Push(list) + } else if t.Type == EXPORT { + obj, err := stack.Pop() + if err != nil { + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export an empty stack.\n", t.Line, t.Column)) + } + + var varName string + switch obj.(type) { + case *MShellString: + varName = obj.(*MShellString).Content + case *MShellLiteral: + varName = obj.(*MShellLiteral).LiteralText + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export a %s.\n", t.Line, t.Column, obj.TypeName())) + } + + // Check that varName is in state.Variables, and varName is string or literal + varValue, ok := state.Variables[varName] + if !ok { + return FailWithMessage(fmt.Sprintf("%d:%d: Variable %s not found in state.Variables.\n", t.Line, t.Column, varName)) + } + + switch varValue.(type) { + case *MShellString: + os.Setenv(varName, varValue.(*MShellString).Content) + case *MShellLiteral: + os.Setenv(varName, varValue.(*MShellLiteral).LiteralText) + default: + return FailWithMessage(fmt.Sprintf("%d:%d: Cannot export a %s as an environment variable.\n", t.Line, t.Column, varValue.TypeName())) + } + } else if t.Type == STOP_ON_ERROR { + state.StopOnError = true + } else { + return FailWithMessage(fmt.Sprintf("%d:%d: We haven't implemented the token type '%s' yet.\n", t.Line, t.Column, t.Type)) + } + + default: + return FailWithMessage(fmt.Sprintf("We haven't implemented the type '%T' yet.\n", t)) + } + + } + + return EvalResult{true, -1, 0} } type Executable interface { - Execute(state *EvalState, context ExecuteContext, stack *MShellStack) (EvalResult, int) - GetStandardInputFile() string - GetStandardOutputFile() string + Execute(state *EvalState, context ExecuteContext, stack *MShellStack) (EvalResult, int) + GetStandardInputFile() string + GetStandardOutputFile() string } - func (list *MShellList) Execute(state *EvalState, context ExecuteContext, stack *MShellStack) (EvalResult, int) { - result, exitCode, _ := RunProcess(*list, context, state) - return result, exitCode + result, exitCode, _ := RunProcess(*list, context, state) + return result, exitCode } func (quotation *MShellQuotation) Execute(state *EvalState, context ExecuteContext, stack *MShellStack, definitions []MShellDefinition) (EvalResult, int) { - quotationContext := ExecuteContext { - StandardInput: nil, - StandardOutput: nil, - } - - if quotation.StandardInputFile != "" { - file, err := os.Open(quotation.StandardInputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", quotation.StandardInputFile, err.Error())), 1 - } - quotationContext.StandardInput = file - defer file.Close() - } else if context.StandardInput != nil { - quotationContext.StandardInput = context.StandardInput - } else { - // Default to stdin of this process itself - quotationContext.StandardInput = os.Stdin - } - - if quotation.StandardOutputFile != "" { - file, err := os.Create(quotation.StandardOutputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", quotation.StandardOutputFile, err.Error())), 1 - } - quotationContext.StandardOutput = file - defer file.Close() - } else if context.StandardOutput != nil { - quotationContext.StandardOutput = context.StandardOutput - } else { - // Default to stdout of this process itself - quotationContext.StandardOutput = os.Stdout - } - - result := state.Evaluate(quotation.Tokens, stack, quotationContext, definitions) - if !result.Success { - return result, 1 - } else { - return SimpleSuccess(), 0 - } + quotationContext := ExecuteContext{ + StandardInput: nil, + StandardOutput: nil, + } + + if quotation.StandardInputFile != "" { + file, err := os.Open(quotation.StandardInputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", quotation.StandardInputFile, err.Error())), 1 + } + quotationContext.StandardInput = file + defer file.Close() + } else if context.StandardInput != nil { + quotationContext.StandardInput = context.StandardInput + } else { + // Default to stdin of this process itself + quotationContext.StandardInput = os.Stdin + } + + if quotation.StandardOutputFile != "" { + file, err := os.Create(quotation.StandardOutputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", quotation.StandardOutputFile, err.Error())), 1 + } + quotationContext.StandardOutput = file + defer file.Close() + } else if context.StandardOutput != nil { + quotationContext.StandardOutput = context.StandardOutput + } else { + // Default to stdout of this process itself + quotationContext.StandardOutput = os.Stdout + } + + result := state.Evaluate(quotation.Tokens, stack, quotationContext, definitions) + if !result.Success { + return result, 1 + } else { + return SimpleSuccess(), 0 + } } - func (list *MShellList) GetStandardInputFile() string { - return list.StandardInputFile + return list.StandardInputFile } func (list *MShellList) GetStandardOutputFile() string { - return list.StandardOutputFile + return list.StandardOutputFile } func (quotation *MShellQuotation) GetStandardInputFile() string { - return quotation.StandardInputFile + return quotation.StandardInputFile } func (quotation *MShellQuotation) GetStandardOutputFile() string { - return quotation.StandardOutputFile + return quotation.StandardOutputFile } - func RunProcess(list MShellList, context ExecuteContext, state *EvalState) (EvalResult, int, string) { - // Check for empty list - if len(list.Items) == 0 { - return FailWithMessage("Cannot execute an empty list.\n"), 1, "" - } - - commandLineArgs := make([]string, 0) - var commandLineQueue []MShellObject - - // Add all items to the queue, first in is the end of the slice, so add in reverse order - for i := len(list.Items) - 1; i >= 0; i-- { - commandLineQueue = append(commandLineQueue, list.Items[i]) - } - - for len(commandLineQueue) > 0 { - item := commandLineQueue[len(commandLineQueue) - 1] - commandLineQueue = commandLineQueue[:len(commandLineQueue) - 1] - - if innerList, ok := item.(*MShellList); ok { - // Add to queue, first in is the end of the slice, so add in reverse order - for i := len(innerList.Items) - 1; i >= 0; i-- { - commandLineQueue = append(commandLineQueue, innerList.Items[i]) - } - } else if !item.IsCommandLineable() { - return FailWithMessage(fmt.Sprintf("Item (%s) cannot be used as a command line argument.\n", item.DebugString())), 1, "" - } else { - commandLineArgs = append(commandLineArgs, item.CommandLine()) - } - } - - cmd := exec.Command(commandLineArgs[0], commandLineArgs[1:]...) - cmd.Env = os.Environ() - - var commandSubWriter bytes.Buffer - // TBD: Should we allow command substituation and redirection at the same time? - // Probably more hassle than worth including, with probable workarounds for that rare case. - if list.StdoutBehavior != STDOUT_NONE { - cmd.Stdout = &commandSubWriter - } else if list.StandardOutputFile != "" { - // Open the file for writing - file, err := os.Create(list.StandardOutputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", list.StandardOutputFile, err.Error())), 1, "" - } - cmd.Stdout = file - defer file.Close() - } else if context.StandardOutput != nil { - cmd.Stdout = context.StandardOutput - } else { - // Default to stdout of this process itself - cmd.Stdout = os.Stdout - } - - if list.StandardInputFile != "" { - // Open the file for reading - file, err := os.Open(list.StandardInputFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", list.StandardInputFile, err.Error())), 1, "" - } - cmd.Stdin = file - defer file.Close() - } else if context.StandardInput != nil { - cmd.Stdin = context.StandardInput - - // Print position of reader - // position, err := cmd.Stdin.(*os.File).Seek(0, io.SeekCurrent) - // if err != nil { - // return FailWithMessage(fmt.Sprintf("Error getting position of reader: %s\n", err.Error())), 1 - // } - // fmt.Fprintf(os.Stderr, "Position of reader: %d\n", position) - } else { - // Default to stdin of this process itself - cmd.Stdin = os.Stdin - } - - if list.StandardErrorFile != "" { - // Open the file for writing - file, err := os.Create(list.StandardErrorFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", list.StandardErrorFile, err.Error())), 1, "" - } - cmd.Stderr = file - defer file.Close() - } else { - cmd.Stderr = os.Stderr - } - - // fmt.Fprintf(os.Stderr, "Running command: %s\n", cmd.String()) - err := cmd.Run() // Manually deal with the exit code upstream - // fmt.Fprintf(os.Stderr, "Command finished\n") - var exitCode int - if err != nil { - if _, ok := err.(*exec.ExitError); !ok { - fmt.Fprintf(os.Stderr, "Error running command: %s\n", err.Error()) - exitCode = 1 - } else { - // Command exited with non-zero exit code - exitCode = err.(*exec.ExitError).ExitCode() - } - } else { - exitCode = cmd.ProcessState.ExitCode() - } - - // fmt.Fprintf(os.Stderr, "Exit code: %d\n", exitCode) - - if list.StdoutBehavior != STDOUT_NONE { - return SimpleSuccess(), exitCode, commandSubWriter.String() - } else { - return SimpleSuccess(), exitCode, "" - } + // Check for empty list + if len(list.Items) == 0 { + return FailWithMessage("Cannot execute an empty list.\n"), 1, "" + } + + commandLineArgs := make([]string, 0) + var commandLineQueue []MShellObject + + // Add all items to the queue, first in is the end of the slice, so add in reverse order + for i := len(list.Items) - 1; i >= 0; i-- { + commandLineQueue = append(commandLineQueue, list.Items[i]) + } + + for len(commandLineQueue) > 0 { + item := commandLineQueue[len(commandLineQueue)-1] + commandLineQueue = commandLineQueue[:len(commandLineQueue)-1] + + if innerList, ok := item.(*MShellList); ok { + // Add to queue, first in is the end of the slice, so add in reverse order + for i := len(innerList.Items) - 1; i >= 0; i-- { + commandLineQueue = append(commandLineQueue, innerList.Items[i]) + } + } else if !item.IsCommandLineable() { + return FailWithMessage(fmt.Sprintf("Item (%s) cannot be used as a command line argument.\n", item.DebugString())), 1, "" + } else { + commandLineArgs = append(commandLineArgs, item.CommandLine()) + } + } + + cmd := exec.Command(commandLineArgs[0], commandLineArgs[1:]...) + cmd.Env = os.Environ() + + var commandSubWriter bytes.Buffer + // TBD: Should we allow command substituation and redirection at the same time? + // Probably more hassle than worth including, with probable workarounds for that rare case. + if list.StdoutBehavior != STDOUT_NONE { + cmd.Stdout = &commandSubWriter + } else if list.StandardOutputFile != "" { + // Open the file for writing + file, err := os.Create(list.StandardOutputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", list.StandardOutputFile, err.Error())), 1, "" + } + cmd.Stdout = file + defer file.Close() + } else if context.StandardOutput != nil { + cmd.Stdout = context.StandardOutput + } else { + // Default to stdout of this process itself + cmd.Stdout = os.Stdout + } + + if list.StandardInputFile != "" { + // Open the file for reading + file, err := os.Open(list.StandardInputFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", list.StandardInputFile, err.Error())), 1, "" + } + cmd.Stdin = file + defer file.Close() + } else if context.StandardInput != nil { + cmd.Stdin = context.StandardInput + + // Print position of reader + // position, err := cmd.Stdin.(*os.File).Seek(0, io.SeekCurrent) + // if err != nil { + // return FailWithMessage(fmt.Sprintf("Error getting position of reader: %s\n", err.Error())), 1 + // } + // fmt.Fprintf(os.Stderr, "Position of reader: %d\n", position) + } else { + // Default to stdin of this process itself + cmd.Stdin = os.Stdin + } + + if list.StandardErrorFile != "" { + // Open the file for writing + file, err := os.Create(list.StandardErrorFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for writing: %s\n", list.StandardErrorFile, err.Error())), 1, "" + } + cmd.Stderr = file + defer file.Close() + } else { + cmd.Stderr = os.Stderr + } + + // fmt.Fprintf(os.Stderr, "Running command: %s\n", cmd.String()) + err := cmd.Run() // Manually deal with the exit code upstream + // fmt.Fprintf(os.Stderr, "Command finished\n") + var exitCode int + if err != nil { + if _, ok := err.(*exec.ExitError); !ok { + fmt.Fprintf(os.Stderr, "Error running command: %s\n", err.Error()) + exitCode = 1 + } else { + // Command exited with non-zero exit code + exitCode = err.(*exec.ExitError).ExitCode() + } + } else { + exitCode = cmd.ProcessState.ExitCode() + } + + // fmt.Fprintf(os.Stderr, "Exit code: %d\n", exitCode) + + if list.StdoutBehavior != STDOUT_NONE { + return SimpleSuccess(), exitCode, commandSubWriter.String() + } else { + return SimpleSuccess(), exitCode, "" + } } func (state *EvalState) RunPipeline(MShellPipe MShellPipe, context ExecuteContext, stack *MShellStack) (EvalResult, int) { - if len(MShellPipe.List.Items) == 0 { - return FailWithMessage("Cannot execute an empty pipe.\n"), 1 - } - - // Check that all list items are Executables - for i, item := range MShellPipe.List.Items { - if _, ok := item.(Executable); !ok { - return FailWithMessage(fmt.Sprintf("Item %d (%s) in pipe is not a list or a quotation.\n", i, item.DebugString())), 1 - } - } - - if len(MShellPipe.List.Items) == 1 { - // Just run the Execute on the first item - asExecutable, _ := MShellPipe.List.Items[0].(Executable) - return asExecutable.Execute(state, context, stack) - } - - // Have at least 2 items here, create pipeline of Executables, set up list of contexts - contexts := make([]ExecuteContext, len(MShellPipe.List.Items)) - - pipeReaders := make([]io.Reader, len(MShellPipe.List.Items) - 1) - pipeWriters := make([]io.Writer, len(MShellPipe.List.Items) - 1) - - // Set up pipes - for i := 0; i < len(MShellPipe.List.Items) - 1; i++ { - pipeReader, pipeWriter, err := os.Pipe() - if err != nil { - return FailWithMessage(fmt.Sprintf("Error creating pipe: %s\n", err.Error())), 1 - } - pipeReaders[i] = pipeReader - pipeWriters[i] = pipeWriter - } - - for i := 0; i < len(MShellPipe.List.Items); i++ { - newContext := ExecuteContext { - StandardInput: nil, - StandardOutput: nil, - } - - if i == 0 { - // Stdin should use the context of this function, or the file marked on the initial object - executableStdinFile := MShellPipe.List.Items[i].(Executable).GetStandardInputFile() - - if executableStdinFile != ""{ - file, err := os.Open(executableStdinFile) - if err != nil { - return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", executableStdinFile, err.Error())), 1 - } - newContext.StandardInput = file - defer file.Close() - } else if context.StandardInput != nil { - newContext.StandardInput = context.StandardInput - } else { - // Default to stdin of this process itself - newContext.StandardInput = os.Stdin - } - - newContext.StandardOutput = pipeWriters[0] - } else if i == len(MShellPipe.List.Items) - 1 { - // Stdout should use the context of this function - newContext.StandardInput = pipeReaders[len(pipeReaders) - 1] - newContext.StandardOutput = context.StandardOutput - } else { - newContext.StandardInput = pipeReaders[i - 1] - newContext.StandardOutput = pipeWriters[i] - } - - contexts[i] = newContext - } - - // Run the executables concurrently - var wg sync.WaitGroup - results := make([]EvalResult, len(MShellPipe.List.Items)) - exitCodes := make([]int, len(MShellPipe.List.Items)) - - for i, item := range MShellPipe.List.Items { - wg.Add(1) - go func(i int, item Executable) { - defer wg.Done() - // fmt.Fprintf(os.Stderr, "Running item %d\n", i) - results[i], exitCodes[i] = item.Execute(state, contexts[i], stack) - - // Close pipe ends that are no longer needed - if i > 0 { - pipeReaders[i-1].(io.Closer).Close() - } - if i < len(MShellPipe.List.Items)-1 { - pipeWriters[i].(io.Closer).Close() - } - }(i, item.(Executable)) - } - - // Wait for all processes to complete - wg.Wait() - - // Check for errors - for i, result := range results { - if !result.Success { - return result, exitCodes[i] - } - } - - // Return the exit code of the last item - return SimpleSuccess(), exitCodes[len(exitCodes) - 1] + if len(MShellPipe.List.Items) == 0 { + return FailWithMessage("Cannot execute an empty pipe.\n"), 1 + } + + // Check that all list items are Executables + for i, item := range MShellPipe.List.Items { + if _, ok := item.(Executable); !ok { + return FailWithMessage(fmt.Sprintf("Item %d (%s) in pipe is not a list or a quotation.\n", i, item.DebugString())), 1 + } + } + + if len(MShellPipe.List.Items) == 1 { + // Just run the Execute on the first item + asExecutable, _ := MShellPipe.List.Items[0].(Executable) + return asExecutable.Execute(state, context, stack) + } + + // Have at least 2 items here, create pipeline of Executables, set up list of contexts + contexts := make([]ExecuteContext, len(MShellPipe.List.Items)) + + pipeReaders := make([]io.Reader, len(MShellPipe.List.Items)-1) + pipeWriters := make([]io.Writer, len(MShellPipe.List.Items)-1) + + // Set up pipes + for i := 0; i < len(MShellPipe.List.Items)-1; i++ { + pipeReader, pipeWriter, err := os.Pipe() + if err != nil { + return FailWithMessage(fmt.Sprintf("Error creating pipe: %s\n", err.Error())), 1 + } + pipeReaders[i] = pipeReader + pipeWriters[i] = pipeWriter + } + + for i := 0; i < len(MShellPipe.List.Items); i++ { + newContext := ExecuteContext{ + StandardInput: nil, + StandardOutput: nil, + } + + if i == 0 { + // Stdin should use the context of this function, or the file marked on the initial object + executableStdinFile := MShellPipe.List.Items[i].(Executable).GetStandardInputFile() + + if executableStdinFile != "" { + file, err := os.Open(executableStdinFile) + if err != nil { + return FailWithMessage(fmt.Sprintf("Error opening file %s for reading: %s\n", executableStdinFile, err.Error())), 1 + } + newContext.StandardInput = file + defer file.Close() + } else if context.StandardInput != nil { + newContext.StandardInput = context.StandardInput + } else { + // Default to stdin of this process itself + newContext.StandardInput = os.Stdin + } + + newContext.StandardOutput = pipeWriters[0] + } else if i == len(MShellPipe.List.Items)-1 { + // Stdout should use the context of this function + newContext.StandardInput = pipeReaders[len(pipeReaders)-1] + newContext.StandardOutput = context.StandardOutput + } else { + newContext.StandardInput = pipeReaders[i-1] + newContext.StandardOutput = pipeWriters[i] + } + + contexts[i] = newContext + } + + // Run the executables concurrently + var wg sync.WaitGroup + results := make([]EvalResult, len(MShellPipe.List.Items)) + exitCodes := make([]int, len(MShellPipe.List.Items)) + + for i, item := range MShellPipe.List.Items { + wg.Add(1) + go func(i int, item Executable) { + defer wg.Done() + // fmt.Fprintf(os.Stderr, "Running item %d\n", i) + results[i], exitCodes[i] = item.Execute(state, contexts[i], stack) + + // Close pipe ends that are no longer needed + if i > 0 { + pipeReaders[i-1].(io.Closer).Close() + } + if i < len(MShellPipe.List.Items)-1 { + pipeWriters[i].(io.Closer).Close() + } + }(i, item.(Executable)) + } + + // Wait for all processes to complete + wg.Wait() + + // Check for errors + for i, result := range results { + if !result.Success { + return result, exitCodes[i] + } + } + + // Return the exit code of the last item + return SimpleSuccess(), exitCodes[len(exitCodes)-1] } diff --git a/mshell-go/Lexer.go b/mshell-go/Lexer.go index 9254f46..f7d00de 100644 --- a/mshell-go/Lexer.go +++ b/mshell-go/Lexer.go @@ -1,569 +1,585 @@ package main import ( - "fmt" - "os" - "strconv" - "strings" - "unicode" + "fmt" + "os" + "strconv" + "strings" + "unicode" ) type TokenType int const ( - EOF TokenType = iota - ERROR - LEFT_SQUARE_BRACKET - RIGHT_SQUARE_BRACKET - LEFT_PAREN - RIGHT_PAREN - EXECUTE - PIPE - QUESTION - POSITIONAL - STRING - SINGLEQUOTESTRING - MINUS - PLUS - EQUALS - INTERPRET - IF - LOOP - READ - STR - BREAK - NOT - AND - OR - GREATERTHANOREQUAL - LESSTHANOREQUAL - LESSTHAN - GREATERTHAN - TRUE - FALSE - VARRETRIEVE - VARSTORE - INTEGER - DOUBLE - LITERAL - INDEXER - ENDINDEXER - STARTINDEXER - SLICEINDEXER - STDOUTLINES - STDOUTSTRIPPED - STDOUTCOMPLETE - EXPORT - TILDEEXPANSION - STOP_ON_ERROR - DEF - END - STDERRREDIRECT + EOF TokenType = iota + ERROR + LEFT_SQUARE_BRACKET + RIGHT_SQUARE_BRACKET + LEFT_PAREN + RIGHT_PAREN + EXECUTE + PIPE + QUESTION + POSITIONAL + STRING + SINGLEQUOTESTRING + MINUS + PLUS + EQUALS + INTERPRET + IF + LOOP + READ + STR + BREAK + NOT + AND + OR + GREATERTHANOREQUAL + LESSTHANOREQUAL + LESSTHAN + GREATERTHAN + TRUE + FALSE + VARRETRIEVE + VARSTORE + INTEGER + DOUBLE + LITERAL + INDEXER + ENDINDEXER + STARTINDEXER + SLICEINDEXER + STDOUTLINES + STDOUTSTRIPPED + STDOUTCOMPLETE + EXPORT + TILDEEXPANSION + STOP_ON_ERROR + DEF + END + STDERRREDIRECT ) func (t TokenType) String() string { - switch t { - case EOF: - return "EOF" - case ERROR: - return "ERROR" - case LEFT_SQUARE_BRACKET: - return "LEFT_SQUARE_BRACKET" - case RIGHT_SQUARE_BRACKET: - return "RIGHT_SQUARE_BRACKET" - case LEFT_PAREN: - return "LEFT_PAREN" - case RIGHT_PAREN: - return "RIGHT_PAREN" - case EXECUTE: - return "EXECUTE" - case PIPE: - return "PIPE" - case QUESTION: - return "QUESTION" - case POSITIONAL: - return "POSITIONAL" - case STRING: - return "STRING" - case SINGLEQUOTESTRING: - return "SINGLEQUOTESTRING" - case MINUS: - return "MINUS" - case PLUS: - return "PLUS" - case EQUALS: - return "EQUALS" - case INTERPRET: - return "INTERPRET" - case IF: - return "IF" - case LOOP: - return "LOOP" - case READ: - return "READ" - case STR: - return "STR" - case BREAK: - return "BREAK" - case NOT: - return "NOT" - case AND: - return "AND" - case OR: - return "OR" - case GREATERTHANOREQUAL: - return "GREATERTHANOREQUAL" - case LESSTHANOREQUAL: - return "LESSTHANOREQUAL" - case LESSTHAN: - return "LESSTHAN" - case GREATERTHAN: - return "GREATERTHAN" - case TRUE: - return "TRUE" - case FALSE: - return "FALSE" - case VARRETRIEVE: - return "VARRETRIEVE" - case VARSTORE: - return "VARSTORE" - case INTEGER: - return "INTEGER" - case DOUBLE: - return "DOUBLE" - case LITERAL: - return "LITERAL" - case INDEXER: - return "INDEXER" - case ENDINDEXER: - return "ENDINDEXER" - case STARTINDEXER: - return "STARTINDEXER" - case SLICEINDEXER: - return "SLICEINDEXER" - case STDOUTLINES: - return "STDOUTLINES" - case STDOUTSTRIPPED: - return "STDOUTSTRIPPED" - case STDOUTCOMPLETE: - return "STDOUTCOMPLETE" - case EXPORT: - return "EXPORT" - case TILDEEXPANSION: - return "TILDEEXPANSION" - case STOP_ON_ERROR: - return "STOP_ON_ERROR" - case DEF: - return "DEF" - case END: - return "END" - case STDERRREDIRECT: - return "STDERRREDIRECT" - default: - return "UNKNOWN" - } + switch t { + case EOF: + return "EOF" + case ERROR: + return "ERROR" + case LEFT_SQUARE_BRACKET: + return "LEFT_SQUARE_BRACKET" + case RIGHT_SQUARE_BRACKET: + return "RIGHT_SQUARE_BRACKET" + case LEFT_PAREN: + return "LEFT_PAREN" + case RIGHT_PAREN: + return "RIGHT_PAREN" + case EXECUTE: + return "EXECUTE" + case PIPE: + return "PIPE" + case QUESTION: + return "QUESTION" + case POSITIONAL: + return "POSITIONAL" + case STRING: + return "STRING" + case SINGLEQUOTESTRING: + return "SINGLEQUOTESTRING" + case MINUS: + return "MINUS" + case PLUS: + return "PLUS" + case EQUALS: + return "EQUALS" + case INTERPRET: + return "INTERPRET" + case IF: + return "IF" + case LOOP: + return "LOOP" + case READ: + return "READ" + case STR: + return "STR" + case BREAK: + return "BREAK" + case NOT: + return "NOT" + case AND: + return "AND" + case OR: + return "OR" + case GREATERTHANOREQUAL: + return "GREATERTHANOREQUAL" + case LESSTHANOREQUAL: + return "LESSTHANOREQUAL" + case LESSTHAN: + return "LESSTHAN" + case GREATERTHAN: + return "GREATERTHAN" + case TRUE: + return "TRUE" + case FALSE: + return "FALSE" + case VARRETRIEVE: + return "VARRETRIEVE" + case VARSTORE: + return "VARSTORE" + case INTEGER: + return "INTEGER" + case DOUBLE: + return "DOUBLE" + case LITERAL: + return "LITERAL" + case INDEXER: + return "INDEXER" + case ENDINDEXER: + return "ENDINDEXER" + case STARTINDEXER: + return "STARTINDEXER" + case SLICEINDEXER: + return "SLICEINDEXER" + case STDOUTLINES: + return "STDOUTLINES" + case STDOUTSTRIPPED: + return "STDOUTSTRIPPED" + case STDOUTCOMPLETE: + return "STDOUTCOMPLETE" + case EXPORT: + return "EXPORT" + case TILDEEXPANSION: + return "TILDEEXPANSION" + case STOP_ON_ERROR: + return "STOP_ON_ERROR" + case DEF: + return "DEF" + case END: + return "END" + case STDERRREDIRECT: + return "STDERRREDIRECT" + default: + return "UNKNOWN" + } } type Token struct { - // One-based line number. - Line int - Column int - Start int - Lexeme string - Type TokenType + // One-based line number. + Line int + Column int + Start int + Lexeme string + Type TokenType } func (t Token) ToJson() string { - return fmt.Sprintf("{\"line\": %d, \"column\": %d, \"start\": %d, \"lexeme\": \"%s\", \"type\": \"%s\"}", t.Line, t.Column, t.Start, t.Lexeme, t.Type) + return fmt.Sprintf("{\"line\": %d, \"column\": %d, \"start\": %d, \"lexeme\": \"%s\", \"type\": \"%s\"}", t.Line, t.Column, t.Start, t.Lexeme, t.Type) } func (t Token) DebugString() string { - return fmt.Sprintf("%d:%d: %s %s", t.Line, t.Column, t.Type, t.Lexeme) + return fmt.Sprintf("%d:%d: %s %s", t.Line, t.Column, t.Type, t.Lexeme) } type Lexer struct { - start int - current int - col int - line int - input []rune + start int + current int + col int + line int + input []rune } func NewLexer(input string) *Lexer { - return &Lexer{ - input: []rune(input), - line: 1, - } + return &Lexer{ + input: []rune(input), + line: 1, + } } func (l *Lexer) atEnd() bool { - return l.current >= len(l.input) + return l.current >= len(l.input) } func (l *Lexer) makeToken(tokenType TokenType) Token { - lexeme := string(l.input[l.start:l.current]) - - return Token{ - Line: l.line, - Column: l.col, - Start: l.start, - Lexeme: lexeme, - Type: tokenType, - } + lexeme := string(l.input[l.start:l.current]) + + return Token{ + Line: l.line, + Column: l.col, + Start: l.start, + Lexeme: lexeme, + Type: tokenType, + } } func (l *Lexer) advance() rune { - c := l.input[l.current] - l.current++ - l.col++ - return c + c := l.input[l.current] + l.current++ + l.col++ + return c } func (l *Lexer) peek() rune { - if l.atEnd() { - return 0 - } - return l.input[l.current] + if l.atEnd() { + return 0 + } + return l.input[l.current] } func (l *Lexer) peekNext() rune { - if l.current+1 >= len(l.input) { - return 0 - } - return l.input[l.current+1] + if l.current+1 >= len(l.input) { + return 0 + } + return l.input[l.current+1] } var notAllowedLiteralChars = map[rune]bool{ - '[': true, - ']': true, - '(': true, - ')': true, - '<': true, - '>': true, - ';': true, - '?': true, + '[': true, + ']': true, + '(': true, + ')': true, + '<': true, + '>': true, + ';': true, + '?': true, } func isAllowedLiteral(r rune) bool { - if unicode.IsSpace(r) { return false } - _, ok := notAllowedLiteralChars[r] - return !ok + if unicode.IsSpace(r) { + return false + } + _, ok := notAllowedLiteralChars[r] + return !ok } func (l *Lexer) scanToken() Token { - l.eatWhitespace() - l.start = l.current - if l.atEnd() { - return l.makeToken(EOF) - } - - c := l.advance() - - if c == '"' { - return l.parseString() - } - - if unicode.IsDigit(c) { return l.parseNumberOrStartIndexer() } - - switch c { - case '\'': - return l.parseSingleQuoteString() - case '[': - return l.makeToken(LEFT_SQUARE_BRACKET) - case ']': - return l.makeToken(RIGHT_SQUARE_BRACKET) - case '(': - return l.makeToken(LEFT_PAREN) - case ')': - return l.makeToken(RIGHT_PAREN) - case ';': - return l.makeToken(EXECUTE) - case '|': - return l.makeToken(PIPE) - case '?': - return l.makeToken(QUESTION) - case '$': - if unicode.IsDigit(l.peek()) { - return l.parsePositional() - } - return l.parseLiteralOrNumber() - case ':': - return l.parseIndexerOrLiteral() - case 'o': - peek := l.peek() - if peek == 's' { - l.advance() - if isAllowedLiteral(l.peek()) { - return l.consumeLiteral() - } else { - return l.makeToken(STDOUTSTRIPPED) - } - } else if peek == 'c' { - l.advance() - if isAllowedLiteral(l.peek()) { - return l.consumeLiteral() - } else { - return l.makeToken(STDOUTCOMPLETE) - } - } else if peek == 'r' { - l.advance() - if isAllowedLiteral(l.peek()) { - return l.consumeLiteral() - } else { - return l.makeToken(OR) - } - } else if isAllowedLiteral(peek) { - return l.consumeLiteral() - } else { - return l.makeToken(STDOUTLINES) - } - default: - return l.parseLiteralOrNumber() - } + l.eatWhitespace() + l.start = l.current + if l.atEnd() { + return l.makeToken(EOF) + } + + c := l.advance() + + if c == '"' { + return l.parseString() + } + + if unicode.IsDigit(c) { + return l.parseNumberOrStartIndexer() + } + + switch c { + case '\'': + return l.parseSingleQuoteString() + case '[': + return l.makeToken(LEFT_SQUARE_BRACKET) + case ']': + return l.makeToken(RIGHT_SQUARE_BRACKET) + case '(': + return l.makeToken(LEFT_PAREN) + case ')': + return l.makeToken(RIGHT_PAREN) + case ';': + return l.makeToken(EXECUTE) + case '|': + return l.makeToken(PIPE) + case '?': + return l.makeToken(QUESTION) + case '$': + if unicode.IsDigit(l.peek()) { + return l.parsePositional() + } + return l.parseLiteralOrNumber() + case ':': + return l.parseIndexerOrLiteral() + case 'o': + peek := l.peek() + if peek == 's' { + l.advance() + if isAllowedLiteral(l.peek()) { + return l.consumeLiteral() + } else { + return l.makeToken(STDOUTSTRIPPED) + } + } else if peek == 'c' { + l.advance() + if isAllowedLiteral(l.peek()) { + return l.consumeLiteral() + } else { + return l.makeToken(STDOUTCOMPLETE) + } + } else if peek == 'r' { + l.advance() + if isAllowedLiteral(l.peek()) { + return l.consumeLiteral() + } else { + return l.makeToken(OR) + } + } else if isAllowedLiteral(peek) { + return l.consumeLiteral() + } else { + return l.makeToken(STDOUTLINES) + } + default: + return l.parseLiteralOrNumber() + } } func (l *Lexer) parseSingleQuoteString() Token { - // When this is called, we've already consumed a single quote. - for { - if l.atEnd() { - fmt.Fprintf(os.Stderr, "%d:%d: Unterminated string.\n", l.line, l.col) - return l.makeToken(ERROR) - } - - c := l.advance() - if c == '\'' { - break - } - } - - return l.makeToken(SINGLEQUOTESTRING) + // When this is called, we've already consumed a single quote. + for { + if l.atEnd() { + fmt.Fprintf(os.Stderr, "%d:%d: Unterminated string.\n", l.line, l.col) + return l.makeToken(ERROR) + } + + c := l.advance() + if c == '\'' { + break + } + } + + return l.makeToken(SINGLEQUOTESTRING) } func (l *Lexer) consumeLiteral() Token { - for { - if l.atEnd() { - break - } - c := l.peek() - if isAllowedLiteral(c) { - l.advance() - } else { - break - } - } - - return l.makeToken(LITERAL) + for { + if l.atEnd() { + break + } + c := l.peek() + if isAllowedLiteral(c) { + l.advance() + } else { + break + } + } + + return l.makeToken(LITERAL) } func (l *Lexer) parseNumberOrStartIndexer() Token { - // Read all the digits - for { - if l.atEnd() { break } - if !unicode.IsDigit(l.peek()) { break } - l.advance() - } - - peek := l.peek() - if peek == ':' { - l.advance() - - c := l.peek() - if unicode.IsDigit(c) { - // Read all the digits - for { - if l.atEnd() { break } - if !unicode.IsDigit(l.peek()) { break } - l.advance() - } - return l.makeToken(SLICEINDEXER) - } else { - return l.makeToken(STARTINDEXER) - } - } else if peek == '>' { - l.advance() - return l.makeToken(STDERRREDIRECT) - } - - if !isAllowedLiteral(peek) { - return l.makeToken(INTEGER) - } else { - return l.consumeLiteral() - } + // Read all the digits + for { + if l.atEnd() { + break + } + if !unicode.IsDigit(l.peek()) { + break + } + l.advance() + } + + peek := l.peek() + if peek == ':' { + l.advance() + + c := l.peek() + if unicode.IsDigit(c) { + // Read all the digits + for { + if l.atEnd() { + break + } + if !unicode.IsDigit(l.peek()) { + break + } + l.advance() + } + return l.makeToken(SLICEINDEXER) + } else { + return l.makeToken(STARTINDEXER) + } + } else if peek == '>' { + l.advance() + return l.makeToken(STDERRREDIRECT) + } + + if !isAllowedLiteral(peek) { + return l.makeToken(INTEGER) + } else { + return l.consumeLiteral() + } } func (l *Lexer) parseIndexerOrLiteral() Token { - c := l.advance() - - // Return literal if at end - if l.atEnd() { - return l.makeToken(LITERAL) - } - - if unicode.IsDigit(c) { - // Read all the digits - for { - if l.atEnd() { break } - if !unicode.IsDigit(l.peek()) { break } - c = l.advance() - } - } else { - return l.consumeLiteral() - } - - if l.peek() == ':' { - l.advance() - return l.makeToken(INDEXER) - } else { - return l.makeToken(ENDINDEXER) - } + c := l.advance() + + // Return literal if at end + if l.atEnd() { + return l.makeToken(LITERAL) + } + + if unicode.IsDigit(c) { + // Read all the digits + for { + if l.atEnd() { + break + } + if !unicode.IsDigit(l.peek()) { + break + } + c = l.advance() + } + } else { + return l.consumeLiteral() + } + + if l.peek() == ':' { + l.advance() + return l.makeToken(INDEXER) + } else { + return l.makeToken(ENDINDEXER) + } } func (l *Lexer) parsePositional() Token { - for { - if l.atEnd() { - break - } - if !unicode.IsDigit(l.peek()) { - break - } - l.advance() - } - return l.makeToken(POSITIONAL) + for { + if l.atEnd() { + break + } + if !unicode.IsDigit(l.peek()) { + break + } + l.advance() + } + return l.makeToken(POSITIONAL) } func (l *Lexer) parseString() Token { - // When this is called, we've already consumed a single double quote. - inEscape := false - for { - if l.atEnd() { - fmt.Fprintf(os.Stderr, "%d:%d: Unterminated string.\n", l.line, l.col) - return l.makeToken(ERROR) - } - c := l.advance() - if inEscape { - if c != 'n' && c != 't' && c != 'r' && c != '\\' && c != '"' { - fmt.Fprintf(os.Stderr, "%d:%d: Invalid escape character '%c'.\n", l.line, l.col, c) - return l.makeToken(ERROR) - } - inEscape = false - } else { - if c == '"' { - break - } - if c == '\\' { - inEscape = true - } - } - } - return l.makeToken(STRING) + // When this is called, we've already consumed a single double quote. + inEscape := false + for { + if l.atEnd() { + fmt.Fprintf(os.Stderr, "%d:%d: Unterminated string.\n", l.line, l.col) + return l.makeToken(ERROR) + } + c := l.advance() + if inEscape { + if c != 'n' && c != 't' && c != 'r' && c != '\\' && c != '"' { + fmt.Fprintf(os.Stderr, "%d:%d: Invalid escape character '%c'.\n", l.line, l.col, c) + return l.makeToken(ERROR) + } + inEscape = false + } else { + if c == '"' { + break + } + if c == '\\' { + inEscape = true + } + } + } + return l.makeToken(STRING) } func (l *Lexer) parseLiteralOrNumber() Token { - for { - if l.atEnd() { - break - } - c := l.peek() - if isAllowedLiteral(c) { - l.advance() - } else { - break - } - } - - literal := string(l.input[l.start:l.current]) - - switch literal { - case "-": - return l.makeToken(MINUS) - case "+": - return l.makeToken(PLUS) - case "=": - return l.makeToken(EQUALS) - case "x": - return l.makeToken(INTERPRET) - case "def": - return l.makeToken(DEF) - case "end": - return l.makeToken(END) - case "export": - return l.makeToken(EXPORT) - case "if": - return l.makeToken(IF) - case "loop": - return l.makeToken(LOOP) - case "read": - return l.makeToken(READ) - case "str": - return l.makeToken(STR) - case "soe": - return l.makeToken(STOP_ON_ERROR) - case "break": - return l.makeToken(BREAK) - case "not": - return l.makeToken(NOT) - case "and": - return l.makeToken(AND) - case ">=": - return l.makeToken(GREATERTHANOREQUAL) - case "<=": - return l.makeToken(LESSTHANOREQUAL) - case "<": - return l.makeToken(LESSTHAN) - case ">": - return l.makeToken(GREATERTHAN) - case "true": - return l.makeToken(TRUE) - case "false": - return l.makeToken(FALSE) - default: - if strings.HasSuffix(literal, "!") { - return l.makeToken(VARRETRIEVE) - } - if strings.HasPrefix(literal, "@") { - return l.makeToken(VARSTORE) - } - if _, err := strconv.Atoi(literal); err == nil { - return l.makeToken(INTEGER) - } - if _, err := strconv.ParseFloat(literal, 64); err == nil { - return l.makeToken(DOUBLE) - } - return l.makeToken(LITERAL) - } + for { + if l.atEnd() { + break + } + c := l.peek() + if isAllowedLiteral(c) { + l.advance() + } else { + break + } + } + + literal := string(l.input[l.start:l.current]) + + switch literal { + case "-": + return l.makeToken(MINUS) + case "+": + return l.makeToken(PLUS) + case "=": + return l.makeToken(EQUALS) + case "x": + return l.makeToken(INTERPRET) + case "def": + return l.makeToken(DEF) + case "end": + return l.makeToken(END) + case "export": + return l.makeToken(EXPORT) + case "if": + return l.makeToken(IF) + case "loop": + return l.makeToken(LOOP) + case "read": + return l.makeToken(READ) + case "str": + return l.makeToken(STR) + case "soe": + return l.makeToken(STOP_ON_ERROR) + case "break": + return l.makeToken(BREAK) + case "not": + return l.makeToken(NOT) + case "and": + return l.makeToken(AND) + case ">=": + return l.makeToken(GREATERTHANOREQUAL) + case "<=": + return l.makeToken(LESSTHANOREQUAL) + case "<": + return l.makeToken(LESSTHAN) + case ">": + return l.makeToken(GREATERTHAN) + case "true": + return l.makeToken(TRUE) + case "false": + return l.makeToken(FALSE) + default: + if strings.HasSuffix(literal, "!") { + return l.makeToken(VARRETRIEVE) + } + if strings.HasPrefix(literal, "@") { + return l.makeToken(VARSTORE) + } + if _, err := strconv.Atoi(literal); err == nil { + return l.makeToken(INTEGER) + } + if _, err := strconv.ParseFloat(literal, 64); err == nil { + return l.makeToken(DOUBLE) + } + return l.makeToken(LITERAL) + } } func (l *Lexer) Tokenize() []Token { - var tokens []Token - for { - t := l.scanToken() - tokens = append(tokens, t) - if t.Type == ERROR || t.Type == EOF { - break - } - } - return tokens + var tokens []Token + for { + t := l.scanToken() + tokens = append(tokens, t) + if t.Type == ERROR || t.Type == EOF { + break + } + } + return tokens } func (l *Lexer) eatWhitespace() { - for { - if l.atEnd() { - return - } - c := l.peek() - switch c { - case ' ', '\t', '\r': - l.advance() - case '#': - for !l.atEnd() && l.peek() != '\n' { - l.advance() - } - case '\n': - l.line++ - l.col = 0 - l.advance() - default: - return - } - } + for { + if l.atEnd() { + return + } + c := l.peek() + switch c { + case ' ', '\t', '\r': + l.advance() + case '#': + for !l.atEnd() && l.peek() != '\n' { + l.advance() + } + case '\n': + l.line++ + l.col = 0 + l.advance() + default: + return + } + } } diff --git a/mshell-go/MShellObject.go b/mshell-go/MShellObject.go index 0a89565..de3a63c 100644 --- a/mshell-go/MShellObject.go +++ b/mshell-go/MShellObject.go @@ -1,536 +1,655 @@ package main import ( - "strconv" - "strings" - "fmt" + "fmt" + "strconv" + "strings" ) type Jsonable interface { - ToJson() string + ToJson() string } type MShellObject interface { - TypeName() string - IsCommandLineable() bool - IsNumeric() bool - FloatNumeric() float64 - CommandLine() string - DebugString() string - Index(index int) (MShellObject, error) - SliceStart(start int) (MShellObject, error) - SliceEnd(end int) (MShellObject, error) - Slice(startInc int, endExc int) (MShellObject, error) - ToJson() string - ToString() string + TypeName() string + IsCommandLineable() bool + IsNumeric() bool + FloatNumeric() float64 + CommandLine() string + DebugString() string + Index(index int) (MShellObject, error) + SliceStart(start int) (MShellObject, error) + SliceEnd(end int) (MShellObject, error) + Slice(startInc int, endExc int) (MShellObject, error) + ToJson() string + ToString() string + IndexErrStr() string } type MShellSimple struct { - Token Token + Token Token } type MShellLiteral struct { - LiteralText string + LiteralText string } type MShellBool struct { - Value bool + Value bool } type MShellQuotation struct { - Tokens []MShellParseItem - StandardInputFile string - StandardOutputFile string - StandardErrorFile string + Tokens []MShellParseItem + StandardInputFile string + StandardOutputFile string + StandardErrorFile string } // type MShellQuotation2 struct { - // Objects []MShellParseItem - // StandardInputFile string - // StandardOutputFile string - // StandardErrorFile string +// Objects []MShellParseItem +// StandardInputFile string +// StandardOutputFile string +// StandardErrorFile string // } type StdoutBehavior int const ( - STDOUT_NONE = iota - STDOUT_LINES - STDOUT_STRIPPED - STDOUT_COMPLETE + STDOUT_NONE = iota + STDOUT_LINES + STDOUT_STRIPPED + STDOUT_COMPLETE ) type MShellList struct { - Items []MShellObject - StandardInputFile string - StandardOutputFile string - StandardErrorFile string - // This sets how stdout is handled, whether it's broken up into lines, stripped of trailing newline, or left as is - StdoutBehavior StdoutBehavior + Items []MShellObject + StandardInputFile string + StandardOutputFile string + StandardErrorFile string + // This sets how stdout is handled, whether it's broken up into lines, stripped of trailing newline, or left as is + StdoutBehavior StdoutBehavior } type MShellString struct { - Content string + Content string } type MShellPipe struct { - List MShellList + List MShellList } type MShellInt struct { - Value int + Value int } // ToString func (obj *MShellLiteral) ToString() string { - return obj.LiteralText + return obj.LiteralText } func (obj *MShellBool) ToString() string { - return strconv.FormatBool(obj.Value) + return strconv.FormatBool(obj.Value) } func (obj *MShellQuotation) ToString() string { - return obj.DebugString() + return obj.DebugString() } func (obj *MShellList) ToString() string { - return obj.DebugString() + return obj.DebugString() } func (obj *MShellString) ToString() string { - return obj.Content + return obj.Content } func (obj *MShellPipe) ToString() string { - return obj.DebugString() + return obj.DebugString() } func (obj *MShellInt) ToString() string { - return strconv.Itoa(obj.Value) + return strconv.Itoa(obj.Value) } func (obj *MShellSimple) ToString() string { - return obj.Token.Lexeme + return obj.Token.Lexeme } - // TypeNames func (obj *MShellLiteral) TypeName() string { - return "Literal" + return "Literal" } func (obj *MShellBool) TypeName() string { - return "Boolean" + return "Boolean" } func (obj *MShellQuotation) TypeName() string { - return "Quotation" + return "Quotation" } func (obj *MShellList) TypeName() string { - return "List" + return "List" } func (obj *MShellString) TypeName() string { - return "String" + return "String" } - + func (obj *MShellPipe) TypeName() string { - return "Pipe" + return "Pipe" } func (obj *MShellInt) TypeName() string { - return "Integer" + return "Integer" } func (obj *MShellSimple) TypeName() string { - return obj.Token.Type.String() + return obj.Token.Type.String() } // IsCommandLineable func (obj *MShellLiteral) IsCommandLineable() bool { - return true + return true } func (obj *MShellBool) IsCommandLineable() bool { - return false + return false } func (obj *MShellQuotation) IsCommandLineable() bool { - return false + return false } func (obj *MShellList) IsCommandLineable() bool { - return false + return false } func (obj *MShellString) IsCommandLineable() bool { - return true + return true } func (obj *MShellPipe) IsCommandLineable() bool { - return false + return false } func (obj *MShellInt) IsCommandLineable() bool { - return true + return true } func (obj *MShellSimple) IsCommandLineable() bool { - return false + return false } // IsNumeric func (obj *MShellLiteral) IsNumeric() bool { - return false + return false } func (obj *MShellBool) IsNumeric() bool { - return false + return false } func (obj *MShellQuotation) IsNumeric() bool { - return false + return false } func (obj *MShellList) IsNumeric() bool { - return false + return false } func (obj *MShellString) IsNumeric() bool { - return false + return false } func (obj *MShellPipe) IsNumeric() bool { - return false + return false } func (obj *MShellInt) IsNumeric() bool { - return true + return true } func (obj *MShellSimple) IsNumeric() bool { - return false + return false } // FloatNumeric func (obj *MShellLiteral) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellBool) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellQuotation) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellList) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellString) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellPipe) FloatNumeric() float64 { - return 0 + return 0 } func (obj *MShellInt) FloatNumeric() float64 { - return float64(obj.Value) + return float64(obj.Value) } func (obj *MShellSimple) FloatNumeric() float64 { - return 0 + return 0 } // CommandLine func (obj *MShellLiteral) CommandLine() string { - return obj.LiteralText + return obj.LiteralText } func (obj *MShellBool) CommandLine() string { - return "" + return "" } func (obj *MShellQuotation) CommandLine() string { - return "" + return "" } func (obj *MShellList) CommandLine() string { - return "" + return "" } func (obj *MShellString) CommandLine() string { - return obj.Content + return obj.Content } func (obj *MShellPipe) CommandLine() string { - return "" + return "" } func (obj *MShellInt) CommandLine() string { - return strconv.Itoa(obj.Value) + return strconv.Itoa(obj.Value) } func (obj *MShellSimple) CommandLine() string { - return "" + return "" } // DebugString func DebugStrs(objs []MShellObject) []string { - debugStrs := make([]string, len(objs)) - for i, obj := range objs { - debugStrs[i] = obj.DebugString() - } - return debugStrs + debugStrs := make([]string, len(objs)) + for i, obj := range objs { + debugStrs[i] = obj.DebugString() + } + return debugStrs } - func (obj *MShellLiteral) DebugString() string { - return obj.LiteralText + return obj.LiteralText } func (obj *MShellBool) DebugString() string { - return strconv.FormatBool(obj.Value) + return strconv.FormatBool(obj.Value) } func (obj *MShellQuotation) DebugString() string { - // Join the tokens with a space, surrounded by '(' and ')' - debugStrs := make([]string, len(obj.Tokens)) - for i, token := range obj.Tokens { - debugStrs[i] = token.DebugString() - } + // Join the tokens with a space, surrounded by '(' and ')' + debugStrs := make([]string, len(obj.Tokens)) + for i, token := range obj.Tokens { + debugStrs[i] = token.DebugString() + } - message := "(" + strings.Join(debugStrs, " ") + ")" - if obj.StandardInputFile != "" { - message += " < " + obj.StandardInputFile - } + message := "(" + strings.Join(debugStrs, " ") + ")" + if obj.StandardInputFile != "" { + message += " < " + obj.StandardInputFile + } - if obj.StandardOutputFile != "" { - message += " > " + obj.StandardOutputFile - } + if obj.StandardOutputFile != "" { + message += " > " + obj.StandardOutputFile + } - return message + return message } func (obj *MShellList) DebugString() string { - // Join the tokens with a space, surrounded by '[' and ']' - return "[" + strings.Join(DebugStrs(obj.Items), " ") + "]" + // Join the tokens with a space, surrounded by '[' and ']' + return "[" + strings.Join(DebugStrs(obj.Items), " ") + "]" } func (obj *MShellString) DebugString() string { - // Surround the string with double quotes - return "\"" + obj.Content + "\"" + // Surround the string with double quotes + return "\"" + obj.Content + "\"" } func (obj *MShellPipe) DebugString() string { - // Join each item with a ' | ' - return strings.Join(DebugStrs(obj.List.Items), " | ") + // Join each item with a ' | ' + return strings.Join(DebugStrs(obj.List.Items), " | ") } func (obj *MShellInt) DebugString() string { - return strconv.Itoa(obj.Value) + return strconv.Itoa(obj.Value) } func (obj *MShellSimple) DebugString() string { - return obj.Token.Lexeme + return obj.Token.Lexeme } -// IsIndexable +func (obj *MShellLiteral) IndexErrStr() string { + return fmt.Sprintf(" (%s)", obj.LiteralText) +} -func CheckRangeInclusive(index int, length int, obj MShellObject, toReturn MShellObject) (MShellObject, error) { - if index < 0 || index >= length { - return nil, fmt.Errorf("Index %d out of range for %s with length %d.\n", index, obj.TypeName(), length) - } else { return toReturn, nil } +func (obj *MShellBool) IndexErrStr() string { + return "" } -func CheckRangeExclusive(index int, length int, obj MShellObject, toReturn MShellObject) (MShellObject, error) { - if index < 0 || index > length { - return nil, fmt.Errorf("Index %d out of range for %s with length %d.\n", index, obj.TypeName(), length) - } else { return toReturn, nil } + +func (obj *MShellQuotation) IndexErrStr() string { + return fmt.Sprintf(" Last item: %s", obj.Tokens[len(obj.Tokens)-1].DebugString()) +} + +func (obj *MShellList) IndexErrStr() string { + return fmt.Sprintf(" Last item: %s", obj.Items[len(obj.Items)-1].DebugString()) +} + +func (obj *MShellString) IndexErrStr() string { + return fmt.Sprintf(" '%s'", obj.Content) +} + +func (obj *MShellPipe) IndexErrStr() string { + return fmt.Sprintf(" Last item: %s", obj.List.Items[len(obj.List.Items)-1].DebugString()) +} + +func (obj *MShellInt) IndexErrStr() string { + return "" +} + +func IndexCheck(index int, length int, obj MShellObject) error { + if index < 0 || index >= length { + return fmt.Errorf("Index %d out of range for %s with length %d.%s\n", index, obj.TypeName(), length, obj.IndexErrStr()) + } else { + return nil + } +} + +func IndexCheckExc(index int, length int, obj MShellObject) error { + if index < 0 || index > length { + return fmt.Errorf("Index %d out of range for %s with length %d.%s\n", index, obj.TypeName(), length, obj.IndexErrStr()) + } else { + return nil + } } // Index func (obj *MShellLiteral) Index(index int) (MShellObject, error) { - return CheckRangeInclusive(index, len(obj.LiteralText), obj, &MShellLiteral{LiteralText: string(obj.LiteralText[index])}) + if err := IndexCheck(index, len(obj.LiteralText), obj); err != nil { + return nil, err + } + return &MShellLiteral{LiteralText: string(obj.LiteralText[index])}, nil } -func (obj *MShellBool) Index(index int) (MShellObject, error) { return nil, fmt.Errorf("Cannot index into a boolean.\n") } +func (obj *MShellBool) Index(index int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot index into a boolean.\n") +} func (obj *MShellQuotation) Index(index int) (MShellObject, error) { - return CheckRangeInclusive(index, len(obj.Tokens), obj, &MShellQuotation{Tokens: []MShellParseItem{obj.Tokens[index]}}) + if err := IndexCheck(index, len(obj.Tokens), obj); err != nil { + return nil, err + } + return &MShellQuotation{Tokens: []MShellParseItem{obj.Tokens[index]}}, nil } - func (obj *MShellList) Index(index int) (MShellObject, error) { - return CheckRangeInclusive(index, len(obj.Items), obj, obj.Items[index]) + if err := IndexCheck(index, len(obj.Items), obj); err != nil { + return nil, err + } + return obj.Items[index], nil } func (obj *MShellString) Index(index int) (MShellObject, error) { - return CheckRangeInclusive(index, len(obj.Content), obj, &MShellString{Content: string(obj.Content[index])}) + if err := IndexCheck(index, len(obj.Content), obj); err != nil { + return nil, err + } + return &MShellString{Content: string(obj.Content[index])}, nil } func (obj *MShellPipe) Index(index int) (MShellObject, error) { - return CheckRangeInclusive(index, len(obj.List.Items), obj, obj.List.Items[index]) + if err := IndexCheck(index, len(obj.List.Items), obj); err != nil { + return nil, err + } + return &MShellPipe{List: MShellList{Items: []MShellObject{obj.List.Items[index]}}}, nil } -func (obj *MShellInt) Index(index int) (MShellObject, error) { return nil, fmt.Errorf("Cannot index into an integer.\n") } +func (obj *MShellInt) Index(index int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot index into an integer.\n") +} -func (obj *MShellSimple) Index(index int) (MShellObject, error) { return nil, fmt.Errorf("Cannot index into a simple token.\n") } +func (obj *MShellSimple) Index(index int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot index into a simple token.\n") +} // SliceStart func (obj *MShellLiteral) SliceStart(start int) (MShellObject, error) { - return CheckRangeInclusive(start, len(obj.LiteralText), obj, &MShellLiteral{LiteralText: obj.LiteralText[start:]}) + if err := IndexCheck(start, len(obj.LiteralText), obj); err != nil { + return nil, err + } + return &MShellLiteral{LiteralText: obj.LiteralText[start:]}, nil } -func (obj *MShellBool) SliceStart(start int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice a boolean.\n") } +func (obj *MShellBool) SliceStart(start int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice a boolean.\n") +} func (obj *MShellQuotation) SliceStart(start int) (MShellObject, error) { - return CheckRangeInclusive(start, len(obj.Tokens), obj, &MShellQuotation{Tokens: obj.Tokens[start:]}) + if err := IndexCheck(start, len(obj.Tokens), obj); err != nil { + return nil, err + } + return &MShellQuotation{Tokens: obj.Tokens[start:]}, nil } func (obj *MShellList) SliceStart(start int) (MShellObject, error) { - return CheckRangeInclusive(start, len(obj.Items), obj, &MShellList{Items: obj.Items[start:]}) + if err := IndexCheck(start, len(obj.Items), obj); err != nil { + return nil, err + } + return &MShellList{Items: obj.Items[start:]}, nil } func (obj *MShellString) SliceStart(start int) (MShellObject, error) { - return CheckRangeInclusive(start, len(obj.Content), obj, &MShellString{Content: obj.Content[start:]}) + if err := IndexCheck(start, len(obj.Content), obj); err != nil { + return nil, err + } + return &MShellString{Content: obj.Content[start:]}, nil } func (obj *MShellPipe) SliceStart(start int) (MShellObject, error) { - return CheckRangeInclusive(start, len(obj.List.Items), obj, &MShellPipe{List: MShellList{Items: obj.List.Items[start:]}}) + if err := IndexCheck(start, len(obj.List.Items), obj); err != nil { + return nil, err + } + return &MShellPipe{List: MShellList{Items: obj.List.Items[start:]}}, nil } -func (obj *MShellInt) SliceStart(start int) (MShellObject, error) { return nil, fmt.Errorf("cannot slice an integer.\n") } +func (obj *MShellInt) SliceStart(start int) (MShellObject, error) { + return nil, fmt.Errorf("cannot slice an integer.\n") +} -func (obj *MShellSimple) SliceStart(start int) (MShellObject, error) { return nil, fmt.Errorf("cannot slice a simple token.\n") } +func (obj *MShellSimple) SliceStart(start int) (MShellObject, error) { + return nil, fmt.Errorf("cannot slice a simple token.\n") +} // SliceEnd func (obj *MShellLiteral) SliceEnd(end int) (MShellObject, error) { - return CheckRangeExclusive(end, len(obj.LiteralText), obj, &MShellLiteral{LiteralText: obj.LiteralText[:end]}) + if err := IndexCheckExc(end, len(obj.LiteralText), obj); err != nil { + return nil, err + } + return &MShellLiteral{LiteralText: obj.LiteralText[:end]}, nil } -func (obj *MShellBool) SliceEnd(end int) (MShellObject, error) { return nil, fmt.Errorf("cannot slice a boolean.\n") } +func (obj *MShellBool) SliceEnd(end int) (MShellObject, error) { + return nil, fmt.Errorf("cannot slice a boolean.\n") +} func (obj *MShellQuotation) SliceEnd(end int) (MShellObject, error) { - return CheckRangeExclusive(end, len(obj.Tokens), obj, &MShellQuotation{Tokens: obj.Tokens[:end]}) + if err := IndexCheckExc(end, len(obj.Tokens), obj); err != nil { + return nil, err + } + return &MShellQuotation{Tokens: obj.Tokens[:end]}, nil } func (obj *MShellList) SliceEnd(end int) (MShellObject, error) { - return CheckRangeExclusive(end, len(obj.Items), obj, &MShellList{Items: obj.Items[:end]}) + if err := IndexCheckExc(end, len(obj.Items), obj); err != nil { + return nil, err + } + return &MShellList{Items: obj.Items[:end]}, nil } func (obj *MShellString) SliceEnd(end int) (MShellObject, error) { - return CheckRangeExclusive(end, len(obj.Content), obj, &MShellString{Content: obj.Content[:end]}) + if err := IndexCheckExc(end, len(obj.Content), obj); err != nil { + return nil, err + } + return &MShellString{Content: obj.Content[:end]}, nil } func (obj *MShellPipe) SliceEnd(end int) (MShellObject, error) { - return CheckRangeExclusive(end, len(obj.List.Items), obj, &MShellPipe{List: MShellList{Items: obj.List.Items[:end]}}) + if err := IndexCheckExc(end, len(obj.List.Items), obj); err != nil { + return nil, err + } + return &MShellPipe{List: MShellList{Items: obj.List.Items[:end]}}, nil } -func (obj *MShellInt) SliceEnd(end int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice an integer.\n") } +func (obj *MShellInt) SliceEnd(end int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice an integer.\n") +} -func (obj *MShellSimple) SliceEnd(end int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice a simple token.\n") } +func (obj *MShellSimple) SliceEnd(end int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice a simple token.\n") +} // Slice +func SliceIndexCheck(startInc int, endExc int, length int, obj MShellObject) error { + if startInc < 0 || startInc > endExc || endExc > length { + return fmt.Errorf("Invalid slice range [%d:%d) for %s with length %d.\n", startInc, endExc, obj.TypeName(), length) + } else { + return nil + } +} + func (obj *MShellLiteral) Slice(startInc int, endExc int) (MShellObject, error) { - return CheckRangeExclusive(startInc, len(obj.LiteralText), obj, &MShellLiteral{LiteralText: obj.LiteralText[startInc:endExc]}) + if err := SliceIndexCheck(startInc, endExc, len(obj.LiteralText), obj); err != nil { + return nil, err + } + return &MShellLiteral{LiteralText: obj.LiteralText[startInc:endExc]}, nil } -func (obj *MShellBool) Slice(startInc int, endExc int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice a boolean.\n") } +func (obj *MShellBool) Slice(startInc int, endExc int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice a boolean.\n") +} func (obj *MShellQuotation) Slice(startInc int, endExc int) (MShellObject, error) { - return CheckRangeExclusive(startInc, len(obj.Tokens), obj, &MShellQuotation{Tokens: obj.Tokens[startInc:endExc]}) + if err := SliceIndexCheck(startInc, endExc, len(obj.Tokens), obj); err != nil { + return nil, err + } + return &MShellQuotation{Tokens: obj.Tokens[startInc:endExc]}, nil } - func (obj *MShellList) Slice(startInc int, endExc int) (MShellObject, error) { - return CheckRangeExclusive(startInc, len(obj.Items), obj, &MShellList{Items: obj.Items[startInc:endExc]}) + if err := SliceIndexCheck(startInc, endExc, len(obj.Items), obj); err != nil { + return nil, err + } + return &MShellList{Items: obj.Items[startInc:endExc]}, nil } func (obj *MShellString) Slice(startInc int, endExc int) (MShellObject, error) { - return CheckRangeExclusive(startInc, len(obj.Content), obj, &MShellString{Content: obj.Content[startInc:endExc]}) + if err := SliceIndexCheck(startInc, endExc, len(obj.Content), obj); err != nil { + return nil, err + } + return &MShellString{Content: obj.Content[startInc:endExc]}, nil } func (obj *MShellPipe) Slice(startInc int, endExc int) (MShellObject, error) { - return CheckRangeExclusive(startInc, len(obj.List.Items), obj, &MShellPipe{List: MShellList{Items: obj.List.Items[startInc:endExc]}}) + if err := SliceIndexCheck(startInc, endExc, len(obj.List.Items), obj); err != nil { + return nil, err + } + return &MShellPipe{List: MShellList{Items: obj.List.Items[startInc:endExc]}}, nil } -func (obj *MShellInt) Slice(startInc int, endExc int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice an integer.\n") } - -func (obj *MShellSimple) Slice(startInc int, endExc int) (MShellObject, error) { return nil, fmt.Errorf("Cannot slice a simple token.\n") } +func (obj *MShellInt) Slice(startInc int, endExc int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice an integer.\n") +} +func (obj *MShellSimple) Slice(startInc int, endExc int) (MShellObject, error) { + return nil, fmt.Errorf("Cannot slice a simple token.\n") +} // ToJson func (obj *MShellLiteral) ToJson() string { - return fmt.Sprintf("{\"type\": \"Literal\", \"value\": \"%s\"}", obj.LiteralText) + return fmt.Sprintf("{\"type\": \"Literal\", \"value\": \"%s\"}", obj.LiteralText) } func (obj *MShellBool) ToJson() string { - return fmt.Sprintf("{\"type\": \"Boolean\", \"value\": %t}", obj.Value) + return fmt.Sprintf("{\"type\": \"Boolean\", \"value\": %t}", obj.Value) } func (obj *MShellQuotation) ToJson() string { - builder := strings.Builder{} - builder.WriteString("{\"type\": \"Quotation\", \"tokens\": [") - if len(obj.Tokens) > 0 { - builder.WriteString(obj.Tokens[0].ToJson()) - for _, token := range obj.Tokens[1:] { - builder.WriteString(", ") - builder.WriteString(token.ToJson()) - } - } - builder.WriteString("]}") - return builder.String() + builder := strings.Builder{} + builder.WriteString("{\"type\": \"Quotation\", \"tokens\": [") + if len(obj.Tokens) > 0 { + builder.WriteString(obj.Tokens[0].ToJson()) + for _, token := range obj.Tokens[1:] { + builder.WriteString(", ") + builder.WriteString(token.ToJson()) + } + } + builder.WriteString("]}") + return builder.String() } func (obj *MShellList) ToJson() string { - builder := strings.Builder{} - builder.WriteString("{\"type\": \"List\", \"items\": [") - if len(obj.Items) > 0 { - builder.WriteString(obj.Items[0].ToJson()) - for _, item := range obj.Items[1:] { - builder.WriteString(", ") - builder.WriteString(item.ToJson()) - } - } - builder.WriteString("]}") - return builder.String() + builder := strings.Builder{} + builder.WriteString("{\"type\": \"List\", \"items\": [") + if len(obj.Items) > 0 { + builder.WriteString(obj.Items[0].ToJson()) + for _, item := range obj.Items[1:] { + builder.WriteString(", ") + builder.WriteString(item.ToJson()) + } + } + builder.WriteString("]}") + return builder.String() } func (obj *MShellString) ToJson() string { - return fmt.Sprintf("{\"type\": \"String\", \"content\": \"%s\"}", obj.Content) + return fmt.Sprintf("{\"type\": \"String\", \"content\": \"%s\"}", obj.Content) } func (obj *MShellPipe) ToJson() string { - return fmt.Sprintf("{\"type\": \"Pipe\", \"list\": %s}", obj.List.ToJson()) + return fmt.Sprintf("{\"type\": \"Pipe\", \"list\": %s}", obj.List.ToJson()) } func (obj *MShellInt) ToJson() string { - return fmt.Sprintf("{\"type\": \"Integer\", \"value\": %d}", obj.Value) + return fmt.Sprintf("{\"type\": \"Integer\", \"value\": %d}", obj.Value) } func (obj *MShellSimple) ToJson() string { - return fmt.Sprintf("{\"type\": \"Simple\", \"token\": %s}", obj.Token.ToJson()) + return fmt.Sprintf("{\"type\": \"Simple\", \"token\": %s}", obj.Token.ToJson()) } func ParseRawString(inputString string) (string, error) { - // Purpose of this function is to remove outer quotes, handle escape characters - if len(inputString) < 2 { - return "", fmt.Errorf("input string should have a minimum length of 2 for surrounding double quotes.\n") - } + // Purpose of this function is to remove outer quotes, handle escape characters + if len(inputString) < 2 { + return "", fmt.Errorf("input string should have a minimum length of 2 for surrounding double quotes.\n") + } - var b strings.Builder + var b strings.Builder index := 1 inEscape := false - for index < len(inputString) - 1 { + for index < len(inputString)-1 { c := inputString[index] if inEscape { diff --git a/mshell-go/Main.go b/mshell-go/Main.go index 77cfbf6..257670e 100644 --- a/mshell-go/Main.go +++ b/mshell-go/Main.go @@ -1,167 +1,167 @@ package main import ( - "os" - "fmt" - "io" - // "runtime/pprof" - // "runtime/trace" - // "runtime" + "fmt" + "io" + "os" + // "runtime/pprof" + // "runtime/trace" + // "runtime" ) func main() { - // Enable profiling - // runtime.SetCPUProfileRate(1000) - // f, err := os.Create("mshell.prof") - // if err != nil { - // fmt.Println(err) - // os.Exit(1) - // return - // } - // pprof.StartCPUProfile(f) - // defer pprof.StopCPUProfile() - - // Enable tracing - // f, err := os.Create("mshell.trace") - // if err != nil { - // fmt.Println(err) - // os.Exit(1) - // return - // } - - // trace.Start(f) - // defer trace.Stop() - - printLex := false - printParse := false - i := 1 - - input := "" - inputSet := false - positionalArgs := []string{} - - for i < len(os.Args) { - arg := os.Args[i] - i++ - if arg == "--lex" { - printLex = true - } else if arg == "--parse" { - printParse = true - } else if arg == "-h" || arg == "--help" { - fmt.Println("Usage: mshell [options] INPUT") - fmt.Println("Usage: mshell [options] < INPUT") - fmt.Println("Options:") - fmt.Println(" --lex Print the tokens of the input") - fmt.Println(" --parse Print the parsed Abstract Syntax Tree") - fmt.Println(" -h, --help Print this help message") - os.Exit(0) - return - } else if input != "" { - positionalArgs = append(positionalArgs, arg) - } else { - inputSet = true - inputBytes, err := os.ReadFile(arg) - if err != nil { - fmt.Println(err) - os.Exit(1) - return - } - input = string(inputBytes) - } - } - - if !inputSet { - inputBytes, err := io.ReadAll(os.Stdin) - if err != nil { - fmt.Println(err) - // Set exit code to 1 - os.Exit(1) - return - } - input = string(inputBytes) - } - - l := NewLexer(input) - - if printLex { - tokens := l.Tokenize() - fmt.Println("Tokens:") - for _, t := range tokens { - // Console.Write($"{t.Line}:{t.Column}:{t.TokenType} {t.RawText}\n"); - fmt.Printf("%d:%d:%s %s\n", t.Line, t.Column, t.Type, t.Lexeme) - } - return - } else if printParse { - p := MShellParser{ lexer: l } - p.NextToken() - file, err := p.ParseFile() - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", input, err) - os.Exit(1) - return - } - - fmt.Println(file.ToJson()) - return - } - - state := EvalState { - PositionalArgs: positionalArgs, - LoopDepth: 0, - Variables: make(map[string]MShellObject), - } - - var stack MShellStack - stack = []MShellObject{} - context := ExecuteContext { - StandardInput: os.Stdin, - StandardOutput: os.Stdout, - } - - var allDefinitions []MShellDefinition - - // Check for environment variable MSHSTDLIB and load that file. Read as UTF-8 - stdlibPath, stdlibSet := os.LookupEnv("MSHSTDLIB") - if stdlibSet { - stdlibBytes, err := os.ReadFile(stdlibPath) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading file %s: %s\n", stdlibPath, err) - os.Exit(1) - return - } - stdlibLexer := NewLexer(string(stdlibBytes)) - stdlibParser := MShellParser{ lexer: stdlibLexer } - stdlibParser.NextToken() - stdlibFile, err := stdlibParser.ParseFile() - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", stdlibPath, err) - os.Exit(1) - return - } - - allDefinitions = append(allDefinitions, stdlibFile.Definitions...) - result := state.Evaluate(stdlibFile.Items, &stack, context, allDefinitions) - - if !result.Success { - fmt.Fprintf(os.Stderr, "Error evaluating MSHSTDLIB file %s.\n", stdlibPath) - os.Exit(1) - return - } - } - - p := MShellParser{ lexer: l } - p.NextToken() - file, err := p.ParseFile() - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", input, err) - os.Exit(1) - return - } - - allDefinitions = append(allDefinitions, file.Definitions...) - result := state.Evaluate(file.Items, &stack, context, allDefinitions) - - if !result.Success { - os.Exit(1) - } + // Enable profiling + // runtime.SetCPUProfileRate(1000) + // f, err := os.Create("mshell.prof") + // if err != nil { + // fmt.Println(err) + // os.Exit(1) + // return + // } + // pprof.StartCPUProfile(f) + // defer pprof.StopCPUProfile() + + // Enable tracing + // f, err := os.Create("mshell.trace") + // if err != nil { + // fmt.Println(err) + // os.Exit(1) + // return + // } + + // trace.Start(f) + // defer trace.Stop() + + printLex := false + printParse := false + i := 1 + + input := "" + inputSet := false + positionalArgs := []string{} + + for i < len(os.Args) { + arg := os.Args[i] + i++ + if arg == "--lex" { + printLex = true + } else if arg == "--parse" { + printParse = true + } else if arg == "-h" || arg == "--help" { + fmt.Println("Usage: mshell [options] INPUT") + fmt.Println("Usage: mshell [options] < INPUT") + fmt.Println("Options:") + fmt.Println(" --lex Print the tokens of the input") + fmt.Println(" --parse Print the parsed Abstract Syntax Tree") + fmt.Println(" -h, --help Print this help message") + os.Exit(0) + return + } else if input != "" { + positionalArgs = append(positionalArgs, arg) + } else { + inputSet = true + inputBytes, err := os.ReadFile(arg) + if err != nil { + fmt.Println(err) + os.Exit(1) + return + } + input = string(inputBytes) + } + } + + if !inputSet { + inputBytes, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Println(err) + // Set exit code to 1 + os.Exit(1) + return + } + input = string(inputBytes) + } + + l := NewLexer(input) + + if printLex { + tokens := l.Tokenize() + fmt.Println("Tokens:") + for _, t := range tokens { + // Console.Write($"{t.Line}:{t.Column}:{t.TokenType} {t.RawText}\n"); + fmt.Printf("%d:%d:%s %s\n", t.Line, t.Column, t.Type, t.Lexeme) + } + return + } else if printParse { + p := MShellParser{lexer: l} + p.NextToken() + file, err := p.ParseFile() + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", input, err) + os.Exit(1) + return + } + + fmt.Println(file.ToJson()) + return + } + + state := EvalState{ + PositionalArgs: positionalArgs, + LoopDepth: 0, + Variables: make(map[string]MShellObject), + } + + var stack MShellStack + stack = []MShellObject{} + context := ExecuteContext{ + StandardInput: os.Stdin, + StandardOutput: os.Stdout, + } + + var allDefinitions []MShellDefinition + + // Check for environment variable MSHSTDLIB and load that file. Read as UTF-8 + stdlibPath, stdlibSet := os.LookupEnv("MSHSTDLIB") + if stdlibSet { + stdlibBytes, err := os.ReadFile(stdlibPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading file %s: %s\n", stdlibPath, err) + os.Exit(1) + return + } + stdlibLexer := NewLexer(string(stdlibBytes)) + stdlibParser := MShellParser{lexer: stdlibLexer} + stdlibParser.NextToken() + stdlibFile, err := stdlibParser.ParseFile() + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", stdlibPath, err) + os.Exit(1) + return + } + + allDefinitions = append(allDefinitions, stdlibFile.Definitions...) + result := state.Evaluate(stdlibFile.Items, &stack, context, allDefinitions) + + if !result.Success { + fmt.Fprintf(os.Stderr, "Error evaluating MSHSTDLIB file %s.\n", stdlibPath) + os.Exit(1) + return + } + } + + p := MShellParser{lexer: l} + p.NextToken() + file, err := p.ParseFile() + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing file %s: %s\n", input, err) + os.Exit(1) + return + } + + allDefinitions = append(allDefinitions, file.Definitions...) + result := state.Evaluate(file.Items, &stack, context, allDefinitions) + + if !result.Success { + os.Exit(1) + } } diff --git a/mshell-go/Parser.go b/mshell-go/Parser.go index 9295ca3..a2ab636 100644 --- a/mshell-go/Parser.go +++ b/mshell-go/Parser.go @@ -1,244 +1,243 @@ package main import ( - "errors" - "fmt" - "strings" - // "os" + "errors" + "fmt" + "strings" + // "os" ) type JsonableList []Jsonable type MShellParseItem interface { - ToJson() string - DebugString() string + ToJson() string + DebugString() string } type MShellFile struct { - Definitions []MShellDefinition - Items []MShellParseItem + Definitions []MShellDefinition + Items []MShellParseItem } type MShellParseList struct { - Items []MShellParseItem + Items []MShellParseItem } func (list *MShellParseList) ToJson() string { - return ToJson(list.Items) + return ToJson(list.Items) } func (list *MShellParseList) DebugString() string { - builder := strings.Builder{} - builder.WriteString("[") - if len(list.Items) > 0 { - builder.WriteString(list.Items[0].DebugString()) - for i := 1; i < len(list.Items); i++ { - builder.WriteString(", ") - builder.WriteString(list.Items[i].DebugString()) - } - } - builder.WriteString("]") - return builder.String() + builder := strings.Builder{} + builder.WriteString("[") + if len(list.Items) > 0 { + builder.WriteString(list.Items[0].DebugString()) + for i := 1; i < len(list.Items); i++ { + builder.WriteString(", ") + builder.WriteString(list.Items[i].DebugString()) + } + } + builder.WriteString("]") + return builder.String() } type MShellParseQuote struct { - Items []MShellParseItem + Items []MShellParseItem } func (quote *MShellParseQuote) ToJson() string { - return ToJson(quote.Items) + return ToJson(quote.Items) } func (quote *MShellParseQuote) DebugString() string { - builder := strings.Builder{} - builder.WriteString("(") - if len(quote.Items) > 0 { - builder.WriteString(quote.Items[0].DebugString()) - for i := 1; i < len(quote.Items); i++ { - builder.WriteString(", ") - builder.WriteString(quote.Items[i].DebugString()) - } - } - builder.WriteString(")") - return builder.String() + builder := strings.Builder{} + builder.WriteString("(") + if len(quote.Items) > 0 { + builder.WriteString(quote.Items[0].DebugString()) + for i := 1; i < len(quote.Items); i++ { + builder.WriteString(", ") + builder.WriteString(quote.Items[i].DebugString()) + } + } + builder.WriteString(")") + return builder.String() } - type MShellDefinition struct { - Name string - Items []MShellParseItem + Name string + Items []MShellParseItem } func (def *MShellDefinition) ToJson() string { - return fmt.Sprintf("{\"name\": \"%s\", \"items\": %s}", def.Name, ToJson(def.Items)) + return fmt.Sprintf("{\"name\": \"%s\", \"items\": %s}", def.Name, ToJson(def.Items)) } func ToJson(objList []MShellParseItem) string { - builder := strings.Builder{} - builder.WriteString("[") - if len(objList) > 0 { - builder.WriteString(objList[0].ToJson()) - for i := 1; i < len(objList); i++ { - builder.WriteString(", ") - builder.WriteString(objList[i].ToJson()) - } - } - builder.WriteString("]") - return builder.String() + builder := strings.Builder{} + builder.WriteString("[") + if len(objList) > 0 { + builder.WriteString(objList[0].ToJson()) + for i := 1; i < len(objList); i++ { + builder.WriteString(", ") + builder.WriteString(objList[i].ToJson()) + } + } + builder.WriteString("]") + return builder.String() } func (file *MShellFile) ToJson() string { - // Start builder for definitions - definitions := strings.Builder{} - definitions.WriteString("[") - for i, def := range file.Definitions { - definitions.WriteString(def.ToJson()) - if i != len(file.Definitions) - 1 { - definitions.WriteString(", ") - } - } - definitions.WriteString("]") - - // Start builder for items - items := strings.Builder{} - items.WriteString("[") - for i, item := range file.Items { - items.WriteString(item.ToJson()) - if i != len(file.Items) - 1 { - items.WriteString(", ") - } - } - items.WriteString("]") - - return fmt.Sprintf("{\"definitions\": %s, \"items\": %s}", definitions.String(), items.String()) + // Start builder for definitions + definitions := strings.Builder{} + definitions.WriteString("[") + for i, def := range file.Definitions { + definitions.WriteString(def.ToJson()) + if i != len(file.Definitions)-1 { + definitions.WriteString(", ") + } + } + definitions.WriteString("]") + + // Start builder for items + items := strings.Builder{} + items.WriteString("[") + for i, item := range file.Items { + items.WriteString(item.ToJson()) + if i != len(file.Items)-1 { + items.WriteString(", ") + } + } + items.WriteString("]") + + return fmt.Sprintf("{\"definitions\": %s, \"items\": %s}", definitions.String(), items.String()) } type MShellParser struct { - lexer *Lexer - curr Token + lexer *Lexer + curr Token } func (parser *MShellParser) NextToken() { - parser.curr = parser.lexer.scanToken() + parser.curr = parser.lexer.scanToken() } func (parser *MShellParser) Match(token Token, tokenType TokenType) error { - if token.Type != tokenType { - message := fmt.Sprintf("Expected %s, got %s", tokenType, token.Type) - return errors.New(message) - } - parser.NextToken() - return nil + if token.Type != tokenType { + message := fmt.Sprintf("Expected %s, got %s", tokenType, token.Type) + return errors.New(message) + } + parser.NextToken() + return nil } func (parser *MShellParser) ParseFile() (*MShellFile, error) { - file := &MShellFile{} - - for parser.curr.Type != EOF && parser.curr.Type != END { - switch parser.curr.Type { - case RIGHT_SQUARE_BRACKET, RIGHT_PAREN: - message := fmt.Sprintf("Unexpected token %s while parsing file", parser.curr.Type) - return file, errors.New(message) - case LEFT_SQUARE_BRACKET: - list, err := parser.ParseList() - if err != nil { - return file, err - } - // fmt.Fprintf(os.Stderr, "List: %s\n", list.ToJson()) - file.Items = append(file.Items, list) - case DEF: - _ = parser.Match(parser.curr, DEF) - if parser.curr.Type != LITERAL { - return file, errors.New(fmt.Sprintf("Expected LITERAL, got %s", parser.curr.Type)) - } - - def := MShellDefinition{Name: parser.curr.Lexeme, Items: []MShellParseItem{}} - _ = parser.Match(parser.curr, LITERAL) - - for { - if parser.curr.Type == END { - break - } else if parser.curr.Type == EOF { - return file, errors.New(fmt.Sprintf("Unexpected EOF while parsing definition %s", def.Name)) - } else { - item, err := parser.ParseItem() - if err != nil { - return file, err - } - def.Items = append(def.Items, item) - } - } - - file.Definitions = append(file.Definitions, def) - _ = parser.Match(parser.curr, END) - // return file, errors.New("DEF Not implemented") - // parser.ParseDefinition() - // case - // parser.ParseItem() - default: - item, err := parser.ParseItem() - if err != nil { - return file, err - } - file.Items = append(file.Items, item) - } - } - return file, nil + file := &MShellFile{} + + for parser.curr.Type != EOF && parser.curr.Type != END { + switch parser.curr.Type { + case RIGHT_SQUARE_BRACKET, RIGHT_PAREN: + message := fmt.Sprintf("Unexpected token %s while parsing file", parser.curr.Type) + return file, errors.New(message) + case LEFT_SQUARE_BRACKET: + list, err := parser.ParseList() + if err != nil { + return file, err + } + // fmt.Fprintf(os.Stderr, "List: %s\n", list.ToJson()) + file.Items = append(file.Items, list) + case DEF: + _ = parser.Match(parser.curr, DEF) + if parser.curr.Type != LITERAL { + return file, errors.New(fmt.Sprintf("Expected LITERAL, got %s", parser.curr.Type)) + } + + def := MShellDefinition{Name: parser.curr.Lexeme, Items: []MShellParseItem{}} + _ = parser.Match(parser.curr, LITERAL) + + for { + if parser.curr.Type == END { + break + } else if parser.curr.Type == EOF { + return file, errors.New(fmt.Sprintf("Unexpected EOF while parsing definition %s", def.Name)) + } else { + item, err := parser.ParseItem() + if err != nil { + return file, err + } + def.Items = append(def.Items, item) + } + } + + file.Definitions = append(file.Definitions, def) + _ = parser.Match(parser.curr, END) + // return file, errors.New("DEF Not implemented") + // parser.ParseDefinition() + // case + // parser.ParseItem() + default: + item, err := parser.ParseItem() + if err != nil { + return file, err + } + file.Items = append(file.Items, item) + } + } + return file, nil } func (parser *MShellParser) ParseList() (*MShellParseList, error) { - list := &MShellParseList{} - err := parser.Match(parser.curr, LEFT_SQUARE_BRACKET) - if err != nil { - return list, err - } - for parser.curr.Type != RIGHT_SQUARE_BRACKET { - item, err := parser.ParseItem() - if err != nil { - return list, err - } - list.Items = append(list.Items, item) - } - err = parser.Match(parser.curr, RIGHT_SQUARE_BRACKET) - if err != nil { - return list, err - } - return list, nil + list := &MShellParseList{} + err := parser.Match(parser.curr, LEFT_SQUARE_BRACKET) + if err != nil { + return list, err + } + for parser.curr.Type != RIGHT_SQUARE_BRACKET { + item, err := parser.ParseItem() + if err != nil { + return list, err + } + list.Items = append(list.Items, item) + } + err = parser.Match(parser.curr, RIGHT_SQUARE_BRACKET) + if err != nil { + return list, err + } + return list, nil } func (parser *MShellParser) ParseItem() (MShellParseItem, error) { - switch parser.curr.Type { - case LEFT_SQUARE_BRACKET: - return parser.ParseList() - case LEFT_PAREN: - return parser.ParseQuote() - default: - return parser.ParseSimple(), nil - } + switch parser.curr.Type { + case LEFT_SQUARE_BRACKET: + return parser.ParseList() + case LEFT_PAREN: + return parser.ParseQuote() + default: + return parser.ParseSimple(), nil + } } -func (parser *MShellParser) ParseSimple() (Token) { - s := parser.curr - parser.NextToken() - return s +func (parser *MShellParser) ParseSimple() Token { + s := parser.curr + parser.NextToken() + return s } func (parser *MShellParser) ParseQuote() (*MShellParseQuote, error) { - quote := &MShellParseQuote{} - err := parser.Match(parser.curr, LEFT_PAREN) - if err != nil { - return quote, err - } - for parser.curr.Type != RIGHT_PAREN { - item, err := parser.ParseItem() - if err != nil { - return quote, err - } - quote.Items = append(quote.Items, item) - } - err = parser.Match(parser.curr, RIGHT_PAREN) - if err != nil { - return quote, err - } - return quote, nil + quote := &MShellParseQuote{} + err := parser.Match(parser.curr, LEFT_PAREN) + if err != nil { + return quote, err + } + for parser.curr.Type != RIGHT_PAREN { + item, err := parser.ParseItem() + if err != nil { + return quote, err + } + quote.Items = append(quote.Items, item) + } + err = parser.Match(parser.curr, RIGHT_PAREN) + if err != nil { + return quote, err + } + return quote, nil } diff --git a/mshell-go/example_test.go b/mshell-go/example_test.go index 9122d76..501f497 100644 --- a/mshell-go/example_test.go +++ b/mshell-go/example_test.go @@ -1,20 +1,20 @@ package main import ( - "testing" + "testing" ) func ModifySlice(myslice []int) { - _ = append(myslice, 1) - _ = append(myslice, 2) - _ = append(myslice, 3) - _ = append(myslice, 4) + _ = append(myslice, 1) + _ = append(myslice, 2) + _ = append(myslice, 3) + _ = append(myslice, 4) } func TestSliceCall(t *testing.T) { - s := make([]int, 0) - ModifySlice(s) - if len(s) != 0 { - t.Errorf("Expected length of 0, but got %d", len(s)) - } + s := make([]int, 0) + ModifySlice(s) + if len(s) != 0 { + t.Errorf("Expected length of 0, but got %d", len(s)) + } }