Wednesday, July 31, 2013

2.1: Configuration File Handling

Chapter 2 is all about breaking long if-else chains into maps of values and functions. This will allow us to separate the branching logic from the algorithm, enabling us to easily modify and extend the branching logic and even completely replace it with a different one.

In this first example we create a process that will read through a configuration file that has the form:

DIRECTIVE PARAMETERS

The algorithm runs through the file and executes the function for the DIRECTIVE passing to it the parameters defined in the file.

To start we define our types, the table that holds the branching logic and the functions that will represent the individual branches. In this case the function accepts a slice of strings and a reference to the branching logic.

type (
 DispatchFunc  func([]string, DispatchTable)
 DispatchTable map[string]DispatchFunc
)

The onReadConfig function expects a filename for its argument. It opens the file, reads each line of text, breaks it into tokens, looks up the function to execute using the dispatch table and the first token and then executes that function passing the rest of the line as parameter. It is the core algorithm of this program, but interestingly, it is fully reentrant and has the same signature as other functions in the dispatch table. It can therefore execute itself through the dispatch table in a round-about recursive way.

func onReadConfig(args []string, dispatch DispatchTable) {
 file, err := os.Open(args[0])
 if err != nil {
  panic(err.Error())
 }
 defer file.Close()
 r := bufio.NewReader(file)
 finished := false
 for !finished {
  line, err := r.ReadString('\n')
  if err == io.EOF {
   finished = true
  } else if err != nil {
   panic(err)
  }
  fields := strings.Fields(line)
  if len(fields) > 0 {
   if f, ok := dispatch[fields[0]]; ok {
    f(fields[1:], dispatch)
   }
  }
 }
}

onDefine is a meta function that defines a directive in terms of another existing directive. What it allows us to do is to define a name that executes an directive with default parameters. For example, if directive CD is defined and it expects a directory as a parameter, we can write DEFINE HOME CD /home/ in the configuration file, therefore creating a new directive HOME that simply executes directive CD with the parameters /home/. In HOP, MJD uses DEFINE to define a directive and actual Perl code in the configuration file that is then dynamically evaluated. With our statically compiled code we will have to do with a less powerful version.

func onDefine(args []string, dispatch DispatchTable) {
 var ok bool
 if _, ok = dispatch[args[0]]; ok {
  fmt.Println("Error in DEFINE: action %q already defined\n", args[0])
  return
 }
 var curaction DispatchFunc
 if curaction, ok = dispatch[args[1]]; !ok {
  fmt.Println("Error in DEFINE: curaction %q not defined\n", args[1])
  return
 }
 dispatch[args[0]] = func(args2 []string, dispatch DispatchTable) {
  curaction(args[2:], dispatch)
 }
}

The main function creates the dispatch table with four directives. CONFIG and DEFINE point to our previous functions while PRINT and CD are two very self-explanatory functions. Examples of configuration files can been seen here and here.

func main() {
 if len(os.Args) != 2 {
  fmt.Printf("Usage %s CONFIG\n", os.Args[0])
  os.Exit(0)
 }

 dispatch := DispatchTable{
  "CONFIG": onReadConfig,
  "DEFINE": onDefine,
  "PRINT": func(args []string, dispatch DispatchTable) {
   fmt.Println(strings.Join(args, " "))
  },
  "CD": func(args []string, dispatch DispatchTable) {
   fmt.Printf("Change dir to: %q\n", args[0])
  },
 }

 onReadConfig(os.Args[1:], dispatch)
}

Get the source at GitHub.