lib/ass_launcher/cmd.rb
require 'clamp'
module AssLauncher
# AssLauncher command-line untils
# @example
# $ass-launcher --help
# @api private
#
module Cmd
# Colorize string for console output
# It's stupid wrapper for ColorizedString
# @api private
module Colorize
require 'colorized_string'
# rubocop:disable Style/MethodMissing
def self.method_missing(m, s)
colorized(s).send(m)
end
# rubocop:enable Style/MethodMissing
def self.colorized(mes)
ColorizedString[mes]
end
end
# @api private
module Support
# Mixin
# @api private
module SrvStrParser
# Parse string like +user:password@host:port+
# @param s [String]
# @return [Array] ['host:port', 'user', 'password']
def parse_srv_str(s)
split = s.split('@')
fail ArgumentError if split.size > 2
host = split.pop
return [host, nil, nil] if split.size.zero?
split = split[0].split(':')
fail ArgumentError if split.size > 2
user = split.shift
pass = split.shift
[host, user, pass]
end
end
# Mixin for validate version
# @api private
module VersionValidator
include AssLauncher::Enterprise::CliDefsLoader
def validate_version
return known_version.sort.last if version.to_s.empty?
unless known_version.include? version
signal_usage_error "Unknown 1C:Enterprise v#{version}\n"\
"Execute `ass-launcher show-version' command"
end
version
end
def known_version
@known_version ||= defs_versions.sort
end
end
# Mixin for cli reporters
# @api private
module AcceptedValuesGet
def accepted_values_get
xxx_list_keys(:switch, param) + xxx_list_keys(:chose, param)
end
def xxx_list_keys(list, p)
list = p.send("#{list}_list".to_sym)
return list.keys if list
[]
end
end
end
# @api private
# Abstract things
module Abstract
# Abstarct subcommand
# @api private
class SubCommand < Clamp::Command
# :nodoc:
module Declaration
def subcommand_(klass)
subcommand(klass.command_name, klass._banner, klass)
end
def declare_subcommands
self::SubCommands.constants.each do |c|
subcommand_ self::SubCommands.const_get(c)
end
end
end
extend Declaration
# :nocov:
def self.command_name
fail 'Abstract'
end
def self._banner
fail 'Abstract'
end
# :nocov:
end
# Mixin
# @api private
module ClientMode
def parrent_command
invocation_path.to_s.split[1]
end
def client
case parrent_command
when 'designer' then :thick
when 'thick' then :thick
when 'thin' then :thin
when 'web' then :web
when 'makeib' then :thick
end
end
def mode
case parrent_command
when 'designer' then :designer
when 'thick' then :enterprise
when 'thin' then :enterprise
when 'web' then :webclient
when 'makeib' then :createinfobase
end
end
end
# Mixin
# @api private
module BinaryWrapper
include AssLauncher::Api
include ClientMode
def self.included(base)
fail "#{base} must include Option::Arch module before include"\
' BinaryWrapper' unless base.include? Option::Arch
end
def binary_wrapper
binary_get || (fail Clamp::ExecutionError
.new(not_inst_message, invocation_path, 1))
end
def not_inst_message
"1C:Enterprise #{client} v #{vrequrement} not installed"
end
private :not_inst_message
def vrequrement
return '' unless version
case version.segments.size
when 3 then "~> #{version}.0"
when 2 then "~> #{version}.0"
else "= #{version}"
end
end
def binary_get
case client
when :thick then thicks_get(vrequrement).last
when :thin then thins_get(vrequrement).last
end
end
private :binary_get
def thicks_get(req)
thicks(req).select do |bw|
arch_any? || bw.arch == arch
end
end
private :thicks_get
def thins_get(req)
thins(req).select do |bw|
arch_any? || bw.arch == arch
end
end
private :thins_get
# rubocop:disable all
def dry_run(cmd)
r = "#{cmd.cmd.gsub(' ', '\\ ')} "
if mode == :createinfobase
r << cmd.args.join(' ')
else
r << cmd.args.map do |a|
unless a =~ %r{^(/|-|'|"|DESIGNER|ENTERPRISE)}
"\'#{a}\'" unless a.to_s.empty?
else
a
end
end.join(' ')
end
end
def run_enterprise(cmd)
if respond_to?(:dry_run?) && dry_run?
puts Colorize.yellow(dry_run(cmd))
else
begin
cmd.run.wait.result.verify!
rescue AssLauncher::Support::Shell::RunAssResult::RunAssError => e
raise Clamp::ExecutionError.new(e.message, invocation_path, 1)
end
end
cmd
end
# rubocop:enable all
end
# @api private
module Option
# Mixin
# Command option
module SearchPath
def self.included(base)
base.option %w[--search-path -I], 'PATH',
'specify 1C:Enterprise installation path' do |s|
AssLauncher.config.search_path = s
s
end
end
end
# Mixin
# Command option
module Version
def self.included(base)
base.option %w[--version -v], 'VERSION',
"specify 1C:Enterprise version requiremet.\n"\
" Expected full version number or only major\n"\
' part of version number' do |s|
Gem::Version.new(s)
end
end
end
# Mixin
# Command option
module Verbose
def self.included(base)
base.option '--verbose', :flag, 'show more information'
end
end
# Mixin
# Command option
module Query
def self.included(base)
base.option %w[--query -q], 'REGEX',
'regular expression based filter' do |s|
begin
Regexp.new(s, Regexp::IGNORECASE)
rescue RegexpError => e
fail ArgumentError, e.message
end
end
end
end
# Mixin
# Command option
module Dbms
# rubocop:disable all
def self.included(base)
dbtypes = AssLauncher::Support::ConnectionString::DBMS_VALUES\
+ ['File']
define_method :valid_db_types do
dbtypes
end
base.option '--dbms', 'DB_TYPE',
"db type: #{dbtypes}.\nValue \"File\""\
' for make file infobase', default: 'File' do |s|
raise ArgumentError,
"valid values: [#{valid_db_types.join(' ')}]" unless\
valid_db_types.include? s
s
end
end
# rubocop:enable all
end
# Mixin
# Command option
module Dbsrv
attr_reader :dbsrv_user, :dbsrv_pass, :dbsrv_host
include Support::SrvStrParser
def parse_dbsrv(s)
@dbsrv_host, @dbsrv_user, @dbsrv_pass = parse_srv_str(s)
end
def self.included(base)
base.option '--dbsrv', 'user:pass@dbsrv', 'db server address' do |s|
parse_dbsrv s
s
end
end
end
# Mixin
# Command option
module Esrv
attr_reader :esrv_user, :esrv_pass, :esrv_host
include Support::SrvStrParser
def parse_esrv(s)
@esrv_host, @esrv_user, @esrv_pass = parse_srv_str(s)
end
def self.included(base)
base.option '--esrv', 'user:pass@esrv',
'enterprise server address' do |s|
parse_esrv(s)
s
end
end
end
# Mixin
# Command option
module User
def self.included(base)
base.option %w[--user -u], 'NAME', 'infobase user name'
end
end
# Mixin
# Command option
module Password
def self.included(base)
base.option %w[--password -p], 'PASSWORD', 'infobase user password'
end
end
# Mixin
# Command option
module Pattern
def self.included(base)
base.option %w[--pattern -P], 'PATH',
'Template for make infobase.'\
' Path to .cf, .dt files' do |s|
fail ArgumentError, "Path not exist: #{s}" unless File.exist?(s)
s
end
end
end
# Mixin
# Command option
module Uc
def self.included(base)
base.option '--uc', 'LOCK_CODE', 'infobase lock code'
end
end
# Mixin
# Command option
module DryRun
def self.included(base)
base.option %w[--dry-run], :flag,
'will not realy run 1C:Enterprise only puts cmd string'
end
end
# Mixin
# Command option
module Raw
def parse_raw(s)
split = s.split(%r{(?<!\\),\s}).map(&:strip)
split.map do |pv|
fail ArgumentError, "Parse error in: #{pv}" unless\
pv =~ %r{^(/|-)}
pv =~ %r{^(\/|-)([^\s]+)+(.*)?}
["#{$1}#{$2}", $3.strip].map { |i| i.gsub('\\,', ',') }
end
end
def raw_param
r = []
raw_list.each do |params|
r += params
end
r
end
def self.included(base)
description = "other 1C CLI parameters in raw(native) format.\n"\
'Parameters and their arguments must be delimited'\
" comma-space sequence: `, '\n"\
"If values includes comma comma must be slashed `\\\\,'\n"\
'WARNING: correctness of parsing will not guaranteed!'
base.option '--raw', '"/Par VAL, -SubPar VAL"', description,
multivalued: true do |s|
parse_raw s
end
end
end
# Mixin
# Command option
module ShowAppiaredOnly
def self.included(base)
base.option ['--show-appiared-only', '-a'], :flag,
'show parameters which appiared in --version only'
end
end
# Mixin
# Command option
module DevMode
def self.included(base)
base.option ['--dev-mode', '-d'], :flag,
"for developers mode. Show DSL methods\n"\
" specifications for builds commands in ruby scripts\n"
end
end
# Mixin
# Command option
module Format
def self.included(base)
base.option ['--format', '-f'], 'ascii|csv', 'output format',
default: :ascii do |s|
fail ArgumentError, "Invalid format `#{s}'" unless\
%w[csv ascii].include? s
s.to_sym
end
end
end
# Mixin
# Command option
module Arch
def expected_archs
[AssLauncher::Enterprise::BinaryWrapper::X86_64,
AssLauncher::Enterprise::BinaryWrapper::I386]
end
def self.included(base)
base.option '--arch', 'ARCH',
'specify x86_64 or i386 platform arch' do |s|
fail ArgumentError, "Invalid arch `#{s}'."\
" Valid values #{expected_archs.join('|')}" unless\
expected_archs.include? s
s
end
end
def x86_64?
arch.to_s == AssLauncher::Enterprise::BinaryWrapper::X86_64
end
def arch_any?
arch.nil?
end
end
end
# @api private
# rubocop:disable Style/ClassAndModuleCamelCase
module Parameter
# Mixin
# Command parameter
module IB_PATH
def self.included(base)
base.parameter 'IB_PATH', 'path to infobase like a strings'\
" 'tcp://srv/ref' or 'http[s]://host/path'"\
" or 'path/to/ib'", attribute_name: :ib_path do |s|
s
end
end
end
# Mixin
# Command parameter
module IB_PATH_NAME
def self.included(base)
base.parameter 'IB_PATH | IB_NAME',
'PATH for file or NAME for server infobase',
attribute_name: :ib_path do |s|
s
end
end
end
end
# rubocop:enable Style/ClassAndModuleCamelCase
# Abstarct cli-help command
# @api private
class Cli < SubCommand
include Support::VersionValidator
include Option::Version
include Option::ShowAppiaredOnly
include Option::DevMode
include Option::Query
include Option::Format
include Option::Verbose
include ClientMode
# Reporter
# @api private
# rubocop:disable Metrics/ClassLength
class Report
USAGE_COLUMNS = [:usage,
:argument,
:parent,
:group,
:desc].freeze
DEVEL_COLUMNS = [:parameter,
:dsl_method,
:accepted_values,
:parent,
:param_klass,
:group,
:require,
:desc].freeze
# Report's row
class Row
include Support::AcceptedValuesGet
(USAGE_COLUMNS + DEVEL_COLUMNS).uniq.each do |col|
attr_accessor col
end
attr_reader :param
def initialize(param)
@param = param
fill
end
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
# rubocop:disable Metrics/MethodLength
def fill
self.parameter = basic_usage
self.dsl_method = dsl_method_get
self.parent = param.parent
self.require = param.binary_matcher.requirement
self.accepted_values = accepted_values_get
.to_s.gsub(/(^\[|\]$)/, '')
self.usage, self.argument = usage_full
self.desc = param.desc
self.param_klass = param.class.name.split('::').last
self.group = param.group
end
private :fill
def usage_full
case param.class.name.split('::').last
when 'Switch' then
["#{basic_usage}(#{accepted_values_get.join('|')})"]
when 'Chose' then
[basic_usage, accepted_values_get.join(', ')]
when 'StringParam' then [basic_usage, 'VALUE']
when 'Path' then [basic_usage, 'PATH']
when 'Flag' then [basic_usage]
when 'PathTwice' then [basic_usage, 'PATH PATH']
else basic_usage
end
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
# rubocop:enable Metrics/MethodLength
def basic_usage
return " #{param.name}" if param.parent
param.name
end
def dsl_method_get
method = param.name.gsub(%r{^\s*(/|-)}, '_')
return " #{method}" if param.parent
method
end
def to_csv(columns)
r = ''
columns.each do |col|
r << "\"#{send(col).to_s.tr('"', '\'')}\";"
end
r.gsub(/;$/, '')
end
end
attr_reader :client, :mode, :version, :query, :appiared_only,
:dev_mode
# rubocop:disable Metrics/ParameterLists
def initialize(client, mode, version, appiared_only, query, dev_mode)
@client = client
@mode = mode
@version = version
@appiared_only = appiared_only
@query = query
@dev_mode = dev_mode
end
# rubocop:enable Metrics/ParameterLists
def clients?(p)
p.binary_matcher.clients.include? client
end
def modes?(p)
p.modes.include? mode
end
def version?(p)
return true if version.nil?
if appiared_only
p.binary_matcher.requirement.to_s =~ /^>=\s*#{version}/
else
p.match_version?(version) unless appiared_only
end
end
def match?(p)
clients?(p) && modes?(p) && version?(p)
end
def not_filtred?(p)
return true unless query
coll_match?(:desc, p) || coll_match?(:parent, p) || \
coll_match?(:name, p)
end
def coll_match?(prop, p)
!(p.send(prop).to_s =~ query).nil?
end
def groups
AssLauncher::Enterprise::Cli::CliSpec.cli_def.parameters_groups
end
def grouped_rows
r = {}
groups.each do |gname, _|
r[gname] = rows.select { |row| row.group == gname }
.sort_by { |row| row.param.full_name }
end
r
end
def rows
@rows ||= execute
end
def select_parameters
r = []
AssLauncher::Enterprise::Cli::CliSpec
.cli_def.parameters.parameters.each do |p|
if match?(p) && not_filtred?(p)
r << p
r << p.parent if p.parent && !r.include?(p.parent)
end
end
r
end
def execute
r = select_parameters.map do |p|
Row.new(p)
end
r.sort_by { |row| row.param.full_name }
end
def max_col_width(col, rows)
[rows.map do |r|
r.send(col).to_s.length
end.max, col.to_s.length].max
end
require 'io/console'
def term_width(trim = 0)
IO.console.winsize[1] - trim
end
def eval_width(col, total, r, trim, rows)
[(term_width(trim) - r.values.inject(0) { |i, o| o + i }) / total,
max_col_width(col, rows)].min
end
# rubocop:disable Style/ConditionalAssignment
def columns_width(columns, rows)
total = columns.size + 1
columns.each_with_object({}) do |col, r|
total -= 1
if [:usage, :parameter, :dsl_method].include? col
r[col] = max_col_width(col, rows)
else
r[col] = eval_width(col, total, r,
4 + (columns.size - 1) * 3, rows)
end
end
end
def main_header
if dev_mode
r = 'DSL METHODS'
else
r = 'CLI PARAMETERS'
end
r << " AVAILABLE FOR: \"#{client}\" CLIENT V#{version}"
r << " IN \"#{mode}\" RUNNING MODE" if client == :thick
r.upcase
end
# rubocop:enable Style/ConditionalAssignment
def filter_header
"FILTERED BY: #{query}" if query
end
# rubocop:disable all
# @doto: refactoring and tests require
def to_table(columns)
require 'command_line_reporter'
extend CommandLineReporter
header title: main_header, width: main_header.length, rule: true,
align: 'center', bold: true, spacing: 0
header title: filter_header, width: filter_header.length, rule: true,
align: 'center', bold: false, color: 'yellow', spacing: 0 if\
filter_header
grouped_rows.each do |gname, rows|
next if rows.size.zero?
table(border: true, encoding: :ascii) do
header title: "PARAMTERS GROUP: \"#{groups[gname][:desc]}\"",
bold: true
row header: true do
columns_width(columns, rows).each do |col, width|
column(col.upcase, width: [width, 1].max)
end
end
rows.each do |row_|
row do
columns.each do |col|
column(row_.send(col))
end
end
end
end
end
nil
end
# rubocop:enable all
def to_csv(columns)
r = "#{columns.join(';')}\n"
rows.each do |row|
r << row.to_csv(columns)
r << "\n"
end
r
end
end
# rubocop:enable Metrics/ClassLength
def self.command_name
'cli-help'
end
def self._banner
'show help for 1C:Enterprise CLI parameters'
end
def columns
cols = dev_mode? ? Report::DEVEL_COLUMNS : Report::USAGE_COLUMNS
cols -= [:parent, :parameter, :group, :require] unless verbose?
cols
end
def formating(report)
return report.to_table(columns) if format == :ascii
report.to_csv(columns)
end
def execute
$stdout.puts formating Report.new(client, mode, validate_version,
show_appiared_only?, query,
dev_mode?)
end
end
# Mixin
# @api private
module ParseIbPath
include AssLauncher::Api
require 'uri'
def connection_string
case ib_path
when %r{https?://}i then cs_http(ws: ib_path)
when %r{tcp://}i then parse_tcp_path
else cs_file(file: ib_path)
end
end
def parse_tcp_path
u = URI(ib_path)
cs_srv(srvr: "#{u.host}:#{u.port}", ref: u.path.gsub(%r{^/}, ''))
end
end
# Abstarct run command
# @api private
class Run < SubCommand
include Option::Version
include Option::DryRun
include Option::SearchPath
include Option::User
include Option::Password
include Option::Uc
include Option::Raw
include Option::Arch
include BinaryWrapper
include ParseIbPath
def self.command_name
'run'
end
def self._banner
'run 1C:Enterprise'
end
def command_(&block)
if client == :thin
binary_wrapper.command((raw_param.flatten || []), &block)
else
binary_wrapper.command(mode, (raw_param.flatten || []), &block)
end
end
# rubocop:disable Metrics/MethodLength
def make_command
usr = user
pass = password
uc_ = uc
cs = connection_string
cmd = command_ do
connection_string cs
_N usr if usr
_P pass if pass
_UC uc_ if uc_
end
cmd
end
# rubocop:enable Metrics/MethodLength
def execute
cmd = run_enterprise(make_command)
puts Colorize.green(cmd.process_holder.result.assout) unless dry_run?
end
end
end
# @api private
# Root of all subcommands
class Main < Clamp::Command
module SubCommands
# show-version subcommand
class ShowVersion < Abstract::SubCommand
include AssLauncher::Enterprise::CliDefsLoader
def self.command_name
'show-version'
end
def self._banner
'Show version of ass_launcher gem and'\
' list of known 1C:Enterprise'
end
def known_versions_list
" - v#{defs_versions.reverse.map(&:to_s).join("\n - v")}"
end
def execute
puts Colorize.yellow('ass_launcher:')\
+ Colorize.green(" v#{AssLauncher::VERSION}")
puts Colorize.yellow('Known 1C:Enterprise:')
puts Colorize.green(known_versions_list)
end
end
# env subcommand
class Env < Abstract::SubCommand
include Abstract::Option::SearchPath
include AssLauncher::Api
def self.command_name
'env'
end
def self._banner
'Show 1C:Enterprise installations'
end
def list(clients)
" - v#{clients.map do |cl|
"#{cl.version} (#{cl.arch})"
end.join("\n - v")}"
end
# rubocop:disable Metrics/AbcSize
def execute
puts Colorize.red "Ruby arch: #{RbConfig::CONFIG['arch']}"
puts Colorize.yellow '1C:Enterprise installations was searching in:'
puts Colorize
.green " - #{AssLauncher::Enterprise.search_paths.join("\n - ")}"
puts Colorize.yellow 'Thick client installations:'
puts Colorize.green list(thicks.reverse)
puts Colorize.yellow 'Thin client installations:'
puts Colorize.green list(thins.reverse)
end
# rubocop:enable Metrics/AbcSize
end
end
# Main cmd invoker
Dir.glob File.join(File.expand_path('../cmd', __FILE__), '*.rb') do |lib|
require lib if File.basename(lib) != 'abstract.rb'
end
extend Abstract::SubCommand::Declaration
declare_subcommands
end
end
end