13 Commits

Author SHA1 Message Date
Matt Nadareski
47d067df1c Add editorconfig, fix issues 2026-01-25 17:14:38 -05:00
Matt Nadareski
90bfcc8fe2 Format GHA definitions 2025-11-17 08:36:23 -05:00
Matt Nadareski
be081c701d Bump version 2025-11-12 19:49:06 -05:00
Matt Nadareski
66b0e27d4e Add support for .NET 10 2025-11-11 16:43:21 -05:00
Matt Nadareski
573fe2e160 Update rolling tag 2025-10-26 20:22:42 -04:00
Matt Nadareski
6ad9fa4521 Fix split method in custom help 2025-10-07 12:37:54 -04:00
Matt Nadareski
a261f428f2 Add and allow custom help text 2025-10-07 12:36:00 -04:00
Matt Nadareski
5011331164 Clarify the case where default also has real flags 2025-10-07 12:27:51 -04:00
Matt Nadareski
e257c30892 Clarify help printing with default features 2025-10-07 12:14:18 -04:00
Matt Nadareski
029cfe6dc6 Add optional default feature to CommandSet 2025-10-07 12:13:11 -04:00
Matt Nadareski
f38b8734b5 Simplify the method name 2025-10-07 12:07:41 -04:00
Matt Nadareski
df54b92031 Add AddChildrenFrom method to CommandSet 2025-10-07 12:03:00 -04:00
Matt Nadareski
39acb90dbc Use All help by default instead of top-level only 2025-10-06 08:50:21 -04:00
21 changed files with 365 additions and 85 deletions

167
.editorconfig Normal file
View File

@@ -0,0 +1,167 @@
# top-most EditorConfig file
root = true
# C# files
[*.cs]
# Indentation and spacing
charset = utf-8
indent_size = 4
indent_style = space
tab_width = 4
trim_trailing_whitespace = true
# New line preferences
end_of_line = lf
insert_final_newline = true
max_line_length = unset
# using directive preferences
csharp_using_directive_placement = outside_namespace
dotnet_diagnostic.IDE0005.severity = error
# Code-block preferences
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_top_level_statements = false
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_inlined_variable_declaration = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
dotnet_diagnostic.IDE0001.severity = warning
dotnet_diagnostic.IDE0002.severity = warning
dotnet_diagnostic.IDE0004.severity = warning
dotnet_diagnostic.IDE0010.severity = error
dotnet_diagnostic.IDE0051.severity = warning
dotnet_diagnostic.IDE0052.severity = warning
dotnet_diagnostic.IDE0072.severity = warning
dotnet_diagnostic.IDE0080.severity = warning
dotnet_diagnostic.IDE0100.severity = error
dotnet_diagnostic.IDE0110.severity = error
dotnet_diagnostic.IDE0120.severity = warning
dotnet_diagnostic.IDE0121.severity = warning
dotnet_diagnostic.IDE0240.severity = error
dotnet_diagnostic.IDE0241.severity = error
dotnet_style_coalesce_expression = true
dotnet_style_namespace_match_folder = false
dotnet_style_null_propagation = true
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_compound_assignment = true
csharp_style_prefer_simple_property_accessors = true
dotnet_style_prefer_simplified_interpolation = true
dotnet_style_prefer_simplified_boolean_expressions = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
# Field preferences
dotnet_diagnostic.IDE0044.severity = warning
dotnet_style_readonly_field = true
# Language keyword vs. framework types preferences
dotnet_diagnostic.IDE0049.severity = error
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_style_prefer_readonly_struct = true
dotnet_diagnostic.IDE0036.severity = warning
dotnet_diagnostic.IDE0040.severity = error
dotnet_diagnostic.IDE0380.severity = error
dotnet_style_require_accessibility_modifiers = always
# New-line preferences
dotnet_diagnostic.IDE2000.severity = warning
dotnet_diagnostic.IDE2002.severity = warning
dotnet_diagnostic.IDE2003.severity = warning
dotnet_diagnostic.IDE2004.severity = warning
dotnet_diagnostic.IDE2005.severity = warning
dotnet_diagnostic.IDE2006.severity = warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
dotnet_style_allow_multiple_blank_lines_experimental = false
dotnet_style_allow_statement_immediately_after_block_experimental = false
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
dotnet_diagnostic.IDE0280.severity = error
# Parentheses preferences
dotnet_diagnostic.IDE0047.severity = warning
dotnet_diagnostic.IDE0048.severity = warning
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = always_for_clarity
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Pattern-matching preferences
dotnet_diagnostic.IDE0019.severity = warning
dotnet_diagnostic.IDE0020.severity = warning
dotnet_diagnostic.IDE0038.severity = warning
dotnet_diagnostic.IDE0066.severity = none
dotnet_diagnostic.IDE0083.severity = warning
dotnet_diagnostic.IDE0260.severity = warning
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# var preferences
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = true
# .NET formatting options
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
# C# formatting options
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false

View File

@@ -1,40 +1,48 @@
name: Build and Test
on:
push:
branches: [ "main" ]
push:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh
- name: Update rolling tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -f rolling
git push origin :refs/tags/rolling || true
git push origin rolling --force
- name: Upload to rolling
uses: ncipollo/release-action@v1.20.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg"
body: "Last built commit: ${{ github.sha }}"
name: "Rolling Release"
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

View File

@@ -3,21 +3,21 @@ name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Run tests
run: dotnet test
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Build
run: dotnet build
- name: Run tests
run: dotnet test

View File

@@ -13,9 +13,16 @@ namespace SabreTools.CommandLine.Test
var input1 = new FlagInput("input1", "--input1", "input1");
var input2 = new FlagInput("input2", "--input2", "input2");
var feature1 = new MockFeature("feature1", "feature1", "feature1");
var inputA = new FlagInput("inputA", "--inputA", "inputA");
var inputB = new FlagInput("inputB", "--inputB", "inputB");
feature1.Add(inputA);
feature1.Add(inputB);
var featureSet = new CommandSet();
featureSet.Add(input1);
featureSet.Add(input2);
featureSet.AddFrom(feature1);
var actualInput1 = featureSet["input1"];
Assert.NotNull(actualInput1);
@@ -27,6 +34,14 @@ namespace SabreTools.CommandLine.Test
var actualInput3 = featureSet["input3"];
Assert.Null(actualInput3);
var actualInputA = featureSet["inputA"];
Assert.NotNull(actualInputA);
Assert.Equal("inputA", actualInputA.Name);
var actualinputB = featureSet["inputB"];
Assert.NotNull(actualinputB);
Assert.Equal("inputB", actualinputB.Name);
}
[Fact]
@@ -764,7 +779,7 @@ namespace SabreTools.CommandLine.Test
[Fact]
public void ProcessArgs_EmptyArgs_Success()
{
CommandSet commandSet = new CommandSet();
var commandSet = new CommandSet();
string[] args = [];
@@ -775,7 +790,7 @@ namespace SabreTools.CommandLine.Test
[Fact]
public void ProcessArgs_ValidArgs_Success()
{
CommandSet commandSet = new CommandSet();
var commandSet = new CommandSet();
Feature feature = new MockFeature("a", "a", "a");
feature.Add(new FlagInput("b", "b", "b"));
feature.Add(new FlagInput("c", "c", "c"));
@@ -791,7 +806,7 @@ namespace SabreTools.CommandLine.Test
[Fact]
public void ProcessArgs_InvalidArg_AddedAsGeneric()
{
CommandSet commandSet = new CommandSet();
var commandSet = new CommandSet();
Feature feature = new MockFeature("a", "a", "a");
feature.Add(new FlagInput("b", "b", "b"));
feature.Add(new FlagInput("d", "d", "d"));
@@ -808,7 +823,7 @@ namespace SabreTools.CommandLine.Test
[Fact]
public void ProcessArgs_NestedArgs_Success()
{
CommandSet commandSet = new CommandSet();
var commandSet = new CommandSet();
Feature feature = new MockFeature("a", "a", "a");
var sub = new FlagInput("b", "b", "b");
sub.Add(new FlagInput("c", "c", "c"));

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
@@ -16,7 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -40,6 +40,41 @@ namespace SabreTools.CommandLine
#endregion
#region Properties
/// <summary>
/// Custom help text to print instead of the automatically
/// generated generic help
/// </summary>
/// <remarks>
/// This only replaces the automatically generated help
/// for <see cref="OutputGenericHelp"/>. It does not impact
/// either <see cref="OutputAllHelp"/> or <see cref="OutputFeatureHelp"/>.
/// </remarks>
public string? CustomHelp { get; set; }
/// <summary>
/// Feature that represents the default functionality
/// for a command set
/// </summary>
/// <remarks>
/// It is recommended to use this in applications that
/// do not need multiple distinct functional modes.
///
/// When printing help text, the flags and description
/// of this feature will be omitted, instead printing
/// the children of the feature directly at the
/// top level.
///
/// If the default feature is included as a normal
/// top-level input for the command set, then the flags
/// will be printed twice - once under the feature
/// itself and once at the top level.
/// </remarks>
public Feature? DefaultFeature { get; set; }
#endregion
#region Constructors
/// <summary>
@@ -120,6 +155,29 @@ namespace SabreTools.CommandLine
public void Add(UserInput input)
=> _inputs.Add(input.Name, input);
/// <summary>
/// Add all children from an input to the set
/// </summary>
/// <param name="input">UserInput object to retrieve children from</param>
/// <remarks>
/// This should only be used in situations where an input is defined
/// but not used within the context of the command set directly.
///
/// This is helpful for when there are applications with default functionality
/// that need to be able to expose both defined features as well as
/// the default functionality in help text.
///
/// If there is any overlap between existing names and the names from
/// any of the children, this operation will overwrite them.
/// </reamrks>
public void AddFrom(UserInput input)
{
foreach (var kvp in input.Children)
{
_inputs.Add(kvp.Key, kvp.Value);
}
}
#endregion
#region Children
@@ -730,13 +788,34 @@ namespace SabreTools.CommandLine
if (_header.Count > 0)
output.AddRange(_header);
// Now append all available top-level flags
output.Add("Available options:");
foreach (var input in _inputs.Values)
// If custom help text is defined
if (CustomHelp is not null)
{
var outputs = input.Format(pre: 2, midpoint: 30, detailed);
if (outputs != null)
output.AddRange(outputs);
string customHelp = CustomHelp.Replace("\r\n", "\n");
string[] customLines = customHelp.Split('\n');
output.AddRange(customLines);
}
else
{
// Append all available top-level flags
output.Add("Available options:");
foreach (var input in _inputs.Values)
{
var outputs = input.Format(pre: 2, midpoint: 30, detailed);
if (outputs is not null)
output.AddRange(outputs);
}
// If there is a default feature
if (DefaultFeature is not null)
{
foreach (var input in DefaultFeature.Children)
{
var outputs = input.Value.Format(pre: 2, midpoint: 30, detailed);
if (outputs is not null)
output.AddRange(outputs);
}
}
}
// Append the footer, if needed
@@ -760,15 +839,26 @@ namespace SabreTools.CommandLine
if (_header.Count > 0)
output.AddRange(_header);
// Now append all available flags recursively
// Append all available flags recursively
output.Add("Available options:");
foreach (var input in _inputs.Values)
{
var outputs = input.FormatRecursive(pre: 2, midpoint: 30, detailed);
if (outputs != null)
if (outputs is not null)
output.AddRange(outputs);
}
// If there is a default feature
if (DefaultFeature is not null)
{
foreach (var input in DefaultFeature.Children)
{
var outputs = input.Value.Format(pre: 2, midpoint: 30, detailed);
if (outputs is not null)
output.AddRange(outputs);
}
}
// Append the footer, if needed
if (_footer.Count > 0)
output.AddRange(_footer);
@@ -891,7 +981,7 @@ namespace SabreTools.CommandLine
// If there's no arguments, show help
if (args.Length == 0)
{
OutputGenericHelp();
OutputAllHelp();
return true;
}
@@ -900,7 +990,7 @@ namespace SabreTools.CommandLine
// Get the associated feature
var topLevel = GetTopLevel(featureName);
if (topLevel == null || topLevel is not Feature feature)
if (topLevel is null || topLevel is not Feature feature)
{
Console.WriteLine($"'{featureName}' is not valid feature flag");
OutputFeatureHelp(featureName);

View File

@@ -46,7 +46,7 @@ namespace SabreTools.CommandLine.Features
try
{
var assembly = Assembly.GetEntryAssembly();
if (assembly == null)
if (assembly is null)
return null;
var assemblyVersion = Attribute.GetCustomAttribute(assembly, typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute;

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="bool"/>
/// Represents a user input bounded to the range of <see cref="bool"/>
/// </summary>
public class BooleanInput : UserInput<bool?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="short"/>
/// Represents a user input bounded to the range of <see cref="short"/>
/// </summary>
public class Int16Input : UserInput<short?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="int"/>
/// Represents a user input bounded to the range of <see cref="int"/>
/// </summary>
public class Int32Input : UserInput<int?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="long"/>
/// Represents a user input bounded to the range of <see cref="long"/>
/// </summary>
public class Int64Input : UserInput<long?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="sbyte"/>
/// Represents a user input bounded to the range of <see cref="sbyte"/>
/// </summary>
public class Int8Input : UserInput<sbyte?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="ushort"/>
/// Represents a user input bounded to the range of <see cref="ushort"/>
/// </summary>
public class UInt16Input : UserInput<ushort?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="uint"/>
/// Represents a user input bounded to the range of <see cref="uint"/>
/// </summary>
public class UInt32Input : UserInput<uint?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="ulong"/>
/// Represents a user input bounded to the range of <see cref="ulong"/>
/// </summary>
public class UInt64Input : UserInput<ulong?>
{

View File

@@ -3,7 +3,7 @@ using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="byte"/>
/// Represents a user input bounded to the range of <see cref="byte"/>
/// </summary>
public class UInt8Input : UserInput<byte?>
{

View File

@@ -41,7 +41,7 @@ namespace SabreTools.CommandLine.Inputs
/// <summary>
/// Set of children associated with this input
/// </summary>
protected readonly Dictionary<string, UserInput> Children = [];
protected internal readonly Dictionary<string, UserInput> Children = [];
#endregion
@@ -86,7 +86,7 @@ namespace SabreTools.CommandLine.Inputs
/// </summary>
public UserInput? this[UserInput subfeature]
{
get
get
{
if (!Children.TryGetValue(subfeature.Name, out var input))
return null;

View File

@@ -27,10 +27,10 @@ namespace SabreTools.CommandLine.Inputs
#region Instance Methods
/// <inheritdoc/>
public override abstract bool ProcessInput(string[] args, ref int index);
public abstract override bool ProcessInput(string[] args, ref int index);
/// <inheritdoc/>
protected override abstract string FormatFlags();
protected abstract override string FormatFlags();
#endregion
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
@@ -11,7 +11,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.2</Version>
<Version>1.4.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>

View File

@@ -1,7 +1,7 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
# - .NET 10.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.

View File

@@ -1,5 +1,5 @@
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
# - .NET 10.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.