#!/usr/bin/env ruby # First we define a bunch of code-generators, then at the end is a # very neat and readable definition of the format of the YAML files. require 'yaml' def error(msg) $stderr.puts "ERROR: #{msg}" @err = 1 end def warning(msg) $stderr.puts "WARNING: #{msg}" end # Generic validators/formatters def semiordered_list(cnt, validator) lambda {|name,ary| if ary.class != Array error "`#{name}' must be a list" else ary.each_index{|i| ary[i] = validator.call("#{name}[#{i}]", ary[i])} ary = ary.first(cnt).concat(ary.last(ary.count-cnt).sort) end ary } end def unordered_list(validator) semiordered_list(0, validator) end def _unknown(map_name, key) error "Unknown item: #{map_name}[#{key.inspect}]" 0 end def unordered_map1(validator) lambda {|name,hash| if hash.class != Hash error "`#{name}' must be a map" else order = Hash[[*validator.keys.map.with_index]] hash = Hash[hash.sort_by{|k,v| order[k] || _unknown(name,k) }] hash.keys.each{|k| if validator[k] hash[k] = validator[k].call("#{name}[#{k.inspect}]", hash[k]) end } end hash } end def unordered_map2(key_validator, val_validator) lambda {|name,hash| if hash.class != Hash error "`#{name}' must be a map" else hash = Hash[hash.sort_by{|k,v| k}] hash.keys.each{|k| key_validator.call("#{name} key #{k.inspect}", k) hash[k] = val_validator.call("#{name}[#{k.inspect}]", hash[k]) } end hash } end string = lambda {|name,str| if str.class != String error "`#{name}' must be a string" else str end } # Regular Expression String def restring(re) lambda {|name,str| if str.class != String error "`#{name}' must be a string" else unless re =~ str error "`#{name}' does not match #{re.inspect}: #{str}" end str end } end # Specific validators/formatters year = lambda {|name, num| if num.class != Fixnum error "`#{name}' must be a year" else if (num < 1900 || num > 3000) error "`#{name}' is a number, but doesn't look like a year" end num end } # This regex is taken from http://www.w3.org/TR/html5/forms.html#valid-e-mail-address _email_regex = /^[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ email_list = lambda {|name, ary| if ary.class != Array error "`#{name}' must be a list" elsif not ary.empty? preserve = 1 if ary.first.end_with?("@parabola.nu") and ary.count >= 2 preserve = 2 end ary = semiordered_list(preserve, restring(_email_regex)).call(name, ary) end ary } shell = lambda {|name, sh| if sh.class != String error "`#{name}' must be a string" else @valid_shells ||= open("/etc/shells").read.split("\n") .find_all{|line| /^[^\#]/ =~ line} .append("/usr/bin/nologin") unless @valid_shells.include?(sh) warning "shell not listed in /etc/shells: #{sh}" end end sh } # The format of the YAML files format = unordered_map1( { "username" => restring(/^[a-z][a-z0-9-]*$/), "fullname" => string, "email" => email_list, "groups" => semiordered_list(1, string), "pgp_keyid" => restring(/^[0-9A-F]{40}$/), "pgp_revoked_keyids" => unordered_list(restring(/^[0-9A-F]{40}$/)), "ssh_keys" => unordered_map2(string, string), "shell" => shell, "extra" => unordered_map1( { "alias" => string, "other_contact" => string, "roles" => string, "website" => string, "occupation" => string, "yob" => year, "location" => string, "languages" => string, "interests" => string, "favorite_distros" => string, }) }) @err = 0 user = format.call("user", YAML::load(STDIN)) if @err != 0 exit @err end print user.to_yaml