Class: ShopifyCLI::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/shopify_cli/context.rb

Overview

Context captures a lot about the current running command. It captures the environment, output, system and file operations. It is useful to have the context especially in tests so that you have a single access point to these resoures.

Constant Summary collapse

GEM_LATEST_URI =
URI.parse("https://rubygems.org/api/v1/versions/shopify-cli/latest.json")
VERSION_CHECK_SECTION =
"versioncheck"
LAST_CHECKED_AT_FIELD =
"last_checked_at"
LATEST_VERSION_FIELD =
"latest_version"
VERSION_CHECK_INTERVAL =
86400

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root: Dir.pwd, env: ($original_env || ENV).to_h) ⇒ Context

:nodoc:



108
109
110
111
# File 'lib/shopify_cli/context.rb', line 108

def initialize(root: Dir.pwd, env: ($original_env || ENV).to_h) # :nodoc:
  self.root = root
  self.env = env
end

Class Attribute Details

.messagesObject (readonly)

Returns the value of attribute messages.



24
25
26
# File 'lib/shopify_cli/context.rb', line 24

def messages
  @messages
end

Instance Attribute Details

#envObject

is an accessor for environment variables. These variables are also added to any command run by the context.



106
107
108
# File 'lib/shopify_cli/context.rb', line 106

def env
  @env
end

#rootObject

is the directory root that the current command is running in. If you want to simulate a ‘cd` for the file operations, you can change this variable.



103
104
105
# File 'lib/shopify_cli/context.rb', line 103

def root
  @root
end

Class Method Details

.abort(error_message, help_message = nil) ⇒ Object

aborts the current running command and outputs an error message:

  • when the ‘help_message` is not provided, the error message appears in a red frame, prefixed by an ✗ icon

  • when the ‘help_message` is provided, the error message appears in a red frame, and the help message appears in a green frame

#### Parameters

  • ‘error_message` - an error message to output

  • ‘help_message` - an optional help message

#### Example

ShopifyCLI::Context.abort("Execution error")
# Output:
# ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┃ ✗ Execution error
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ShopifyCLI::Context.abort("Execution error", "export EXECUTION=1")
# Output:
# ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┃ Execution error
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┏━━ Try this ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┃ export EXECUTION=1
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Raises:



85
86
87
88
89
90
91
92
# File 'lib/shopify_cli/context.rb', line 85

def abort(error_message, help_message = nil)
  raise ShopifyCLI::Abort, "{{x}} #{error_message}" if help_message.nil?

  frame(message("core.error"), color: :red) { self.puts(error_message) }
  frame(message("core.try_this"), color: :green) { self.puts(help_message) }

  raise ShopifyCLI::AbortSilent
end

.load_messages(messages) ⇒ Object

adds a new set of messages to be used by the CLI. The messages are expected to be a hash of symbols, and multiple levels are allowed. When fetching messages a dot notation is used to separate different levels. See Context::message for more information.

#### Parameters

  • ‘messages` - Hash containing the new keys to register



32
33
34
35
36
37
# File 'lib/shopify_cli/context.rb', line 32

def load_messages(messages)
  @messages ||= {}
  @messages = @messages.merge(messages) do |key|
    Context.new.abort("Message key '#{key}' already exists and cannot be registered") if @messages.key?(key)
  end
end

.message(key, *params) ⇒ Object

returns the user-facing messages for the given key. Returns the key if no message is available.

#### Parameters

  • ‘key` - a symbol representing the message

  • ‘params` - the parameters to format the string with



44
45
46
47
48
# File 'lib/shopify_cli/context.rb', line 44

def message(key, *params)
  key_parts = key.split(".").map(&:to_sym)
  str = Context.messages.dig(*key_parts)
  str ? str % params : key
end

.puts(*args) ⇒ Object

a wrapper around Kernel.puts to allow for easy formatting

#### Parameters

  • ‘text` - a string message to output



54
55
56
# File 'lib/shopify_cli/context.rb', line 54

def puts(*args)
  Kernel.puts(CLI::UI.fmt(*args))
end

Instance Method Details

#abort(error_message, help_message = nil) ⇒ Object

proxy call to Context.abort.

#### Parameters

  • ‘error_message` - an error message to output

  • ‘help_message` - an optional help message



450
451
452
# File 'lib/shopify_cli/context.rb', line 450

def abort(error_message, help_message = nil)
  Context.abort(error_message, help_message)
end

#binread(fname) ⇒ Object

will read a binary file relative to the context root unless the file path is absolute.

#### Parameters

  • ‘fname` - filename of the file that you are reading, relative to root unless it is absolute.

#### Example

@ctx.read('binary.out')


244
245
246
# File 'lib/shopify_cli/context.rb', line 244

def binread(fname)
  File.binread(ctx_path(fname))
end

#binwrite(fname, content) ⇒ Object

will write/overwrite a binary file with the provided contents, relative to the context root unless the file path is absolute.

#### Parameters

  • ‘fname` - filename of the file that you are writing, relative to root unless it is absolute.

  • ‘content` - the body contents of the file that you are writing

#### Example

@ctx.binwrite('binary.out', 'ASCII-8BIT encoded binary')


259
260
261
# File 'lib/shopify_cli/context.rb', line 259

def binwrite(fname, content)
  File.binwrite(ctx_path(fname), content)
end

#capture2(*args, **kwargs) ⇒ Object

Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console

#### Parameters

  • ‘*args`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)

  • ‘**kwargs`: additional arguments to pass to Open3.capture2

#### Returns

  • ‘output`: output (STDOUT) of the command execution

  • ‘status`: boolean success status of the command execution

#### Usage

out, stat = @ctx.capture2('ls', 'a_folder')


517
518
519
# File 'lib/shopify_cli/context.rb', line 517

def capture2(*args, **kwargs)
  CLI::Kit::System.capture2(*args, env: @env, **kwargs)
end

#capture2e(*args, **kwargs) ⇒ Object

Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console

#### Parameters

  • ‘*args`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)

  • ‘**kwargs`: additional arguments to pass to Open3.capture2e

#### Returns

  • ‘output`: output (STDOUT merged with STDERR) of the command execution

  • ‘status`: boolean success status of the command execution

#### Usage

out_and_err, stat = @ctx.capture2e('ls', 'a_folder')


537
538
539
# File 'lib/shopify_cli/context.rb', line 537

def capture2e(*args, **kwargs)
  CLI::Kit::System.capture2e(*args, env: @env, **kwargs)
end

#capture3(*args, **kwargs) ⇒ Object

Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console

#### Parameters

  • ‘*args`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)

  • ‘**kwargs`: additional arguments to pass to Open3.capture3

#### Returns

  • ‘output`: STDOUT of the command execution

  • ‘error`: STDERR of the command execution

  • ‘status`: boolean success status of the command execution

#### Usage

out, err, stat = @ctx.capture3('ls', 'a_folder')


558
559
560
# File 'lib/shopify_cli/context.rb', line 558

def capture3(*args, **kwargs)
  CLI::Kit::System.capture3(*args, env: @env, **kwargs)
end

#chdir(path) ⇒ Object

will change directories and update the root, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘path` - the file path to a directory, relative to the context root to remove from the FS



268
269
270
271
# File 'lib/shopify_cli/context.rb', line 268

def chdir(path)
  Dir.chdir(ctx_path(path))
  self.root = ctx_path(path)
end

#ci?Boolean

will return true if the cli is being tested on CI

Returns:

  • (Boolean)


174
175
176
# File 'lib/shopify_cli/context.rb', line 174

def ci?
  ENV["CI"]
end

#cp(from, to) ⇒ Object

will copy a directory from the FS, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘from` - the path of the original file

  • ‘to` - the destination path



309
310
311
# File 'lib/shopify_cli/context.rb', line 309

def cp(from, to)
  FileUtils.cp(ctx_path(from), ctx_path(to))
end

#cp_r(from, to) ⇒ Object

will recursively copy a directory from the FS, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘from` - the path of the original file

  • ‘to` - the destination path



298
299
300
# File 'lib/shopify_cli/context.rb', line 298

def cp_r(from, to)
  FileUtils.cp_r(ctx_path(from), ctx_path(to))
end

#debug(text) ⇒ Object

outputs a message, prefixed by a red ‘DEBUG` tag. This will only output to the console if you have `DEBUG=1` set in your shell environment.

#### Parameters

  • ‘text` - a string message to output



460
461
462
# File 'lib/shopify_cli/context.rb', line 460

def debug(text)
  puts("{{red:DEBUG}} #{text}") if debug?
end

#debug?Boolean

will return true if the cli is running with the DEBUG flag

Returns:

  • (Boolean)


180
181
182
# File 'lib/shopify_cli/context.rb', line 180

def debug?
  getenv("DEBUG")
end

#development?Boolean

will return true if the cli is running on your development instance.

Returns:

  • (Boolean)


163
164
165
# File 'lib/shopify_cli/context.rb', line 163

def development?
  !system? && !testing?
end

#dir_exist?(path) ⇒ Boolean

checks if a directory exists, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘path` - the file path to a directory, relative to the context root to remove from the FS

Returns:

  • (Boolean)


278
279
280
# File 'lib/shopify_cli/context.rb', line 278

def dir_exist?(path)
  Dir.exist?(ctx_path(path))
end

#done(text) ⇒ Object

outputs a message, prefixed by a checkmark indicating that something completed

#### Parameters

  • ‘text` - a string message to output



441
442
443
# File 'lib/shopify_cli/context.rb', line 441

def done(text)
  puts("{{v}} #{text}")
end

#error(text) ⇒ Object

a wrapper around $stderr.puts to allow for easy formatting

#### Parameters

  • ‘text` - a string message to output



423
424
425
# File 'lib/shopify_cli/context.rb', line 423

def error(text)
  $stderr.puts(CLI::UI.fmt(text))
end

#executable_file_extension(ext = ".exe") ⇒ Object

Returns file extension depending on OS since windows has multiple extensions, the default is .exe unless otherwise specified

#### Parameters

  • ext: optional extension for windows file

#### Returns

  • ext: string for file extension on windows

    : empty string otherwise
    


646
647
648
649
650
651
652
# File 'lib/shopify_cli/context.rb', line 646

def executable_file_extension(ext = ".exe")
  if windows?
    ext
  else
    ""
  end
end

#file_exist?(path) ⇒ Boolean

checks if a file exists, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘path` - the file path to a file, relative to the context root to remove from the FS

Returns:

  • (Boolean)


287
288
289
# File 'lib/shopify_cli/context.rb', line 287

def file_exist?(path)
  File.exist?(ctx_path(path))
end

#getenv(name) ⇒ Object

get a environment variable value by name.

#### Parameters

  • ‘name` - the name of the environment variable that you want to fetch

#### Returns

  • ‘value` - will return the value, or nil if the variable does not exist



192
193
194
195
# File 'lib/shopify_cli/context.rb', line 192

def getenv(name)
  v = @env[name]
  v == "" ? nil : v
end

#linux?Boolean

will return true if the cli is running on a linux distro

Returns:

  • (Boolean)


134
135
136
# File 'lib/shopify_cli/context.rb', line 134

def linux?
  os == :linux
end

#mac?Boolean

will return true if the cli is running on a Intel x86 Apple computer.

Returns:

  • (Boolean)


129
130
131
# File 'lib/shopify_cli/context.rb', line 129

def mac?
  os == :mac
end

#mac_m1?Boolean

will return true if the cli is running on an ARM Apple computer.

Returns:

  • (Boolean)


124
125
126
# File 'lib/shopify_cli/context.rb', line 124

def mac_m1?
  os == :mac_m1
end

#message(key, *params) ⇒ Object

proxy call to Context.message.

#### Parameters

  • ‘key` - a symbol representing the message

  • ‘params` - the parameters to format the string with



469
470
471
# File 'lib/shopify_cli/context.rb', line 469

def message(key, *params)
  Context.message(key, *params)
end

#mkdir_p(path) ⇒ Object

will create a directory, recursively if it does not exist. So if you create a directory ‘foo/bar/dun`, this will also create the directories `foo` and `foo/bar` if they do not exist. The path will be made relative to the command root unless absolute

#### Parameters

  • ‘path` - file path of the directory that you want to create



362
363
364
# File 'lib/shopify_cli/context.rb', line 362

def mkdir_p(path)
  FileUtils.mkdir_p(path)
end

#new_versionObject

Checks if there’s a newer version of the CLI available and returns version string if this should be conveyed to the user (i.e., if it’s been over 24 hours since last check)

#### Parameters

#### Returns

  • ‘version`: string of newer version available, IFF new version is available AND it’s time to inform user,

    : nil otherwise
    


622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/shopify_cli/context.rb', line 622

def new_version
  if (time_of_last_check + VERSION_CHECK_INTERVAL) < (Time.now.to_i)
    # Fork is not supported in Windows
    if Process.respond_to?(:fork)
      fork { retrieve_latest_gem_version }
    else
      thread = Thread.new { retrieve_latest_gem_version }
      at_exit { thread.join }
    end
    latest_version =
      ShopifyCLI::Config.get(VERSION_CHECK_SECTION, LATEST_VERSION_FIELD, default: ShopifyCLI::VERSION)
    latest_version if ::Semantic::Version.new(latest_version) > ::Semantic::Version.new(ShopifyCLI::VERSION)
  end
end

#on_siginfoObject

captures the info signal (ctrl-T) and provide a handler to it.

#### Example

@ctx.on_siginfo do
  @ctx.open_url!("http://google.com")
end


570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/shopify_cli/context.rb', line 570

def on_siginfo
  # Reset any previous SIGINFO handling we had so the only action we take is the given block
  trap("INFO", "DEFAULT")

  fork do
    r, w = IO.pipe
    @signal = false
    trap("SIGINFO") do
      @signal = true
      w.write(0)
    end
    while r.read(1)
      next unless @signal
      @signal = false
      yield
    end
  rescue Interrupt
    exit(0)
  end
end

#open_browser_url!(uri) ⇒ Object

will output to the console a link for the user to either copy/paste or click on.

#### Parameters

  • ‘uri` - a http URI to open in a browser



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/shopify_cli/context.rb', line 383

def open_browser_url!(uri)
  if tty?
    if linux? && which("xdg-open")
      system("xdg-open", uri.to_s)
    elsif windows?
      system("start \"\" \"#{uri}\"")
    elsif mac? || mac_m1?
      system("open", uri.to_s)
    else
      open_url!(uri)
    end
  else
    open_url!(uri)
  end
end

#open_url!(uri) ⇒ Object

will output to the console a link for the user to either copy/paste or click on.

#### Parameters

  • ‘uri` - a http URI to open in a browser



372
373
374
375
# File 'lib/shopify_cli/context.rb', line 372

def open_url!(uri)
  help = message("core.context.open_url", uri)
  puts(help)
end

#osObject

will return which operating system that the cli is running on [:mac, :linux]



114
115
116
117
118
119
120
121
# File 'lib/shopify_cli/context.rb', line 114

def os
  host = uname
  return :mac_m1 if /arm64.*darwin/i.match(host)
  return :mac if /darwin/i.match(host)
  return :windows if /mswin|mingw|cygwin/i.match(host)
  return :linux if /linux|bsd/i.match(host)
  :unknown
end

will output a message, prefixed by a yellow star, indicating that task started.

#### Parameters

  • ‘text` - a string message to output



405
406
407
# File 'lib/shopify_cli/context.rb', line 405

def print_task(text)
  puts "{{yellow:*}} #{text}"
end

#puts(*args) ⇒ Object

proxy call to Context.puts.

#### Parameters

  • ‘text` - a string message to output



414
415
416
# File 'lib/shopify_cli/context.rb', line 414

def puts(*args)
  Context.puts(*args)
end

#read(fname) ⇒ Object

will read a file relative to the context root unless the file path is absolute.

#### Parameters

  • ‘fname` - filename of the file that you are reading, relative to root unless it is absolute.

#### Example

@ctx.read('file.txt')


231
232
233
# File 'lib/shopify_cli/context.rb', line 231

def read(fname)
  File.read(ctx_path(fname))
end

#rename(from, to) ⇒ Object

will rename a file from one place to another, relative to the command root unless the path is absolute.

#### Parameters

  • ‘from` - the path of the original file

  • ‘to` - the destination path



320
321
322
# File 'lib/shopify_cli/context.rb', line 320

def rename(from, to)
  File.rename(ctx_path(from), ctx_path(to))
end

#rm(fname) ⇒ Object

will remove a plain file from the FS, the filepath is relative to the command root unless absolute.

#### Parameters

  • ‘fname` - the file path relative to the context root to remove from the FS



330
331
332
# File 'lib/shopify_cli/context.rb', line 330

def rm(fname)
  FileUtils.rm(ctx_path(fname))
end

#rm_r(fname) ⇒ Object

will remove a directory from the FS, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘fname` - the file path to a directory, relative to the context root to remove from the FS



340
341
342
# File 'lib/shopify_cli/context.rb', line 340

def rm_r(fname)
  FileUtils.rm_r(ctx_path(fname))
end

#rm_rf(fname) ⇒ Object

will force remove a directory from the FS, the filepath is relative to the command root unless absolute

#### Parameters

  • ‘fname` - the file path to a directory, relative to the context root to remove from the FS



350
351
352
# File 'lib/shopify_cli/context.rb', line 350

def rm_rf(fname)
  FileUtils.rm_rf(ctx_path(fname))
end

#ruby_gem_version(gem) ⇒ Object

Uses bundle to grab the version of a gem

#### Parameters

  • gem: the name of the gem to check

#### Returns

  • version: a Semantic::Version object with the gem version



661
662
663
664
# File 'lib/shopify_cli/context.rb', line 661

def ruby_gem_version(gem)
  version = Bundler.load.specs.find { |s| s.name == gem }.version
  ::Semantic::Version.new(version.to_s)
end

#setenv(key, value) ⇒ Object

set a environment variable value by name.

#### Parameters

  • ‘key` - the name of the environment variable that you want to set

  • ‘value` - the value of the variable



203
204
205
# File 'lib/shopify_cli/context.rb', line 203

def setenv(key, value)
  @env[key] = value
end

#system(*args, **kwargs) ⇒ Object

Execute a command in the user’s environment Outputs result of the command without capturing it

#### Parameters

  • ‘*args`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)

  • ‘**kwargs`: additional keyword arguments to pass to Process.spawn

#### Returns

  • ‘status`: The `Process::Status` result of the command execution.

#### Usage

stat = @ctx.system('ls', 'a_folder')


493
494
495
496
497
498
499
# File 'lib/shopify_cli/context.rb', line 493

def system(*args, **kwargs)
  process_status = CLI::Kit::System.system(*args, env: @env, **kwargs)
  unless process_status.success?
    abort("System call failed: #{args.join(" ")}")
  end
  process_status
end

#system?Boolean

will return true if the cli is being run from an installation, and not a development instance. The gem installation will not have a ‘test’ directory. See ‘#development?` for checking for development environment.

Returns:

  • (Boolean)


157
158
159
# File 'lib/shopify_cli/context.rb', line 157

def system?
  !Dir.exist?(File.join(ShopifyCLI::ROOT, "test"))
end

#testing?Boolean

will return true while tests are running, either locally or on CI

Returns:

  • (Boolean)


168
169
170
# File 'lib/shopify_cli/context.rb', line 168

def testing?
  ci? || ENV["SHOPIFY_CLI_TEST"]
end

#tty?Boolean

will return true if being launched from a tty

Returns:

  • (Boolean)


149
150
151
# File 'lib/shopify_cli/context.rb', line 149

def tty?
  $stdin.tty?
end

#unameObject

will grab the host info of the computer running the cli. This indicates the computer architecture and operating system



475
476
477
# File 'lib/shopify_cli/context.rb', line 475

def uname
  @uname ||= RbConfig::CONFIG["host"]
end

#unknown_os?Boolean

will return true if the os is unknown

Returns:

  • (Boolean)


144
145
146
# File 'lib/shopify_cli/context.rb', line 144

def unknown_os?
  os == :unknown
end

#warn(*args) ⇒ Object

a wrapper around Kernel.warn to allow for easy formatting

#### Parameters

  • ‘text` - a string message to output



432
433
434
# File 'lib/shopify_cli/context.rb', line 432

def warn(*args)
  Kernel.warn(CLI::UI.fmt(*args))
end

#which(cmd) ⇒ Object

TODO:

This is currently a duplicate of CLI::Kit::System.which() - we should make that method public when we make Kit changes and make this a wrapper instead.

Checks if the given command exists in the system

#### Parameters

  • ‘cmd`: The command to test

#### Returns The path of the executable if it is found



601
602
603
604
605
606
607
608
609
610
611
# File 'lib/shopify_cli/context.rb', line 601

def which(cmd)
  exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
  ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
    exts.each do |ext|
      exe = File.join(File.expand_path(path), "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end

  nil
end

#windows?Boolean

will return true if the cli is running on Windows

Returns:

  • (Boolean)


139
140
141
# File 'lib/shopify_cli/context.rb', line 139

def windows?
  os == :windows
end

#write(fname, content) ⇒ Object

will write/overwrite a file with the provided contents, relative to the context root unless the file path is absolute.

#### Parameters

  • ‘fname` - filename of the file that you are writing, relative to root unless it is absolute.

  • ‘content` - the body contents of the file that you are writing

#### Example

@ctx.write('new.txt', 'hello world')


218
219
220
# File 'lib/shopify_cli/context.rb', line 218

def write(fname, content)
  File.write(ctx_path(fname), content)
end