yegor256/texsc

View on GitHub
bin/texsc

Summary

Maintainability
Test Coverage
#!/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