pax_global_header00006660000000000000000000000064143607320520014514gustar00rootroot0000000000000052 comment=110976b594c77d534c04770a1a0d24a62443711e discus-0.5.0/000077500000000000000000000000001436073205200130105ustar00rootroot00000000000000discus-0.5.0/.gitignore000066400000000000000000000000141436073205200147730ustar00rootroot00000000000000__pycache__ discus-0.5.0/AUTHORS000066400000000000000000000004141436073205200140570ustar00rootroot00000000000000Current Author ---------- Nicolas Carrier (carrier.nicolas0@gmail.com) Main Author ----------- Stormy Henderson (stormy@futuresouth.com or stormy@raincrazy.com) Contributors ------------ John Soward is helping with the FreeBSD port. Aaron Marasco fixed a color bug. discus-0.5.0/COPYING000066400000000000000000000431051436073205200140460ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. discus-0.5.0/README.md000066400000000000000000000055001436073205200142670ustar00rootroot00000000000000# Discus ## Overview Discus is a program to display hard drive space usage, much like the standard UNIX 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 ``` ## Development ### Required packages Command to install development packages for debian 10 and 11: ``` sudo apt install flake8 moreutils ``` ### Unit tests Only a few unit tests exist at the time of writing, but one has to start somewhere :) ``` PYTHONPATH=. python3 -m unittest tests.unit_tests -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 `flake8` or the `pep8` 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: * A cleaner config file format, using argparse, deprecating the former discusrc file format. * Configure bash auto-completion. * 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. discus-0.5.0/changelog000066400000000000000000000025741436073205200146720ustar00rootroot00000000000000VERSION 0.5.0 ----- o Fix for issue https://github.com/ncarrier/discus/issues/1 o A discusrc file isn't mandatory anymore VERSION 0.4.0 ----- March 13th, 2020 o More standard return statuses, 64, for usage error, 78 for config o Display width adapted to the terminal size o Ignore silently statvfs permission errors VERSION 0.3.0 ----- March 27th, 2020 o Python 3 compatibility. o dropped support for the stat_prog configuration option VERSION 0.2.9 ----- September 15th, 2000 o Fixed a color bug (thanks to Aaron Marasco). o Added mention of the configuration files in the README. VERSION 0.2.8 ----- September 15th, 2000 o Colors may be altered in the configuration file. VERSION 0.2.7 ----- September 13th, 2000 o May define your own graph characters in config file. o Fixed bug with reserved space that caused falsely inflated disk space usage to be reported. VERSION 0.2.4 ----- September 13th, 2000 o Added /etc/discusrc and $HOME/.discusrc configuration files. o Will fall back to using the external stat program if the statvfs module is unavailable. o May use shell commands to obtain mounts information. An example is provided in the config file using /bin/mount with awk. o May specify your own labels for KB/MB/GB/TB in config file. VERSION 0.2.2 ----- September 12th, 2000 o First public release. discus-0.5.0/discus.1000066400000000000000000000036421436073205200143710ustar00rootroot00000000000000.\" Hey, EMACS: -*- nroff -*- .\" DISCUS .\" 1 .\" other parameters are allowed: see man(7), man(1) .TH DISCUS 1 "October 20, 2003" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME discus \- print a report of disk space usage .SH SYNOPSIS .B discus .RI [ options ] .br .SH DESCRIPTION \fBdiscus\fP 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. You may also copy /etc/discusrc to $HOME/.discusrc and customize things to your preference. .SH OPTIONS .TP .B \-h, \-\-help Show summary of options. .TP .B \-c Disable color. .TP .B \-d Display device names instead of graphs. .TP .B \-p Number of digits to right of decimal place. .TP .B \-s Do not use smart formatting. .TP .B \-t, \-g, \-m, \-k Display sizes in terabytes, gigabytes, megabytes, or kilobytes, respectively. Assumes \-s. .TP .B \-v, \-\-version Show version of program. .TP .B \-r Takes into account even the reserved space to root; it will be counted in percentage and in available columns only (Used is for real used space). .SH FILES .SH FILES .BR /etc/discusrc, .BR $HOME/.discusrc .SH SEE ALSO .BR df (1), .BR pydf (1). .br .SH AUTHOR 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). discus-0.5.0/discus.py000077500000000000000000000375061436073205200146720ustar00rootroot00000000000000#!/usr/bin/python3 # Discus is a program that reports hard drive space usage. # Copyright 2000 Stormy Henderson (stormy@futuresouth.com). # Copyright 2019-2021 Nicolas Carrier (carrier.nicolas0@gmail.com). # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307 # USA. import subprocess import os import sys import re import copy import shutil from collections import namedtuple import argparse from argparse import RawTextHelpFormatter # These colors should work on VT100-type displays and can be overridden by the # user. black = "\033[30m" red = "\033[31m" 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_green = "\033[42m" on_yellow = "\033[43m" on_blue = "\033[44m" on_magenta = "\033[45m" on_cyan = "\033[46m" on_white = "\033[47m" normal = "\033[0m" bold = "\033[1m" underline = "\033[4m" blink = "\033[5m" reverse = "\033[7m" dim = "\033[8m" opts = { "placing": True, "reserved": True, # Labels to display next to numbers. "akabytes": ["KB", "MB", "GB", "TB", "PB", "EB"], "color": 1, # Power of 1024, smallest value is 0, for KB, used only when smart # formatting is disabled, overridden by -t, -g, -m and -k options "divisor": 1, "graph": 1, "graph_char": "*", "graph_fill": "-", # Example mtab entry that uses a shell command (always use a ! as # first character) rather than a file: # !/bin/mount |awk '{print $1, $3}'" "mtab": "/etc/mtab", # Number of decimal places to display, same as -p "places": 1, # Filesystems to ignore. "skip_list": ["/dev/pts", "/proc", "/dev", "/proc/usb", "/sys"], # Use smart formatting of numbers. "smart": 1, "color_header": blue, "color_normal": normal, "color_safe": normal, # 0%- 70% full "color_warning": bold + yellow, # 70%- 85% full "color_danger": bold + red # 85%-100% full } HOMEPAGE = "https://github.com/ncarrier/discus" VERSION = "0.5.0" MINIMUM_WIDTH = 68 # values taken from sysexit.h EX_OK = 0 EX_USAGE = 64 EX_CONFIG = 78 class StatsFactory: """Factory class to get statistics about a mount point.""" Stats = namedtuple('Stats', ['total', 'free', 'used']) def __init__(self, reserved): """Constructor, initialize private fields.""" self.__reserved = reserved 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: free = stats.f_bfree * stats.f_frsize used = total - stats.f_bfree * stats.f_frsize return StatsFactory.Stats(total=total, free=free, used=used) 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"]) Options.__new__.__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? self.__formatter = (self.__smart_format if self.__smart else self.__manual_format) def format(self, size): """ Format the size for human use. size: size in kilobytes. """ size, divisor, places = self.__formatter(size) if size == 0: 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): """ Use smart formatting, which increases the divisor until size is 3 or less digits in size. """ # Keep reducing digits until there are 3 or less. count = 0 while size > (0.9999999999 * pow(10, 3)): # But don't let it get too small, either. if (size / 1024.0) < 0.05: break size = size / 1024.0 count = count + 1 # Display a proportionate number of decimal places to the number of # main digits. if not self.__placing: if count < 2: fudge = count else: fudge = 2 else: # User specified how many decimal places were wanted. fudge = self.__places return size, count, fudge def __manual_format(self, size): """ We're not using smart display, so figure things up on the specified KB/MB/GB/TB basis. """ divisor = self.__divisor size = size / pow(1024.0, divisor) return size, divisor, self.__places 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 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? # -4 accounts for the [] and the starting space width = width - 4 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, bar_width): if counter >= 0.7 * width and counter < 0.85 * width: result = result + color("warning") elif counter >= 0.85 * width: result = result + color("danger") result = result + graph_char result = result + (width - bar_width) * 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 - width if where > 0: text = "+" + text[where:] return text def parse_options(args=sys.argv[1:]): """""" parser = argparse.ArgumentParser(description=f"Discus version {VERSION}, " "to display disk usage.", formatter_class=RawTextHelpFormatter) parser.add_argument("-d", "--device", action="store_true", default=False, help="show device instead of graph") parser.add_argument("-c", "--color", action="store_false", default=True, help="disable color") parser.add_argument("-g", "--gigabytes", action="store_const", const=2, dest="divisor", help="display sizes in gigabytes") parser.add_argument("-k", "--kilobytes", action="store_const", const=0, dest="divisor", help="display sizes in kilobytes") parser.add_argument("-m", "--megabytes", action="store_const", const=1, dest="divisor", help="display sizes in megabytes") parser.add_argument("-p", "--places", type=int, choices=range(0, 10), help="number of digits to right of decimal place") parser.add_argument("-r", "--reserved", action="store_true", default=False, help="count reserved space as used") parser.add_argument("-s", "--smart", action="store_false", default=True, help="do not use smart formatting") parser.add_argument("-t", "--terabytes", action="store_const", const=3, dest="divisor", help="display sizes in terabytes") parser.add_argument("-v", "--version", action="version", version=(f"Discus version {VERSION} by Nicolas " "Carrier (carrier.nicolas0@gmail.com)\n" f"Home page: {HOMEPAGE}")) return parser.parse_args(args) def interpret_options(o): opts["smart"] = 1 if o.smart else 0 if o.divisor is not None: if o.places is None: opts["places"] = o.divisor opts["divisor"] = o.divisor opts["smart"] = 0 if o.places is not None: opts["placing"] = True opts["places"] = o.places opts["graph"] = 0 if o.device else 1 opts["color"] = 1 if o.color else 0 opts["reserved"] = 1 if o.reserved else 0 def read_mounts(mtab, skip_list): """Read the mounts file.""" mounts = [] # 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 # provided by John Soward. if mtab[0] == "!": mtab = subprocess.getoutput(mtab[1:]) mtab = str.split(mtab, "\n") else: fp = open(mtab) mtab = fp.readlines() fp.close() # Extract the mounted filesystems from the read file. for entry in mtab: entry = str.split(entry) device = entry[0] mount = entry[1] # Sandro Tosi - to fix Debian bug 291276, convert escaped octal values # from /etc/mtab (or /proc/mounts) to real characters for octc in re.findall(r'\\(\d{3})', mount): mount = mount.replace(r'\%s' % octc, chr(int(octc, 8))) # Skip entries we aren't interested in. if mount in skip_list: continue mounts.append(Mount(mount, device)) return mounts def color(code): """Function that return color codes if color is enabled.""" if opts["color"]: return opts["color_" + code] return "" def get_header(graph): """Generate a list of headers.""" # Has the user requested to see device names instead of a graph? if graph: return ["Mount", "Total", "Used", "Avail", "Prcnt", " Graph"] else: 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] data = [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 datum in data: for i, v in enumerate(datum[:-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 widths def main(): """Define main program.""" options = parse_options() interpret_options(options) 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__": # Before starting, we try to load the configuration files which may # override global objects' values. # First the global /etc file, then the user's file, if it exists. try: exec(compile(open("/etc/discusrc", "rb").read(), "/etc/discusrc", 'exec')) except IOError: pass try: exec(compile(open(os.environ['HOME'] + "/.discusrc", "rb").read(), os.environ['HOME'] + "/.discusrc", 'exec')) except IOError: pass # Add internal color setting. opts["color_clear"] = normal if "stat_prog" in opts: print("support for stat_prog option has been removed in 0.3.0", file=sys.stderr) main() discus-0.5.0/discusrc000066400000000000000000000040431436073205200145530ustar00rootroot00000000000000 ## Discus configuration file. ## ## To make personal modifications, copy this file to your home directory ## with the name .discusrc ## An option is enabled if its value is 1, disabled if 0. Multiple entries, ## where applicable, must be enclosed in square brackets and separated with ## commas. String values must be quoted. ## Labels to display next to numbers. Thanks to Frank Elsner. opts["akabytes"] = ["KB", "MB", "GB", "TB", "PB", "EB"] opts["color"] = 1 # Power of 1024, smallest value is 0, for KB, used only when smart formatting # is disabled, overridden by -t, -g, -m and -k options opts["divisor"] = 1 opts["graph"] = 1 opts["graph_char"] = "*" opts["graph_fill"] = "-" ## Example mtab entry that uses a shell command (always use a ! as ## first character) rather than a file: ## opts["mtab"] = "!/bin/mount |awk '{print $1, $3}'" opts["mtab"] = "/etc/mtab" ## Number of decimal places to display, same as -p opts["places"] = 1 ## Filesystems to ignore. opts["skip_list"] = ["/dev/pts", "/proc", "/dev", "/proc/usb", "/sys"] ## Use smart formatting of numbers. opts["smart"] = 1 ## Location of stat program in lieu of Python's statvfs module. # Deprecated starting from version 0.3.0 #opts["stat_prog"] = "stat -ft" ## These colors should work on VT100-type displays. Change them if you use ## something else. black = "\033[30m" red = "\033[31m" 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_green = "\033[42m" on_yellow = "\033[43m" on_blue = "\033[44m" on_magenta = "\033[45m" on_cyan = "\033[46m" on_white = "\033[47m" normal = "\033[0m" bold = "\033[1m" underline = "\033[4m" blink = "\033[5m" reverse = "\033[7m" dim = "\033[8m" opts["color_header"] = blue opts["color_normal"] = normal opts["color_safe"] = normal ## 0%- 70% full opts["color_warning"] = bold + yellow ## 70%- 85% full opts["color_danger"] = bold + red ## 85%-100% full ## EOF discus-0.5.0/tests/000077500000000000000000000000001436073205200141525ustar00rootroot00000000000000discus-0.5.0/tests/__init__.py000066400000000000000000000000001436073205200162510ustar00rootroot00000000000000discus-0.5.0/tests/issues/000077500000000000000000000000001436073205200154655ustar00rootroot00000000000000discus-0.5.0/tests/issues/1/000077500000000000000000000000001436073205200156255ustar00rootroot00000000000000discus-0.5.0/tests/issues/1/Dockerfile000066400000000000000000000002121436073205200176120ustar00rootroot00000000000000FROM ubuntu:bionic-20200807 RUN apt-get update && apt-get -y -q install python3 CMD cp /workspace/discusrc /etc && /workspace/discus.py discus-0.5.0/tests/issues/1/README.md000066400000000000000000000005501436073205200171040ustar00rootroot00000000000000* **URL**: https://github.com/ncarrier/discus/issues/1 * **How to launch the reproducer**: From the root of the discus source tree, run: ``` docker build . --tag discus-issue-1-reproducer --file tests/issues/1/Dockerfile docker run --rm -it -v ${PWD}:/workspace discus-issue-1-reproducer ``` * **failing revision**: 35d57a8a5d1e8cbae3a5358b7b98978ebe97c87f discus-0.5.0/tests/mtab.291276000066400000000000000000000001041436073205200155640ustar00rootroot00000000000000/dev/sda2 /media/ACER\040UFD ext4 rw,relatime,errors=remount-ro 0 0 discus-0.5.0/tests/mtab.oneline000066400000000000000000000000631436073205200164470ustar00rootroot00000000000000/dev/sda2 / ext4 rw,relatime,errors=remount-ro 0 0 discus-0.5.0/tests/pre-commit.sh000077500000000000000000000010511436073205200165620ustar00rootroot00000000000000#!/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 export PYTHONPATH=${root} # 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'" \ "${root}/discus.py" \ "flake8 $(find ${root} -name *.py)" \ "python3 -m unittest tests.unit_tests -v" discus-0.5.0/tests/unit_tests.py000066400000000000000000000202361436073205200167300ustar00rootroot00000000000000import unittest import argparse from discus import StatsFactory, SizeFormatter, DiskData, Disk from discus import Mount, parse_options, read_mounts, opts 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 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") 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") class DiskTests(unittest.TestCase): """Unit tests for the Disk class""" def test_graph(self): opts["color"] = "" self.assertEqual(Disk.graph(10, 14), " [*---------]") self.assertEqual(Disk.graph(20, 14), " [**--------]") self.assertEqual(Disk.graph(50, 14), " [*****-----]") self.assertEqual(Disk.graph(80, 14), " [********--]") self.assertEqual(Disk.graph(100, 14), " [**********]") class ParseOptionsTests(unittest.TestCase): """Tests for the parse_options function.""" def test_device(self): """Test for the --device option.""" options = parse_options([]) self.assertFalse(options.device) options = parse_options(["-d"]) self.assertTrue(options.device) options = parse_options(["--device"]) self.assertTrue(options.device) def test_color(self): """Test for the --color option.""" options = parse_options([]) self.assertTrue(options.color) options = parse_options(["-c"]) self.assertFalse(options.color) options = parse_options(["--color"]) self.assertFalse(options.color) def test_gigabytes(self): """Test for the --gigabytes option.""" options = parse_options([]) self.assertEqual(options.divisor, None) options = parse_options(["-g"]) self.assertEqual(options.divisor, 2) options = parse_options(["--gigabytes"]) self.assertEqual(options.divisor, 2) def test_kilobytes(self): """Test for the --kilobytes option.""" options = parse_options([]) self.assertEqual(options.divisor, None) options = parse_options(["-k"]) self.assertEqual(options.divisor, 0) options = parse_options(["--kilobytes"]) self.assertEqual(options.divisor, 0) def test_megabytes(self): """Test for the --megabytes option.""" options = parse_options([]) self.assertEqual(options.divisor, None) options = parse_options(["-m"]) self.assertEqual(options.divisor, 1) options = parse_options(["--megabytes"]) self.assertEqual(options.divisor, 1) def test_places(self): options = parse_options([]) self.assertEqual(options.places, None) options = parse_options(["-p", "0"]) self.assertEqual(options.places, 0) options = parse_options(["-p", "9"]) self.assertEqual(options.places, 9) with self.assertRaises((SystemExit, argparse.ArgumentError)): parse_options(["-p", "10"]) with self.assertRaises((SystemExit, argparse.ArgumentError)): parse_options(["-p", "-1"]) def test_reserved(self): """ Test for the --reserved option.""" options = parse_options([]) self.assertFalse(options.reserved) options = parse_options(["-r"]) self.assertTrue(options.reserved) options = parse_options(["--reserved"]) self.assertTrue(options.reserved) def test_smart(self): options = parse_options([]) self.assertTrue(options.smart) options = parse_options(["-s"]) self.assertFalse(options.smart) options = parse_options(["--smart"]) self.assertFalse(options.smart) def test_terabytes(self): """Test for the --terabytes option.""" options = parse_options([]) self.assertEqual(options.divisor, None) options = parse_options(["-t"]) self.assertEqual(options.divisor, 3) options = parse_options(["--terabytes"]) self.assertEqual(options.divisor, 3) class ReadMountsTests(unittest.TestCase): """Tests for the read_mounts function.""" def test_simple_mtab(self): """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): """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].mount, "/media/ACER UFD")