I’m stuck on Stage #SX3
Im not sure why but but in local my implementation works but test fails.
\[tester::#SX3\] Running tests for Stage #SX3 (History Persistence - Append history to file)
\[tester::#SX3\] Original history file content:
\[tester::#SX3\] \[/tmp/pear.txt\] echo grape strawberry orange
\[tester::#SX3\] \[/tmp/pear.txt\] echo pineapple apple
\[tester::#SX3\] \[/tmp/pear.txt\]
\[tester::#SX3\] Running ./your_program.sh
\[your-program\] $ echo strawberry blueberry pear
\[your-program\] strawberry blueberry pear
\[tester::#SX3\] ✓ Ran echo strawberry blueberry pear
\[your-program\] $ echo apple pear strawberry
\[your-program\] apple pear strawberry
\[tester::#SX3\] ✓ Ran echo apple pear strawberry
\[your-program\] $ echo strawberry grape pineapple
\[your-program\] strawberry grape pineapple
\[tester::#SX3\] ✓ Ran echo strawberry grape pineapple
\[your-program\] $ history -a /tmp/pear.txt
\[tester::#SX3\] Reading contents from /tmp/pear.txt
\[tester::#SX3\] \[/tmp/pear.txt\] echo grape strawberry orange
\[tester::#SX3\] \[/tmp/pear.txt\] echo pineapple apple
\[tester::#SX3\] \[/tmp/pear.txt\]
\[tester::#SX3\] \[/tmp/pear.txt\]
\[tester::#SX3\] ✓ Found command “echo grape strawberry orange” in /tmp/pear.txt
\[tester::#SX3\] ✓ Found command “echo pineapple apple” in /tmp/pear.txt
\[tester::#SX3\] expected command “echo strawberry blueberry pear” at line 3, got “”
\[tester::#SX3\] Test failed
package main
import (
"fmt"
"io"
"os"
"os/exec"
"slices"
"strconv"
"strings"
"sync"
"github.com/chzyer/readline"
"github.com/codecrafters-io/shell-starter-go/app/bellcompleter"
)
var _ = fmt.Print
var buildInCommands = []string{"echo", "exit", "type", "pwd", "history"}
var dividerCommands = []string{">", "1>", "2>", ">>", "1>>", "2>>"}
var history string = ".shell_history/" + strconv.Itoa(os.Getpid())
var historyAppendOffset int = 0
func main() {
if os.Getenv("HISTFILE") != "" {
history = os.Getenv("HISTFILE")
} else {
f, e := os.Create(history)
if e != nil {
panic(e)
}
defer f.Close()
}
prefixCompleter := readline.NewPrefixCompleter(
readline.PcItem("echo"),
readline.PcItem("exit"),
)
completer := &bellcompleter.BellCompleter{Completer: prefixCompleter, TabCount: 0}
cfg := &readline.Config{
Prompt: "$ ",
AutoComplete: completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistoryFile: history,
DisableAutoSaveHistory: false,
}
// Initialize the readline instance.
rl, err := readline.NewEx(cfg)
if err != nil {
panic(err)
}
defer rl.Close() // Ensure the terminal is restored to its original state on exit
for {
if err != nil {
panic("input failed")
}
line, err := rl.Readline()
tokens := parseCommand(line)
commands := make([][]string, 1)
i := 0
for _, token := range tokens {
if token == "|" {
// fmt.Println("Ci")
commands = append(commands, []string{})
i++
} else {
commands[i] = append(commands[i], token)
}
}
if err != nil {
panic("Failed!")
}
var wg sync.WaitGroup
pipeReaders := make([]*io.PipeReader, len(commands))
pipeWriters := make([]*io.PipeWriter, len(commands))
for i := 0; i < len(commands)-1; i++ {
pipeReaders[i+1], pipeWriters[i] = io.Pipe()
}
for i, command := range commands {
if command[0] == "exit" {
os.Exit(0)
}
wg.Add(1)
go func(cmd []string, idx int) {
if pipeReaders[idx] != nil {
defer pipeReaders[idx].Close()
}
if pipeWriters[idx] != nil {
defer pipeWriters[idx].Close()
}
executeCommand(cmd, pipeWriters[idx], pipeReaders[idx], rl)
defer wg.Done()
}(command, i)
}
wg.Wait()
}
}
func executeCommand(command []string, pipeWriter *io.PipeWriter, pipeReader *io.PipeReader, rl *readline.Instance) {
execTokens, outStd, redirectionType, _ := extractPipelineCommands(command)
stdin, stdout, stderr := getIOs(pipeReader, pipeWriter, redirectionType, outStd)
var commandError error
if command[0] == "echo" {
fmt.Fprintln(stdout, strings.Join(execTokens[1:], " "))
} else if command[0] == "type" {
fmt.Fprintln(stdout, handleTypeCommand(command))
} else if command[0] == "pwd" {
currentPath, _ := os.Getwd()
fmt.Fprintln(stdout, currentPath)
} else if command[0] == "cd" {
pathToGo := command[1]
if command[1] == "~" {
pathToGo = os.Getenv("HOME")
}
if err := os.Chdir(pathToGo); err != nil {
fmt.Fprintf(stderr, "cd: %s: No such file or directory\n", pathToGo)
}
} else if command[0] == "history" {
if len(execTokens) > 1 {
switch execTokens[1] {
case "-r":
appenedFileIntoHistory(execTokens[2], rl)
return
case "-w":
appendHistoryIntoFile(execTokens[2])
case "-a":
createNewHistory(execTokens[2])
}
}
f, _ := os.ReadFile(history)
lines := strings.Split(strings.TrimRight(string(f), "\n"), "\n")
linesToPrint := len(lines)
if len(execTokens) > 1 {
i, _ := strconv.Atoi(execTokens[1])
linesToPrint = min(linesToPrint, i)
// fmt.Println(linesToPrint)
}
for i, l := range lines[(len(lines) - linesToPrint):] {
if l != "" {
fmt.Fprintf(stdout, " %d %s\n", (i + (len(lines) - linesToPrint) + 1), l)
}
}
} else {
commandError = executeSingleCommand(execTokens, stdin, stdout, stderr)
}
errorOutput := parseError(commandError)
switch redirectionType {
case "redirect", "redirectAppend":
if errorOutput != "" {
fmt.Println(errorOutput)
}
case "redirectError", "redirectAppendError":
if stdout != nil {
}
}
}
func appenedFileIntoHistory(input string, rl *readline.Instance) {
historyFile, _ := os.ReadFile(input)
lines := strings.Split(strings.TrimRight(string(historyFile), "\n"), "\n")
for _, line := range lines {
rl.SaveHistory(line)
}
}
func appendHistoryIntoFile(output string) {
file, _ := os.Create(output)
historyData, _ := os.ReadFile(history)
file.Write(historyData)
}
func createNewHistory(output string) {
historyData, e := os.ReadFile(history)
if e != nil {
panic(e)
}
lines := strings.Split(strings.TrimRight(string(historyData), "\n"), "\n")
existingFileLines, _ := os.ReadFile(output)
newLines := strings.Split(strings.Trim(string(existingFileLines), "\n"), "\n")
newLines = append(newLines, lines[historyAppendOffset:]...)
historyAppendOffset = len(lines)
file, e := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644)
if e != nil {
panic(e)
}
defer file.Close()
for _, line := range newLines {
file.WriteString(line + "\n")
}
}
func statOrZero(f *os.File) int64 {
if fi, err := f.Stat(); err == nil {
return fi.Size()
}
return 0
}
func getIOs(pipeReader *io.PipeReader, pipeWriter *io.PipeWriter, redirectionType string, outStd string) (io.Reader, io.Writer, io.Writer) {
var stdin io.Reader
if pipeReader != nil {
stdin = pipeReader
}
var stdout io.Writer
var stderr io.Writer
stdout = os.Stdout
stderr = os.Stderr
if pipeWriter != nil {
stdout = pipeWriter
} else {
switch redirectionType {
case "redirect":
f, _ := os.OpenFile(outStd, os.O_CREATE|os.O_WRONLY, 0644)
stdout = f
case "redirectAppend":
f, _ := os.OpenFile(outStd, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
stdout = f
case "redirectError":
f, _ := os.OpenFile(outStd, os.O_CREATE|os.O_WRONLY, 0644)
stderr = f
case "redirectAppendError":
f, _ := os.OpenFile(outStd, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
stderr = f
}
}
return stdin, stdout, stderr
}
func executeSingleCommand(command []string, pipeReader io.Reader, pipeWriter io.Writer, pipeErr io.Writer) error {
path := checkAndGetInPaths(command[0], strings.Split(os.Getenv("PATH"), ":"))
if path == "" {
fmt.Fprintln(os.Stderr, command[0]+": not found")
return nil
} else {
var cmd *exec.Cmd
if len(command) == 1 {
cmd = exec.Command(command[0])
} else {
cmd = exec.Command(command[0], command[1:]...)
}
cmd.Stdin = pipeReader
cmd.Stdout = pipeWriter
cmd.Stderr = pipeErr
return cmd.Run()
}
}
func handleTypeCommand(command []string) string {
if slices.Contains(buildInCommands, command[1]) {
return command[1] + " is a shell builtin"
} else {
path := checkAndGetInPaths(command[1], strings.Split(os.Getenv("PATH"), ":"))
if path == "" {
return command[1] + ": not found"
} else {
return command[1] + " is " + path
}
}
}
func checkAndGetInPaths(command string, paths []string) string {
for _, path := range paths {
foundPath, err := exec.LookPath(path + "/" + command)
if err == nil {
return foundPath
}
}
return ""
}
func parseCommand(line string) []string {
var res []string
var currentWord strings.Builder
isSingleQuotes := false
isDoubleQuotes := false
isEscapeMode := false
for _, ch := range strings.Trim(line, "\n") {
// fmt.P/rintln(res)
switch {
case isEscapeMode:
isEscapeMode = false
currentWord.WriteRune(ch)
case ch == '\\' && !isSingleQuotes:
isEscapeMode = true
case ch == '\'' && !isDoubleQuotes:
isSingleQuotes = !isSingleQuotes
case ch == '"' && !isSingleQuotes:
isDoubleQuotes = !isDoubleQuotes
case ch == ' ' && !isSingleQuotes && !isDoubleQuotes:
if currentWord.Len() > 0 {
res = append(res, currentWord.String())
currentWord.Reset()
}
default:
currentWord.WriteRune(ch)
}
}
if currentWord.Len() > 0 {
res = append(res, currentWord.String())
}
// fmt.Println(res)
return res
}
func extractPipelineCommands(tokens []string) ([]string, string, string, bool) {
for i, t := range tokens {
if !slices.Contains(dividerCommands, t) {
continue
}
if i+1 >= len(tokens) {
return nil, "", "redirect", false
}
var commandType string
switch {
case t == ">" || t == "1>":
commandType = "redirect"
case t == "2>":
commandType = "redirectError"
case t == ">>" || t == "1>>":
commandType = "redirectAppend"
case t == "2>>":
commandType = "redirectAppendError"
}
return tokens[:i], strings.Join(tokens[i+1:], " "), commandType, true
}
return tokens, "", "none", true
}
func parseError(err error) string {
if err == nil {
return ""
}
if exitErr, ok := err.(*exec.ExitError); ok {
return strings.Trim(string(exitErr.Stderr), "\n")
}
return fmt.Sprintf("%v", err)
}
Please ignore the terrible code, im kind of new to golang
