├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── Steepfile ├── all-gem.gemspec ├── bin ├── console └── setup ├── exe └── all-gem ├── lib ├── all-gem.rb └── all-gem │ ├── cli.rb │ └── version.rb ├── sig └── all-gem.rbs ├── sig_internal ├── bundler.rbs ├── open3.rbs └── rubygems.rbs └── test ├── all-gem └── cli_test.rb └── test_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: {} 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | ruby: ['2.6', '2.7', '3.0', '3.1', head] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 20 | - run: bundle exec rake 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /Gemfile.lock 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in all-gem.gemspec 6 | gemspec 7 | 8 | gem "rake" 9 | gem 'steep' 10 | gem 'activesupport', '< 7' # https://github.com/soutaro/steep/issues/466 11 | 12 | gem 'test-unit' 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Masataka Pocke Kuwabara 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # all-gem 2 | 3 | all-gem runs various versions of command provided by gem. 4 | 5 | It was inspired by [all-ruby](https://github.com/akr/all-ruby). 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'all-gem' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle install 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install all-gem 22 | 23 | ## Usage 24 | 25 | Example: 26 | 27 | ```bash 28 | # Execute `rbs --version` command with all installed versions of rbs gem 29 | $ all-gem rbs --version 30 | 31 | # Execute `rbs --version` command with all versions of rbs gem 32 | # It installs gems before execution if it is not installed 33 | $ all-gem --remote rbs --version 34 | 35 | # Execute `rbs --version` command with all latest major versions of rbs gem 36 | $ all-gem --remote --major rbs --version 37 | ``` 38 | 39 | See the result of `all-gem help` 40 | 41 | ## Development 42 | 43 | After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 44 | 45 | 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). 46 | 47 | ## Contributing 48 | 49 | Bug reports and pull requests are welcome on GitHub at https://github.com/pocke/all-gem. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | task default: %i[steep test] 7 | 8 | Rake::TestTask.new(:test) do |t| 9 | t.libs << "test" 10 | t.libs << "lib" 11 | t.test_files = FileList["test/**/*_test.rb"] 12 | end 13 | 14 | task :steep do 15 | sh 'steep check' 16 | end 17 | -------------------------------------------------------------------------------- /Steepfile: -------------------------------------------------------------------------------- 1 | D = Steep::Diagnostic 2 | 3 | target :lib do 4 | signature "sig" 5 | signature "sig_internal" 6 | 7 | check "lib" # Directory name 8 | 9 | library 'rubygems' 10 | library 'optparse' 11 | 12 | configure_code_diagnostics(D::Ruby.default) 13 | end 14 | -------------------------------------------------------------------------------- /all-gem.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/all-gem/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "all-gem" 7 | spec.version = AllGem::VERSION 8 | spec.authors = ["Masataka Pocke Kuwabara"] 9 | spec.email = ["kuwabara@pocke.me"] 10 | spec.license = 'MIT' 11 | 12 | spec.summary = "Run various versions of command provided by gem" 13 | spec.description = "Run various versions of command provided by gem" 14 | spec.homepage = "https://github.com/pocke/all-gem" 15 | spec.required_ruby_version = ">= 2.6.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject do |f| 25 | (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 26 | end 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | # Uncomment to register a new dependency of your gem 33 | # spec.add_dependency "example-gem", "~> 1.0" 34 | 35 | # For more information and examples about making a new gem, check out our 36 | # guide at: https://bundler.io/guides/creating_gem.html 37 | end 38 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "all/gem" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/all-gem: -------------------------------------------------------------------------------- 1 | #!ruby 2 | 3 | $LOAD_PATH << File.join(__dir__, "../lib") 4 | require 'all-gem' 5 | 6 | begin 7 | AllGem::CLI.new(stdout: $stdout, stderr: $stderr, stdin: $stdin).run(ARGV) 8 | rescue AllGem::CLI::Error => ex 9 | $stderr.puts ex.message 10 | exit ex.status 11 | end 12 | -------------------------------------------------------------------------------- /lib/all-gem.rb: -------------------------------------------------------------------------------- 1 | require 'all-gem/cli' 2 | require 'all-gem/version' 3 | 4 | module AllGem 5 | end 6 | -------------------------------------------------------------------------------- /lib/all-gem/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'open3' 3 | 4 | module AllGem 5 | class CLI 6 | class Error < StandardError 7 | attr_reader :status 8 | 9 | def initialize(message, status = 1) 10 | super(message) 11 | @status = status 12 | end 13 | end 14 | 15 | class Options 16 | attr_accessor :remote, :since, :level 17 | 18 | def initialize 19 | @remote = false 20 | @since = nil 21 | @level = :all 22 | end 23 | end 24 | 25 | attr_reader :stdout, :stderr, :stdin, :op, :opts 26 | 27 | def initialize(stdout:, stderr:, stdin:) 28 | @stdout = stdout 29 | @stderr = stderr 30 | @stdin = stdin 31 | 32 | set_optparse 33 | end 34 | 35 | def run(argv) 36 | argv = @op.order(argv) 37 | 38 | command = argv[0] or raise Error.new("COMMAND is not specified.\n\n" + op.help) 39 | spec = gemspec_from_command(command) 40 | 41 | versions = versions_of(spec, opts) 42 | versions = filter_by_level(versions, opts) 43 | install! spec, versions 44 | 45 | execute_and_print argv, versions 46 | end 47 | 48 | private 49 | 50 | def set_optparse 51 | @opts = Options.new 52 | @op = OptionParser.new 53 | 54 | op.banner = <<~BANNER 55 | Usage: all-gem [all-gem options] COMMAND [command options] 56 | BANNER 57 | 58 | op.on('--remote', 'Install the gems if it does not exist in the local') { opts.remote = true } 59 | op.on('--since VERSION', 'Run only versions since specified one') { |v| opts.since = Gem::Version.new(v) } 60 | op.on('--major', 'Run only each major version') { opts.level = :major } 61 | op.on('--minor', 'Run only each minor version') { opts.level = :minor } 62 | op.on('--patch', 'Run only each patch version') { opts.level = :patch } 63 | op.on('--version', 'Display all-ruby version') { stdout.puts "all-gem-#{AllGem::VERSION}"; exit 0 } 64 | # op.on('--dir') # TODO: consider the API, make and cd to a directory 65 | # op.on('--pre') # TODO: consider the API 66 | end 67 | 68 | def execute_and_print(argv, versions) 69 | command = argv[0] 70 | 71 | # @type var prev: [String?, Process::Status?] 72 | prev = [nil, nil] 73 | prev_idx = -1 74 | versions.each.with_index do |v, idx| 75 | no_bundler do 76 | output, status = Open3.capture2e(command, "_#{v}_", *(argv[1..] or raise)) 77 | if prev != [output, status] 78 | if prev_idx != idx-1 79 | print_result command, versions, idx-1, (prev[0] or raise), (prev[1] or raise) 80 | end 81 | print_result command, versions, idx, output, status 82 | 83 | prev_idx = idx 84 | else 85 | stdout.puts '...' if prev_idx == idx-1 86 | end 87 | 88 | __skip__ = prev = [output, status] 89 | end 90 | end 91 | 92 | if prev_idx != versions.size-1 93 | print_result command, versions, versions.size-1, (prev[0] or raise), (prev[1] or raise) 94 | end 95 | end 96 | 97 | def print_result(command, versions, idx, output, status) 98 | exit_status_indent = 8 99 | longest_version = versions.max_by { |v| command.size + '-'.size + v.to_s.size } or raise 100 | output_indent = command.size + '-'.size + longest_version.to_s.size + ' '.size 101 | 102 | version = versions[idx] 103 | label = "#{command}-#{version}" 104 | 105 | stdout.print label 106 | lines = output.lines 107 | stdout.puts "#{' ' * (output_indent - label.size)}#{lines[0]}" 108 | stdout.puts (lines[1..] or raise).map { |line| "#{' ' * output_indent}#{line}"} 109 | stdout.puts "#{' ' * exit_status_indent}exit #{status.exitstatus}" unless status.success? 110 | end 111 | 112 | def gemspec_from_command(command) 113 | spec = Gem::Specification.find { |s| s.executables.include?(command) } 114 | # TODO: did you mean 115 | spec or raise Error.new("Could not find #{command} as an executable file") 116 | end 117 | 118 | def versions_of(spec, opts) 119 | versions = local_versions(spec) 120 | versions.concat remote_versions(spec) if opts.remote 121 | 122 | versions.uniq! 123 | versions.sort! 124 | 125 | since = opts.since 126 | versions = versions.select { |v| since <= v } if since 127 | 128 | versions 129 | end 130 | 131 | def remote_versions(spec) 132 | f = Gem::SpecFetcher.fetcher 133 | f.detect(:released) { |tuple| tuple.name == spec.name }.map { |arr| arr[0].version } 134 | end 135 | 136 | def local_versions(spec) 137 | Gem::Specification.select { |s| s.name == spec.name }.map { |s| s.version } 138 | end 139 | 140 | def install!(spec, versions) 141 | installed = local_versions(spec) 142 | 143 | targets = (versions - installed).map { |v| "#{spec.name}:#{v}" } 144 | return if targets.empty? 145 | 146 | no_bundler do 147 | __skip__ = system('gem', 'install', *targets, '--conservative', exception: true) 148 | end 149 | end 150 | 151 | def filter_by_level(versions, opts) 152 | return versions if opts.level == :all 153 | 154 | n = { major: 0, minor: 1, patch: 2 }[opts.level] 155 | versions.group_by { |v| v.segments[0..n] }.map { |_, vs| vs.max or raise } 156 | end 157 | 158 | def no_bundler(&block) 159 | if defined?(Bundler) 160 | Bundler.with_unbundled_env do 161 | block.call 162 | end 163 | else 164 | block.call 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/all-gem/version.rb: -------------------------------------------------------------------------------- 1 | module AllGem 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /sig/all-gem.rbs: -------------------------------------------------------------------------------- 1 | module AllGem 2 | VERSION: String 3 | 4 | class CLI 5 | class Error < StandardError 6 | attr_reader status: Integer 7 | 8 | def initialize: (String message, ?Integer status) -> void 9 | end 10 | 11 | class Options 12 | attr_accessor remote: bool 13 | attr_accessor since: Gem::Version? 14 | attr_accessor level: :all | :major | :minor | :patch 15 | end 16 | 17 | attr_reader stdout: IO 18 | attr_reader stderr: IO 19 | attr_reader stdin: IO 20 | attr_reader opts: Options 21 | attr_reader op: OptionParser 22 | 23 | @op: OptionParser 24 | 25 | def initialize: (stdout: IO, stderr: IO, stdin: IO) -> void 26 | 27 | def run: (Array[String]) -> void 28 | 29 | private 30 | 31 | def set_optparse: () -> void 32 | 33 | def execute_and_print: (Array[String] argv, Array[Gem::Version]) -> void 34 | 35 | def print_result: (String command, Array[Gem::Version], Integer index, String output, Process::Status) -> void 36 | 37 | def gemspec_from_command: (String command) -> Gem::Specification 38 | 39 | def versions_of: (Gem::Specification, Options) -> Array[Gem::Version] 40 | 41 | def local_versions: (Gem::Specification) -> Array[Gem::Version] 42 | 43 | def remote_versions: (Gem::Specification) -> Array[Gem::Version] 44 | 45 | def install!: (Gem::Specification, Array[Gem::Version]) -> void 46 | 47 | def filter_by_level: (Array[Gem::Version], Options) -> Array[Gem::Version] 48 | 49 | def no_bundler: [T] () { () -> T } -> T 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /sig_internal/bundler.rbs: -------------------------------------------------------------------------------- 1 | module Bundler 2 | def self.with_unbundled_env: [T] () { () -> T } -> T 3 | end 4 | -------------------------------------------------------------------------------- /sig_internal/open3.rbs: -------------------------------------------------------------------------------- 1 | module Open3 2 | def self.capture2e: (*String) -> [String, Process::Status] 3 | end 4 | -------------------------------------------------------------------------------- /sig_internal/rubygems.rbs: -------------------------------------------------------------------------------- 1 | module Gem 2 | class Specification < BasicSpecification 3 | extend Enumerable[Specification] 4 | 5 | attr_reader name: String 6 | attr_reader version: Version 7 | 8 | def self.each: () -> Enumerator[Specification, Array[Specification]] 9 | | () { (Specification) -> void } -> Array[Specification] 10 | 11 | def executables: () -> Array[String] 12 | end 13 | 14 | class NameTuple 15 | attr_reader name: String 16 | attr_reader version: Version 17 | end 18 | 19 | class Source 20 | end 21 | 22 | class Version 23 | def segments: () -> Array[Integer] 24 | end 25 | 26 | class SpecFetcher 27 | def self.fetcher: () -> instance 28 | 29 | type specs_type = :complete | :released | :prerelease | :latest 30 | 31 | def detect: (specs_type) { (NameTuple) -> boolish } -> Array[[NameTuple, Source]] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/all-gem/cli_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class AllGemCLITest < Test::Unit::TestCase 4 | def test_no_options 5 | cli = cli() 6 | cli.run(%w[rbs --version]) 7 | assert_match(/^rbs-[\w.]+\s+/, cli.stdout.string) 8 | assert cli.stderr.string.empty? 9 | end 10 | 11 | def test_major_option 12 | cli = cli() 13 | cli.run(%w[--major --remote rbs --version]) 14 | assert_equal ['rbs-0'], cli.stdout.string.scan(/^rbs-0/) 15 | assert_equal ['rbs-1'], cli.stdout.string.scan(/^rbs-1/) 16 | assert_equal ['rbs-2'], cli.stdout.string.scan(/^rbs-2/) 17 | 18 | refute cli.stdout.string.include?('rbs-0.20.0') 19 | refute cli.stdout.string.include?('rbs-1.6.1') 20 | 21 | assert cli.stderr.string.empty? 22 | end 23 | 24 | def test_no_args 25 | cli = cli() 26 | error = assert_raises AllGem::CLI::Error do 27 | cli.run([]) 28 | end 29 | assert error.message.include?('COMMAND is not specified') 30 | end 31 | 32 | def cli 33 | AllGem::CLI.new(stdout: StringIO.new, stderr: StringIO.new, stdin: StringIO.new) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require 'stringio' 3 | 4 | require 'all-gem' 5 | --------------------------------------------------------------------------------