Read a configuration file: Difference between revisions
(Add Go solution.) |
(→Tcl: Added implementation) |
||
Line 501: | Line 501: | ||
puts "needspeeling = #{needspeeling}" |
puts "needspeeling = #{needspeeling}" |
||
puts "seedsremoved = #{seedsremoved}"</lang> |
puts "seedsremoved = #{seedsremoved}"</lang> |
||
=={{header|Tcl}}== |
|||
This code stores the configuration values in a global array (<code>cfg</code>) so they can't pollute the global namespace in unexpected ways. |
|||
<lang tcl>proc readConfig {filename {defaults {}}} { |
|||
global cfg |
|||
# Read the file in |
|||
set f [open $filename] |
|||
set contents [read $f] |
|||
close $f |
|||
# Set up the defaults, if supplied |
|||
foreach {var defaultValue} $defaults { |
|||
set cfg($var) $defaultValue |
|||
} |
|||
# Parse the file's contents |
|||
foreach line [split $contents "\n"] { |
|||
set line [string trim $line] |
|||
# Skip comments |
|||
if {[string match "#*" $line] || [string match ";*" $line]} continue |
|||
# Skip blanks |
|||
if {$line eq ""} continue |
|||
if {[regexp {^\w+$} $line]} { |
|||
# Boolean case |
|||
set cfg([string tolower $line]) true |
|||
} elseif {[regexp {^(\w+)\s+([^,]+)$} $line -> var value]} { |
|||
# Simple value case |
|||
set cfg([string tolower $var]) $value |
|||
} elseif {[regexp {^(\w+)\s+(.+)$} $line -> var listValue]} { |
|||
# List value case |
|||
set cfg([string tolower $var]) {} |
|||
foreach value [split $listValue ","] { |
|||
lappend cfg([string tolower $var]) [string trim $value] |
|||
} |
|||
} else { |
|||
error "malformatted config file: $filename" |
|||
} |
|||
} |
|||
} |
|||
# Need to supply some default values due to config file grammar ambiguities |
|||
readConfig "fruit.cfg" { |
|||
needspeeling false |
|||
seedsremoved false |
|||
} |
|||
puts "Full name: $cfg(fullname)" |
|||
puts "Favourite: $cfg(favouritefruit)" |
|||
puts "Peeling? $cfg(needspeeling)" |
|||
puts "Unseeded? $cfg(seedsremoved)" |
|||
puts "Family: $cfg(otherfamily)"</lang> |
|||
=={{header|Visual Basic}}== |
=={{header|Visual Basic}}== |
Revision as of 08:58, 21 July 2011
You are encouraged to solve this task according to the task description, using any language you may know.
The task is to read a configuration file in standard configuration file, and set variables accordingly. For this task, we have a configuration file as follows:
# This is a configuration file in standard configuration file format # # Lines begininning with a hash or a semicolon are ignored by the application # program. Blank lines are also ignored by the application program. # This is the fullname parameter FULLNAME Foo Barber # This is a favourite fruit FAVOURITEFRUIT banana # This is a boolean that should be set NEEDSPEELING # This boolean is commented out ; SEEDSREMOVED # Configuration option names are not case sensitive, but configuration parameter # data is case sensitive and may be preserved by the application program. # An optional equals sign can be used to separate configuration parameter data # from the option name. This is dropped by the parser. # A configuration option may take multiple parameters separated by commas. # Leading and trailing whitespace around parameter names and parameter data fields # are ignored by the application program. OTHERFAMILY Rhu Barber, Harry Barber
For the task we need to set four variables according to the configuration entries as follows:
- fullname = Foo Barber
- favouritefruit = banana
- needspeeling = true
- seedsremoved = false
We also have an option that contains multiple parameters. These may be stored in an array.
- othernames(1) = Rhu Barber
- othernames(2) = Harry Barber
D
<lang d>import std.stdio, std.getopt, std.string, std.conv, std.regexp ;
template VarName(alias Var) { enum VarName = Var.stringof.toupper ; }
void setOpt(alias Var)(const string line) {
auto m = RegExp(`^`~VarName!Var~`(\s+(.*))?`).match(line) ; if(m.length > 0) { static if (is(typeof(Var) == string)) Var = m.length > 2 ? m[2] : "" ; static if (is(typeof(Var) == bool)) Var = true ; static if (is(typeof(Var) == int)) Var = m.length > 2 ? to!int(m[2]) : 0 ; }
}
void main(string[] args) {
string fullname, favouritefruit ; bool needspeeling, seedsremoved ; // default false ; int count ; // a line of "COUNT 5" added at end of config file foreach(line ; File("readcfg.txt").byLine) { auto opt = chomp(text(line)) ; setOpt!fullname(opt) ; setOpt!favouritefruit(opt) ; setOpt!needspeeling(opt) ; setOpt!seedsremoved(opt) ; setOpt!count(opt) ; } writefln("%14s = %s", VarName!fullname, fullname) ; writefln("%14s = %s", VarName!favouritefruit, favouritefruit) ; writefln("%14s = %s", VarName!needspeeling, needspeeling) ; writefln("%14s = %s", VarName!seedsremoved, seedsremoved) ; writefln("%14s = %s", VarName!count, count) ;
}</lang>
Go
This make assumptions about the way the config file is supposed to be structured similar to the ones made by the Python solution. <lang go>package config
import ( "os" "io" "fmt" "bytes" "strings" "io/ioutil" )
var ( ENONE = os.NewError("Requested value does not exist") EBADTYPE = os.NewError("Requested type and actual type do not match") EBADVAL = os.NewError("Value and type do not match") )
type varError struct { err os.Error n string t VarType }
func (err *varError) String() string { return fmt.Sprintf("%v: (%q, %v)", err.err, err.n, err.t) }
type VarType int
const ( Bool VarType = 1 + iota Array String )
func (t VarType) String() string { switch t { case Bool: return "Bool" case Array: return "Array" case String: return "String" }
panic("Unknown VarType") }
type confvar struct { Type VarType Val interface{} }
type Config struct { m map[string]confvar }
func Parse(r io.Reader) (c *Config, err os.Error) { c = new(Config) c.m = make(map[string]confvar)
buf, err := ioutil.ReadAll(r) if err != nil { return }
lines := bytes.Split(buf, []byte{'\n'})
for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } switch line[0] { case '#', ';': continue }
parts := bytes.SplitN(line, []byte{' '}, 2) nam := string(bytes.ToLower(parts[0]))
if len(parts) == 1 { c.m[nam] = confvar{Bool, true} continue }
if strings.Contains(string(parts[1]), ",") { tmpB := bytes.Split(parts[1], []byte{','}) for i := range tmpB { tmpB[i] = bytes.TrimSpace(tmpB[i]) } tmpS := make([]string, 0, len(tmpB)) for i := range tmpB { tmpS = append(tmpS, string(tmpB[i])) }
c.m[nam] = confvar{Array, tmpS} continue }
c.m[nam] = confvar{String, string(bytes.TrimSpace(parts[1]))} }
return }
func (c *Config) Bool(name string) (bool, os.Error) { name = strings.ToLower(name)
if _, ok := c.m[name]; !ok { return false, nil }
if c.m[name].Type != Bool { return false, &varError{EBADTYPE, name, Bool} }
v, ok := c.m[name].Val.(bool) if !ok { return false, &varError{EBADVAL, name, Bool} } return v, nil }
func (c *Config) Array(name string) ([]string, os.Error) { name = strings.ToLower(name)
if _, ok := c.m[name]; !ok { return nil, &varError{ENONE, name, Array} }
if c.m[name].Type != Array { return nil, &varError{EBADTYPE, name, Array} }
v, ok := c.m[name].Val.([]string) if !ok { return nil, &varError{EBADVAL, name, Array} } return v, nil }
func (c *Config) String(name string) (string, os.Error) { name = strings.ToLower(name)
if _, ok := c.m[name]; !ok { return "", &varError{ENONE, name, String} }
if c.m[name].Type != String { return "", &varError{EBADTYPE, name, String} }
v, ok := c.m[name].Val.(string) if !ok { return "", &varError{EBADVAL, name, String} }
return v, nil }</lang>
Usage example: <lang go>package main
import ( "os" "fmt" "config" )
func main() { if len(os.Args) != 2 { fmt.Printf("Usage: %v <configfile>\n", os.Args[0]) os.Exit(1) }
file, err := os.Open(os.Args[1]) if err != nil { fmt.Println(err) os.Exit(1) } defer file.Close()
conf, err := config.Parse(file) if err != nil { fmt.Println(err) os.Exit(1) }
fullname, err := conf.String("fullname") if err != nil { fmt.Println(err) os.Exit(1) }
favouritefruit, err := conf.String("favouritefruit") if err != nil { fmt.Println(err) os.Exit(1) }
needspeeling, err := conf.Bool("needspeeling") if err != nil { fmt.Println(err) os.Exit(1) }
seedsremoved, err := conf.Bool("seedsremoved") if err != nil { fmt.Println(err) os.Exit(1) }
otherfamily, err := conf.Array("otherfamily") if err != nil { fmt.Println(err) os.Exit(1) }
fmt.Printf("FULLNAME: %q\n", fullname) fmt.Printf("FAVOURITEFRUIT: %q\n", favouritefruit) fmt.Printf("NEEDSPEELING: %q\n", needspeeling) fmt.Printf("SEEDSREMOVED: %q\n", seedsremoved) fmt.Printf("OTHERFAMILY: %q\n", otherfamily) }</lang>
J
<lang j>require'regex' set=:4 :'(x)=:y'
cfgString=:4 :0
y set (1;&,~'(?i:',y,')\s*(.*)') y&set rxapply x
)
cfgBoolean=:4 :0
y set 0 (1;&,~'(?i:',y,')\s*(.*)') y&set rxapply x if.-.0-:y do.y set 1 end.
)
taskCfg=:3 :0
cfg=: ('[#;].*';) rxrplc 1!:1<y cfg cfgString 'fullname' cfg cfgString 'favouritefruit' cfg cfgBoolean 'needspeeling' cfg cfgBoolean 'seedsremoved' i.0 0
)</lang>
Example use:
<lang j> taskCfg 'fruit.conf'
(,' = ',]&.do)&>;: 'fullname favouritefruit needspeeling seedsremoved'
fullname = Foo Barber favouritefruit = banana needspeeling = 1 seedsremoved = 0 </lang>
Perl
This is an all-singing, all-dancing version that checks the configuration file syntax and contents and raises exceptions if it fails. (It is intentionally over-commented for pedagogical purposes.)
<lang perl> my $fullname; my $favouritefruit; my $needspeeling; my $seedsremoved; my @otherfamily;
- configuration file definition. See read_conf_file below for explanation.
my $conf_definition = {
'fullname' => [ 'string', \$fullname ], 'favouritefruit' => [ 'string', \$favouritefruit ], 'needspeeling' => [ 'boolean', \$needspeeling ], 'seedsremoved' => [ 'boolean', \$seedsremoved ], 'otherfamily' => [ 'array', \@otherfamily ],
};
my $arg = shift; # take the configuration file name from the command line
# (or first subroutine argument if this were in a sub)
my $file; # this is going to be a file handle reference open $file, $arg or die "Can't open configuration file '$arg': $!";
read_conf_file($file, $conf_definition);
print "fullname = $fullname\n"; print "favouritefruit = $favouritefruit\n"; print "needspeeling = ", ($needspeeling ? 'true' : 'false'), "\n"; print "seedsremoved = ", ($seedsremoved ? 'true' : 'false'), "\n"; for (my $i = 0; $i < @otherfamily; ++$i) {
print "othernames(", $i + 1, ") = ", $otherfamily[$i], "\n";
}
- read_conf_file: Given a file handle opened for reading and a configuration definition,
- read the file.
- If the configuration file doesn't match the definition, raise an exception with "die".
- The configuration definition is (a reference to) an associative array
- where the keys are the configuration variable names in all lower case
- and the values are references to arrays.
- The first element of each of these arrays is the expected type: 'boolean', 'string', or 'array';
- the second element is a reference to the variable that should be assigned the data.
sub read_conf_file {
my ($fh, $def) = @_; # copy parameters
local $_; # avoid interfering with use of $_ in main program while (<$fh>) { # read a line from $fh into $_ until end of file next if /^#/; # skip "#" comments next if /^;/; # skip ";" comments next if /^$/; # skip blank lines chomp; # strip final newline
$_ =~ /^\s*(\w+)\s*(.*)$/i or die "Syntax error"; my $key = $1; my $rest = $2; $key =~ tr/[A-Z]/[a-z]/; # convert keyword to lower case
if (!exists $def->{$key}) { die "Unknown keyword: '$key'"; }
if ($def->{$key}[0] eq 'boolean') { if ($rest) { die "Syntax error: extra data following boolean '$key'"; } ${$def->{$key}[1]} = 1; next; # done with this line, go back to "while" }
$rest =~ s/\s*$//; # drop trailing whitespace $rest =~ s/^=\s*//; # drop equals sign if present
if ($def->{$key}[0] eq 'string') { ${$def->{$key}[1]} = $rest; } elsif ($def->{$key}[0] eq 'array') { @{$def->{$key}[1]} = split /\s*,\s*/, $rest; } else { die "Internal error (unknown type in configuration definition)"; } }
}
</lang>
PicoLisp
'read' supports only a single comment character. Therefore, we use a pipe to filter the comments. <lang PicoLisp>(de rdConf (File)
(pipe (in File (while (echo "#" ";") (till "^J"))) (while (read) (set @ (or (line T) T)) ) ) )</lang>
Test: <lang PicoLisp>(off FULLNAME FAVOURITEFRUIT NEEDSPEELING SEEDSREMOVED OTHERFAMILY) (rdConf "conf.txt")</lang> Output:
: (list FULLNAME FAVOURITEFRUIT NEEDSPEELING SEEDSREMOVED OTHERFAMILY) -> ("Foo Barber" "banana" T NIL "Rhu Barber, Harry Barber")
Python
This task is not well-defined, so we have to make some assumptions (see comments in code). <lang python>def readconf(fn):
ret = {} with file(fn) as fp: for line in fp: # Assume whitespace is ignorable line = line.strip() if not line or line.startswith('#'): continue boolval = True # Assume leading ";" means a false boolean if line.startswith(';'): # Remove one or more leading semicolons line = line.lstrip(';') # If more than just one word, not a valid boolean if len(line.split()) != 1: continue boolval = False bits = line.split(None, 1) if len(bits) == 1: # Assume booleans are just one standalone word k = bits[0] v = boolval else: # Assume more than one word is a string value k, v = bits ret[k.lower()] = v return ret
if __name__ == '__main__':
import sys conf = readconf(sys.argv[1]) for k, v in sorted(conf.items()): print k, '=', v</lang>
Ruby
<lang ruby>fullname = favouritefruit = needspeeling = seedsremoved = false
open("fruit.conf", "r") do |file|
file.each_line do |line| line.chomp! key, value = line.split(nil, 2) case key when /^([#;]|$)/; # ignore line when "FULLNAME"; fullname = value when "FAVOURITEFRUIT"; favouritefruit = value when "NEEDSPEELING"; needspeeling = true when "SEEDSREMOVED"; seedsremoved = true when /^./; puts "#{key}: unknown key" end end
end
puts "fullname = #{fullname}" puts "favouritefruit = #{favouritefruit}" puts "needspeeling = #{needspeeling}" puts "seedsremoved = #{seedsremoved}"</lang>
Tcl
This code stores the configuration values in a global array (cfg
) so they can't pollute the global namespace in unexpected ways.
<lang tcl>proc readConfig {filename {defaults {}}} {
global cfg # Read the file in set f [open $filename] set contents [read $f] close $f # Set up the defaults, if supplied foreach {var defaultValue} $defaults {
set cfg($var) $defaultValue
} # Parse the file's contents foreach line [split $contents "\n"] {
set line [string trim $line] # Skip comments if {[string match "#*" $line] || [string match ";*" $line]} continue # Skip blanks if {$line eq ""} continue
if {[regexp {^\w+$} $line]} { # Boolean case set cfg([string tolower $line]) true } elseif {[regexp {^(\w+)\s+([^,]+)$} $line -> var value]} { # Simple value case set cfg([string tolower $var]) $value } elseif {[regexp {^(\w+)\s+(.+)$} $line -> var listValue]} { # List value case set cfg([string tolower $var]) {} foreach value [split $listValue ","] { lappend cfg([string tolower $var]) [string trim $value] } } else { error "malformatted config file: $filename" }
}
}
- Need to supply some default values due to config file grammar ambiguities
readConfig "fruit.cfg" {
needspeeling false seedsremoved false
} puts "Full name: $cfg(fullname)" puts "Favourite: $cfg(favouritefruit)" puts "Peeling? $cfg(needspeeling)" puts "Unseeded? $cfg(seedsremoved)" puts "Family: $cfg(otherfamily)"</lang>
Visual Basic
<lang vb>' Configuration file parser routines. ' ' (c) Copyright 1993 - 2011 Mark Hobley ' ' This configuration parser contains code ported from an application program ' written in Microsoft Quickbasic ' ' This code can be redistributed or modified under the terms of version 1.2 of ' the GNU Free Documentation Licence as published by the Free Software Foundation.
Sub readini()
var.filename = btrim$(var.winpath) & ini.inifile var.filebuffersize = ini.inimaxlinelength Call openfileread If flg.error = "Y" Then flg.abort = "Y" Exit Sub End If If flg.exists <> "Y" Then flg.abort = "Y" Exit Sub End If var.inistream = var.stream
readinilabela:
Call readlinefromfile If flg.error = "Y" Then flg.abort = "Y" Call closestream flg.error = "Y" Exit Sub End If If flg.endoffile <> "Y" Then iniline$ = message$ If iniline$ <> "" Then If Left$(iniline$, 1) <> ini.commentchar AND Left$(iniline$, 1) <> ini.ignorechar Then endofinicommand% = 0 For l% = 1 To Len(iniline$) If Mid$(iniline$, l%, 1) < " " Then endofinicommand% = l% End If If Not (endofinicommand%) Then If Mid$(iniline$, l%, 1) = " " Then endofinicommand% = l% End If End If If endofinicommand% Then l% = Len(iniline$) End If Next l% iniarg$ = "" If endofinicommand% Then If endofinicommand% <> Len(iniline$) Then iniarg$ = btrim$(Mid$(iniline$, endofinicommand% + 1)) If iniarg$ = "" Then GoTo readinilabelb End If inicommand$ = Left$(iniline$, endofinicommand% - 1) End If Else inicommand$ = btrim$(iniline$) End If
readinilabelb:
'interpret command inicommand$ = UCase$(inicommand$) Select Case inicommand$ Case "FULLNAME" If iniarg$ <> "" Then ini.fullname = iniarg$ End If Case "FAVOURITEFRUIT" If iniarg$ <> "" Then ini.favouritefruit = iniarg$ End If Case "NEEDSPEELING" ini.needspeeling = "Y" Case "SEEDSREMOVED" ini.seedsremoved = "Y" Case "OTHERFAMILY" If iniarg$ <> "" Then ini.otherfamily = iniarg$ CALL familyparser End If Case Else '!! error handling required End Select End If End If GoTo readinilabela End If Call closestream Exit Sub
readinierror:
End Sub
Sub openfileread()
flg.streamopen = "N" Call checkfileexists If flg.error = "Y" Then Exit Sub If flg.exists <> "Y" Then Exit Sub Call getfreestream If flg.error = "Y" Then Exit Sub var.errorsection = "Opening File" var.errordevice = var.filename If ini.errortrap = "Y" Then On Local Error GoTo openfilereaderror End If flg.endoffile = "N" Open var.filename For Input As #var.stream Len = var.filebuffersize flg.streamopen = "Y" Exit Sub
openfilereaderror:
var.errorcode = Err Call errorhandler resume '!!
End Sub
Public Sub checkfileexists()
var.errorsection = "Checking File Exists" var.errordevice = var.filename If ini.errortrap = "Y" Then On Local Error GoTo checkfileexistserror End If flg.exists = "N" If Dir$(var.filename, 0) <> "" Then flg.exists = "Y" End If Exit Sub
checkfileexistserror:
var.errorcode = Err Call errorhandler
End Sub
Public Sub getfreestream()
var.errorsection = "Opening Free Data Stream" var.errordevice = "" If ini.errortrap = "Y" Then On Local Error GoTo getfreestreamerror End If var.stream = FreeFile Exit Sub
getfreestreamerror:
var.errorcode = Err Call errorhandler resume '!!
End Sub
Sub closestream()
If ini.errortrap = "Y" Then On Local Error GoTo closestreamerror End If var.errorsection = "Closing Stream" var.errordevice = "" flg.resumenext = "Y" Close #var.stream If flg.error = "Y" Then flg.error = "N" '!! Call unexpectederror End If flg.streamopen = "N" Exit Sub
closestreamerror:
var.errorcode = Err Call errorhandler resume next
End Sub
Sub readlinefromfile()
If ini.errortrap = "Y" Then On Local Error GoTo readlinefromfileerror End If If EOF(var.stream) Then flg.endoffile = "Y" Exit Sub End If Line Input #var.stream, tmp$ message$ = tmp$ Exit Sub
readlinefromfileerror:
var.errorcode = Err Call errorhandler resume '!!
End Sub
Public Sub errorhandler()
tmp$ = btrim$(var.errorsection) tmp2$ = btrim$(var.errordevice) If tmp2$ <> "" Then tmp$ = tmp$ + " (" + tmp2$ + ")" End If tmp$ = tmp$ + " : " + Str$(var.errorcode) tmp1% = MsgBox(tmp$, 0, "Error!") flg.error = "Y" If flg.resumenext = "Y" Then flg.resumenext = "N"
' Resume Next
Else flg.error = "N"
' Resume
End If
End Sub
Public Function btrim$(arg$)
btrim$ = LTrim$(RTrim$(arg$))
End Function</lang>