Class: JSON::Repair::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/json/repair/cli.rb,
sig/json/repair/cli.rbs

Constant Summary collapse

OVERWRITE_DESC =

Marked private_constant in lib/json/repair/cli.rb. RBS has no private_constant syntax, so the declaration is unavoidably public in the signature; do not rely on it from outside this class.

Returns:

  • (::String)
'Replace the input file in place (requires filename; conflicts with --output)'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ CLI

Returns a new instance of CLI.

Parameters:

  • stdin: (stream) (defaults to: $stdin)
  • stdout: (stream) (defaults to: $stdout)
  • stderr: (stream) (defaults to: $stderr)


15
16
17
18
19
# File 'lib/json/repair/cli.rb', line 15

def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr)
  @stdin = stdin
  @stdout = stdout
  @stderr = stderr
end

Class Method Details

.call(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ ::Integer

Parameters:

  • argv (::Array[::String])
  • stdin: (stream) (defaults to: $stdin)
  • stdout: (stream) (defaults to: $stdout)
  • stderr: (stream) (defaults to: $stderr)

Returns:

  • (::Integer)


11
12
13
# File 'lib/json/repair/cli.rb', line 11

def self.call(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
  new(stdin: stdin, stdout: stdout, stderr: stderr).call(argv)
end

Instance Method Details

#call(argv) ⇒ ::Integer

Reset per-invocation state so a single instance can be safely reused (e.g. cli = CLI.new; cli.call(['-v']); cli.call(['x'])).

Parameters:

  • argv (::Array[::String])

Returns:

  • (::Integer)


23
24
25
26
27
28
29
30
31
# File 'lib/json/repair/cli.rb', line 23

def call(argv)
  @output_path = @halt = nil
  @overwrite = false
  run(argv)
rescue OptionParser::ParseError, JSON::JSONRepairError, SystemCallError, IOError,
       SystemStackError => e
  @stderr.puts "json-repair: #{e.message}"
  1
end

#define_options(opts) ⇒ void

This method returns an undefined value.

Parameters:

  • opts (::OptionParser)


116
117
118
119
120
121
# File 'lib/json/repair/cli.rb', line 116

def define_options(opts)
  opts.on('-o', '--output FILE', 'Write repaired JSON to FILE') { |f| @output_path = f }
  opts.on('--overwrite', OVERWRITE_DESC) { @overwrite = true }
  opts.on('-v', '--version', 'Print version and exit') { halt_with(JSON::Repair::VERSION) }
  opts.on('-h', '--help', 'Print this help and exit') { halt_with(opts.help) }
end

#halt_with(message) ⇒ void

This method returns an undefined value.

Print to stdout and short-circuit parser.parse so trailing args after --version/--help do not raise OptionParser::ParseError and flip the exit code (the option text promises "...and exit").

Parameters:

  • message (::String)


126
127
128
129
130
# File 'lib/json/repair/cli.rb', line 126

def halt_with(message)
  @stdout.puts message
  @halt = 0
  throw :halt
end

#parser::OptionParser

Returns:

  • (::OptionParser)


103
104
105
106
107
108
109
110
111
# File 'lib/json/repair/cli.rb', line 103

def parser
  OptionParser.new do |opts|
    opts.banner = 'Usage: json-repair [filename] [options]'
    opts.separator ''
    opts.separator 'Repair a broken JSON document. Reads stdin when no filename is given.'
    opts.separator ''
    define_options(opts)
  end
end

#read_input(input_path) ⇒ ::String

Parameters:

  • input_path (::String, nil)

Returns:

  • (::String)

Raises:



64
65
66
67
68
69
70
# File 'lib/json/repair/cli.rb', line 64

def read_input(input_path)
  raw = (input_path ? File.read(input_path) : @stdin.read).to_s
  raw.force_encoding(Encoding::UTF_8)
  raise JSON::JSONRepairError, 'input is not valid UTF-8' unless raw.valid_encoding?

  raw
end

#replace_in_place(input_path, repaired) ⇒ void

This method returns an undefined value.

Write to a uniquely-named tempfile alongside the input, then move it over the original. Tempfile.create uses O_EXCL + a random suffix, so the temp path is safe against symlink / clobber races; FileUtils.mv with force: true handles cross-device renames and Windows, where File.rename cannot overwrite an existing destination. The original file's mode is preserved (Tempfile defaults to 0600).

Symlinks are followed via File.realpath so the underlying file is rewritten in place and the link is left pointing at it; otherwise the rename would replace the link itself with a regular file.

Parameters:

  • input_path (::String)
  • repaired (::String)


92
93
94
95
96
97
98
99
100
101
# File 'lib/json/repair/cli.rb', line 92

def replace_in_place(input_path, repaired)
  real_path = File.realpath(input_path)
  original_mode = File.stat(real_path).mode
  Tempfile.create(['json-repair', '.tmp'], File.dirname(real_path)) do |tmp|
    tmp.write(repaired)
    tmp.close
    File.chmod(original_mode, tmp.path)
    FileUtils.mv(tmp.path, real_path, force: true)
  end
end

#run(argv) ⇒ ::Integer

Parameters:

  • argv (::Array[::String])

Returns:

  • (::Integer)


35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/json/repair/cli.rb', line 35

def run(argv)
  positional = catch(:halt) { parser.parse(argv) }
  halt = @halt
  return halt if halt

  input_path = positional.first
  return 1 unless validate(positional, input_path)

  repaired = JSON.repair(read_input(input_path))
  write_output(repaired, input_path)
  0
end

#validate(positional, input_path) ⇒ Boolean

Parameters:

  • positional (::Array[::String])
  • input_path (::String, nil)

Returns:

  • (Boolean)


48
49
50
51
52
53
54
# File 'lib/json/repair/cli.rb', line 48

def validate(positional, input_path)
  error = validation_error(positional, input_path)
  return true unless error

  @stderr.puts "json-repair: #{error}"
  false
end

#validation_error(positional, input_path) ⇒ ::String?

Parameters:

  • positional (::Array[::String])
  • input_path (::String, nil)

Returns:

  • (::String, nil)


56
57
58
59
60
61
62
# File 'lib/json/repair/cli.rb', line 56

def validation_error(positional, input_path)
  return "unexpected argument: #{positional[1]}" if positional.length > 1
  return '--overwrite requires a filename' if @overwrite && input_path.nil?
  return '--overwrite and --output are mutually exclusive' if @overwrite && @output_path

  nil
end

#write_output(repaired, input_path) ⇒ void

This method returns an undefined value.

Parameters:

  • repaired (::String)
  • input_path (::String, nil)


72
73
74
75
76
77
78
79
80
# File 'lib/json/repair/cli.rb', line 72

def write_output(repaired, input_path)
  if @overwrite && input_path
    replace_in_place(input_path, repaired)
  elsif (output_path = @output_path)
    File.write(output_path, repaired)
  else
    @stdout.write(repaired, "\n")
  end
end