diff -Nru discus-0.3.0/BUGS discus-0.4.0/BUGS --- discus-0.3.0/BUGS 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/BUGS 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ - -These problems remain unfixed as of this release: - - o RedHat 6.0 Commerce with RH 6.1 Python outputs all zeros (reported by - Jerrad Pierce) - diff -Nru discus-0.3.0/debian/changelog discus-0.4.0/debian/changelog --- discus-0.3.0/debian/changelog 2020-05-09 22:17:51.000000000 +0000 +++ discus-0.4.0/debian/changelog 2020-12-23 22:48:26.000000000 +0000 @@ -1,3 +1,17 @@ +discus (0.4.0-1) unstable; urgency=medium + + * New upstream version 0.4.0. (LP: #1459032) + * debian/control: + - Added Multi-Arch: foreign; + - Bumped Standards-Version to 4.5.1. + * debian/copyright: + - Added list of files. + - Updated upstream copyright years. + * debian/docs: removed files. + * debian/tests: improved tests for autopkgtest. (Closes: #969818) + + -- Paulo Henrique de Lima Santana (phls) Wed, 23 Dec 2020 19:48:26 -0300 + discus (0.3.0-1) unstable; urgency=medium * New upstream version 0.3.0. (Closes: #945636) diff -Nru discus-0.3.0/debian/control discus-0.4.0/debian/control --- discus-0.3.0/debian/control 2020-05-09 22:17:51.000000000 +0000 +++ discus-0.4.0/debian/control 2020-12-23 22:48:26.000000000 +0000 @@ -5,7 +5,7 @@ Build-Depends: debhelper-compat (= 13), dh-python, python3, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Rules-Requires-Root: no Homepage: https://github.com/ncarrier/discus Vcs-Browser: https://salsa.debian.org/debian/discus @@ -13,6 +13,7 @@ Package: discus Architecture: all +Multi-Arch: foreign Depends: ${misc:Depends}, ${python3:Depends}, Description: pretty version of df(1) command diff -Nru discus-0.3.0/debian/copyright discus-0.4.0/debian/copyright --- discus-0.3.0/debian/copyright 2020-05-09 22:17:51.000000000 +0000 +++ discus-0.4.0/debian/copyright 2020-12-23 22:48:26.000000000 +0000 @@ -2,7 +2,16 @@ Upstream-Name: discus Source: https://github.com/ncarrier/discus -Files: * +Files: AUTHORS + README.md + changelog + discus.1 + discus.py + discusrc + tests/mtab.291276 + tests/mtab.oneline + tests/pre-commit.sh + .gitignore Copyright: 2000 Stormy Henderson 2020 Nicolas Carrier License: GPL-2+ diff -Nru discus-0.3.0/debian/docs discus-0.4.0/debian/docs --- discus-0.3.0/debian/docs 2020-05-09 20:10:44.000000000 +0000 +++ discus-0.4.0/debian/docs 2020-12-23 22:48:26.000000000 +0000 @@ -1,3 +1,2 @@ AUTHORS -README -TODO +README.md diff -Nru discus-0.3.0/debian/tests/control discus-0.4.0/debian/tests/control --- discus-0.3.0/debian/tests/control 2020-05-09 20:10:46.000000000 +0000 +++ discus-0.4.0/debian/tests/control 2020-12-23 22:48:26.000000000 +0000 @@ -1 +1,3 @@ -Test-Command: discus -h +Tests: pre-commit.sh +Depends: @, python3-pytest, parallel +Restrictions: allow-stderr diff -Nru discus-0.3.0/debian/tests/mtab.291276 discus-0.4.0/debian/tests/mtab.291276 --- discus-0.3.0/debian/tests/mtab.291276 1970-01-01 00:00:00.000000000 +0000 +++ discus-0.4.0/debian/tests/mtab.291276 2020-12-23 22:48:26.000000000 +0000 @@ -0,0 +1 @@ +/dev/sda2 /media/ACER\040UFD ext4 rw,relatime,errors=remount-ro 0 0 diff -Nru discus-0.3.0/debian/tests/mtab.oneline discus-0.4.0/debian/tests/mtab.oneline --- discus-0.3.0/debian/tests/mtab.oneline 1970-01-01 00:00:00.000000000 +0000 +++ discus-0.4.0/debian/tests/mtab.oneline 2020-12-23 22:48:26.000000000 +0000 @@ -0,0 +1 @@ +/dev/sda2 / ext4 rw,relatime,errors=remount-ro 0 0 diff -Nru discus-0.3.0/debian/tests/pre-commit.sh discus-0.4.0/debian/tests/pre-commit.sh --- discus-0.3.0/debian/tests/pre-commit.sh 1970-01-01 00:00:00.000000000 +0000 +++ discus-0.4.0/debian/tests/pre-commit.sh 2020-12-23 22:48:26.000000000 +0000 @@ -0,0 +1,20 @@ +#!/bin/bash + +here="$( cd "$( dirname $(realpath "${BASH_SOURCE[0]}") )" >/dev/null 2>&1 && pwd )" +root=$(realpath ${here}/..) + +set -xeu + +on_error() { + exit_status=$? + echo An error occurred +} + +trap on_error ERR + +# 1. runs of discus with all the possible options, to check for regressions +# 2. check the coding style +# 3. run the unittests +parallel -- "parallel ${root}/discus.py -- -h -c -d -s -t -g -m -k -v -r '' '-p 3'" \ + "pep8 ${root}/discus.py" \ + "python3 -m unittest ${root}/discus.py -v" diff -Nru discus-0.3.0/discus.1 discus-0.4.0/discus.1 --- discus-0.3.0/discus.1 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/discus.1 2020-04-13 07:05:40.000000000 +0000 @@ -27,7 +27,7 @@ size from kilobytes, megabytes, gigabytes, or terabytes). Or choose your own size, along with specifying the number of decimal places you'd like to see. You may also copy /etc/discusrc to $HOME/.discusrc and customize things to -your preference. +your preference. .SH OPTIONS .TP @@ -47,7 +47,7 @@ Do not use smart formatting. .TP .B \-t, \-g, \-m, \-k -Display sizes in terabytes, gigabytes, megabytes, or kilobytes, respectively. +Display sizes in terabytes, gigabytes, megabytes, or kilobytes, respectively. Assumes \-s. .TP .B \-v, \-\-version @@ -64,6 +64,6 @@ .BR pydf (1). .br .SH AUTHOR -This manual page was adapted by Ron Farrer from one +This manual page was adapted by Ron Farrer from one written by Stormy Henderson for the Debian GNU/Linux system (but may be used by others). diff -Nru discus-0.3.0/discus.py discus-0.4.0/discus.py --- discus-0.3.0/discus.py 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/discus.py 2020-04-13 07:05:40.000000000 +0000 @@ -24,71 +24,118 @@ import sys import re import unittest +import copy +import shutil +from collections import namedtuple opts = {"placing": True, "reserved": True} -VERSION = "0.3.0" +VERSION = "0.4.0" +MINIMUM_WIDTH = 68 +# values taken from sysexit.h +EX_OK = 0 +EX_USAGE = 64 +EX_CONFIG = 78 -class Disk: - """Contains everything needed to represent a disk textually.""" - def __init__(self, device, mount): - """ - Collect the stats when the object is created, and store them for later, - when a report is requested. - """ - self.getstats(mount) - self.device = device - self.mount = mount +class StatsFactory: + """Factory class to get statistics about a mount point.""" + Stats = namedtuple('Stats', ['total', 'free', 'used']) - def report(self): - """Generate a report, and return it as text.""" - percent = self.percentage() - total = self.format(self.total) - used = self.format(self.used) - free = self.format(self.free) + def __init__(self, reserved): + """Constructor, initialize private fields.""" + self.__reserved = reserved - # Perform a swaparoo if the user wants the device names instead - # of my pretty bar graph. - if opts["graph"]: - graph = self.graph(percent) - mount = self.trim(self.mount) + def getStats(self, mount): + """Gather statistics about specified filesystem.""" + try: + stats = os.statvfs(mount) + except PermissionError: + return StatsFactory.Stats(total="-", free="-", used="-") + + total = stats.f_blocks * stats.f_frsize + # if we have to take care of reserved space for root, then use + # available blocks (but keep counting free space with all free blocks) + if self.__reserved: + free = stats.f_bavail * stats.f_frsize else: - graph = self.trim(self.mount) - mount = self.trim(self.device) + free = stats.f_bfree * stats.f_frsize + used = total - stats.f_bfree * stats.f_frsize - # Format the result, and return it. - return color("normal") + "%-11s %12s %12s %12s %5.1f%% %s" % \ - ( - mount, - total, - used, - free, - percent, - graph - ) + color("clear") + return StatsFactory.Stats(total=total, free=free, used=used) - def format(self, size): - """Format the size for human use.""" - labels = opts["akabytes"] +class StatsFactoryTests(unittest.TestCase): + """Unit tests for the StatsFactory class""" + def test_getStatsReservedTrue(self): + """Normal use case with reserved == True.""" + factory = StatsFactory(True) + s = factory.getStats("/") + self.assertNotEqual(s.total, 0, "a size of 0 for / is unlikely") + self.assertNotEqual(s.free, 0, "0 bytes free for / is unlikely") + self.assertNotEqual(s.used, 0, "0 bytes used for / is unlikely") + + def test_getStatsReservedFalse(self): + """Normal use case with reserved == False.""" + factory = StatsFactory(False) + s = factory.getStats("/") + self.assertNotEqual(s.total, 0, "a size of 0 for / is unlikely") + self.assertNotEqual(s.free, 0, "0 bytes free for / is unlikely") + self.assertNotEqual(s.used, 0, "0 bytes used for / is unlikely") + self.assertEqual(s.total, s.free + s.used, "total != free + used") + + def test_getStatsFailure(self): + """Failure use case, non existent mount point.""" + factory = StatsFactory(False) + raised = False + try: + factory.getStats("non existent mount point") + except Exception: + raised = True + self.assertTrue(raised) + + +class SizeFormatter: + """ + Class responsible of formatting sizes, smartly or not. + if opts["smart"] is false, divisor will be used to divide the size to the + corresponding unit, that is 0 -> KB, 1 -> MB... Supposing that the size is + fed in kilo bytes. + """ + DEFAULT_AKABYTES = ["KB", "MB", "GB", "TB", "PB", "EB"] + # helper class for manipulating options + Options = namedtuple("Options", ["smart", "placing", "akabytes", "places", + "divisor"], + defaults=(True, True, DEFAULT_AKABYTES, 1, 1)) + + def __init__(self, smart, placing, akabytes, places, divisor): + """Constructor, initialize private fields.""" + self.__smart = smart + self.__placing = placing + self.__akabytes = akabytes.copy() + self.__places = places + self.__divisor = divisor # Is smart display enabled? - if opts["smart"]: - size, divisor, places = self.smart_format(size) - else: - size, divisor, places = self.manual_format(size) + self.__formatter = (self.__smart_format if self.__smart + else self.__manual_format) - # And now actually format the result. + def format(self, size): + """ + Format the size for human use. + size: size in kilobytes. + """ + size, divisor, places = self.__formatter(size) if size == 0: - result = "%d %s" % (size, labels[divisor]) - else: - result = "%%0.%df %%s" % places - result = result % (size, labels[divisor]) + places = 0 + unit = self.__akabytes[divisor] + + # And now actually format the result. + result = f"{size:0.{places}f} {unit}" return result - def smart_format(self, size): + def __smart_format(self, size): """ Use smart formatting, which increases the divisor until size is 3 or less digits in size. @@ -104,88 +151,182 @@ # Display a proportionate number of decimal places to the number of # main digits. - if not opts["placing"]: + if not self.__placing: if count < 2: fudge = count else: fudge = 2 else: # User specified how many decimal places were wanted. - fudge = opts["places"] + fudge = self.__places return size, count, fudge - def manual_format(self, size): + def __manual_format(self, size): """ We're not using smart display, so figure things up on the specified KB/MB/GB/TB basis. """ - divisor = opts["divisor"] + divisor = self.__divisor size = size / pow(1024.0, divisor) - return size, divisor, opts["places"] + return size, divisor, self.__places - def percentage(self): - """Compute the percentage of space used.""" - try: - percent = (1.0 - (1.0 * self.free / self.total)) * 100 - except ZeroDivisionError: - percent = 0.0 - return percent +class SizeFormatterTests(unittest.TestCase): + """Unit tests for the SizeFormatter class""" + + def test_manual_format(self): + """Format sizes in manual format mode.""" + # kilo bytes + opts = SizeFormatter.Options(smart=False, divisor=0) + sf = SizeFormatter(*opts) + # TODO, this is not correct, if the converted size is exact, then there + # should be no .0 part, but to implement that, we have to manipulate + # sizes internally in bytes, not in kilo bytes + self.assertEqual(sf.format(124684), "124684.0 KB", "124684 in KB fail") + # mega bytes + opts = SizeFormatter.Options(smart=False, divisor=1) + sf = SizeFormatter(*opts) + self.assertEqual(sf.format(124684), "121.8 MB", "124684 in MB fail") + # giga bytes + opts = SizeFormatter.Options(smart=False, divisor=2) + sf = SizeFormatter(*opts) + self.assertEqual(sf.format(124684), "0.1 GB", "124684 in GB fail") + # tera bytes + opts = SizeFormatter.Options(smart=False, divisor=3) + sf = SizeFormatter(*opts) + self.assertEqual(sf.format(pow(1024, 3)), "1.0 TB", "1 TB in TB fail") + self.assertEqual(sf.format(0), "0 TB", "0 TB in TB fail") + + def test_smart_format(self): + """Format sizes in smart format mode.""" + opts = SizeFormatter.Options(smart=True) + sf = SizeFormatter(*opts) + self.assertEqual(sf.format(124684), "121.8 MB", "124684 fail") + self.assertEqual(sf.format(1024), "1.0 MB", "1024 fail") + self.assertEqual(sf.format(1), "1.0 KB", "1 fail") + self.assertEqual(sf.format(0), "0 KB", "0 fail") + self.assertEqual(sf.format(999), "999.0 KB", "999 fail") + self.assertEqual(sf.format(1000), "1.0 MB", "1000 fail") + + +Mount = namedtuple('Mount', ['mount', 'device']) + + +class DiskData: + """ + Class representing a disk's data, formatted for output, in string form. + """ + Base = namedtuple('BaseDiskData', + ['percent', 'total', 'used', 'free', 'mount', 'device']) + + @staticmethod + def get(stats, percent, mount, size_formatter): + """Factory method returning a BaseDiskData instance.""" + sf = size_formatter + if not isinstance(percent, str): + percent = f"{percent:.1f}%" + total = sf.format(stats.total / 1024) + used = sf.format(stats.used / 1024) + free = sf.format(stats.free / 1024) + else: + total = stats.total + used = stats.used + free = stats.free + return DiskData.Base(percent, + total, + used, + free, + mount.mount, + mount.device) + + +class DiskDataTests(unittest.TestCase): + """Unit tests for the DiskData class""" + + def test_get(self): + """Tests for the get method.""" + sf = SizeFormatter(*SizeFormatter.Options()) + mount = Mount("/", "/dev/sda1") + d = DiskData.get(StatsFactory.Stats(1000000, 1000000 - 50000, 50000), + 5.0, + mount, + sf) + self.assertEqual(d.percent, "5.0%", "percent doesn't match") + self.assertEqual(d.total, "976.6 KB", "total doesn't match") + self.assertEqual(d.used, "48.8 KB", "used doesn't match") + self.assertEqual(d.free, "927.7 KB", "free doesn't match") + self.assertEqual(d.mount, mount.mount, "mount doesn't match") + self.assertEqual(d.device, mount.device, "device doesn't match") - def graph(self, percent): + +class Disk: + """Contains everything needed to represent a disk textually.""" + + def __init__(self, mount, stats_factory, size_formatter): + """ + Collect the stats when the object is created, and store them for later, + when a report is requested. + """ + stats = stats_factory.getStats(mount.mount) + if isinstance(stats.free, str): + self.__percent = "-" + else: + self.__percent = self.__percentage(stats.free, stats.total) + self.__data = DiskData.get(stats, self.__percent, mount, + size_formatter) + + def report(self): + """Generate a report, and return it as text.""" + d = self.__data + return [d.mount if opts["graph"] else d.device, d.total, d.used, + d.free, d.percent, + self.__percent if opts["graph"] else d.mount] + + @staticmethod + def graph(percent, width): """Format a percentage as a bar graph.""" # How many stars to place? - percent = percent / 10 - percent = round(percent) - percent = int(percent) - percent = 10 - percent + # -4 accounts for the [] and the starting space + width = width - 3 + if isinstance(percent, str): + bar_width = 0 + else: + bar_width = int(round(percent * width / 100)) # Now generate the string, using the characters in the config file. result = color("safe") graph_char = opts["graph_char"] graph_fill = opts["graph_fill"] - for counter in range(0, (10 - percent)): - if counter < 6: - result = result + graph_char - elif counter == 6: - result = result + color("warning") + graph_char - elif counter == 7: - result = result + "*" - elif counter == 8: - result = result + color("danger") + graph_char - elif counter == 9: - result = result + graph_char - - result = result + color("normal") - return "[%s%s]" % \ - ( - result, - graph_fill * percent - ) + for counter in range(1, width + 1): + if counter > bar_width: + break - def getstats(self, mount): - """Gather statistics about specified filesystem.""" - stats = os.statvfs(mount) - blocksize = int(stats.f_frsize) - self.total = int(stats.f_blocks) * (blocksize / 1024.0) - # if we have to take care of reserved space for root, then use - # available blocks (but keep counting free space with all free - # blocks) - if opts["reserved"]: - self.free = int(stats.f_bavail) * (blocksize / 1024.0) - self.used = (self.total - int(stats.f_bfree) * - (blocksize / 1024.0)) - else: - self.free = int(stats.f_bfree) * (blocksize / 1024.0) - self.used = (self.total - int(stats.f_bfree) * - (blocksize / 1024.0)) + if counter >= 0.7 * width and counter < 0.85 * width: + result = result + color("warning") + elif counter >= 0.85 * width: + result = result + color("danger") - def trim(self, text): + result = result + graph_char + + result = result + (width - counter) * graph_fill + + return " [" + color("safe") + result + color("normal") + "]" + + @staticmethod + def __percentage(free, total): + """Compute the percentage of space used.""" + if total == 0: + return 0.0 + + return 100 * (1.0 - free / total) + + @staticmethod + def trim(text, width): """Don't let long names mess up the display: shorten them.""" where = len(text) - where = where - 10 + where = where - width if where > 0: text = "+" + text[where:] @@ -194,14 +335,14 @@ def version(): """Print version.""" - print(f"Discus version {VERSION} by Nicolas Carrier " - "(carrier.nicolas0@gmail.com).") + print("Discus version {} by Nicolas Carrier " + "(carrier.nicolas0@gmail.com).".format(VERSION)) print("Home page: https://github.com/ncarrier/discus") - raise SystemExit + sys.exit(0) -def help(text=""): - """Print a help file.""" +def usage(exit_status, text=""): + """Print the help of the tool.""" # Only print the general help if no specific message is provided. if text == "": print("""Discus version %s, to display disk usage. @@ -220,16 +361,16 @@ else: print(text) - raise SystemExit + sys.exit(exit_status) def parse_options(): """Read the user's options and integrate them with the defaults.""" try: - options, args = getopt.getopt(sys.argv[1:], "p:tgmksdcrvh", - ["help", "version"]) + options, _ = getopt.getopt(sys.argv[1:], "p:tgmksdcrvh", + ["help", "version"]) except Exception: - help() + sys.exit(EX_USAGE) for option, value in options: # Display terabytes. @@ -259,13 +400,14 @@ # Display X decimal places. if option == "-p": opts["placing"] = True + fail = False try: opts["places"] = int(value) except ValueError: - help("The -p option requires a number from 0 to 9.") + fail = True - if opts["places"] < 0 or opts["places"] > 9: - help("The -p option requires a number from 0 to 9.") + if fail or opts["places"] < 0 or opts["places"] > 9: + usage(EX_USAGE, "The -p option requires a number from 0 to 9.") # Disable smart display. if option == "-s": @@ -285,17 +427,15 @@ # Display help. if option in ["-h", "--help"]: - help() + usage(EX_OK) if option == "-r": opts["reserved"] = 1 -def read_mounts(): +def read_mounts(mtab, skip_list): """Read the mounts file.""" mounts = [] - devices = [] - mtab = opts["mtab"] # If the first letter of the mtab file begins with a !, it is a # shell command to be executed, and not a file to be read. Idea @@ -304,7 +444,7 @@ mtab = subprocess.getoutput(mtab[1:]) mtab = str.split(mtab, "\n") else: - fp = open(opts["mtab"]) + fp = open(mtab) mtab = fp.readlines() fp.close() @@ -320,30 +460,29 @@ mount = mount.replace(r'\%s' % octc, chr(int(octc, 8))) # Skip entries we aren't interested in. - if mount in opts["skip_list"]: + if mount in skip_list: continue - devices.append(device) - mounts.append(mount) + mounts.append(Mount(mount, device)) + + return mounts - return devices, mounts class ReadMountsTests(unittest.TestCase): + """Tests for the read_mounts function.""" def test_simple_mtab(self): - opts["mtab"] = "tests/mtab.oneline" - opts["skip_list"] = [] - devices, mounts = read_mounts() - self.assertEqual(len(devices), 1, "one device should be detected") - self.assertEqual(devices[0], "/dev/sda2") - self.assertEqual(len(mounts), 1, "one mount point should be detected") - self.assertEqual(mounts[0], "/") + """Test with a simple mtabe consisting of only one line.""" + mounts = read_mounts("tests/mtab.oneline", []) + self.assertEqual(len(mounts), 1, "one device should be detected") + self.assertEqual(mounts[0].device, "/dev/sda2") + self.assertEqual(mounts[0].mount, "/") def test_bug_291276(self): - opts["mtab"] = "tests/mtab.291276" - opts["skip_list"] = [] - _, mounts = read_mounts() + """Test to reproduce the debian bug 291276.""" + mounts = read_mounts("tests/mtab.291276", []) self.assertEqual(len(mounts), 1, "one mount point should be detected") - self.assertEqual(mounts[0], "/media/ACER UFD") + self.assertEqual(mounts[0].mount, "/media/ACER UFD") + def color(code): """Function that return color codes if color is enabled.""" @@ -353,37 +492,69 @@ return "" -def format_header(): - """Generate a colored heading.""" +def get_header(graph): + """Generate a list of headers.""" # Has the user requested to see device names instead of a graph? - if opts["graph"]: - mount = "Mount" - graph = " Graph" + if graph: + return ["Mount", "Total", "Used", "Avail", "Prcnt", " Graph"] else: - mount = "Device" - graph = "Mount" + return ["Device", "Total", "Used", "Avail", "Prcnt", " Mount"] + + +def format_fields(f, w): + """ + Format a list of fields into one string, given a list of corresponding + widths. + """ + a = ["", ">", ">", ">", ">", ""] + return " ".join([f"{f[i]:{a[i]}{w[i]}}" for i in range(0, len(w))]) + + +def get_layout(headers, reports): + graph_column_width = 14 + widths = [11, 11, 12, 12, 8, graph_column_width] + inputs = [copy.deepcopy(headers)] + copy.deepcopy(reports) + + size = shutil.get_terminal_size((MINIMUM_WIDTH, 20)) + # limit the width to a minimum and account to the inter-column gap + columns = max(MINIMUM_WIDTH, size.columns - len(widths)) + for l in inputs: + for i, v in enumerate(l[:-1]): + if len(v) > widths[i]: + widths[i] = len(v) + + widths[-1] = columns - sum(widths[:-1]) - 10 + if widths[-1] < graph_column_width: + widths[-1] = graph_column_width + widths[0] = columns - sum(widths[1:]) - return color("header") + "%-16s%-14s%-13s%-11s%-10s%s" % \ - ( - mount, - "Total", - "Used", - "Avail", - "Prcnt", - graph - ) + color("clear") + return widths def main(): """Define main program.""" parse_options() - devices, mounts = read_mounts() - print(format_header()) - - # Create a disk object for each mount, and print a report. - for count in range(0, len(devices)): - disk = Disk(devices[count], mounts[count]) - print(disk.report()) + mounts = read_mounts(opts["mtab"], opts["skip_list"]) + headers = get_header(opts["graph"]) + stats_factory = StatsFactory(opts["reserved"]) + size_formatter = SizeFormatter(opts["smart"], opts["placing"], + opts["akabytes"], opts["places"], + opts["divisor"]) + + # Create a disk object for each mount, store its report. + reports = [Disk(m, stats_factory, size_formatter).report() for m in mounts] + + widths = get_layout(headers, reports) + print(color("header") + format_fields(headers, widths)) + for report in reports: + if opts["graph"]: + r = report[:-1] + [Disk.graph(report[-1], widths[-1])] + else: + r = report[:-1] + [" " + Disk.trim(report[-1], widths[-1] - 2)] + # trim mount field if it exceeds its alloted width + if len(r[0]) >= widths[0]: + r[0] = Disk.trim(r[0], widths[0] - 1) + print(color("normal") + format_fields(r, widths) + color("clear")) if __name__ == "__main__": @@ -394,7 +565,7 @@ exec(compile(open("/etc/discusrc", "rb").read(), "/etc/discusrc", 'exec')) except IOError: - help("/etc/discusrc must exist and be readable.") + usage(EX_CONFIG, "/etc/discusrc must exist and be readable.") try: exec(compile(open(os.environ['HOME'] + "/.discusrc", "rb").read(), diff -Nru discus-0.3.0/discusrc discus-0.4.0/discusrc --- discus-0.3.0/discusrc 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/discusrc 2020-04-13 07:05:40.000000000 +0000 @@ -40,15 +40,15 @@ black = "\033[30m" red = "\033[31m" -green = "\033[32m" +green = "\033[32m" yellow = "\033[33m" blue = "\033[34m" magenta = "\033[35m" cyan = "\033[36m" white = "\033[37m" -on_black = "\033[40m" -on_red = "\033[41m" +on_black = "\033[40m" +on_red = "\033[41m" on_green = "\033[42m" on_yellow = "\033[43m" on_blue = "\033[44m" diff -Nru discus-0.3.0/INSTALL discus-0.4.0/INSTALL --- discus-0.3.0/INSTALL 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/INSTALL 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ - -Discus requires that the following software be installed. - - o Python, at least 3.6 - -To install Discus without the benefit of a Debian package, type something like -the following as root: - -cp discus.py /usr/local/bin -chmod a+rx /usr/local/bin/discus -cp discusrc /etc -chmod a+r /etc/discusrc -gzip -9 discus.1 -cp discus.1.gz /usr/local/man/man1 -chmod a+r /usr/local/man/man1/discus.1.gz diff -Nru discus-0.3.0/README discus-0.4.0/README --- discus-0.3.0/README 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/README 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -Discus is a program to display hard drive space usage, much like the -standard UN*X command df. - -Discus aims to make df(1) prettier. Features include color, bar graphs, and -smart formatting of numbers (automatically choosing the most suitable size -from kilobytes, megabytes, gigabytes, or terabytes). Or choose your own -size, along with specifying the number of decimal places you'd like to see. - -To configure Discus on a system-wide basis, edit the /etc/discusrc file. -But you should probably change things just for yourself, by copying -/etc/discusrc to ~/.discusrc and editing that. - -The source code is contained in the discus file itself, as it is a Python -code encapsulated in a shell script. - -Yes, I chose the name Discus because of the similarity to "disk use." And -no, I didn't misspell discuss. A discus is a round thingie people throw. - -Newest versions of Discus may be found at: -https://github.com/ncarrier/discus diff -Nru discus-0.3.0/README.md discus-0.4.0/README.md --- discus-0.3.0/README.md 1970-01-01 00:00:00.000000000 +0000 +++ discus-0.4.0/README.md 2020-04-13 07:05:40.000000000 +0000 @@ -0,0 +1,112 @@ +# Discus + +## Overview + +Discus is a program to display hard drive space usage, much like the standard +UN*X command df. + +Discus aims to make df(1) prettier. +Features include color, bar graphs, and smart formatting of numbers +(automatically choosing the most suitable size from kilobytes, megabytes, +gigabytes, or terabytes). Or choose your own size, along with specifying the +number of decimal places you'd like to see. + +To configure Discus on a system-wide basis, edit the **/etc/discusrc** file. +But you should probably change things just for yourself, by copying +**/etc/discusrc** to **~/.discusrc** and editing that. + +The source code is contained in the discus.py file itself, as it is a Python +code encapsulated in a shell script. + +Stormy Henderson, the original author, said: + +> Yes, I chose the name Discus because of the similarity to "disk use." +> And no, I didn't misspell discuss. +> A discus is a round thingie people throw. + +Newest versions of Discus may be found at: +https://github.com/ncarrier/discus + +## Dependencies + +Python 3.6 or above. + +## Usage + +Usually: + +``` +./discus.py +``` + +should be sufficient. + +Please do: + +``` +./discus.py --help +``` + +for more information. + +## Installation + +``` +cp discus.py /usr/local/bin +chmod a+rx /usr/local/bin/discus +cp discusrc /etc +chmod a+r /etc/discusrc +gzip -9 discus.1 +cp discus.1.gz /usr/local/man/man1 +chmod a+r /usr/local/man/man1/discus.1.gz +``` + +## Test + +### Unit tests + +Only a few unit tests exist at the time of writing, but one has to start +somewhere :) + +``` +python3 -m unittest discus.py -v +``` + +### Pre-commit tests + +The `tests/pre-commit.sh` script allows to perform tests prior of committing. +You can run it directly or even better, install it as a git hook script by +running: + +``` +ln -s ../../tests/pre-commit.sh .git/hooks/pre-commit +``` + +## Coding style + +The source code follows the PEP8 coding style, which can be checked with, for +example, the `pep8` or the `flake8` command-line tools in debian. + +## Known bugs + +These problems remain unfixed as of this release: + + * RedHat 6.0 Commerce with RH 6.1 Python outputs all zeros (reported by + Jerrad Pierce) + * The known bugs list hasn't been checked :) + +## To do + +Simple things I'm considering adding to Discus: + + * Adapt the width to the terminal's display. + * Choose your own column labels. + * Compact option to squeeze in everything including device name. + * A Makefile for non-Debian users, or rather, setuptools support. + +Complicated things I'm considering adding to Discus: + + * Add du(1) functionality to combine both disk usage functions into + one software package. + +Want your wish added? Please open an issue. diff -Nru discus-0.3.0/tests/pre-commit.sh discus-0.4.0/tests/pre-commit.sh --- discus-0.3.0/tests/pre-commit.sh 1970-01-01 00:00:00.000000000 +0000 +++ discus-0.4.0/tests/pre-commit.sh 2020-04-13 07:05:40.000000000 +0000 @@ -0,0 +1,20 @@ +#!/bin/bash + +here="$( cd "$( dirname $(realpath "${BASH_SOURCE[0]}") )" >/dev/null 2>&1 && pwd )" +root=$(realpath ${here}/..) + +set -xeu + +on_error() { + exit_status=$? + echo An error occurred +} + +trap on_error ERR + +# 1. runs of discus with all the possible options, to check for regressions +# 2. check the coding style +# 3. run the unittests +parallel -- "parallel ${root}/discus.py -- -h -c -d -s -t -g -m -k -v -r '' '-p 3'" \ + "pep8 ${root}/discus.py" \ + "python3 -m unittest ${root}/discus.py -v" diff -Nru discus-0.3.0/TODO discus-0.4.0/TODO --- discus-0.3.0/TODO 2020-03-27 06:39:14.000000000 +0000 +++ discus-0.4.0/TODO 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ - -Simple things I'm considering adding to Discus: - - o Choose your own column labels. - o Compact option to squeeze in everything including device name. - o A Makefile for non-Debian users. - - -Complicated things I'm considering adding to Discus: - - o Add du(1) functionality to combine both disk usage functions into - one software package. - - -Want your wish added? Email me!