Home | History | Annotate | Download | only in driver
      1 // Copyright 2014 Google Inc. All Rights Reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package driver
     16 
     17 import (
     18 	"bytes"
     19 	"fmt"
     20 	"io"
     21 	"os"
     22 	"os/exec"
     23 	"runtime"
     24 	"sort"
     25 	"strconv"
     26 	"strings"
     27 	"time"
     28 
     29 	"github.com/google/pprof/internal/plugin"
     30 	"github.com/google/pprof/internal/report"
     31 	"github.com/google/pprof/third_party/svg"
     32 )
     33 
     34 // commands describes the commands accepted by pprof.
     35 type commands map[string]*command
     36 
     37 // command describes the actions for a pprof command. Includes a
     38 // function for command-line completion, the report format to use
     39 // during report generation, any postprocessing functions, and whether
     40 // the command expects a regexp parameter (typically a function name).
     41 type command struct {
     42 	format      int           // report format to generate
     43 	postProcess PostProcessor // postprocessing to run on report
     44 	visualizer  PostProcessor // display output using some callback
     45 	hasParam    bool          // collect a parameter from the CLI
     46 	description string        // single-line description text saying what the command does
     47 	usage       string        // multi-line help text saying how the command is used
     48 }
     49 
     50 // help returns a help string for a command.
     51 func (c *command) help(name string) string {
     52 	message := c.description + "\n"
     53 	if c.usage != "" {
     54 		message += "  Usage:\n"
     55 		lines := strings.Split(c.usage, "\n")
     56 		for _, line := range lines {
     57 			message += fmt.Sprintf("    %s\n", line)
     58 		}
     59 	}
     60 	return message + "\n"
     61 }
     62 
     63 // AddCommand adds an additional command to the set of commands
     64 // accepted by pprof. This enables extensions to add new commands for
     65 // specialized visualization formats. If the command specified already
     66 // exists, it is overwritten.
     67 func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) {
     68 	pprofCommands[cmd] = &command{format, post, nil, false, desc, usage}
     69 }
     70 
     71 // SetVariableDefault sets the default value for a pprof
     72 // variable. This enables extensions to set their own defaults.
     73 func SetVariableDefault(variable, value string) {
     74 	if v := pprofVariables[variable]; v != nil {
     75 		v.value = value
     76 	}
     77 }
     78 
     79 // PostProcessor is a function that applies post-processing to the report output
     80 type PostProcessor func(input io.Reader, output io.Writer, ui plugin.UI) error
     81 
     82 // interactiveMode is true if pprof is running on interactive mode, reading
     83 // commands from its shell.
     84 var interactiveMode = false
     85 
     86 // pprofCommands are the report generation commands recognized by pprof.
     87 var pprofCommands = commands{
     88 	// Commands that require no post-processing.
     89 	"comments": {report.Comments, nil, nil, false, "Output all profile comments", ""},
     90 	"disasm":   {report.Dis, nil, nil, true, "Output assembly listings annotated with samples", listHelp("disasm", true)},
     91 	"dot":      {report.Dot, nil, nil, false, "Outputs a graph in DOT format", reportHelp("dot", false, true)},
     92 	"list":     {report.List, nil, nil, true, "Output annotated source for functions matching regexp", listHelp("list", false)},
     93 	"peek":     {report.Tree, nil, nil, true, "Output callers/callees of functions matching regexp", "peek func_regex\nDisplay callers and callees of functions matching func_regex."},
     94 	"raw":      {report.Raw, nil, nil, false, "Outputs a text representation of the raw profile", ""},
     95 	"tags":     {report.Tags, nil, nil, false, "Outputs all tags in the profile", "tags [tag_regex]* [-ignore_regex]* [>file]\nList tags with key:value matching tag_regex and exclude ignore_regex."},
     96 	"text":     {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("text", true, true)},
     97 	"top":      {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)},
     98 	"traces":   {report.Traces, nil, nil, false, "Outputs all profile samples in text form", ""},
     99 	"tree":     {report.Tree, nil, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)},
    100 
    101 	// Save binary formats to a file
    102 	"callgrind": {report.Callgrind, nil, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)},
    103 	"proto":     {report.Proto, nil, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format", ""},
    104 	"topproto":  {report.TopProto, nil, awayFromTTY("pb.gz"), false, "Outputs top entries in compressed protobuf format", ""},
    105 
    106 	// Generate report in DOT format and postprocess with dot
    107 	"gif": {report.Dot, invokeDot("gif"), awayFromTTY("gif"), false, "Outputs a graph image in GIF format", reportHelp("gif", false, true)},
    108 	"pdf": {report.Dot, invokeDot("pdf"), awayFromTTY("pdf"), false, "Outputs a graph in PDF format", reportHelp("pdf", false, true)},
    109 	"png": {report.Dot, invokeDot("png"), awayFromTTY("png"), false, "Outputs a graph image in PNG format", reportHelp("png", false, true)},
    110 	"ps":  {report.Dot, invokeDot("ps"), awayFromTTY("ps"), false, "Outputs a graph in PS format", reportHelp("ps", false, true)},
    111 
    112 	// Save SVG output into a file
    113 	"svg": {report.Dot, massageDotSVG(), awayFromTTY("svg"), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)},
    114 
    115 	// Visualize postprocessed dot output
    116 	"eog":    {report.Dot, invokeDot("svg"), invokeVisualizer("svg", []string{"eog"}), false, "Visualize graph through eog", reportHelp("eog", false, false)},
    117 	"evince": {report.Dot, invokeDot("pdf"), invokeVisualizer("pdf", []string{"evince"}), false, "Visualize graph through evince", reportHelp("evince", false, false)},
    118 	"gv":     {report.Dot, invokeDot("ps"), invokeVisualizer("ps", []string{"gv --noantialias"}), false, "Visualize graph through gv", reportHelp("gv", false, false)},
    119 	"web":    {report.Dot, massageDotSVG(), invokeVisualizer("svg", browsers()), false, "Visualize graph through web browser", reportHelp("web", false, false)},
    120 
    121 	// Visualize callgrind output
    122 	"kcachegrind": {report.Callgrind, nil, invokeVisualizer("grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)},
    123 
    124 	// Visualize HTML directly generated by report.
    125 	"weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
    126 }
    127 
    128 // pprofVariables are the configuration parameters that affect the
    129 // reported generated by pprof.
    130 var pprofVariables = variables{
    131 	// Filename for file-based output formats, stdout by default.
    132 	"output": &variable{stringKind, "", "", helpText("Output filename for file-based outputs")},
    133 
    134 	// Comparisons.
    135 	"drop_negative": &variable{boolKind, "f", "", helpText(
    136 		"Ignore negative differences",
    137 		"Do not show any locations with values <0.")},
    138 
    139 	// Comparisons.
    140 	"positive_percentages": &variable{boolKind, "f", "", helpText(
    141 		"Ignore negative samples when computing percentages",
    142 		"Do not count negative samples when computing the total value",
    143 		"of the profile, used to compute percentages. If set, and the -base",
    144 		"option is used, percentages reported will be computed against the",
    145 		"main profile, ignoring the base profile.")},
    146 
    147 	// Graph handling options.
    148 	"call_tree": &variable{boolKind, "f", "", helpText(
    149 		"Create a context-sensitive call tree",
    150 		"Treat locations reached through different paths as separate.")},
    151 
    152 	// Display options.
    153 	"relative_percentages": &variable{boolKind, "f", "", helpText(
    154 		"Show percentages relative to focused subgraph",
    155 		"If unset, percentages are relative to full graph before focusing",
    156 		"to facilitate comparison with original graph.")},
    157 	"unit": &variable{stringKind, "minimum", "", helpText(
    158 		"Measurement units to display",
    159 		"Scale the sample values to this unit.",
    160 		"For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
    161 		"For memory profiles, use megabytes, kilobytes, bytes, etc.",
    162 		"Using auto will scale each value independently to the most natural unit.")},
    163 	"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
    164 	"source_path":    &variable{stringKind, "", "", "Search path for source files"},
    165 
    166 	// Filtering options
    167 	"nodecount": &variable{intKind, "-1", "", helpText(
    168 		"Max number of nodes to show",
    169 		"Uses heuristics to limit the number of locations to be displayed.",
    170 		"On graphs, dotted edges represent paths through nodes that have been removed.")},
    171 	"nodefraction": &variable{floatKind, "0.005", "", "Hide nodes below <f>*total"},
    172 	"edgefraction": &variable{floatKind, "0.001", "", "Hide edges below <f>*total"},
    173 	"trim": &variable{boolKind, "t", "", helpText(
    174 		"Honor nodefraction/edgefraction/nodecount defaults",
    175 		"Set to false to get the full profile, without any trimming.")},
    176 	"focus": &variable{stringKind, "", "", helpText(
    177 		"Restricts to samples going through a node matching regexp",
    178 		"Discard samples that do not include a node matching this regexp.",
    179 		"Matching includes the function name, filename or object name.")},
    180 	"ignore": &variable{stringKind, "", "", helpText(
    181 		"Skips paths going through any nodes matching regexp",
    182 		"If set, discard samples that include a node matching this regexp.",
    183 		"Matching includes the function name, filename or object name.")},
    184 	"prune_from": &variable{stringKind, "", "", helpText(
    185 		"Drops any functions below the matched frame.",
    186 		"If set, any frames matching the specified regexp and any frames",
    187 		"below it will be dropped from each sample.")},
    188 	"hide": &variable{stringKind, "", "", helpText(
    189 		"Skips nodes matching regexp",
    190 		"Discard nodes that match this location.",
    191 		"Other nodes from samples that include this location will be shown.",
    192 		"Matching includes the function name, filename or object name.")},
    193 	"show": &variable{stringKind, "", "", helpText(
    194 		"Only show nodes matching regexp",
    195 		"If set, only show nodes that match this location.",
    196 		"Matching includes the function name, filename or object name.")},
    197 	"tagfocus": &variable{stringKind, "", "", helpText(
    198 		"Restricts to samples with tags in range or matched by regexp",
    199 		"Use name=value syntax to limit the matching to a specific tag.",
    200 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
    201 		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
    202 	"tagignore": &variable{stringKind, "", "", helpText(
    203 		"Discard samples with tags in range or matched by regexp",
    204 		"Use name=value syntax to limit the matching to a specific tag.",
    205 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
    206 		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
    207 	"tagshow": &variable{stringKind, "", "", helpText(
    208 		"Only consider tags matching this regexp",
    209 		"Discard tags that do not match this regexp")},
    210 	"taghide": &variable{stringKind, "", "", helpText(
    211 		"Skip tags matching this regexp",
    212 		"Discard tags that match this regexp")},
    213 	// Heap profile options
    214 	"divide_by": &variable{floatKind, "1", "", helpText(
    215 		"Ratio to divide all samples before visualization",
    216 		"Divide all samples values by a constant, eg the number of processors or jobs.")},
    217 	"mean": &variable{boolKind, "f", "", helpText(
    218 		"Average sample value over first value (count)",
    219 		"For memory profiles, report average memory per allocation.",
    220 		"For time-based profiles, report average time per event.")},
    221 	"sample_index": &variable{stringKind, "", "", helpText(
    222 		"Sample value to report (0-based index or name)",
    223 		"Profiles contain multiple values per sample.",
    224 		"Use sample_index=i to select the ith value (starting at 0).")},
    225 	"normalize": &variable{boolKind, "f", "", helpText(
    226 		"Scales profile based on the base profile.")},
    227 
    228 	// Data sorting criteria
    229 	"flat": &variable{boolKind, "t", "cumulative", helpText("Sort entries based on own weight")},
    230 	"cum":  &variable{boolKind, "f", "cumulative", helpText("Sort entries based on cumulative weight")},
    231 
    232 	// Output granularity
    233 	"functions": &variable{boolKind, "t", "granularity", helpText(
    234 		"Aggregate at the function level.",
    235 		"Takes into account the filename/lineno where the function was defined.")},
    236 	"files": &variable{boolKind, "f", "granularity", "Aggregate at the file level."},
    237 	"lines": &variable{boolKind, "f", "granularity", "Aggregate at the source code line level."},
    238 	"addresses": &variable{boolKind, "f", "granularity", helpText(
    239 		"Aggregate at the function level.",
    240 		"Includes functions' addresses in the output.")},
    241 	"noinlines": &variable{boolKind, "f", "granularity", helpText(
    242 		"Aggregate at the function level.",
    243 		"Attributes inlined functions to their first out-of-line caller.")},
    244 	"addressnoinlines": &variable{boolKind, "f", "granularity", helpText(
    245 		"Aggregate at the function level, including functions' addresses in the output.",
    246 		"Attributes inlined functions to their first out-of-line caller.")},
    247 }
    248 
    249 func helpText(s ...string) string {
    250 	return strings.Join(s, "\n") + "\n"
    251 }
    252 
    253 // usage returns a string describing the pprof commands and variables.
    254 // if commandLine is set, the output reflect cli usage.
    255 func usage(commandLine bool) string {
    256 	var prefix string
    257 	if commandLine {
    258 		prefix = "-"
    259 	}
    260 	fmtHelp := func(c, d string) string {
    261 		return fmt.Sprintf("    %-16s %s", c, strings.SplitN(d, "\n", 2)[0])
    262 	}
    263 
    264 	var commands []string
    265 	for name, cmd := range pprofCommands {
    266 		commands = append(commands, fmtHelp(prefix+name, cmd.description))
    267 	}
    268 	sort.Strings(commands)
    269 
    270 	var help string
    271 	if commandLine {
    272 		help = "  Output formats (select at most one):\n"
    273 	} else {
    274 		help = "  Commands:\n"
    275 		commands = append(commands, fmtHelp("o/options", "List options and their current values"))
    276 		commands = append(commands, fmtHelp("quit/exit/^D", "Exit pprof"))
    277 	}
    278 
    279 	help = help + strings.Join(commands, "\n") + "\n\n" +
    280 		"  Options:\n"
    281 
    282 	// Print help for variables after sorting them.
    283 	// Collect radio variables by their group name to print them together.
    284 	radioOptions := make(map[string][]string)
    285 	var variables []string
    286 	for name, vr := range pprofVariables {
    287 		if vr.group != "" {
    288 			radioOptions[vr.group] = append(radioOptions[vr.group], name)
    289 			continue
    290 		}
    291 		variables = append(variables, fmtHelp(prefix+name, vr.help))
    292 	}
    293 	sort.Strings(variables)
    294 
    295 	help = help + strings.Join(variables, "\n") + "\n\n" +
    296 		"  Option groups (only set one per group):\n"
    297 
    298 	var radioStrings []string
    299 	for radio, ops := range radioOptions {
    300 		sort.Strings(ops)
    301 		s := []string{fmtHelp(radio, "")}
    302 		for _, op := range ops {
    303 			s = append(s, "  "+fmtHelp(prefix+op, pprofVariables[op].help))
    304 		}
    305 
    306 		radioStrings = append(radioStrings, strings.Join(s, "\n"))
    307 	}
    308 	sort.Strings(radioStrings)
    309 	return help + strings.Join(radioStrings, "\n")
    310 }
    311 
    312 func reportHelp(c string, cum, redirect bool) string {
    313 	h := []string{
    314 		c + " [n] [focus_regex]* [-ignore_regex]*",
    315 		"Include up to n samples",
    316 		"Include samples matching focus_regex, and exclude ignore_regex.",
    317 	}
    318 	if cum {
    319 		h[0] += " [-cum]"
    320 		h = append(h, "-cum sorts the output by cumulative weight")
    321 	}
    322 	if redirect {
    323 		h[0] += " >f"
    324 		h = append(h, "Optionally save the report on the file f")
    325 	}
    326 	return strings.Join(h, "\n")
    327 }
    328 
    329 func listHelp(c string, redirect bool) string {
    330 	h := []string{
    331 		c + "<func_regex|address> [-focus_regex]* [-ignore_regex]*",
    332 		"Include functions matching func_regex, or including the address specified.",
    333 		"Include samples matching focus_regex, and exclude ignore_regex.",
    334 	}
    335 	if redirect {
    336 		h[0] += " >f"
    337 		h = append(h, "Optionally save the report on the file f")
    338 	}
    339 	return strings.Join(h, "\n")
    340 }
    341 
    342 // browsers returns a list of commands to attempt for web visualization.
    343 func browsers() []string {
    344 	cmds := []string{"chrome", "google-chrome", "firefox"}
    345 	switch runtime.GOOS {
    346 	case "darwin":
    347 		return append(cmds, "/usr/bin/open")
    348 	case "windows":
    349 		return append(cmds, "cmd /c start")
    350 	default:
    351 		userBrowser := os.Getenv("BROWSER")
    352 		if userBrowser != "" {
    353 			cmds = append([]string{userBrowser, "sensible-browser"}, cmds...)
    354 		} else {
    355 			cmds = append([]string{"sensible-browser"}, cmds...)
    356 		}
    357 		return append(cmds, "xdg-open")
    358 	}
    359 }
    360 
    361 var kcachegrind = []string{"kcachegrind"}
    362 
    363 // awayFromTTY saves the output in a file if it would otherwise go to
    364 // the terminal screen. This is used to avoid dumping binary data on
    365 // the screen.
    366 func awayFromTTY(format string) PostProcessor {
    367 	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
    368 		if output == os.Stdout && (ui.IsTerminal() || interactiveMode) {
    369 			tempFile, err := newTempFile("", "profile", "."+format)
    370 			if err != nil {
    371 				return err
    372 			}
    373 			ui.PrintErr("Generating report in ", tempFile.Name())
    374 			output = tempFile
    375 		}
    376 		_, err := io.Copy(output, input)
    377 		return err
    378 	}
    379 }
    380 
    381 func invokeDot(format string) PostProcessor {
    382 	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
    383 		cmd := exec.Command("dot", "-T"+format)
    384 		cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr
    385 		if err := cmd.Run(); err != nil {
    386 			return fmt.Errorf("Failed to execute dot. Is Graphviz installed? Error: %v", err)
    387 		}
    388 		return nil
    389 	}
    390 }
    391 
    392 // massageDotSVG invokes the dot tool to generate an SVG image and alters
    393 // the image to have panning capabilities when viewed in a browser.
    394 func massageDotSVG() PostProcessor {
    395 	generateSVG := invokeDot("svg")
    396 	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
    397 		baseSVG := new(bytes.Buffer)
    398 		if err := generateSVG(input, baseSVG, ui); err != nil {
    399 			return err
    400 		}
    401 		_, err := output.Write([]byte(svg.Massage(baseSVG.String())))
    402 		return err
    403 	}
    404 }
    405 
    406 func invokeVisualizer(suffix string, visualizers []string) PostProcessor {
    407 	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
    408 		tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix)
    409 		if err != nil {
    410 			return err
    411 		}
    412 		deferDeleteTempFile(tempFile.Name())
    413 		if _, err := io.Copy(tempFile, input); err != nil {
    414 			return err
    415 		}
    416 		tempFile.Close()
    417 		// Try visualizers until one is successful
    418 		for _, v := range visualizers {
    419 			// Separate command and arguments for exec.Command.
    420 			args := strings.Split(v, " ")
    421 			if len(args) == 0 {
    422 				continue
    423 			}
    424 			viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...)
    425 			viewer.Stderr = os.Stderr
    426 			if err = viewer.Start(); err == nil {
    427 				// Wait for a second so that the visualizer has a chance to
    428 				// open the input file. This needs to be done even if we're
    429 				// waiting for the visualizer as it can be just a wrapper that
    430 				// spawns a browser tab and returns right away.
    431 				defer func(t <-chan time.Time) {
    432 					<-t
    433 				}(time.After(time.Second))
    434 				// On interactive mode, let the visualizer run in the background
    435 				// so other commands can be issued.
    436 				if !interactiveMode {
    437 					return viewer.Wait()
    438 				}
    439 				return nil
    440 			}
    441 		}
    442 		return err
    443 	}
    444 }
    445 
    446 // variables describe the configuration parameters recognized by pprof.
    447 type variables map[string]*variable
    448 
    449 // variable is a single configuration parameter.
    450 type variable struct {
    451 	kind  int    // How to interpret the value, must be one of the enums below.
    452 	value string // Effective value. Only values appropriate for the Kind should be set.
    453 	group string // boolKind variables with the same Group != "" cannot be set simultaneously.
    454 	help  string // Text describing the variable, in multiple lines separated by newline.
    455 }
    456 
    457 const (
    458 	// variable.kind must be one of these variables.
    459 	boolKind = iota
    460 	intKind
    461 	floatKind
    462 	stringKind
    463 )
    464 
    465 // set updates the value of a variable, checking that the value is
    466 // suitable for the variable Kind.
    467 func (vars variables) set(name, value string) error {
    468 	v := vars[name]
    469 	if v == nil {
    470 		return fmt.Errorf("no variable %s", name)
    471 	}
    472 	var err error
    473 	switch v.kind {
    474 	case boolKind:
    475 		var b bool
    476 		if b, err = stringToBool(value); err == nil {
    477 			if v.group != "" && !b {
    478 				err = fmt.Errorf("%q can only be set to true", name)
    479 			}
    480 		}
    481 	case intKind:
    482 		_, err = strconv.Atoi(value)
    483 	case floatKind:
    484 		_, err = strconv.ParseFloat(value, 64)
    485 	case stringKind:
    486 		// Remove quotes, particularly useful for empty values.
    487 		if len(value) > 1 && strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
    488 			value = value[1 : len(value)-1]
    489 		}
    490 	}
    491 	if err != nil {
    492 		return err
    493 	}
    494 	vars[name].value = value
    495 	if group := vars[name].group; group != "" {
    496 		for vname, vvar := range vars {
    497 			if vvar.group == group && vname != name {
    498 				vvar.value = "f"
    499 			}
    500 		}
    501 	}
    502 	return err
    503 }
    504 
    505 // boolValue returns the value of a boolean variable.
    506 func (v *variable) boolValue() bool {
    507 	b, err := stringToBool(v.value)
    508 	if err != nil {
    509 		panic("unexpected value " + v.value + " for bool ")
    510 	}
    511 	return b
    512 }
    513 
    514 // intValue returns the value of an intKind variable.
    515 func (v *variable) intValue() int {
    516 	i, err := strconv.Atoi(v.value)
    517 	if err != nil {
    518 		panic("unexpected value " + v.value + " for int ")
    519 	}
    520 	return i
    521 }
    522 
    523 // floatValue returns the value of a Float variable.
    524 func (v *variable) floatValue() float64 {
    525 	f, err := strconv.ParseFloat(v.value, 64)
    526 	if err != nil {
    527 		panic("unexpected value " + v.value + " for float ")
    528 	}
    529 	return f
    530 }
    531 
    532 // stringValue returns a canonical representation for a variable.
    533 func (v *variable) stringValue() string {
    534 	switch v.kind {
    535 	case boolKind:
    536 		return fmt.Sprint(v.boolValue())
    537 	case intKind:
    538 		return fmt.Sprint(v.intValue())
    539 	case floatKind:
    540 		return fmt.Sprint(v.floatValue())
    541 	}
    542 	return v.value
    543 }
    544 
    545 func stringToBool(s string) (bool, error) {
    546 	switch strings.ToLower(s) {
    547 	case "true", "t", "yes", "y", "1", "":
    548 		return true, nil
    549 	case "false", "f", "no", "n", "0":
    550 		return false, nil
    551 	default:
    552 		return false, fmt.Errorf(`illegal value "%s" for bool variable`, s)
    553 	}
    554 }
    555 
    556 // makeCopy returns a duplicate of a set of shell variables.
    557 func (vars variables) makeCopy() variables {
    558 	varscopy := make(variables, len(vars))
    559 	for n, v := range vars {
    560 		vcopy := *v
    561 		varscopy[n] = &vcopy
    562 	}
    563 	return varscopy
    564 }
    565