├── .rspec ├── binbundle.sublime-project ├── README.rdoc ├── lib ├── binbundle │ ├── version.rb │ ├── prompt.rb │ ├── jewel.rb │ ├── jewelry_box.rb │ └── gem_bins.rb └── binbundle.rb ├── .gitignore ├── .irbrc ├── spec ├── binbundle_spec.rb ├── gemlist.txt ├── binbundle │ ├── jewel_spec.rb │ ├── prompt_spec.rb │ ├── jewelry_box_spec.rb │ └── gem_bins_spec.rb └── spec_helper.rb ├── Gemfile ├── .rubocop.yml ├── CHANGELOG.md ├── LICENSE.txt ├── binbundle.gemspec ├── bin └── binbundle ├── Rakefile ├── Gemfile.lock ├── src └── _README.md ├── README.md └── CODE_OF_CONDUCT.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /binbundle.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Binbundle 2 | 3 | A command line tool for bundling installed gem binaries and restoring them. 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/binbundle/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Main bundle 4 | module Binbundle 5 | # Version 6 | VERSION = '1.0.11' 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | Binfile 13 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | `gem rdoc binbundle --ri` 3 | 4 | # rubocop:disable Style/MixinUsage 5 | include Binbundle # standard:disable all 6 | # rubocop:enable Style/MixinUsage 7 | -------------------------------------------------------------------------------- /spec/binbundle_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Binbundle do 4 | it 'has a version number' do 5 | expect(Binbundle::VERSION).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/gemlist.txt: -------------------------------------------------------------------------------- 1 | gem install mdless 2 | 3 | # Executables: searchlink, funkle 4 | sudo gem install searchlink --version '2.3.66' 5 | 6 | # Executables: bargle 7 | gem install --user-install bargle_funk 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in binbundle.gemspec 6 | gemspec 7 | 8 | gem 'rake', '~> 13.0' 9 | 10 | gem 'rspec', '~> 3.0' 11 | 12 | gem 'rubocop', '~> 1.21' 13 | gem 'rubocop-rake', require: false 14 | gem 'rubocop-rspec', require: false 15 | -------------------------------------------------------------------------------- /lib/binbundle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'optparse' 4 | require 'fileutils' 5 | require 'tty-spinner' 6 | require 'english' 7 | require_relative 'binbundle/version' 8 | require_relative 'binbundle/prompt' 9 | require_relative 'binbundle/jewel' 10 | require_relative 'binbundle/jewelry_box' 11 | require_relative 'binbundle/gem_bins' 12 | 13 | # Parent module 14 | module Binbundle 15 | # StandardError class 16 | class Error < StandardError; end 17 | # Your code goes here... 18 | end 19 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | - rubocop-rake 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.6 7 | # SuggestExtensions: false 8 | 9 | Style/StringLiterals: 10 | Enabled: true 11 | EnforcedStyle: single_quotes 12 | 13 | Style/StringLiteralsInInterpolation: 14 | Enabled: true 15 | EnforcedStyle: single_quotes 16 | 17 | Layout/LineLength: 18 | Max: 120 19 | 20 | Metrics/MethodLength: 21 | Max: 40 22 | 23 | Metrics/BlockLength: 24 | Max: 40 25 | 26 | Metrics/ClassLength: 27 | Max: 200 28 | 29 | Metrics/CyclomaticComplexity: 30 | Max: 10 31 | 32 | Metrics/PerceivedComplexity: 33 | Max: 30 34 | 35 | Metrics/AbcSize: 36 | Max: 40 37 | 38 | Metrics/CyclomaticComplexity: 39 | Max: 15 40 | -------------------------------------------------------------------------------- /spec/binbundle/jewel_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Binbundle::Jewel do 4 | subject(:j) do 5 | described_class.new('searchlink', ['searchlink'], '2.3.66') 6 | end 7 | 8 | describe '.new' do 9 | it 'to be a Jewel' do 10 | expect(j).to be_a(described_class) 11 | end 12 | end 13 | 14 | describe '.to_s' do 15 | it 'output with sudo' do 16 | j.sudo = true 17 | j.user_install = false 18 | expect(j.to_s.scan(/^sudo gem install/).count).to eq 1 19 | end 20 | 21 | it 'output with user_install' do 22 | j.sudo = false 23 | j.user_install = true 24 | expect(j.to_s.scan(/^gem install --user-install/).count).to eq 1 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/binbundle/prompt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Binbundle::Prompt do 4 | ENV['TESTING'] = 'true' 5 | 6 | subject(:prompt) do 7 | described_class 8 | end 9 | 10 | describe '.yn' do 11 | it 'to respond interpret "yes"' do 12 | expect(prompt.yn('Do a thing', default_response: 'yes')).to be true 13 | end 14 | 15 | it 'to interpret a TrueClass' do 16 | expect(prompt.yn('Do a thing', default_response: true)).to be true 17 | end 18 | 19 | it 'to interpret "no"' do 20 | expect(prompt.yn('Do a thing', default_response: 'no')).to be false 21 | end 22 | 23 | it 'to interpret a FalseClass' do 24 | expect(prompt.yn('Do a thing', default_response: false)).to be false 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | unless ENV['CI'] == 'true' 4 | # SimpleCov::Formatter::Codecov # For CI 5 | require 'simplecov' 6 | SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter 7 | SimpleCov.start 8 | end 9 | 10 | require 'binbundle' 11 | 12 | RSpec.configure do |config| 13 | # Enable flags like --only-failures and --next-failure 14 | config.example_status_persistence_file_path = '.rspec_status' 15 | 16 | # Disable RSpec exposing methods globally on `Module` and `main` 17 | config.disable_monkey_patching! 18 | 19 | config.expect_with :rspec do |c| 20 | c.syntax = :expect 21 | end 22 | end 23 | 24 | def defaults 25 | { 26 | bin_for: nil, 27 | dry_run: true, 28 | file: 'spec/gemlist.txt', 29 | gem_for: nil, 30 | include_version: true, 31 | local: false, 32 | sudo: false, 33 | user_install: false 34 | } 35 | end 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.10 2 | 3 | 2024-08-04 13:00 4 | 5 | #### IMPROVED 6 | 7 | - When installing, test if the gem/version is already installed and skip 8 | - When installing, verify that the specified gem exists on the host before running the install command. Adds a pause before execution, but saves a lot of time looking up missing gems. 9 | 10 | ### 1.0.9 11 | 12 | 2024-08-02 10:42 13 | 14 | #### NEW 15 | 16 | - `binbundle gem for BIN` will show what gem is responsible for a binary 17 | - `binbundle bins for GEM` will show what binaries a gem installs 18 | 19 | ### 1.0.8 20 | 21 | 2024-08-02 09:23 22 | 23 | #### IMPROVED 24 | 25 | - Updates for automation 26 | - Fix tests and rubocop warnings 27 | 28 | ### 1.0.7 29 | 30 | 2024-08-02 09:15 31 | 32 | #### IMPROVED 33 | 34 | - Updates for automation 35 | - Fix tests and rubocop warnings 36 | 37 | ## 1.0.6 38 | 39 | - Add spinners 40 | 41 | ## [0.1.0] - 2024-08-01 42 | 43 | - Initial release 44 | -------------------------------------------------------------------------------- /spec/binbundle/jewelry_box_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Binbundle::JewelryBox do 4 | subject(:jb) do 5 | described_class.new 6 | end 7 | 8 | describe '.new' do 9 | it 'to be a JewelryBox' do 10 | expect(jb).to be_a(described_class) 11 | end 12 | end 13 | 14 | describe '.gem_list' do 15 | it 'converts gem list to an array of hashes' do 16 | expect(jb).to be_a(Array) 17 | end 18 | end 19 | 20 | describe '.sudo' do 21 | it 'output all lines with sudo' do 22 | jb.sudo = true 23 | jb.user_install = false 24 | jb.init_from_contents(IO.read('spec/gemlist.txt')) 25 | expect(jb.to_s.scan(/^sudo gem install/).count).to eq 3 26 | end 27 | end 28 | 29 | describe '.user_install' do 30 | it 'output all lines with --user-install' do 31 | jb.sudo = false 32 | jb.user_install = true 33 | jb.init_from_contents(IO.read('spec/gemlist.txt')) 34 | expect(jb.to_s.scan(/^gem install --user-install/).count).to eq 3 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Brett Terpstra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/binbundle/prompt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Binbundle 4 | # Command line prompts 5 | module Prompt 6 | ## 7 | ## Ask a yes or no question in the terminal 8 | ## 9 | ## @param question [String] The question 10 | ## to ask 11 | ## @param default_response [Boolean] default 12 | ## response if no input 13 | ## 14 | ## @return [Boolean] yes or no 15 | ## 16 | def self.yn(question, default_response: nil) 17 | $stdin.reopen('/dev/tty') 18 | 19 | default = if default_response.is_a?(String) 20 | default_response =~ /y/i ? true : false 21 | else 22 | default_response 23 | end 24 | 25 | # if this isn't an interactive shell, answer default 26 | return default if !$stdout.isatty || ENV['TESTING'] == 'true' 27 | 28 | # clear the buffer 29 | if ARGV&.length 30 | ARGV.length.times do 31 | ARGV.shift 32 | end 33 | end 34 | system 'stty cbreak' 35 | 36 | options = if default.nil? 37 | '[y/n]' 38 | else 39 | "[#{default ? 'Y/n' : 'y/N'}]" 40 | end 41 | $stdout.syswrite "#{question.sub(/\?$/, '')} #{options}? " 42 | res = $stdin.sysread 1 43 | puts 44 | system 'stty cooked' 45 | 46 | res.chomp! 47 | res.downcase! 48 | 49 | return default if res.empty? 50 | 51 | res =~ /y/i ? true : false 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /binbundle.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path(File.join('..', 'lib'), __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'binbundle/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'binbundle' 9 | s.version = Binbundle::VERSION 10 | s.authors = ['Brett Terpstra'] 11 | s.email = ['me@brettterpstra.com'] 12 | 13 | s.summary = 'Bundle all your gem binaries.' 14 | s.description = 'Easily take a snapshot of all binaries installed with Gem and restore on a fresh system.' 15 | s.homepage = 'https://github.com/ttscoff/binbundle' 16 | s.license = 'MIT' 17 | s.required_ruby_version = '>= 2.6.0' 18 | 19 | s.metadata['homepage_uri'] = s.homepage 20 | s.metadata['source_code_uri'] = 'https://github.com/ttscoff/binbundle' 21 | s.metadata['changelog_uri'] = 'https://github.com/ttscoff/binbundle/CHANGELOG.md' 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | s.files = Dir.chdir(File.expand_path(__dir__)) do 26 | `git ls-files -z`.split("\x0").reject do |f| 27 | (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 28 | end 29 | end 30 | s.bindir = 'bin' 31 | s.executables = s.files.grep(%r{\Abin/}) { |f| File.basename(f) } 32 | s.require_paths = ['lib'] 33 | 34 | s.add_development_dependency 'simplecov', '~> 0.21' 35 | s.add_development_dependency 'simplecov-console', '~> 0.9' 36 | s.add_development_dependency 'yard', '~> 0.9', '>= 0.9.26' 37 | 38 | s.add_runtime_dependency('tty-spinner', '~> 0.9') 39 | end 40 | -------------------------------------------------------------------------------- /lib/binbundle/jewel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Binbundle 4 | # String helpers 5 | class Jewel 6 | # Include version info in output 7 | attr_writer :include_version 8 | 9 | # Include sudo 10 | attr_writer :sudo 11 | 12 | # Include --user-install 13 | attr_writer :user_install 14 | 15 | # Binaries 16 | attr_reader :bins 17 | 18 | # Gem name 19 | attr_reader :gem 20 | 21 | # Version 22 | attr_reader :version 23 | 24 | ## 25 | ## Create a new Jewel object 26 | ## 27 | ## @param gem_name [String] The gem name 28 | ## @param bins [Array|String] The executables 29 | ## @param version [String] The semantic version 30 | ## 31 | ## @return [Jewel] new jewel object 32 | ## 33 | def initialize(gem_name = '', bins = [], version = nil) 34 | @gem = gem_name 35 | @bins = if bins.is_a?(String) 36 | bins.split(/ *, */) 37 | else 38 | bins 39 | end 40 | @version = version 41 | @include_version = true 42 | end 43 | 44 | ## 45 | ## Output Jewel as command 46 | ## 47 | ## @return [String] Command representation of the object. 48 | ## 49 | def to_s 50 | version = @include_version && @version ? " -v '#{@version}'" : '' 51 | if @sudo 52 | "sudo gem install #{@gem}#{version}" 53 | elsif @user_install 54 | "gem install --user-install #{@gem}#{version}" 55 | else 56 | "gem install #{@gem}#{version}" 57 | end 58 | end 59 | 60 | ## 61 | ## Output a Binfile-ready version of the Jewel 62 | ## 63 | ## @return [String] Binfile string 64 | ## 65 | def gem_command 66 | ver = @include_version ? " -v '#{@version}'" : '' 67 | ui = @user_install ? '--user-install ' : '' 68 | sudo = @sudo ? 'sudo ' : '' 69 | "# Executables: #{@bins.join(', ')}\n#{to_s}" 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/binbundle/gem_bins_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Binbundle::GemBins do 4 | subject(:jb) do 5 | described_class.new(defaults) 6 | end 7 | 8 | describe '.new' do 9 | it 'to be a GemBins' do 10 | expect(jb).to be_a(described_class) 11 | end 12 | end 13 | 14 | describe '.bins_to_s' do 15 | it 'generates correct output' do 16 | expect(jb.bins_to_s).to match(/\# Executables: /) 17 | end 18 | end 19 | 20 | describe '.generate' do 21 | it 'generates correct output' do 22 | jb.dry_run = true 23 | expect { jb.generate }.to output(/\# Executables:/).to_stdout 24 | end 25 | 26 | it 'writes to a file' do 27 | jb.dry_run = false 28 | jb.file = 'Testfile' 29 | ENV['TESTING'] = 'true' 30 | jb.generate 31 | expect(File.exist?('Testfile')).to be true 32 | FileUtils.rm('Testfile') 33 | end 34 | end 35 | 36 | describe '.install' do 37 | it 'installs tests' do 38 | ENV['TESTING'] = 'true' 39 | jb.dry_run = false 40 | expect { jb.install }.to raise_error(SystemExit) 41 | end 42 | 43 | it 'fails on missing file' do 44 | jb.file = 'non_existent.txt' 45 | ENV['TESTING'] = 'true' 46 | jb.dry_run = false 47 | expect { jb.install }.to raise_error(SystemExit) 48 | end 49 | end 50 | 51 | describe '.info' do 52 | it 'gets the binary for gem' do 53 | options = { bin_for: 'searchlink' } 54 | expect(jb.info(options)).to match(/funkle/) 55 | end 56 | 57 | it 'gets the gem for binary' do 58 | options = { gem_for: 'funkle' } 59 | expect(jb.info(options)).to eq 'searchlink' 60 | end 61 | 62 | it 'fails on local search' do 63 | options = { gem_for: 'funkle', local: true } 64 | 65 | expect(jb.info(options)).to match(/Gem for funkle not found/) 66 | end 67 | 68 | it 'fails on missing file' do 69 | jb.file = 'non_existent.txt' 70 | options = { gem_for: 'funkle' } 71 | expect { jb.info(options) }.to raise_error(SystemExit) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /bin/binbundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../lib/binbundle' 5 | 6 | options = { 7 | bin_for: nil, 8 | dry_run: false, 9 | file: 'Binfile', 10 | gem_for: nil, 11 | include_version: true, 12 | local: false, 13 | sudo: false, 14 | user_install: false 15 | } 16 | 17 | optparse = OptionParser.new do |opts| 18 | opts.banner = "Usage: #{File.basename(__FILE__)} [options] [bundle|install]" 19 | 20 | opts.on('--dry-run', 'Output to STDOUT instead of file') do 21 | options[:dry_run] = true 22 | end 23 | 24 | opts.on('-f', '--file FILE', 'Output to alternative filename (default Binfile)') do |opt| 25 | options[:file] = opt 26 | end 27 | 28 | opts.on('--[no-]versions', 'Include version info in output or restore (default true)') do |opt| 29 | options[:include_version] = opt 30 | end 31 | 32 | opts.on('-l', '--local', 'Use installed gems instead of Binfile for gem_for and bins_for') do 33 | options[:local] = true 34 | end 35 | 36 | opts.on('-s', '--sudo', 'Install gems with sudo') do 37 | options[:sudo] = true 38 | end 39 | 40 | opts.on('-u', '--user-install', 'Use --user-install to install gems') do 41 | options[:user_install] = true 42 | end 43 | 44 | opts.on('-v', '--version', 'Display version') do 45 | puts "#{File.basename(__FILE__)} v#{Binbundle::VERSION}" 46 | Process.exit 0 47 | end 48 | 49 | opts.on('-h', '--help', 'Display this screen') do 50 | puts opts 51 | exit 52 | end 53 | end 54 | 55 | optparse.parse! 56 | 57 | if options[:sudo] && options[:user_install] 58 | puts 'Error: --sudo and --user-install cannot be used together' 59 | Process.exit 1 60 | end 61 | 62 | gb = Binbundle::GemBins.new(options) 63 | 64 | if ARGV.count.positive? 65 | subcommand = ARGV.shift 66 | params = ARGV.join(' ').sub(/ *for */, '') 67 | 68 | case subcommand 69 | when /^(inst|rest)/ 70 | options[:install] = true 71 | when /^gem/ 72 | options[:gem_for] = params 73 | when /^bin/ 74 | options[:bin_for] = params 75 | end 76 | end 77 | 78 | if options[:gem_for] || options[:bin_for] 79 | puts gb.info(options) 80 | elsif options[:install] 81 | gb.install 82 | else 83 | gb.generate 84 | end 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake/clean' 4 | require 'rubygems' 5 | require 'rubygems/package_task' 6 | require 'rdoc/task' 7 | require 'bundler/gem_tasks' 8 | require 'rspec/core/rake_task' 9 | require 'rubocop/rake_task' 10 | require 'yard' 11 | 12 | desc 'Run test suite' 13 | task test: %i[rubocop spec] 14 | 15 | desc 'Package gem' 16 | task package: %i[clobber build] 17 | 18 | RuboCop::RakeTask.new do |t| 19 | t.formatters = ['progress'] 20 | end 21 | 22 | RSpec::Core::RakeTask.new(:spec) 23 | 24 | Rake::RDocTask.new do |rd| 25 | rd.main = 'README.rdoc' 26 | rd.rdoc_files.include('README.rdoc', 'lib/**/*.rb', 'bin/**/*') 27 | rd.title = 'binbundle' 28 | end 29 | 30 | YARD::Rake::YardocTask.new do |t| 31 | t.files = ['lib/binbundle/*.rb'] 32 | t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--no-private', '-p', 'yard_templates'] 33 | # t.stats_options = ['--list-undoc'] 34 | end 35 | 36 | desc 'Development version check' 37 | task :ver do 38 | gver = `git ver` 39 | cver = IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1] 40 | res = `grep VERSION lib/binbundle/version.rb` 41 | version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1] 42 | puts "git tag: #{gver}" 43 | puts "version.rb: #{version}" 44 | puts "changelog: #{cver}" 45 | end 46 | 47 | desc 'Changelog version check' 48 | task :cver do 49 | puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1] 50 | end 51 | 52 | desc 'Bump incremental version number' 53 | task :bump, :type do |_, args| 54 | args.with_defaults(type: 'inc') 55 | version_file = 'lib/binbundle/version.rb' 56 | content = IO.read(version_file) 57 | content.sub!(/VERSION = '(?\d+)\.(?\d+)\.(?\d+)(?
\S+)?'/) do
58 |     m = Regexp.last_match
59 |     major = m['major'].to_i
60 |     minor = m['minor'].to_i
61 |     inc = m['inc'].to_i
62 |     pre = m['pre']
63 | 
64 |     case args[:type]
65 |     when /^maj/
66 |       major += 1
67 |       minor = 0
68 |       inc = 0
69 |     when /^min/
70 |       minor += 1
71 |       inc = 0
72 |     else
73 |       inc += 1
74 |     end
75 | 
76 |     $stdout.puts "At version #{major}.#{minor}.#{inc}#{pre}"
77 |     "VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
78 |   end
79 |   File.open(version_file, 'w+') { |f| f.puts content }
80 | end
81 | 
82 | task default: %i[spec rubocop]
83 | 


--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
 1 | PATH
 2 |   remote: .
 3 |   specs:
 4 |     binbundle (1.0.10)
 5 |       tty-spinner (~> 0.9)
 6 | 
 7 | GEM
 8 |   remote: https://rubygems.org/
 9 |   specs:
10 |     ansi (1.5.0)
11 |     ast (2.4.2)
12 |     diff-lcs (1.5.1)
13 |     docile (1.4.1)
14 |     json (2.7.2)
15 |     language_server-protocol (3.17.0.3)
16 |     parallel (1.25.1)
17 |     parser (3.3.4.0)
18 |       ast (~> 2.4.1)
19 |       racc
20 |     racc (1.8.1)
21 |     rainbow (3.1.1)
22 |     rake (13.2.1)
23 |     regexp_parser (2.9.2)
24 |     rexml (3.3.4)
25 |       strscan
26 |     rspec (3.13.0)
27 |       rspec-core (~> 3.13.0)
28 |       rspec-expectations (~> 3.13.0)
29 |       rspec-mocks (~> 3.13.0)
30 |     rspec-core (3.13.0)
31 |       rspec-support (~> 3.13.0)
32 |     rspec-expectations (3.13.1)
33 |       diff-lcs (>= 1.2.0, < 2.0)
34 |       rspec-support (~> 3.13.0)
35 |     rspec-mocks (3.13.1)
36 |       diff-lcs (>= 1.2.0, < 2.0)
37 |       rspec-support (~> 3.13.0)
38 |     rspec-support (3.13.1)
39 |     rubocop (1.65.1)
40 |       json (~> 2.3)
41 |       language_server-protocol (>= 3.17.0)
42 |       parallel (~> 1.10)
43 |       parser (>= 3.3.0.2)
44 |       rainbow (>= 2.2.2, < 4.0)
45 |       regexp_parser (>= 2.4, < 3.0)
46 |       rexml (>= 3.2.5, < 4.0)
47 |       rubocop-ast (>= 1.31.1, < 2.0)
48 |       ruby-progressbar (~> 1.7)
49 |       unicode-display_width (>= 2.4.0, < 3.0)
50 |     rubocop-ast (1.31.3)
51 |       parser (>= 3.3.1.0)
52 |     rubocop-rake (0.6.0)
53 |       rubocop (~> 1.0)
54 |     rubocop-rspec (3.0.3)
55 |       rubocop (~> 1.61)
56 |     ruby-progressbar (1.13.0)
57 |     simplecov (0.22.0)
58 |       docile (~> 1.1)
59 |       simplecov-html (~> 0.11)
60 |       simplecov_json_formatter (~> 0.1)
61 |     simplecov-console (0.9.1)
62 |       ansi
63 |       simplecov
64 |       terminal-table
65 |     simplecov-html (0.12.3)
66 |     simplecov_json_formatter (0.1.4)
67 |     strscan (3.1.0)
68 |     terminal-table (3.0.2)
69 |       unicode-display_width (>= 1.1.1, < 3)
70 |     tty-cursor (0.7.1)
71 |     tty-spinner (0.9.3)
72 |       tty-cursor (~> 0.7)
73 |     unicode-display_width (2.5.0)
74 |     yard (0.9.36)
75 | 
76 | PLATFORMS
77 |   arm64-darwin-20
78 |   x86_64-darwin-20
79 | 
80 | DEPENDENCIES
81 |   binbundle!
82 |   rake (~> 13.0)
83 |   rspec (~> 3.0)
84 |   rubocop (~> 1.21)
85 |   rubocop-rake
86 |   rubocop-rspec
87 |   simplecov (~> 0.21)
88 |   simplecov-console (~> 0.9)
89 |   yard (~> 0.9, >= 0.9.26)
90 | 
91 | BUNDLED WITH
92 |    2.2.29
93 | 


--------------------------------------------------------------------------------
/lib/binbundle/jewelry_box.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Binbundle
 4 |   # String helpers
 5 |   class JewelryBox < Array
 6 |     # Include version info in output
 7 |     attr_writer :include_version
 8 | 
 9 |     # Include sudo
10 |     attr_writer :sudo
11 | 
12 |     # Include --user-install
13 |     attr_writer :user_install
14 | 
15 |     ##
16 |     ## Create a new JewelryBox object
17 |     ##
18 |     ## @param      include_version  [Boolean] include version
19 |     ## @param      sudo             [Boolean] include sudo
20 |     ## @param      user_install     [Boolean] include --user-install
21 |     ## @param      contents         [String] The contents to parse
22 |     ##
23 |     ## @return [JewelryBox] New JewelryBox object
24 |     def initialize(include_version: true, sudo: false, user_install: false, contents: nil)
25 |       @include_version = include_version
26 |       @sudo = sudo
27 |       @user_install = user_install
28 | 
29 |       super()
30 | 
31 |       return unless contents
32 | 
33 |       init_from_contents(contents)
34 |     end
35 | 
36 |     ##
37 |     ## Read and parse a Binfile for gems to install
38 |     ##
39 |     ## @param      contents  [String] The contents
40 |     ##
41 |     def init_from_contents(contents)
42 |       rx = /(?mix)(?:\#\sExecutables:\s(?[\S\s,]+?)\n)?(?:sudo\s)?gem\sinstall
43 |             \s(?:--user-install\s)?
44 |             (?\S+)(?:\s(?:-v|--version)\s'(?[0-9.]+)')?/
45 |       contents.to_enum(:scan, rx).map { Regexp.last_match }.each do |m|
46 |         g = Jewel.new(m['gem'], m['bins'], m['version'])
47 |         g.include_version = @include_version
48 |         g.sudo = @sudo
49 |         g.user_install = @user_install
50 |         push(g)
51 |       end
52 |     end
53 | 
54 |     ##
55 |     ## Find a gem for a given binary name
56 |     ##
57 |     ## @param      bin   [String] The bin to search for
58 |     ##
59 |     ## @return     [String] Associated gem name
60 |     ##
61 |     def gem_for_bin(bin)
62 |       m = select { |gem| gem.bins&.include?(bin) }.first
63 |       return "Gem for #{bin} not found" unless m
64 | 
65 |       m.gem
66 |     end
67 | 
68 |     ##
69 |     ## List binaries for a given gem name
70 |     ##
71 |     ## @param      gem   [String] The gem name to search for
72 |     ##
73 |     def bins_for_gem(gem)
74 |       m = select { |g| g.gem == gem }.first
75 |       return "Gem #{gem} not found" unless m
76 | 
77 |       m.bins ? m.bins.join(', ') : 'Missing info'
78 |     end
79 | 
80 |     ##
81 |     ## Output a Binfile version of the JewelryBox
82 |     ##
83 |     ## @return     [String] String representation of the object.
84 |     ##
85 |     def to_s
86 |       map(&:to_s).join("\n")
87 |     end
88 |   end
89 | end
90 | 


--------------------------------------------------------------------------------
/src/_README.md:
--------------------------------------------------------------------------------
 1 | # Binbundle
 2 | 
 3 | [![Gem](https://img.shields.io/gem/v/binbundle.svg)](https://rubygems.org/gems/na)
 4 | [![GitHub license](https://img.shields.io/github/license/ttscoff/binbundle.svg)](./LICENSE.txt)
 5 | 
 6 | Creates a "bundle" file of all installed gems with executables. The resulting file is an executable script that can be run standalone, or in combination with this script to add options like `sudo` or `--user-install` to the `gem install` commands. These options can be specified when creating the file as well. A file created with `sudo` or `--user-install` commands can still be overridden when running via this script and `--install`.
 7 | 
 8 | Created file is called `Binfile` in the current directory unless another path is specified with `--file`.
 9 | 
10 | ### Installation
11 | 
12 | Install with:
13 | 
14 |     $ gem install binbundle
15 | 
16 | If this causes errors, use:
17 | 
18 |     $ gem install --user-install binbundle
19 | 
20 | ### Usage
21 | 
22 | Run `binbundle bundle` to create a Binfile in the current directory, or with `--file FILENAME` to specify a path/filename. That file can optionally be made executable (you'll be prompted). In the future when doing a clean install or using a new system, you can just run that file as a standalone to reinstall all of your gem binaries.
23 | 
24 | Example:
25 | 
26 |     binbundle bundle --file ~/dotfiles/Binfile
27 | 
28 | Using this script with the `install` subcommand will read the Binfile and execute it line by line, adding options like version numbers, sudo, or the `--user-install` flag, overriding any of these specified when bundling.
29 | 
30 | Example:
31 | 
32 |     binbundle install --no-versions --user-install --file ~/dotfiles/Binfile
33 | 
34 | You can also run with subcommands `bundle` or `install`, e.g. `bundle_gem_bins install`.
35 | 
36 | #### Options
37 | 
38 | ```
39 | Usage: binbundle [options] [bundle|install]
40 |         --[no-]versions              Include version info in output (default true)
41 |         --dry-run                    Output to STDOUT instead of file
42 |     -s, --sudo                       Install gems with sudo
43 |     -u, --user-install               Use --user-install to install gems
44 |     -f, --file FILE                  Output to alternative filename (default Binfile)
45 |     -l, --local                      Use installed gems instead of Binfile for gem_for and bins_for
46 |     -v, --version                    Display version
47 |     -h, --help                       Display this screen
48 | ```
49 | 
50 | #### Info commands
51 | 
52 | You can retrieve some basic info about gems and their binaries using `binbundle gem for BIN` or `binbundle bins for GEM`. This will read `Binfile` or any file specified by `--file` and return the requested info, either the gem associated with the given binary (BIN), or the binaries associated with the given gem name (GEM).
53 | 
54 | Use either info command with `--local` to parse installed gems rather than a Binfile.
55 | 
56 | ### Recommendations
57 | 
58 | I recommend using Binbundle along with a tool like [Dotbot](https://github.com/anishathalye/dotbot). Commit your bundle to a repo that you can easily clone to a new machine and then make `gem install binbundle` and `binbundle install ~/dotfiles/Binfile` part of your restore process.
59 | 
60 | 
61 | ### Support
62 | 
63 | PayPal link: [paypal.me/ttscoff](https://paypal.me/ttscoff)
64 | 
65 | ### Changelog
66 | 
67 | See [CHANGELOG.md](https://github.com/ttscoff/binbundle/blob/main/CHANGELOG.md)
68 | 
69 | ### Development
70 | 
71 | After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
72 | 
73 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
74 | 
75 | ### Contributing
76 | 
77 | Bug reports and pull requests are welcome on GitHub at . This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ttscoff/binbundle/blob/main/CODE_OF_CONDUCT.md).
78 | 
79 | ### License
80 | 
81 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
82 | 
83 | ### Code of Conduct
84 | 
85 | Everyone interacting in the Binbundle project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ttscoff/binbundle/blob/main/CODE_OF_CONDUCT.md).
86 | 
87 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Binbundle
 2 | 
 3 | [![Gem](https://img.shields.io/gem/v/binbundle.svg)](https://rubygems.org/gems/na)
 4 | [![GitHub license](https://img.shields.io/github/license/ttscoff/binbundle.svg)](./LICENSE.txt)
 5 | 
 6 | Creates a "bundle" file of all installed gems with executables. The resulting file is an executable script that can be run standalone, or in combination with this script to add options like `sudo` or `--user-install` to the `gem install` commands. These options can be specified when creating the file as well. A file created with `sudo` or `--user-install` commands can still be overridden when running via this script and `--install`.
 7 | 
 8 | Created file is called `Binfile` in the current directory unless another path is specified with `--file`.
 9 | 
10 | ### Installation
11 | 
12 | Install with:
13 | 
14 |     $ gem install binbundle
15 | 
16 | If this causes errors, use:
17 | 
18 |     $ gem install --user-install binbundle
19 | 
20 | ### Usage
21 | 
22 | Run `binbundle bundle` to create a Binfile in the current directory, or with `--file FILENAME` to specify a path/filename. That file can optionally be made executable (you'll be prompted). In the future when doing a clean install or using a new system, you can just run that file as a standalone to reinstall all of your gem binaries.
23 | 
24 | Example:
25 | 
26 |     binbundle bundle --file ~/dotfiles/Binfile
27 | 
28 | Using this script with the `install` subcommand will read the Binfile and execute it line by line, adding options like version numbers, sudo, or the `--user-install` flag, overriding any of these specified when bundling.
29 | 
30 | Example:
31 | 
32 |     binbundle install --no-versions --user-install --file ~/dotfiles/Binfile
33 | 
34 | The available subcommands are:
35 | 
36 | - `bundle`: create a new Binfile (or specified --file)
37 | - `install`: install gems from Binfile
38 | - `gem for EXE`: find out which gem is responsible for an executable EXE (e.g. `binbundle gem for searchlink`)
39 | - `bin for GEM`: find out what executables gem GEM installs (e.g. `binbundle bin for yard`)
40 | 
41 | #### Options
42 | 
43 | ```
44 | Usage: binbundle [options] [bundle|install]
45 |         --[no-]versions              Include version info in output (default true)
46 |         --dry-run                    Output to STDOUT instead of file
47 |     -s, --sudo                       Install gems with sudo
48 |     -u, --user-install               Use --user-install to install gems
49 |     -f, --file FILE                  Output to alternative filename (default Binfile)
50 |     -l, --local                      Use installed gems instead of Binfile for gem_for and bins_for
51 |     -v, --version                    Display version
52 |     -h, --help                       Display this screen
53 | ```
54 | 
55 | #### Info commands
56 | 
57 | You can retrieve some basic info about gems and their binaries using `binbundle gem for BIN` or `binbundle bins for GEM`. This will read `Binfile` or any file specified by `--file` and return the requested info, either the gem associated with the given binary (BIN), or the binaries associated with the given gem name (GEM).
58 | 
59 | Use either info command with `--local` to parse installed gems rather than a Binfile.
60 | 
61 | ### Recommendations
62 | 
63 | I recommend using Binbundle along with a tool like [Dotbot](https://github.com/anishathalye/dotbot). Commit your bundle to a repo that you can easily clone to a new machine and then make `gem install binbundle` and `binbundle install ~/dotfiles/Binfile` part of your restore process.
64 | 
65 | 
66 | ### Support
67 | 
68 | PayPal link: [paypal.me/ttscoff](https://paypal.me/ttscoff)
69 | 
70 | ### Changelog
71 | 
72 | See [CHANGELOG.md](https://github.com/ttscoff/binbundle/blob/main/CHANGELOG.md)
73 | 
74 | ### Development
75 | 
76 | After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
77 | 
78 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
79 | 
80 | ### Contributing
81 | 
82 | Bug reports and pull requests are welcome on GitHub at . This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ttscoff/binbundle/blob/main/CODE_OF_CONDUCT.md).
83 | 
84 | ### License
85 | 
86 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
87 | 
88 | ### Code of Conduct
89 | 
90 | Everyone interacting in the Binbundle project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ttscoff/binbundle/blob/main/CODE_OF_CONDUCT.md).
91 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
 1 | # Contributor Covenant Code of Conduct
 2 | 
 3 | ## Our Pledge
 4 | 
 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
 6 | 
 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
 8 | 
 9 | ## Our Standards
10 | 
11 | Examples of behavior that contributes to a positive environment for our community include:
12 | 
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 | 
19 | Examples of unacceptable behavior include:
20 | 
21 | * The use of sexualized language or imagery, and sexual attention or
22 |   advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email
26 |   address, without their explicit permission
27 | * Other conduct which could reasonably be considered inappropriate in a
28 |   professional setting
29 | 
30 | ## Enforcement Responsibilities
31 | 
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 | 
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 | 
36 | ## Scope
37 | 
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 | 
40 | ## Enforcement
41 | 
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at brett.terpstra@oracle.com. All complaints will be reviewed and investigated promptly and fairly.
43 | 
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 | 
46 | ## Enforcement Guidelines
47 | 
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 | 
50 | ### 1. Correction
51 | 
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 | 
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 | 
56 | ### 2. Warning
57 | 
58 | **Community Impact**: A violation through a single incident or series of actions.
59 | 
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 | 
62 | ### 3. Temporary Ban
63 | 
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 | 
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 | 
68 | ### 4. Permanent Ban
69 | 
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior,  harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 | 
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 | 
74 | ## Attribution
75 | 
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 | 
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 | 
81 | [homepage]: https://www.contributor-covenant.org
82 | 
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 | 


--------------------------------------------------------------------------------
/lib/binbundle/gem_bins.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | module Binbundle
  4 |   # Main class
  5 |   class GemBins
  6 |     # Get gem for bin :gem_for
  7 |     attr_writer :gem_for
  8 | 
  9 |     # Get bins for gem :bin_for
 10 |     attr_writer :bin_for
 11 | 
 12 |     # Set options (for testing)
 13 |     attr_writer :file, :dry_run
 14 | 
 15 |     ##
 16 |     ## Create a new GemBins object
 17 |     ##
 18 |     ## @param      options  The options
 19 |     ## @option options [Boolean] :include_version Include version number in output
 20 |     ## @option options [Boolean] :sudo Include sudo in output
 21 |     ## @option options [Boolean] :user_install Include --user-install in output
 22 |     ## @option options [Boolean] :dry_run Output to STDOUT
 23 |     ## @option options [String] :file File to parse
 24 |     ## @option options [Boolean] :local Work from local gems instead of Binfile
 25 |     ##
 26 |     def initialize(options = {})
 27 |       @include_version = options[:include_version] || false
 28 |       @user_install = options[:user_install]
 29 |       @sudo = options[:sudo]
 30 |       @dry_run = options[:dry_run]
 31 |       @file = File.expand_path(options[:file])
 32 |     end
 33 | 
 34 |     ##
 35 |     ## Retrieve info (bin_for or gem_for)
 36 |     ##
 37 |     ## @param      options  The options
 38 |     ## @option options [Boolean] :local Work from local gems instead of Binfile
 39 |     ## @option options [String] :gem_for Find gem for this binary
 40 |     ## @option options [String] :bin_for Find bins for this gem
 41 |     ##
 42 |     ## @return [String] resulting gem or bins
 43 |     def info(options)
 44 |       unless File.exist?(@file) || options[:local]
 45 |         puts "File #{@file} not found"
 46 |         Process.exit 1
 47 |       end
 48 | 
 49 |       contents = if options[:local]
 50 |                    bins_to_s
 51 |                  else
 52 |                    IO.read(@file)
 53 |                  end
 54 | 
 55 |       gem_list = JewelryBox.new(contents: contents,
 56 |                                 include_version: @include_version,
 57 |                                 sudo: @sudo,
 58 |                                 user_install: @user_install)
 59 |       if options[:gem_for]
 60 |         gem_list.gem_for_bin(options[:gem_for])
 61 |       elsif options[:bin_for]
 62 |         gem_list.bins_for_gem(options[:bin_for])
 63 |       end
 64 |     end
 65 | 
 66 |     ##
 67 |     ## Install all gems in Binfile
 68 |     ##
 69 |     def install
 70 |       unless File.exist?(@file)
 71 |         puts "File #{@file} not found"
 72 |         Process.exit 1
 73 |       end
 74 | 
 75 |       contents = IO.read(@file)
 76 |       lines = JewelryBox.new(contents: contents, include_version: @include_version, sudo: @sudo,
 77 |                              user_install: @user_install)
 78 | 
 79 |       total = lines.count
 80 |       lines.delete_if { |l| installed?(l.gem, l.version) }
 81 |       installed = total - lines.count
 82 |       if installed.positive?
 83 |         puts "#{installed} gems already installed, skipping"
 84 |         total = lines.count
 85 |       end
 86 | 
 87 |       successes = 0
 88 |       failures = 0
 89 |       if total.zero?
 90 |         puts "All gems already installed"
 91 |         Process.exit 0
 92 |       end
 93 | 
 94 |       res = Prompt.yn("Install #{total} gems from #{File.basename(@file)}", default_response: true)
 95 |       Process.exit 0 unless res
 96 | 
 97 |       puts "Installing gems from #{@file}"
 98 | 
 99 |       if @dry_run
100 |         puts lines
101 |         Process.exit 0
102 |       end
103 | 
104 |       spinner = TTY::Spinner.new("[:spinner] Validating #{total} gems remotely ...", hide_cursor: true, format: :dots_2)
105 |       spinner.auto_spin
106 | 
107 |       lines.delete_if { |g| !valid?(g.gem) }
108 |       bad = total - lines.count
109 |       if bad.positive?
110 |         spinner.error
111 |         spinner.stop
112 |         puts "Failed to find #{bad} gems remotely or locally, skipping"
113 |         total = lines.count
114 | 
115 |         if total.zero?
116 |           puts "No gems left to install, some not found"
117 |           Process.exit 1
118 |         end
119 |       else
120 |         spinner.success
121 |         spinner.stop
122 |       end
123 | 
124 |       `sudo echo -n ''` if @sudo
125 | 
126 |       @errors = []
127 | 
128 |       lines.each do |cmd|
129 |         # rubocop:disable Naming/VariableNumber
130 |         spinner = TTY::Spinner.new("[:spinner] #{cmd} ...", hide_cursor: true, format: :dots_2)
131 |         # rubocop:enable Naming/VariableNumber
132 | 
133 |         spinner.auto_spin
134 | 
135 |         output = `/bin/bash -c '#{cmd}' 2>&1`
136 |         result = $CHILD_STATUS.success?
137 | 
138 |         if result
139 |           successes += 1
140 |           spinner.success
141 |           spinner.stop
142 |         else
143 |           failures += 1
144 |           spinner.error
145 |           spinner.stop
146 |           @errors << output
147 |         end
148 |       end
149 | 
150 |       puts "Total #{total}, installed: #{successes}, #{failures} errors."
151 | 
152 |       return if @errors.empty?
153 | 
154 |       puts 'ERRORS:'
155 |       puts @errors.join("\n")
156 |       Process.exit 1
157 |     end
158 | 
159 |     ##
160 |     ## Output all gems as Binfile format
161 |     ##
162 |     ## @return     [String] Binfile format
163 |     ##
164 |     def bins_to_s
165 |       local_gems.map(&:gem_command).join("\n\n")
166 |     end
167 | 
168 |     ##
169 |     ## Output or write Binfile
170 |     ##
171 |     def generate
172 |       output = bins_to_s
173 | 
174 |       if @dry_run
175 |         puts output
176 |       else
177 |         write_file(output)
178 |       end
179 |     end
180 | 
181 |     ##
182 |     ## Writes to Binfile
183 |     ##
184 |     def write_file(output)
185 |       if File.exist?(@file)
186 |         res = Prompt.yn("#{@file} already exists, overwrite", default_response: false)
187 |         Process.exit 1 unless res
188 |       end
189 | 
190 |       File.open(@file, 'w') do |f|
191 |         f.puts '#!/bin/bash'
192 |         f.puts
193 |         f.puts output
194 |       end
195 | 
196 |       puts "Wrote list to #{@file}"
197 | 
198 |       res = Prompt.yn('Make file executable', default_response: true)
199 | 
200 |       return unless res
201 | 
202 |       FileUtils.chmod 0o777, @file
203 |       puts 'Made file executable'
204 |     end
205 | 
206 |     def valid?(gem_name)
207 |       !Gem.latest_version_for(gem_name).nil?
208 |     end
209 | 
210 |     def installed?(gem_name, version = nil)
211 |       selected = local_gems.select do |g|
212 |         if g.gem == gem_name
213 |           if version && @include_version
214 |             g.version == version
215 |           else
216 |             true
217 |           end
218 |         else
219 |           false
220 |         end
221 |       end
222 |       selected.count.positive?
223 |     end
224 | 
225 |     private
226 | 
227 |     ##
228 |     ## Find local gems and group by name
229 |     ##
230 |     ## @return     [Array] array of local gems as Hashes
231 |     ##
232 |     def local_gems
233 |       gems_with_bins = JewelryBox.new(include_version: @include_version, sudo: @sudo, user_install: @user_install)
234 | 
235 |       all = Gem::Specification.sort_by { |g| [g.name.downcase, g.version] }.group_by(&:name)
236 |       all.delete_if { |_, specs| specs.delete_if { |spec| spec.executables.empty? }.empty? }
237 |       all.each do |g, specs|
238 |         gems_with_bins << Jewel.new(g, specs.last.executables.sort.uniq, specs.last.version.to_s)
239 |       end
240 | 
241 |       gems_with_bins
242 |     end
243 |   end
244 | end
245 | 


--------------------------------------------------------------------------------