Now that the valid values for settings are defined, the root command of the DSC Resource needs to handle when users specify those values.
Define the flag variables #
Open the cmd/root.go
file in the editor.
Add variables for each of the fields of the Settings
type so users can specify those values at
the command line.
var targetScope config.Scope
var targetEnsure config.Ensure
var updateAutomatically bool
var updateFrequency config.Frequency
Handle arguments as input #
The first input method the resource should support is command line arguments. This makes using the resource outside of DSC easier, like when iteratively developing the command. It’s also the most common way users expect to call a command directly.
Define the persistent flags #
Next, find the init
function at the bottom of the file. Inside it, define persistent flags so
users can pass the values to both get
and set
as arguments.
func init() {
rootCmd.PersistentFlags().Var(
enumflag.New(&targetScope, "scope", config.ScopeMap, enumflag.EnumCaseInsensitive),
"scope",
"The target scope for the configuration.",
)
rootCmd.RegisterFlagCompletionFunc("scope", config.ScopeFlagCompletion)
rootCmd.PersistentFlags().Var(
enumflag.New(&targetEnsure, "ensure", config.EnsureMap, enumflag.EnumCaseInsensitive),
"ensure",
"Whether the configuration file should exist.",
)
rootCmd.RegisterFlagCompletionFunc("ensure", config.EnsureFlagCompletion)
rootCmd.PersistentFlags().BoolVar(
&updateAutomatically,
"updateAutomatically",
false,
"Whether the configuration should set the app to automatically update.",
)
rootCmd.PersistentFlags().Var(
&updateFrequency,
"updateFrequency",
"How frequently the configuration should update, between 1 and 90 days inclusive.",
)
}
Use the enumflag
module to for the ensure
and scope
flags. It handles parsing the user inputs
and converting them to the enumeration values. Use the flag completion functions you defined
earlier to ensure that users can opt into shell completions for those flags.
Validate input flags #
With the Settings
defined and command line flags added to the root command, you can begin
validating that the settings flags work as expected.
Open the cmd/get.go
file in the editor.
At the bottom of the file, create a new getState
function that takes two parameters, a pointer to
cobra.Command
and a slice of strings, and returns an error.
func getState(cmd *cobra.Command, args []string) error {
return nil
}
Replace the Run
entry in the getCmd
variable’s definition with the RunE
field set to the
getState
function. Update the documentation for the command to be more specific to the DSC
Resource.
var getCmd = &cobra.Command{
Use: "get",
Short: "Gets the current state of a tstoy configuration file.",
Long: `The get command returns the current state of a tstoy configuration
file as a JSON blob to stdout.`,
RunE: getState,
}
Next, update the getState
function to report the value of any specified flags. You’ll replace
this implementation later, but it’s useful for validating that the flags work as expected.
func getState(cmd *cobra.Command, args []string) error {
if targetScope != config.ScopeUndefined {
fmt.Println("Specified --scope as", targetScope)
}
if targetEnsure != config.EnsureUndefined {
fmt.Println("Specified --ensure as", targetEnsure)
}
if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed {
fmt.Println("Specified --updateAutomatically as", updateAutomatically)
}
if updateFrequency != 0 {
fmt.Println("Specified --updateFrequency as", updateFrequency)
}
return nil
}
Run the DSC Resource with different flags to verify the output.
go run ./main.go get --scope machine --ensure absent
go run ./main.go get --updateAutomatically --updateFrequency 45
go run ./main.go get --updateAutomatically=false --ensure Absent
Specified --ensure as absent
Specified --scope as machine
Specified --updateAutomatically as true
Specified --updateFrequency as 45
Specified --ensure as absent
Specified --updateAutomatically as false
Next, you can test that the arguments are validating correctly:
go run ./main.go get --scope 1
go run ./main.go get --scope incorrect
go run ./main.go get --updateFrequency 100
Error: invalid argument "1" for "--scope" flag: must be 'machine', 'user'
Error: invalid argument "incorrect" for "--scope" flag: must be 'machine', 'user'
Error: invalid argument "100" for "--updateFrequency" flag: invalid value 100;
must be an integer between 1 and 90, inclusive
With validation confirmed, the command can accept command-line arguments.
Handle JSON input over stdin #
When command-based DSC Resources are called by dsc
itself, they may get their input as a JSON
blob over stdin
. While specifying flags at the command line is useful for testing, it’s more
robust for the DSC Resource to support sending input over stdin
. This also makes it easier for
other integrating tools to interact with the DSC Resource.
Add handlers for JSON input #
Create the input
folder in the project root. Inside it, create the input.go
file. This file
defines how you handle input from stdin
.
mkdir ./input
touch ./input/input.go
Open input/input.go
and set the package name to input
.
package input
Now you need to ensure that the DSC Resource can handle a JSON blob as input along with the other flags. Implement a new type that satisfies the pflag.Value interface.
Define a type called JSONFlag
as a struct with the Target
field as the any
type.
type JSONFlag struct {
Target any
}
Implement the String
, Set
, and Type
methods for JSONFlag
.
func (f *JSONFlag) String() string {
b, err := json.Marshal(f.Target)
if err != nil {
return "failed to marshal object"
}
return string(b)
}
func (f *JSONFlag) Set(v string) error {
return json.Unmarshal([]byte(v), f.Target)
}
func (f *JSONFlag) Type() string {
return "json"
}
Add handler for stdin #
Next, the DSC Resource needs a function that can handle reading from stdin
. The function must
operate on the list of arguments for the DSC Resource. If there’s input on stdin
, it’s added to
the list of arguments with the --inputJSON
flag.
func HandleStdIn(args []string) []string {
info, _ := os.Stdin.Stat()
if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
// do nothing
} else {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
// remove surrounding whitespace
jsonBlob := strings.Trim(string(stdin), "\n")
jsonBlob = strings.Trim(jsonBlob, "\r")
jsonBlob = strings.TrimSpace(jsonBlob)
// only add to arguments if the string is non-empty.
if jsonBlob != "" {
args = append(args, "--inputJSON", jsonBlob)
}
}
return args
}
The function doesn’t need to validate that the input is valid JSON. Instead, the JSONFlag
and the
command handle the validation. Implementing the function to append the JSON as an argument also
gives the user the choice to pass a JSON blob as a normal argument.
Add inputJSON to the root command #
Before you can pass a JSON blob to the commands, you must update the root command to accept the
--inputJson
flag.
Open cmd/root.go
and add the inputJSON
variable with its type as a pointer to
config.Settings
.
var inputJSON *config.Settings
In the init
function, add a new persistent flag for --inputJSON
.
rootCmd.PersistentFlags().Var(
&input.JSONFlag{Target: &inputJSON},
"inputJSON",
"Specify options as a JSON blob instead of using the scope, ensure, and update* flags.",
)
The new flag uses the JSONFlag
type defined in the input
package and sets the Target
field to
the inputJSON
variable. Because that variable has the Settings
type, when the flag
automatically unmarshals the input, it deserializes the blob to Settings
for the DSC Resource.
Update root command to handle stdin { toc_text “Update root command for stdin” } #
Finally, you must update the Execute
function to take a list of arguments, because argument
passing must be explicitly handled to support stdin
.
// Unlike normal cobra apps, this one sets the args explicitly from main to
// account for JSON blobs sent from stdin.
func Execute(args []string) {
rootCmd.SetArgs(args)
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
Update main to handle stdin
#
Now that the HandleStdIn
function is defined and the root command is updated, main
needs to be
updated to pass the arguments to the Execute
function and handle JSON over stdin
.
Open main.go
and replace the main
function.
func main() {
args := []string{}
for index, arg := range os.Args {
// skip the first index, because it's the application name
if index > 0 {
args = append(args, arg)
}
}
// Check stdin and add any found JSON blob after an --inputJSON flag.
args = input.HandleStdIn(args)
// execute with the combined arguments
cmd.Execute(args)
}
Verify using JSON as input #
Now that the DSC Resource can accept JSON as input over stdin
or as an argument, the get
command needs to handle that input.
Open cmd/get.go
and ensure that the getState
function prints the value for the inputJSON
variable if it’s not null.
func getState(cmd *cobra.Command, args []string) error {
if inputJSON != nil {
fmt.Println("Specified inputJSON as:")
(*inputJSON).Print()
}
if targetScope != config.ScopeUndefined {
fmt.Println("Specified --scope as", targetScope)
}
if targetEnsure != config.EnsureUndefined {
fmt.Println("Specified --ensure as", targetEnsure)
}
if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed {
fmt.Println("Specified --updateAutomatically as", updateAutomatically)
}
if updateFrequency != 0 {
fmt.Println("Specified --updateFrequency as", updateFrequency)
}
return nil
}
You can verify the behavior with a few commands:
go run ./main.go get --inputJSON '{ "scope": "machine" }'
go run ./main.go get --inputJSON '{ "scope": "machine" }' --scope user
'{ "scope": "machine" }' | go run ./main.go get
'{ "scope": "machine" }' | go run ./main.go get --scope user
'{ "ensure": "present" }' | go run ./main.go get
Specified inputJSON as:
{"scope":"machine"}
Specified inputJSON as:
{"scope":"machine"}
Specified --scope as user
Specified inputJSON as:
{"scope":"machine"}
Specified inputJSON as:
{"scope":"machine"}
Specified --scope as user
Specified inputJSON as:
{"ensure":"present"}
The DSC Resource is now fully implemented to handle input as arguments and as a JSON blob over stdin.