@@ -7,7 +7,7 @@ use rc_core::{AliasManager, ListOptions, ObjectStore as _, RemotePath};
77use rc_s3:: S3Client ;
88use serde:: Serialize ;
99use std:: io:: Write as _;
10- use std:: process:: Command ;
10+ use std:: process:: { Command , Output } ;
1111
1212use crate :: exit_code:: ExitCode ;
1313use crate :: output:: { Formatter , OutputConfig } ;
@@ -140,13 +140,25 @@ pub async fn execute(args: FindArgs, output_config: OutputConfig) -> ExitCode {
140140 return ExitCode :: UsageError ;
141141 }
142142
143+ let exec_argv_template = match parse_exec_template ( exec_template) {
144+ Ok ( template) => template,
145+ Err ( e) => {
146+ formatter. error ( & e) ;
147+ return ExitCode :: UsageError ;
148+ }
149+ } ;
150+
143151 for m in & matches {
144152 let object_path = full_object_path ( & alias_name, & bucket, & m. key ) ;
145- let command_text = exec_template. replace ( "{}" , & object_path) ;
146- let output = match run_shell_command ( & command_text) {
153+ let ( program, exec_args, command_text) =
154+ render_exec_command ( & exec_argv_template, & object_path) ;
155+ let output = match run_exec_command ( & program, & exec_args) {
147156 Ok ( output) => output,
148157 Err ( e) => {
149- formatter. error ( & format ! ( "Failed to run command for {}: {}" , object_path, e) ) ;
158+ formatter. error ( & format ! (
159+ "Failed to run command for {}: {} ({})" ,
160+ object_path, command_text, e
161+ ) ) ;
150162 return ExitCode :: GeneralError ;
151163 }
152164 } ;
@@ -162,31 +174,24 @@ pub async fn execute(args: FindArgs, output_config: OutputConfig) -> ExitCode {
162174
163175 if !output. status . success ( ) {
164176 formatter. error ( & format ! (
165- "Command failed for {}: {}" ,
166- object_path, command_text
177+ "Command failed for {} (status {}) : {}" ,
178+ object_path, output . status , command_text
167179 ) ) ;
168180 return ExitCode :: GeneralError ;
169181 }
170182 }
171183 }
172184
173- let display_matches: Vec < MatchInfo > = matches
174- . iter ( )
175- . map ( |m| MatchInfo {
176- key : if args. print {
177- full_object_path ( & alias_name, & bucket, & m. key )
178- } else {
179- m. key . clone ( )
180- } ,
181- size_bytes : m. size_bytes ,
182- size_human : m. size_human . clone ( ) ,
183- last_modified : m. last_modified . clone ( ) ,
184- } )
185- . collect ( ) ;
185+ let mut display_matches = matches;
186+ if args. print {
187+ for m in & mut display_matches {
188+ m. key = full_object_path ( & alias_name, & bucket, & m. key ) ;
189+ }
190+ }
186191
187192 // Calculate totals
188- let total_count = matches . len ( ) ;
189- let total_size: i64 = matches . iter ( ) . filter_map ( |m| m. size_bytes ) . sum ( ) ;
193+ let total_count = display_matches . len ( ) ;
194+ let total_size: i64 = display_matches . iter ( ) . filter_map ( |m| m. size_bytes ) . sum ( ) ;
190195
191196 if args. count {
192197 // Only print count
@@ -234,18 +239,35 @@ pub async fn execute(args: FindArgs, output_config: OutputConfig) -> ExitCode {
234239}
235240
236241fn full_object_path ( alias : & str , bucket : & str , key : & str ) -> String {
237- format ! ( "{ alias}/{ bucket}/{ key}" )
242+ RemotePath :: new ( alias, bucket, key) . to_full_path ( )
238243}
239244
240- fn run_shell_command ( command : & str ) -> std:: io:: Result < std:: process:: Output > {
241- #[ cfg( target_family = "windows" ) ]
242- {
243- Command :: new ( "cmd" ) . args ( [ "/C" , command] ) . output ( )
244- }
245- #[ cfg( not( target_family = "windows" ) ) ]
246- {
247- Command :: new ( "sh" ) . args ( [ "-c" , command] ) . output ( )
245+ fn parse_exec_template ( exec_template : & str ) -> Result < Vec < String > , String > {
246+ let args = shlex:: split ( exec_template)
247+ . ok_or_else ( || "Invalid --exec template: unbalanced quotes" . to_string ( ) ) ?;
248+ if args. is_empty ( ) {
249+ return Err ( "Invalid --exec template: command cannot be empty" . to_string ( ) ) ;
248250 }
251+
252+ Ok ( args)
253+ }
254+
255+ fn render_exec_command (
256+ argv_template : & [ String ] ,
257+ object_path : & str ,
258+ ) -> ( String , Vec < String > , String ) {
259+ let rendered: Vec < String > = argv_template
260+ . iter ( )
261+ . map ( |arg| arg. replace ( "{}" , object_path) )
262+ . collect ( ) ;
263+ let program = rendered[ 0 ] . clone ( ) ;
264+ let args = rendered[ 1 ..] . to_vec ( ) ;
265+ let command_text = rendered. join ( " " ) ;
266+ ( program, args, command_text)
267+ }
268+
269+ fn run_exec_command ( program : & str , args : & [ String ] ) -> std:: io:: Result < Output > {
270+ Command :: new ( program) . args ( args) . output ( )
249271}
250272
251273/// Filters for find command
@@ -507,5 +529,46 @@ mod tests {
507529 full_object_path( "test" , "bucket" , "a/b.txt" ) ,
508530 "test/bucket/a/b.txt"
509531 ) ;
532+ assert_eq ! ( full_object_path( "test" , "bucket" , "" ) , "test/bucket" ) ;
533+ }
534+
535+ #[ test]
536+ fn test_parse_exec_template ( ) {
537+ assert_eq ! (
538+ parse_exec_template( "echo EXEC:{}" ) . unwrap( ) ,
539+ vec![ "echo" . to_string( ) , "EXEC:{}" . to_string( ) ]
540+ ) ;
541+ assert_eq ! (
542+ parse_exec_template( r#"printf '%s\n' "{}""# ) . unwrap( ) ,
543+ vec![ "printf" . to_string( ) , "%s\\ n" . to_string( ) , "{}" . to_string( ) ]
544+ ) ;
545+ }
546+
547+ #[ test]
548+ fn test_parse_exec_template_errors ( ) {
549+ assert ! ( parse_exec_template( "" ) . is_err( ) ) ;
550+ assert ! ( parse_exec_template( "'unterminated" ) . is_err( ) ) ;
551+ }
552+
553+ #[ test]
554+ fn test_render_exec_command ( ) {
555+ let template = vec ! [
556+ "echo" . to_string( ) ,
557+ "prefix:{}" . to_string( ) ,
558+ "{}" . to_string( ) ,
559+ ] ;
560+ let ( program, args, text) = render_exec_command ( & template, "test/bucket/a.txt" ) ;
561+ assert_eq ! ( program, "echo" ) ;
562+ assert_eq ! (
563+ args,
564+ vec![
565+ "prefix:test/bucket/a.txt" . to_string( ) ,
566+ "test/bucket/a.txt" . to_string( )
567+ ]
568+ ) ;
569+ assert_eq ! (
570+ text,
571+ "echo prefix:test/bucket/a.txt test/bucket/a.txt" . to_string( )
572+ ) ;
510573 }
511574}
0 commit comments