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