Skip to content

Commit f346dd2

Browse files
authored
Merge pull request #418 from nexB/388-417-consistent-stderr-stdout-outputs
#388 and #417 Ensure that a --quiet run is really quiet
2 parents 538648e + 80991cd commit f346dd2

File tree

4 files changed

+101
-48
lines changed

4 files changed

+101
-48
lines changed

src/scancode/cli.py

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ def wrap(self, timeout=None):
9292

9393
from scancode import utils
9494

95+
echo_stderr = partial(click.secho, err=True)
96+
9597

9698
info_text = '''
9799
ScanCode scans code and other files for origin and license.
@@ -189,12 +191,12 @@ def print_about(ctx, param, value):
189191
190192
scancode --format html samples/zlib scancode_result.html
191193
192-
Scan a single file for copyrights. Print scan results on terminal as JSON:
194+
Scan a single file for copyrights. Print scan results to stdout as JSON:
193195
194196
scancode --copyright samples/zlib/zlib.h
195197
196-
Scan a single file for licenses, print verbose progress on terminal as each file
197-
is scanned. Save scan to a JSON file:
198+
Scan a single file for licenses, print verbose progress to stderr as each
199+
file is scanned. Save scan to a JSON file:
198200
199201
scancode --license --verbose samples/zlib/zlib.h licenses.json
200202
@@ -257,6 +259,7 @@ def validate_formats(ctx, param, value):
257259
raise click.BadParameter('Invalid template file: "%(value)s" does not exists or is not readable.' % locals())
258260
return value
259261

262+
260263
@click.command(name='scancode', epilog=epilog_text, cls=ScanCommand)
261264
@click.pass_context
262265

@@ -277,16 +280,16 @@ def validate_formats(ctx, param, value):
277280
'or the path to a custom template' % ' or '.join(formats)),
278281
callback=validate_formats)
279282
@click.option('--verbose', is_flag=True, default=False, help='Print verbose file-by-file progress messages.')
280-
@click.option('--quiet', is_flag=True, default=False, help='Do not print progress messages.')
283+
@click.option('--quiet', is_flag=True, default=False, help='Do not print summary or progress messages.')
281284
@click.option('-n', '--processes', is_flag=False, default=1, type=int, show_default=True, help='Scan <input> using n parallel processes.')
282285

283286
@click.help_option('-h', '--help')
284287
@click.option('--examples', is_flag=True, is_eager=True, callback=print_examples, help=('Show command examples and exit.'))
285288
@click.option('--about', is_flag=True, is_eager=True, callback=print_about, help='Show information about ScanCode and licensing and exit.')
286289
@click.option('--version', is_flag=True, is_eager=True, callback=print_version, help='Show the version and exit.')
287-
@click.option('--diag', is_flag=True, default=False, help='Include detailed diagnostic messages in results if there are scanning errors.')
288-
@click.option('--timeout', is_flag=False, default=DEFAULT_TIMEOUT, type=int, show_default=True, help='Stop scanning a file if it takes longer than a timeout in seconds.')
289-
@click.option('--max-memory', is_flag=False, default=DEFAULT_MAX_MEMORY, type=int, show_default=True, help='Stop scanning a file if it its scan requires more than a maximum amount of memory in megabytes.')
290+
@click.option('--diag', is_flag=True, default=False, help='Include additional diagnostic information such as error messages or result details.')
291+
@click.option('--timeout', is_flag=False, default=DEFAULT_TIMEOUT, type=int, show_default=True, help='Stop scanning a file if scanning takes longer than a timeout in seconds.')
292+
@click.option('--max-memory', is_flag=False, default=DEFAULT_MAX_MEMORY, type=int, show_default=True, help='Stop scanning a file if scanning requires more than a maximum amount of memory in megabytes.')
290293

291294
def scancode(ctx, input, output_file, copyright, license, package,
292295
email, url, info, license_score, format,
@@ -295,7 +298,8 @@ def scancode(ctx, input, output_file, copyright, license, package,
295298
*args, **kwargs):
296299
"""scan the <input> file or directory for origin clues and license and save results to the <output_file>.
297300
298-
The scan results are printed on terminal if <output_file> is not provided.
301+
The scan results are printed to stdout if <output_file> is not provided.
302+
Error and progress is printed to stderr.
299303
"""
300304
possible_scans = [copyright, license, package, email, url, info]
301305
# Default scan when no options is provided
@@ -306,26 +310,25 @@ def scancode(ctx, input, output_file, copyright, license, package,
306310

307311
scans_cache_class = get_scans_cache_class()
308312
try:
309-
to_stdout = output_file == sys.stdout
310313
files_count, results = scan(input, copyright, license, package, email, url, info, license_score,
311-
verbose, quiet, processes, scans_cache_class, to_stdout,
314+
verbose, quiet, processes, scans_cache_class,
312315
diag, timeout, max_memory)
313-
click.secho('Saving results.', err=to_stdout, fg='green')
316+
if not quiet:
317+
echo_stderr('Saving results.', fg='green')
314318
save_results(files_count, results, format, input, output_file)
315319
finally:
316320
# cleanup
317321
cache = scans_cache_class()
318322
cache.clear()
319323

320-
# TODO: addproper return code
324+
# TODO: add proper return code
321325
# rc = 1 if has__errors else 0
322326
# ctx.exit(rc)
323327

324328

325329
def scan(input_path, copyright=True, license=True, package=True,
326330
email=False, url=False, info=True, license_score=0,
327-
verbose=False, quiet=False, processes=1,
328-
scans_cache_class=None, to_stdout=False,
331+
verbose=False, quiet=False, processes=1, scans_cache_class=None,
329332
diag=False, timeout=DEFAULT_TIMEOUT, max_memory=DEFAULT_MAX_MEMORY):
330333
"""
331334
Return a tuple of (file_count, indexing_time, scan_results) where
@@ -354,19 +357,22 @@ def scan(input_path, copyright=True, license=True, package=True,
354357
scans = info and ['infos'] or []
355358
scans.extend([k for k, v in scanners.items() if v])
356359
_scans = ', '.join(scans)
357-
click.secho('Scanning files for: %(_scans)s with %(processes)d process(es)...' % locals(), err=to_stdout)
360+
if not quiet:
361+
echo_stderr('Scanning files for: %(_scans)s with %(processes)d process(es)...' % locals())
358362

359363
scan_summary['scans'] = scans[:]
360364
scan_start = time()
361365
indexing_time = 0
362366
if license:
363367
# build index outside of the main loop
364368
# this also ensures that forked processes will get the index on POSIX naturally
365-
click.secho('Building license detection index...', err=to_stdout, fg='green', nl=False)
369+
if not quiet:
370+
echo_stderr('Building license detection index...', fg='green', nl=False)
366371
from licensedcode.index import get_index
367372
_idx = get_index()
368373
indexing_time = time() - scan_start
369-
click.secho('Done.', err=to_stdout, fg='green', nl=True)
374+
if not quiet:
375+
echo_stderr('Done.', fg='green', nl=True)
370376

371377
scan_summary['indexing_time'] = indexing_time
372378

@@ -391,10 +397,13 @@ def scan(input_path, copyright=True, license=True, package=True,
391397
scanned_files = pool.imap_unordered(scanit, logged_resources, chunksize=1)
392398
pool.close()
393399

394-
click.secho('Scanning files...', err=to_stdout, fg='green')
400+
if not quiet:
401+
echo_stderr('Scanning files...', fg='green')
395402

396403
def scan_event(item):
397404
"""Progress event displayed each time a file is scanned"""
405+
if quiet:
406+
return ''
398407
if item:
399408
_scan_success, _scanned_path = item
400409
_progress_line = verbose and _scanned_path or fileutils.file_name(_scanned_path)
@@ -403,7 +412,8 @@ def scan_event(item):
403412
scanning_errors = []
404413
files_count = 0
405414
with utils.progressmanager(scanned_files, item_show_func=scan_event,
406-
show_pos=True, verbose=verbose, quiet=quiet) as scanned:
415+
show_pos=True, verbose=verbose, quiet=quiet,
416+
file=sys.stderr) as scanned:
407417
while True:
408418
try:
409419
result = scanned.next()
@@ -422,6 +432,8 @@ def scan_event(item):
422432
# http://bugs.python.org/issue15101
423433
pool.terminate()
424434

435+
# TODO: add stats to results somehow
436+
425437
# Compute stats
426438
##########################
427439
scan_summary['files_count'] = files_count
@@ -434,19 +446,20 @@ def scan_event(item):
434446
files_scanned_per_second = round(float(files_count) / scanning_time , 2)
435447
scan_summary['files_scanned_per_second'] = files_scanned_per_second
436448

437-
# Display stats
438-
##########################
439-
click.secho('Scanning done.', fg=scanning_errors and 'red' or 'green', err=to_stdout)
440-
if scanning_errors:
441-
click.secho('Some files failed to scan properly. See scan for details:', fg='red', err=to_stdout)
442-
for errored_path in scanning_errors:
443-
click.secho(' ' + errored_path, fg='red', err=to_stdout)
444-
445-
click.secho('Scan statistics: %(files_count)d files scanned in %(total_time)ds.' % locals(), err=to_stdout)
446-
click.secho('Scan options: %(_scans)s with %(processes)d process(es).' % locals(), err=to_stdout)
447-
click.secho('Scanning speed: %(files_scanned_per_second)s files per sec.' % locals(), err=to_stdout)
448-
click.secho('Scanning time: %(scanning_time)ds.' % locals(), err=to_stdout)
449-
click.secho('Indexing time: %(indexing_time)ds.' % locals(), err=to_stdout, reset=True)
449+
if not quiet:
450+
# Display stats
451+
##########################
452+
echo_stderr('Scanning done.', fg=scanning_errors and 'red' or 'green')
453+
if scanning_errors:
454+
echo_stderr('Some files failed to scan properly. See scan for details:', fg='red')
455+
for errored_path in scanning_errors:
456+
echo_stderr(' ' + errored_path, fg='red')
457+
458+
echo_stderr('Scan statistics: %(files_count)d files scanned in %(total_time)ds.' % locals())
459+
echo_stderr('Scan options: %(_scans)s with %(processes)d process(es).' % locals())
460+
echo_stderr('Scanning speed: %(files_scanned_per_second)s files per sec.' % locals())
461+
echo_stderr('Scanning time: %(scanning_time)ds.' % locals())
462+
echo_stderr('Indexing time: %(indexing_time)ds.' % locals(), reset=True)
450463

451464
# finally return an iterator on cached results
452465
scan_names = []
@@ -585,22 +598,25 @@ def save_results(files_count, scanned_files, format, input, output_file):
585598
"""
586599
Save scan results to file or screen.
587600
"""
588-
if output_file != sys.stdout:
601+
# note: in tests, sys.sdtout is not used, but some io wrapper with no name attributes
602+
is_real_file = hasattr(output_file, 'name')
603+
604+
if output_file != sys.stdout and is_real_file:
589605
parent_dir = os.path.dirname(output_file.name)
590606
if parent_dir:
591607
fileutils.create_dir(abspath(expanduser(parent_dir)))
592608

593609
if format and format not in formats:
594610
# render using a user-provided custom format template
595611
if not os.path.isfile(format):
596-
click.secho('\nInvalid template passed.', err=True, fg='red')
612+
echo_stderr('\nInvalid template passed.', fg='red')
597613
else:
598614
for template_chunk in as_template(scanned_files, template=format):
599615
try:
600616
output_file.write(template_chunk)
601617
except Exception as e:
602618
extra_context = 'ERROR: Failed to write output to HTML for: ' + repr(template_chunk)
603-
click.secho(extra_context, err=True, fg='red')
619+
echo_stderr(extra_context, fg='red')
604620
e.args += (extra_context,)
605621
raise e
606622

@@ -610,7 +626,7 @@ def save_results(files_count, scanned_files, format, input, output_file):
610626
output_file.write(template_chunk)
611627
except Exception as e:
612628
extra_context = 'ERROR: Failed to write output to HTML for: ' + repr(template_chunk)
613-
click.secho(extra_context, err=True, fg='red')
629+
echo_stderr(extra_context, fg='red')
614630
e.args += (extra_context,)
615631
raise e
616632

@@ -619,9 +635,9 @@ def save_results(files_count, scanned_files, format, input, output_file):
619635
try:
620636
create_html_app_assets(scanned_files, output_file)
621637
except HtmlAppAssetCopyWarning:
622-
click.secho('\nHTML app creation skipped when printing to terminal.', err=True, fg='yellow')
638+
echo_stderr('\nHTML app creation skipped when printing to stdout.', fg='yellow')
623639
except HtmlAppAssetCopyError:
624-
click.secho('\nFailed to create HTML app.', err=True, fg='red')
640+
echo_stderr('\nFailed to create HTML app.', fg='red')
625641

626642
elif format == 'json':
627643
meta = OrderedDict()

src/scancode/extract_cli.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from __future__ import absolute_import
2727
from __future__ import unicode_literals
2828

29+
from functools import partial
2930
import os
3031

3132
import click
@@ -39,10 +40,13 @@
3940
from scancode import utils
4041

4142

43+
echo_stderr = partial(click.secho, err=True)
44+
45+
4246
def print_version(ctx, param, value):
4347
if not value or ctx.resilient_parsing:
4448
return
45-
click.secho('ScanCode extractcode version ' + version)
49+
echo_stderr('ScanCode extractcode version ' + version)
4650
ctx.exit()
4751

4852

@@ -80,7 +84,7 @@ class ExtractCommand(utils.BaseCommand):
8084
@click.argument('input', metavar='<input>', type=click.Path(exists=True, readable=True))
8185

8286
@click.option('--verbose', is_flag=True, default=False, help='Print verbose file-by-file progress messages.')
83-
@click.option('--quiet', is_flag=True, default=False, help='Do not print any progress message.')
87+
@click.option('--quiet', is_flag=True, default=False, help='Do not print any summary or progress message.')
8488

8589
@click.help_option('-h', '--help')
8690
@click.option('--about', is_flag=True, is_eager=True, callback=print_about, help='Show information about ScanCode and licensing and exit.')
@@ -100,6 +104,8 @@ def extract_event(item):
100104
"""
101105
Display an extract event.
102106
"""
107+
if quiet:
108+
return ''
103109
if not item:
104110
return ''
105111
if verbose:
@@ -123,26 +129,27 @@ def display_extract_summary():
123129
source = fileutils.as_posixpath(xev.source)
124130
source = utils.get_relative_path(path=source, len_base_path=len_base_path, base_is_dir=base_is_dir)
125131
for e in xev.errors:
126-
click.secho('ERROR extracting: %(source)s: %(e)r' % locals(), fg='red', err=not verbose)
132+
echo_stderr('ERROR extracting: %(source)s: %(e)r' % locals(), fg='red')
127133
for warn in xev.warnings:
128-
click.secho('WARNING extracting: %(source)s: %(warn)r' % locals(), fg='yellow', err=not verbose)
134+
echo_stderr('WARNING extracting: %(source)s: %(warn)r' % locals(), fg='yellow')
129135

130136
summary_color = 'green'
131137
if has_warnings:
132138
summary_color = 'yellow'
133139
if has_errors:
134140
summary_color = 'red'
135141

136-
click.secho('Extracting done.', fg=summary_color, err=not verbose, reset=True)
142+
echo_stderr('Extracting done.', fg=summary_color, reset=True)
143+
137144

138145
# use for relative paths computation
139146
len_base_path = len(abs_location)
140147
base_is_dir = filetype.is_dir(abs_location)
141148

142149
extract_results = []
143150
has_extract_errors = False
144-
145-
click.secho('Extracting archives...', fg='green', err=not verbose)
151+
if not quiet:
152+
echo_stderr('Extracting archives...', fg='green')
146153

147154
with utils.progressmanager(extract_archives(abs_location), item_show_func=extract_event,
148155
verbose=verbose, quiet=quiet) as extraction_events:
@@ -151,7 +158,8 @@ def display_extract_summary():
151158
has_extract_errors = has_extract_errors or xev.errors
152159
extract_results.append(xev)
153160

154-
display_extract_summary()
161+
if not quiet:
162+
display_extract_summary()
155163

156164
rc = 1 if has_extract_errors else 0
157165
ctx.exit(rc)

src/scancode/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2015 nexB Inc. and others. All rights reserved.
2+
# Copyright (c) 2016 nexB Inc. and others. All rights reserved.
33
# http://nexb.com and https://github.com/nexB/scancode-toolkit/
44
# The ScanCode software is licensed under the Apache License version 2.0.
55
# Data generated with ScanCode require an acknowledgment.
@@ -84,7 +84,7 @@ def __init__(self, *args, **kwargs):
8484

8585
class ProgressLogger(ProgressBar):
8686
"""
87-
A subclass of Click ProgressBar providing a verbose line- by-line progress
87+
A subclass of Click ProgressBar providing a verbose line-by-line progress
8888
reporting.
8989
9090
In contrast with the progressbar the label, percent, ETA, pos, bar_template

tests/scancode/test_cli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,32 @@ def test_scan_can_handle_licenses_with_unicode_metadata(monkeypatch):
464464
result = runner.invoke(cli.scancode, ['--license', test_dir, result_file], catch_exceptions=True)
465465
assert result.exit_code == 0
466466
assert 'Scanning done' in result.output
467+
468+
469+
def test_scan_quiet_to_file_does_not_echo_anything(monkeypatch):
470+
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True)
471+
test_dir = test_env.extract_test_tar('info/basic.tgz')
472+
runner = CliRunner()
473+
result1_file = test_env.get_temp_file('json')
474+
result1 = runner.invoke(cli.scancode, ['--quiet', '--info', test_dir, result1_file], catch_exceptions=True)
475+
assert result1.exit_code == 0
476+
assert not result1.output
477+
478+
479+
def test_scan_quiet_to_stdout_only_echoes_json_results(monkeypatch):
480+
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True)
481+
test_dir = test_env.extract_test_tar('info/basic.tgz')
482+
runner = CliRunner()
483+
result1_file = test_env.get_temp_file('json')
484+
result1 = runner.invoke(cli.scancode, ['--quiet', '--info', test_dir, result1_file], catch_exceptions=True)
485+
assert result1.exit_code == 0
486+
assert not result1.output
487+
488+
# also test with an output of JSON to stdout
489+
runner2 = CliRunner()
490+
result2 = runner2.invoke(cli.scancode, ['--quiet', '--info', test_dir], catch_exceptions=False)
491+
assert result2.exit_code == 0
492+
493+
# outputs to file or stdout should be identical
494+
result1_output = open(result1_file).read()
495+
assert result1_output == result2.output

0 commit comments

Comments
 (0)