[Go] Executing a quoted executable

I’m stuck on Stage #qj0.

Mostly, I don’t understand the instruction, and what is the point of renaming the cat command.
Edit: I understood the issue, and my code should already handle that somehow, but I can’t get to find the command for exe with 'single quotes'. It is always not found.

Here are my logs:

remote: [your-program] $ "exe with \'single quotes\'" /tmp/quz/f3
remote: [your-program] exe with 'single quotes': command not found
remote: [tester::#QJ0] Output does not match expected value.
remote: [tester::#QJ0] Expected: "strawberry pineapple."
remote: [tester::#QJ0] Received: "exe with 'single quotes': command not found"

And here’s a snippet of my code:

package main

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
)

// Ensures gofmt doesn't remove the "fmt" import in stage 1 (feel free to remove this!)
var _ = fmt.Fprint

type CommandFunction func(argument string)

func main() {
	reader := bufio.NewReader(os.Stdin)

	commands := make(map[string]CommandFunction)

	commands["exit"] = func(arguments string) {
		if integer, err := strconv.Atoi(arguments); err == nil {
			os.Exit(integer)
		} else {
			_, _ = fmt.Fprint(os.Stdout, "Invalid argument for exit\n")
		}
	}

	commands["echo"] = func(arguments string) {
		_, _ = fmt.Fprint(os.Stdout, arguments+"\n")
	}

	commands["pwd"] = func(arguments string) {
		path, err := os.Getwd()
		if err == nil {
			_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v\n", path))
		}
	}

	commands["cd"] = func(arguments string) {
		first := arguments[0]
		if first == '~' {
			arguments = os.Getenv("HOME") + arguments[1:]
		}
		err := os.Chdir(arguments)
		if err != nil {
			_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v: No such file or directory\n", arguments))
		}
	}

	commands["type"] = func(arguments string) {
		if _, exists := commands[arguments]; exists {
			_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v is a shell builtin\n", arguments))
		} else {
			path, err := exec.LookPath(arguments)
			if err == nil {
				_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v is %v\n", arguments, path))
			} else {
				_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v: not found\n", arguments))
			}
		}
	}

	for {
		input := read(reader)
		fields := split(input)
		if len(fields) == 0 {
			continue
		}
		command := fields[0]
		arguments := fields[1:]
		if function, exists := commands[command]; exists {
			arguments := strings.Join(arguments, " ")
			// Run a registered command.
			function(arguments)
		} else {
			// Run a system command (usually in the PATH).
			run := exec.Command(command, arguments...)
			run.Stdout = os.Stdout
			run.Stderr = os.Stderr
			err := run.Run()
			if err != nil {
				_, _ = fmt.Fprint(os.Stdout, fmt.Sprintf("%v: command not found\n", command))
			}
		}
	}
}

func split(input string) []string {
	var result []string
	var current []rune
	single := false
	double := false
	escape := false
	path := false

	if strings.HasPrefix(input, "cat ") || strings.HasPrefix(input, "ls ") {
		path = true
	}

	escapable := func(character rune) bool {
		return character == '"' || character == '\'' || character == '\\' || character == ' ' || character == 'n'
	}

	for _, character := range input {
		if escape {
			// If the character is an escapable character, and not in a single-quoted string, or used as a path, append it to the current argument.
			if escapable(character) && !single && !path {
				current = append(current, character)
			} else {
				// If not, append the escape character and the current character.
				current = append(current, '\\', character)
			}
			escape = false
		} else if character == '\\' {
			escape = true
		} else if character == '\'' && !double {
			single = !single
		} else if character == '"' && !single {
			double = !double
		} else if character == ' ' && !single && !double {
			if len(current) > 0 {
				result = append(result, string(current))
				current = nil
			}
		} else {
			current = append(current, character)
		}
	}

	if len(current) > 0 {
		result = append(result, string(current))
	}

	return result
}

func read(reader *bufio.Reader) string {
	_, _ = fmt.Fprint(os.Stdout, "$ ")
	input, _ := reader.ReadString('\n')
	return strings.TrimSpace(input)
}

Hi @Dyrits, could you upload your code to GitHub and share the link? It will be much easier to debug if I can run it directly.

Here is my code on GitHub:

Here are my logs:

remote: [tester::#QJ0] Running tests for Stage #QJ0 (Quoting - Executing a quoted executable)
remote: [tester::#QJ0] [setup] export PATH=/tmp/quz:$PATH
remote: [tester::#QJ0] [setup] Available executables:
remote: [tester::#QJ0] [setup] - 'exe  with  space'
remote: [tester::#QJ0] [setup] - 'exe with "quotes"'
remote: [tester::#QJ0] [setup] - "exe with \'single quotes\'"
remote: [tester::#QJ0] [setup] - 'exe with \n newline'
remote: [tester::#QJ0] Running ./your_program.sh
remote: [tester::#QJ0] [setup] echo -n "raspberry apple." > "/tmp/quz/f1"
remote: [tester::#QJ0] [setup] echo -n "raspberry grape." > "/tmp/quz/f2"
remote: [tester::#QJ0] [setup] echo -n "pineapple orange." > "/tmp/quz/f3"
remote: [tester::#QJ0] [setup] echo -n "mango banana." > "/tmp/quz/f4"
remote: [your-program] $ 'exe  with  space' /tmp/quz/f1
remote: [your-program] raspberry apple.
remote: [tester::#QJ0] ✓ Received expected response
remote: [your-program] $ 'exe with "quotes"' /tmp/quz/f2
remote: [your-program] raspberry grape.
remote: [tester::#QJ0] ✓ Received expected response
remote: [your-program] $ "exe with \'single quotes\'" /tmp/quz/f3
remote: [your-program] exe with 'single quotes': command not found
remote: [tester::#QJ0] Output does not match expected value.
remote: [tester::#QJ0] Expected: "pineapple orange."
remote: [tester::#QJ0] Received: "exe with 'single quotes': command not found"
remote: [your-program] $ 
remote: [tester::#QJ0] Assertion failed.
remote: [tester::#QJ0] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details)

Is there something more I need to do than remove the quotes ?

	// Remove outer quotes if they exist.
	if (strings.HasPrefix(command, "'") && strings.HasSuffix(command, "'")) ||
		(strings.HasPrefix(command, "\"") && strings.HasSuffix(command, "\"")) {
		command = command[1 : len(command)-1]
	}

I spent hours trying to escape my string properly, but this step is like going backwards…

It works doing it that way:

	if command == "exe with 'single quotes'" {
		command = "exe with \\'single quotes\\'"
	}

I don’t want to change my split function over again and it doesn’t seem good to me to (re-)add unnecessary escape characters…

So, the solution would be:

command = strings.ReplaceAll(command, "'", `\'`)

Which I don’t really like.

Hi @Dyrits, there is no need to remove \ inside double quotes when the following character is '.

Here’s an example from zsh on my machine for reference:

Cramming the split function to handle both single and double quotes might not be ideal, as it violates the Single Responsibility Principle.

It would be better to create additional functions to handle specific responsibilities separately.

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.