Introduction

In this post, we will see how to use files as a flag in the Golang Command Line Interface (CLI). This is useful when you want to pass a file (not the file name, but an object of file type) as a flag to your CLI application.

Use the file name as a flag is straightforward, you can use the flag.String or flag.StringVar function to get the file name and then do the necessary checks to validate if this exist, create it, clean it, etc. But, if you want to pass the file as a flag, you need to create a custom flag type that implements the Value interface of the flag package for the struct type that represents the file flag.

For impatient πŸ˜”, you can check the πŸ‘‰ GitHub repository -> github.com/slashdevops/go-files-as-a-flag or in the πŸ‘‰ Implementation section.

Custom Flag Type

To implement a custom flag type for a file flag, you need to create a struct type that represents the file flag you want to create. This struct type must implement the Value interface of the flag package.

This interface has the following methods:

  • πŸ‘‰ String() string: presents the current value as a string.
  • πŸ‘‰ Set(string) error: is called once, in command line order, for each flag present.
  • πŸ‘‰ Get() interface{}: returns the contents of the Value.
  • πŸ‘‰ IsBoolFlag() bool: returns true if the flag is a boolean flag.

So, I created a struct type called FileVar that has a *os.File field to store the file and implemented the Value interface methods.

I didn’t need to use more fields because the *os.File field is enough for my use case. But, you can add more fields if you need to store more information about the file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type FileVar struct {
  *os.File
}

func (f *FileVar) String() string {
  ... (implementation is below) πŸ‘‡
}

func (f *FileVar) Set(value string) error {
  ... (implementation is below) πŸ‘‡
}

func (f *FileVar) Get() interface{} {
  ... (implementation is below) πŸ‘‡
}

func (f *FileVar) IsBoolFlag() bool {
  ... (implementation is below) πŸ‘‡
}

NOTE: check the lines πŸ‘‰ 12, πŸ‘‰ 17, πŸ‘‰ 26, πŸ‘‰ 37, πŸ‘‰ 42 of the Implementation to see how I implemented these methods.

Usage

To use the file flag in your CLI application, you need to create a new flag set using the flag.NewFlagSet function. Then, you can add the file flag using the FlagSet.Var function as is implemented in the Implementation line πŸ‘‰ 48 and πŸ‘‰ 57.

Example of usage with --help flag:

1
2
3
4
5
6
go run main.go --help
Usage of File as Flag CLI:
  -file.content string
        content to write to the file
  -output.file value
        output file (default /dev/stdout)

NOTE: as you can see, the output.file flag has the default value /dev/stdout. So, if you don’t pass this flag, the output will be written to the stdout, let’s see this in the next example.

Example of usage with --file.content flag:

1
2
3
4
go run main.go --file.content 'Hello, World'

# and the output will be:
Hello, World!

Now, let’s see how to use the output.file flag to write the output to a file.

Example of usage with --file.content and --output.file flags:

1
2
3
4
5
6
7
8
9
go run main.go \
  -file.content 'Hello, World!' \
  -output.file /tmp/my-output-file.txt

# let's check the content of the file
cat /tmp/my-output-file.txt

# and the output will be:
Hello, World!

NOTE: as you can see, the output was written to the /tmp/my-output-file.txt file.

Critical Points

For me ☝️ the magic πŸͺ„ of this implementation is in the Set method of the FileVar struct. This method is called once, in command line order, for each flag present. So, in this method, I open the file with the os.OpenFile function and set the *os.File field of the FileVar struct with the file descriptor.

The options os.O_APPEND|os.O_CREATE|os.O_WRONLY are used to open the file in write-only mode, create it if it doesn’t exist, and append the content if the file already exists.

NOTE:, maybe you want to overwrite the file content instead of appending it, you can use the os.O_TRUNC option instead of the os.O_APPEND option.

1
2
3
4
5
6
7
8
9
func (f *FileVar) Set(value string) error {
  file, err := os.OpenFile(value, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
  if err != nil {
    return err
  }

  f.File = file
  return nil
}

Implementation

This code is available in the GitHub repository -> github.com/slashdevops/go-files-as-a-flag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package main

import (
	"flag"
	"fmt"
	"os"
)

// FileVar is a custom flag type for files
// This should implement the Value interface of the flag package
// Reference: https://pkg.go.dev/gg-scm.io/tool/internal/flag#FlagSet.Var
type FileVar struct {
	*os.File
}

// String presents the current value as a string.
func (f *FileVar) String() string {
	if f.File == nil {
		return ""
	}

	return f.Name()
}

// Set is called once, in command line order, for each flag present.
func (f *FileVar) Set(value string) error {
	file, err := os.OpenFile(value, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}

	f.File = file
	return nil
}

// Get returns the contents of the Value.
func (f *FileVar) Get() interface{} {
	return f.File
}

// IsBoolFlag returns true if the flag is a boolean flag
func (f *FileVar) IsBoolFlag() bool {
	return false
}

func main() {
	// Create a new flag set
	fs := flag.NewFlagSet("File as Flag CLI", flag.ExitOnError)

	// Add a flag to get some content
	var content string
	fs.StringVar(&content, "file.content", "", "content to write to the file")

	// Add a custom file flag
	file := &FileVar{os.Stdout}
	defer file.Close()
	fs.Var(file, "output.file", "output file")

	// Parse the command line arguments
	fs.Parse(os.Args[1:])

	// Check if the content is empty (required)
	if content == "" {
		fs.PrintDefaults()
		fmt.Println("error: '-file.content' is required")
		os.Exit(1)
	}

	// Write the content to the file
	file.Write([]byte(content))
}

Conclusion

The flag package of the Golang standard library is powerful and flexible as you saw in this post. You can create custom flag types to extend the functionality of the package and adapt it to your needs. For this you should implement the Value interface of the flag package for the struct type that represents your flag type.

I hope this post helps you to use files as a flag in your Golang CLI applications. If you have any questions or suggestions, please let me know in the comments below. πŸ™