Skip to content

Commit 5707548

Browse files
committed
Added progressbar command for commandline progressbars
1 parent 671b723 commit 5707548

4 files changed

Lines changed: 396 additions & 2 deletions

File tree

docs/progressbar.algorithms.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
progressbar.algorithms module
2+
=============================
3+
4+
.. automodule:: progressbar.algorithms
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

progressbar/__main__.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import argparse
2+
import contextlib
3+
import pathlib
4+
import sys
5+
import time
6+
from typing import BinaryIO
7+
8+
import progressbar
9+
10+
11+
def size_to_bytes(size_str: str) -> int:
12+
'''
13+
Convert a size string with suffixes 'k', 'm', etc., to bytes.
14+
15+
Note: This function also supports '@' as a prefix to a file path to get the
16+
file size.
17+
18+
>>> size_to_bytes('1024k')
19+
1048576
20+
>>> size_to_bytes('1024m')
21+
1073741824
22+
>>> size_to_bytes('1024g')
23+
1099511627776
24+
>>> size_to_bytes('1024')
25+
1024
26+
>>> size_to_bytes('1024p')
27+
1125899906842624
28+
'''
29+
30+
# Define conversion rates
31+
suffix_exponent = {
32+
'k': 1,
33+
'm': 2,
34+
'g': 3,
35+
't': 4,
36+
'p': 5,
37+
}
38+
39+
# Initialize the default exponent to 0 (for bytes)
40+
exponent = 0
41+
42+
# Check if the size starts with '@' (for file sizes, not handled here)
43+
if size_str.startswith('@'):
44+
return pathlib.Path(size_str[1:]).stat().st_size
45+
46+
# Check if the last character is a known suffix and adjust the multiplier
47+
if size_str[-1].lower() in suffix_exponent:
48+
# Update exponent based on the suffix
49+
exponent = suffix_exponent[size_str[-1].lower()]
50+
# Remove the suffix from the size_str
51+
size_str = size_str[:-1]
52+
53+
# Convert the size_str to an integer and apply the exponent
54+
return int(size_str) * (1024 ** exponent)
55+
56+
57+
def create_argument_parser() -> argparse.ArgumentParser:
58+
'''
59+
Create the argument parser for the `progressbar` command.
60+
61+
>>> parser = create_argument_parser()
62+
>>> parser.parse_args(['-p', '-t', '-e', '-r', '-a', '-b', '-8', '-T', '-A', '-F', '-n', '-q', 'input', '-o', 'output'])
63+
Namespace(average_rate=True, bytes=True, eta=True, fineta=False, format=None, height=None, input=['input'], interval=None, last_written=None, line_mode=False, name=None, numeric=True, output='output', progress=True, quiet=True, rate=True, rate_limit=None, remote=None, size=None, stop_at_size=False, sync=False, timer=True, wait=False, watchfd=None, width=None)
64+
65+
Returns:
66+
argparse.ArgumentParser: The argument parser for the `progressbar` command.
67+
'''
68+
69+
parser = argparse.ArgumentParser(
70+
description='''
71+
Monitor the progress of data through a pipe.
72+
73+
Note that this is a Python implementation of the original `pv` command
74+
that is functional but not yet feature complete.
75+
''')
76+
77+
# Display switches
78+
parser.add_argument('-p', '--progress', action='store_true',
79+
help='Turn the progress bar on.')
80+
parser.add_argument('-t', '--timer', action='store_true',
81+
help='Turn the timer on.')
82+
parser.add_argument('-e', '--eta', action='store_true',
83+
help='Turn the ETA timer on.')
84+
parser.add_argument('-I', '--fineta', action='store_true',
85+
help='Display the ETA as local time of arrival.')
86+
parser.add_argument('-r', '--rate', action='store_true',
87+
help='Turn the rate counter on.')
88+
parser.add_argument('-a', '--average-rate', action='store_true',
89+
help='Turn the average rate counter on.')
90+
parser.add_argument('-b', '--bytes', action='store_true',
91+
help='Turn the total byte counter on.')
92+
parser.add_argument('-8', '--bits', action='store_true',
93+
help='Display total bits instead of bytes.')
94+
parser.add_argument('-T', '--buffer-percent', action='store_true',
95+
help='Turn on the transfer buffer percentage display.')
96+
parser.add_argument('-A', '--last-written', type=int,
97+
help='Show the last NUM bytes written.')
98+
parser.add_argument('-F', '--format', type=str,
99+
help='Use the format string FORMAT for output format.')
100+
parser.add_argument('-n', '--numeric', action='store_true',
101+
help='Numeric output.')
102+
parser.add_argument('-q', '--quiet', action='store_true', help='No output.')
103+
104+
# Output modifiers
105+
parser.add_argument('-W', '--wait', action='store_true',
106+
help='Wait until the first byte has been transferred.')
107+
parser.add_argument('-D', '--delay-start', type=float, help='Delay start.')
108+
parser.add_argument('-s', '--size', type=str,
109+
help='Assume total data size is SIZE.')
110+
parser.add_argument('-l', '--line-mode', action='store_true',
111+
help='Count lines instead of bytes.')
112+
parser.add_argument('-0', '--null', action='store_true',
113+
help='Count lines terminated with a zero byte.')
114+
parser.add_argument('-i', '--interval', type=float,
115+
help='Interval between updates.')
116+
parser.add_argument('-m', '--average-rate-window', type=int,
117+
help='Window for average rate calculation.')
118+
parser.add_argument('-w', '--width', type=int,
119+
help='Assume terminal is WIDTH characters wide.')
120+
parser.add_argument('-H', '--height', type=int,
121+
help='Assume terminal is HEIGHT rows high.')
122+
parser.add_argument('-N', '--name', type=str,
123+
help='Prefix output information with NAME.')
124+
parser.add_argument('-f', '--force', action='store_true',
125+
help='Force output.')
126+
parser.add_argument('-c', '--cursor', action='store_true',
127+
help='Use cursor positioning escape sequences.')
128+
129+
# Data transfer modifiers
130+
parser.add_argument('-L', '--rate-limit', type=str,
131+
help='Limit transfer to RATE bytes per second.')
132+
parser.add_argument('-B', '--buffer-size', type=str,
133+
help='Use transfer buffer size of BYTES.')
134+
parser.add_argument('-C', '--no-splice', action='store_true',
135+
help='Never use splice.')
136+
parser.add_argument('-E', '--skip-errors', action='store_true',
137+
help='Ignore read errors.')
138+
parser.add_argument('-Z', '--error-skip-block', type=str,
139+
help='Skip block size when ignoring errors.')
140+
parser.add_argument('-S', '--stop-at-size', action='store_true',
141+
help='Stop transferring after SIZE bytes.')
142+
parser.add_argument('-Y', '--sync', action='store_true',
143+
help='Synchronise buffer caches to disk after writes.')
144+
parser.add_argument('-K', '--direct-io', action='store_true',
145+
help='Set O_DIRECT flag on all inputs/outputs.')
146+
parser.add_argument('-X', '--discard', action='store_true',
147+
help='Discard input data instead of transferring it.')
148+
parser.add_argument('-d', '--watchfd', type=str,
149+
help='Watch file descriptor of process.')
150+
parser.add_argument('-R', '--remote', type=int,
151+
help='Remote control another running instance of pv.')
152+
153+
# General options
154+
parser.add_argument('-P', '--pidfile', type=pathlib.Path,
155+
help='Save process ID in FILE.')
156+
parser.add_argument(
157+
'input',
158+
help='Input file path. Uses stdin if not specified.',
159+
default='-',
160+
nargs='*',
161+
)
162+
parser.add_argument(
163+
'-o',
164+
'--output',
165+
default='-',
166+
help='Output file path. Uses stdout if not specified.')
167+
168+
return parser
169+
170+
171+
def main(argv: list[str] = sys.argv[1:]):
172+
'''
173+
Main function for the `progressbar` command.
174+
'''
175+
parser = create_argument_parser()
176+
args = parser.parse_args(argv)
177+
178+
binary_mode = '' if args.line_mode else 'b'
179+
180+
with contextlib.ExitStack() as stack:
181+
if args.output and args.output != '-':
182+
output_stream = stack.enter_context(
183+
open(args.output, 'w' + binary_mode))
184+
else:
185+
if args.line_mode:
186+
output_stream = sys.stdout
187+
else:
188+
output_stream = sys.stdout.buffer
189+
190+
input_paths = []
191+
total_size = 0
192+
filesize_available = True
193+
for filename in args.input:
194+
input_path: BinaryIO | pathlib.Path
195+
if filename == '-':
196+
if args.line_mode:
197+
input_path = sys.stdin
198+
else:
199+
input_path = sys.stdin.buffer
200+
201+
filesize_available = False
202+
else:
203+
input_path = pathlib.Path(filename)
204+
if not input_path.exists():
205+
parser.error(f'File not found: {filename}')
206+
207+
if not args.size:
208+
total_size += input_path.stat().st_size
209+
210+
input_paths.append(input_path)
211+
212+
# Determine the size for the progress bar (if provided)
213+
if args.size:
214+
total_size = size_to_bytes(args.size)
215+
filesize_available = True
216+
217+
if filesize_available:
218+
# Create the progress bar components
219+
widgets = [
220+
progressbar.Percentage(),
221+
' ',
222+
progressbar.Bar(),
223+
' ',
224+
progressbar.Timer(),
225+
' ',
226+
progressbar.FileTransferSpeed(),
227+
]
228+
else:
229+
widgets = [
230+
progressbar.SimpleProgress(),
231+
' ',
232+
progressbar.DataSize(),
233+
' ',
234+
progressbar.Timer(),
235+
]
236+
237+
if args.eta:
238+
widgets.append(' ')
239+
widgets.append(progressbar.AdaptiveETA())
240+
241+
# Initialize the progress bar
242+
bar = progressbar.ProgressBar(
243+
# widgets=widgets,
244+
max_value=total_size or None,
245+
max_error=False,
246+
)
247+
248+
# Data processing and updating the progress bar
249+
buffer_size = size_to_bytes(
250+
args.buffer_size) if args.buffer_size else 1024
251+
total_transferred = 0
252+
253+
bar.start()
254+
with contextlib.suppress(KeyboardInterrupt):
255+
for input_path in input_paths:
256+
if isinstance(input_path, pathlib.Path):
257+
input_stream = stack.enter_context(
258+
input_path.open('r' + binary_mode))
259+
else:
260+
input_stream = input_path
261+
262+
while True:
263+
if args.line_mode:
264+
data = input_stream.readline(buffer_size)
265+
else:
266+
data = input_stream.read(buffer_size)
267+
268+
if not data:
269+
break
270+
271+
output_stream.write(data)
272+
total_transferred += len(data)
273+
bar.update(total_transferred)
274+
275+
bar.finish(dirty=True)
276+
277+
278+
if __name__ == '__main__':
279+
main()

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ exclude = ['docs*', 'tests*']
108108
[tool.setuptools]
109109
include-package-data = true
110110

111-
# [project.scripts]
112-
# progressbar2 = 'progressbar.cli:main'
111+
[project.scripts]
112+
progressbar = 'progressbar.cli:main'
113113

114114
[project.optional-dependencies]
115115
docs = ['sphinx>=1.8.5', 'sphinx-autodoc-typehints>=1.6.0']

0 commit comments

Comments
 (0)