diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 11f8b3e..44a2935 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore src - name: Build diff --git a/README.md b/README.md index 1304b48..3c98413 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,21 @@ To provision a domain account for the computer, you can use the following comman djoin.exe /provision /domain domainname /machine machinename /savefile filename ``` +### Specifying a computer name + +When you provide a computer name using the [`-ComputerName`][] argument, the Answer File Generator +does not check whether that computer name is valid. Specifying an invalid computer name will cause +Windows setup to fail when applying the answer file. Make sure you follow the +[rules for computer names](https://learn.microsoft.com/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-computername) +when choosing a name. + +If you do not specify a computer name, Windows will automatically choose one during installation. +In addition, you can also use the `#` character to generate a name containing a random number. + +Every occurrence of `#` in the provided computer name will be replaced with a digit between 0 and 9 +when generating the answer file. For example `PC-###` would be replaced with `PC-123` (or some +other random number). + ## Using JSON to provide options Because the large number of command line arguments may get unwieldy, the Answer File Generator @@ -315,7 +330,7 @@ The core functionality for generating answer files is implemented in the To build Answer File Generator, make sure you have the following installed: -- [Microsoft .Net 8.0 SDK](https://dotnet.microsoft.com/download) or later +- [Microsoft .Net 9.0 SDK](https://dotnet.microsoft.com/download) or later To build the application, library, and tests, simply use the `dotnet build` command in the `src` directory. You can run the unit tests using `dotnet test`. @@ -338,6 +353,7 @@ any other adverse effects caused by the use of answer files generated by this to [`-AutoLogonUser`]: doc/CommandLine.md#-autologonuser [`-AutoLogonCount`]: doc/CommandLine.md#-autologoncount [`-Feature`]: doc/CommandLine.md#-feature +[`-ComputerName`]: doc/CommandLine.md#-computername [`-FirstLogonCommand`]: doc/CommandLine.md#-firstlogoncommand [`-Install`]: doc/CommandLine.md#-install [`-InstallToDisk`]: doc/CommandLine.md#-installtodisk diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 64fa5aa..1c49f7f 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -1,5 +1,11 @@ # What's new in Answer File Generator +## Answer File Generator 2.1 (2025-03-09) + +- You can now [generate computer names containing a random number](../README.md#specifying-a-computer-name), + by using the `#` character in the name you provide. +- Improved error messages for certain kinds of invalid JSON input. + ## Answer File Generator 2.0 (2024-10-07) - The [`-DomainAccount`][] argument now allows you to specify users from different domains than the diff --git a/doc/CommandLine.md b/doc/CommandLine.md index cc51e41..10063ef 100644 --- a/doc/CommandLine.md +++ b/doc/CommandLine.md @@ -404,10 +404,14 @@ Required argument: -JoinDomain ### `-ComputerName` -The network name for the computer. If not specified, Windows will generate a default name. +The network name for the computer. If not specified, Windows will generate a default name. Any `#` +characters in the name will be replaced with a random digit between 0 and 9. For example, `PC-###` +would be replaced with `PC-123` (or some other random number). Must not be blank. +See [specifying a computer name](../README.md#specifying-a-computer-name). + ```yaml Value: Alias: -n @@ -485,7 +489,7 @@ See [first log-on commands and scripts](../README.md#first-log-on-commands-and-s ```yaml Value: (multiple allowed) -Aliases: -SetupScript, -s +Alias: -s ``` ### `-Language` diff --git a/doc/Library.md b/doc/Library.md index 638ee67..ce5fcf5 100644 --- a/doc/Library.md +++ b/doc/Library.md @@ -56,17 +56,17 @@ Version 2.0 of the library has a few breaking changes from version 1.x: - The [`DomainOptionsBase.DomainAccounts`][] property has a different type. - The `AnswerFileOptions.SetupScripts` property was renamed to [`FirstLogonScripts`][]. -[`AnswerFileGenerator.Generate`]: https://www.ookii.org/docs/answerfile-2.0/html/Overload_Ookii_AnswerFile_AnswerFileGenerator_Generate.htm -[`AnswerFileGenerator`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_AnswerFileGenerator.htm -[`AnswerFileOptions.DisplayResolution`]: https://www.ookii.org/docs/answerfile-2.0/html/P_Ookii_AnswerFile_AnswerFileOptions_DisplayResolution.htm -[`AnswerFileOptions.InstallOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/P_Ookii_AnswerFile_AnswerFileOptions_InstallOptions.htm -[`AnswerFileOptions.JoinDomain`]: https://www.ookii.org/docs/answerfile-2.0/html/P_Ookii_AnswerFile_AnswerFileOptions_JoinDomain.htm -[`AnswerFileOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_AnswerFileOptions.htm -[`CleanBiosOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_CleanBiosOptions.htm -[`CleanEfiOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_CleanEfiOptions.htm -[`DomainOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_DomainOptions.htm -[`DomainOptionsBase.DomainAccounts`]: https://www.ookii.org/docs/answerfile-2.0/html/P_Ookii_AnswerFile_DomainOptionsBase_DomainAccounts.htm -[`DomainOptionsBase`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_DomainOptionsBase.htm -[`ExistingPartitionOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_ExistingPartitionOptions.htm -[`FirstLogonScripts`]: https://www.ookii.org/docs/answerfile-2.0/html/P_Ookii_AnswerFile_AnswerFileOptions_FirstLogonScripts.htm -[`ManualInstallOptions`]: https://www.ookii.org/docs/answerfile-2.0/html/T_Ookii_AnswerFile_ManualInstallOptions.htm +[`AnswerFileGenerator.Generate`]: https://www.ookii.org/docs/answerfile-2.1/html/Overload_Ookii_AnswerFile_AnswerFileGenerator_Generate.htm +[`AnswerFileGenerator`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_AnswerFileGenerator.htm +[`AnswerFileOptions.DisplayResolution`]: https://www.ookii.org/docs/answerfile-2.1/html/P_Ookii_AnswerFile_AnswerFileOptions_DisplayResolution.htm +[`AnswerFileOptions.InstallOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/P_Ookii_AnswerFile_AnswerFileOptions_InstallOptions.htm +[`AnswerFileOptions.JoinDomain`]: https://www.ookii.org/docs/answerfile-2.1/html/P_Ookii_AnswerFile_AnswerFileOptions_JoinDomain.htm +[`AnswerFileOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_AnswerFileOptions.htm +[`CleanBiosOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_CleanBiosOptions.htm +[`CleanEfiOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_CleanEfiOptions.htm +[`DomainOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_DomainOptions.htm +[`DomainOptionsBase.DomainAccounts`]: https://www.ookii.org/docs/answerfile-2.1/html/P_Ookii_AnswerFile_DomainOptionsBase_DomainAccounts.htm +[`DomainOptionsBase`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_DomainOptionsBase.htm +[`ExistingPartitionOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_ExistingPartitionOptions.htm +[`FirstLogonScripts`]: https://www.ookii.org/docs/answerfile-2.1/html/P_Ookii_AnswerFile_AnswerFileOptions_FirstLogonScripts.htm +[`ManualInstallOptions`]: https://www.ookii.org/docs/answerfile-2.1/html/T_Ookii_AnswerFile_ManualInstallOptions.htm diff --git a/doc/Ookii.AnswerFile.shfbproj b/doc/Ookii.AnswerFile.shfbproj index a25b402..332aacb 100644 --- a/doc/Ookii.AnswerFile.shfbproj +++ b/doc/Ookii.AnswerFile.shfbproj @@ -54,7 +54,7 @@ False OnlyWarningsAndErrors 100 - Ookii.AnswerFile 2.0 Documentation + Ookii.AnswerFile 2.1 Documentation 1.0.0.0 MemberName AboveNamespaces diff --git a/doc/refs.json b/doc/refs.json index fdde923..b6e692f 100644 --- a/doc/refs.json +++ b/doc/refs.json @@ -1,6 +1,6 @@ { "#apiPrefix": "https://learn.microsoft.com/dotnet/api/", - "#prefix": "https://www.ookii.org/docs/answerfile-2.0/html/", + "#prefix": "https://www.ookii.org/docs/answerfile-2.1/html/", "#suffix": ".htm", "AnswerFileGenerator": "T_Ookii_AnswerFile_AnswerFileGenerator", "AnswerFileGenerator.Generate": "Overload_Ookii_AnswerFile_AnswerFileGenerator_Generate", diff --git a/src/.editorconfig b/src/.editorconfig index b2301f6..39df802 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -188,117 +188,37 @@ csharp_preserve_single_line_statements = true # Naming rules -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i -dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion -dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces -dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase +dotnet_naming_rule.types_and_namespaces_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_and_namespaces_should_be_pascal_case.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion -dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters -dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods -dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.consts_should_be_pascal_case.severity = warning +dotnet_naming_rule.consts_should_be_pascal_case.symbols = const_value +dotnet_naming_rule.consts_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties -dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.private_fields_should_be_underscored.severity = warning +dotnet_naming_rule.private_fields_should_be_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_underscored.style = underscored -dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.events_should_be_pascalcase.symbols = events -dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion -dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables -dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase - -dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion -dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants -dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase - -dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion -dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters -dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase - -dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields -dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion -dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields -dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase - -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase - -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums -dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions -dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.type_parameters_should_be_tpascal_case.severity = warning +dotnet_naming_rule.type_parameters_should_be_tpascal_case.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascal_case.style = tpascal_case # Symbol specifications -dotnet_naming_symbols.interfaces.applicable_kinds = interface -dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interfaces.required_modifiers = - -dotnet_naming_symbols.enums.applicable_kinds = enum -dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.enums.required_modifiers = - -dotnet_naming_symbols.events.applicable_kinds = event -dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.events.required_modifiers = - -dotnet_naming_symbols.methods.applicable_kinds = method -dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.methods.required_modifiers = - -dotnet_naming_symbols.properties.applicable_kinds = property -dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.properties.required_modifiers = - -dotnet_naming_symbols.public_fields.applicable_kinds = field -dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_fields.required_modifiers = +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_fields.required_modifiers = - -dotnet_naming_symbols.private_static_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_static_fields.required_modifiers = static - -dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = class, struct, interface, enum, namespace dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types_and_namespaces.required_modifiers = @@ -306,42 +226,18 @@ dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, meth dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.const_value.applicable_kinds = field, local +dotnet_naming_symbols.const_value.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.const_value.required_modifiers = const + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_fields.required_modifiers = + dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter dotnet_naming_symbols.type_parameters.applicable_accessibilities = * dotnet_naming_symbols.type_parameters.required_modifiers = -dotnet_naming_symbols.private_constant_fields.applicable_kinds = field -dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_constant_fields.required_modifiers = const - -dotnet_naming_symbols.local_variables.applicable_kinds = local -dotnet_naming_symbols.local_variables.applicable_accessibilities = local -dotnet_naming_symbols.local_variables.required_modifiers = - -dotnet_naming_symbols.local_constants.applicable_kinds = local -dotnet_naming_symbols.local_constants.applicable_accessibilities = local -dotnet_naming_symbols.local_constants.required_modifiers = const - -dotnet_naming_symbols.parameters.applicable_kinds = parameter -dotnet_naming_symbols.parameters.applicable_accessibilities = * -dotnet_naming_symbols.parameters.required_modifiers = - -dotnet_naming_symbols.public_constant_fields.applicable_kinds = field -dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_constant_fields.required_modifiers = const - -dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static - -dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static - -dotnet_naming_symbols.local_functions.applicable_kinds = local_function -dotnet_naming_symbols.local_functions.applicable_accessibilities = * -dotnet_naming_symbols.local_functions.required_modifiers = - # Naming styles dotnet_naming_style.pascal_case.required_prefix = @@ -354,20 +250,15 @@ dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.tpascalcase.required_prefix = T -dotnet_naming_style.tpascalcase.required_suffix = -dotnet_naming_style.tpascalcase.word_separator = -dotnet_naming_style.tpascalcase.capitalization = pascal_case +dotnet_naming_style.underscored.required_prefix = _ +dotnet_naming_style.underscored.required_suffix = +dotnet_naming_style.underscored.word_separator = +dotnet_naming_style.underscored.capitalization = camel_case -dotnet_naming_style._camelcase.required_prefix = _ -dotnet_naming_style._camelcase.required_suffix = -dotnet_naming_style._camelcase.word_separator = -dotnet_naming_style._camelcase.capitalization = camel_case - -dotnet_naming_style.camelcase.required_prefix = -dotnet_naming_style.camelcase.required_suffix = -dotnet_naming_style.camelcase.word_separator = -dotnet_naming_style.camelcase.capitalization = camel_case +dotnet_naming_style.tpascal_case.required_prefix = T +dotnet_naming_style.tpascal_case.required_suffix = +dotnet_naming_style.tpascal_case.word_separator = +dotnet_naming_style.tpascal_case.capitalization = pascal_case dotnet_diagnostic.IDE0051.severity = warning dotnet_diagnostic.IDE0052.severity = warning @@ -378,7 +269,10 @@ dotnet_diagnostic.IDE0073.severity = silent dotnet_diagnostic.IDE0060.severity = warning csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_primary_constructors = false:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion [*.{cs,vb}] dotnet_style_operator_placement_when_wrapping = beginning_of_line @@ -415,4 +309,9 @@ dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_event = false:silent -indent_style = space \ No newline at end of file +indent_style = space +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_diagnostic.CA1510.severity = none +dotnet_diagnostic.CA1511.severity = none +dotnet_diagnostic.CA1512.severity = none +dotnet_diagnostic.CA1513.severity = none \ No newline at end of file diff --git a/src/Create-Release.ps1 b/src/Create-Release.ps1 index b82f871..3338321 100644 Binary files a/src/Create-Release.ps1 and b/src/Create-Release.ps1 differ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 25d924d..1261e04 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,6 @@ Sven Groot Ookii.org Copyright (c) Sven Groot (Ookii.org) - 2.0.0 + 2.1.0 \ No newline at end of file diff --git a/src/GenerateAnswerFile/ArgumentCategory.cs b/src/GenerateAnswerFile/ArgumentCategory.cs index fd02071..f88b9b0 100644 --- a/src/GenerateAnswerFile/ArgumentCategory.cs +++ b/src/GenerateAnswerFile/ArgumentCategory.cs @@ -2,8 +2,12 @@ enum ArgumentCategory { + [ResourceDescription(nameof(Properties.Resources.CategoryInstall))] Install, + [ResourceDescription(nameof(Properties.Resources.CategoryUserAccounts))] UserAccounts, + [ResourceDescription(nameof(Properties.Resources.CategoryDomain))] Domain, + [ResourceDescription(nameof(Properties.Resources.CategoryOther))] Other, } diff --git a/src/GenerateAnswerFile/ArgumentCategoryAttribute.cs b/src/GenerateAnswerFile/ArgumentCategoryAttribute.cs deleted file mode 100644 index 70c3ab6..0000000 --- a/src/GenerateAnswerFile/ArgumentCategoryAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace GenerateAnswerFile; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] -class ArgumentCategoryAttribute : Attribute -{ - public ArgumentCategoryAttribute(ArgumentCategory category) - { - Category = category; - } - - public ArgumentCategory Category { get; } -} diff --git a/src/GenerateAnswerFile/Arguments.cs b/src/GenerateAnswerFile/Arguments.cs index 5c27f36..c9ba7ca 100644 --- a/src/GenerateAnswerFile/Arguments.cs +++ b/src/GenerateAnswerFile/Arguments.cs @@ -1,10 +1,6 @@ using Ookii.AnswerFile; using Ookii.CommandLine; -using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Validation; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; namespace GenerateAnswerFile; @@ -13,9 +9,8 @@ partial class Arguments : BaseArguments { #region Installation options - [CommandLineArgument("Feature")] + [CommandLineArgument("Feature", Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.FeaturesDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("c")] [ValidateInstallMethod(InstallMethod.ExistingPartition, InstallMethod.CleanBios, InstallMethod.CleanEfi, InstallMethod.Manual)] [Requires(nameof(WindowsVersion))] @@ -23,91 +18,79 @@ partial class Arguments : BaseArguments [MultiValueSeparator] public string[]? Features { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.InstallDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("i")] [ValidateEnumValue] public InstallMethod Install { get; set; } = InstallMethod.PreInstalled; - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.InstallToDiskDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("disk")] [ValidateInstallMethod(InstallMethod.ExistingPartition, InstallMethod.CleanEfi, InstallMethod.CleanBios)] [ValidateRange(0, null)] public int InstallToDisk { get; set; } = 0; - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.InstallToPartitionDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("part")] [ValidateInstallMethod(InstallMethod.CleanEfi, InstallMethod.CleanBios, InstallMethod.ExistingPartition)] [ValidateRange(1, null)] public int? InstallToPartition { get; set; } - [CommandLineArgument("Partition")] + [CommandLineArgument("Partition", Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.PartitionsDescription))] [ResourceValueDescription(nameof(Properties.Resources.PartitionsValueDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("p")] [ValidateInstallMethod(InstallMethod.CleanEfi, InstallMethod.CleanBios)] [MultiValueSeparator] public Partition[]? Partitions { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.ImageIndexDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("wim")] [ValidateInstallMethod(InstallMethod.ExistingPartition, InstallMethod.CleanEfi, InstallMethod.CleanBios)] public int ImageIndex { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.ProductKeyDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("key")] [ValidateNotWhiteSpace] public string? ProductKey { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Install)] [ResourceDescription(nameof(Properties.Resources.WindowsVersionDescription))] - [ArgumentCategory(ArgumentCategory.Install)] [Alias("v")] public Version? WindowsVersion { get; set; } - #endregion #region User account options - [CommandLineArgument("LocalAccount")] + [CommandLineArgument("LocalAccount", Category = ArgumentCategory.UserAccounts)] [ResourceDescription(nameof(Properties.Resources.LocalAccountsDescription))] [ResourceValueDescription(nameof(Properties.Resources.LocalCredentialValueDescription))] - [ArgumentCategory(ArgumentCategory.UserAccounts)] [Alias("a")] [MultiValueSeparator] public LocalCredential[]? LocalAccounts { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.UserAccounts)] [ResourceDescription(nameof(Properties.Resources.AutoLogonUserDescription))] [ResourceValueDescription(nameof(Properties.Resources.OptionalDomainUserValueDescription))] - [ArgumentCategory(ArgumentCategory.UserAccounts)] [Alias("alu")] [Requires(nameof(AutoLogonPassword))] [ValidateNotWhiteSpace] public DomainUser? AutoLogonUser { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.UserAccounts)] [ResourceDescription(nameof(Properties.Resources.AutoLogonPasswordDescription))] - [ArgumentCategory(ArgumentCategory.UserAccounts)] [Alias("alp")] [Requires(nameof(AutoLogonUser))] [ValidateNotWhiteSpace] public string? AutoLogonPassword { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.UserAccounts)] [ResourceDescription(nameof(Properties.Resources.AutoLogonCountDescription))] - [ArgumentCategory(ArgumentCategory.UserAccounts)] [Alias("alc")] [Requires(nameof(AutoLogonUser))] [ValidateRange(1, null)] @@ -117,59 +100,52 @@ partial class Arguments : BaseArguments #region Domain options - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.JoinDomainDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Requires(nameof(JoinDomainUser), nameof(JoinDomainPassword))] [Alias("jd")] [ValidateNotWhiteSpace] public string? JoinDomain { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.JoinDomainUserDescription))] [ResourceValueDescription(nameof(Properties.Resources.OptionalDomainUserValueDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Alias("jdu")] [Requires(nameof(JoinDomain))] [ValidateNotWhiteSpace] public DomainUser? JoinDomainUser { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.JoinDomainPasswordDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Alias("jdp")] [Requires(nameof(JoinDomain))] [ValidateNotWhiteSpace] public string? JoinDomainPassword { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.OUPathDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Alias("ou")] [Requires(nameof(JoinDomain))] [ValidateNotWhiteSpace] public string? OUPath { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.JoinDomainProvisioningFileDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [ResourceValueDescription(nameof(Properties.Resources.PathValueDescription))] [Prohibits(nameof(JoinDomain))] [Alias("jdpf")] public FileInfo? JoinDomainProvisioningFile { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.JoinDomainOfflineDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Requires(nameof(JoinDomainProvisioningFile))] [ValidateInstallMethod(InstallMethod.ExistingPartition, InstallMethod.CleanBios, InstallMethod.CleanEfi, InstallMethod.Manual)] [Alias("jdo")] public bool JoinDomainOffline { get; set; } - [CommandLineArgument("DomainAccount")] + [CommandLineArgument("DomainAccount", Category = ArgumentCategory.Domain)] [ResourceDescription(nameof(Properties.Resources.DomainAccountsDescription))] [ResourceValueDescription(nameof(Properties.Resources.DomainUserGroupValueDescription))] - [ArgumentCategory(ArgumentCategory.Domain)] [Alias("da")] [RequiresAnyOther(nameof(JoinDomain), nameof(JoinDomainProvisioningFile))] [ValidateNotWhiteSpace] @@ -180,77 +156,66 @@ partial class Arguments : BaseArguments #region Other options - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.ComputerNameDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("n")] [ValidateNotWhiteSpace] public string? ComputerName { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.DisableDefenderDesciption))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("d")] public bool DisableDefender { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.DisableCloudDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("dc")] public bool DisableCloud { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.DisableServerManagerDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("dsm")] public bool DisableServerManager { get; set; } - [CommandLineArgument("FirstLogonCommand")] + [CommandLineArgument("FirstLogonCommand", Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.FirstLogonCommandsDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("cmd")] [ValidateNotWhiteSpace] [MultiValueSeparator] public string[]? FirstLogonCommands { get; set; } - [CommandLineArgument("FirstLogonScript")] + [CommandLineArgument("FirstLogonScript", Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.FirstLogonScriptsDescription))] - [ArgumentCategory(ArgumentCategory.Other)] - [Alias("SetupScript")] + [Alias("SetupScript", IsHidden = true)] [Alias("s")] [ValidateNotWhiteSpace] [MultiValueSeparator] public string[]? FirstLogonScripts { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.EnableRemoteDesktopDescriptoin))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("rdp")] public bool EnableRemoteDesktop { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.DisplayResolutionDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("res")] public Resolution? DisplayResolution { get; set; } - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.LanguageDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("lang")] [ValidateNotWhiteSpace] public string Language { get; set; } = "en-US"; - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.ProcessorArchitectureDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [Alias("arch")] [ValidateNotWhiteSpace] public string ProcessorArchitecture { get; set; } = "amd64"; - [CommandLineArgument] + [CommandLineArgument(Category = ArgumentCategory.Other)] [ResourceDescription(nameof(Properties.Resources.TimeZoneDescription))] - [ArgumentCategory(ArgumentCategory.Other)] [ValidateNotWhiteSpace] public string TimeZone { get; set; } = "Pacific Standard Time"; @@ -277,7 +242,7 @@ public static CancelMode MarkdownHelp(CommandLineParser parser) #endif -#endregion + #endregion public AnswerFileOptions ToOptions() { diff --git a/src/GenerateAnswerFile/CustomUsageWriter.cs b/src/GenerateAnswerFile/CustomUsageWriter.cs index fdf3546..ef90f92 100644 --- a/src/GenerateAnswerFile/CustomUsageWriter.cs +++ b/src/GenerateAnswerFile/CustomUsageWriter.cs @@ -1,7 +1,6 @@ using Ookii.CommandLine; using Ookii.CommandLine.Validation; using System.Globalization; -using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; @@ -34,57 +33,33 @@ protected override void WriteMoreInfoMessage() WriteLine(string.Format(CultureInfo.CurrentCulture, Properties.Resources.UsageHelpMoreInfoFormat, ExecutableName)); } - protected override void WriteArgumentDescriptions() +#if DEBUG + + protected override void WriteArgumentDescriptionListHeader() { - var groups = Parser.Arguments.GroupBy(a => GetCategory(a)).OrderBy(c => c.Key); - foreach (var group in groups) + if (!Markdown) { - if (group.Key is ArgumentCategory category) - { - if (Markdown) - { - WriteLine($"## {GetCategoryDescription(category).TrimEnd(':')}"); - WriteLine(); - } - else - { - Writer.ResetIndent(); - WriteColor(UsagePrefixColor); - Write(GetCategoryDescription(category)); - ResetColor(); - WriteLine(); - WriteLine(); - } - } - else if (Markdown) - { - WriteLine("## General options"); - WriteLine(); - } + base.WriteArgumentDescriptionListHeader(); + return; + } - if (ShouldIndent && !Markdown) - { - Writer.Indent = ArgumentDescriptionIndent; - } + Writer.ResetIndent(); + Writer.Indent = 0; + WriteLine("## General options"); + WriteLine(); + } - foreach (var argument in group) - { - if (!argument.IsHidden) - { - if (Markdown) - { - WriteArgumentMarkdown(argument); - } - else - { - WriteArgumentDescription(argument); - } - } - } + protected override void WriteArgumentCategoryHeader(CategoryInfo category) + { + if (!Markdown) + { + base.WriteArgumentCategoryHeader(category); + return; } - } -#if DEBUG + WriteLine($"## {category.Description.TrimEnd(':')}"); + WriteLine(); + } protected override void WriteParserUsageSyntax() { @@ -122,9 +97,9 @@ protected override void WriteParserUsageSyntax() protected override void WriteArgumentName(string argumentName, string prefix) { if (!Markdown) - { + { base.WriteArgumentName(argumentName, prefix); - return; + return; } Write($"{prefix}{argumentName}"); @@ -141,12 +116,13 @@ protected override void WriteValueDescription(string valueDescription) Write($"<{valueDescription}>"); } -#endif - - - private void WriteArgumentMarkdown(CommandLineArgument argument) + protected override void WriteArgumentDescription(CommandLineArgument argument) { -#if DEBUG + if (!Markdown) + { + base.WriteArgumentDescription(argument); + return; + } WriteLine($"### `-{argument.ArgumentName}`"); WriteLine(); @@ -191,7 +167,7 @@ private void WriteArgumentMarkdown(CommandLineArgument argument) WriteLine(); foreach (var validator in argument.Validators) { - if (validator is not (RequiresAttribute or ProhibitsAttribute or RequiresAnyOtherAttribute or + if (validator is not (RequiresAttribute or ProhibitsAttribute or RequiresAnyOtherAttribute or ValidateInstallMethodAttribute or ValidateEnumValueAttribute)) { var help = validator.GetUsageHelp(argument); @@ -233,10 +209,12 @@ private void WriteArgumentMarkdown(CommandLineArgument argument) WriteLine(); var prefix = Parser.ArgumentNamePrefixes[0]; - if (argument.Aliases.Length > 0) + var aliases = argument.Aliases.Where(a => !a.IsHidden); + var aliasCount = aliases.Count(); + if (aliasCount > 0) { - var plural = argument.Aliases.Length > 1 ? "es" : ""; - WriteLine($"Alias{plural}: {string.Join(", ", argument.Aliases.Select(a => prefix + a))}"); + var plural = aliasCount > 1 ? "es" : ""; + WriteLine($"Alias{plural}: {string.Join(", ", aliases.Select(a => prefix + a.Alias))}"); } if (argument.IsRequired) @@ -284,20 +262,8 @@ private void WriteArgumentMarkdown(CommandLineArgument argument) WriteLine("```"); WriteLine(); -#endif } - private static ArgumentCategory? GetCategory(CommandLineArgument argument) - => argument.Member?.GetCustomAttribute()?.Category; +#endif - private static string GetCategoryDescription(ArgumentCategory category) - { - return category switch - { - ArgumentCategory.Install => Properties.Resources.CategoryInstall, - ArgumentCategory.UserAccounts => Properties.Resources.CategoryUserAccounts, - ArgumentCategory.Domain => Properties.Resources.CategoryDomain, - _ => Properties.Resources.CategoryOther, - }; - } } diff --git a/src/GenerateAnswerFile/GenerateAnswerFile.csproj b/src/GenerateAnswerFile/GenerateAnswerFile.csproj index eb633d6..1d49758 100644 --- a/src/GenerateAnswerFile/GenerateAnswerFile.csproj +++ b/src/GenerateAnswerFile/GenerateAnswerFile.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable Windows Answer File Generator @@ -15,7 +15,7 @@ - + diff --git a/src/GenerateAnswerFile/Properties/Resources.Designer.cs b/src/GenerateAnswerFile/Properties/Resources.Designer.cs index 3f419c6..7fb49a3 100644 --- a/src/GenerateAnswerFile/Properties/Resources.Designer.cs +++ b/src/GenerateAnswerFile/Properties/Resources.Designer.cs @@ -88,7 +88,7 @@ internal static string AutoLogonPasswordDescription { } /// - /// Looks up a localized string similar to The name of a user to automatically log on, in the format 'domain\user', or just 'user' for local users. If not specified, automatic logon will not be used.. + /// Looks up a localized string similar to The name of a user to automatically log on, in the format 'domain\user', or just 'user' for local users. If not specified, automatic log-on will not be used.. /// internal static string AutoLogonUserDescription { get { @@ -133,7 +133,7 @@ internal static string CategoryUserAccounts { } /// - /// Looks up a localized string similar to The network name for the computer.. + /// Looks up a localized string similar to The network name for the computer. If not specified, Windows will generate a default name. Any '#' characters in the name will be replaced with a random digit between 0 and 9. For example, 'PC-###' would be replaced with 'PC-123' (or some other random number).. /// internal static string ComputerNameDescription { get { @@ -223,7 +223,7 @@ internal static string FeaturesDescription { } /// - /// Looks up a localized string similar to A command to run during first logon. Can have multiple values. Commands are executed before the scripts specified by -FirstLogonScript.. + /// Looks up a localized string similar to A command to run during first logon. Can have multiple values. All commands are executed before the scripts specified by -FirstLogonScript, in the order specified.. /// internal static string FirstLogonCommandsDescription { get { @@ -232,7 +232,7 @@ internal static string FirstLogonCommandsDescription { } /// - /// Looks up a localized string similar to The full path of a Windows PowerShell script to run during first logon. Can have multiple values. Scripts are executed after the commands specified by -FirstLogonCommand.. + /// Looks up a localized string similar to The full path of a Windows PowerShell script to run during first log-on, plus arguments. Can have multiple values. Scripts are executed after the commands specified by -FirstLogonCommand, in the order specified.. /// internal static string FirstLogonScriptsDescription { get { @@ -277,7 +277,7 @@ internal static string InstallToPartitionDescription { } /// - /// Looks up a localized string similar to Need at least one method.. + /// Looks up a localized string similar to At least one installation method must be provided.. /// internal static string InvalidMethodCount { get { @@ -304,7 +304,7 @@ internal static string JoinDomainOfflineDescription { } /// - /// Looks up a localized string similar to The password of the user specified by -JoinDomainUser. Will be stored in plain text.. + /// Looks up a localized string similar to The password of the user specified by -JoinDomainUser. This will be stored in plain text in the answer file.. /// internal static string JoinDomainPasswordDescription { get { @@ -331,7 +331,7 @@ internal static string JoinDomainUserDescription { } /// - /// Looks up a localized string similar to JSON input was found; additional arguments are available if JSON input is not provided. For more information, see: https://github.com/SvenGroot/GenerateAnswerFile. + /// Looks up a localized string similar to JSON input was found; additional command line arguments are available if JSON input is not provided. For more information, see: https://github.com/SvenGroot/GenerateAnswerFile. /// internal static string JsonUsageFooter { get { diff --git a/src/GenerateAnswerFile/Properties/Resources.resx b/src/GenerateAnswerFile/Properties/Resources.resx index 0e7f31b..7ff13d9 100644 --- a/src/GenerateAnswerFile/Properties/Resources.resx +++ b/src/GenerateAnswerFile/Properties/Resources.resx @@ -142,7 +142,7 @@ User account options: - The network name for the computer. If not specified, Windows will generate a default name. + The network name for the computer. If not specified, Windows will generate a default name. Any '#' characters in the name will be replaced with a random digit between 0 and 9. For example, 'PC-###' would be replaced with 'PC-123' (or some other random number). Disable Windows cloud consumer features. This prevents auto-installation of recommended store apps. diff --git a/src/GenerateAnswerFile/RequiresAnyOtherAttribute.cs b/src/GenerateAnswerFile/RequiresAnyOtherAttribute.cs index 9b5bf39..edef3d4 100644 --- a/src/GenerateAnswerFile/RequiresAnyOtherAttribute.cs +++ b/src/GenerateAnswerFile/RequiresAnyOtherAttribute.cs @@ -6,19 +6,17 @@ namespace GenerateAnswerFile; // Basically RequiresAny, but applied to an argument rather than a class. internal class RequiresAnyOtherAttribute : ArgumentValidationWithHelpAttribute { - public RequiresAnyOtherAttribute(params string[] arguments) + public RequiresAnyOtherAttribute(params string[] arguments) { Arguments = arguments; } public string[] Arguments { get; } - public override ValidationMode Mode => ValidationMode.AfterParsing; - public override CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.DependencyFailed; - public override bool IsValid(CommandLineArgument argument, object? value) - => Arguments.Any(a => argument.Parser.GetArgument(a)!.HasValue); + public override bool IsValidPostParsing(CommandLineArgument argument) + => !argument.HasValue || Arguments.Any(a => argument.Parser.GetArgument(a)!.HasValue); public override string GetErrorMessage(CommandLineArgument argument, object? value) => string.Format(Properties.Resources.RequiresAnyOtherErrorFormat, argument.ArgumentName, string.Join(", ", Arguments.Select(a => $"-{a}"))); diff --git a/src/GenerateAnswerFile/ResourceUsageFooterAtribute.cs b/src/GenerateAnswerFile/ResourceUsageFooterAtribute.cs index 26b36c2..672adb5 100644 --- a/src/GenerateAnswerFile/ResourceUsageFooterAtribute.cs +++ b/src/GenerateAnswerFile/ResourceUsageFooterAtribute.cs @@ -1,5 +1,4 @@ using Ookii.CommandLine; -using System.ComponentModel; namespace GenerateAnswerFile; diff --git a/src/GenerateAnswerFile/ValidateInstallMethodAttribute.cs b/src/GenerateAnswerFile/ValidateInstallMethodAttribute.cs index eade305..2b32325 100644 --- a/src/GenerateAnswerFile/ValidateInstallMethodAttribute.cs +++ b/src/GenerateAnswerFile/ValidateInstallMethodAttribute.cs @@ -10,7 +10,7 @@ class ValidateInstallMethodAttribute : ArgumentValidationWithHelpAttribute private readonly InstallMethod[] _methods; public ValidateInstallMethodAttribute(params InstallMethod[] methods) - { + { if (methods.Length == 0) { throw new ArgumentException(Properties.Resources.InvalidMethodCount, nameof(methods)); @@ -21,10 +21,13 @@ public ValidateInstallMethodAttribute(params InstallMethod[] methods) public InstallMethod[] Methods => _methods; - public override ValidationMode Mode => ValidationMode.AfterParsing; - - public override bool IsValid(CommandLineArgument argument, object? value) + public override bool IsValidPostParsing(CommandLineArgument argument) { + if (!argument.HasValue) + { + return true; + } + var installArg = argument.Parser.GetArgument(nameof(Arguments.Install))!; var method = ((InstallMethod?)installArg.Value) ?? InstallMethod.PreInstalled; return _methods.Contains(method); diff --git a/src/GenerateAnswerFile/mdhelp.json b/src/GenerateAnswerFile/mdhelp.json index 79b5f9f..088e0df 100644 --- a/src/GenerateAnswerFile/mdhelp.json +++ b/src/GenerateAnswerFile/mdhelp.json @@ -3,6 +3,7 @@ "Override": true, "Text": "Displays a help message." }, + "ComputerName": "See [specifying a computer name](../README.md#specifying-a-computer-name).", "Feature": "See [optional features](../README.md#optional-features).", "ImageIndex": "See [selecting the edition to install](../README.md#selecting-the-edition-to-install).", "Install": "See [installation method](../README.md#installation-method).", diff --git a/src/Ookii.AnswerFile.Tests/AnswerFileGeneratorTests.cs b/src/Ookii.AnswerFile.Tests/AnswerFileGeneratorTests.cs index 1bbc58f..dda8871 100644 --- a/src/Ookii.AnswerFile.Tests/AnswerFileGeneratorTests.cs +++ b/src/Ookii.AnswerFile.Tests/AnswerFileGeneratorTests.cs @@ -1,6 +1,3 @@ -using System.Drawing; -using System.Runtime.CompilerServices; - namespace Ookii.AnswerFile.Tests; [TestClass] @@ -24,7 +21,7 @@ public void TestGeneratePreInstalled() Count = 9999, }, LocalAccounts = - { + { new LocalCredential("MyAccount", "Password"), new LocalCredential("MyAccount2", "Password2", "Users") }, diff --git a/src/Ookii.AnswerFile.Tests/AnswerFileOptionsTests.cs b/src/Ookii.AnswerFile.Tests/AnswerFileOptionsTests.cs index c37cc97..ce974ed 100644 --- a/src/Ookii.AnswerFile.Tests/AnswerFileOptionsTests.cs +++ b/src/Ookii.AnswerFile.Tests/AnswerFileOptionsTests.cs @@ -1,5 +1,4 @@ -using System.Drawing; -using System.Text.Json; +using System.Text.RegularExpressions; namespace Ookii.AnswerFile.Tests; @@ -193,4 +192,25 @@ public void TestJsonSerializationManual() Assert.AreEqual(new Version(10, 0, 22000, 1), install.OptionalFeatures.WindowsVersion); CollectionAssert.AreEqual(new[] { "Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform" }, install.OptionalFeatures.Features); } + + [TestMethod] + public void TestRandomComputerName() + { + var options = new AnswerFileOptions() + { + ComputerName = "test-####", + }; + + Assert.IsTrue(Regex.IsMatch(options.ComputerName, @"^test-\d{4}$")); + options.ComputerName = "foo#bar"; + Assert.IsTrue(Regex.IsMatch(options.ComputerName, @"^foo\dbar$")); + options.ComputerName = "####################"; + Assert.IsTrue(Regex.IsMatch(options.ComputerName, @"^\d{20}$")); + options.ComputerName = "test-###-#####"; + Assert.IsTrue(Regex.IsMatch(options.ComputerName, @"^test-\d{3}-\d{5}$")); + + var json = "{\"ComputerName\": \"test-####\"}"; + options = AnswerFileOptions.FromJson(json); + Assert.IsTrue(Regex.IsMatch(options!.ComputerName!, @"^test-\d{4}$")); + } } diff --git a/src/Ookii.AnswerFile.Tests/DomainUserGroupTests.cs b/src/Ookii.AnswerFile.Tests/DomainUserGroupTests.cs index c9542a1..5de605c 100644 --- a/src/Ookii.AnswerFile.Tests/DomainUserGroupTests.cs +++ b/src/Ookii.AnswerFile.Tests/DomainUserGroupTests.cs @@ -1,6 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Ookii.AnswerFile.Tests; +namespace Ookii.AnswerFile.Tests; [TestClass] public class DomainUserGroupTests diff --git a/src/Ookii.AnswerFile.Tests/LocalCredentialTests.cs b/src/Ookii.AnswerFile.Tests/LocalCredentialTests.cs index 9eaf4a2..b65ca16 100644 --- a/src/Ookii.AnswerFile.Tests/LocalCredentialTests.cs +++ b/src/Ookii.AnswerFile.Tests/LocalCredentialTests.cs @@ -1,6 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Ookii.AnswerFile.Tests; +namespace Ookii.AnswerFile.Tests; [TestClass] public class LocalCredentialTests diff --git a/src/Ookii.AnswerFile.Tests/Ookii.AnswerFile.Tests.csproj b/src/Ookii.AnswerFile.Tests/Ookii.AnswerFile.Tests.csproj index bce9d30..7579362 100644 --- a/src/Ookii.AnswerFile.Tests/Ookii.AnswerFile.Tests.csproj +++ b/src/Ookii.AnswerFile.Tests/Ookii.AnswerFile.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable @@ -76,8 +76,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ookii.AnswerFile.Tests/Usings.cs b/src/Ookii.AnswerFile.Tests/Usings.cs index ab67c7e..540383d 100644 --- a/src/Ookii.AnswerFile.Tests/Usings.cs +++ b/src/Ookii.AnswerFile.Tests/Usings.cs @@ -1 +1 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Ookii.AnswerFile/AnswerFileGenerator.cs b/src/Ookii.AnswerFile/AnswerFileGenerator.cs index 12ce16e..cb2e535 100644 --- a/src/Ookii.AnswerFile/AnswerFileGenerator.cs +++ b/src/Ookii.AnswerFile/AnswerFileGenerator.cs @@ -1,7 +1,4 @@ -using System.Data.Common; -using System.Drawing; -using System.Reflection; -using System.Text; +using System.Text; using System.Xml; namespace Ookii.AnswerFile; @@ -21,7 +18,7 @@ public class AnswerFileGenerator { internal const string PublicKeyToken = "31bf3856ad364e35"; - private static readonly XmlWriterSettings XmlSettings = new() + private static readonly XmlWriterSettings _xmlSettings = new() { Indent = true, NamespaceHandling = NamespaceHandling.OmitDuplicates, @@ -77,7 +74,7 @@ public static void Generate(XmlWriter writer, AnswerFileOptions options) /// public static void Generate(string outputPath, AnswerFileOptions options) { - using var writer = XmlWriter.Create(outputPath, XmlSettings); + using var writer = XmlWriter.Create(outputPath, _xmlSettings); Generate(writer, options); } @@ -92,7 +89,7 @@ public static void Generate(string outputPath, AnswerFileOptions options) /// public static void Generate(TextWriter writer, AnswerFileOptions options) { - using var xmlWriter = XmlWriter.Create(writer, XmlSettings); + using var xmlWriter = XmlWriter.Create(writer, _xmlSettings); Generate(xmlWriter, options); } @@ -334,7 +331,7 @@ private void GenerateOobePass() using (var firstLogon = Writer.WriteAutoCloseElement("FirstLogonCommands")) { int order = 1; - + // Work around the LogonCount issue if the count is 1 (see above). if (Options.AutoLogon?.Count == 1) { diff --git a/src/Ookii.AnswerFile/AnswerFileOptions.cs b/src/Ookii.AnswerFile/AnswerFileOptions.cs index 5be2ab2..7b7ebd6 100644 --- a/src/Ookii.AnswerFile/AnswerFileOptions.cs +++ b/src/Ookii.AnswerFile/AnswerFileOptions.cs @@ -1,7 +1,7 @@ using System.Collections.ObjectModel; -using System.Drawing; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace Ookii.AnswerFile; @@ -10,14 +10,13 @@ namespace Ookii.AnswerFile; /// class. /// /// -public class AnswerFileOptions +public partial class AnswerFileOptions { + private string? _computerName; private Collection? _localAccounts; private Collection? _firstLogonCommands; private Collection? _firstLogonScripts; -#pragma warning disable CA1822 // Mark members as static - /// /// Gets the schema that can be used for validation of the JSON representation of this object. /// @@ -29,8 +28,6 @@ public class AnswerFileOptions [JsonPropertyName("$schema")] public string JsonSchema => "https://www.ookii.org/Link/AnswerFileJsonSchema-2.0"; -#pragma warning restore CA1822 // Mark members as static - /// /// Gets or sets the installation method to use, along with the options for that method. /// @@ -66,7 +63,32 @@ public class AnswerFileOptions /// The computer name, or to let Windows pick a computer name. The /// default value is . /// - public string? ComputerName { get; set; } + /// + /// + /// If this property is set to a value containing the # character, each # will be replaced + /// with a random digit between 0 and 9. For example, the value "PC-###" will be replaced with + /// "PC-123" (or any other random number). + /// + /// + /// While random numbers can be used to generate a distinct computer name, it is not + /// necessarily guaranteed to be a unique name on the network. + /// + /// + public string? ComputerName + { + get => _computerName; + set + { + if (value != null) + { + // Replace # with random digits. + value = RandomNumberRegex().Replace(value, + m => Random.Shared.Next().ToString(new string('0', m.Length)).Substring(0, m.Length)); + } + + _computerName = value; + } + } /// /// Gets or sets a value which indicates whether Windows Defender is enabled after installation. @@ -286,4 +308,10 @@ public Collection FirstLogonScripts /// A JSON representation of the current instance. public string ToJson() => JsonSerializer.Serialize(this, typeof(AnswerFileOptions), SourceGenerationContext.Default); + + // This regex is used to replace the # characters in the computer name with random digits. + // It processes groups of # in batches of 9, to avoid exceeding the maximum value of an int + // for a single random number. + [GeneratedRegex("#{1,9}")] + private static partial Regex RandomNumberRegex(); } diff --git a/src/Ookii.AnswerFile/AutoCloseElement.cs b/src/Ookii.AnswerFile/AutoCloseElement.cs index 77412ee..61c6627 100644 --- a/src/Ookii.AnswerFile/AutoCloseElement.cs +++ b/src/Ookii.AnswerFile/AutoCloseElement.cs @@ -5,7 +5,7 @@ namespace Ookii.AnswerFile; class AutoCloseElement : IDisposable { private readonly XmlWriter _writer; - + public AutoCloseElement(XmlWriter writer) { _writer = writer; diff --git a/src/Ookii.AnswerFile/CleanEfiOptions.cs b/src/Ookii.AnswerFile/CleanEfiOptions.cs index 93babf9..05277f5 100644 --- a/src/Ookii.AnswerFile/CleanEfiOptions.cs +++ b/src/Ookii.AnswerFile/CleanEfiOptions.cs @@ -1,6 +1,4 @@ -using System.Collections.ObjectModel; - -namespace Ookii.AnswerFile; +namespace Ookii.AnswerFile; /// /// Provides options for a clean installation on UEFI-based systems. diff --git a/src/Ookii.AnswerFile/CleanOptionsBase.cs b/src/Ookii.AnswerFile/CleanOptionsBase.cs index c72b961..9f371c0 100644 --- a/src/Ookii.AnswerFile/CleanOptionsBase.cs +++ b/src/Ookii.AnswerFile/CleanOptionsBase.cs @@ -1,5 +1,4 @@ -using System.CodeDom.Compiler; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Text.Json.Serialization; namespace Ookii.AnswerFile; diff --git a/src/Ookii.AnswerFile/DomainOptions.cs b/src/Ookii.AnswerFile/DomainOptions.cs index 9ecb268..3dcb8e7 100644 --- a/src/Ookii.AnswerFile/DomainOptions.cs +++ b/src/Ookii.AnswerFile/DomainOptions.cs @@ -1,6 +1,4 @@ -using System.Collections.ObjectModel; - -namespace Ookii.AnswerFile; +namespace Ookii.AnswerFile; /// /// Provides options for joining a domain. diff --git a/src/Ookii.AnswerFile/DomainUserGroup.cs b/src/Ookii.AnswerFile/DomainUserGroup.cs index 96c3580..fd938bd 100644 --- a/src/Ookii.AnswerFile/DomainUserGroup.cs +++ b/src/Ookii.AnswerFile/DomainUserGroup.cs @@ -1,7 +1,4 @@ -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace Ookii.AnswerFile; +namespace Ookii.AnswerFile; /// /// Represents a domain user and the local group to which the user should be added. diff --git a/src/Ookii.AnswerFile/LocalCredential.cs b/src/Ookii.AnswerFile/LocalCredential.cs index e78597c..ffe88c0 100644 --- a/src/Ookii.AnswerFile/LocalCredential.cs +++ b/src/Ookii.AnswerFile/LocalCredential.cs @@ -1,6 +1,4 @@ -using System.Text.RegularExpressions; - -namespace Ookii.AnswerFile; +namespace Ookii.AnswerFile; /// /// Provides credentials for a local user account. diff --git a/src/Ookii.AnswerFile/Ookii.AnswerFile.csproj b/src/Ookii.AnswerFile/Ookii.AnswerFile.csproj index 9fa6538..970e9e1 100644 --- a/src/Ookii.AnswerFile/Ookii.AnswerFile.csproj +++ b/src/Ookii.AnswerFile/Ookii.AnswerFile.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0;net8.0 enable enable True @@ -30,6 +30,7 @@ + diff --git a/src/Ookii.AnswerFile/OptionalFeatures.cs b/src/Ookii.AnswerFile/OptionalFeatures.cs index 70b4bbc..a87b541 100644 --- a/src/Ookii.AnswerFile/OptionalFeatures.cs +++ b/src/Ookii.AnswerFile/OptionalFeatures.cs @@ -59,7 +59,7 @@ internal void GenerateServicingPass(AnswerFileGenerator generator) foreach (var component in Features) { - generator.Writer.WriteEmptyElement("selection", new KeyValueList { + generator.Writer.WriteEmptyElement("selection", new KeyValueList { { "name", component }, { "state", "true" }, }); diff --git a/src/Ookii.AnswerFile/Properties/Resources.Designer.cs b/src/Ookii.AnswerFile/Properties/Resources.Designer.cs index 7a5ebb7..64c3722 100644 --- a/src/Ookii.AnswerFile/Properties/Resources.Designer.cs +++ b/src/Ookii.AnswerFile/Properties/Resources.Designer.cs @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to You must specify a domain for all domain users to add to a local group when using provisioning to join a domain.. + /// Looks up a localized string similar to All domain accounts to add to a local group must use the format 'domain\user' or 'group:domain\user' when using provisioning to join a domain.. /// internal static string DomainAccountWithoutDomain { get { @@ -70,7 +70,7 @@ internal static string DomainAccountWithoutDomain { } /// - /// Looks up a localized string similar to Format is not 'user,password'.. + /// Looks up a localized string similar to Invalid credential value, expected format 'user,password'.. /// internal static string InvalidLocalCredential { get { @@ -88,7 +88,7 @@ internal static string InvalidResolution { } /// - /// Looks up a localized string similar to Could not determine the partition to install to.. + /// Looks up a localized string similar to The partition to install to could not be determined.. /// internal static string UnknownTargetPartition { get { diff --git a/src/Ookii.AnswerFile/ProvisionedDomainOptions.cs b/src/Ookii.AnswerFile/ProvisionedDomainOptions.cs index 14fcd36..c2f7651 100644 --- a/src/Ookii.AnswerFile/ProvisionedDomainOptions.cs +++ b/src/Ookii.AnswerFile/ProvisionedDomainOptions.cs @@ -1,7 +1,4 @@ -using System.IO; -using System.Net; - -namespace Ookii.AnswerFile; +namespace Ookii.AnswerFile; /// /// Provides options for joining a domain using a provisioned computer account. diff --git a/src/Ookii.AnswerFile/Resolution.cs b/src/Ookii.AnswerFile/Resolution.cs index df6d5a0..52e0df5 100644 --- a/src/Ookii.AnswerFile/Resolution.cs +++ b/src/Ookii.AnswerFile/Resolution.cs @@ -1,8 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using System.Drawing; -using System.Text.Json.Serialization; -using System.Text.Json; using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Ookii.AnswerFile; diff --git a/src/Ookii.AnswerFile/SourceGenerationContext.cs b/src/Ookii.AnswerFile/SourceGenerationContext.cs index 722c6ba..809c2e3 100644 --- a/src/Ookii.AnswerFile/SourceGenerationContext.cs +++ b/src/Ookii.AnswerFile/SourceGenerationContext.cs @@ -11,6 +11,8 @@ namespace Ookii.AnswerFile; // value of the type. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, ReadCommentHandling = JsonCommentHandling.Skip, + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = true, WriteIndented = true)] partial class SourceGenerationContext : JsonSerializerContext {