1
\$\begingroup\$

I'm creating a program that will hopefully operate like linux terminal (basic commands) for learning purposes.

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)
/*
type FileDir struct {
    Name        string
    Permissions string
    Owner       string
    ModifyDate  int64
    Size        int32
    IsDir       bool
}
*/

func handleInput(input string) {
    tokens := strings.Split(strings.TrimSpace(input), " ")
    // cmd := tokens[0] // will be used later
    options := make(map[string]int)
    var positional []string

    if len(tokens) > 1 {
        // get options
        for j := 1; j < len(tokens); j++ {
            if strings.HasPrefix(tokens[j], "--") {
                options[tokens[j][2:]] = 1
            } else if strings.HasPrefix(tokens[j], "-") {
                if len(tokens[j]) == 2 {
                    options[tokens[j][1:]] = 1
                } else {
                    for _, flag := range tokens[j][1:] {
                        options[string(flag)] = 1
                    }
                }
            } else {
                positional = append(positional, tokens[j])
            }
        }
    }

    // handling of above input, based on cmd
    /* switch cmd {
        // ...
           case "ls" {
            _, hiddenFlag := options["a"]
            _, hiddenBig := options["all"]
            _, long := options["l"]
            _, human := options["h"]
            if len(tokens) == 1 {
                contents := getContentNames(false) // returns []string
                sort.Strings(contents)
                fmt.Println(strings.Join(contents, "  "))
            } else {
                // TODO  details
                if long {
                    contents := getContents(hiddenFlag || hiddenBig) // returns []FileDir
                    sort.Sort(ByName(contents))
                    for _, fileDir := range contents {
                        d := "-"
                        if fileDir.IsDir {
                            d = "d"
                        }
                        var s string
                        if human {
                            s = formatBytes(fileDir.Size)
                        } else {
                            s = fmt.Sprintf("%7d", fileDir.Size)
                        }
                        // WIP
                        fmt.Printf("%s%s %s %s %s MON DY HH:MM %s\n", d, fileDir.Permissions, fileDir.Owner, fileDir.Owner, s, fileDir.Name)
                    }
                } else {
                    contents := getContentNames(hiddenFlag || hiddenBig)
                    sort.Strings(contents)
                    fmt.Println(strings.Join(contents, " "))
                }
            }
        }
        // ...
        }*/
}

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    var userInput string
    for {
        fmt.Printf("[root@FAKE /]$ ")
        scanner.Scan()
        userInput = scanner.Text()
        if userInput == "exit" || userInput == "quit" || userInput == "logout" {
            break
        }
        handleInput(userInput)
    }
}

At this time I want to know if the parsing of options I'm currently doing is the most optimal/efficient way for basic linux commands (ls (-alhs vs -l vs -l -a), man [cmd], cd [loc], tail -f #, etc) so that flags together and/or separate will be able to be later handled. I did include my current handling of the command ls but this section is WIP. If there is a radical change to the way I've done things it might be nice to show how it would optimize of ls handling (but this isn't necessary)

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Why is there so much commented-out code? \$\endgroup\$
    – Harith
    Mar 29 at 16:05
  • \$\begingroup\$ @Harith I am asking about parsing optimization. The code should parse all options without as it is without the commented code. The commented out code is handling after the parsing, just to show that variables (cmd, options, etc) which are currently unused are used, and how I plan on using parsed input \$\endgroup\$
    – depperm
    Mar 29 at 16:09

1 Answer 1

4
\$\begingroup\$

Handle input correctly

Your implementation does not handle an empty string correctly (based on the commented out cmd := input[0], which will panic).

The input-handling is based on the assumptions that:

  1. shells are responsible for parsing the input

  2. all programs follow the same conventions i.e., -- represent a long name while - represent a short name/multiple short names concatenated together.

This is not the case, since it's impossible for a shell to determine out how a program accepts its inputs. In your implementation, you assume that arguments that start with - are actually multiple short arguments concatenated together, i.e. -abcd is actually -a, -b, -c, -d. What if there's a program that does not have this behavior and -abcd is actually an illegal argument? Another good example is Go itself, where it treats arguments starting with - as long arguments (for example, go test -race; your implementation will determine that the arguments are actually go test -r -a -c -e).

What's more common, and what other shells do, is to pass the arguments as-is to the fork command when creating the process to run the desired program. Speaking of...

Forking

Shells do not typically implement every command. In your commented out code, you are implementing the ls command. Shells typically fork a new process with the specified arguments; in the ls case, shells would typically fork a process that runs /bin/ls (or wherever the ls binary is located). As a very basic example, in Go, this would look something like:

func execCommmand(input []string) {
    if len(input) == 0 {
       return
    }
    name := input[0]
    args := input[1:]
    exec.Command(name, args...).Run()
}

This is not always the case. There are some commands (called builtins) that are actually implemented by the shell. Some examples are cd, exit, alias. You can see the full list by doing man builtin.

Handling more complex inputs

If you want your shell to handle more complex inputs (involving pipes, redirections, et cetera), you may need to rework your input handling. For example, ls|grep abcd is valid command, but your implementation will determine the command name as ls|grep with 1 argument abcd.

General comments

  1. Your implementation does not handle exit properly. Most shells allow you to pass in a second argument to exit which indicates the return code (non-numeric arguments will return 255). Your implementation does not exit the program when it is passed additional arguments. It also does not work if you additional spaces around the word (e.g., exit does not work).

  2. Handling Ctrl+C/interrupts: Currently, passing in Ctrl+C (the interrupt signal) kills your shell. Ideally, you'd want to mimic the behavior of existing shells, which is to clear the input and start from a new line (in case of a process running in the foreground, you'd want to send the signal to it as well).

  3. map[string]int can be replaced with map[string]struct{}.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.