1+ use crate :: cli:: UsageError ;
2+ use crate :: cli:: mode:: { self , MainFn } ;
13use aoc:: { PUZZLES , PuzzleFn } ;
24use std:: collections:: VecDeque ;
35use std:: error:: Error ;
46use std:: num:: NonZeroUsize ;
7+ use std:: path:: PathBuf ;
8+ use std:: { fs, io} ;
59use utils:: date:: { Day , Year } ;
610use utils:: multiversion:: { VERSIONS , Version } ;
711
812#[ derive( Debug , Default ) ]
9- pub struct Options {
13+ pub struct Arguments {
1014 program_name : Option < String > ,
1115 pub help : bool ,
1216 pub version_override : Option < Version > ,
1317 pub threads_override : Option < NonZeroUsize > ,
18+ pub inputs_dir : Option < PathBuf > ,
19+ mode : Option < MainFn > ,
1420 pub year : Option < Year > ,
1521 pub day : Option < Day > ,
22+ pub extra_args : VecDeque < String > ,
1623}
1724
18- impl Options {
19- pub fn parse ( ) -> Result < Self , String > {
25+ impl Arguments {
26+ pub fn parse ( ) -> Result < Self , UsageError > {
2027 let mut result = Self :: default ( ) ;
2128
2229 let mut args: VecDeque < String > = std:: env:: args ( ) . collect ( ) ;
2330 result. program_name = args. pop_front ( ) ;
2431
2532 while let Some ( option) = args. pop_front ( ) {
2633 if option == "--" {
27- break ;
34+ result. extra_args = args;
35+ return Ok ( result) ;
2836 }
2937
3038 if let Some ( option) = option. strip_prefix ( "--" ) {
3139 // Long form options
3240 if let Some ( ( before, after) ) = option. split_once ( '=' ) {
3341 result
3442 . handle_long ( before, ArgumentValue :: Provided ( after. to_string ( ) ) )
35- . map_err ( |e| format ! ( "option --{before}: {e}" ) ) ?;
43+ . map_err ( |e| {
44+ UsageError :: InvalidArguments ( format ! ( "option --{before}: {e}" ) . into ( ) )
45+ } ) ?;
3646 } else {
3747 result
3848 . handle_long ( option, ArgumentValue :: Available ( & mut args) )
39- . map_err ( |e| format ! ( "option --{option}: {e}" ) ) ?;
49+ . map_err ( |e| {
50+ UsageError :: InvalidArguments ( format ! ( "option --{option}: {e}" ) . into ( ) )
51+ } ) ?;
4052 }
4153 continue ;
4254 }
@@ -50,13 +62,19 @@ impl Options {
5062 for option in options {
5163 result
5264 . handle_short ( option, ArgumentValue :: None )
53- . map_err ( |e| format ! ( "option -{option}: {e}" ) ) ?;
65+ . map_err ( |e| {
66+ UsageError :: InvalidArguments (
67+ format ! ( "option -{option}: {e}" ) . into ( ) ,
68+ )
69+ } ) ?;
5470 }
5571
56- // Last short form option can consume a value
72+ // The last short form option can consume a value
5773 result
5874 . handle_short ( last, ArgumentValue :: Available ( & mut args) )
59- . map_err ( |e| format ! ( "option -{last}: {e}" ) ) ?;
75+ . map_err ( |e| {
76+ UsageError :: InvalidArguments ( format ! ( "option -{last}: {e}" ) . into ( ) )
77+ } ) ?;
6078 continue ;
6179 }
6280 }
@@ -65,46 +83,64 @@ impl Options {
6583 break ;
6684 }
6785
86+ if let Some ( i) = args. iter ( ) . position ( |x| x == "--" ) {
87+ result. extra_args = args. split_off ( i + 1 ) ;
88+ args. pop_back ( ) ;
89+ }
90+
6891 if let Some ( year) = args. pop_front ( ) {
6992 result. year = match year. parse ( ) {
7093 Ok ( y) => Some ( y) ,
71- Err ( err) => return Err ( err. to_string ( ) ) ,
94+ Err ( err) => return Err ( UsageError :: InvalidArguments ( err. into ( ) ) ) ,
7295 } ;
7396
7497 if let Some ( day) = args. pop_front ( ) {
7598 result. day = match day. parse ( ) {
7699 Ok ( y) => Some ( y) ,
77- Err ( err) => return Err ( err. to_string ( ) ) ,
100+ Err ( err) => return Err ( UsageError :: InvalidArguments ( err. into ( ) ) ) ,
78101 } ;
79102
80103 if !args. is_empty ( ) {
81- return Err ( "too many arguments" . to_string ( ) ) ;
104+ return Err ( UsageError :: TooManyArguments ) ;
82105 }
83106 }
84107 }
85108
86109 Ok ( result)
87110 }
88111
89- pub fn help ( & self ) -> String {
112+ pub fn help_string ( & self ) -> String {
90113 format ! (
91114 r"Usage:
92115 {program_name}
93116 Run all solutions
94117
95118 {program_name} $year
96119 Run all solutions for the provided year
97-
120+
98121 {program_name} $year $day
99122 Run the solution for the provided date
100123
101124Options:
125+ --stdin
126+ Run a single solution, reading input from stdin. $year and $day must be provided.
127+
128+ --test
129+ Runs all solutions against all inputs from the inputs directory, comparing the outputs to
130+ the stored correct answers in the inputs directory. $year may be provided to only test the
131+ provided year. A custom command template may be provided following a `--` argument to test
132+ another binary. Requires the 'test-runner' feature to be enabled.
133+
102134 --multiversion/-m $version
103135 Override which implementation of multiversioned functions should be used.
104136 Supported versions: {multiversion_options:?}
105-
137+
106138 --threads/-t $threads
107139 Override the number of threads to use for multithreaded solutions.
140+ In `--test` mode this controls the number of simultaneous tests.
141+
142+ --inputs $dir
143+ Specify the directory storing inputs. Defaults to './inputs'.
108144
109145 --help/-h
110146 Print this help
@@ -121,6 +157,10 @@ Options:
121157 "help" => self . option_help ( value) ,
122158 "multiversion" => self . option_multiversion ( value) ,
123159 "threads" => self . option_threads ( value) ,
160+ "inputs" => self . option_inputs ( value) ,
161+ "stdin" => self . option_mode ( value, mode:: stdin:: main) ,
162+ #[ cfg( feature = "test-runner" ) ]
163+ "test" => self . option_mode ( value, mode:: test:: main) ,
124164 _ => Err ( "unknown option" . into ( ) ) ,
125165 }
126166 }
@@ -158,13 +198,54 @@ Options:
158198 Ok ( ( ) )
159199 }
160200
201+ fn option_inputs ( & mut self , value : ArgumentValue ) -> Result < ( ) , Box < dyn Error > > {
202+ let value = value. required ( ) ?. into ( ) ;
203+ if self . inputs_dir . is_some ( ) {
204+ return Err ( "option provided more than once" . into ( ) ) ;
205+ }
206+ if !fs:: metadata ( & value) . is_ok_and ( |m| m. is_dir ( ) ) {
207+ return Err ( "inputs path must be a directory" . into ( ) ) ;
208+ }
209+ self . inputs_dir = Some ( value) ;
210+ Ok ( ( ) )
211+ }
212+
213+ fn option_mode ( & mut self , value : ArgumentValue , mode : MainFn ) -> Result < ( ) , Box < dyn Error > > {
214+ value. none ( ) ?;
215+ if self . mode . is_some ( ) {
216+ return Err ( "mode options are mutually exclusive" . into ( ) ) ;
217+ }
218+ self . mode = Some ( mode) ;
219+ Ok ( ( ) )
220+ }
221+
222+ pub fn main_fn ( & self ) -> MainFn {
223+ self . mode . unwrap_or ( mode:: default:: main)
224+ }
225+
161226 pub fn matching_puzzles ( & self ) -> Vec < ( Year , Day , PuzzleFn ) > {
162227 PUZZLES
163228 . iter ( )
164229 . copied ( )
165230 . filter ( |& ( y, d, ..) | self . year . unwrap_or ( y) == y && self . day . unwrap_or ( d) == d)
166231 . collect ( )
167232 }
233+
234+ pub fn inputs_dir ( & self ) -> PathBuf {
235+ self . inputs_dir
236+ . clone ( )
237+ . unwrap_or_else ( || PathBuf :: from ( "./inputs" ) )
238+ }
239+
240+ pub fn read_input ( & self , year : Year , day : Day ) -> Result < String , ( String , io:: Error ) > {
241+ let mut path = self . inputs_dir ( ) ;
242+ path. push ( format ! ( "year{year:#}" ) ) ;
243+ path. push ( format ! ( "day{day:#}.txt" ) ) ;
244+ match fs:: read_to_string ( & path) {
245+ Ok ( s) => Ok ( s. trim_ascii_end ( ) . replace ( "\r \n " , "\n " ) ) ,
246+ Err ( err) => Err ( ( path. to_string_lossy ( ) . to_string ( ) , err) ) ,
247+ }
248+ }
168249}
169250
170251#[ must_use]
0 commit comments