Skip to content

Commit 86cbe14

Browse files
committed
chgrp: Add chgrp.py
1 parent 196a476 commit 86cbe14

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

userland/utilities/chgrp.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import grp
2+
import pwd
3+
import shutil
4+
import sys
5+
from pathlib import Path
6+
7+
from tqdm import tqdm
8+
9+
from .. import core
10+
11+
12+
parser = core.create_parser(
13+
usage=(
14+
"%prog [OPTION]... GROUP FILE...",
15+
"%prog [OPTION]... --reference=RFILE FILE...",
16+
),
17+
description="Change the group ownership of each FILE.",
18+
)
19+
20+
parser.add_option(
21+
"-f",
22+
"--silent",
23+
"--quiet",
24+
dest="verbosity",
25+
action="store_const",
26+
const=0,
27+
default=1,
28+
help="suppress most error messages",
29+
)
30+
parser.add_option(
31+
"-c",
32+
"--changes",
33+
dest="verbosity",
34+
action="store_const",
35+
const=2,
36+
help="report only when changes are made",
37+
)
38+
parser.add_option(
39+
"-v",
40+
"--verbose",
41+
dest="verbosity",
42+
action="store_const",
43+
const=3,
44+
help="print a diagnostic for each file",
45+
)
46+
47+
parser.add_option(
48+
"--progress",
49+
dest="progress",
50+
action="store_true",
51+
help="show a progress bar when changing groups",
52+
)
53+
parser.add_option(
54+
"--no-progress",
55+
dest="progress",
56+
action="store_false",
57+
default=False,
58+
help="do not show a progress bar (default)",
59+
)
60+
61+
parser.add_option(
62+
"--dereference",
63+
action="store_true",
64+
default=True,
65+
help="affect symlink referents instead of the symlinks themselves (default)",
66+
)
67+
parser.add_option(
68+
"-h",
69+
"--no-dereference",
70+
dest="dereference",
71+
action="store_false",
72+
help="opposite of --dereference",
73+
)
74+
75+
parser.add_option(
76+
"--no-preserve-root",
77+
dest="preserve_root",
78+
action="store_false",
79+
default=False,
80+
help="do not treat '/' specially (default)",
81+
)
82+
parser.add_option(
83+
"--preserve-root",
84+
action="store_true",
85+
help="fail to operate recursively on '/'",
86+
)
87+
88+
parser.add_option(
89+
"--from",
90+
dest="from_spec", # prevent name collision with the `from` keyword
91+
metavar="[CURRENT_OWNER][:CURRENT_GROUP]",
92+
help="only affect files with CURRENT_OWNER and CURRENT_GROUP"
93+
" (either is optional and only checked if given)",
94+
)
95+
96+
parser.add_option(
97+
"--reference",
98+
metavar="RFILE",
99+
help="use the group of RFILE instead of from an argument",
100+
)
101+
102+
parser.add_option(
103+
"-R", "--recursive", action="store_true", help="operate on directories recursively"
104+
)
105+
parser.add_option(
106+
"-H",
107+
dest="recurse_mode",
108+
action="store_const",
109+
const="H",
110+
help="traverse directory symlinks only if they were given as command line arguments",
111+
)
112+
parser.add_option(
113+
"-L",
114+
dest="recurse_mode",
115+
action="store_const",
116+
const="L",
117+
help="traverse all directory symlinks encountered",
118+
)
119+
parser.add_option(
120+
"-P",
121+
dest="recurse_mode",
122+
action="store_const",
123+
const="P",
124+
default="P",
125+
help="do not traverse any symlinks (default)",
126+
)
127+
128+
129+
@core.command(parser)
130+
def python_userland_chgrp(opts, args):
131+
if not args:
132+
parser.error("missing operand")
133+
134+
from_uid: int | None = None
135+
from_gid: int | None = None
136+
137+
if opts.from_spec:
138+
from_spec = opts.from_spec.split(":")
139+
140+
if from_spec[0]:
141+
try:
142+
from_uid = pwd.getpwnam(from_spec[0])
143+
except KeyError:
144+
parser.error(f"invalid user: '{opts.from_spec}'")
145+
146+
if len(from_spec) > 1 and from_spec[1]:
147+
try:
148+
from_gid = grp.getgrnam(from_spec[1])
149+
except KeyError:
150+
parser.error(f"invalid group: '{opts.from_spec}'")
151+
152+
gid: int
153+
gname: str | None = None
154+
155+
if opts.reference:
156+
try:
157+
gid = Path(opts.reference).stat(follow_symlinks=True).st_gid
158+
except OSError as e:
159+
print(e, file=sys.stderr)
160+
return 1
161+
else:
162+
gname = args.pop(0)
163+
164+
if not args:
165+
parser.error(f"missing operand after '{gname}'")
166+
167+
if gname.isdecimal():
168+
gid = int(gname)
169+
else:
170+
try:
171+
gid = grp.getgrnam(gname).gr_gid
172+
except KeyError:
173+
parser.error(f"invalid group: '{gname}'")
174+
175+
failed = False
176+
177+
def chown(file: Path) -> None:
178+
nonlocal failed
179+
180+
try:
181+
stat = file.stat(follow_symlinks=opts.dereference)
182+
prev_uid = stat.st_uid
183+
prev_gid = stat.st_gid
184+
except OSError as e:
185+
failed = True
186+
if opts.verbosity:
187+
print(e, file=sys.stderr)
188+
print(
189+
f"failed to change group of '{file}' to {gname or gid}",
190+
file=sys.stderr,
191+
)
192+
return
193+
194+
try:
195+
prev_gname = grp.getgrgid(prev_gid).gr_name
196+
except KeyError:
197+
prev_gname = str(prev_gid)
198+
199+
# Note: while it's possible, we do not check if prev_gid == gid at
200+
# this point because even if they are the same, an error should be
201+
# printed if the current user has insufficient permission to change
202+
# the group membership of that file (for coreutils compat).
203+
if (from_uid is not None and prev_uid == from_uid) or (
204+
from_gid is not None and prev_gid == from_gid
205+
):
206+
if opts.verbosity > 2:
207+
print(f"group of '{file}' retained as {prev_gname}")
208+
return
209+
210+
try:
211+
shutil.chown(file, group=gid, follow_symlinks=opts.dereference)
212+
except OSError as e:
213+
failed = True
214+
if opts.verbosity:
215+
print(e, file=sys.stderr)
216+
if opts.verbosity:
217+
print(
218+
f"failed to change group of '{file}' to {gname or gid}",
219+
file=sys.stderr,
220+
)
221+
return
222+
223+
if prev_gid == gid:
224+
if opts.verbosity > 2:
225+
print(f"group of '{file}' retained as {prev_gname}")
226+
elif opts.verbosity > 1:
227+
print(f"changed group of '{file}' from {prev_gname} to {gname or gid}")
228+
229+
files = map(
230+
Path,
231+
(
232+
tqdm(args, ascii=True, desc="Changing group ownership")
233+
if opts.progress
234+
else args
235+
),
236+
)
237+
238+
if opts.recursive:
239+
240+
def traverse(file: Path) -> None:
241+
nonlocal failed
242+
243+
if opts.preserve_root and file.root == str(file):
244+
print(
245+
f"recursive operation on '{file}' prevented; use --no-preserve-root to override",
246+
file=sys.stderr,
247+
)
248+
failed = True
249+
return
250+
251+
for child in file.iterdir():
252+
if child.is_dir(follow_symlinks=opts.recurse_mode == "L"):
253+
traverse(child)
254+
chown(file)
255+
256+
for file in files:
257+
if file.is_dir(
258+
follow_symlinks=opts.recurse_mode == "H" or opts.recurse_mode == "L"
259+
):
260+
traverse(file)
261+
else:
262+
chown(file)
263+
else:
264+
for file in files:
265+
chown(file)
266+
267+
return int(failed)

0 commit comments

Comments
 (0)