Skip to content

Commit a907167

Browse files
committed
truncate: Add truncate.py and test script
1 parent 28d2073 commit a907167

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

src/truncate.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/python3
2+
3+
import sys
4+
from optparse import OptionParser
5+
from pathlib import Path
6+
from typing import Callable
7+
8+
9+
def truncate(opts, files: list[Path], size_prefix: str | None, size_num: int | None):
10+
get_new_size: Callable[[int], int] = (
11+
{
12+
"+": lambda old_size: old_size + size_num,
13+
"-": lambda old_size: old_size - size_num,
14+
"<": lambda old_size: min(old_size, size_num),
15+
">": lambda old_size: max(old_size, size_num),
16+
"/": lambda old_size: size_num * (old_size // size_num),
17+
"%": lambda old_size: size_num * -(old_size // -size_num),
18+
}[size_prefix]
19+
if size_prefix
20+
else (
21+
(lambda _: size_num)
22+
if size_num is not None
23+
else (lambda old_size: old_size)
24+
)
25+
)
26+
27+
size_attr = "st_blocks" if opts.io_blocks else "st_size"
28+
29+
try:
30+
reference_size = (
31+
getattr(opts.reference.stat(follow_symlinks=True), size_attr)
32+
if opts.reference
33+
else None
34+
)
35+
except OSError as e:
36+
print(e)
37+
sys.exit(1)
38+
39+
for file in files:
40+
if not file.exists() and opts.no_create:
41+
continue
42+
43+
stat = file.stat(follow_symlinks=True)
44+
45+
old_size = getattr(stat, size_attr)
46+
new_size = get_new_size(reference_size or old_size)
47+
48+
if new_size == old_size:
49+
continue
50+
51+
try:
52+
with file.open("rb+") as io:
53+
io.truncate(
54+
new_size * stat.st_blksize if opts.io_blocks else new_size,
55+
)
56+
except OSError as e:
57+
print(e)
58+
sys.exit(1)
59+
60+
61+
if __name__ == "__main__":
62+
parser = OptionParser(
63+
usage="Usage: %prog [OPTION]... -s SIZE FILE..."
64+
+ "\n %prog [OPTION]... -r RFILE FILE...",
65+
description="Shrink or extend each FILE to SIZE.",
66+
add_help_option=False,
67+
)
68+
parser.add_option("--help", action="help", help="show usage information and exit")
69+
70+
parser.add_option(
71+
"-c", "--no-create", action="store_true", help="do not create files"
72+
)
73+
parser.add_option("-s", "--size", help="set or adjust file size by SIZE bytes")
74+
parser.add_option(
75+
"-o",
76+
"--io-blocks",
77+
action="store_true",
78+
help="interpret SIZE as number of IO blocks",
79+
)
80+
81+
parser.add_option("-r", "--reference", metavar="RFILE", help="base size on RFILE")
82+
83+
opts, args = parser.parse_args()
84+
85+
if opts.reference:
86+
opts.reference = Path(opts.reference)
87+
88+
if opts.size:
89+
size_prefix = opts.size[0]
90+
if size_prefix not in frozenset("+-<>/%"):
91+
size_prefix = None
92+
93+
try:
94+
size_num = int(opts.size[1:] if size_prefix else opts.size)
95+
except ValueError:
96+
parser.error(f"invalid number: '{opts.size}'")
97+
98+
if opts.reference and not size_prefix:
99+
parser.error("you must specify a relative '--size' with '--reference'")
100+
elif not opts.reference:
101+
parser.error("you must specify either '--size' or '--reference'")
102+
else:
103+
size_prefix = None
104+
size_num = None
105+
106+
if not args:
107+
parser.error("missing file operand")
108+
109+
truncate(opts, map(Path, args), size_prefix, size_num)

tests/truncate.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env sh
2+
3+
set -eux
4+
5+
tempdir="$(mktemp -d)"
6+
7+
trap 'rm -rf ${tempdir}' EXIT
8+
9+
get_size() {
10+
wc -c "$1" | cut -d ' ' -f 1
11+
}
12+
13+
## basic truncation
14+
15+
echo 'foo bar' > "${tempdir}"/a
16+
17+
./truncate.py -s 3 "${tempdir}"/a
18+
19+
test "$(cat "${tempdir}"/a)" = 'foo'
20+
21+
## size extension
22+
23+
./truncate.py -s +7 "${tempdir}"/a
24+
25+
size="$(get_size "${tempdir}"/a)"
26+
test "${size}" = 10
27+
28+
## ensure minimum size
29+
30+
./truncate.py -s '>5' "${tempdir}"/a
31+
32+
size="$(get_size "${tempdir}"/a)"
33+
test "${size}" = 10
34+
35+
## truncate to maximum size
36+
37+
./truncate.py -s '<8' "${tempdir}"/a
38+
39+
size="$(get_size "${tempdir}"/a)"
40+
test "${size}" = 8
41+
42+
## round size to multiple
43+
44+
./truncate.py -s %5 "${tempdir}"/a
45+
46+
size="$(get_size "${tempdir}"/a)"
47+
test "${size}" = 10
48+
49+
## ensure size is multiple
50+
51+
./truncate.py -s /2 "${tempdir}"/a
52+
53+
size="$(get_size "${tempdir}"/a)"
54+
test "${size}" = 10
55+
56+
## truncate with reference file
57+
58+
touch "${tempdir}"/b
59+
60+
./truncate.py -r "${tempdir}"/a "${tempdir}"/b
61+
62+
size="$(get_size "${tempdir}"/b)"
63+
test "${size}" = 10
64+
65+
## truncate with reference file and size adjustment
66+
67+
./truncate.py -r "${tempdir}"/a -s +10 "${tempdir}"/b
68+
69+
size="$(get_size "${tempdir}"/b)"
70+
test "${size}" = 20
71+
72+
## truncate with block size
73+
74+
./truncate.py -s 0 -o "${tempdir}"/a
75+
76+
test ! -s "${tempdir}"/a
77+
78+
exit 0

0 commit comments

Comments
 (0)