Improved CLI and only showing applicable options per command.

This commit is contained in:
Sebastian Ludwig 2016-12-05 10:41:24 +01:00
parent b33d8425f8
commit c619dd61e4
4 changed files with 749 additions and 468 deletions

View file

@ -21,8 +21,8 @@ module Twine
class Error < StandardError class Error < StandardError
end end
require 'twine/version'
require 'twine/plugin' require 'twine/plugin'
require 'twine/cli'
require 'twine/twine_file' require 'twine/twine_file'
require 'twine/encoding' require 'twine/encoding'
require 'twine/output_processor' require 'twine/output_processor'
@ -37,5 +37,5 @@ module Twine
require 'twine/formatters/jquery' require 'twine/formatters/jquery'
require 'twine/formatters/tizen' require 'twine/formatters/tizen'
require 'twine/runner' require 'twine/runner'
require 'twine/version' require 'twine/cli'
end end

View file

@ -1,187 +1,394 @@
require 'optparse' require 'optparse'
require 'io/console'
module Twine module Twine
module CLI module CLI
NEEDED_COMMAND_ARGUMENTS = { ALL_FORMATS = Formatters.formatters.map(&:format_name).map(&:downcase)
'generate-localization-file' => 3, OPTIONS = {
'generate-all-localization-files' => 3, consume_all: {
'consume-localization-file' => 3, switch: ['-a', '--[no-]consume-all'],
'consume-all-localization-files' => 3, description: 'Normally Twine will ignore any translation keys that do not exist in your Twine file.',
'generate-loc-drop' => 3, boolean: true
'consume-loc-drop' => 3, },
'validate-twine-file' => 2 consume_comments: {
switch: ['-c', '--[no-]consume-comments'],
description: <<-DESC,
Normally Twine will ignore all comments in the file. With this flag set, any
comments encountered will be read and parsed into the Twine data file. This is especially useful
when creating your first Twine data file from an existing project.
DESC
boolean: true
},
create_folders: {
switch: ['-r', '--[no-]create-folders'],
description: <<-DESC,
This flag may be used to create output folders for all languages, if they don't exist yet.
As a result all languages will be exported, not only the ones where an output folder already exists.
DESC
boolean: true
},
developer_language: {
switch: ['-d', '--developer-language LANG'],
description: <<-DESC,
When writing the Twine data file, set the specified language as the "developer language". In
practice, this just means that this language will appear first in the Twine data file. When
generating files this language will be used as default language and its translations will be
used if a definition is not localized for the output language.
DESC
},
encoding: {
switch: ['-e', '--encoding ENCODING'],
description: <<-DESC,
Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate
encoding for these files. For example, you could use this to write Apple .strings files in UTF-16.
When reading files, Twine does its best to determine the encoding automatically. However, if the
files are UTF-16 without BOM, you need to specify if it's UTF-16LE or UTF16-BE.
DESC
},
file_name: {
switch: ['-n', '--file-name FILE_NAME'],
description: 'This flag may be used to overwrite the default file name of the format.'
},
format: {
switch: ['-f', '--format FORMAT', ALL_FORMATS],
description: <<-DESC,
The file format to read or write: (#{ALL_FORMATS.join(', ')}). Additional formatters can be placed in the formats/ directory.
DESC
},
:include => {
switch: ['-i', '--include SET', [:all, :translated, :untranslated]],
description: <<-DESC,
This flag will determine which definitions are included. It's possible values are:
all: All definitions both translated and untranslated for the specified language are included.
This is the default value.
translated: Only definitions with translation for the specified language are included.
untranslated: Only definitions without translation for the specified language are included.
DESC
default: :all
},
languages: {
switch: ['-l', '--lang LANGUAGES', Array],
description: 'Comma separated list of language codes to use for the specified action.'
},
output_path: {
switch: ['-o', '--output-file OUTPUT_FILE'],
description: 'Write a new Twine file at this location instead of replacing the original file.'
},
pedantic: {
switch: ['-p', '--[no-]pedantic'],
description: 'When validating a Twine file, perform additional checks that go beyond pure validity (like presence of tags).'
},
tags: {
switch: ['-t', '--tags TAG1,TAG2,TAG3', Array],
description: <<-DESC,
Only definitions with ANY of the specified tags will be processed. Specify this option multiple
times to only include definitions with ALL of the specified tags. Prefix a tag with ~ to include
definitions NOT containing that tag. Omit this option to match all definitions in the Twine data file.
DESC
repeated: true
},
untagged: {
switch: ['-u', '--[no-]untagged'],
description: <<-DESC,
If you have specified tags using the --tags flag, then only those tags will be selected. If you also
want to select all definitions that are untagged, then you can specify this option to do so.
DESC
},
validate: {
switch: ['--[no-]validate'],
description: 'Validate the Twine file before formatting it.'
}
}
COMMANDS = {
'generate-localization-file' => {
description: 'Generates a localization file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.',
arguments: [:twine_file, :output_path],
optional_options: [
:developer_language,
:encoding,
:format,
:include,
:languages,
:tags,
:untagged,
:validate
],
option_validation: Proc.new { |options|
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'specify only a single language for the `generate-localization-file` command.'
end
},
example: 'twine generate-localization-file twine.txt ko.xml --tags FT'
},
'generate-all-localization-files' => {
description: 'Generates all the localization files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent translations.',
arguments: [:twine_file, :output_path],
optional_options: [
:create_folders,
:developer_language,
:encoding,
:file_name,
:format,
:include,
:tags,
:untagged,
:validate
],
example: 'twine generate-all-localization-files twine.txt Resources/Locales/ --tags FT,FB'
},
'generate-loc-drop' => {
description: 'Generates a zip archive of localization files in a given format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command.',
arguments: [:twine_file, :output_path],
required_options: [
:format
],
optional_options: [
:developer_language,
:encoding,
:include,
:tags,
:untagged,
:validate
],
example: 'twine generate-loc-drop twine.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
},
'consume-localization-file' => {
description: 'Slurps all of the translations from a localization file into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:languages,
:output_path,
:tags
],
option_validation: Proc.new { |options|
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'specify only a single language for the `consume-localization-file` command.'
end
},
example: 'twine consume-localization-file twine.txt ja.strings'
},
'consume-all-localization-files' => {
description: 'Slurps all of the translations from a directory into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:output_path,
:tags
],
example: 'twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
},
'consume-loc-drop' => {
description: 'Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.',
arguments: [:twine_file, :input_path],
optional_options: [
:consume_all,
:consume_comments,
:developer_language,
:encoding,
:format,
:output_path,
:tags
],
example: 'twine consume-loc-drop twine.txt LocDrop5.zip'
},
'validate-twine-file' => {
description: 'Validates that the given Twine file is parseable, contains no duplicates, and that no key contains invalid characters. Exits with a non-zero exit code if those criteria are not met.',
arguments: [:twine_file],
optional_options: [
:developer_language,
:pedantic
],
example: 'twine validate-twine-file twine.txt'
}
} }
def self.parse(args) def self.parse(args)
options = { include: :all } command = args.select { |a| a[0] != '-' }[0]
parser = OptionParser.new do |opts| unless COMMANDS.keys.include? command
opts.banner = 'Usage: twine COMMAND TWINE_FILE [INPUT_OR_OUTPUT_PATH] [--lang LANG1,LANG2...] [--tags TAG1,TAG2,TAG3...] [--format FORMAT]' Twine::stderr.puts "Invalid command: #{command}" unless command.nil?
opts.separator '' print_help(args)
opts.separator 'The purpose of this script is to convert back and forth between multiple data formats, allowing us to treat our strings (and translations) as data stored in a text file. We can then use the data file to create drops for the localization team, consume similar drops returned by the localization team, and create formatted localization files to ship with your products.' abort
opts.separator ''
opts.separator 'Commands:'
opts.separator ''
opts.separator '- generate-localization-file'
opts.separator ' Generates a localization file in a certain LANGUAGE given a particular FORMAT. This script will attempt to guess both the language and the format given the filename and extension. For example, "ko.xml" will generate a Korean language file for Android.'
opts.separator ''
opts.separator '- generate-all-localization-files'
opts.separator ' Generates all the localization files necessary for a given project. The parent directory to all of the locale-specific directories in your project should be specified as the INPUT_OR_OUTPUT_PATH. This command will most often be executed by your build script so that each build always contains the most recent translations.'
opts.separator ''
opts.separator '- consume-localization-file'
opts.separator ' Slurps all of the translations from a localization file into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
opts.separator ''
opts.separator '- consume-all-localization-files'
opts.separator ' Slurps all of the translations from a directory into the specified TWINE_FILE. If you have some files returned to you by your translators you can use this command to incorporate all of their changes. This script will attempt to guess both the language and the format given the filename and extension. For example, "ja.strings" will assume that the file is a Japanese iOS strings file.'
opts.separator ''
opts.separator '- generate-loc-drop'
opts.separator ' Generates a zip archive of localization files in a given format. The purpose of this command is to create a very simple archive that can be handed off to a translation team. The translation team can unzip the archive, translate all of the strings in the archived files, zip everything back up, and then hand that final archive back to be consumed by the consume-loc-drop command.'
opts.separator ''
opts.separator '- consume-loc-drop'
opts.separator ' Consumes an archive of translated files. This archive should be in the same format as the one created by the generate-loc-drop command.'
opts.separator ''
opts.separator '- validate-twine-file'
opts.separator ' Validates that the given Twine file is parseable, contains no duplicates, and that no key contains invalid characters. Exits with a non-zero exit code if those criteria are not met.'
opts.separator ''
opts.separator 'General Options:'
opts.separator ''
opts.on('-l', '--lang LANGUAGES', Array, 'The language code(s) to use for the specified action.') do |l|
options[:languages] = l
end
opts.on('-t', '--tags TAG1,TAG2,TAG3', Array, 'The tag(s) to use for the specified action. Only definitions with ANY of the specified tags will be processed.',
' Specify this option multiple times to only include definitions with ALL of the specified tags. Prefix a tag',
' with ~ to include definitions NOT containing that tag. Omit this option to match all definitions in the Twine',
' data file.') do |t|
options[:tags] = (options[:tags] || []) << t
end
opts.on('-u', '--[no-]untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select',
' all definitions that are untagged, then you can specify this option to do so.') do |u|
options[:untagged] = u
end
formats = Formatters.formatters.map(&:format_name).map(&:downcase)
opts.on('-f', '--format FORMAT', formats, "The file format to read or write: (#{formats.join(', ')}).",
" Additional formatters can be placed in the formats/ directory.") do |f|
options[:format] = f
end
opts.on('-a', '--[no-]consume-all', 'Normally, when consuming a localization file, Twine will ignore any translation keys that do not exist in your Twine file.') do |a|
options[:consume_all] = true
end
opts.on('-i', '--include SET', [:all, :translated, :untranslated],
"This flag will determine which definitions are included when generating localization files. It's possible values are:",
" all: All definitions both translated and untranslated for the specified language are included. This is the default value.",
" translated: Only definitions with translation for the specified language are included.",
" untranslated: Only definitions without translation for the specified language are included.") do |i|
options[:include] = i
end
opts.on('-o', '--output-file OUTPUT_FILE', 'Write a new Twine file at this location instead of replacing the original file. This flag is only useful when',
' running the consume-localization-file or consume-loc-drop commands.') do |o|
options[:output_path] = o
end
opts.on('-n', '--file-name FILE_NAME', 'When running the generate-all-localization-files command, this flag may be used to overwrite the default file name of',
' the format.') do |n|
options[:file_name] = n
end
opts.on('-r', '--[no-]create-folders', "When running the generate-all-localization-files command, this flag may be used to create output folders for all languages,",
" if they don't exist yet. As a result all languages will be exported, not only the ones where an output folder already",
" exists.") do |r|
options[:create_folders] = r
end
opts.on('-d', '--developer-language LANG', 'When writing the Twine data file, set the specified language as the "developer language". In practice, this just',
' means that this language will appear first in the Twine data file. When generating files this language will be',
' used as default language and its translations will be used if a definition is not localized for the output language.') do |d|
options[:developer_language] = d
end
opts.on('-c', '--[no-]consume-comments', 'Normally, when consuming a localization file, Twine will ignore all comments in the file. With this flag set, any comments',
' encountered will be read and parsed into the Twine data file. This is especially useful when creating your first',
' Twine data file from an existing project.') do |c|
options[:consume_comments] = c
end
opts.on('-e', '--encoding ENCODING', 'Twine defaults to encoding all output files in UTF-8. This flag will tell Twine to use an alternate encoding for these',
' files. For example, you could use this to write Apple .strings files in UTF-16. When reading files, Twine does its best',
" to determine the encoding automatically. However, if the files are UTF-16 without BOM, you need to specify if it's",
' UTF-16LE or UTF16-BE.') do |e|
options[:output_encoding] = e
end
opts.on('--[no-]validate', 'Validate the Twine file before formatting it.') do |validate|
options[:validate] = validate
end
opts.on('-p', '--[no-]pedantic', 'When validating a Twine file, perform additional checks that go beyond pure validity (like presence of tags).') do |p|
options[:pedantic] = p
end
opts.on('-h', '--help', 'Show this message.') do |h|
puts opts.help
exit
end
opts.on('--version', 'Print the version number and exit.') do
puts "Twine version #{Twine::VERSION}"
exit
end
opts.separator ''
opts.separator 'Examples:'
opts.separator ''
opts.separator '> twine generate-localization-file twine.txt ko.xml --tags FT'
opts.separator '> twine generate-all-localization-files twine.txt Resources/Locales/ --tags FT,FB'
opts.separator '> twine consume-localization-file twine.txt ja.strings'
opts.separator '> twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
opts.separator '> twine generate-loc-drop twine.txt LocDrop5.zip --tags FT,FB --format android --lang de,en,en-GB,ja,ko'
opts.separator '> twine consume-loc-drop twine.txt LocDrop5.zip'
opts.separator '> twine validate-twine-file twine.txt'
end
begin
parser.parse! args
rescue OptionParser::ParseError => e
Twine::stderr.puts e.message
exit false
end end
if args.length == 0 options = parse_command_options(command, args)
puts parser.help
exit false
end
number_of_needed_arguments = NEEDED_COMMAND_ARGUMENTS[args[0]]
unless number_of_needed_arguments
raise Twine::Error.new "Invalid command: #{args[0]}"
end
options[:command] = args[0]
if args.length < 2
raise Twine::Error.new 'You must specify your twine file.'
end
options[:twine_file] = args[1]
if args.length < number_of_needed_arguments
raise Twine::Error.new 'Not enough arguments.'
elsif args.length > number_of_needed_arguments
raise Twine::Error.new "Unknown argument: #{args[number_of_needed_arguments]}"
end
case options[:command]
when 'generate-localization-file'
options[:output_path] = args[2]
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'Please only specify a single language for the generate-localization-file command.'
end
when 'generate-all-localization-files'
options[:output_path] = args[2]
when 'consume-localization-file'
options[:input_path] = args[2]
if options[:languages] and options[:languages].length > 1
raise Twine::Error.new 'Please only specify a single language for the consume-localization-file command.'
end
when 'consume-all-localization-files'
options[:input_path] = args[2]
when 'generate-loc-drop'
options[:output_path] = args[2]
if !options[:format]
raise Twine::Error.new 'You must specify a format.'
end
when 'consume-loc-drop'
options[:input_path] = args[2]
when 'validate-twine-file'
end
return options return options
end end
private
def self.print_help(args)
verbose = false
help_parser = OptionParser.new
help_parser.banner = 'Usage: twine [command] [options]'
help_parser.define('-h', '--help', 'Show this message.')
help_parser.define('--verbose', 'More detailed help.') { verbose = true }
help_parser.parse!(args)
Twine::stdout.puts help_parser.help
Twine::stdout.puts ''
Twine::stdout.puts 'Commands:'
COMMANDS.each do |name, properties|
if verbose
Twine::stdout.puts ''
Twine::stdout.puts ''
Twine::stdout.puts "# #{name}"
Twine::stdout.puts ''
Twine::stdout.puts properties[:description]
else
Twine::stdout.puts "- #{name}"
end
end
Twine::stdout.puts ''
Twine::stdout.puts 'type `twine [command] --help` for further information about a command.'
end
# source: https://www.safaribooksonline.com/library/view/ruby-cookbook/0596523696/ch01s15.html
def self.word_wrap(s, width)
s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n").rstrip
end
def self.indent(string, first_line, following_lines)
lines = string.split("\n")
indentation = ' ' * following_lines
lines.map! { |line| indentation + line }
result = lines.join("\n").strip
' ' * first_line + result
end
# ensure the description forms a neat block on the right
def self.prepare_description!(options, summary_width)
lines = options[:description].split "\n"
# remove leadinge HEREDOC spaces
space_match = lines[0].match(/^\s+/)
if space_match
leading_spaces = space_match[0].length
lines.map! { |l| l[leading_spaces..-1] }
end
merged_lines = []
lines.each do |line|
# if the line is a continuation of the previous one
if not merged_lines.empty? and (line[0] != ' ' or line[0, 4] == ' ')
merged_lines[-1] += ' ' + line.strip
else
merged_lines << line.rstrip
end
end
summary_width += 7 # account for description padding
max_description_width = IO.console.winsize[1] - summary_width
merged_lines.map! do |line|
if line[0] == ' '
line = word_wrap(line.strip, max_description_width - 2)
line = indent(line, 2, 4)
else
line = word_wrap(line, max_description_width)
end
line
end
options[:switch] << indent(merged_lines.join("\n"), 0, summary_width)
end
def self.parse_command_options(command_name, args)
args.delete(command_name)
command = COMMANDS[command_name]
result = {
command: command_name
}
parser = OptionParser.new
parser.banner = "Usage: twine #{command_name} #{command[:arguments].map { |c| "[#{c}]" }.join(' ')} [options]"
[:required_options, :optional_options].each do |option_type|
options = command[option_type]
if options and options.size > 0
parser.separator ''
parser.separator option_type.to_s.gsub('_', ' ').capitalize + ":"
options.each do |option_name|
option = OPTIONS[option_name]
result[option_name] = option[:default] if option[:default]
prepare_description!(option, parser.summary_width)
parser.define(*option[:switch]) do |value|
if option[:repeated]
result[option_name] = (result[option_name] || []) << value
elsif option[:boolean]
result[option_name] = true
else
result[option_name] = value
end
end
end
end
end
parser.define('-h', '--help', 'Show this message.') do
puts parser.help
exit
end
parser.separator ''
parser.separator 'Examples:'
parser.separator ''
parser.separator "> #{command[:example]}"
begin
parser.parse! args
rescue OptionParser::ParseError => e
raise Twine::Error.new e.message
end
arguments = args.reject { |a| a[0] == '-' }
number_of_missing_arguments = command[:arguments].size - arguments.size
if number_of_missing_arguments > 0
missing_arguments = command[:arguments][-number_of_missing_arguments, number_of_missing_arguments]
raise Twine::Error.new "#{number_of_missing_arguments} missing argument#{number_of_missing_arguments > 1 ? "s" : ""}: #{missing_arguments.join(', ')}. Check `twine #{command_name} -h`"
end
if args.length > command[:arguments].size
raise Twine::Error.new "Unknown argument: #{args[command[:arguments].size]}"
end
if command[:required_options]
command[:required_options].each do |option_name|
if result[option_name] == nil
raise Twine::Error.new "missing option: #{OPTIONS[option_name][:switch][0]}"
end
end
end
command[:option_validation].call(result) if command[:option_validation]
command[:arguments].each do |argument_name|
result[argument_name] = args.shift
end
result
end
end end
end end

View file

@ -53,7 +53,7 @@ module Twine
raise Twine::Error.new "Nothing to generate! The resulting file would not contain any translations." unless output raise Twine::Error.new "Nothing to generate! The resulting file would not contain any translations." unless output
IO.write(@options[:output_path], output, encoding: encoding) IO.write(@options[:output_path], output, encoding: output_encoding)
end end
def generate_all_localization_files def generate_all_localization_files
@ -89,7 +89,7 @@ module Twine
next next
end end
IO.write(file_path, output, encoding: encoding) IO.write(file_path, output, encoding: output_encoding)
end end
else else
language_found = false language_found = false
@ -111,7 +111,7 @@ module Twine
next next
end end
IO.write(file_path, output, encoding: encoding) IO.write(file_path, output, encoding: output_encoding)
end end
unless language_found unless language_found
@ -121,36 +121,6 @@ module Twine
end end
def consume_localization_file
lang = nil
if @options[:languages]
lang = @options[:languages][0]
end
read_localization_file(@options[:input_path], lang)
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def consume_all_localization_files
if !File.directory?(@options[:input_path])
raise Twine::Error.new("Directory does not exist: #{@options[:output_path]}")
end
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
if File.file?(item)
begin
read_localization_file(item)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
end
end
end
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def generate_loc_drop def generate_loc_drop
validate_twine_file if @options[:validate] validate_twine_file if @options[:validate]
@ -177,7 +147,7 @@ module Twine
next next
end end
IO.write(temp_path, output, encoding: encoding) IO.write(temp_path, output, encoding: output_encoding)
zipfile.add(zip_path, temp_path) zipfile.add(zip_path, temp_path)
end end
end end
@ -185,6 +155,36 @@ module Twine
end end
end end
def consume_localization_file
lang = nil
if @options[:languages]
lang = @options[:languages][0]
end
read_localization_file(@options[:input_path], lang)
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def consume_all_localization_files
if !File.directory?(@options[:input_path])
raise Twine::Error.new("Directory does not exist: #{@options[:input_path]}")
end
Dir.glob(File.join(@options[:input_path], "**/*")) do |item|
if File.file?(item)
begin
read_localization_file(item)
rescue Twine::Error => e
Twine::stderr.puts "#{e.message}"
end
end
end
output_path = @options[:output_path] || @options[:twine_file]
write_twine_data(output_path)
end
def consume_loc_drop def consume_loc_drop
require_rubyzip require_rubyzip
@ -260,8 +260,8 @@ module Twine
private private
def encoding def output_encoding
@options[:output_encoding] || 'UTF-8' @options[:encoding] || 'UTF-8'
end end
def require_rubyzip def require_rubyzip
@ -296,9 +296,9 @@ module Twine
formatter, lang = prepare_read_write(path, lang) formatter, lang = prepare_read_write(path, lang)
encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path) external_encoding = @options[:encoding] || Twine::Encoding.encoding_for_path(path)
IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: encoding, internal_encoding: 'UTF-8') do |io| IO.open(IO.sysopen(path, 'rb'), 'rb', external_encoding: external_encoding, internal_encoding: 'UTF-8') do |io|
io.read(2) if Twine::Encoding.has_bom?(path) io.read(2) if Twine::Encoding.has_bom?(path)
formatter.read(io, lang) formatter.read(io, lang)
end end

View file

@ -2,7 +2,7 @@ require 'twine_test'
class CLITest < TwineTest class CLITest < TwineTest
def setup def setup
super super()
@twine_file_path = File.join @output_dir, SecureRandom.uuid @twine_file_path = File.join @output_dir, SecureRandom.uuid
@input_path = File.join @output_dir, SecureRandom.uuid @input_path = File.join @output_dir, SecureRandom.uuid
@ -13,292 +13,366 @@ class CLITest < TwineTest
@options = Twine::CLI::parse command.split @options = Twine::CLI::parse command.split
end end
class TestValidateTwineFile < CLITest def parse_with(parameters)
def test_command raise "you need to implement `parse_with` in your test class"
parse "validate-twine-file #{@twine_file_path}" end
assert_equal 'validate-twine-file', @options[:command] def assert_option_consume_all
assert_equal @twine_file_path, @options[:twine_file] parse_with '--consume-all'
end assert @options[:consume_all]
end
def test_pedantic def assert_option_consume_comments
parse "validate-twine-file #{@twine_file_path} --pedantic" parse_with '--consume-comments'
assert @options[:pedantic] assert @options[:consume_comments]
end end
def test_missing_parameter def assert_option_developer_language
assert_raises Twine::Error do random_language = KNOWN_LANGUAGES.sample
parse 'validate-twine-file' parse_with "--developer-language #{random_language}"
end assert_equal random_language, @options[:developer_language]
end end
def test_extra_parameter def assert_option_encoding
assert_raises Twine::Error do parse_with '--encoding UTF16'
parse 'validate-twine-file twine_file extra' assert_equal 'UTF16', @options[:encoding]
end end
def assert_option_format
random_format = Twine::Formatters.formatters.sample.format_name.downcase
parse_with "--format #{random_format}"
assert_equal random_format, @options[:format]
end
def assert_option_include
random_set = [:all, :translated, :untranslated].sample
parse_with "--include #{random_set}"
assert_equal random_set, @options[:include]
end
def assert_option_single_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--lang #{random_language}"
assert_equal [random_language], @options[:languages]
end
def assert_option_multiple_languages
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
parse_with "--lang #{random_languages.join(',')}"
assert_equal random_languages.sort, @options[:languages].sort
end
def assert_option_languages
assert_option_single_language
assert_option_multiple_languages
end
def assert_option_output_path
parse_with "--output-file #{@output_path}"
assert_equal @output_path, @options[:output_path]
end
def assert_option_tags
# single tag
random_tag = "tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [[random_tag]], @options[:tags]
# multiple OR tags
random_tags = ["tag#{rand(100)}", "tag#{rand(100)}", "tag#{rand(100)}"]
parse_with "--tags #{random_tags.join(',')}"
sorted_tags = @options[:tags].map { |tags| tags.sort }
assert_equal [random_tags.sort], sorted_tags
# multiple AND tags
random_tag_1 = "tag#{rand(100)}"
random_tag_2 = "tag#{rand(100)}"
parse_with "--tags #{random_tag_1} --tags #{random_tag_2}"
assert_equal [[random_tag_1], [random_tag_2]], @options[:tags]
# NOT tag
random_tag = "~tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [[random_tag]], @options[:tags]
end
def assert_option_untagged
parse_with '--untagged'
assert @options[:untagged]
end
def assert_option_validate
parse_with "--validate"
assert @options[:validate]
end
end
class TestGenerateLocalizationFileCLI < CLITest
def parse_with(parameters)
parse "generate-localization-file #{@twine_file_path} #{@output_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'generate-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "generate-localization-file #{@twine_file}"
end end
end end
class TestGenerateLocalizationFile < CLITest def test_extra_argument
def test_command assert_raises Twine::Error do
parse "generate-localization-file #{@twine_file_path} #{@output_path}" parse_with "extra"
assert_equal 'generate-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse 'generate-localization-file twine_file'
end
end
def test_validate
parse "generate-localization-file #{@twine_file_path} #{@output_path} --validate"
assert @options[:validate]
end
def test_extra_parameter
assert_raises Twine::Error do
parse 'generate-localization-file twine_file output extra'
end
end
def test_only_allows_one_language
assert_raises Twine::Error do
parse "generate-localization-file twine_file output --lang en,fr"
end
end end
end end
class TestGenerateAllLocalizationFiles < CLITest def test_options
def test_command assert_option_developer_language
parse "generate-all-localization-files #{@twine_file_path} #{@output_dir}" assert_option_encoding
assert_option_format
assert_option_include
assert_option_single_language
assert_raises(Twine::Error) { assert_option_multiple_languages }
assert_option_tags
assert_option_untagged
assert_option_validate
end
end
assert_equal 'generate-all-localization-files', @options[:command] class TestGenerateAllLocalizationFilesCLI < CLITest
assert_equal @twine_file_path, @options[:twine_file] def parse_with(parameters)
assert_equal @output_dir, @options[:output_path] parse "generate-all-localization-files #{@twine_file_path} #{@output_dir} " + parameters
end end
def test_missing_parameter def test_command
assert_raises Twine::Error do parse_with ""
parse "generate-all-localization-files twine_file"
end
end
def test_validate assert_equal 'generate-all-localization-files', @options[:command]
parse "generate-all-localization-files #{@twine_file_path} #{@output_dir} --validate" assert_equal @twine_file_path, @options[:twine_file]
assert @options[:validate] assert_equal @output_dir, @options[:output_path]
end end
def test_extra_parameter def test_missing_argument
assert_raises Twine::Error do assert_raises Twine::Error do
parse "generate-all-localization-files twine_file output extra" parse "generate-all-localization-files twine_file"
end
end end
end end
class TestConsumeLocalizationFile < CLITest def test_extra_arguemnt
def test_command assert_raises Twine::Error do
parse "consume-localization-file #{@twine_file_path} #{@input_path}" parse_with "extra"
assert_equal 'consume-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "consume-localization-file twine_file"
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse "consume-localization-file twine_file output extra"
end
end
def test_only_allows_one_language
assert_raises Twine::Error do
parse "consume-localization-file twine_file output --lang en,fr"
end
end end
end end
class TestConsumeAllLocalizationFiles < CLITest def test_options
def test_command assert_option_developer_language
parse "consume-all-localization-files #{@twine_file_path} #{@input_dir}" assert_option_encoding
assert_option_format
assert_option_include
assert_option_tags
assert_option_untagged
assert_option_validate
end
assert_equal 'consume-all-localization-files', @options[:command] def test_option_create_folders
assert_equal @twine_file_path, @options[:twine_file] parse_with '--create-folders'
assert_equal @input_dir, @options[:input_path] assert @options[:create_folders]
end end
def test_missing_parameter def test_option_file_name
assert_raises Twine::Error do random_filename = "#{rand(10000)}"
parse "consume-all-localization-files twine_file" parse_with "--file-name #{random_filename}"
end assert_equal random_filename, @options[:file_name]
end end
end
def test_extra_parameter class TestGenerateLocDropCLI < CLITest
assert_raises Twine::Error do def parse_with(parameters)
parse "consume-all-localization-files twine_file output extra" parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple " + parameters
end end
def test_command
parse_with ""
assert_equal 'generate-loc-drop', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "generate-loc-drop twine_file --format apple"
end end
end end
class TestGenerateLocDrop < CLITest def test_extra_argument
def test_command assert_raises Twine::Error do
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple" parse_with "extra"
assert_equal 'generate-loc-drop', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @output_path, @options[:output_path]
end
def test_missing_parameter
assert_raises Twine::Error do
parse "generate-loc-drop twine_file --format apple"
end
end
def test_validate
parse "generate-loc-drop #{@twine_file_path} #{@output_path} --format apple --validate"
assert @options[:validate]
end
def test_extra_parameter
assert_raises Twine::Error do
parse "generate-loc-drop twine_file output extra --format apple"
end
end
def test_format_needed
assert_raises Twine::Error do
parse "generate-loc-drop twine_file output"
end
end end
end end
class TestConsumeLocDrop < CLITest def test_options
def test_command assert_option_developer_language
parse "consume-loc-drop #{@twine_file_path} #{@input_path}" assert_option_encoding
assert_option_include
assert_equal 'consume-loc-drop', @options[:command] assert_option_tags
assert_equal @twine_file_path, @options[:twine_file] assert_option_untagged
assert_equal @input_path, @options[:input_path] assert_option_validate
end
def test_missing_parameter
assert_raises Twine::Error do
parse "consume-loc-drop twine_file"
end
end
def test_extra_parameter
assert_raises Twine::Error do
parse "consume-loc-drop twine_file input extra"
end
end
end end
class TestParameters < CLITest def test_option_format_required
def parse_with(parameter) assert_raises Twine::Error do
parse 'validate-twine-file input.txt ' + parameter parse "generate-loc-drop twine_file output"
end
def test_default_options
parse_with ''
expected = {command: 'validate-twine-file', twine_file: 'input.txt', include: :all}
assert_equal expected, @options
end
def test_create_folders
parse_with '--create-folders'
assert @options[:create_folders]
end
def test_consume_all
parse_with '--consume-all'
assert @options[:consume_all]
end
def test_consume_comments
parse_with '--consume-comments'
assert @options[:consume_comments]
end
def test_untagged
parse_with '--untagged'
assert @options[:untagged]
end
def test_developer_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--developer-lang #{random_language}"
assert_equal random_language, @options[:developer_language]
end
def test_single_language
random_language = KNOWN_LANGUAGES.sample
parse_with "--lang #{random_language}"
assert_equal [random_language], @options[:languages]
end
def test_multiple_languages
random_languages = KNOWN_LANGUAGES.shuffle[0, 3]
parse_with "--lang #{random_languages.join(',')}"
assert_equal random_languages.sort, @options[:languages].sort
end
def test_single_tag
random_tag = "tag#{rand(100)}"
parse_with "--tags #{random_tag}"
assert_equal [[random_tag]], @options[:tags]
end
def test_multiple_OR_tags
random_tags = ["tag#{rand(100)}", "tag#{rand(100)}", "tag#{rand(100)}"]
parse_with "--tags #{random_tags.join(',')}"
sorted_tags = @options[:tags].map { |tags| tags.sort }
assert_equal [random_tags.sort], sorted_tags
end
def test_multiple_AND_tags
random_tag_1 = "tag#{rand(100)}"
random_tag_2 = "tag#{rand(100)}"
parse_with "--tags #{random_tag_1} --tags #{random_tag_2}"
assert_equal [[random_tag_1], [random_tag_2]], @options[:tags]
end
def test_format
random_format = Twine::Formatters.formatters.sample.format_name.downcase
parse_with "--format #{random_format}"
assert_equal random_format, @options[:format]
end
def test_include
random_set = [:all, :translated, :untranslated].sample
parse_with "--include #{random_set}"
assert_equal random_set, @options[:include]
end
def test_output_path
parse_with "--output-file #{@output_path}"
assert_equal @output_path, @options[:output_path]
end
def test_file_name
random_filename = "#{rand(10000)}"
parse_with "--file-name #{random_filename}"
assert_equal random_filename, @options[:file_name]
end
def test_encoding
parse_with '--encoding UTF16'
assert_equal 'UTF16', @options[:output_encoding]
end end
end end
end end
class TestConsumeLocalizationFileCLI < CLITest
def parse_with(parameters)
parse "consume-localization-file #{@twine_file_path} #{@input_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-localization-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-localization-file twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_single_language
assert_raises(Twine::Error) { assert_option_multiple_languages }
assert_option_output_path
assert_option_tags
end
end
class TestConsumeAllLocalizationFilesCLI < CLITest
def parse_with(parameters)
parse "consume-all-localization-files #{@twine_file_path} #{@input_dir} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-all-localization-files', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_dir, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-all-localization-files twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_tags
end
end
class TestConsumeLocDropCLI < CLITest
def parse_with(parameters)
parse "consume-loc-drop #{@twine_file_path} #{@input_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'consume-loc-drop', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
assert_equal @input_path, @options[:input_path]
end
def test_missing_argument
assert_raises Twine::Error do
parse "consume-loc-drop twine_file"
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with "extra"
end
end
def test_options
assert_option_consume_all
assert_option_consume_comments
assert_option_developer_language
assert_option_encoding
assert_option_format
assert_option_output_path
assert_option_tags
end
end
class TestValidateTwineFileCLI < CLITest
def parse_with(parameters)
parse "validate-twine-file #{@twine_file_path} " + parameters
end
def test_command
parse_with ""
assert_equal 'validate-twine-file', @options[:command]
assert_equal @twine_file_path, @options[:twine_file]
end
def test_missing_argument
assert_raises Twine::Error do
parse 'validate-twine-file'
end
end
def test_extra_argument
assert_raises Twine::Error do
parse_with 'extra'
end
end
def test_options
assert_option_developer_language
end
def test_option_pedantic
parse "validate-twine-file #{@twine_file_path} --pedantic"
assert @options[:pedantic]
end
end