func (s *replaceVarsStateVarColon) nextRune(r rune) (replaceVarsState, error) {
	if s.currDefaultCmd == 0 {
		if !(r == '-' || r == '+') {
			return nil, fmt.Errorf("invalid default command after ':': %s", string(r))
		}
		s.currDefaultCmd = r
		return s, nil
	} else if s.escaped {
		if r != '}' {
			s.currDefaultVal += "\\"
		}
		s.escaped = false
	} else if r == '\\' {
		s.escaped = true
		return s, nil
	} else if r == '}' {
		val, _, err := s.resolveCurrVar()
		if err != nil {
			return nil, err
		}
		s.result += val
		s.reset()
		return &replaceVarsStateNone{s.replaceVarsBase}, nil
	}
	s.currDefaultVal += string(r)
	return s, nil
}
The first time nextRune is called after a reset, it attempts to set the currDefaultCmd.
After that, it appends characters to currDefaultVal until a close bracket is encountered.
package dockerfile
import (
	"errors"
	"fmt"
)
func replaceVariables(input string, vars map[string]string) (string, error) {
	var err error
	var state replaceVarsState = &replaceVarsStateNone{
		&replaceVarsBase{vars: vars},
	}
	for i := 0; i < len(input); i++ {
		state, err = state.nextRune(rune(input[i]))
		if err != nil {
			return "", err
		}
	}
	return state.endOfInput()
}
type replaceVarsState interface {
	nextRune(r rune) (replaceVarsState, error)
	endOfInput() (string, error)
}
type replaceVarsBase struct {
	result         string
	vars           map[string]string
	currVar        string
	varsInProgress []string
	currDefaultCmd rune
	currDefaultVal string
	escaped        bool
}
func (b *replaceVarsBase) reset() {
	b.currVar = ""
	b.currDefaultCmd = 0
	b.currDefaultVal = ""
}
func (b *replaceVarsBase) resolveCurrVar() (string, bool, error) {
	val, ok := b.vars[b.currVar]
	if b.currDefaultCmd == '-' {
		if b.currDefaultVal == "" {
			return "", false, errors.New("missing currDefault variable value")
		}
		if !ok {
			val = b.currDefaultVal
		}
	} else if b.currDefaultCmd == '+' {
		if b.currDefaultVal == "" {
			return "", false, errors.New("missing currDefault variable value")
		}
		if ok {
			val = b.currDefaultVal
		} else {
			val = ""
		}
	} else if b.currDefaultCmd != 0 {
		return "", false, fmt.Errorf("invalid default command: %s", string(b.currDefaultCmd))
	} else if !ok {
		return "", false, nil
	}
	return val, true, nil
}
type replaceVarsStateNone struct{ *replaceVarsBase }
func (s *replaceVarsStateNone) nextRune(r rune) (replaceVarsState, error) {
	if s.escaped {
		if r != '$' {
			s.result += "\\"
		}
		s.escaped = false
	} else if r == '\\' {
		s.escaped = true
		return s, nil
	} else if r == '$' {
		return &replaceVarsStateDollar{s.replaceVarsBase}, nil
	}
	s.result += string(r)
	return s, nil
}
func (s *replaceVarsStateNone) endOfInput() (string, error) {
	return s.result, nil
}
type replaceVarsStateDollar struct{ *replaceVarsBase }
func (s *replaceVarsStateDollar) nextRune(r rune) (replaceVarsState, error) {
	if r == '{' {
		return &replaceVarsStateVarBracket{s.replaceVarsBase}, nil
	}
	s.currVar += string(r)
	return &replaceVarsStateVar{s.replaceVarsBase}, nil
}
func (s *replaceVarsStateDollar) endOfInput() (string, error) {
	return s.result + "$", nil
}
type replaceVarsStateVar struct{ *replaceVarsBase }
func (s *replaceVarsStateVar) resolveCurrVar(recursing bool) (string, error) {
	val, ok, err := s.replaceVarsBase.resolveCurrVar()
	if err != nil {
		return "", err
	}
	if !ok {
		if recursing {
			return "${" + s.currVar + "}", nil
		}
		return "$" + s.currVar, nil
	}
	return val, nil
}
func (s *replaceVarsStateVar) nextRune(r rune) (replaceVarsState, error) {
	if err := validKeyRune(r); err != nil {
		val, err := s.resolveCurrVar(false)
		if err != nil {
			return nil, err
		}
		if len(s.varsInProgress) == 0 {
			s.result += val
			if r == '\\' {
				s.escaped = true
			} else if r == '$' {
				s.reset()
				return &replaceVarsStateDollar{s.replaceVarsBase}, nil
			} else {
				s.result += string(r)
			}
			s.reset()
			return &replaceVarsStateNone{s.replaceVarsBase}, nil
		} else if r == '}' {
			s.currVar = s.varsInProgress[len(s.varsInProgress)-1] + val
			s.varsInProgress = s.varsInProgress[0 : len(s.varsInProgress)-1]
			if len(s.varsInProgress) > 0 {
				return &replaceVarsStateVarBracket{s.replaceVarsBase}, nil
			}
			val, err := s.resolveCurrVar(true)
			if err != nil {
				return nil, err
			}
			s.result += val
			s.reset()
			return &replaceVarsStateNone{s.replaceVarsBase}, nil
		} else if r == ':' {
			if len(s.varsInProgress) != 1 {
				return nil, errors.New("TODO")
			}
			val, err := s.resolveCurrVar(false)
			if err != nil {
				return nil, err
			}
			s.currVar = s.varsInProgress[len(s.varsInProgress)-1] + val
			s.varsInProgress = s.varsInProgress[0 : len(s.varsInProgress)-1]
			return &replaceVarsStateVarColon{s.replaceVarsBase}, nil
		}
		s.currVar = s.varsInProgress[len(s.varsInProgress)-1] + val
		s.varsInProgress = s.varsInProgress[0 : len(s.varsInProgress)-1]
		return &replaceVarsStateVarBracket{s.replaceVarsBase}, nil
	}
	s.currVar += string(r)
	return s, nil
}
func (s *replaceVarsStateVar) endOfInput() (string, error) {
	if len(s.varsInProgress) != 0 {
		return "", errors.New("unexpected end of input: in recursive variable resolution")
	}
	val, err := s.resolveCurrVar(false)
	if err != nil {
		return "", err
	}
	return s.result + val, nil
}
type replaceVarsStateVarBracket struct{ *replaceVarsBase }
func (s *replaceVarsStateVarBracket) resolveCurrVar() (string, error) {
	val, ok, err := s.replaceVarsBase.resolveCurrVar()
	if err != nil {
		return "", err
	}
	if !ok {
		return "${" + s.currVar + "}", nil
	}
	return val, nil
}
func (s *replaceVarsStateVarBracket) nextRune(r rune) (replaceVarsState, error) {
	if r == '$' {
		s.varsInProgress = append(s.varsInProgress, s.currVar)
		s.currVar = ""
		return &replaceVarsStateDollar{s.replaceVarsBase}, nil
	} else if r == ':' {
		if s.currVar == "" {
			return nil, errors.New("missing variable value")
		}
		return &replaceVarsStateVarColon{s.replaceVarsBase}, nil
	} else if r == '}' {
		if len(s.varsInProgress) == 0 {
			val, err := s.resolveCurrVar()
			if err != nil {
				return nil, err
			}
			s.result += val
			s.reset()
			return &replaceVarsStateNone{s.replaceVarsBase}, nil
		}
		val, err := s.resolveCurrVar()
		if err != nil {
			return nil, err
		}
		s.currVar = s.varsInProgress[len(s.varsInProgress)-1] + val
		s.varsInProgress = s.varsInProgress[0 : len(s.varsInProgress)-1]
		return &replaceVarsStateVarBracket{s.replaceVarsBase}, nil
	}
	s.currVar += string(r)
	return s, nil
}
func (s *replaceVarsStateVarBracket) endOfInput() (string, error) {
	if s.currVar == "" {
		return "", errors.New("unexpected termination: missing variable value")
	}
	return "", errors.New("missing close bracket after variable")
}
type replaceVarsStateVarColon struct{ *replaceVarsBase }