github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/command/utils.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package command
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  
    15  	"github.com/docker/cli/cli/streams"
    16  	"github.com/docker/docker/api/types/filters"
    17  	mounttypes "github.com/docker/docker/api/types/mount"
    18  	"github.com/docker/docker/api/types/versions"
    19  	"github.com/moby/sys/sequential"
    20  	"github.com/pkg/errors"
    21  	"github.com/spf13/pflag"
    22  )
    23  
    24  // CopyToFile writes the content of the reader to the specified file
    25  func CopyToFile(outfile string, r io.Reader) error {
    26  	// We use sequential file access here to avoid depleting the standby list
    27  	// on Windows. On Linux, this is a call directly to os.CreateTemp
    28  	tmpFile, err := sequential.CreateTemp(filepath.Dir(outfile), ".docker_temp_")
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	tmpPath := tmpFile.Name()
    34  
    35  	_, err = io.Copy(tmpFile, r)
    36  	tmpFile.Close()
    37  
    38  	if err != nil {
    39  		os.Remove(tmpPath)
    40  		return err
    41  	}
    42  
    43  	if err = os.Rename(tmpPath, outfile); err != nil {
    44  		os.Remove(tmpPath)
    45  		return err
    46  	}
    47  
    48  	return nil
    49  }
    50  
    51  // capitalizeFirst capitalizes the first character of string
    52  func capitalizeFirst(s string) string {
    53  	switch l := len(s); l {
    54  	case 0:
    55  		return s
    56  	case 1:
    57  		return strings.ToLower(s)
    58  	default:
    59  		return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
    60  	}
    61  }
    62  
    63  // PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
    64  func PrettyPrint(i any) string {
    65  	switch t := i.(type) {
    66  	case nil:
    67  		return "None"
    68  	case string:
    69  		return capitalizeFirst(t)
    70  	default:
    71  		return capitalizeFirst(fmt.Sprintf("%s", t))
    72  	}
    73  }
    74  
    75  // PromptForConfirmation requests and checks confirmation from user.
    76  // This will display the provided message followed by ' [y/N] '. If
    77  // the user input 'y' or 'Y' it returns true other false.  If no
    78  // message is provided "Are you sure you want to proceed? [y/N] "
    79  // will be used instead.
    80  func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool {
    81  	if message == "" {
    82  		message = "Are you sure you want to proceed?"
    83  	}
    84  	message += " [y/N] "
    85  
    86  	_, _ = fmt.Fprint(outs, message)
    87  
    88  	// On Windows, force the use of the regular OS stdin stream.
    89  	if runtime.GOOS == "windows" {
    90  		ins = streams.NewIn(os.Stdin)
    91  	}
    92  
    93  	reader := bufio.NewReader(ins)
    94  	answer, _, _ := reader.ReadLine()
    95  	return strings.ToLower(string(answer)) == "y"
    96  }
    97  
    98  // PruneFilters returns consolidated prune filters obtained from config.json and cli
    99  func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
   100  	if dockerCli.ConfigFile() == nil {
   101  		return pruneFilters
   102  	}
   103  	for _, f := range dockerCli.ConfigFile().PruneFilters {
   104  		k, v, ok := strings.Cut(f, "=")
   105  		if !ok {
   106  			continue
   107  		}
   108  		if k == "label" {
   109  			// CLI label filter supersede config.json.
   110  			// If CLI label filter conflict with config.json,
   111  			// skip adding label! filter in config.json.
   112  			if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", v) {
   113  				continue
   114  			}
   115  		} else if k == "label!" {
   116  			// CLI label! filter supersede config.json.
   117  			// If CLI label! filter conflict with config.json,
   118  			// skip adding label filter in config.json.
   119  			if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", v) {
   120  				continue
   121  			}
   122  		}
   123  		pruneFilters.Add(k, v)
   124  	}
   125  
   126  	return pruneFilters
   127  }
   128  
   129  // AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
   130  func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
   131  	flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
   132  	flags.SetAnnotation("platform", "version", []string{"1.32"})
   133  }
   134  
   135  // ValidateOutputPath validates the output paths of the `export` and `save` commands.
   136  func ValidateOutputPath(path string) error {
   137  	dir := filepath.Dir(filepath.Clean(path))
   138  	if dir != "" && dir != "." {
   139  		if _, err := os.Stat(dir); os.IsNotExist(err) {
   140  			return errors.Errorf("invalid output path: directory %q does not exist", dir)
   141  		}
   142  	}
   143  	// check whether `path` points to a regular file
   144  	// (if the path exists and doesn't point to a directory)
   145  	if fileInfo, err := os.Stat(path); !os.IsNotExist(err) {
   146  		if err != nil {
   147  			return err
   148  		}
   149  
   150  		if fileInfo.Mode().IsDir() || fileInfo.Mode().IsRegular() {
   151  			return nil
   152  		}
   153  
   154  		if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil {
   155  			return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
   156  		}
   157  	}
   158  	return nil
   159  }
   160  
   161  // ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
   162  // helper to `ValidateOutputPath`
   163  func ValidateOutputPathFileMode(fileMode os.FileMode) error {
   164  	switch {
   165  	case fileMode&os.ModeDevice != 0:
   166  		return errors.New("got a device")
   167  	case fileMode&os.ModeIrregular != 0:
   168  		return errors.New("got an irregular file")
   169  	}
   170  	return nil
   171  }
   172  
   173  func stringSliceIndex(s, subs []string) int {
   174  	j := 0
   175  	if len(subs) > 0 {
   176  		for i, x := range s {
   177  			if j < len(subs) && subs[j] == x {
   178  				j++
   179  			} else {
   180  				j = 0
   181  			}
   182  			if len(subs) == j {
   183  				return i + 1 - j
   184  			}
   185  		}
   186  	}
   187  	return -1
   188  }
   189  
   190  // StringSliceReplaceAt replaces the sub-slice find, with the sub-slice replace, in the string
   191  // slice s, returning a new slice and a boolean indicating if the replacement happened.
   192  // requireIdx is the index at which old needs to be found at (or -1 to disregard that).
   193  func StringSliceReplaceAt(s, find, replace []string, requireIndex int) ([]string, bool) {
   194  	idx := stringSliceIndex(s, find)
   195  	if (requireIndex != -1 && requireIndex != idx) || idx == -1 {
   196  		return s, false
   197  	}
   198  	out := append([]string{}, s[:idx]...)
   199  	out = append(out, replace...)
   200  	out = append(out, s[idx+len(find):]...)
   201  	return out, true
   202  }
   203  
   204  // ValidateMountWithAPIVersion validates a mount with the server API version.
   205  func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
   206  	if m.BindOptions != nil {
   207  		if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
   208  			return errors.Errorf("bind-recursive=disabled requires API v1.40 or later")
   209  		}
   210  		// ReadOnlyNonRecursive can be safely ignored when API < 1.44
   211  		if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") {
   212  			return errors.Errorf("bind-recursive=readonly requires API v1.44 or later")
   213  		}
   214  	}
   215  	return nil
   216  }