1+ using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
2+ using System ;
3+ using System . Collections ;
4+ using System . Collections . Generic ;
5+ using System . IO ;
6+ using System . Linq ;
7+ using System . Management . Automation ;
8+ using System . Management . Automation . Language ;
9+ using System . Reflection ;
10+
11+ namespace Microsoft . Windows . PowerShell . ScriptAnalyzer . Commands
12+ {
13+ /// <summary>
14+ /// Creates a new PSScriptAnalyzer settings file in the specified directory
15+ /// optionally based on a preset, a blank template, or all rules with default arguments.
16+ /// </summary>
17+ [ Cmdlet ( VerbsCommon . New , "ScriptAnalyzerSettingsFile" , SupportsShouldProcess = true ) ]
18+ [ OutputType ( typeof ( string ) ) ]
19+ public sealed class NewScriptAnalyzerSettingsFileCommand : PSCmdlet , IOutputWriter
20+ {
21+ private const string BaseOption_All = "All" ;
22+ private const string BaseOption_Blank = "Blank" ;
23+
24+ /// <summary>
25+ /// Target directory (or file path) where the settings file will be created. Defaults to
26+ /// current location.
27+ /// </summary>
28+ [ Parameter ( Position = 0 ) ]
29+ [ ValidateNotNullOrEmpty ]
30+ public string Path { get ; set ; }
31+
32+ /// <summary>
33+ /// Settings file format/extension (e.g. json, psd1). Defaults to first supported format.
34+ /// </summary>
35+ [ Parameter ]
36+ [ ArgumentCompleter ( typeof ( FileFormatCompleter ) ) ]
37+ [ ValidateNotNullOrEmpty ]
38+ public string FileFormat { get ; set ; }
39+
40+ /// <summary>
41+ /// Base content: 'Blank', 'All', or a preset name returned by Get-SettingPresets.
42+ /// 'Blank' -> minimal empty settings.
43+ /// 'All' -> include all rules and their configurable arguments with current defaults.
44+ /// preset -> copy preset contents.
45+ /// </summary>
46+ [ Parameter ]
47+ [ ArgumentCompleter ( typeof ( SettingsBaseCompleter ) ) ]
48+ [ ValidateNotNullOrEmpty ]
49+ public string Base { get ; set ; } = BaseOption_Blank ;
50+
51+ /// <summary>
52+ /// Overwrite existing file if present.
53+ /// </summary>
54+ [ Parameter ]
55+ public SwitchParameter Force { get ; set ; }
56+
57+ protected override void BeginProcessing ( )
58+ {
59+ Helper . Instance = new Helper ( SessionState . InvokeCommand ) ;
60+ Helper . Instance . Initialize ( ) ;
61+
62+ string [ ] rulePaths = Helper . ProcessCustomRulePaths ( null , SessionState , false ) ;
63+ ScriptAnalyzer . Instance . Initialize ( this , rulePaths , null , null , null , null == rulePaths ) ;
64+ }
65+
66+ protected override void ProcessRecord ( )
67+ {
68+ // Default Path
69+ if ( string . IsNullOrWhiteSpace ( Path ) )
70+ {
71+ Path = SessionState . Path . CurrentFileSystemLocation . ProviderPath ;
72+ }
73+
74+ // If user passed an existing file path, switch to its directory.
75+ if ( File . Exists ( Path ) )
76+ {
77+ Path = System . IO . Path . GetDirectoryName ( Path ) ;
78+ }
79+
80+ // Require the directory to already exist (do not create it).
81+ if ( ! Directory . Exists ( Path ) )
82+ {
83+ ThrowTerminatingError ( new ErrorRecord (
84+ new DirectoryNotFoundException ( $ "Directory '{ Path } ' does not exist.") ,
85+ "DIRECTORY_NOT_FOUND" ,
86+ ErrorCategory . ObjectNotFound ,
87+ Path ) ) ;
88+ return ;
89+ }
90+
91+ // Ensure FileSystem provider for target Path.
92+ ProviderInfo providerInfo ;
93+ try
94+ {
95+ SessionState . Path . GetResolvedProviderPathFromPSPath ( Path , out providerInfo ) ;
96+ }
97+ catch ( Exception ex )
98+ {
99+ ThrowTerminatingError ( new ErrorRecord (
100+ new InvalidOperationException ( $ "Cannot resolve path '{ Path } ': { ex . Message } ", ex ) ,
101+ "PATH_RESOLVE_FAILED" ,
102+ ErrorCategory . InvalidArgument ,
103+ Path ) ) ;
104+ return ;
105+ }
106+
107+ if ( ! string . Equals ( providerInfo . Name , "FileSystem" , StringComparison . OrdinalIgnoreCase ) )
108+ {
109+ ThrowTerminatingError ( new ErrorRecord (
110+ new InvalidOperationException ( "Target path must be in the FileSystem provider." ) ,
111+ "INVALID_PROVIDER" ,
112+ ErrorCategory . InvalidArgument ,
113+ Path ) ) ;
114+ }
115+
116+ // Default format to first supported.
117+ if ( string . IsNullOrWhiteSpace ( FileFormat ) )
118+ {
119+ FileFormat = Settings . GetSettingsFormats ( ) . First ( ) ;
120+ }
121+
122+ // Validate requested format.
123+ if ( ! Settings . GetSettingsFormats ( ) . Any ( f => string . Equals ( f , FileFormat , StringComparison . OrdinalIgnoreCase ) ) )
124+ {
125+ ThrowTerminatingError ( new ErrorRecord (
126+ new ArgumentException ( $ "Unsupported settings format '{ FileFormat } '.") ,
127+ "UNSUPPORTED_FORMAT" ,
128+ ErrorCategory . InvalidArgument ,
129+ FileFormat ) ) ;
130+ }
131+
132+ var targetFile = System . IO . Path . Combine ( Path , $ "{ Settings . DefaultSettingsFileName } .{ FileFormat } ") ;
133+
134+ if ( File . Exists ( targetFile ) && ! Force )
135+ {
136+ WriteWarning ( $ "Settings file already exists: { targetFile } . Use -Force to overwrite.") ;
137+ return ;
138+ }
139+
140+ SettingsData data ;
141+ try
142+ {
143+ data = BuildSettingsData ( ) ;
144+ }
145+ catch ( Exception ex )
146+ {
147+ ThrowTerminatingError ( new ErrorRecord (
148+ ex ,
149+ "BUILD_SETTINGS_FAILED" ,
150+ ErrorCategory . InvalidData ,
151+ Base ) ) ;
152+ return ;
153+ }
154+
155+ string content ;
156+ try
157+ {
158+ content = Settings . Serialize ( data , FileFormat ) ;
159+ }
160+ catch ( Exception ex )
161+ {
162+ ThrowTerminatingError ( new ErrorRecord (
163+ ex ,
164+ "SERIALIZE_FAILED" ,
165+ ErrorCategory . InvalidData ,
166+ FileFormat ) ) ;
167+ return ;
168+ }
169+
170+ if ( ShouldProcess ( targetFile , "Create settings file" ) )
171+ {
172+ try
173+ {
174+ File . WriteAllText ( targetFile , content ) ;
175+ WriteVerbose ( $ "Created settings file: { targetFile } ") ;
176+ }
177+ catch ( Exception ex )
178+ {
179+ ThrowTerminatingError ( new ErrorRecord (
180+ ex ,
181+ "CREATE_FILE_FAILED" ,
182+ ErrorCategory . InvalidData ,
183+ targetFile ) ) ;
184+ return ;
185+ }
186+ WriteObject ( targetFile ) ;
187+ }
188+ }
189+
190+ private SettingsData BuildSettingsData ( )
191+ {
192+ if ( string . Equals ( Base , BaseOption_Blank , StringComparison . OrdinalIgnoreCase ) )
193+ {
194+ return new SettingsData ( ) ; // empty snapshot
195+ }
196+
197+ if ( string . Equals ( Base , BaseOption_All , StringComparison . OrdinalIgnoreCase ) )
198+ {
199+ return BuildAllSettingsData ( ) ;
200+ }
201+
202+ // Preset
203+ var presetPath = Settings . TryResolvePreset ( Base ) ;
204+ if ( presetPath == null )
205+ {
206+ throw new FileNotFoundException ( $ "Preset '{ Base } ' not found.") ;
207+ }
208+ return Settings . Create ( presetPath ) ;
209+ }
210+
211+ private SettingsData BuildAllSettingsData ( )
212+ {
213+ var ruleNames = new List < string > ( ) ;
214+ var ruleArgs = new Dictionary < string , Dictionary < string , object > > ( StringComparer . OrdinalIgnoreCase ) ;
215+
216+ var modNames = ScriptAnalyzer . Instance . GetValidModulePaths ( ) ;
217+ var rules = ScriptAnalyzer . Instance . GetRule ( modNames , null ) ?? Enumerable . Empty < IRule > ( ) ;
218+
219+ foreach ( var rule in rules )
220+ {
221+ var name = rule . GetName ( ) ;
222+ ruleNames . Add ( name ) ;
223+
224+ if ( rule is ConfigurableRule configurable )
225+ {
226+ var props = rule . GetType ( ) . GetProperties ( BindingFlags . Instance | BindingFlags . Public ) ;
227+ var argDict = new Dictionary < string , object > ( StringComparer . OrdinalIgnoreCase ) ;
228+ foreach ( var p in props )
229+ {
230+ if ( p . GetCustomAttribute < ConfigurableRulePropertyAttribute > ( inherit : true ) == null )
231+ {
232+ continue ;
233+ }
234+ argDict [ p . Name ] = p . GetValue ( rule ) ;
235+ }
236+ if ( argDict . Count > 0 )
237+ {
238+ ruleArgs [ name ] = argDict ;
239+ }
240+ }
241+ }
242+
243+ return new SettingsData
244+ {
245+ IncludeRules = ruleNames ,
246+ RuleArguments = ruleArgs ,
247+ } ;
248+ }
249+
250+ #region Completers
251+
252+ private sealed class FileFormatCompleter : IArgumentCompleter
253+ {
254+ public IEnumerable < CompletionResult > CompleteArgument ( string commandName ,
255+ string parameterName , string wordToComplete , CommandAst commandAst ,
256+ IDictionary fakeBoundParameters )
257+ {
258+ foreach ( var fmt in Settings . GetSettingsFormats ( ) )
259+ {
260+ if ( fmt . StartsWith ( wordToComplete ?? string . Empty , StringComparison . OrdinalIgnoreCase ) )
261+ {
262+ yield return new CompletionResult ( fmt , fmt , CompletionResultType . ParameterValue , $ "Settings format '{ fmt } '") ;
263+ }
264+ }
265+ }
266+ }
267+
268+ private sealed class SettingsBaseCompleter : IArgumentCompleter
269+ {
270+ public IEnumerable < CompletionResult > CompleteArgument ( string commandName ,
271+ string parameterName , string wordToComplete , CommandAst commandAst ,
272+ IDictionary fakeBoundParameters )
273+ {
274+ var bases = new List < string > { BaseOption_Blank , BaseOption_All } ;
275+ bases . AddRange ( Settings . GetSettingPresets ( ) ) ;
276+
277+ foreach ( var b in bases )
278+ {
279+ if ( b . StartsWith ( wordToComplete ?? string . Empty , StringComparison . OrdinalIgnoreCase ) )
280+ {
281+ yield return new CompletionResult ( b , b , CompletionResultType . ParameterValue , $ "Base template '{ b } '") ;
282+ }
283+ }
284+ }
285+ }
286+
287+ #endregion
288+ }
289+ }
0 commit comments