compose.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "os"
  19. "os/signal"
  20. "path/filepath"
  21. "strconv"
  22. "strings"
  23. "syscall"
  24. "github.com/compose-spec/compose-go/v2/cli"
  25. "github.com/compose-spec/compose-go/v2/loader"
  26. "github.com/compose-spec/compose-go/v2/types"
  27. "github.com/docker/buildx/util/logutil"
  28. dockercli "github.com/docker/cli/cli"
  29. "github.com/docker/cli/cli-plugins/manager"
  30. "github.com/docker/cli/cli/command"
  31. "github.com/docker/compose/v2/cmd/formatter"
  32. "github.com/docker/compose/v2/internal/desktop"
  33. "github.com/docker/compose/v2/internal/experimental"
  34. "github.com/docker/compose/v2/internal/tracing"
  35. "github.com/docker/compose/v2/pkg/api"
  36. "github.com/docker/compose/v2/pkg/compose"
  37. ui "github.com/docker/compose/v2/pkg/progress"
  38. "github.com/docker/compose/v2/pkg/remote"
  39. "github.com/docker/compose/v2/pkg/utils"
  40. buildkit "github.com/moby/buildkit/util/progress/progressui"
  41. "github.com/morikuni/aec"
  42. "github.com/sirupsen/logrus"
  43. "github.com/spf13/cobra"
  44. "github.com/spf13/pflag"
  45. )
  46. const (
  47. // ComposeParallelLimit set the limit running concurrent operation on docker engine
  48. ComposeParallelLimit = "COMPOSE_PARALLEL_LIMIT"
  49. // ComposeProjectName define the project name to be used, instead of guessing from parent directory
  50. ComposeProjectName = "COMPOSE_PROJECT_NAME"
  51. // ComposeCompatibility try to mimic compose v1 as much as possible
  52. ComposeCompatibility = "COMPOSE_COMPATIBILITY"
  53. // ComposeRemoveOrphans remove “orphaned" containers, i.e. containers tagged for current project but not declared as service
  54. ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
  55. // ComposeIgnoreOrphans ignore "orphaned" containers
  56. ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
  57. // ComposeEnvFiles defines the env files to use if --env-file isn't used
  58. ComposeEnvFiles = "COMPOSE_ENV_FILES"
  59. // ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
  60. ComposeMenu = "COMPOSE_MENU"
  61. )
  62. type Backend interface {
  63. api.Service
  64. SetDesktopClient(cli *desktop.Client)
  65. SetExperiments(experiments *experimental.State)
  66. }
  67. // Command defines a compose CLI command as a func with args
  68. type Command func(context.Context, []string) error
  69. // CobraCommand defines a cobra command function
  70. type CobraCommand func(context.Context, *cobra.Command, []string) error
  71. // AdaptCmd adapt a CobraCommand func to cobra library
  72. func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
  73. return func(cmd *cobra.Command, args []string) error {
  74. ctx, cancel := context.WithCancel(cmd.Context())
  75. s := make(chan os.Signal, 1)
  76. signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
  77. go func() {
  78. <-s
  79. cancel()
  80. signal.Stop(s)
  81. close(s)
  82. }()
  83. err := fn(ctx, cmd, args)
  84. var composeErr compose.Error
  85. if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
  86. err = dockercli.StatusError{
  87. StatusCode: 130,
  88. Status: compose.CanceledStatus,
  89. }
  90. }
  91. if errors.As(err, &composeErr) {
  92. err = dockercli.StatusError{
  93. StatusCode: composeErr.GetMetricsFailureCategory().ExitCode,
  94. Status: err.Error(),
  95. }
  96. }
  97. return err
  98. }
  99. }
  100. // Adapt a Command func to cobra library
  101. func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
  102. return AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
  103. return fn(ctx, args)
  104. })
  105. }
  106. type ProjectOptions struct {
  107. ProjectName string
  108. Profiles []string
  109. ConfigPaths []string
  110. WorkDir string
  111. ProjectDir string
  112. EnvFiles []string
  113. Compatibility bool
  114. Progress string
  115. Offline bool
  116. All bool
  117. }
  118. // ProjectFunc does stuff within a types.Project
  119. type ProjectFunc func(ctx context.Context, project *types.Project) error
  120. // ProjectServicesFunc does stuff within a types.Project and a selection of services
  121. type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
  122. // WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
  123. func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func(cmd *cobra.Command, args []string) error {
  124. return o.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
  125. return fn(ctx, project)
  126. })
  127. }
  128. // WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
  129. func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
  130. return Adapt(func(ctx context.Context, args []string) error {
  131. options := []cli.ProjectOptionsFn{
  132. cli.WithResolvedPaths(true),
  133. cli.WithDiscardEnvFile,
  134. }
  135. project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
  136. if err != nil {
  137. return err
  138. }
  139. ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
  140. return fn(ctx, project, args)
  141. })
  142. }
  143. func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
  144. f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
  145. f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
  146. f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
  147. f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file")
  148. f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
  149. f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
  150. f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
  151. f.StringVar(&o.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
  152. f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
  153. _ = f.MarkHidden("workdir")
  154. }
  155. // get default value for a command line flag that is set by a coma-separated value in environment variable
  156. func defaultStringArrayVar(env string) []string {
  157. return strings.FieldsFunc(os.Getenv(env), func(c rune) bool {
  158. return c == ','
  159. })
  160. }
  161. func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
  162. name := o.ProjectName
  163. var project *types.Project
  164. if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
  165. p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
  166. if err != nil {
  167. envProjectName := os.Getenv(ComposeProjectName)
  168. if envProjectName != "" {
  169. return nil, envProjectName, nil
  170. }
  171. return nil, "", err
  172. }
  173. project = p
  174. name = p.Name
  175. }
  176. return project, name, nil
  177. }
  178. func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
  179. if o.ProjectName != "" {
  180. return o.ProjectName, nil
  181. }
  182. envProjectName := os.Getenv(ComposeProjectName)
  183. if envProjectName != "" {
  184. return envProjectName, nil
  185. }
  186. project, _, err := o.ToProject(ctx, dockerCli, nil)
  187. if err != nil {
  188. return "", err
  189. }
  190. return project.Name, nil
  191. }
  192. func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
  193. remotes := o.remoteLoaders(dockerCli)
  194. for _, r := range remotes {
  195. po = append(po, cli.WithResourceLoader(r))
  196. }
  197. options, err := o.toProjectOptions(po...)
  198. if err != nil {
  199. return nil, err
  200. }
  201. if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
  202. api.Separator = "_"
  203. }
  204. return options.LoadModel(ctx)
  205. }
  206. func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
  207. var metrics tracing.Metrics
  208. remotes := o.remoteLoaders(dockerCli)
  209. for _, r := range remotes {
  210. po = append(po, cli.WithResourceLoader(r))
  211. }
  212. options, err := o.toProjectOptions(po...)
  213. if err != nil {
  214. return nil, metrics, compose.WrapComposeError(err)
  215. }
  216. options.WithListeners(func(event string, metadata map[string]any) {
  217. switch event {
  218. case "extends":
  219. metrics.CountExtends++
  220. case "include":
  221. paths := metadata["path"].(types.StringList)
  222. for _, path := range paths {
  223. var isRemote bool
  224. for _, r := range remotes {
  225. if r.Accept(path) {
  226. isRemote = true
  227. break
  228. }
  229. }
  230. if isRemote {
  231. metrics.CountIncludesRemote++
  232. } else {
  233. metrics.CountIncludesLocal++
  234. }
  235. }
  236. }
  237. })
  238. if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
  239. api.Separator = "_"
  240. }
  241. project, err := options.LoadProject(ctx)
  242. if err != nil {
  243. return nil, metrics, compose.WrapComposeError(err)
  244. }
  245. if project.Name == "" {
  246. return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
  247. }
  248. project, err = project.WithServicesEnabled(services...)
  249. if err != nil {
  250. return nil, metrics, err
  251. }
  252. for name, s := range project.Services {
  253. s.CustomLabels = map[string]string{
  254. api.ProjectLabel: project.Name,
  255. api.ServiceLabel: name,
  256. api.VersionLabel: api.ComposeVersion,
  257. api.WorkingDirLabel: project.WorkingDir,
  258. api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
  259. api.OneoffLabel: "False", // default, will be overridden by `run` command
  260. }
  261. if len(o.EnvFiles) != 0 {
  262. s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
  263. }
  264. project.Services[name] = s
  265. }
  266. if !o.All {
  267. project = project.WithoutUnnecessaryResources()
  268. }
  269. project, err = project.WithSelectedServices(services)
  270. return project, metrics, err
  271. }
  272. func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
  273. if o.Offline {
  274. return nil
  275. }
  276. git := remote.NewGitRemoteLoader(o.Offline)
  277. oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
  278. return []loader.ResourceLoader{git, oci}
  279. }
  280. func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
  281. return cli.NewProjectOptions(o.ConfigPaths,
  282. append(po,
  283. cli.WithWorkingDirectory(o.ProjectDir),
  284. // First apply os.Environment, always win
  285. cli.WithOsEnv,
  286. // Load PWD/.env if present and no explicit --env-file has been set
  287. cli.WithEnvFiles(o.EnvFiles...),
  288. // read dot env file to populate project environment
  289. cli.WithDotEnv,
  290. // get compose file path set by COMPOSE_FILE
  291. cli.WithConfigFileEnv,
  292. // if none was selected, get default compose.yaml file from current dir or parent folder
  293. cli.WithDefaultConfigPath,
  294. // .. and then, a project directory != PWD maybe has been set so let's load .env file
  295. cli.WithEnvFiles(o.EnvFiles...),
  296. cli.WithDotEnv,
  297. // eventually COMPOSE_PROFILES should have been set
  298. cli.WithDefaultProfiles(o.Profiles...),
  299. cli.WithName(o.ProjectName))...)
  300. }
  301. // PluginName is the name of the plugin
  302. const PluginName = "compose"
  303. // RunningAsStandalone detects when running as a standalone program
  304. func RunningAsStandalone() bool {
  305. return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
  306. }
  307. // RootCommand returns the compose command with its child commands
  308. func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //nolint:gocyclo
  309. // filter out useless commandConn.CloseWrite warning message that can occur
  310. // when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
  311. // https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
  312. logrus.AddHook(logutil.NewFilter([]logrus.Level{
  313. logrus.WarnLevel,
  314. },
  315. "commandConn.CloseWrite:",
  316. "commandConn.CloseRead:",
  317. ))
  318. experiments := experimental.NewState()
  319. opts := ProjectOptions{}
  320. var (
  321. ansi string
  322. noAnsi bool
  323. verbose bool
  324. version bool
  325. parallel int
  326. dryRun bool
  327. )
  328. c := &cobra.Command{
  329. Short: "Docker Compose",
  330. Long: "Define and run multi-container applications with Docker",
  331. Use: PluginName,
  332. TraverseChildren: true,
  333. // By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
  334. RunE: func(cmd *cobra.Command, args []string) error {
  335. if len(args) == 0 {
  336. return cmd.Help()
  337. }
  338. if version {
  339. return versionCommand(dockerCli).Execute()
  340. }
  341. _ = cmd.Help()
  342. return dockercli.StatusError{
  343. StatusCode: compose.CommandSyntaxFailure.ExitCode,
  344. Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
  345. }
  346. },
  347. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  348. ctx := cmd.Context()
  349. parent := cmd.Root()
  350. if parent != nil {
  351. parentPrerun := parent.PersistentPreRunE
  352. if parentPrerun != nil {
  353. err := parentPrerun(cmd, args)
  354. if err != nil {
  355. return err
  356. }
  357. }
  358. }
  359. if verbose {
  360. logrus.SetLevel(logrus.TraceLevel)
  361. }
  362. if noAnsi {
  363. if ansi != "auto" {
  364. return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
  365. }
  366. ansi = "never"
  367. fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
  368. }
  369. if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
  370. ansi = v
  371. }
  372. formatter.SetANSIMode(dockerCli, ansi)
  373. if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
  374. ui.NoColor()
  375. formatter.SetANSIMode(dockerCli, formatter.Never)
  376. }
  377. switch ansi {
  378. case "never":
  379. ui.Mode = ui.ModePlain
  380. case "always":
  381. ui.Mode = ui.ModeTTY
  382. }
  383. switch opts.Progress {
  384. case ui.ModeAuto:
  385. ui.Mode = ui.ModeAuto
  386. if ansi == "never" {
  387. ui.Mode = ui.ModePlain
  388. }
  389. case ui.ModeTTY:
  390. if ansi == "never" {
  391. return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
  392. }
  393. ui.Mode = ui.ModeTTY
  394. case ui.ModePlain:
  395. if ansi == "always" {
  396. return fmt.Errorf("can't use --progress plain while ANSI support is forced")
  397. }
  398. ui.Mode = ui.ModePlain
  399. case ui.ModeQuiet, "none":
  400. ui.Mode = ui.ModeQuiet
  401. case ui.ModeJSON:
  402. ui.Mode = ui.ModeJSON
  403. default:
  404. return fmt.Errorf("unsupported --progress value %q", opts.Progress)
  405. }
  406. // (4) options validation / normalization
  407. if opts.WorkDir != "" {
  408. if opts.ProjectDir != "" {
  409. return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
  410. }
  411. opts.ProjectDir = opts.WorkDir
  412. fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
  413. }
  414. for i, file := range opts.EnvFiles {
  415. if !filepath.IsAbs(file) {
  416. file, err := filepath.Abs(file)
  417. if err != nil {
  418. return err
  419. }
  420. opts.EnvFiles[i] = file
  421. }
  422. }
  423. composeCmd := cmd
  424. for {
  425. if composeCmd.Name() == PluginName {
  426. break
  427. }
  428. if !composeCmd.HasParent() {
  429. return fmt.Errorf("error parsing command line, expected %q", PluginName)
  430. }
  431. composeCmd = composeCmd.Parent()
  432. }
  433. if v, ok := os.LookupEnv(ComposeParallelLimit); ok && !composeCmd.Flags().Changed("parallel") {
  434. i, err := strconv.Atoi(v)
  435. if err != nil {
  436. return fmt.Errorf("%s must be an integer (found: %q)", ComposeParallelLimit, v)
  437. }
  438. parallel = i
  439. }
  440. if parallel > 0 {
  441. logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
  442. backend.MaxConcurrency(parallel)
  443. }
  444. // dry run detection
  445. ctx, err := backend.DryRunMode(ctx, dryRun)
  446. if err != nil {
  447. return err
  448. }
  449. cmd.SetContext(ctx)
  450. // (6) Desktop integration
  451. var desktopCli *desktop.Client
  452. if !dryRun {
  453. if desktopCli, err = desktop.NewFromDockerClient(ctx, dockerCli); desktopCli != nil {
  454. logrus.Debugf("Enabled Docker Desktop integration (experimental) @ %s", desktopCli.Endpoint())
  455. backend.SetDesktopClient(desktopCli)
  456. } else if err != nil {
  457. // not fatal, Compose will still work but behave as though
  458. // it's not running as part of Docker Desktop
  459. logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
  460. } else {
  461. logrus.Trace("Docker Desktop integration not enabled")
  462. }
  463. }
  464. // (7) experimental features
  465. if err := experiments.Load(ctx, desktopCli); err != nil {
  466. logrus.Debugf("Failed to query feature flags from Desktop: %v", err)
  467. }
  468. backend.SetExperiments(experiments)
  469. return nil
  470. },
  471. }
  472. c.AddCommand(
  473. upCommand(&opts, dockerCli, backend, experiments),
  474. downCommand(&opts, dockerCli, backend),
  475. startCommand(&opts, dockerCli, backend),
  476. restartCommand(&opts, dockerCli, backend),
  477. stopCommand(&opts, dockerCli, backend),
  478. psCommand(&opts, dockerCli, backend),
  479. listCommand(dockerCli, backend),
  480. logsCommand(&opts, dockerCli, backend),
  481. configCommand(&opts, dockerCli),
  482. killCommand(&opts, dockerCli, backend),
  483. runCommand(&opts, dockerCli, backend),
  484. removeCommand(&opts, dockerCli, backend),
  485. execCommand(&opts, dockerCli, backend),
  486. attachCommand(&opts, dockerCli, backend),
  487. pauseCommand(&opts, dockerCli, backend),
  488. unpauseCommand(&opts, dockerCli, backend),
  489. topCommand(&opts, dockerCli, backend),
  490. eventsCommand(&opts, dockerCli, backend),
  491. portCommand(&opts, dockerCli, backend),
  492. imagesCommand(&opts, dockerCli, backend),
  493. versionCommand(dockerCli),
  494. buildCommand(&opts, dockerCli, backend),
  495. pushCommand(&opts, dockerCli, backend),
  496. pullCommand(&opts, dockerCli, backend),
  497. createCommand(&opts, dockerCli, backend),
  498. copyCommand(&opts, dockerCli, backend),
  499. waitCommand(&opts, dockerCli, backend),
  500. scaleCommand(&opts, dockerCli, backend),
  501. statsCommand(&opts, dockerCli),
  502. watchCommand(&opts, dockerCli, backend),
  503. alphaCommand(&opts, dockerCli, backend),
  504. )
  505. c.Flags().SetInterspersed(false)
  506. opts.addProjectFlags(c.Flags())
  507. c.RegisterFlagCompletionFunc( //nolint:errcheck
  508. "project-name",
  509. completeProjectNames(backend),
  510. )
  511. c.RegisterFlagCompletionFunc( //nolint:errcheck
  512. "project-directory",
  513. func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  514. return []string{}, cobra.ShellCompDirectiveFilterDirs
  515. },
  516. )
  517. c.RegisterFlagCompletionFunc( //nolint:errcheck
  518. "file",
  519. func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  520. return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
  521. },
  522. )
  523. c.RegisterFlagCompletionFunc( //nolint:errcheck
  524. "profile",
  525. completeProfileNames(dockerCli, &opts),
  526. )
  527. c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
  528. c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
  529. c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
  530. c.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Execute command in dry run mode")
  531. c.Flags().MarkHidden("version") //nolint:errcheck
  532. c.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
  533. c.Flags().MarkHidden("no-ansi") //nolint:errcheck
  534. c.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
  535. c.Flags().MarkHidden("verbose") //nolint:errcheck
  536. return c
  537. }
  538. var printerModes = []string{
  539. ui.ModeAuto,
  540. ui.ModeTTY,
  541. ui.ModePlain,
  542. ui.ModeJSON,
  543. ui.ModeQuiet,
  544. }
  545. func SetUnchangedOption(name string, experimentalFlag bool) bool {
  546. var value bool
  547. // If the var is defined we use that value first
  548. if envVar, ok := os.LookupEnv(name); ok {
  549. value = utils.StringToBool(envVar)
  550. } else {
  551. // if not, we try to get it from experimental feature flag
  552. value = experimentalFlag
  553. }
  554. return value
  555. }