lib/chef-cli/command/shell_init.rb



#
# Copyright:: Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

autoload :ERB, "erb"

require_relative "../commands_map"
require_relative "../builtin_commands"
require_relative "base"
require_relative "../dist"

module Mixlib
  autoload :ShellOut, "mixlib/shellout"
end

module ChefCLI

  class ShellCompletionTemplateContext

    def commands
      ChefCLI.commands_map.command_specs.inject({}) do |cmd_info, (_key, cmd_spec)|
        cmd_info[cmd_spec.name] = cmd_spec.description
        cmd_info
      end
    end

    def get_binding
      binding
    end
  end

  module Command
    class ShellInit < ChefCLI::Command::Base

      SUPPORTED_SHELLS = %w{ bash fish zsh sh powershell posh}.map(&:freeze).freeze

      banner(<<~HELP)
        Usage: #{ChefCLI::Dist::EXEC} shell-init

        `#{ChefCLI::Dist::EXEC} shell-init` modifies your shell environment to make ChefCLI or Workstation your
        default Ruby.

          To enable for just the current shell session:

            In sh, bash, and zsh:
              eval "$(#{ChefCLI::Dist::EXEC} shell-init SHELL_NAME)"
            In fish:
              eval (#{ChefCLI::Dist::EXEC} shell-init fish)
            In Powershell:
              #{ChefCLI::Dist::EXEC} shell-init powershell | Invoke-Expression

          To permanently enable:

            In sh, bash, and zsh:
              echo 'eval "$(#{ChefCLI::Dist::EXEC} shell-init SHELL_NAME)"' >> ~/.YOUR_SHELL_RC_FILE
            In fish:
              echo 'eval (#{ChefCLI::Dist::EXEC} shell-init SHELL_NAME)' >> ~/.config/fish/config.fish
            In Powershell
              "#{ChefCLI::Dist::EXEC} shell-init powershell | Invoke-Expression" >> $PROFILE

        OPTIONS:

      HELP

      option :omnibus_dir,
        long: "--omnibus-dir OMNIBUS_DIR",
        description: "Alternate path to omnibus install (used for testing)"

      def initialize
        super
        @shell_completion_template_context = nil
      end

      def omnibus_root
        config[:omnibus_dir] || super
      end

      def run(argv)
        # Currently we don't have any shell-specific features, so we ignore the
        # shell name. We'll need it if we add completion.
        remaining_args = parse_options(argv)
        shell_name = remaining_args.first
        if shell_name.nil?
          err("Please specify what shell you are using\n")
          err(opt_parser.to_s)
          return 1
        elsif !SUPPORTED_SHELLS.include?(shell_name)
          err("Shell `#{shell_name}' is not currently supported")
          err("Supported shells are: #{SUPPORTED_SHELLS.join(" ")}")
          return 1
        end

        env = omnibus_env.dup
        path = env.delete("PATH")
        export(shell_name, "PATH", path)
        env.each do |var_name, value|
          export(shell_name, var_name, value)
        end

        emit_shell_cmd(completion_for(shell_name))
        0
      end

      def emit_shell_cmd(cmd)
        msg(cmd) unless cmd.empty?
      end

      def completion_for(shell)
        return "" unless (completion_template_basename = completion_template_for(shell))

        completion_template_path = expand_completion_template_path(completion_template_basename)
        erb = ERB.new(File.read(completion_template_path), trim_mode: "-")
        context_binding = shell_completion_template_context.get_binding
        erb.result(context_binding)
      end

      def completion_template_for(shell)
        case shell
        when "bash"
          "bash.sh.erb"
        when "fish"
          "chef.fish.erb"
        when "zsh"
          "zsh.zsh.erb"
        else
          # Pull requests accepted!
          nil
        end
      end

      def expand_completion_template_path(basename)
        File.join(File.expand_path("../completions", __dir__), basename)
      end

      def shell_completion_template_context
        @shell_completion_template_context ||= ShellCompletionTemplateContext.new
      end

      def export(shell, var, val)
        case shell
        when "sh", "bash", "zsh"
          posix_shell_export(var, val)
        when "fish"
          fish_shell_export(var, val)
        when "powershell", "posh"
          powershell_export(var, val)
        end
      end

      def posix_shell_export(var, val)
        emit_shell_cmd(%Q{export #{var}="#{val}"})
      end

      def fish_shell_export(var, val)
        # Fish's syntax for setting PATH is special. Path elements are
        # divided by spaces (instead of colons). We also send STDERR to
        # /dev/null to avoid Fish's helpful warnings about nonexistent
        # PATH elements.
        if var == "PATH"
          emit_shell_cmd(%Q{set -gx #{var} "#{val.split(":").join('" "')}" 2>/dev/null;})
        else
          emit_shell_cmd(%Q{set -gx #{var} "#{val}";})
        end
      end

      def powershell_export(var, val)
        emit_shell_cmd(%Q{$env:#{var}="#{val}"})
      end

      def check_license_acceptance
        # It gives a very weird error if users try to eval the shell-init command and it requests the
        # license from them. Instead, let users shell-init without accepting the license.
      end
    end
  end
end