From 5f005f67cf14856f39aa8177d000a087a7e4ee84 Mon Sep 17 00:00:00 2001
From: Markus Reiter <me@reitermark.us>
Date: Tue, 29 Sep 2020 23:46:30 +0200
Subject: [PATCH] Refactor global `Cask::Config`.

---
 Library/Homebrew/cask/auditor.rb              |  13 ++-
 Library/Homebrew/cask/cask.rb                 |  15 ++-
 Library/Homebrew/cask/cask_loader.rb          |  35 +++---
 Library/Homebrew/cask/caskroom.rb             |   4 +-
 Library/Homebrew/cask/cmd.rb                  |   6 -
 Library/Homebrew/cask/cmd/abstract_command.rb |   3 +-
 Library/Homebrew/cask/cmd/audit.rb            |   4 +-
 Library/Homebrew/cask/cmd/upgrade.rb          |   2 +-
 Library/Homebrew/cask/config.rb               |  58 +++++-----
 Library/Homebrew/cask/installer.rb            |   7 +-
 Library/Homebrew/cli/args.rb                  |   5 +-
 Library/Homebrew/cli/named_args.rb            |  13 ++-
 Library/Homebrew/cmd/outdated.rb              |   2 +-
 .../test/cask/artifact/binary_spec.rb         |  14 ++-
 .../cask_loader/from__path_loader_spec.rb     |   4 +-
 Library/Homebrew/test/cask/cmd/audit_spec.rb  |   2 +-
 Library/Homebrew/test/cask/cmd/cat_spec.rb    |  17 ++-
 .../Homebrew/test/cask/cmd/install_spec.rb    |  44 +++++++
 Library/Homebrew/test/cask/cmd/style_spec.rb  |   3 +-
 .../Homebrew/test/cask/cmd/uninstall_spec.rb  |   8 +-
 .../Homebrew/test/cask/cmd/upgrade_spec.rb    | 109 +++++-------------
 Library/Homebrew/test/cask/cmd_spec.rb        |  20 ----
 Library/Homebrew/test/cask/dsl_spec.rb        |  11 +-
 Library/Homebrew/test/cask/installer_spec.rb  |   2 +-
 .../spec/shared_context/homebrew_cask.rb      |   4 +-
 25 files changed, 207 insertions(+), 198 deletions(-)

diff --git a/Library/Homebrew/cask/auditor.rb b/Library/Homebrew/cask/auditor.rb
index df3e396005..264c04b7a3 100644
--- a/Library/Homebrew/cask/auditor.rb
+++ b/Library/Homebrew/cask/auditor.rb
@@ -80,11 +80,14 @@ module Cask
 
     def audit_languages(languages)
       ohai "Auditing language: #{languages.map { |lang| "'#{lang}'" }.to_sentence}"
-      localized_cask = CaskLoader.load(cask.sourcefile_path)
-      config = localized_cask.config
-      config.languages = languages
-      localized_cask.config = config
-      audit_cask_instance(localized_cask)
+
+      original_config = cask.config
+      localized_config = original_config.merge(Config.new(explicit: { languages: languages }))
+      cask.config = localized_config
+
+      audit_cask_instance(cask)
+    ensure
+      cask.config = original_config
     end
 
     def audit_cask_instance(cask)
diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb
index 54a43bf425..bb2b932793 100644
--- a/Library/Homebrew/cask/cask.rb
+++ b/Library/Homebrew/cask/cask.rb
@@ -16,13 +16,13 @@ module Cask
     extend Searchable
     include Metadata
 
-    attr_reader :token, :sourcefile_path, :config
+    attr_reader :token, :sourcefile_path, :config, :default_config
 
     def self.each(&block)
       return to_enum unless block_given?
 
       Tap.flat_map(&:cask_files).each do |f|
-        block.call CaskLoader::FromTapPathLoader.new(f).load
+        block.call CaskLoader::FromTapPathLoader.new(f).load(config: nil)
       rescue CaskUnreadableError => e
         opoo e.message
       end
@@ -34,12 +34,19 @@ module Cask
       @tap
     end
 
-    def initialize(token, sourcefile_path: nil, tap: nil, &block)
+    def initialize(token, sourcefile_path: nil, tap: nil, config: nil, &block)
       @token = token
       @sourcefile_path = sourcefile_path
       @tap = tap
       @block = block
-      self.config = Config.for_cask(self)
+
+      @default_config = config || Config.new
+
+      self.config = if config_path.exist?
+        Config.from_json(File.read(config_path))
+      else
+        @default_config
+      end
     end
 
     def config=(config)
diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb
index 2e07c32708..8946c3b58e 100644
--- a/Library/Homebrew/cask/cask_loader.rb
+++ b/Library/Homebrew/cask/cask_loader.rb
@@ -19,8 +19,8 @@ module Cask
         content = ref.to_str
 
         token  = /(?:"[^"]*"|'[^']*')/
-        curly  = /\(\s*#{token}\s*\)\s*\{.*\}/
-        do_end = /\s+#{token}\s+do(?:\s*;\s*|\s+).*end/
+        curly  = /\(\s*#{token.source}\s*\)\s*\{.*\}/
+        do_end = /\s+#{token.source}\s+do(?:\s*;\s*|\s+).*end/
         regex  = /\A\s*cask(?:#{curly.source}|#{do_end.source})\s*\Z/m
 
         content.match?(regex)
@@ -30,14 +30,16 @@ module Cask
         @content = content.force_encoding("UTF-8")
       end
 
-      def load
+      def load(config:)
+        @config = config
+
         instance_eval(content, __FILE__, __LINE__)
       end
 
       private
 
       def cask(header_token, **options, &block)
-        Cask.new(header_token, **options, &block)
+        Cask.new(header_token, **options, config: @config, &block)
       end
     end
 
@@ -57,19 +59,22 @@ module Cask
         @path = path
       end
 
-      def load
+      def load(config:)
         raise CaskUnavailableError.new(token, "'#{path}' does not exist.")  unless path.exist?
         raise CaskUnavailableError.new(token, "'#{path}' is not readable.") unless path.readable?
         raise CaskUnavailableError.new(token, "'#{path}' is not a file.")   unless path.file?
 
-        @content = IO.read(path)
+        @content = path.read(encoding: "UTF-8")
+        @config = config
 
         begin
           instance_eval(content, path).tap do |cask|
             raise CaskUnreadableError.new(token, "'#{path}' does not contain a cask.") unless cask.is_a?(Cask)
           end
         rescue NameError, ArgumentError, ScriptError => e
-          raise CaskUnreadableError.new(token, e.message)
+          error = CaskUnreadableError.new(token, e.message)
+          error.set_backtrace e.backtrace
+          raise error
         end
       end
 
@@ -102,7 +107,7 @@ module Cask
         super Cache.path/File.basename(@url.path)
       end
 
-      def load
+      def load(config:)
         path.dirname.mkpath
 
         begin
@@ -147,7 +152,7 @@ module Cask
         super Tap.fetch(user, repo).cask_dir/"#{token}.rb"
       end
 
-      def load
+      def load(config:)
         tap.install unless tap.installed?
 
         super
@@ -156,8 +161,6 @@ module Cask
 
     # Loads a cask from an existing {Cask} instance.
     class FromInstanceLoader
-      attr_reader :cask
-
       def self.can_load?(ref)
         ref.is_a?(Cask)
       end
@@ -166,8 +169,8 @@ module Cask
         @cask = cask
       end
 
-      def load
-        cask
+      def load(config:)
+        @cask
       end
     end
 
@@ -182,7 +185,7 @@ module Cask
         super CaskLoader.default_path(token)
       end
 
-      def load
+      def load(config:)
         raise CaskUnavailableError.new(token, "No Cask with this name exists.")
       end
     end
@@ -191,8 +194,8 @@ module Cask
       self.for(ref).path
     end
 
-    def self.load(ref)
-      self.for(ref).load
+    def self.load(ref, config: nil)
+      self.for(ref).load(config: config)
     end
 
     def self.for(ref)
diff --git a/Library/Homebrew/cask/caskroom.rb b/Library/Homebrew/cask/caskroom.rb
index e3519440bb..5da0995785 100644
--- a/Library/Homebrew/cask/caskroom.rb
+++ b/Library/Homebrew/cask/caskroom.rb
@@ -36,9 +36,9 @@ module Cask
         token = path.basename.to_s
 
         if tap_path = CaskLoader.tap_paths(token).first
-          CaskLoader::FromTapPathLoader.new(tap_path).load
+          CaskLoader::FromTapPathLoader.new(tap_path).load(config: nil)
         elsif caskroom_path = Pathname.glob(path.join(".metadata/*/*/*/*.rb")).first
-          CaskLoader::FromPathLoader.new(caskroom_path).load
+          CaskLoader::FromPathLoader.new(caskroom_path).load(config: nil)
         else
           CaskLoader.load(token)
         end
diff --git a/Library/Homebrew/cask/cmd.rb b/Library/Homebrew/cask/cmd.rb
index fde3f0cf9a..644737c24b 100644
--- a/Library/Homebrew/cask/cmd.rb
+++ b/Library/Homebrew/cask/cmd.rb
@@ -236,12 +236,6 @@ module Cask
 
       args = self.class.parser.parse(argv, ignore_invalid_options: true)
 
-      Config::DEFAULT_DIRS.each_key do |name|
-        Config.global.public_send(:"#{name}=", args[name]) if args[name]
-      end
-
-      Config.global.languages = args.language if args.language
-
       Tap.default_cask_tap.install unless Tap.default_cask_tap.installed?
 
       command, argv = detect_internal_command(*argv) ||
diff --git a/Library/Homebrew/cask/cmd/abstract_command.rb b/Library/Homebrew/cask/cmd/abstract_command.rb
index 2408effac1..4a55567258 100644
--- a/Library/Homebrew/cask/cmd/abstract_command.rb
+++ b/Library/Homebrew/cask/cmd/abstract_command.rb
@@ -106,8 +106,7 @@ module Cask
       def casks(alternative: -> { [] })
         return @casks if defined?(@casks)
 
-        casks = args.named.empty? ? alternative.call : args.named
-        @casks = casks.map { |cask| CaskLoader.load(cask) }
+        @casks = args.named.empty? ? alternative.call : args.named.to_casks
       rescue CaskUnavailableError => e
         reason = [e.reason, *suggestion_message(e.token)].join(" ")
         raise e.class.new(e.token, reason)
diff --git a/Library/Homebrew/cask/cmd/audit.rb b/Library/Homebrew/cask/cmd/audit.rb
index 747f770c83..c9f1dda0f0 100644
--- a/Library/Homebrew/cask/cmd/audit.rb
+++ b/Library/Homebrew/cask/cmd/audit.rb
@@ -62,8 +62,8 @@ module Cask
           else
             name
           end
-        end.map(&CaskLoader.public_method(:load))
-
+        end
+        casks = casks.map { |c| CaskLoader.load(c, config: Config.from_args(args)) }
         casks = Cask.to_a if casks.empty?
 
         failed_casks = casks.reject do |cask|
diff --git a/Library/Homebrew/cask/cmd/upgrade.rb b/Library/Homebrew/cask/cmd/upgrade.rb
index 7f56f805c0..6e58348438 100644
--- a/Library/Homebrew/cask/cmd/upgrade.rb
+++ b/Library/Homebrew/cask/cmd/upgrade.rb
@@ -118,7 +118,7 @@ module Cask
         old_cask_installer =
           Installer.new(old_cask, **old_options)
 
-        new_cask.config = Config.global.merge(old_config)
+        new_cask.config = new_cask.default_config.merge(old_config)
 
         new_options = {
           binaries:       binaries,
diff --git a/Library/Homebrew/cask/config.rb b/Library/Homebrew/cask/config.rb
index 2583478cce..04a71b9e3e 100644
--- a/Library/Homebrew/cask/config.rb
+++ b/Library/Homebrew/cask/config.rb
@@ -36,20 +36,24 @@ module Cask
       }.merge(DEFAULT_DIRS).freeze
     end
 
-    def self.global
-      @global ||= new
-    end
-
-    def self.clear
-      @global = nil
-    end
-
-    def self.for_cask(cask)
-      if cask.config_path.exist?
-        from_json(File.read(cask.config_path))
-      else
-        global
-      end
+    def self.from_args(args)
+      new(explicit: {
+        appdir:               args.appdir,
+        colorpickerdir:       args.colorpickerdir,
+        prefpanedir:          args.prefpanedir,
+        qlplugindir:          args.qlplugindir,
+        mdimporterdir:        args.mdimporterdir,
+        dictionarydir:        args.dictionarydir,
+        fontdir:              args.fontdir,
+        servicedir:           args.servicedir,
+        input_methoddir:      args.input_methoddir,
+        internet_plugindir:   args.internet_plugindir,
+        audio_unit_plugindir: args.audio_unit_plugindir,
+        vst_plugindir:        args.vst_plugindir,
+        vst3_plugindir:       args.vst3_plugindir,
+        screen_saverdir:      args.screen_saverdir,
+        languages:            args.language,
+      }.compact)
     end
 
     def self.from_json(json)
@@ -95,19 +99,19 @@ module Cask
 
     def env
       @env ||= self.class.canonicalize(
-        Shellwords.shellsplit(ENV.fetch("HOMEBREW_CASK_OPTS", ""))
-                  .select { |arg| arg.include?("=") }
-                  .map { |arg| arg.split("=", 2) }
-                  .map do |(flag, value)|
-                    key = flag.sub(/^--/, "")
-
-                    if key == "language"
-                      key = "languages"
-                      value = value.split(",")
-                    end
-
-                    [key, value]
-                  end,
+        Homebrew::EnvConfig.cask_opts
+          .select { |arg| arg.include?("=") }
+          .map { |arg| arg.split("=", 2) }
+          .map do |(flag, value)|
+            key = flag.sub(/^--/, "")
+
+            if key == "language"
+              key = "languages"
+              value = value.split(",")
+            end
+
+            [key, value]
+          end,
       )
     end
 
diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb
index 0cbf38eb5f..6efeeada6a 100644
--- a/Library/Homebrew/cask/installer.rb
+++ b/Library/Homebrew/cask/installer.rb
@@ -99,7 +99,7 @@ module Cask
       opoo "macOS's Gatekeeper has been disabled for this Cask" unless quarantine?
       stage
 
-      @cask.config = Config.global.merge(old_config)
+      @cask.config = @cask.default_config.merge(old_config)
 
       install_artifacts
 
@@ -284,10 +284,11 @@ module Cask
 
       if cask_or_formula.is_a?(Cask)
         formula_deps = cask_or_formula.depends_on.formula.map { |f| Formula[f] }
-        cask_deps = cask_or_formula.depends_on.cask.map(&CaskLoader.public_method(:load))
+        cask_deps = cask_or_formula.depends_on.cask.map { |c| CaskLoader.load(c, config: nil) }
       else
         formula_deps = cask_or_formula.deps.reject(&:build?).map(&:to_formula)
-        cask_deps = cask_or_formula.requirements.map(&:cask).compact.map(&CaskLoader.public_method(:load))
+        cask_deps = cask_or_formula.requirements.map(&:cask).compact
+                                   .map { |c| CaskLoader.load(c, config: nil) }
       end
 
       acc[cask_or_formula] ||= []
diff --git a/Library/Homebrew/cli/args.rb b/Library/Homebrew/cli/args.rb
index ed8a15c3e5..ab5d2579a2 100644
--- a/Library/Homebrew/cli/args.rb
+++ b/Library/Homebrew/cli/args.rb
@@ -20,7 +20,7 @@ module Homebrew
 
         # Can set these because they will be overwritten by freeze_named_args!
         # (whereas other values below will only be overwritten if passed).
-        self[:named_args] = NamedArgs.new
+        self[:named_args] = NamedArgs.new(parent: self)
         self[:remaining] = []
       end
 
@@ -34,6 +34,7 @@ module Homebrew
           override_spec: spec(nil),
           force_bottle:  force_bottle?,
           flags:         flags_only,
+          parent:        self,
         )
       end
 
@@ -49,7 +50,7 @@ module Homebrew
       end
 
       def named
-        named_args || NamedArgs.new
+        named_args
       end
 
       def no_named?
diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb
index 8a47787b58..e82c911175 100644
--- a/Library/Homebrew/cli/named_args.rb
+++ b/Library/Homebrew/cli/named_args.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
-require "cask/cask_loader"
 require "delegate"
+
+require "cask/cask_loader"
+require "cli/args"
 require "formulary"
 require "missing_formula"
 
@@ -11,11 +13,12 @@ module Homebrew
     #
     # @api private
     class NamedArgs < SimpleDelegator
-      def initialize(*args, override_spec: nil, force_bottle: false, flags: [])
+      def initialize(*args, parent: Args.new, override_spec: nil, force_bottle: false, flags: [])
         @args = args
         @override_spec = override_spec
         @force_bottle = force_bottle
         @flags = flags
+        @parent = parent
 
         super(@args)
       end
@@ -130,7 +133,9 @@ module Homebrew
       end
 
       def to_casks
-        @to_casks ||= downcased_unique_named.map(&Cask::CaskLoader.method(:load)).freeze
+        @to_casks ||= downcased_unique_named
+                      .map { |name| Cask::CaskLoader.load(name, config: Cask::Config.from_args(@parent)) }
+                      .freeze
       end
 
       def to_kegs
@@ -155,7 +160,7 @@ module Homebrew
             warn_if_cask_conflicts(name, "keg")
           rescue NoSuchKegError, FormulaUnavailableError
             begin
-              casks << Cask::CaskLoader.load(name)
+              casks << Cask::CaskLoader.load(name, config: Cask::Config.from_args(@parent))
             rescue Cask::CaskUnavailableError
               raise "No installed keg or cask with the name \"#{name}\""
             end
diff --git a/Library/Homebrew/cmd/outdated.rb b/Library/Homebrew/cmd/outdated.rb
index a0d6b9eaf3..e1a26b7f15 100644
--- a/Library/Homebrew/cmd/outdated.rb
+++ b/Library/Homebrew/cmd/outdated.rb
@@ -174,7 +174,7 @@ module Homebrew
 
   def outdated_casks(args:)
     if args.named.present?
-      select_outdated(args.named.uniq.map(&Cask::CaskLoader.method(:load)), args: args)
+      select_outdated(args.named.to_casks, args: args)
     else
       select_outdated(Cask::Caskroom.casks, args: args)
     end
diff --git a/Library/Homebrew/test/cask/artifact/binary_spec.rb b/Library/Homebrew/test/cask/artifact/binary_spec.rb
index 83098af32a..230046770c 100644
--- a/Library/Homebrew/test/cask/artifact/binary_spec.rb
+++ b/Library/Homebrew/test/cask/artifact/binary_spec.rb
@@ -7,10 +7,16 @@ describe Cask::Artifact::Binary, :cask do
     end
   }
   let(:artifacts) { cask.artifacts.select { |a| a.is_a?(described_class) } }
-  let(:expected_path) { cask.config.binarydir.join("binary") }
+  let(:binarydir) { cask.config.binarydir }
+  let(:expected_path) { binarydir.join("binary") }
 
-  after do
-    FileUtils.rm expected_path if expected_path.exist?
+  around do |example|
+    binarydir.mkpath
+
+    example.run
+  ensure
+    FileUtils.rm_f expected_path
+    FileUtils.rmdir binarydir
   end
 
   context "when --no-binaries is specified" do
@@ -80,7 +86,7 @@ describe Cask::Artifact::Binary, :cask do
   end
 
   it "creates parent directory if it doesn't exist" do
-    FileUtils.rmdir Cask::Config.global.binarydir
+    FileUtils.rmdir binarydir
 
     artifacts.each do |artifact|
       artifact.install_phase(command: NeverSudoSystemCommand, force: false)
diff --git a/Library/Homebrew/test/cask/cask_loader/from__path_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from__path_loader_spec.rb
index 36de965054..86fa8061e2 100644
--- a/Library/Homebrew/test/cask/cask_loader/from__path_loader_spec.rb
+++ b/Library/Homebrew/test/cask/cask_loader/from__path_loader_spec.rb
@@ -13,7 +13,7 @@ describe Cask::CaskLoader::FromPathLoader do
 
       it "raises an error" do
         expect {
-          described_class.new(path).load
+          described_class.new(path).load(config: nil)
         }.to raise_error(Cask::CaskUnreadableError, /does not contain a cask/)
       end
     end
@@ -29,7 +29,7 @@ describe Cask::CaskLoader::FromPathLoader do
 
       it "raises an error" do
         expect {
-          described_class.new(path).load
+          described_class.new(path).load(config: nil)
         }.to raise_error(Cask::CaskUnreadableError, /undefined local variable or method/)
       end
     end
diff --git a/Library/Homebrew/test/cask/cmd/audit_spec.rb b/Library/Homebrew/test/cask/cmd/audit_spec.rb
index 968e61ce64..18f2fc162c 100644
--- a/Library/Homebrew/test/cask/cmd/audit_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/audit_spec.rb
@@ -20,7 +20,7 @@ describe Cask::Cmd::Audit, :cask do
 
     it "audits specified Casks if tokens are given" do
       cask_token = "nice-app"
-      expect(Cask::CaskLoader).to receive(:load).with(cask_token).and_return(cask)
+      expect(Cask::CaskLoader).to receive(:load).with(cask_token, any_args).and_return(cask)
 
       expect(Cask::Auditor).to receive(:audit)
         .with(cask, quarantine: true)
diff --git a/Library/Homebrew/test/cask/cmd/cat_spec.rb b/Library/Homebrew/test/cask/cmd/cat_spec.rb
index b6a4d0f184..645e666ca8 100644
--- a/Library/Homebrew/test/cask/cmd/cat_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/cat_spec.rb
@@ -23,6 +23,19 @@ describe Cask::Cmd::Cat, :cask do
         end
       RUBY
     }
+    let(:caffeine_content) {
+      <<~'RUBY'
+        cask "local-caffeine" do
+          version "1.2.3"
+          sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94"
+
+          url "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip"
+          homepage "https://brew.sh/"
+
+          app "Caffeine.app"
+        end
+      RUBY
+    }
 
     it "displays the Cask file content about the specified Cask" do
       expect {
@@ -32,8 +45,8 @@ describe Cask::Cmd::Cat, :cask do
 
     it "can display multiple Casks" do
       expect {
-        described_class.run("basic-cask", "basic-cask")
-      }.to output(basic_cask_content * 2).to_stdout
+        described_class.run("basic-cask", "local-caffeine")
+      }.to output(basic_cask_content + caffeine_content).to_stdout
     end
   end
 
diff --git a/Library/Homebrew/test/cask/cmd/install_spec.rb b/Library/Homebrew/test/cask/cmd/install_spec.rb
index a5b0adbf7b..585d7ac84e 100644
--- a/Library/Homebrew/test/cask/cmd/install_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/install_spec.rb
@@ -31,6 +31,50 @@ describe Cask::Cmd::Install, :cask do
     expect(caffeine.config.appdir.join("Caffeine.app")).to be_a_directory
   end
 
+  it "recognizes the --appdir flag" do
+    appdir = mktmpdir
+
+    expect(Cask::CaskLoader).to receive(:load).with("local-caffeine", any_args)
+      .and_wrap_original { |f, *args|
+        caffeine = f.call(*args)
+        expect(caffeine.config.appdir).to eq appdir
+        caffeine
+      }
+
+    described_class.run("local-caffeine", "--appdir=#{appdir}")
+  end
+
+  it "recognizes the --appdir flag from HOMEBREW_CASK_OPTS" do
+    appdir = mktmpdir
+
+    expect(Cask::CaskLoader).to receive(:load).with("local-caffeine", any_args)
+      .and_wrap_original { |f, *args|
+        caffeine = f.call(*args)
+        expect(caffeine.config.appdir).to eq appdir
+        caffeine
+      }
+
+    ENV["HOMEBREW_CASK_OPTS"] = "--appdir=#{appdir}"
+
+    described_class.run("local-caffeine")
+  end
+
+  it "prefers an explicit --appdir flag to one from HOMEBREW_CASK_OPTS" do
+    global_appdir = mktmpdir
+    appdir = mktmpdir
+
+    expect(Cask::CaskLoader).to receive(:load).with("local-caffeine", any_args)
+      .and_wrap_original { |f, *args|
+        caffeine = f.call(*args)
+        expect(caffeine.config.appdir).to eq appdir
+        caffeine
+      }
+
+    ENV["HOMEBREW_CASK_OPTS"] = "--appdir=#{global_appdir}"
+
+    described_class.run("local-caffeine", "--appdir=#{appdir}")
+  end
+
   it "skips double install (without nuking existing installation)" do
     described_class.run("local-transmission")
     described_class.run("local-transmission")
diff --git a/Library/Homebrew/test/cask/cmd/style_spec.rb b/Library/Homebrew/test/cask/cmd/style_spec.rb
index 2acb184082..32ce90aa2b 100644
--- a/Library/Homebrew/test/cask/cmd/style_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/style_spec.rb
@@ -30,7 +30,8 @@ describe Cask::Cmd::Style, :cask do
     subject { cli.cask_paths }
 
     before do
-      allow(cli).to receive(:args).and_return(instance_double(Homebrew::CLI::Args, named: tokens))
+      args = instance_double(Homebrew::CLI::Args, named: Homebrew::CLI::NamedArgs.new(*tokens))
+      allow(cli).to receive(:args).and_return(args)
     end
 
     context "when no cask tokens are given" do
diff --git a/Library/Homebrew/test/cask/cmd/uninstall_spec.rb b/Library/Homebrew/test/cask/cmd/uninstall_spec.rb
index a29977447e..f5c330615a 100644
--- a/Library/Homebrew/test/cask/cmd/uninstall_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/uninstall_spec.rb
@@ -64,7 +64,7 @@ describe Cask::Cmd::Uninstall, :cask do
     Cask::Installer.new(cask).install
 
     expect(cask).to be_installed
-    expect(Cask::Config.global.appdir.join("MyFancyApp.app")).to exist
+    expect(cask.config.appdir.join("MyFancyApp.app")).to exist
 
     expect {
       described_class.run("with-uninstall-script-app")
@@ -143,8 +143,8 @@ describe Cask::Cmd::Uninstall, :cask do
     end
   end
 
-  describe "when Casks in Taps have been renamed or removed" do
-    let(:app) { Cask::Config.global.appdir.join("ive-been-renamed.app") }
+  context "when Casks in Taps have been renamed or removed" do
+    let(:app) { Cask::Config.new.appdir.join("ive-been-renamed.app") }
     let(:caskroom_path) { Cask::Caskroom.path.join("ive-been-renamed").tap(&:mkpath) }
     let(:saved_caskfile) {
       caskroom_path.join(".metadata", "latest", "timestamp", "Casks").join("ive-been-renamed.rb")
@@ -168,7 +168,7 @@ describe Cask::Cmd::Uninstall, :cask do
       RUBY
     end
 
-    it "can still uninstall those Casks" do
+    it "can still uninstall them" do
       described_class.run("ive-been-renamed")
 
       expect(app).not_to exist
diff --git a/Library/Homebrew/test/cask/cmd/upgrade_spec.rb b/Library/Homebrew/test/cask/cmd/upgrade_spec.rb
index 2c0fff3d7b..3a9137657c 100644
--- a/Library/Homebrew/test/cask/cmd/upgrade_spec.rb
+++ b/Library/Homebrew/test/cask/cmd/upgrade_spec.rb
@@ -3,6 +3,16 @@
 require_relative "shared_examples/invalid_option"
 
 describe Cask::Cmd::Upgrade, :cask do
+  let(:version_latest_path_2) { version_latest.config.appdir.join("Caffeine Pro.app") }
+  let(:version_latest_path_1) { version_latest.config.appdir.join("Caffeine Mini.app") }
+  let(:version_latest) { Cask::CaskLoader.load("version-latest") }
+  let(:auto_updates_path) { auto_updates.config.appdir.join("MyFancyApp.app") }
+  let(:auto_updates) { Cask::CaskLoader.load("auto-updates") }
+  let(:local_transmission_path) { local_transmission.config.appdir.join("Transmission.app") }
+  let(:local_transmission) { Cask::CaskLoader.load("local-transmission") }
+  let(:local_caffeine_path) { local_caffeine.config.appdir.join("Caffeine.app") }
+  let(:local_caffeine) { Cask::CaskLoader.load("local-caffeine") }
+
   it_behaves_like "a command that handles invalid options"
 
   context "successful upgrade" do
@@ -23,11 +33,6 @@ describe Cask::Cmd::Upgrade, :cask do
 
     describe 'without --greedy it ignores the Casks with "version latest" or "auto_updates true"' do
       it "updates all the installed Casks when no token is provided" do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -48,11 +53,6 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it "updates only the Casks specified in the command line" do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -73,11 +73,6 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it 'updates "auto_updates" and "latest" Casks when their tokens are provided in the command line' do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        auto_updates = Cask::CaskLoader.load("auto-updates")
-        auto_updates_path = Cask::Config.global.appdir.join("MyFancyApp.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -100,16 +95,6 @@ describe Cask::Cmd::Upgrade, :cask do
 
     describe "with --greedy it checks additional Casks" do
       it 'includes the Casks with "auto_updates true" or "version latest"' do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        auto_updates = Cask::CaskLoader.load("auto-updates")
-        auto_updates_path = Cask::Config.global.appdir.join("MyFancyApp.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-        version_latest = Cask::CaskLoader.load("version-latest")
-        version_latest_path_1 = Cask::Config.global.appdir.join("Caffeine Mini.app")
-        version_latest_path_2 = Cask::Config.global.appdir.join("Caffeine Pro.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -148,24 +133,21 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it 'does not include the Casks with "auto_updates true" when the version did not change' do
-        cask = Cask::CaskLoader.load("auto-updates")
-        cask_path = cask.config.appdir.join("MyFancyApp.app")
-
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.57")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.57")
 
         described_class.run("auto-updates", "--greedy")
 
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.61")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.61")
 
         described_class.run("auto-updates", "--greedy")
 
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.61")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.61")
       end
     end
   end
@@ -188,11 +170,6 @@ describe Cask::Cmd::Upgrade, :cask do
 
     describe 'without --greedy it ignores the Casks with "version latest" or "auto_updates true"' do
       it "would update all the installed Casks when no token is provided" do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -215,11 +192,6 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it "would update only the Casks specified in the command line" do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -242,11 +214,6 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it 'would update "auto_updates" and "latest" Casks when their tokens are provided in the command line' do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        auto_updates = Cask::CaskLoader.load("auto-updates")
-        auto_updates_path = Cask::Config.global.appdir.join("MyFancyApp.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -271,16 +238,6 @@ describe Cask::Cmd::Upgrade, :cask do
 
     describe "with --greedy it checks additional Casks" do
       it 'would include the Casks with "auto_updates true" or "version latest"' do
-        local_caffeine = Cask::CaskLoader.load("local-caffeine")
-        local_caffeine_path = Cask::Config.global.appdir.join("Caffeine.app")
-        auto_updates = Cask::CaskLoader.load("auto-updates")
-        auto_updates_path = Cask::Config.global.appdir.join("MyFancyApp.app")
-        local_transmission = Cask::CaskLoader.load("local-transmission")
-        local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-        version_latest = Cask::CaskLoader.load("version-latest")
-        version_latest_path_1 = Cask::Config.global.appdir.join("Caffeine Mini.app")
-        version_latest_path_2 = Cask::Config.global.appdir.join("Caffeine Pro.app")
-
         expect(local_caffeine).to be_installed
         expect(local_caffeine_path).to be_a_directory
         expect(local_caffeine.versions).to include("1.2.2")
@@ -319,26 +276,23 @@ describe Cask::Cmd::Upgrade, :cask do
       end
 
       it 'does not include the Casks with "auto_updates true" when the version did not change' do
-        cask = Cask::CaskLoader.load("auto-updates")
-        cask_path = cask.config.appdir.join("MyFancyApp.app")
-
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.57")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.57")
 
         described_class.run("--dry-run", "auto-updates", "--greedy")
 
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.57")
-        expect(cask.versions).not_to include("2.61")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.57")
+        expect(auto_updates.versions).not_to include("2.61")
 
         described_class.run("--dry-run", "auto-updates", "--greedy")
 
-        expect(cask).to be_installed
-        expect(cask_path).to be_a_directory
-        expect(cask.versions).to include("2.57")
-        expect(cask.versions).not_to include("2.61")
+        expect(auto_updates).to be_installed
+        expect(auto_updates_path).to be_a_directory
+        expect(auto_updates.versions).to include("2.57")
+        expect(auto_updates.versions).not_to include("2.61")
       end
     end
   end
@@ -417,9 +371,6 @@ describe Cask::Cmd::Upgrade, :cask do
       bad_checksum = Cask::CaskLoader.load("bad-checksum")
       bad_checksum_path = bad_checksum.config.appdir.join("Caffeine.app")
 
-      local_transmission = Cask::CaskLoader.load("local-transmission")
-      local_transmission_path = Cask::Config.global.appdir.join("Transmission.app")
-
       bad_checksum_2 = Cask::CaskLoader.load("bad-checksum2")
       bad_checksum_2_path = bad_checksum_2.config.appdir.join("container")
 
diff --git a/Library/Homebrew/test/cask/cmd_spec.rb b/Library/Homebrew/test/cask/cmd_spec.rb
index a41b64f1e1..e715d05671 100644
--- a/Library/Homebrew/test/cask/cmd_spec.rb
+++ b/Library/Homebrew/test/cask/cmd_spec.rb
@@ -16,26 +16,6 @@ describe Cask::Cmd, :cask do
       }.to output(/Displays information about the given cask/).to_stdout
     end
 
-    it "respects the env variable when choosing what appdir to create" do
-      allow(described_class).to receive(:lookup_command).with("noop").and_return(noop_command)
-
-      ENV["HOMEBREW_CASK_OPTS"] = "--appdir=/custom/appdir"
-
-      described_class.run("noop")
-
-      expect(Cask::Config.global.appdir).to eq(Pathname.new("/custom/appdir"))
-    end
-
-    it "overrides the env variable when passing --appdir directly" do
-      allow(described_class).to receive(:lookup_command).with("noop").and_return(noop_command)
-
-      ENV["HOMEBREW_CASK_OPTS"] = "--appdir=/custom/appdir"
-
-      described_class.run("noop", "--appdir=/even/more/custom/appdir")
-
-      expect(Cask::Config.global.appdir).to eq(Pathname.new("/even/more/custom/appdir"))
-    end
-
     it "exits with a status of 1 when something goes wrong" do
       allow(described_class).to receive(:lookup_command).and_raise(Cask::CaskError)
       command = described_class.new("noop")
diff --git a/Library/Homebrew/test/cask/dsl_spec.rb b/Library/Homebrew/test/cask/dsl_spec.rb
index cebf4c1af7..0ef5993221 100644
--- a/Library/Homebrew/test/cask/dsl_spec.rb
+++ b/Library/Homebrew/test/cask/dsl_spec.rb
@@ -503,16 +503,15 @@ describe Cask::DSL, :cask do
     end
 
     it "does not include a trailing slash" do
-      original_appdir = Cask::Config.global.appdir
-      Cask::Config.global.appdir = "#{original_appdir}/"
+      config = Cask::Config.new(explicit: {
+                                  appdir: "/Applications/",
+                                })
 
-      cask = Cask::Cask.new("appdir-trailing-slash") do
+      cask = Cask::Cask.new("appdir-trailing-slash", config: config) do
         binary "#{appdir}/some/path"
       end
 
-      expect(cask.artifacts.first.source).to eq(original_appdir/"some/path")
-    ensure
-      Cask::Config.global.appdir = original_appdir
+      expect(cask.artifacts.first.source).to eq(Pathname("/Applications/some/path"))
     end
   end
 
diff --git a/Library/Homebrew/test/cask/installer_spec.rb b/Library/Homebrew/test/cask/installer_spec.rb
index bb13ec0050..a7b5045bd4 100644
--- a/Library/Homebrew/test/cask/installer_spec.rb
+++ b/Library/Homebrew/test/cask/installer_spec.rb
@@ -195,7 +195,7 @@ describe Cask::Installer, :cask do
 
       described_class.new(nested_app).install
 
-      expect(Cask::Config.global.appdir.join("MyNestedApp.app")).to be_a_directory
+      expect(nested_app.config.appdir.join("MyNestedApp.app")).to be_a_directory
     end
 
     it "generates and finds a timestamped metadata directory for an installed Cask" do
diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb
index 87c9517b6c..db6836796f 100644
--- a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb
+++ b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb
@@ -37,7 +37,6 @@ RSpec.shared_context "Homebrew Cask", :needs_macos do
 
     begin
       Cask::Config::DEFAULT_DIRS_PATHNAMES.values.each(&:mkpath)
-      Cask::Config.global.binarydir.mkpath
 
       Tap.default_cask_tap.tap do |tap|
         FileUtils.mkdir_p tap.path.dirname
@@ -52,11 +51,10 @@ RSpec.shared_context "Homebrew Cask", :needs_macos do
       example.run
     ensure
       FileUtils.rm_rf Cask::Config::DEFAULT_DIRS_PATHNAMES.values
-      FileUtils.rm_rf [Cask::Config.global.binarydir, Cask::Caskroom.path, Cask::Cache.path]
+      FileUtils.rm_rf [Cask::Config.new.binarydir, Cask::Caskroom.path, Cask::Cache.path]
       Tap.default_cask_tap.path.unlink
       third_party_tap.path.unlink
       FileUtils.rm_rf third_party_tap.path.parent
-      Cask::Config.clear
     end
   end
 end
-- 
GitLab