bin/texsc
#!/usr/bin/env ruby
# frozen_string_literal: true
# Copyright (c) 2020-2024 Yegor Bugayenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
VERSION = '0.0.0'
$stdout.sync = true
require 'backtrace'
require 'loog'
require 'open3'
require 'shellwords'
require 'slop'
def config(path)
args = []
f = File.expand_path(path)
if File.exist?(f)
body = File.read(f)
args += body.split(/\s+/).map(&:strip)
puts "Found #{args.length} lines in #{File.absolute_path(f)}"
end
args
end
begin
log = Loog::REGULAR
args = config('~/.texsc') + config('.texsc') + ARGV
begin
opts = Slop.parse(args, strict: true, help: true) do |o|
o.banner = "Usage (#{VERSION}): texsc [options] files
Options are:"
o.string '--pws', 'The location of aspell.en.pws file'
o.array '--ignore', 'The name of the command or environment to ignore'
o.integer '--min-word-length',
'The minimum length of the word to be checked', default: 3
o.bool '--version', 'Print current version' do
puts VERSION
exit
end
o.bool '--verbose', 'Make it more verbose than usual' do
log = Loog::VERBOSE
end
o.bool '--help', 'Read this: https://github.com/yegor256/texsc' do
puts o
exit
end
end
rescue Slop::Error => e
raise e.message
end
candidates = opts.arguments
candidates += Dir['*.tex'] if candidates.empty?
if opts[:pws]
log.debug("PWS with an additional dictionary is here: #{opts[:pws]}")
opts[:pws] = File.expand_path(opts[:pws])
log.debug("The real path of PWS is: #{opts[:pws]}")
raise "The PWS file #{opts[:pws].inspect} is not found" unless File.exist?(opts[:pws])
end
errors = 0
files = 0
candidates.each do |f|
tex = File.read(f)
ignores = opts[:ignore].map do |ee|
c, o = ee.split(':')
{ cmd: c, opts: o || 'p' }
end
ignores.each do |i|
tex = tex.gsub(/\\begin{#{Regexp.escape(i[:cmd])}}.+\\end{#{Regexp.escape(i[:cmd])}}/m, '')
end
log.info("Checking #{f} (#{tex.length} chars)...")
log.debug(
tex.split("\n").each_with_index.map do |t, i|
if t.start_with?('---')
log.info("Line #{i + 1} starts with '---', this may lead to unexpected errors")
end
format('%<pos>4d: %<line>s', pos: i + 1, line: t)
end.join("\n")
)
cmd = [
'aspell',
Shellwords.escape("--ignore=#{opts['min-word-length']}"),
'--dont-tex-check-comments',
'--lang=en',
'--mode=tex',
opts[:pws] ? "-p #{Shellwords.escape(opts[:pws])}" : '',
ignores.map do |i|
"--add-tex-command '#{Shellwords.escape(i[:cmd])} #{Shellwords.escape(i[:opts])}'"
end.join(' '),
'pipe'
].join(' ')
log.debug(cmd)
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
stdin.print(tex)
stdin.close
status = thread.value.to_i
unless status.zero?
log.error(stderr.read)
raise "Failed to run aspell, exit code is #{status}"
end
out = stdout.read
if out.nil?
log.info('aspell produced no output, hm...')
else
lines = out.split("\n")
log.debug("aspell produced #{lines.length} lines of output")
lines.each_with_index do |t, i|
if t.start_with?('&')
log.info("[#{i}] #{t}")
errors += 1
end
end
end
end
files += 1
end
unless errors.zero?
log.info("#{errors} spelling errors found in #{files} file(s)")
exit 1
end
log.info("No spelling errors found in #{files} file(s), the text is clean")
rescue StandardError => e
if opts[:verbose]
puts Backtrace.new(e)
else
puts "ERROR: #{e.message}"
end
exit(255)
end