<commit_before>package expr

import (
	"errors"
	"fmt"
	"io"

	"github.com/grafana/metrictank/api/models"
	"github.com/grafana/metrictank/consolidation"
)

// Req represents a request for one/more series
type Req struct {
	Query string // whatever was parsed as the query out of a graphite target. e.g. target=sum(foo.{b,a}r.*) -> foo.{b,a}r.* -> this will go straight to index lookup
	From  uint32
	To    uint32
	Cons  consolidation.Consolidator // can be 0 to mean undefined
}

// NewReq creates a new Req. pass cons=0 to leave consolidator undefined,
// leaving up to the caller (in graphite's case, it would cause a lookup into storage-aggregation.conf)
func NewReq(query string, from, to uint32, cons consolidation.Consolidator) Req {
	return Req{
		Query: query,
		From:  from,
		To:    to,
		Cons:  cons,
	}
}

type Plan struct {
	Reqs          []Req          // data that needs to be fetched before functions can be executed
	funcs         []GraphiteFunc // top-level funcs to execute, the head of each tree for each target
	exprs         []*expr
	MaxDataPoints uint32
	From          uint32                  // global request scoped from
	To            uint32                  // global request scoped to
	data          map[Req][]models.Series // input data to work with. set via Run(), as well as
	// new data generated by processing funcs. useful for two reasons:
	// 1) reuse partial calculations e.g. queries like target=movingAvg(sum(foo), 10)&target=sum(foo) (TODO)
	// 2) central place to return data back to pool when we're done.
}

func (p Plan) Dump(w io.Writer) {
	fmt.Fprintf(w, "Plan:\n")
	fmt.Fprintf(w, "* Exprs:\n")
	for _, e := range p.exprs {
		fmt.Fprintln(w, e.Print(2))
	}
	fmt.Fprintf(w, "* Reqs:\n")
	for _, r := range p.Reqs {
		fmt.Fprintln(w, "   ", r)
	}
	fmt.Fprintf(w, "MaxDataPoints: %d\n", p.MaxDataPoints)
	fmt.Fprintf(w, "From: %d\n", p.From)
	fmt.Fprintf(w, "To: %d\n", p.To)
}

// Plan validates the expressions and comes up with the initial (potentially non-optimal) execution plan
// which is just a list of requests and the expressions.
// traverse tree and as we go down:
// * make sure function exists
// * validation of arguments
// * allow functions to modify the Context (change data range or consolidation)
// * future version: allow functions to mark safe to pre-aggregate using consolidateBy or not
func NewPlan(exprs []*expr, from, to, mdp uint32, stable bool, reqs []Req) (Plan, error) {
	var err error
	var funcs []GraphiteFunc
	for _, e := range exprs {
		var fn GraphiteFunc
		context := Context{
			from: from,
			to:   to,
		}
		fn, reqs, err = newplan(e, context, stable, reqs)
		if err != nil {
			return Plan{}, err
		}
		funcs = append(funcs, fn)
	}
	return Plan{
		Reqs:          reqs,
		exprs:         exprs,
		funcs:         funcs,
		MaxDataPoints: mdp,
		From:          from,
		To:            to,
	}, nil
}

// newplan adds requests as needed for the given expr, resolving function calls as needed
func newplan(e *expr, context Context, stable bool, reqs []Req) (GraphiteFunc, []Req, error) {
	if e.etype != etFunc && e.etype != etName {
		return nil, nil, errors.New("request must be a function call or metric pattern")
	}
	if e.etype == etName {
		req := NewReq(e.str, context.from, context.to, context.consol)
		reqs = append(reqs, req)
		return NewGet(req), reqs, nil
	} else if e.etype == etFunc && e.str == "seriesByTag" {
		// `seriesByTag` function requires resolving expressions to series
		// (similar to path expressions handled above). Since we need the
		// arguments of seriesByTag to do the resolution, we store the function
		// string back into the Query member of a new request to be parsed later.
		// TODO - find a way to prevent this parse/encode/parse/encode loop
		expressionStr := "seriesByTag(" + e.argsStr + ")"
		req := NewReq(expressionStr, context.from, context.to, context.consol)
		reqs = append(reqs, req)
		return NewGet(req), reqs, nil
	}
	// here e.type is guaranteed to be etFunc
	fdef, ok := funcs[e.str]
	if !ok {
		return nil, nil, ErrUnknownFunction(e.str)
	}
	if stable && !fdef.stable {
		return nil, nil, ErrUnknownFunction(e.str)
	}

	fn := fdef.constr()
	reqs, err := newplanFunc(e, fn, context, stable, reqs)
	return fn, reqs, err
}

// newplanFunc adds requests as needed for the given expr, and validates the function input
// provided you already know the expression is a function call to the given function
func newplanFunc(e *expr, fn GraphiteFunc, context Context, stable bool, reqs []Req) ([]Req, error) {
	// first comes the interesting task of validating the arguments as specified by the function,
	// against the arguments that were parsed.

	argsExp, _ := fn.Signature()
	var err error

	// note:
	// * signature may have seriesLists in it, which means one or more args of type seriesList
	//   so it's legal to have more e.args than signature args in that case.
	// * we can't do extensive, accurate validation of the type here because what the output from a function we depend on
	//   might be dynamically typed. e.g. movingAvg returns 1..N series depending on how many it got as input

	// first validate the mandatory args
	pos := 0    // pos in args of next given arg to process
	cutoff := 0 // marks the index of the first optional point (if any)
	var argExp Arg
	for cutoff, argExp = range argsExp {
		if argExp.Optional() {
			break
		}
		if len(e.args) <= pos {
			return nil, ErrMissingArg
		}
		pos, err = e.consumeBasicArg(pos, argExp)
		if err != nil {
			return nil, err
		}
	}
	if !argExp.Optional() {
		cutoff++
	}

	// we stopped iterating the mandatory args.
	// any remaining args should be due to optional args otherwise there's too many
	// we also track here which keywords can also be used for the given optional args
	// so that those args should not be specified via their keys anymore.

	seenKwargs := make(map[string]struct{})
	for _, argOpt := range argsExp[cutoff:] {
		if len(e.args) <= pos {
			break // no more args specified. we're done.
		}
		pos, err = e.consumeBasicArg(pos, argOpt)
		if err != nil {
			return nil, err
		}
		seenKwargs[argOpt.Key()] = struct{}{}
	}
	if len(e.args) > pos {
		return nil, ErrTooManyArg
	}

	// for any provided keyword args, verify that they are what the function stipulated
	// and that they have not already been specified via their position
	for key := range e.namedArgs {
		_, ok := seenKwargs[key]
		if ok {
			return nil, ErrKwargSpecifiedTwice{key}
		}
		err = e.consumeKwarg(key, argsExp[cutoff:])
		if err != nil {
			return nil, err
		}
		seenKwargs[key] = struct{}{}
	}

	// functions now have their non-series input args set,
	// so they should now be able to specify any context alterations
	context = fn.Context(context)
	// now that we know the needed context for the data coming into
	// this function, we can set up the input arguments for the function
	// that are series
	pos = 0
	for _, argExp = range argsExp {
		if pos >= len(e.args) {
			break // no more args specified. we're done.
		}
		switch argExp.(type) {
		case ArgSeries, ArgSeriesList, ArgSeriesLists, ArgIn:
			pos, reqs, err = e.consumeSeriesArg(pos, argExp, context, stable, reqs)
			if err != nil {
				return nil, err
			}
		default:
			pos++
		}
	}
	return reqs, err
}

// Run invokes all processing as specified in the plan (expressions, from/to) with the input as input
func (p Plan) Run(input map[Req][]models.Series) ([]models.Series, error) {
	var out []models.Series
	p.data = input
	for _, fn := range p.funcs {
		series, err := fn.Exec(p.data)
		if err != nil {
			return nil, err
		}
		out = append(out, series...)
	}
	for i, o := range out {
		if p.MaxDataPoints != 0 && len(o.Datapoints) > int(p.MaxDataPoints) {
			// series may have been created by a function that didn't know which consolidation function to default to.
			// in the future maybe we can do more clever things here. e.g. perSecond maybe consolidate by max.
			if o.Consolidator == 0 {
				o.Consolidator = consolidation.Avg
			}
			out[i].Datapoints, out[i].Interval = consolidation.ConsolidateStable(o.Datapoints, o.Interval, p.MaxDataPoints, o.Consolidator)
		}
	}
	return out, nil
}

// Clean returns all buffers (all input data + generated series along the way)
// back to the pool.
func (p Plan) Clean() {
	for _, series := range p.data {
		for _, serie := range series {
			pointSlicePool.Put(serie.Datapoints[:0])
		}
	}
}
<commit_msg>clearer description of pos/cutoff<commit_after>package expr

import (
	"errors"
	"fmt"
	"io"

	"github.com/grafana/metrictank/api/models"
	"github.com/grafana/metrictank/consolidation"
)

// Req represents a request for one/more series
type Req struct {
	Query string // whatever was parsed as the query out of a graphite target. e.g. target=sum(foo.{b,a}r.*) -> foo.{b,a}r.* -> this will go straight to index lookup
	From  uint32
	To    uint32
	Cons  consolidation.Consolidator // can be 0 to mean undefined
}

// NewReq creates a new Req. pass cons=0 to leave consolidator undefined,
// leaving up to the caller (in graphite's case, it would cause a lookup into storage-aggregation.conf)
func NewReq(query string, from, to uint32, cons consolidation.Consolidator) Req {
	return Req{
		Query: query,
		From:  from,
		To:    to,
		Cons:  cons,
	}
}

type Plan struct {
	Reqs          []Req          // data that needs to be fetched before functions can be executed
	funcs         []GraphiteFunc // top-level funcs to execute, the head of each tree for each target
	exprs         []*expr
	MaxDataPoints uint32
	From          uint32                  // global request scoped from
	To            uint32                  // global request scoped to
	data          map[Req][]models.Series // input data to work with. set via Run(), as well as
	// new data generated by processing funcs. useful for two reasons:
	// 1) reuse partial calculations e.g. queries like target=movingAvg(sum(foo), 10)&target=sum(foo) (TODO)
	// 2) central place to return data back to pool when we're done.
}

func (p Plan) Dump(w io.Writer) {
	fmt.Fprintf(w, "Plan:\n")
	fmt.Fprintf(w, "* Exprs:\n")
	for _, e := range p.exprs {
		fmt.Fprintln(w, e.Print(2))
	}
	fmt.Fprintf(w, "* Reqs:\n")
	for _, r := range p.Reqs {
		fmt.Fprintln(w, "   ", r)
	}
	fmt.Fprintf(w, "MaxDataPoints: %d\n", p.MaxDataPoints)
	fmt.Fprintf(w, "From: %d\n", p.From)
	fmt.Fprintf(w, "To: %d\n", p.To)
}

// Plan validates the expressions and comes up with the initial (potentially non-optimal) execution plan
// which is just a list of requests and the expressions.
// traverse tree and as we go down:
// * make sure function exists
// * validation of arguments
// * allow functions to modify the Context (change data range or consolidation)
// * future version: allow functions to mark safe to pre-aggregate using consolidateBy or not
func NewPlan(exprs []*expr, from, to, mdp uint32, stable bool, reqs []Req) (Plan, error) {
	var err error
	var funcs []GraphiteFunc
	for _, e := range exprs {
		var fn GraphiteFunc
		context := Context{
			from: from,
			to:   to,
		}
		fn, reqs, err = newplan(e, context, stable, reqs)
		if err != nil {
			return Plan{}, err
		}
		funcs = append(funcs, fn)
	}
	return Plan{
		Reqs:          reqs,
		exprs:         exprs,
		funcs:         funcs,
		MaxDataPoints: mdp,
		From:          from,
		To:            to,
	}, nil
}

// newplan adds requests as needed for the given expr, resolving function calls as needed
func newplan(e *expr, context Context, stable bool, reqs []Req) (GraphiteFunc, []Req, error) {
	if e.etype != etFunc && e.etype != etName {
		return nil, nil, errors.New("request must be a function call or metric pattern")
	}
	if e.etype == etName {
		req := NewReq(e.str, context.from, context.to, context.consol)
		reqs = append(reqs, req)
		return NewGet(req), reqs, nil
	} else if e.etype == etFunc && e.str == "seriesByTag" {
		// `seriesByTag` function requires resolving expressions to series
		// (similar to path expressions handled above). Since we need the
		// arguments of seriesByTag to do the resolution, we store the function
		// string back into the Query member of a new request to be parsed later.
		// TODO - find a way to prevent this parse/encode/parse/encode loop
		expressionStr := "seriesByTag(" + e.argsStr + ")"
		req := NewReq(expressionStr, context.from, context.to, context.consol)
		reqs = append(reqs, req)
		return NewGet(req), reqs, nil
	}
	// here e.type is guaranteed to be etFunc
	fdef, ok := funcs[e.str]
	if !ok {
		return nil, nil, ErrUnknownFunction(e.str)
	}
	if stable && !fdef.stable {
		return nil, nil, ErrUnknownFunction(e.str)
	}

	fn := fdef.constr()
	reqs, err := newplanFunc(e, fn, context, stable, reqs)
	return fn, reqs, err
}

// newplanFunc adds requests as needed for the given expr, and validates the function input
// provided you already know the expression is a function call to the given function
func newplanFunc(e *expr, fn GraphiteFunc, context Context, stable bool, reqs []Req) ([]Req, error) {
	// first comes the interesting task of validating the arguments as specified by the function,
	// against the arguments that were parsed.

	argsExp, _ := fn.Signature()
	var err error

	// note:
	// * signature may have seriesLists in it, which means one or more args of type seriesList
	//   so it's legal to have more e.args than signature args in that case.
	// * we can't do extensive, accurate validation of the type here because what the output from a function we depend on
	//   might be dynamically typed. e.g. movingAvg returns 1..N series depending on how many it got as input

	// first validate the mandatory args
	pos := 0    // e.args[pos]     : next given arg to process
	cutoff := 0 // argsExp[cutoff] : will be first optional arg (if any)
	var argExp Arg
	for cutoff, argExp = range argsExp {
		if argExp.Optional() {
			break
		}
		if len(e.args) <= pos {
			return nil, ErrMissingArg
		}
		pos, err = e.consumeBasicArg(pos, argExp)
		if err != nil {
			return nil, err
		}
	}
	if !argExp.Optional() {
		cutoff++
	}

	// we stopped iterating the mandatory args.
	// any remaining args should be due to optional args otherwise there's too many
	// we also track here which keywords can also be used for the given optional args
	// so that those args should not be specified via their keys anymore.

	seenKwargs := make(map[string]struct{})
	for _, argOpt := range argsExp[cutoff:] {
		if len(e.args) <= pos {
			break // no more args specified. we're done.
		}
		pos, err = e.consumeBasicArg(pos, argOpt)
		if err != nil {
			return nil, err
		}
		seenKwargs[argOpt.Key()] = struct{}{}
	}
	if len(e.args) > pos {
		return nil, ErrTooManyArg
	}

	// for any provided keyword args, verify that they are what the function stipulated
	// and that they have not already been specified via their position
	for key := range e.namedArgs {
		_, ok := seenKwargs[key]
		if ok {
			return nil, ErrKwargSpecifiedTwice{key}
		}
		err = e.consumeKwarg(key, argsExp[cutoff:])
		if err != nil {
			return nil, err
		}
		seenKwargs[key] = struct{}{}
	}

	// functions now have their non-series input args set,
	// so they should now be able to specify any context alterations
	context = fn.Context(context)
	// now that we know the needed context for the data coming into
	// this function, we can set up the input arguments for the function
	// that are series
	pos = 0
	for _, argExp = range argsExp {
		if pos >= len(e.args) {
			break // no more args specified. we're done.
		}
		switch argExp.(type) {
		case ArgSeries, ArgSeriesList, ArgSeriesLists, ArgIn:
			pos, reqs, err = e.consumeSeriesArg(pos, argExp, context, stable, reqs)
			if err != nil {
				return nil, err
			}
		default:
			pos++
		}
	}
	return reqs, err
}

// Run invokes all processing as specified in the plan (expressions, from/to) with the input as input
func (p Plan) Run(input map[Req][]models.Series) ([]models.Series, error) {
	var out []models.Series
	p.data = input
	for _, fn := range p.funcs {
		series, err := fn.Exec(p.data)
		if err != nil {
			return nil, err
		}
		out = append(out, series...)
	}
	for i, o := range out {
		if p.MaxDataPoints != 0 && len(o.Datapoints) > int(p.MaxDataPoints) {
			// series may have been created by a function that didn't know which consolidation function to default to.
			// in the future maybe we can do more clever things here. e.g. perSecond maybe consolidate by max.
			if o.Consolidator == 0 {
				o.Consolidator = consolidation.Avg
			}
			out[i].Datapoints, out[i].Interval = consolidation.ConsolidateStable(o.Datapoints, o.Interval, p.MaxDataPoints, o.Consolidator)
		}
	}
	return out, nil
}

// Clean returns all buffers (all input data + generated series along the way)
// back to the pool.
func (p Plan) Clean() {
	for _, series := range p.data {
		for _, serie := range series {
			pointSlicePool.Put(serie.Datapoints[:0])
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2018 The Terraformer Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package terraform_utils

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"strings"

	"github.com/hashicorp/hcl2/hclwrite"

	"github.com/hashicorp/hcl/hcl/ast"
	hclPrinter "github.com/hashicorp/hcl/hcl/printer"
	hclParcer "github.com/hashicorp/hcl/json/parser"
)

// Copy code from https://github.com/kubernetes/kops project with few changes for support many provider and heredoc

const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"

// sanitizer fixes up an invalid HCL AST, as produced by the HCL parser for JSON
type astSanitizer struct{}

// output prints creates b printable HCL output and returns it.
func (v *astSanitizer) visit(n interface{}) {
	switch t := n.(type) {
	case *ast.File:
		v.visit(t.Node)
	case *ast.ObjectList:
		var index int
		for {
			if index == len(t.Items) {
				break
			}
			v.visit(t.Items[index])
			index++
		}
	case *ast.ObjectKey:
	case *ast.ObjectItem:
		v.visitObjectItem(t)
	case *ast.LiteralType:
	case *ast.ListType:
	case *ast.ObjectType:
		v.visit(t.List)
	default:
		fmt.Printf(" unknown type: %T\n", n)
	}

}

func (v *astSanitizer) visitObjectItem(o *ast.ObjectItem) {
	for i, k := range o.Keys {
		if i == 0 {
			text := k.Token.Text
			if text != "" && text[0] == '"' && text[len(text)-1] == '"' {
				v := text[1 : len(text)-1]
				safe := true
				for _, c := range v {
					if !strings.ContainsRune(safeChars, c) {
						safe = false
						break
					}
				}
				if safe {
					k.Token.Text = v
				}
			}
		}
	}
	switch t := o.Val.(type) {
	case *ast.LiteralType: // heredoc support
		if strings.HasPrefix(t.Token.Text, `"<<`) {
			t.Token.Text = t.Token.Text[1:]
			t.Token.Text = t.Token.Text[:len(t.Token.Text)-1]
			t.Token.Text = strings.Replace(t.Token.Text, `\n`, "\n", -1)
			t.Token.Text = strings.Replace(t.Token.Text, `\t`, "", -1)
			t.Token.Type = 10
			// check if text json for Unquote and Indent
			tmp := map[string]interface{}{}
			jsonTest := t.Token.Text
			lines := strings.Split(jsonTest, "\n")
			jsonTest = strings.Join(lines[1:len(lines)-1], "\n")
			jsonTest = strings.Replace(jsonTest, "\\\"", "\"", -1)
			// it's json we convert to heredoc back
			err := json.Unmarshal([]byte(jsonTest), &tmp)
			if err == nil {
				dataJsonBytes, err := json.MarshalIndent(tmp, "", "  ")
				if err == nil {
					jsonData := strings.Split(string(dataJsonBytes), "\n")
					// first line for heredoc
					jsonData = append([]string{lines[0]}, jsonData...)
					// last line for heredoc
					jsonData = append(jsonData, lines[len(lines)-1])
					hereDoc := strings.Join(jsonData, "\n")
					t.Token.Text = hereDoc
				}
			}
		}
	default:
	}

	// A hack so that Assign.IsValid is true, so that the printer will output =
	o.Assign.Line = 1

	v.visit(o.Val)
}

func HclPrint(data interface{}, mapsObjects map[string]struct{}) ([]byte, error) {
	dataJsonBytes, err := json.MarshalIndent(data, "", "  ")
	dataJson := string(dataJsonBytes)
	dataJson = strings.Replace(dataJson, "\\u003c", "<", -1)
	if err != nil {
		return []byte{}, fmt.Errorf("error marshalling terraform data to json: %v", err)
	}
	nodes, err := hclParcer.Parse([]byte(dataJson))
	if err != nil {
		return []byte{}, fmt.Errorf("error parsing terraform json: %v", err)
	}
	var sanitizer astSanitizer
	sanitizer.visit(nodes)

	var b bytes.Buffer
	err = hclPrinter.Fprint(&b, nodes)
	if err != nil {
		return nil, fmt.Errorf("error writing HCL: %v", err)
	}
	s := b.String()

	// Remove extra whitespace...
	s = strings.Replace(s, "\n\n", "\n", -1)

	// ...but leave whitespace between resources
	s = strings.Replace(s, "}\nresource", "}\n\nresource", -1)

	// We don't need to escape > or <
	s = strings.Replace(s, "\\u003c", "<", -1)
	s = strings.Replace(s, "\\u003e", ">", -1)

	// Apply Terraform style (alignment etc.)
	formatted := hclwrite.Format([]byte(s))
	formatted, err = hclPrinter.Format([]byte(s))
	// hack for support terraform 0.12
	formatted = terraform12Adjustments(formatted, mapsObjects)
	if err != nil {
		log.Println("Invalid HCL follows:")
		for i, line := range strings.Split(s, "\n") {
			fmt.Printf("%d\t%s", i+1, line)
		}
		return nil, fmt.Errorf("error formatting HCL: %v", err)
	}

	return formatted, nil
}

func terraform12Adjustments(formatted []byte, mapsObjects map[string]struct{}) []byte {
	s := string(formatted)
	old := " = {"
	new := " {"
	lines := strings.Split(s, "\n")
	for i, line := range lines {
		if !strings.Contains(line, old) {
			continue
		}
		key := strings.Trim(strings.Split(line, old)[0], " ")
		if _, exist := mapsObjects[key]; exist {
			continue
		}
		lines[i] = strings.Replace(line, old, new, -1)
	}
	s = strings.Join(lines, "\n")
	return []byte(s)
}

// Sanitize name for terraform style
func TfSanitize(name string) string {
	name = strings.Replace(name, " ", "", -1)
	//name = strings.Replace(name, "-", "_", -1)
	name = strings.Replace(name, "*", "--", -1)
	name = strings.Replace(name, ".", "--", -1)
	name = strings.Replace(name, ":", "--", -1)
	name = strings.Replace(name, "/", "--", -1)
	return name
}

// Print hcl file from TerraformResource + provider
func HclPrintResource(resources []Resource, providerData map[string]interface{}) ([]byte, error) {
	resourcesByType := map[string]map[string]interface{}{}
	mapsObjects := map[string]struct{}{}
	for _, res := range resources {

		r := resourcesByType[res.InstanceInfo.Type]
		if r == nil {
			r = make(map[string]interface{})
			resourcesByType[res.InstanceInfo.Type] = r
		}

		if r[res.ResourceName] != nil {
			log.Println(resources)
			return []byte{}, fmt.Errorf("[ERR]: duplicate resource found: %s.%s", res.InstanceInfo.Type, res.ResourceName)
		}

		r[res.ResourceName] = res.Item

		for k := range res.InstanceState.Attributes {
			if strings.HasSuffix(k, ".%") {
				t := strings.Split(k, ".")
				key := t[len(t)-2]
				mapsObjects[key] = struct{}{}
			}
		}
	}

	data := map[string]interface{}{}
	if len(resourcesByType) > 0 {
		data["resource"] = resourcesByType
	}
	if len(providerData) > 0 {
		data["provider"] = providerData
	}
	var err error

	hclBytes, err := HclPrint(data, mapsObjects)
	if err != nil {
		return []byte{}, err
	}
	return hclBytes, nil
}
<commit_msg>Replace Terraform Unsafe Chars with `--`<commit_after>// Copyright 2018 The Terraformer Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package terraform_utils

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"regexp"
	"strings"

	"github.com/hashicorp/hcl2/hclwrite"

	"github.com/hashicorp/hcl/hcl/ast"
	hclPrinter "github.com/hashicorp/hcl/hcl/printer"
	hclParcer "github.com/hashicorp/hcl/json/parser"
)

// Copy code from https://github.com/kubernetes/kops project with few changes for support many provider and heredoc

const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"

var unsafeChars = regexp.MustCompile(`[^-0-9A-Za-z_]`)

// sanitizer fixes up an invalid HCL AST, as produced by the HCL parser for JSON
type astSanitizer struct{}

// output prints creates b printable HCL output and returns it.
func (v *astSanitizer) visit(n interface{}) {
	switch t := n.(type) {
	case *ast.File:
		v.visit(t.Node)
	case *ast.ObjectList:
		var index int
		for {
			if index == len(t.Items) {
				break
			}
			v.visit(t.Items[index])
			index++
		}
	case *ast.ObjectKey:
	case *ast.ObjectItem:
		v.visitObjectItem(t)
	case *ast.LiteralType:
	case *ast.ListType:
	case *ast.ObjectType:
		v.visit(t.List)
	default:
		fmt.Printf(" unknown type: %T\n", n)
	}

}

func (v *astSanitizer) visitObjectItem(o *ast.ObjectItem) {
	for i, k := range o.Keys {
		if i == 0 {
			text := k.Token.Text
			if text != "" && text[0] == '"' && text[len(text)-1] == '"' {
				v := text[1 : len(text)-1]
				safe := true
				for _, c := range v {
					if !strings.ContainsRune(safeChars, c) {
						safe = false
						break
					}
				}
				if safe {
					k.Token.Text = v
				}
			}
		}
	}
	switch t := o.Val.(type) {
	case *ast.LiteralType: // heredoc support
		if strings.HasPrefix(t.Token.Text, `"<<`) {
			t.Token.Text = t.Token.Text[1:]
			t.Token.Text = t.Token.Text[:len(t.Token.Text)-1]
			t.Token.Text = strings.Replace(t.Token.Text, `\n`, "\n", -1)
			t.Token.Text = strings.Replace(t.Token.Text, `\t`, "", -1)
			t.Token.Type = 10
			// check if text json for Unquote and Indent
			tmp := map[string]interface{}{}
			jsonTest := t.Token.Text
			lines := strings.Split(jsonTest, "\n")
			jsonTest = strings.Join(lines[1:len(lines)-1], "\n")
			jsonTest = strings.Replace(jsonTest, "\\\"", "\"", -1)
			// it's json we convert to heredoc back
			err := json.Unmarshal([]byte(jsonTest), &tmp)
			if err == nil {
				dataJsonBytes, err := json.MarshalIndent(tmp, "", "  ")
				if err == nil {
					jsonData := strings.Split(string(dataJsonBytes), "\n")
					// first line for heredoc
					jsonData = append([]string{lines[0]}, jsonData...)
					// last line for heredoc
					jsonData = append(jsonData, lines[len(lines)-1])
					hereDoc := strings.Join(jsonData, "\n")
					t.Token.Text = hereDoc
				}
			}
		}
	default:
	}

	// A hack so that Assign.IsValid is true, so that the printer will output =
	o.Assign.Line = 1

	v.visit(o.Val)
}

func HclPrint(data interface{}, mapsObjects map[string]struct{}) ([]byte, error) {
	dataJsonBytes, err := json.MarshalIndent(data, "", "  ")
	dataJson := string(dataJsonBytes)
	dataJson = strings.Replace(dataJson, "\\u003c", "<", -1)
	if err != nil {
		return []byte{}, fmt.Errorf("error marshalling terraform data to json: %v", err)
	}
	nodes, err := hclParcer.Parse([]byte(dataJson))
	if err != nil {
		return []byte{}, fmt.Errorf("error parsing terraform json: %v", err)
	}
	var sanitizer astSanitizer
	sanitizer.visit(nodes)

	var b bytes.Buffer
	err = hclPrinter.Fprint(&b, nodes)
	if err != nil {
		return nil, fmt.Errorf("error writing HCL: %v", err)
	}
	s := b.String()

	// Remove extra whitespace...
	s = strings.Replace(s, "\n\n", "\n", -1)

	// ...but leave whitespace between resources
	s = strings.Replace(s, "}\nresource", "}\n\nresource", -1)

	// We don't need to escape > or <
	s = strings.Replace(s, "\\u003c", "<", -1)
	s = strings.Replace(s, "\\u003e", ">", -1)

	// Apply Terraform style (alignment etc.)
	formatted := hclwrite.Format([]byte(s))
	formatted, err = hclPrinter.Format([]byte(s))
	// hack for support terraform 0.12
	formatted = terraform12Adjustments(formatted, mapsObjects)
	if err != nil {
		log.Println("Invalid HCL follows:")
		for i, line := range strings.Split(s, "\n") {
			fmt.Printf("%d\t%s", i+1, line)
		}
		return nil, fmt.Errorf("error formatting HCL: %v", err)
	}

	return formatted, nil
}

func terraform12Adjustments(formatted []byte, mapsObjects map[string]struct{}) []byte {
	s := string(formatted)
	old := " = {"
	new := " {"
	lines := strings.Split(s, "\n")
	for i, line := range lines {
		if !strings.Contains(line, old) {
			continue
		}
		key := strings.Trim(strings.Split(line, old)[0], " ")
		if _, exist := mapsObjects[key]; exist {
			continue
		}
		lines[i] = strings.Replace(line, old, new, -1)
	}
	s = strings.Join(lines, "\n")
	return []byte(s)
}

// Sanitize name for terraform style
func TfSanitize(name string) string {
	name = unsafeChars.ReplaceAllString(name, "--")
	return name
}

// Print hcl file from TerraformResource + provider
func HclPrintResource(resources []Resource, providerData map[string]interface{}) ([]byte, error) {
	resourcesByType := map[string]map[string]interface{}{}
	mapsObjects := map[string]struct{}{}
	for _, res := range resources {

		r := resourcesByType[res.InstanceInfo.Type]
		if r == nil {
			r = make(map[string]interface{})
			resourcesByType[res.InstanceInfo.Type] = r
		}

		if r[res.ResourceName] != nil {
			log.Println(resources)
			return []byte{}, fmt.Errorf("[ERR]: duplicate resource found: %s.%s", res.InstanceInfo.Type, res.ResourceName)
		}

		r[res.ResourceName] = res.Item

		for k := range res.InstanceState.Attributes {
			if strings.HasSuffix(k, ".%") {
				t := strings.Split(k, ".")
				key := t[len(t)-2]
				mapsObjects[key] = struct{}{}
			}
		}
	}

	data := map[string]interface{}{}
	if len(resourcesByType) > 0 {
		data["resource"] = resourcesByType
	}
	if len(providerData) > 0 {
		data["provider"] = providerData
	}
	var err error

	hclBytes, err := HclPrint(data, mapsObjects)
	if err != nil {
		return []byte{}, err
	}
	return hclBytes, nil
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/go-telegram-bot-api/telegram-bot-api"
	bt "github.com/ikool-cn/gobeanstalk-connection-pool"
)

var (
	telegramBot *TelegramBot
)

// TelegramBot ...
type TelegramBot struct {
	Name          string
	SelfChatID    int64
	ChannelChatID int64
	ComicPath     string
	DeleteDelay   time.Duration
	Client        *tgbotapi.BotAPI
	Queue         *bt.Pool
	Tube          string
}

// NewTelegramBot ...
func NewTelegramBot(cfg *TelegramConfig, btdAddr string) (t *TelegramBot) {
	bot, err := tgbotapi.NewBotAPI(cfg.Token)
	if err != nil {
		logger.Panicf("tg bot init failed: %+v", err)
	}
	delay, err := time.ParseDuration(cfg.DeleteDelay)
	if err != nil {
		logger.Panicf("delete delay error: %+v", err)
	}

	t = &TelegramBot{
		Name:          bot.Self.UserName,
		SelfChatID:    cfg.SelfChatID,
		ChannelChatID: cfg.ChannelChatID,
		ComicPath:     cfg.ComicPath,
		DeleteDelay:   delay,
		Client:        bot,
		Tube:          "tg",
	}
	t.Queue = &bt.Pool{
		Dial: func() (*bt.Conn, error) {
			return bt.Dial(btdAddr)
		},
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: 60 * time.Second,
		MaxLifetime: 180 * time.Second,
		Wait:        true,
	}
	return
}

func (t *TelegramBot) putQueue(msg []byte) {
	conn, err := t.Queue.Get()
	if err != nil {
		logger.Errorf("%+v: %s", err, string(msg))
		return
	}
	conn.Use(t.Tube)
	_, err = conn.Put(msg, 1, t.DeleteDelay, time.Minute)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
}

func (t *TelegramBot) send(chat int64, msg string) (tgbotapi.Message, error) {
	logger.Debugf("[%d]%s", chat, msg)
	return t.Client.Send(tgbotapi.NewMessage(chat, msg))
}

func (t *TelegramBot) delMessage() {
	for {
		conn, err := t.Queue.Get()
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		conn.Watch(t.Tube)
		job, err := conn.Reserve()
		if err != nil {
			logger.Warningf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		msg := &tgbotapi.Message{}
		err = json.Unmarshal(job.Body, msg)
		if err != nil {
			logger.Errorf("%+v", err)
			err = conn.Bury(job.ID, 0)
			if err != nil {
				logger.Errorf("%+v", err)
			}
			time.Sleep(3 * time.Second)
			continue
		}
		delMsg := tgbotapi.DeleteMessageConfig{
			ChatID:    msg.Chat.ID,
			MessageID: msg.MessageID,
		}
		logger.Infof(":[%s]{%s}", getMsgTitle(msg), strconv.Quote(msg.Text))

		_, err = t.Client.DeleteMessage(delMsg)
		if err != nil {
			logger.Errorf("%+v", err)
			err = conn.Bury(job.ID, 0)
			if err != nil {
				logger.Errorf("%+v", err)
			}
			time.Sleep(3 * time.Second)
			continue
		}
		err = conn.Delete(job.ID)
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
		}
		t.Queue.Release(conn, false)
	}
}

func (t *TelegramBot) tgBot() {
	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60
	for {
		updates, err := t.Client.GetUpdatesChan(u)
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		var message *tgbotapi.Message
		for update := range updates {
			if update.Message != nil {
				message = update.Message
			} else if update.EditedMessage != nil {
				message = update.EditedMessage
			} else if update.CallbackQuery != nil {
				logger.Infof(
					"recv:(%s)[%s]reaction:{%s}",
					update.CallbackQuery.ChatInstance,
					update.CallbackQuery.From.String(),
					update.CallbackQuery.Data,
				)
				data := strings.SplitN(update.CallbackQuery.Data, ":", 2)
				switch data[0] {
				case "comic", "pic":
					go onReaction(t, update.CallbackQuery)
				}
				continue
			} else {
				continue
			}
			if message.Chat.IsGroup() {
				logger.Infof(
					"recv:(%s)[%s]{%s}",
					message.Chat.Title,
					message.From.String(),
					strconv.Quote(message.Text))
			} else {
				logger.Infof(
					"recv:[%s]{%s}",
					message.From.String(),
					strconv.Quote(message.Text),
				)
			}

			if message.IsCommand() {
				switch message.Command() {
				case "start":
					go onStart(t, message)
				case "comic":
					go onComic(t, message)
				case "pic":
					go onPic(t, message)
				default:
					logger.Infof("ignore unkown cmd: %+v", message.Command())
					continue
				}
			} else {
				if message.Text == "" {
					continue
				}
				checkRepeat(t, message)
			}
		}
		logger.Warning("tg bot restarted.")
		time.Sleep(3 * time.Second)
	}
}

func checkRepeat(t *TelegramBot, message *tgbotapi.Message) {
	key := "tg_last_" + strconv.FormatInt(message.Chat.ID, 10)
	flattendMsg := strings.TrimSpace(message.Text)
	defer redisClient.LTrim(key, 0, 10)
	defer redisClient.LPush(key, flattendMsg)

	lastMsgs, err := redisClient.LRange(key, 0, 6).Result()
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	i := 0
	for _, s := range lastMsgs {
		if s == flattendMsg {
			i++
		}
	}
	if i > 1 {
		redisClient.Del(key)
		logger.Infof("repeat: %s", strconv.Quote(message.Text))
		msg := tgbotapi.NewMessage(message.Chat.ID, message.Text)
		t.Client.Send(msg)
	}
}

func onStart(t *TelegramBot, message *tgbotapi.Message) {
	msg := tgbotapi.NewMessage(message.Chat.ID, "呀呀呀")
	msg.ReplyToMessageID = message.MessageID
	t.Client.Send(msg)
}

func onComic(t *TelegramBot, message *tgbotapi.Message) {
	files, err := filepath.Glob(t.ComicPath)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	rand.Seed(time.Now().UnixNano())
	file := files[rand.Intn(len(files))]
	number := strings.Split(strings.Split(file, "@")[1], ".")[0]
	msg := tgbotapi.NewMessage(message.Chat.ID, "🔞 https://nhentai.net/g/"+number)

	msg.ReplyMarkup = buildInlineKeyboardMarkup("comic", number)

	logger.Infof("send:[%s]{%s}", getMsgTitle(message), strconv.Quote(file))
	msgSent, err := t.Client.Send(msg)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	data, err := json.Marshal(msgSent)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	t.putQueue(data)
}

func onPic(t *TelegramBot, message *tgbotapi.Message) {
	files, err := filepath.Glob(filepath.Join(twitterBot.ImgPath, "*"))
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	if files == nil {
		logger.Error("find no pics")
	}
	rand.Seed(time.Now().UnixNano())
	file := files[rand.Intn(len(files))]

	logger.Infof("send:[%s]{%s}", getMsgTitle(message), strconv.Quote(file))

	msg := tgbotapi.NewDocumentUpload(message.Chat.ID, file)
	msg.ReplyMarkup = buildInlineKeyboardMarkup("pic", filepath.Base(file))

	_, err = t.Client.Send(msg)
	if err != nil {
		logger.Errorf("%+v", err)
	}
}

func onReaction(t *TelegramBot, callbackQuery *tgbotapi.CallbackQuery) {
	var callbackText string

	_type, _id, reaction, err := saveReaction(callbackQuery.Data, callbackQuery.From.ID)
	if err == nil {
		diss := redisClient.SCard(buildReactionKey(_type, _id, "diss")).Val()
		if diss <= 1 {
			msg := tgbotapi.NewEditMessageReplyMarkup(
				callbackQuery.Message.Chat.ID,
				callbackQuery.Message.MessageID,
				buildInlineKeyboardMarkup(_type, _id),
			)
			_, err = t.Client.Send(msg)
		} else {
			delMsg := tgbotapi.DeleteMessageConfig{
				ChatID:    callbackQuery.Message.Chat.ID,
				MessageID: callbackQuery.Message.MessageID,
			}
			_, err = t.Client.DeleteMessage(delMsg)
			if err == nil {
				err = probate(_type, _id)
			}
		}
	}

	if err != nil {
		logger.Debugf("%+v", err)
		callbackText = err.Error()
	} else {
		callbackText = reaction + " " + _id + "!"
	}

	callbackMsg := tgbotapi.NewCallback(callbackQuery.ID, callbackText)
	_, err = t.Client.AnswerCallbackQuery(callbackMsg)
	if err != nil {
		logger.Errorf("%+v", err)
	}
}

func getMsgTitle(m *tgbotapi.Message) string {
	if m.Chat.IsGroup() {
		return m.Chat.Title
	}
	return m.From.String()
}

func buildReactionData(_type, _id, reaction string) string {
	return _type + ":" + _id + ":" + reaction
}
func buildReactionKey(_type, _id, reaction string) string {
	return "reaction_" + buildReactionData(_type, _id, reaction)
}

func buildInlineKeyboardMarkup(_type, _id string) tgbotapi.InlineKeyboardMarkup {

	likeCount, _ := redisClient.SCard(buildReactionKey(_type, _id, "like")).Result()
	dissCount, _ := redisClient.SCard(buildReactionKey(_type, _id, "diss")).Result()

	likeText := "❤"
	if likeCount > 0 {
		likeText = likeText + " " + strconv.FormatInt(likeCount, 10)
	}
	dissText := "💔"
	if dissCount > 0 {
		dissText = dissText + " " + strconv.FormatInt(dissCount, 10)
	}

	row := tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData(likeText, buildReactionData(_type, _id, "like")),
		tgbotapi.NewInlineKeyboardButtonData(dissText, buildReactionData(_type, _id, "diss")),
	)
	return tgbotapi.NewInlineKeyboardMarkup(row)
}

func saveReaction(key string, user int) (_type, _id, reaction string int64, err error) {
	token := strings.Split(key, ":")
	if len(token) != 3 {
		err = fmt.Errorf("react data error: %s", key)
		return
	}
	_type = token[0]
	_id = token[1]
	reaction = token[2]

	pipe := redisClient.Pipeline()
	switch reaction {
	case "like":
		likeCount := pipe.SAdd(buildReactionKey(_type, _id, "like"), strconv.Itoa(user))
		dissCount := pipe.SRem(buildReactionKey(_type, _id, "diss"), strconv.Itoa(user))
		_, err = pipe.Exec()
		if err == nil {
			if likeCount.Val()+dissCount.Val() == 0 {
				err = fmt.Errorf("not modified")
			}
		}
	case "diss":
		dissCount := pipe.SAdd(buildReactionKey(_type, _id, "diss"), strconv.Itoa(user))
		likeCount := pipe.SRem(buildReactionKey(_type, _id, "like"), strconv.Itoa(user))
		_, err = pipe.Exec()
		if err == nil {
			if likeCount.Val()+dissCount.Val() == 0 {
				err = fmt.Errorf("not modified")
			}
		}
	default:
		err = fmt.Errorf("react type error: %s", key)
	}
	return
}
<commit_msg>fix typo<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/go-telegram-bot-api/telegram-bot-api"
	bt "github.com/ikool-cn/gobeanstalk-connection-pool"
)

var (
	telegramBot *TelegramBot
)

// TelegramBot ...
type TelegramBot struct {
	Name          string
	SelfChatID    int64
	ChannelChatID int64
	ComicPath     string
	DeleteDelay   time.Duration
	Client        *tgbotapi.BotAPI
	Queue         *bt.Pool
	Tube          string
}

// NewTelegramBot ...
func NewTelegramBot(cfg *TelegramConfig, btdAddr string) (t *TelegramBot) {
	bot, err := tgbotapi.NewBotAPI(cfg.Token)
	if err != nil {
		logger.Panicf("tg bot init failed: %+v", err)
	}
	delay, err := time.ParseDuration(cfg.DeleteDelay)
	if err != nil {
		logger.Panicf("delete delay error: %+v", err)
	}

	t = &TelegramBot{
		Name:          bot.Self.UserName,
		SelfChatID:    cfg.SelfChatID,
		ChannelChatID: cfg.ChannelChatID,
		ComicPath:     cfg.ComicPath,
		DeleteDelay:   delay,
		Client:        bot,
		Tube:          "tg",
	}
	t.Queue = &bt.Pool{
		Dial: func() (*bt.Conn, error) {
			return bt.Dial(btdAddr)
		},
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: 60 * time.Second,
		MaxLifetime: 180 * time.Second,
		Wait:        true,
	}
	return
}

func (t *TelegramBot) putQueue(msg []byte) {
	conn, err := t.Queue.Get()
	if err != nil {
		logger.Errorf("%+v: %s", err, string(msg))
		return
	}
	conn.Use(t.Tube)
	_, err = conn.Put(msg, 1, t.DeleteDelay, time.Minute)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
}

func (t *TelegramBot) send(chat int64, msg string) (tgbotapi.Message, error) {
	logger.Debugf("[%d]%s", chat, msg)
	return t.Client.Send(tgbotapi.NewMessage(chat, msg))
}

func (t *TelegramBot) delMessage() {
	for {
		conn, err := t.Queue.Get()
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		conn.Watch(t.Tube)
		job, err := conn.Reserve()
		if err != nil {
			logger.Warningf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		msg := &tgbotapi.Message{}
		err = json.Unmarshal(job.Body, msg)
		if err != nil {
			logger.Errorf("%+v", err)
			err = conn.Bury(job.ID, 0)
			if err != nil {
				logger.Errorf("%+v", err)
			}
			time.Sleep(3 * time.Second)
			continue
		}
		delMsg := tgbotapi.DeleteMessageConfig{
			ChatID:    msg.Chat.ID,
			MessageID: msg.MessageID,
		}
		logger.Infof(":[%s]{%s}", getMsgTitle(msg), strconv.Quote(msg.Text))

		_, err = t.Client.DeleteMessage(delMsg)
		if err != nil {
			logger.Errorf("%+v", err)
			err = conn.Bury(job.ID, 0)
			if err != nil {
				logger.Errorf("%+v", err)
			}
			time.Sleep(3 * time.Second)
			continue
		}
		err = conn.Delete(job.ID)
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
		}
		t.Queue.Release(conn, false)
	}
}

func (t *TelegramBot) tgBot() {
	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60
	for {
		updates, err := t.Client.GetUpdatesChan(u)
		if err != nil {
			logger.Errorf("%+v", err)
			time.Sleep(3 * time.Second)
			continue
		}
		var message *tgbotapi.Message
		for update := range updates {
			if update.Message != nil {
				message = update.Message
			} else if update.EditedMessage != nil {
				message = update.EditedMessage
			} else if update.CallbackQuery != nil {
				logger.Infof(
					"recv:(%s)[%s]reaction:{%s}",
					update.CallbackQuery.ChatInstance,
					update.CallbackQuery.From.String(),
					update.CallbackQuery.Data,
				)
				data := strings.SplitN(update.CallbackQuery.Data, ":", 2)
				switch data[0] {
				case "comic", "pic":
					go onReaction(t, update.CallbackQuery)
				}
				continue
			} else {
				continue
			}
			if message.Chat.IsGroup() {
				logger.Infof(
					"recv:(%s)[%s]{%s}",
					message.Chat.Title,
					message.From.String(),
					strconv.Quote(message.Text))
			} else {
				logger.Infof(
					"recv:[%s]{%s}",
					message.From.String(),
					strconv.Quote(message.Text),
				)
			}

			if message.IsCommand() {
				switch message.Command() {
				case "start":
					go onStart(t, message)
				case "comic":
					go onComic(t, message)
				case "pic":
					go onPic(t, message)
				default:
					logger.Infof("ignore unkown cmd: %+v", message.Command())
					continue
				}
			} else {
				if message.Text == "" {
					continue
				}
				checkRepeat(t, message)
			}
		}
		logger.Warning("tg bot restarted.")
		time.Sleep(3 * time.Second)
	}
}

func checkRepeat(t *TelegramBot, message *tgbotapi.Message) {
	key := "tg_last_" + strconv.FormatInt(message.Chat.ID, 10)
	flattendMsg := strings.TrimSpace(message.Text)
	defer redisClient.LTrim(key, 0, 10)
	defer redisClient.LPush(key, flattendMsg)

	lastMsgs, err := redisClient.LRange(key, 0, 6).Result()
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	i := 0
	for _, s := range lastMsgs {
		if s == flattendMsg {
			i++
		}
	}
	if i > 1 {
		redisClient.Del(key)
		logger.Infof("repeat: %s", strconv.Quote(message.Text))
		msg := tgbotapi.NewMessage(message.Chat.ID, message.Text)
		t.Client.Send(msg)
	}
}

func onStart(t *TelegramBot, message *tgbotapi.Message) {
	msg := tgbotapi.NewMessage(message.Chat.ID, "呀呀呀")
	msg.ReplyToMessageID = message.MessageID
	t.Client.Send(msg)
}

func onComic(t *TelegramBot, message *tgbotapi.Message) {
	files, err := filepath.Glob(t.ComicPath)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	rand.Seed(time.Now().UnixNano())
	file := files[rand.Intn(len(files))]
	number := strings.Split(strings.Split(file, "@")[1], ".")[0]
	msg := tgbotapi.NewMessage(message.Chat.ID, "🔞 https://nhentai.net/g/"+number)

	msg.ReplyMarkup = buildInlineKeyboardMarkup("comic", number)

	logger.Infof("send:[%s]{%s}", getMsgTitle(message), strconv.Quote(file))
	msgSent, err := t.Client.Send(msg)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	data, err := json.Marshal(msgSent)
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	t.putQueue(data)
}

func onPic(t *TelegramBot, message *tgbotapi.Message) {
	files, err := filepath.Glob(filepath.Join(twitterBot.ImgPath, "*"))
	if err != nil {
		logger.Errorf("%+v", err)
		return
	}
	if files == nil {
		logger.Error("find no pics")
	}
	rand.Seed(time.Now().UnixNano())
	file := files[rand.Intn(len(files))]

	logger.Infof("send:[%s]{%s}", getMsgTitle(message), strconv.Quote(file))

	msg := tgbotapi.NewDocumentUpload(message.Chat.ID, file)
	msg.ReplyMarkup = buildInlineKeyboardMarkup("pic", filepath.Base(file))

	_, err = t.Client.Send(msg)
	if err != nil {
		logger.Errorf("%+v", err)
	}
}

func onReaction(t *TelegramBot, callbackQuery *tgbotapi.CallbackQuery) {
	var callbackText string

	_type, _id, reaction, err := saveReaction(callbackQuery.Data, callbackQuery.From.ID)
	if err == nil {
		diss := redisClient.SCard(buildReactionKey(_type, _id, "diss")).Val()
		if diss <= 1 {
			msg := tgbotapi.NewEditMessageReplyMarkup(
				callbackQuery.Message.Chat.ID,
				callbackQuery.Message.MessageID,
				buildInlineKeyboardMarkup(_type, _id),
			)
			_, err = t.Client.Send(msg)
		} else {
			delMsg := tgbotapi.DeleteMessageConfig{
				ChatID:    callbackQuery.Message.Chat.ID,
				MessageID: callbackQuery.Message.MessageID,
			}
			_, err = t.Client.DeleteMessage(delMsg)
			if err == nil {
				err = probate(_type, _id)
			}
		}
	}

	if err != nil {
		logger.Debugf("%+v", err)
		callbackText = err.Error()
	} else {
		callbackText = reaction + " " + _id + "!"
	}

	callbackMsg := tgbotapi.NewCallback(callbackQuery.ID, callbackText)
	_, err = t.Client.AnswerCallbackQuery(callbackMsg)
	if err != nil {
		logger.Errorf("%+v", err)
	}
}

func getMsgTitle(m *tgbotapi.Message) string {
	if m.Chat.IsGroup() {
		return m.Chat.Title
	}
	return m.From.String()
}

func buildReactionData(_type, _id, reaction string) string {
	return _type + ":" + _id + ":" + reaction
}
func buildReactionKey(_type, _id, reaction string) string {
	return "reaction_" + buildReactionData(_type, _id, reaction)
}

func buildInlineKeyboardMarkup(_type, _id string) tgbotapi.InlineKeyboardMarkup {

	likeCount, _ := redisClient.SCard(buildReactionKey(_type, _id, "like")).Result()
	dissCount, _ := redisClient.SCard(buildReactionKey(_type, _id, "diss")).Result()

	likeText := "❤"
	if likeCount > 0 {
		likeText = likeText + " " + strconv.FormatInt(likeCount, 10)
	}
	dissText := "💔"
	if dissCount > 0 {
		dissText = dissText + " " + strconv.FormatInt(dissCount, 10)
	}

	row := tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData(likeText, buildReactionData(_type, _id, "like")),
		tgbotapi.NewInlineKeyboardButtonData(dissText, buildReactionData(_type, _id, "diss")),
	)
	return tgbotapi.NewInlineKeyboardMarkup(row)
}

func saveReaction(key string, user int) (_type, _id, reaction string, err error) {
	token := strings.Split(key, ":")
	if len(token) != 3 {
		err = fmt.Errorf("react data error: %s", key)
		return
	}
	_type = token[0]
	_id = token[1]
	reaction = token[2]

	pipe := redisClient.Pipeline()
	switch reaction {
	case "like":
		likeCount := pipe.SAdd(buildReactionKey(_type, _id, "like"), strconv.Itoa(user))
		dissCount := pipe.SRem(buildReactionKey(_type, _id, "diss"), strconv.Itoa(user))
		_, err = pipe.Exec()
		if err == nil {
			if likeCount.Val()+dissCount.Val() == 0 {
				err = fmt.Errorf("not modified")
			}
		}
	case "diss":
		dissCount := pipe.SAdd(buildReactionKey(_type, _id, "diss"), strconv.Itoa(user))
		likeCount := pipe.SRem(buildReactionKey(_type, _id, "like"), strconv.Itoa(user))
		_, err = pipe.Exec()
		if err == nil {
			if likeCount.Val()+dissCount.Val() == 0 {
				err = fmt.Errorf("not modified")
			}
		}
	default:
		err = fmt.Errorf("react type error: %s", key)
	}
	return
}
<|endoftext|>


<commit_before>// Copyright (c) 2017, Mitchell Cooper
package main

import (
	"encoding/json"
	"errors"
	wikiclient "github.com/cooper/go-wikiclient"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

var templateDirs string
var templates = make(map[string]wikiTemplate)

type wikiTemplate struct {
	path       string             // template directory path
	template   *template.Template // master HTML template
	staticPath string             // static file directory path, if any
	staticRoot string             // static file directory HTTP root, if any
	manifest   struct {

		// human-readable template name
		// Name   string

		// template author's name
		// Author string

		// URL to template code on the web, such as GitHub repository
		// Code   string

		// wiki logo info
		Logo struct {

			// ideally one of these dimensions will be specified and the other
			// not. used for the logo specified by the wiki 'logo' directive.
			// usually the height is specified. if both are present, the
			// logo will be generated in those exact dimensions.
			Height int
			Width  int
		}
	}
}

// search all template directories for a template by its name
func findTemplate(name string) (wikiTemplate, error) {

	// template is already cached
	if t, ok := templates[name]; ok {
		return t, nil
	}

	for _, templateDir := range strings.Split(templateDirs, ",") {
		templatePath := templateDir + "/" + name
		t, err := loadTemplate(name, templatePath)

		// an error occurred in loading the template
		if err != nil {
			return t, err
		}

		// no template but no error means try the next directory
		if t.template == nil {
			continue
		}

		return t, nil
	}

	// never found a template
	return wikiTemplate{}, errors.New("unable to find template " + name)
}

// load a template from its known path
func loadTemplate(name, templatePath string) (wikiTemplate, error) {
	var t wikiTemplate
	var tryNextDirectory bool

	// template is already cached
	if t, ok := templates[name]; ok {
		return t, nil
	}

	// parse HTML templates
	tmpl := template.New("")
	err := filepath.Walk(templatePath, func(filePath string, info os.FileInfo, err error) error {

		// walk error, probably missing template
		if err != nil {
			tryNextDirectory = true
			return err
		}

		// found template file
		if strings.HasSuffix(filePath, ".tpl") {

			// error in parsing
			if _, err := tmpl.ParseFiles(filePath); err != nil {
				return err
			}
		}

		// found static content directory
		if info.IsDir() && info.Name() == "static" {
			t.staticPath = filePath
			t.staticRoot = "/tmpl/" + name
			fileServer := http.FileServer(http.Dir(filePath))
			pfx := t.staticRoot + "/"
			http.Handle(pfx, http.StripPrefix(pfx, fileServer))
			log.Printf("[%s] template registered: %s", name, pfx)
		}

		// found manifest
		if info.Name() == "manifest.json" {

			// couldn't read manifest
			contents, err := ioutil.ReadFile(filePath)
			if err != nil {
				return err
			}

			// couldn't parse manifest
			if err := json.Unmarshal(contents, &t.manifest); err != nil {
				return err
			}
		}

		return err
	})

	// not found
	if tryNextDirectory {
		return t, nil
	}

	// other error
	if err != nil {
		return t, err
	}

	// cache the template
	t.path = templatePath
	t.template = tmpl
	templates[name] = t

	return t, nil
}

type wikiPage struct {
	WholeTitle string             // optional, shown in <title> as-is
	Title      string             // page title
	WikiTitle  string             // wiki title
	WikiLogo   string             // path to wiki logo image
	WikiRoot   string             // wiki HTTP root
	Res        wikiclient.Message // response
	StaticRoot string             // path to static resources
	navigation []interface{}      // slice of nav items [display, url]
}

type navItem struct {
	Display string
	Link    string
}

func (p wikiPage) VisibleTitle() string {
	if p.WholeTitle != "" {
		return p.WholeTitle
	}
	return p.Title + " - " + p.WikiTitle
}

func (p wikiPage) PageCSS() template.CSS {
	return template.CSS(p.Res.Get("css"))
}

func (p wikiPage) HTMLContent() template.HTML {
	return template.HTML(p.Res.Get("content"))
}

func (p wikiPage) Navigation() []navItem {

	// no navigation
	if len(p.navigation) != 2 {
		return nil
	}

	// first item is ordered keys, second is values
	displays := p.navigation[0].([]interface{})
	urls := p.navigation[1].([]interface{})
	items := make([]navItem, len(displays))
	for i := 0; i < len(items); i++ {
		items[i] = navItem{
			displays[i].(string),
			urls[i].(string),
		}
	}

	return items
}
<commit_msg>never display wiki title twice<commit_after>// Copyright (c) 2017, Mitchell Cooper
package main

import (
	"encoding/json"
	"errors"
	wikiclient "github.com/cooper/go-wikiclient"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

var templateDirs string
var templates = make(map[string]wikiTemplate)

type wikiTemplate struct {
	path       string             // template directory path
	template   *template.Template // master HTML template
	staticPath string             // static file directory path, if any
	staticRoot string             // static file directory HTTP root, if any
	manifest   struct {

		// human-readable template name
		// Name   string

		// template author's name
		// Author string

		// URL to template code on the web, such as GitHub repository
		// Code   string

		// wiki logo info
		Logo struct {

			// ideally one of these dimensions will be specified and the other
			// not. used for the logo specified by the wiki 'logo' directive.
			// usually the height is specified. if both are present, the
			// logo will be generated in those exact dimensions.
			Height int
			Width  int
		}
	}
}

// search all template directories for a template by its name
func findTemplate(name string) (wikiTemplate, error) {

	// template is already cached
	if t, ok := templates[name]; ok {
		return t, nil
	}

	for _, templateDir := range strings.Split(templateDirs, ",") {
		templatePath := templateDir + "/" + name
		t, err := loadTemplate(name, templatePath)

		// an error occurred in loading the template
		if err != nil {
			return t, err
		}

		// no template but no error means try the next directory
		if t.template == nil {
			continue
		}

		return t, nil
	}

	// never found a template
	return wikiTemplate{}, errors.New("unable to find template " + name)
}

// load a template from its known path
func loadTemplate(name, templatePath string) (wikiTemplate, error) {
	var t wikiTemplate
	var tryNextDirectory bool

	// template is already cached
	if t, ok := templates[name]; ok {
		return t, nil
	}

	// parse HTML templates
	tmpl := template.New("")
	err := filepath.Walk(templatePath, func(filePath string, info os.FileInfo, err error) error {

		// walk error, probably missing template
		if err != nil {
			tryNextDirectory = true
			return err
		}

		// found template file
		if strings.HasSuffix(filePath, ".tpl") {

			// error in parsing
			if _, err := tmpl.ParseFiles(filePath); err != nil {
				return err
			}
		}

		// found static content directory
		if info.IsDir() && info.Name() == "static" {
			t.staticPath = filePath
			t.staticRoot = "/tmpl/" + name
			fileServer := http.FileServer(http.Dir(filePath))
			pfx := t.staticRoot + "/"
			http.Handle(pfx, http.StripPrefix(pfx, fileServer))
			log.Printf("[%s] template registered: %s", name, pfx)
		}

		// found manifest
		if info.Name() == "manifest.json" {

			// couldn't read manifest
			contents, err := ioutil.ReadFile(filePath)
			if err != nil {
				return err
			}

			// couldn't parse manifest
			if err := json.Unmarshal(contents, &t.manifest); err != nil {
				return err
			}
		}

		return err
	})

	// not found
	if tryNextDirectory {
		return t, nil
	}

	// other error
	if err != nil {
		return t, err
	}

	// cache the template
	t.path = templatePath
	t.template = tmpl
	templates[name] = t

	return t, nil
}

type wikiPage struct {
	WholeTitle string             // optional, shown in <title> as-is
	Title      string             // page title
	WikiTitle  string             // wiki title
	WikiLogo   string             // path to wiki logo image
	WikiRoot   string             // wiki HTTP root
	Res        wikiclient.Message // response
	StaticRoot string             // path to static resources
	navigation []interface{}      // slice of nav items [display, url]
}

type navItem struct {
	Display string
	Link    string
}

func (p wikiPage) VisibleTitle() string {
	if p.WholeTitle != "" {
		return p.WholeTitle
	}
	if p.Title == p.WikiTitle {
		return p.Title
	}
	return p.Title + " - " + p.WikiTitle
}

func (p wikiPage) PageCSS() template.CSS {
	return template.CSS(p.Res.Get("css"))
}

func (p wikiPage) HTMLContent() template.HTML {
	return template.HTML(p.Res.Get("content"))
}

func (p wikiPage) Navigation() []navItem {

	// no navigation
	if len(p.navigation) != 2 {
		return nil
	}

	// first item is ordered keys, second is values
	displays := p.navigation[0].([]interface{})
	urls := p.navigation[1].([]interface{})
	items := make([]navItem, len(displays))
	for i := 0; i < len(items); i++ {
		items[i] = navItem{
			displays[i].(string),
			urls[i].(string),
		}
	}

	return items
}
<|endoftext|>


<commit_before>package cli

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/jinzhu/now"
)

// Time wrap time.Time
type Time time.Time

func init() {
	now.TimeFormats = append(now.TimeFormats, time.ANSIC)
	now.TimeFormats = append(now.TimeFormats, time.UnixDate)
	now.TimeFormats = append(now.TimeFormats, time.RubyDate)
	now.TimeFormats = append(now.TimeFormats, time.RFC822)
	now.TimeFormats = append(now.TimeFormats, time.RFC822Z)
	now.TimeFormats = append(now.TimeFormats, time.RFC850)
	now.TimeFormats = append(now.TimeFormats, time.RFC1123)
	now.TimeFormats = append(now.TimeFormats, time.RFC1123Z)
	now.TimeFormats = append(now.TimeFormats, time.RFC3339)
	now.TimeFormats = append(now.TimeFormats, time.RFC3339Nano)
	now.TimeFormats = append(now.TimeFormats, time.Kitchen)
	now.TimeFormats = append(now.TimeFormats, time.Stamp)
	now.TimeFormats = append(now.TimeFormats, time.StampMilli)
	now.TimeFormats = append(now.TimeFormats, time.StampMicro)
	now.TimeFormats = append(now.TimeFormats, time.StampNano)
}

func (t *Time) Decode(s string) error {
	v, err := now.Parse(s)
	if err != nil {
		return err
	}
	*t = Time(v)
	return nil
}

func (t Time) Encode() string {
	return time.Time(t).Format(time.RFC3339Nano)
}

// Duration wrap time.Duration
type Duration time.Duration

func (d *Duration) Decode(s string) error {
	v, err := time.ParseDuration(s)
	if err != nil {
		return err
	}
	*d = Duration(v)
	return nil
}

func (d Duration) Encode() string {
	return time.Duration(d).String()
}

// File reads data from file or stdin(if filename is empty)
type File struct {
	filename string
	data     []byte
}

func (rf File) Data() []byte {
	return rf.data
}

func (rf File) String() string {
	return string(rf.data)
}

func (rf *File) Decode(s string) error {
	var (
		data []byte
		err  error
	)
	if s == "" {
		data, err = ioutil.ReadAll(os.Stdin)
	} else {
		data, err = ioutil.ReadFile(s)
	}
	if err != nil {
		return err
	}
	rf.data = data
	rf.filename = s
	return nil
}

func (rf File) Encode() string {
	return rf.filename
}

// PidFile
type PidFile struct {
	filename string
}

func (pid PidFile) String() string {
	return pid.filename
}

func (pid *PidFile) Decode(s string) error {
	pid.filename = s
	dir, _ := filepath.Split(pid.filename)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}
	if content, err := ioutil.ReadFile(pid.filename); err == nil {
		pidStr := strings.TrimSpace(string(content))
		if pidStr == "" {
			return nil
		}
		if _, err := os.Stat(filepath.Join("/proc", pidStr)); err == nil {
			return fmt.Errorf("pid file found, ensoure %s is not running", os.Args[0])
		}
	}
	if err := ioutil.WriteFile(pid.filename, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
		return err
	}
	return nil
}

func (pid PidFile) Remove() error {
	if pid.filename != "" {
		return os.Remove(pid.filename)
	}
	return nil
}
<commit_msg>fix PidFile Decode method<commit_after>package cli

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/jinzhu/now"
)

// Time wrap time.Time
type Time time.Time

func init() {
	now.TimeFormats = append(now.TimeFormats, time.ANSIC)
	now.TimeFormats = append(now.TimeFormats, time.UnixDate)
	now.TimeFormats = append(now.TimeFormats, time.RubyDate)
	now.TimeFormats = append(now.TimeFormats, time.RFC822)
	now.TimeFormats = append(now.TimeFormats, time.RFC822Z)
	now.TimeFormats = append(now.TimeFormats, time.RFC850)
	now.TimeFormats = append(now.TimeFormats, time.RFC1123)
	now.TimeFormats = append(now.TimeFormats, time.RFC1123Z)
	now.TimeFormats = append(now.TimeFormats, time.RFC3339)
	now.TimeFormats = append(now.TimeFormats, time.RFC3339Nano)
	now.TimeFormats = append(now.TimeFormats, time.Kitchen)
	now.TimeFormats = append(now.TimeFormats, time.Stamp)
	now.TimeFormats = append(now.TimeFormats, time.StampMilli)
	now.TimeFormats = append(now.TimeFormats, time.StampMicro)
	now.TimeFormats = append(now.TimeFormats, time.StampNano)
}

func (t *Time) Decode(s string) error {
	v, err := now.Parse(s)
	if err != nil {
		return err
	}
	*t = Time(v)
	return nil
}

func (t Time) Encode() string {
	return time.Time(t).Format(time.RFC3339Nano)
}

// Duration wrap time.Duration
type Duration time.Duration

func (d *Duration) Decode(s string) error {
	v, err := time.ParseDuration(s)
	if err != nil {
		return err
	}
	*d = Duration(v)
	return nil
}

func (d Duration) Encode() string {
	return time.Duration(d).String()
}

// File reads data from file or stdin(if filename is empty)
type File struct {
	filename string
	data     []byte
}

func (rf File) Data() []byte {
	return rf.data
}

func (rf File) String() string {
	return string(rf.data)
}

func (rf *File) Decode(s string) error {
	var (
		data []byte
		err  error
	)
	if s == "" {
		data, err = ioutil.ReadAll(os.Stdin)
	} else {
		data, err = ioutil.ReadFile(s)
	}
	if err != nil {
		return err
	}
	rf.data = data
	rf.filename = s
	return nil
}

func (rf File) Encode() string {
	return rf.filename
}

// PidFile
type PidFile struct {
	filename string
}

func (pid PidFile) String() string {
	return pid.filename
}

func (pid *PidFile) Decode(s string) error {
	pid.filename = s
	return nil
}

func (pid *PidFile) New() error {
	dir, _ := filepath.Split(pid.filename)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("PidFile: %v", err)
	}
	if content, err := ioutil.ReadFile(pid.filename); err == nil {
		pidStr := strings.TrimSpace(string(content))
		if pidStr == "" {
			return nil
		}
		if _, err := os.Stat(filepath.Join("/proc", pidStr)); err == nil {
			return fmt.Errorf("pid file found, ensoure %s is not running", os.Args[0])
		}
	}
	if err := ioutil.WriteFile(pid.filename, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
		return err
	}
	return nil
}

func (pid PidFile) Remove() error {
	if pid.filename != "" {
		return os.Remove(pid.filename)
	}
	return nil
}
<|endoftext|>


<commit_before>package stdlib

import (
	"bufio"
	"io/ioutil"
	"os"
	"os/exec"

	"github.com/PuerkitoBio/agora/runtime"
)

type OsMod struct {
	ctx *runtime.Ctx
	ob  runtime.Object
}

type file struct {
	runtime.Object
	f *os.File
	s *bufio.Scanner
}

func (o *OsMod) newFile(f *os.File) *file {
	ob := runtime.NewObject()
	of := &file{
		ob,
		f,
		nil,
	}
	ob.Set(runtime.String("Name"), runtime.String(f.Name()))
	ob.Set(runtime.String("Close"), runtime.NewNativeFunc(o.ctx, "os.File.Close", of.closeFile))
	ob.Set(runtime.String("ReadLine"), runtime.NewNativeFunc(o.ctx, "os.File.ReadLine", of.readLine))
	ob.Set(runtime.String("Seek"), runtime.NewNativeFunc(o.ctx, "os.File.Seek", of.seek))
	ob.Set(runtime.String("Write"), runtime.NewNativeFunc(o.ctx, "os.File.Write", of.write))
	ob.Set(runtime.String("WriteLine"), runtime.NewNativeFunc(o.ctx, "os.File.WriteLine", of.writeLine))
	return of
}

func (of *file) closeFile(args ...runtime.Val) runtime.Val {
	e := of.f.Close()
	if e != nil {
		panic(e)
	}
	return runtime.Nil
}

func (of *file) readLine(args ...runtime.Val) runtime.Val {
	if of.s == nil {
		of.s = bufio.NewScanner(of.f)
	}
	if of.s.Scan() {
		return runtime.String(of.s.Text())
	}
	if e := of.s.Err(); e != nil {
		panic(e)
	}
	return runtime.Nil
}

func (of *file) seek(args ...runtime.Val) runtime.Val {
	off := int64(0)
	if len(args) > 0 {
		off = args[0].Int()
	}
	rel := 0
	if len(args) > 1 {
		rel = int(args[1].Int())
	}
	n, e := of.f.Seek(off, rel)
	if e != nil {
		panic(e)
	}
	return runtime.Number(n)
}

func (of *file) write(args ...runtime.Val) runtime.Val {
	n := 0
	for _, v := range args {
		m, e := of.f.WriteString(v.String())
		if e != nil {
			panic(e)
		}
		n += m
	}
	return runtime.Number(n)
}

func (of *file) writeLine(args ...runtime.Val) runtime.Val {
	n := of.write(args...)
	m, e := of.f.WriteString("\n")
	if e != nil {
		panic(e)
	}
	return runtime.Number(int(n.Int()) + m)
}

func (o *OsMod) ID() string {
	return "os"
}

func (o *OsMod) Run(_ ...runtime.Val) (v runtime.Val, err error) {
	defer runtime.PanicToError(&err)
	if o.ob == nil {
		// Prepare the object
		o.ob = runtime.NewObject()
		o.ob.Set(runtime.String("PathSeparator"), runtime.String(os.PathSeparator))
		o.ob.Set(runtime.String("PathListSeparator"), runtime.String(os.PathListSeparator))
		o.ob.Set(runtime.String("DevNull"), runtime.String(os.DevNull))
		o.ob.Set(runtime.String("Exec"), runtime.NewNativeFunc(o.ctx, "os.Exec", o.os_Exec))
		o.ob.Set(runtime.String("Exit"), runtime.NewNativeFunc(o.ctx, "os.Exit", o.os_Exit))
		o.ob.Set(runtime.String("Getenv"), runtime.NewNativeFunc(o.ctx, "os.Getenv", o.os_Getenv))
		o.ob.Set(runtime.String("Getwd"), runtime.NewNativeFunc(o.ctx, "os.Getwd", o.os_Getwd))
		o.ob.Set(runtime.String("ReadFile"), runtime.NewNativeFunc(o.ctx, "os.ReadFile", o.os_ReadFile))
		o.ob.Set(runtime.String("WriteFile"), runtime.NewNativeFunc(o.ctx, "os.WriteFile", o.os_WriteFile))
		o.ob.Set(runtime.String("Open"), runtime.NewNativeFunc(o.ctx, "os.Open", o.os_Open))
		o.ob.Set(runtime.String("TryOpen"), runtime.NewNativeFunc(o.ctx, "os.TryOpen", o.os_TryOpen))
	}
	return o.ob, nil
}

func (o *OsMod) SetCtx(ctx *runtime.Ctx) {
	o.ctx = ctx
}

func (o *OsMod) os_Exit(args ...runtime.Val) runtime.Val {
	if len(args) == 0 {
		os.Exit(0)
	}
	os.Exit(int(args[0].Int()))
	return runtime.Nil
}

func (o *OsMod) os_Getenv(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	return runtime.String(os.Getenv(args[0].String()))
}

func (o *OsMod) os_Getwd(args ...runtime.Val) runtime.Val {
	pwd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	return runtime.String(pwd)
}

func (o *OsMod) os_Exec(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	c := exec.Command(args[0].String(), toString(args[1:])...)
	b, e := c.CombinedOutput()
	if e != nil {
		panic(e)
	}
	return runtime.String(b)
}

func (o *OsMod) os_ReadFile(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	b, e := ioutil.ReadFile(args[0].String())
	if e != nil {
		panic(e)
	}
	return runtime.String(b)
}

func (o *OsMod) os_WriteFile(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	f, e := os.Create(args[0].String())
	if e != nil {
		panic(e)
	}
	defer f.Close()
	n := 0
	for _, v := range args[1:] {
		m, e := f.WriteString(v.String())
		if e != nil {
			panic(e)
		}
		n += m
	}
	return runtime.Number(n)
}

func (o *OsMod) os_TryOpen(args ...runtime.Val) (ret runtime.Val) {
	defer func() {
		if e := recover(); e != nil {
			ret = runtime.Nil
		}
	}()
	ret = o.os_Open(args...)
	return ret
}

func (o *OsMod) os_Open(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	nm := args[0].String()
	flg := "r" // defaults to read-only
	if len(args) > 1 {
		// Second arg is the flag (same as C's fopen)
		// r  - open for reading
		// w  - open for writing (file need not exist)
		// a  - open for appending (file need not exist)
		// r+ - open for reading and writing, start at beginning
		// w+ - open for reading and writing (overwrite file)
		// a+ - open for reading and writing (append if file exists)
		flg = args[1].String()
	}
	var flgi int
	switch flg {
	case "r":
		flgi = os.O_RDONLY
	case "w":
		flgi = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
	case "a":
		flgi = os.O_APPEND | os.O_CREATE
	case "r+":
		flgi = os.O_RDWR
	case "w+":
		flgi = os.O_RDWR | os.O_CREATE | os.O_TRUNC
	case "a+":
		flgi = os.O_RDWR | os.O_APPEND | os.O_CREATE
	default:
		panic("invalid file flag mode: " + flg)
	}
	f, e := os.OpenFile(nm, flgi, 0666)
	if e != nil {
		panic(e)
	}
	return o.newFile(f)
}

func toString(args []runtime.Val) []string {
	s := make([]string, len(args))
	for i, a := range args {
		s[i] = a.String()
	}
	return s
}
<commit_msg>add stdlib functions to os<commit_after>package stdlib

import (
	"bufio"
	"io/ioutil"
	"os"
	"os/exec"

	"github.com/PuerkitoBio/agora/runtime"
)

type OsMod struct {
	ctx *runtime.Ctx
	ob  runtime.Object
}

type file struct {
	runtime.Object
	f *os.File
	s *bufio.Scanner
}

func (o *OsMod) newFile(f *os.File) *file {
	ob := runtime.NewObject()
	of := &file{
		ob,
		f,
		nil,
	}
	ob.Set(runtime.String("Name"), runtime.String(f.Name()))
	ob.Set(runtime.String("Close"), runtime.NewNativeFunc(o.ctx, "os.File.Close", of.closeFile))
	ob.Set(runtime.String("ReadLine"), runtime.NewNativeFunc(o.ctx, "os.File.ReadLine", of.readLine))
	ob.Set(runtime.String("Seek"), runtime.NewNativeFunc(o.ctx, "os.File.Seek", of.seek))
	ob.Set(runtime.String("Write"), runtime.NewNativeFunc(o.ctx, "os.File.Write", of.write))
	ob.Set(runtime.String("WriteLine"), runtime.NewNativeFunc(o.ctx, "os.File.WriteLine", of.writeLine))
	return of
}

func (of *file) closeFile(args ...runtime.Val) runtime.Val {
	e := of.f.Close()
	if e != nil {
		panic(e)
	}
	return runtime.Nil
}

func (of *file) readLine(args ...runtime.Val) runtime.Val {
	if of.s == nil {
		of.s = bufio.NewScanner(of.f)
	}
	if of.s.Scan() {
		return runtime.String(of.s.Text())
	}
	if e := of.s.Err(); e != nil {
		panic(e)
	}
	return runtime.Nil
}

func (of *file) seek(args ...runtime.Val) runtime.Val {
	off := int64(0)
	if len(args) > 0 {
		off = args[0].Int()
	}
	rel := 0
	if len(args) > 1 {
		rel = int(args[1].Int())
	}
	n, e := of.f.Seek(off, rel)
	if e != nil {
		panic(e)
	}
	return runtime.Number(n)
}

func (of *file) write(args ...runtime.Val) runtime.Val {
	n := 0
	for _, v := range args {
		m, e := of.f.WriteString(v.String())
		if e != nil {
			panic(e)
		}
		n += m
	}
	return runtime.Number(n)
}

func (of *file) writeLine(args ...runtime.Val) runtime.Val {
	n := of.write(args...)
	m, e := of.f.WriteString("\n")
	if e != nil {
		panic(e)
	}
	return runtime.Number(int(n.Int()) + m)
}

func (o *OsMod) ID() string {
	return "os"
}

func (o *OsMod) Run(_ ...runtime.Val) (v runtime.Val, err error) {
	defer runtime.PanicToError(&err)
	if o.ob == nil {
		// Prepare the object
		o.ob = runtime.NewObject()
		o.ob.Set(runtime.String("TempDir"), runtime.String(os.TempDir()))
		o.ob.Set(runtime.String("PathSeparator"), runtime.String(os.PathSeparator))
		o.ob.Set(runtime.String("PathListSeparator"), runtime.String(os.PathListSeparator))
		o.ob.Set(runtime.String("DevNull"), runtime.String(os.DevNull))
		o.ob.Set(runtime.String("Exec"), runtime.NewNativeFunc(o.ctx, "os.Exec", o.os_Exec))
		o.ob.Set(runtime.String("Exit"), runtime.NewNativeFunc(o.ctx, "os.Exit", o.os_Exit))
		o.ob.Set(runtime.String("Getenv"), runtime.NewNativeFunc(o.ctx, "os.Getenv", o.os_Getenv))
		o.ob.Set(runtime.String("Getwd"), runtime.NewNativeFunc(o.ctx, "os.Getwd", o.os_Getwd))
		o.ob.Set(runtime.String("ReadFile"), runtime.NewNativeFunc(o.ctx, "os.ReadFile", o.os_ReadFile))
		o.ob.Set(runtime.String("WriteFile"), runtime.NewNativeFunc(o.ctx, "os.WriteFile", o.os_WriteFile))
		o.ob.Set(runtime.String("Open"), runtime.NewNativeFunc(o.ctx, "os.Open", o.os_Open))
		o.ob.Set(runtime.String("TryOpen"), runtime.NewNativeFunc(o.ctx, "os.TryOpen", o.os_TryOpen))
		o.ob.Set(runtime.String("Mkdir"), runtime.NewNativeFunc(o.ctx, "os.Mkdir", o.os_Mkdir))
		o.ob.Set(runtime.String("Remove"), runtime.NewNativeFunc(o.ctx, "os.Remove", o.os_Remove))
		o.ob.Set(runtime.String("RemoveAll"), runtime.NewNativeFunc(o.ctx, "os.RemoveAll", o.os_RemoveAll))
		o.ob.Set(runtime.String("Rename"), runtime.NewNativeFunc(o.ctx, "os.Rename", o.os_Rename))
		o.ob.Set(runtime.String("ReadDir"), runtime.NewNativeFunc(o.ctx, "os.ReadDir", o.os_ReadDir))
	}
	return o.ob, nil
}

func (o *OsMod) SetCtx(ctx *runtime.Ctx) {
	o.ctx = ctx
}

func (o *OsMod) os_Exit(args ...runtime.Val) runtime.Val {
	if len(args) == 0 {
		os.Exit(0)
	}
	os.Exit(int(args[0].Int()))
	return runtime.Nil
}

func (o *OsMod) os_Getenv(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	return runtime.String(os.Getenv(args[0].String()))
}

func (o *OsMod) os_Getwd(args ...runtime.Val) runtime.Val {
	pwd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	return runtime.String(pwd)
}

func (o *OsMod) os_Exec(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	c := exec.Command(args[0].String(), toString(args[1:])...)
	b, e := c.CombinedOutput()
	if e != nil {
		panic(e)
	}
	return runtime.String(b)
}

func (o *OsMod) os_Mkdir(args ...runtime.Val) runtime.Val {
	// No-op if no arg
	if len(args) == 0 {
		return runtime.Nil
	}
	perm := int64(0666)
	// Last args *may* be the permissions to use if it is a number
	if l, ok := args[len(args)-1].(runtime.Number); ok {
		perm = l.Int()
		args = args[:len(args)-1]
	}
	// Use the mkdir-all version, to create all missing dirs as required
	for _, v := range args {
		if e := os.MkdirAll(v.String(), os.FileMode(perm)); e != nil {
			panic(e)
		}
	}
	return runtime.Nil
}

func createFileInfo(fi os.FileInfo) runtime.Val {
	o := runtime.NewObject()
	o.Set(runtime.String("Name"), runtime.String(fi.Name()))
	o.Set(runtime.String("Size"), runtime.Number(fi.Size()))
	o.Set(runtime.String("IsDir"), runtime.Bool(fi.IsDir()))
	return o
}

func (o *OsMod) os_ReadDir(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	fis, e := ioutil.ReadDir(args[0].String())
	if e != nil {
		panic(e)
	}
	ob := runtime.NewObject()
	for i, fi := range fis {
		ob.Set(runtime.Number(i), createFileInfo(fi))
	}
	return ob
}

func (o *OsMod) os_Remove(args ...runtime.Val) runtime.Val {
	for _, v := range args {
		if e := os.Remove(v.String()); e != nil {
			panic(e)
		}
	}
	return runtime.Nil
}

func (o *OsMod) os_RemoveAll(args ...runtime.Val) runtime.Val {
	for _, v := range args {
		if e := os.RemoveAll(v.String()); e != nil {
			panic(e)
		}
	}
	return runtime.Nil
}

func (o *OsMod) os_Rename(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(2, args)
	if e := os.Rename(args[0].String(), args[1].String()); e != nil {
		panic(e)
	}
	return runtime.Nil
}

func (o *OsMod) os_ReadFile(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	b, e := ioutil.ReadFile(args[0].String())
	if e != nil {
		panic(e)
	}
	return runtime.String(b)
}

func (o *OsMod) os_WriteFile(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	f, e := os.Create(args[0].String())
	if e != nil {
		panic(e)
	}
	defer f.Close()
	n := 0
	for _, v := range args[1:] {
		m, e := f.WriteString(v.String())
		if e != nil {
			panic(e)
		}
		n += m
	}
	return runtime.Number(n)
}

func (o *OsMod) os_TryOpen(args ...runtime.Val) (ret runtime.Val) {
	defer func() {
		if e := recover(); e != nil {
			ret = runtime.Nil
		}
	}()
	ret = o.os_Open(args...)
	return ret
}

func (o *OsMod) os_Open(args ...runtime.Val) runtime.Val {
	runtime.ExpectAtLeastNArgs(1, args)
	nm := args[0].String()
	flg := "r" // defaults to read-only
	if len(args) > 1 {
		// Second arg is the flag (same as C's fopen)
		// r  - open for reading
		// w  - open for writing (file need not exist)
		// a  - open for appending (file need not exist)
		// r+ - open for reading and writing, start at beginning
		// w+ - open for reading and writing (overwrite file)
		// a+ - open for reading and writing (append if file exists)
		flg = args[1].String()
	}
	var flgi int
	switch flg {
	case "r":
		flgi = os.O_RDONLY
	case "w":
		flgi = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
	case "a":
		flgi = os.O_APPEND | os.O_CREATE
	case "r+":
		flgi = os.O_RDWR
	case "w+":
		flgi = os.O_RDWR | os.O_CREATE | os.O_TRUNC
	case "a+":
		flgi = os.O_RDWR | os.O_APPEND | os.O_CREATE
	default:
		panic("invalid file flag mode: " + flg)
	}
	f, e := os.OpenFile(nm, flgi, 0666)
	if e != nil {
		panic(e)
	}
	return o.newFile(f)
}

func toString(args []runtime.Val) []string {
	s := make([]string, len(args))
	for i, a := range args {
		s[i] = a.String()
	}
	return s
}
<|endoftext|>


<commit_before>package statistics

import "errors"

func Factorial(n int) (int, error) {
	if n < 0 {
		return 0, errors.New("Less than 0")
	}

	if n == 0 {
		return 1, nil
	}

	total := 1
	for i := 1; i <= n; i++ {
		total *= i
	}

	return total, nil
}<commit_msg>Documentation for factorial function<commit_after>package statistics

import "errors"

// calculate the factorial of a given number.
// if less than 0 then return an error and by default
// factorial of 0 is 1.
func Factorial(n int) (int, error) {
	if n < 0 {
		return 0, errors.New("Less than 0")
	}

	if n == 0 {
		return 1, nil
	}

	total := 1
	for i := 1; i <= n; i++ {
		total *= i
	}

	return total, nil
}<|endoftext|>


<commit_before>/*
The MIT License (MIT)

Copyright (c) 2016 Lau, Chok Sheak (for software "findfile")

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package main

import (
	"bytes"
	"io/ioutil"
	"os"
	"strings"
)

/**************************************************************************/

// Config files

func setConfigOptions(options string) {
	// If user's home directory does not exist, return.
	exists, _ := pathExists(userHomeDir)
	if !exists {
		putln("User's home directory %v does not exist. Cannot set config options.", userHomeDir)
		exit(1)
	}

	// Create findfile config dir.
	exists, _ = pathExists(configFileDir)
	if !exists {
		err := os.Mkdir(configFileDir, 0755)
		if err != nil {
			putln("Cannot create config dir \"%v\" to store config options. %v", configFileDir, err)
			exit(1)
		}
	}

	// Parse input string as options list.
	newOptionStrings := splitArgumentsString(options, "command line")

	if len(newOptionStrings) == 0 {
		putln("No options were specified to be set in the config file.")
		exit(1)
	}

	// Validate for disallowed options.
	checkForUnrecognizedOptions(newOptionStrings, "option value of "+optionSetConfig.getDefinition().flags)
	checkForDisallowedConfigOptions(newOptionStrings, "config file")

	// Read options from config file.
	mergedOptionsMap := readConfigFileOptionsMap()

	// Info.
	putBlankLine()
	putln("Setting config options:")
	putln("%v%v", printIndent, options)
	putBlankLine()

	// Merge old and new options.
	isChanged := false

	for _, newOptionString := range newOptionStrings {
		newOption := getOptionByFlag(newOptionString)
		newDef := newOption.getDefinition()
		oldOptionString := mergedOptionsMap[newDef.name]

		if oldOptionString == "" {
			putln("Adding new option: %v (%v)", newOptionString, newDef.flags)
			mergedOptionsMap[newDef.name] = newOptionString
			isChanged = true
		} else if oldOptionString == newOptionString {
			putln("Option already exists: %v (%v)",
				oldOptionString,
				newDef.flags)
		} else {
			putln("Replacing existing option %v with %v (%v)",
				oldOptionString,
				newOptionString,
				newDef.flags)
			mergedOptionsMap[newDef.name] = newOptionString
			isChanged = true
		}
	}

	// Write back list of options into config file.
	putBlankLine()
	writeOptionsToConfigFileIfChanged(mergedOptionsMap, configFilePath, isChanged)
}

func unsetConfigOptions(options string) {
	// If config file does not exist, return.
	exists, _ := pathExists(configFilePath)
	if !exists {
		putln("Config file \"%v\" does not exist. Nothing to unset.", configFilePath)
		exit(1)
	}

	// Read options from config file.
	mergedOptionsMap := readConfigFileOptionsMap()

	// Validate that config file args are present.
	if len(mergedOptionsMap) == 0 {
		putln("Config file \"%v\" is empty. Nothing to unset.", configFilePath)
		exit(1)
	}

	// Parse input string as options list.
	newOptionStrings := splitArgumentsString(options, "command line")
	checkForUnrecognizedOptions(newOptionStrings, "option value of "+optionUnsetConfig.getDefinition().flags)
	checkForDisallowedConfigOptions(newOptionStrings, "config file")

	// Info.
	putBlankLine()
	putln("Unsetting config options:")
	putln("%v%v", printIndent, options)
	putBlankLine()

	// Remove new options from old options.
	isChanged := false

	for _, newOptionString := range newOptionStrings {
		newOption := getOptionByFlag(newOptionString)
		newDef := newOption.getDefinition()
		oldOptionString := mergedOptionsMap[newDef.name]

		if oldOptionString == "" {
			putln("Ignoring option: %v (%v)", newOptionString, newDef.flags)
		} else {
			putln("Removing existing option: %v (%v)", oldOptionString, newDef.flags)
			delete(mergedOptionsMap, newDef.name)
			isChanged = true
		}
	}

	// Write back list of options into config file.
	putBlankLine()
	writeOptionsToConfigFileIfChanged(mergedOptionsMap, configFilePath, isChanged)
}

func readConfigFileOptionsMap() map[string]string {
	// Read options from config file.
	fileContents := readConfigFile()
	rawOldOptionStrings := splitArgumentsString(fileContents, "config file "+configFilePath)

	// Add good old options, thus removing bad old options.
	mergedOptionsMap := make(map[string]string)

	for _, oldOptionString := range rawOldOptionStrings {
		oldOption := getOptionByFlag(oldOptionString)
		if oldOption == nil {
			putln("Removing unknown option %v in the config file %v.", oldOptionString, configFilePath)
			continue
		}
		mergedOptionsMap[oldOption.getDefinition().name] = oldOptionString
	}

	return mergedOptionsMap
}

func writeOptionsToConfigFileIfChanged(optionsToWrite map[string]string, configFilePath string, isChanged bool) {
	if isChanged {
		writeOptionsToConfigFile(optionsToWrite, configFilePath)
	} else {
		putln("No changes were made to the config file.")
	}

}

func writeOptionsToConfigFile(optionsToWrite map[string]string, configFilePath string) {
	var optionsBuffer bytes.Buffer

	for _, optionString := range optionsToWrite {
		if optionsBuffer.Len() > 0 {
			optionsBuffer.WriteRune(' ')
		}

		hasSpace := strings.ContainsAny(optionString, " \a\b\f\n\r\t\v")
		if hasSpace {
			optionsBuffer.WriteRune('"')
			optionsBuffer.WriteString(escapeString(optionString))
			optionsBuffer.WriteRune('"')
		} else {
			optionsBuffer.WriteString(optionString)
		}
	}

	// Write back list of options into config file.
	putln("Writing options to config file \"%v\".", configFilePath)
	putln("    %v", optionsBuffer.String())
	err := ioutil.WriteFile(configFilePath, optionsBuffer.Bytes(), 0755)
	if err != nil {
		putln("Failed to write options to file: %v", err)
		exit(1)
	}
	putBlankLine()
}

/**************************************************************************/
<commit_msg>Improve config set/unset options output<commit_after>/*
The MIT License (MIT)

Copyright (c) 2016 Lau, Chok Sheak (for software "findfile")

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package main

import (
	"bytes"
	"io/ioutil"
	"os"
	"strings"
)

/**************************************************************************/

// Config files

func setConfigOptions(options string) {
	// If user's home directory does not exist, return.
	exists, _ := pathExists(userHomeDir)
	if !exists {
		putln("User's home directory %v does not exist. Cannot set config options.", userHomeDir)
		exit(1)
	}

	// Create findfile config dir.
	exists, _ = pathExists(configFileDir)
	if !exists {
		err := os.Mkdir(configFileDir, 0755)
		if err != nil {
			putln("Cannot create config dir \"%v\" to store config options. %v", configFileDir, err)
			exit(1)
		}
	}

	// Parse input string as options list.
	newOptionStrings := splitArgumentsString(options, "command line")

	if len(newOptionStrings) == 0 {
		putln("No options were specified to be set in the config file.")
		exit(1)
	}

	// Validate for disallowed options.
	checkForUnrecognizedOptions(newOptionStrings, "option value of "+optionSetConfig.getDefinition().flags)
	checkForDisallowedConfigOptions(newOptionStrings, "config file")

	// Read options from config file.
	mergedOptionsMap := readConfigFileOptionsMap()

	// Info.
	putBlankLine()
	putln("Setting config options:")
	printArgumentsAsList(newOptionStrings, false)
	putBlankLine()

	// Merge old and new options.
	isChanged := false

	for _, newOptionString := range newOptionStrings {
		newOption := getOptionByFlag(newOptionString)
		newDef := newOption.getDefinition()
		oldOptionString := mergedOptionsMap[newDef.name]

		if oldOptionString == "" {
			putln("Adding new option: %v (%v)", newOptionString, newDef.flags)
			mergedOptionsMap[newDef.name] = newOptionString
			isChanged = true
		} else if oldOptionString == newOptionString {
			putln("Option already exists: %v (%v)",
				oldOptionString,
				newDef.flags)
		} else {
			putln("Replacing existing option %v with %v (%v)",
				oldOptionString,
				newOptionString,
				newDef.flags)
			mergedOptionsMap[newDef.name] = newOptionString
			isChanged = true
		}
	}

	// Write back list of options into config file.
	putBlankLine()
	writeOptionsToConfigFileIfChanged(mergedOptionsMap, configFilePath, isChanged)
}

func unsetConfigOptions(options string) {
	// If config file does not exist, return.
	exists, _ := pathExists(configFilePath)
	if !exists {
		putln("Config file \"%v\" does not exist. Nothing to unset.", configFilePath)
		exit(1)
	}

	// Read options from config file.
	mergedOptionsMap := readConfigFileOptionsMap()

	// Validate that config file args are present.
	if len(mergedOptionsMap) == 0 {
		putln("Config file \"%v\" is empty. Nothing to unset.", configFilePath)
		exit(1)
	}

	// Parse input string as options list.
	newOptionStrings := splitArgumentsString(options, "command line")
	checkForUnrecognizedOptions(newOptionStrings, "option value of "+optionUnsetConfig.getDefinition().flags)
	checkForDisallowedConfigOptions(newOptionStrings, "config file")

	// Info.
	putBlankLine()
	putln("Unsetting config options:")
	printArgumentsAsList(newOptionStrings, false)
	putBlankLine()

	// Remove new options from old options.
	isChanged := false

	for _, newOptionString := range newOptionStrings {
		newOption := getOptionByFlag(newOptionString)
		newDef := newOption.getDefinition()
		oldOptionString := mergedOptionsMap[newDef.name]

		if oldOptionString == "" {
			putln("Ignoring option: %v (%v)", newOptionString, newDef.flags)
		} else {
			putln("Removing existing option: %v (%v)", oldOptionString, newDef.flags)
			delete(mergedOptionsMap, newDef.name)
			isChanged = true
		}
	}

	// Write back list of options into config file.
	putBlankLine()
	writeOptionsToConfigFileIfChanged(mergedOptionsMap, configFilePath, isChanged)
}

func readConfigFileOptionsMap() map[string]string {
	// Read options from config file.
	fileContents := readConfigFile()
	rawOldOptionStrings := splitArgumentsString(fileContents, "config file "+configFilePath)

	// Add good old options, thus removing bad old options.
	mergedOptionsMap := make(map[string]string)

	for _, oldOptionString := range rawOldOptionStrings {
		oldOption := getOptionByFlag(oldOptionString)
		if oldOption == nil {
			putln("Removing unknown option %v in the config file %v.", oldOptionString, configFilePath)
			continue
		}
		mergedOptionsMap[oldOption.getDefinition().name] = oldOptionString
	}

	return mergedOptionsMap
}

func writeOptionsToConfigFileIfChanged(optionsToWrite map[string]string, configFilePath string, isChanged bool) {
	if isChanged {
		writeOptionsToConfigFile(optionsToWrite, configFilePath)
	} else {
		putln("No changes were made to the config file.")
	}

}

func writeOptionsToConfigFile(optionsToWrite map[string]string, configFilePath string) {
	var optionsBuffer bytes.Buffer

	for _, optionString := range optionsToWrite {
		if optionsBuffer.Len() > 0 {
			optionsBuffer.WriteRune(' ')
		}

		hasSpace := strings.ContainsAny(optionString, " \a\b\f\n\r\t\v")
		if hasSpace {
			optionsBuffer.WriteRune('"')
			optionsBuffer.WriteString(escapeString(optionString))
			optionsBuffer.WriteRune('"')
		} else {
			optionsBuffer.WriteString(optionString)
		}
	}

	// Write back list of options into config file.
	putln("Writing options to config file \"%v\".", configFilePath)
	putln("    %v", optionsBuffer.String())
	err := ioutil.WriteFile(configFilePath, optionsBuffer.Bytes(), 0755)
	if err != nil {
		putln("Failed to write options to file: %v", err)
		exit(1)
	}
	putBlankLine()
}

/**************************************************************************/
<|endoftext|>