66// file in the root directory of this project.
77// SPDX-License-Identifier: MIT
88//
9- // TODO:
10- // - implement -h, -H, -L, -P
11- //
129
10+ mod common;
11+
12+ use self :: common:: error_string;
1313use clap:: Parser ;
14- use gettextrs:: { bind_textdomain_codeset, setlocale, textdomain, LocaleCategory } ;
15- use std:: ffi:: CString ;
16- use std:: path:: Path ;
17- use std:: { fs, io} ;
14+ use gettextrs:: { bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory } ;
15+ use std:: { cell:: RefCell , ffi:: CString , io, os:: unix:: fs:: MetadataExt } ;
1816
1917/// chgrp - change file group ownership
2018#[ derive( Parser ) ]
21- #[ command( version, about) ]
19+ #[ command( version, about, disable_help_flag = true ) ]
2220struct Args {
21+ #[ arg( long, action = clap:: ArgAction :: HelpLong ) ] // Bec. help clashes with -h
22+ help : Option < bool > ,
23+
2324 /// Change symbolic links, rather than the files they point to
24- #[ arg( short = 'h' , long) ]
25+ #[ arg( short = 'h' , long, default_value_t = false ) ]
2526 no_derereference : bool ,
2627
2728 /// Follow command line symlinks during -R recursion
28- #[ arg( short = 'H' , long ) ]
29+ #[ arg( short = 'H' , overrides_with_all = [ "follow_cli" , "follow_symlinks" , "follow_none" ] ) ]
2930 follow_cli : bool ,
3031
3132 /// Follow symlinks during -R recursion
32- #[ arg( short = 'L' , group = "deref" ) ]
33- dereference : bool ,
33+ #[ arg( short = 'L' , overrides_with_all = [ "follow_cli" , "follow_symlinks" , "follow_none" ] ) ]
34+ follow_symlinks : bool ,
3435
3536 /// Never follow symlinks during -R recursion
36- #[ arg( short = 'P' , group = "deref" ) ]
37- no_dereference2 : bool ,
37+ #[ arg( short = 'P' , overrides_with_all = [ "follow_cli" , "follow_symlinks" , "follow_none" ] , default_value_t = true ) ]
38+ follow_none : bool ,
3839
3940 /// Recursively change groups of directories and their contents
4041 #[ arg( short, short_alias = 'R' , long) ]
@@ -47,66 +48,143 @@ struct Args {
4748 files : Vec < String > ,
4849}
4950
50- fn chgrp_file ( filename : & str , gid : u32 , recurse : bool ) -> Result < ( ) , io:: Error > {
51- let path = Path :: new ( filename) ;
52- let metadata = fs:: metadata ( path) ?;
53-
54- // recurse into directories
55- if metadata. is_dir ( ) && recurse {
56- for entry in fs:: read_dir ( path) ? {
57- let entry = entry?;
58- let entry_path = entry. path ( ) ;
59- let entry_filename = entry_path. to_str ( ) . unwrap ( ) ;
60- chgrp_file ( entry_filename, gid, recurse) ?;
61- }
62- }
51+ fn chgrp_file ( filename : & str , gid : Option < u32 > , args : & Args ) -> bool {
52+ let recurse = args. recurse ;
53+ let no_derereference = args. no_derereference ;
6354
64- // change the group
65- let pathstr = CString :: new ( filename) . unwrap ( ) ;
66- unsafe {
67- if libc:: chown ( pathstr. as_ptr ( ) , libc:: geteuid ( ) , gid) != 0 {
68- return Err ( io:: Error :: last_os_error ( ) ) ;
69- }
70- }
55+ let terminate = RefCell :: new ( false ) ;
56+
57+ ftw:: traverse_directory (
58+ filename,
59+ |entry| {
60+ if * terminate. borrow ( ) {
61+ return Ok ( false ) ;
62+ }
7163
72- Ok ( ( ) )
64+ let md = entry. metadata ( ) . unwrap ( ) ;
65+
66+ // According to the spec:
67+ // "The user ID of the file shall be used as the owner argument."
68+ let uid = md. uid ( ) ;
69+
70+ // Don't change the group ID if the group argument is empty
71+ let gid = gid. unwrap_or ( libc:: gid_t:: MAX ) ;
72+
73+ let ret = unsafe {
74+ libc:: fchownat (
75+ entry. dir_fd ( ) ,
76+ entry. file_name ( ) . as_ptr ( ) ,
77+ uid,
78+ gid,
79+ // Default is to change the file that the symbolic link points to unless the
80+ // -h flag is specified.
81+ if no_derereference {
82+ libc:: AT_SYMLINK_NOFOLLOW
83+ } else {
84+ 0
85+ } ,
86+ )
87+ } ;
88+ if ret != 0 {
89+ let e = io:: Error :: last_os_error ( ) ;
90+ let err_str = match e. kind ( ) {
91+ io:: ErrorKind :: PermissionDenied => {
92+ gettext ! ( "cannot access '{}': {}" , entry. path( ) , error_string( & e) )
93+ }
94+ _ => {
95+ gettext ! ( "changing group of '{}': {}" , entry. path( ) , error_string( & e) )
96+ }
97+ } ;
98+ eprintln ! ( "chgrp: {}" , err_str) ;
99+ * terminate. borrow_mut ( ) = true ;
100+ return Err ( ( ) ) ;
101+ }
102+
103+ Ok ( recurse)
104+ } ,
105+ |_| Ok ( ( ) ) , // Do nothing on `postprocess_dir`
106+ |entry, error| {
107+ let e = error. inner ( ) ;
108+ let err_str = match e. kind ( ) {
109+ io:: ErrorKind :: PermissionDenied => {
110+ gettext ! (
111+ "cannot read directory '{}': {}" ,
112+ entry. path( ) ,
113+ error_string( & e)
114+ )
115+ }
116+ _ => {
117+ gettext ! ( "changing group of '{}': {}" , entry. path( ) , error_string( & e) )
118+ }
119+ } ;
120+ eprintln ! ( "chgrp: {}" , err_str) ;
121+ * terminate. borrow_mut ( ) = true ;
122+ } ,
123+ ftw:: TraverseDirectoryOpts {
124+ follow_symlinks_on_args : args. follow_cli ,
125+ follow_symlinks : args. follow_symlinks ,
126+ ..Default :: default ( )
127+ } ,
128+ ) ;
129+
130+ let failed = * terminate. borrow ( ) ;
131+ !failed
73132}
74133
75134// lookup string group by name, or parse numeric group ID
76- fn parse_group ( group : & str ) -> Result < u32 , & ' static str > {
135+ fn parse_group ( group : & str ) -> Result < Option < u32 > , String > {
136+ // empty strings are accepted without errors
137+ if group. is_empty ( ) {
138+ return Ok ( None ) ;
139+ }
140+
77141 match group. parse :: < u32 > ( ) {
78- Ok ( gid) => Ok ( gid) ,
142+ Ok ( gid) => Ok ( Some ( gid) ) ,
79143 Err ( _) => {
80144 // lookup group by name
81145 let group_cstr = CString :: new ( group) . unwrap ( ) ;
82- let group = unsafe { libc:: getgrnam ( group_cstr. as_ptr ( ) ) } ;
83- if group. is_null ( ) {
84- return Err ( "group not found" ) ;
146+ let group_st = unsafe { libc:: getgrnam ( group_cstr. as_ptr ( ) ) } ;
147+ if group_st. is_null ( ) {
148+ let err_str = gettext ! ( "invalid group: '{}'" , group) ;
149+ return Err ( err_str) ;
85150 }
86151
87- let gid = unsafe { ( * group ) . gr_gid } ;
88- Ok ( gid)
152+ let gid = unsafe { ( * group_st ) . gr_gid } ;
153+ Ok ( Some ( gid) )
89154 }
90155 }
91156}
92157
93158fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
159+ // parse command line arguments
160+ let mut args = Args :: parse ( ) ;
161+
162+ // Enable `no_derereference` if `-R` is enabled without either `-H` or `-L`
163+ if args. recurse && !( args. follow_cli || args. follow_symlinks ) {
164+ args. no_derereference = true ;
165+ }
166+
167+ // initialize translations
94168 setlocale ( LocaleCategory :: LcAll , "" ) ;
95169 textdomain ( env ! ( "PROJECT_NAME" ) ) ?;
96170 bind_textdomain_codeset ( env ! ( "PROJECT_NAME" ) , "UTF-8" ) ?;
97171
98- let args = Args :: parse ( ) ;
99-
100172 let mut exit_code = 0 ;
101173
102174 // lookup string group by name, or parse numeric group ID
103- let gid = parse_group ( & args. group ) ?;
175+ let gid = match parse_group ( & args. group ) {
176+ Ok ( gid) => gid,
177+ Err ( e) => {
178+ eprintln ! ( "chgrp: {}" , e) ;
179+ std:: process:: exit ( 1 ) ;
180+ }
181+ } ;
104182
105183 // apply the group to each file
106184 for filename in & args. files {
107- if let Err ( e) = chgrp_file ( filename, gid, args. recurse ) {
185+ let success = chgrp_file ( filename, gid, & args) ;
186+ if !success {
108187 exit_code = 1 ;
109- eprintln ! ( "{}: {}" , filename, e) ;
110188 }
111189 }
112190
0 commit comments