Module: Babushka::ShellHelpers

Includes:
LogHelpers
Included in:
Asset, Asset, DepDefiner, DepDefiner, GitRepo, GitRepo, PathChecker, PkgHelper, Renderable, Resource, RunHelpers, Source, SystemProfile, SystemProfile, Task
Defined in:
lib/babushka/helpers/shell_helpers.rb

Class Method Summary (collapse)

Methods included from LogHelpers

debug, deprecated!, log, log_block, log_error, log_ok, log_stderr, log_warn, removed!

Class Method Details

+ (Object) cmd_dir(cmd_name)

Return the directory from which the specified command would run if invoked via the PATH. If the command doesn't appear in the PATH, nil is returned.

For example, on a stock OS X machine:

cmd_dir('ruby')     #=> "/usr/bin"
cmd_dir('babushka') #=> nil

This is a direct implementation because the behaviour and output of `which` and `type` vary across different platforms and shells. It's also faster to not shell out.



189
190
191
192
193
# File 'lib/babushka/helpers/shell_helpers.rb', line 189

def cmd_dir cmd_name
  ENV['PATH'].split(':').detect {|path|
    File.executable? File.join(path, cmd_name.to_s)
  }
end

+ (Object) current_username



221
222
223
224
# File 'lib/babushka/helpers/shell_helpers.rb', line 221

def current_username
  require 'etc'
  Etc.getpwuid(Process.euid).name
end

+ (Object) log_shell(message, *cmd, &block)

Run a shell command, logging before and after using #log_block, and using a spinner while the command runs. The first argument, message, is the message to print before running the command, and the remaining arguments are identical to those of #shell.

As an example, suppose we called #log_shell as follows:

log_shell('Sleeping for a bit', 'sleep 10')

While the command runs, the log would show

Sleeping for a bit... (without a newline)

The command runs with a /-| spinner that animates each time a line of output is emitted by the command. Once the command terminates, the log would be completed to show

Sleeping for a bit... done.


210
211
212
213
214
215
# File 'lib/babushka/helpers/shell_helpers.rb', line 210

def log_shell message, *cmd, &block
  opts = cmd.extract_options!
  LogHelpers.log_block message do
    shell(*cmd.dup.push(opts.merge(:spinner => true)), &block)
  end
end

+ (Object) login_shell(cmd, opts = {}, &block)

Run cmd in a separate interactive shell. This is useful for running commands that depend on something shell-related that was changed during this run, like changing the user's shell. It's also useful for running commands that are only valid on an interactive shell, like rvm-related commands.



108
109
110
111
112
113
114
# File 'lib/babushka/helpers/shell_helpers.rb', line 108

def  cmd, opts = {}, &block
  if shell('echo $SHELL').p.basename == 'zsh'
    shell "zsh -i -c '#{cmd}'", opts, &block
  else
    shell "bash -l -c '#{cmd}'", opts, &block
  end
end

+ (Object) raw_shell(*cmd)

This method is a shortcut for accessing the results of a shell command without using a block. The method itself returns the shell object that is yielded to the block by #shell. As an example, this shell command:

shell('grep rails Gemfile') {|shell| shell.stdout }.empty?

can be simplified to this:

raw_shell('grep rails Gemfile').stdout.empty?


99
100
101
# File 'lib/babushka/helpers/shell_helpers.rb', line 99

def raw_shell *cmd
  shell(*cmd) {|s| s }
end

+ (Object) shell(*cmd, &block)

Run cmd.

If the command succeeds (i.e. returns 0), its output will be returned with a trailing newline stripped, if there was one. If the command fails (i.e. returns a non-zero value), nil will be returned.

If a block is given, it will be yielded once the command has run, with a Babushka::Shell object as its sole argument. Details of the shell command are contained in this object - see the methods cmd, ok?, result, stdout, and stderr.

Several options can be provided to alter #shell's behaviour.

<tt>:sudo => true</tt> runs the the command as root. If the command
  contains piping or redirection, a 'sudo su' variant will be used
  instead so that the pipe receiver or redirect targets are also
  included in the sudo.
<tt>:as => 'user'</tt> causes sudo to run as the specified user instead
  of root.
<tt>:sudo => 'user'</tt> is a shortcut that has the same effect as
  <tt>:sudo => true, :as => 'user'</tt>
<tt>:cd</tt> specifies the directory in which the command should run.
  If the path doesn't exist or isn't a directory, an error is raised
  unless the <tt>:create</tt> option is also set.
  To achieve the directory change, the command is rewritten to change
  directory first: `cd #{dir} && #{cmd}`.
<tt>:create</tt> causes the directory specified by the <tt>:cd</tt>
  option to be created if it doesn't already exist.
<tt>:input</tt> can be used to supply input for the shell command. It
  be any object that can be written to an IO with <tt>io << obj</tt>.
  When passed, it will be written to the command's stdin pipe before
  any output is read.
<tt>:spinner => true</tt> When this option is passed, a /-\| spinner
  is printed to stdout, and advanced whenever a line is read on the
  command's stdout or stderr pipes. This is useful for monitoring the
  progress of a long-running command, like a build or an installer.


45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/babushka/helpers/shell_helpers.rb', line 45

def shell *cmd, &block
  shell!(*cmd, &block)
rescue Shell::ShellCommandFailed => e
  if cmd.extract_options[:log]
    # Don't log the error if the command already logged
  elsif e.stdout.empty? && e.stderr.empty?
    LogHelpers.log "$ #{e.cmd.join(' ')}".colorize('grey') + ' ' + "#{Logging::CROSS_CHAR} shell command failed".colorize('red')
  else
    LogHelpers.log "$ #{e.cmd.join(' ')}", :closing_status => 'shell command failed' do
      LogHelpers.log_error(e.stderr.empty? ? e.stdout : e.stderr)
    end
  end
end

+ (Object) shell!(*cmd, &block)

Run cmd via #shell, raising an exception if it doesn't exit with success.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/babushka/helpers/shell_helpers.rb', line 73

def shell! *cmd, &block
  opts = cmd.extract_options!
  cmd = cmd.first if cmd.map(&:class) == [Array]

  if opts[:cd]
    if !opts[:cd].p.exists?
      if opts[:create]
        opts[:cd].p.mkdir
      else
        raise Errno::ENOENT, opts[:cd]
      end
    elsif !opts[:cd].p.dir?
      raise Errno::ENOTDIR, opts[:cd]
    end
  end
  shell_method = (opts[:as] || opts[:sudo]) ? :sudo : :shell_cmd
  send shell_method, *cmd.dup.push(opts), &block
end

+ (Boolean) shell?(*cmd)

Run cmd, returning true if its exit code was 0.

This is useful to run shell commands whose output isn't important, but whose exit code is. Unlike #shell, which logs the output of shell commands that exit with non-zero status, #shell? runs silently.

The idea is that #shell is for when you're interested in the command's output, and #shell? is for when you're interested in the exit status.



67
68
69
# File 'lib/babushka/helpers/shell_helpers.rb', line 67

def shell? *cmd
  shell(*cmd) {|s| s.stdout.chomp if s.ok? }
end

+ (Object) shell_cmd(*cmd, &block)



217
218
219
# File 'lib/babushka/helpers/shell_helpers.rb', line 217

def shell_cmd *cmd, &block
  Shell.new(*cmd).run(&block)
end

+ (Object) sudo(*cmd, &block)

Run cmd via `sudo`, bypassing it if possible (i.e. if we're running as root already, or as the user that was requested).

The return behaviour and block handling of #sudo are identical to that of #shell. In fact, #sudo constructs a sudo command, and then uses #shell internally to run the command.

All the options that can be passed to #shell are valid for #sudo as well. The :sudo and :as options can be ommitted, though, which will cause the command to be run as root. Hence, this sudo call:

sudo('ls')

is equivalent to these two shell calls:

shell('ls', :sudo => true)
shell('ls', :as => 'root')

In the same manner, this sudo call:

sudo('ls', :as => 'ben')

is equivalent to these two shell calls:

shell('ls', :sudo => 'ben')
shell('ls', :as => 'ben')


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/babushka/helpers/shell_helpers.rb', line 136

def sudo *cmd, &block
  opts = cmd.extract_options!
  env = cmd.first.is_a?(Hash) ? cmd.shift : {}

  raw_as = opts[:as] || opts[:sudo] || 'root'
  as = raw_as == true ? 'root' : raw_as

  cmd = if cmd.map(&:class) == [Array]
    Shellwords.join(cmd.first)
  elsif cmd.length > 1
    Shellwords.join(cmd)
  else
    cmd.first
  end

  sudo_cmd = if current_username == as
    cmd # Don't sudo if we're already running as the specified user.
  elsif opts[:su] || cmd[' |'] || cmd[' >']
    "sudo su - #{as} -c \"#{cmd.gsub('"', '\"')}\""
  else
    "sudo -u #{as} #{cmd}"
  end

  shell [env, sudo_cmd], opts.discard(:as, :sudo, :su), &block
end

+ (Object) which(cmd_name)

This method returns the full path to the specified command in the PATH, if that command appears anywhere in the PATH. If it doesn't, nil is returned.

For example, on a stock OS X machine:

which('ruby')     #=> "/usr/bin/ruby"
which('babushka') #=> nil

This is roughly equivalent to using `which` or `type` on the shell. However, because those commands' behaviour and output vary across platforms and shells, we instead use the logic in #cmd_dir.



173
174
175
176
# File 'lib/babushka/helpers/shell_helpers.rb', line 173

def which cmd_name
  matching_dir = cmd_dir(cmd_name)
  File.join(matching_dir, cmd_name.to_s) unless matching_dir.nil?
end