├── .github ├── FUNDING.yml └── workflows │ ├── docs.yml │ ├── dotnet.yml │ └── publish_nuget.yml ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── build.cmd ├── build.ps1 ├── build.sh ├── build ├── build.cs └── build.csproj ├── docs ├── .vitepress │ ├── algolia-config.json │ ├── config.js │ └── dist │ │ ├── assets │ │ └── style.13643d27.css │ │ ├── guide │ │ └── index.html │ │ ├── index.html │ │ └── oakton.png ├── guide │ ├── bootstrapping.md │ ├── commands.md │ ├── discovery.md │ ├── getting_started.md │ ├── help.md │ ├── host │ │ ├── describe.md │ │ ├── environment.md │ │ ├── extensions.md │ │ ├── index.md │ │ ├── integration_with_i_host.md │ │ ├── ioc.md │ │ ├── resources.md │ │ └── run.md │ ├── index.md │ ├── opts.md │ └── parsing.md ├── index.md ├── public │ └── oakton.png └── vite.config.js ├── mdsnippets.json ├── package-lock.json ├── package.json └── src ├── AspNetCoreExtensionCommands ├── AspNetCoreExtensionCommands.csproj └── BuildCommand.cs ├── CommonAssemblyInfo ├── AssemblyInfo.cs ├── AssemblyInfoCommand.cs ├── AssemblyInfoInput.cs ├── CommonAssemblyInfo.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── DotNet6BootstrappedConsole ├── DotNet6BootstrappedConsole.csproj └── Program.cs ├── EnvironmentCheckDemonstrator ├── Describers.cs ├── EnvironmentCheckDemonstrator.csproj ├── FakeResource.cs ├── Program.cs └── TimeSpanExtensions.cs ├── ExtensionCommands ├── ExtensionCommand.cs └── ExtensionCommands.csproj ├── MinimalApi ├── MinimalApi.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── MultipleCommands ├── MultipleCommands.csproj └── Program.cs ├── MvcApp ├── Controllers │ └── HomeController.cs ├── Describers.cs ├── Models │ └── ErrorViewModel.cs ├── MvcApp.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── Views │ ├── Home │ │ ├── Index.cshtml │ │ └── Privacy.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _CookieConsentPartial.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.json ├── description.html └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ ├── js │ └── site.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── Net5WebApi ├── Controllers │ └── WeatherForecastController.cs ├── Net5WebApi.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── appsettings.Development.json └── appsettings.json ├── Oakton.sln ├── Oakton ├── ActivatorCommandCreator.cs ├── Argument.cs ├── AssemblyInfo.cs ├── CommandExecutor.cs ├── CommandFactory.cs ├── CommandFailureException.cs ├── CommandLineHostingExtensions.cs ├── CommandRun.cs ├── Commands │ ├── CheckEnvironmentCommand.cs │ └── RunCommand.cs ├── DependencyInjectionCommandCreator.cs ├── DescriptionAttribute.cs ├── Descriptions │ ├── ConfigurationPreview.cs │ ├── DescribeCommand.cs │ ├── DescribeInput.cs │ ├── DescriptionExtensions.cs │ ├── IDescribedSystemPart.cs │ ├── IDescribedSystemPartFactory.cs │ ├── IDescribesProperties.cs │ ├── IRequiresServices.cs │ ├── ITreeDescriber.cs │ ├── IWriteToConsole.cs │ └── LambdaDescribedSystemPart.cs ├── Environment │ ├── EnvironmentCheckException.cs │ ├── EnvironmentCheckExtensions.cs │ ├── EnvironmentCheckResults.cs │ ├── EnvironmentChecker.cs │ ├── FileExistsCheck.cs │ ├── IEnvironmentCheck.cs │ ├── IEnvironmentCheckFactory.cs │ └── LambdaCheck.cs ├── FlagAliasAttribute.cs ├── Help │ ├── CommandUsage.cs │ ├── HelpCommand.cs │ ├── HelpInput.cs │ └── UsageGraph.cs ├── HostWrapperCommand.cs ├── HostedCommandExtensions.cs ├── ICommandCreator.cs ├── ICommandFactory.cs ├── IHostBuilderInput.cs ├── IOaktonCommand.cs ├── IServiceRegistrations.cs ├── IgnoreOnCommandLineAttribute.cs ├── InjectServiceAttribute.cs ├── Internal │ ├── ArgsExtensions.cs │ └── Conversion │ │ ├── ArrayConversion.cs │ │ ├── Conversions.cs │ │ ├── DateTimeConverter.cs │ │ ├── EnumerationConversion.cs │ │ ├── IConversionProvider.cs │ │ ├── NullableConvertor.cs │ │ ├── StringConverterProvider.cs │ │ └── TimeSpanConverter.cs ├── InvalidUsageException.cs ├── NetCoreInput.cs ├── Oakton.csproj ├── OaktonAsyncCommand.cs ├── OaktonCommand.cs ├── OaktonCommandAssemblyAttribute.cs ├── OaktonEnvironment.cs ├── OaktonOptions.cs ├── Parsing │ ├── ArgPreprocessor.cs │ ├── BooleanFlag.cs │ ├── DictionaryFlag.cs │ ├── EnumerableArgument.cs │ ├── EnumerableFlag.cs │ ├── Flag.cs │ ├── FlagAliases.cs │ ├── ITokenHandler.cs │ ├── InputParser.cs │ ├── OptionReader.cs │ ├── QueueExtensions.cs │ ├── StringTokenizer.cs │ ├── TokenHandlerBase.cs │ └── TokenParser.cs ├── PreBuiltHostBuilder.cs ├── Resources │ ├── IStatefulResource.cs │ ├── IStatefulResourceSource.cs │ ├── ResourceAction.cs │ ├── ResourceEnvironmentCheck.cs │ ├── ResourceHostExtensions.cs │ ├── ResourceInput.cs │ ├── ResourceSetupException.cs │ ├── ResourceSetupHostService.cs │ ├── ResourcesCommand.cs │ └── StatefulResourceBase.cs └── jasper-icon.png ├── OaktonSample ├── OaktonSample.csproj ├── Program.cs └── Properties │ └── launchSettings.json ├── OptsFileUsageDemo ├── HelloCommand.cs ├── OptsFileUsageDemo.csproj ├── Program.cs └── sample.opts ├── Tests ├── ActivatorCommandCreatorTests.cs ├── ArgPreprocessorTester.cs ├── ArgumentTester.cs ├── AspNetCoreInputTests.cs ├── BooleanFlagTester.cs ├── Bugs │ └── bug_24_negative_numbers.cs ├── CommandExecutorTester.cs ├── CommandFactoryTester.cs ├── Conversion │ ├── ConversionsTester.cs │ ├── DateTime_conversion_specs.cs │ ├── StringConverterProviderTester.cs │ └── TimeSpanConverterTester.cs ├── CustomCommandCreatorTests.cs ├── Descriptions │ ├── ConfigurationPreviewTests.cs │ └── DescribeCommandTests.cs ├── DictionaryFlagTester.cs ├── EnumerableArgumentTester.cs ├── Environment │ ├── EnvironmentCheckResultsTests.cs │ ├── EnvironmentCheckerTests.cs │ ├── ExtensionMethodTests.cs │ └── LambdaCheckTests.cs ├── FlagTester.cs ├── HostedCommandsTester.cs ├── InputParserTester.cs ├── NoXUnitParallelization.cs ├── OptionReaderTester.cs ├── OptionsSamples.cs ├── Resources │ ├── ResourceCommandContext.cs │ ├── ResourceHostExtensionsTests.cs │ ├── resource_command_execution.cs │ ├── resource_filtering.cs │ └── resource_ordering.cs ├── SpecificationExtensions.cs ├── StringTokenizerTester.cs ├── Tests.csproj ├── TimeSpanExtensions.cs ├── UsageGraphTester.cs ├── can_use_fields_as_arguments_and_flags.cs ├── filtering_launcher_args.cs └── using_injected_services.cs ├── WorkerService ├── Program.cs ├── Properties │ └── launchSettings.json ├── Worker.cs ├── WorkerService.csproj ├── appsettings.Development.json └── appsettings.json └── quickstart ├── Program.cs └── quickstart.csproj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JasperFx] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # eventsourcingnetcore 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | publish_docs_job: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install .NET 6.0.x 13 | uses: actions/setup-dotnet@v3 14 | with: 15 | dotnet-version: 6.0.x 16 | 17 | - name: Install .NET Core 7.0.x 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: 7.0.x 21 | 22 | - name: Setup .NET 8 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: 8.0.x 26 | 27 | - name: Setup .NET 9 28 | uses: actions/setup-dotnet@v1 29 | with: 30 | dotnet-version: 9.0.x 31 | 32 | - name: Install Node.js 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: 15.x 36 | 37 | - name: Build & Deploy docs 38 | env: 39 | TARGET_BRANCH: gh-pages 40 | TARGET_DIR: doc-target 41 | run: | 42 | sudo apt install libxml2-utils 43 | dotnet tool install -g MarkdownSnippets.Tool 44 | npm install 45 | npm run docs-build 46 | git clone -b ${TARGET_BRANCH} https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git ${TARGET_DIR} 47 | cd ${TARGET_DIR} 48 | git config user.email action@github.com 49 | git config user.name "GitHub Action" 50 | shopt -s extglob 51 | shopt -s dotglob 52 | rm -rf !(.git) 2> /dev/null || true 53 | cp -R ../docs/.vitepress/dist/* . 54 | touch .nojekyll 55 | git add --all 56 | DOCS_VERSION="$(xmllint --xpath "/Project/PropertyGroup/PackageVersion/text()" ../src/Oakton/Oakton.csproj)" 57 | git commit -a -m "Documentation Update for ${DOCS_VERSION}" --allow-empty 58 | git push origin ${TARGET_BRANCH} 59 | 60 | - name: Generate Algolia DocSearch index 61 | uses: darrenjennings/algolia-docsearch-action@master 62 | with: 63 | algolia_application_id: ${{ secrets.ALGOLIA_APPLICATION_ID }} 64 | algolia_api_key: ${{ secrets.ALGOLIA_API_KEY }} 65 | file: 'docs/.vitepress/algolia-config.json' 66 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | config: Release 11 | disable_test_parallelization: true 12 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 20 20 | 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Setup .NET 6 26 | uses: actions/setup-dotnet@v3 27 | with: 28 | dotnet-version: 6.0.x 29 | 30 | - name: Setup .NET 7 31 | uses: actions/setup-dotnet@v3 32 | with: 33 | dotnet-version: 7.0.x 34 | 35 | - name: Setup .NET 8 36 | uses: actions/setup-dotnet@v1 37 | with: 38 | dotnet-version: 8.0.x 39 | 40 | - name: Setup .NET 9 41 | uses: actions/setup-dotnet@v1 42 | with: 43 | dotnet-version: 9.0.x 44 | 45 | - name: Build Script 46 | run: dotnet run --project build/build.csproj -- ci 47 | -------------------------------------------------------------------------------- /.github/workflows/publish_nuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish Nugets 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v3 14 | with: 15 | dotnet-version: 6.0.x 16 | - name: Setup .NET 7 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 7.0.x 20 | 21 | - name: Setup .NET 8 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: 8.0.x 25 | 26 | - name: Setup .NET 9 27 | uses: actions/setup-dotnet@v1 28 | with: 29 | dotnet-version: 9.0.x 30 | 31 | - name: Pack 32 | run: dotnet pack src/Oakton/Oakton.csproj --configuration Release 33 | 34 | - name: Publish to NuGet 35 | run: | 36 | find . -name '*.nupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate \; 37 | # find . -name '*.snupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \; 38 | shell: bash 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oakton is being folded into the larger [JasperFx](https://github.com/jasperfx/jasperfx) project 2 | 3 | # Oakton 4 | 5 | Parsing and Utilities for Command Line Tools in .Net 6 | 7 | [![Discord](https://img.shields.io/discord/1074998995086225460?color=blue&label=Chat%20on%20Discord)](https://discord.gg/WMxrvegf8H) 8 | 9 | Check out the [documentation](https://jasperfx.github.io/oakton); 10 | 11 | ## Support Plans 12 | 13 |
14 | JasperFx logo 15 |
16 | 17 | While Oakton is open source, [JasperFx Software offers paid support and consulting contracts](https://bit.ly/3szhwT2) for Oakton. 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 2.0.0.{build} 2 | 3 | configuration: Release 4 | 5 | os: Visual Studio 2019 6 | 7 | environment: 8 | DOTNET_CLI_TELEMETRY_OPTOUT: true 9 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 10 | 11 | install: 12 | - cmd: choco install dotnetcore-sdk -y 13 | 14 | before_build: 15 | - dotnet --info 16 | 17 | build_script: 18 | - cmd: >- 19 | rake ci 20 | test: off 21 | 22 | artifacts: 23 | - path: '**/Oakton.*.nupkg' # find all NuGet packages recursively -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | dotnet run --project build/build.csproj -c Release -- %* 4 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | 2 | dotnet run --project build/build.csproj -c Release -- $args 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | 5 | dotnet run --project build/build.csproj -c Release -- "$@" 6 | -------------------------------------------------------------------------------- /build/build.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/.vitepress/algolia-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_name": "oakton_index", 3 | "start_urls": [ 4 | "https://jasperfx.github.io/oakton/guide", 5 | "https://jasperfx.github.io/oakton/guide/host" 6 | ], 7 | "selectors": { 8 | "lvl0": { 9 | "selector": ".VPLink.link.VPNavBarMenuLink.active", 10 | "global": true 11 | }, 12 | "lvl1": ".content h1", 13 | "lvl2": ".content h2", 14 | "lvl3": ".content h3", 15 | "lvl4": ".content h4", 16 | "lvl5": ".content h5", 17 | "text": ".content p, .content li", 18 | "lang": { 19 | "selector": "/html/@lang", 20 | "type": "xpath", 21 | "global": true 22 | } 23 | }, 24 | "strip_chars": " .,;:#", 25 | "selectors_exclude": ["sup"], 26 | "custom_settings": { 27 | "attributesForFaceting": ["lang", "tags"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Oakton', 3 | description: 'Add Robust Command Line Options to .Net Applications', 4 | head: [], 5 | base: '/oakton/', 6 | themeConfig: { 7 | logo: null, 8 | repo: 'JasperFx/oakton', 9 | docsDir: 'docs', 10 | docsBranch: 'master', 11 | editLinks: true, 12 | editLinkText: 'Suggest changes to this page', 13 | base: '/oakton/', 14 | nav: [ 15 | { text: 'Guide', link: '/guide/' }, 16 | { text: 'Discord | Join Chat', link: 'https://discord.gg/WMxrvegf8H' } 17 | ], 18 | 19 | algolia: { 20 | appId: '5G434AGRPI', 21 | apiKey: 'daed9f557ebf95072747f226f6117220', 22 | indexName: 'oakton_index' 23 | }, 24 | 25 | sidebar: { 26 | '/': 27 | [ 28 | { 29 | text: 'Getting Started', 30 | collapsible: false, 31 | collapsed: false, 32 | items: tableOfContents() 33 | } 34 | ] 35 | } 36 | }, 37 | markdown: { 38 | linkify: false 39 | } 40 | } 41 | 42 | function tableOfContents() { 43 | return [ 44 | {text: 'What is Oakton', link: '/guide/getting_started'}, 45 | {text: "Commands", link: '/guide/commands'}, 46 | { 47 | text: "Integration with IHost", 48 | link: '/guide/host/integration_with_i_host', 49 | collapsible: true, 50 | collapsed: true, 51 | items: [ 52 | {text: "Integration with IHost", link: '/guide/host/'}, 53 | {text: "Improved \"Run\" Command", link: '/guide/host/run'}, 54 | {text: "Environment Checks", link: '/guide/host/environment'}, 55 | {text: "Writing Extension Commands", link: '/guide/host/extensions'}, 56 | {text: "The \"describe\" command", link: '/guide/host/describe'}, 57 | {text: "Stateful Resources", link: '/guide/host/resources'}, 58 | {text: "Using IoC Services", link: '/guide/host/ioc.md'} 59 | ] 60 | }, 61 | {text: "Bootstrapping with CommandExecutor", link: '/guide/bootstrapping'}, 62 | {text: "Parsing Arguments and Optional Flags", link: '/guide/parsing'}, 63 | {text: "Help Text", link: '/guide/help'}, 64 | {text: "\"Opts\" Files", link: '/guide/opts'}, 65 | {text: "Command Assembly Discovery", link: '/guide/discovery'}, 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /docs/.vitepress/dist/oakton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/docs/.vitepress/dist/oakton.png -------------------------------------------------------------------------------- /docs/guide/discovery.md: -------------------------------------------------------------------------------- 1 | # Command Assembly Discovery 2 | 3 | This feature probably won't be commonly used, but there is a mechanism to automatically find and load Oakton commands from other assemblies through file scanning. 4 | 5 | The first step is to mark any assembly containing Oakton commands you want discovered and loaded through this mechanism with this attribute: 6 | 7 | 8 | 9 | ```cs 10 | [assembly:Oakton.OaktonCommandAssembly] 11 | ``` 12 | snippet source | anchor 13 | 14 | 15 | Next, when you build a `CommandFactory`, you need to explicitly opt into the auto-discovery of commands by using the `RegisterCommandsFromExtensionAssemblies()` option as shown below in the Oakton.AspNetCore code: 16 | 17 | 18 | 19 | ```cs 20 | return CommandExecutor.For(factory => 21 | { 22 | factory.RegisterCommands(typeof(RunCommand).GetTypeInfo().Assembly); 23 | if (applicationAssembly != null) 24 | { 25 | factory.RegisterCommands(applicationAssembly); 26 | } 27 | 28 | // This method will direct the CommandFactory to go look for extension 29 | // assemblies with Oakton commands 30 | factory.RegisterCommandsFromExtensionAssemblies(); 31 | 32 | factory.ConfigureRun = cmd => 33 | { 34 | if (cmd.Input is IHostBuilderInput i) 35 | { 36 | factory.ApplyExtensions(source); 37 | i.HostBuilder = source; 38 | } 39 | }; 40 | }); 41 | ``` 42 | snippet source | anchor 43 | 44 | -------------------------------------------------------------------------------- /docs/guide/help.md: -------------------------------------------------------------------------------- 1 | # Help Text 2 | 3 | Oakton comes with its own `[Description]` attribute that can be applied to fields or properties on 4 | the input class to provide help information on usage like this: 5 | 6 | 7 | 8 | ```cs 9 | public class NameInput 10 | { 11 | [Description("The name to be printed to the console output")] 12 | public string Name { get; set; } 13 | 14 | [Description("The color of the text. Default is black")] 15 | public ConsoleColor Color { get; set; } = ConsoleColor.Black; 16 | 17 | [Description("Optional title preceeding the name")] 18 | public string TitleFlag { get; set; } 19 | } 20 | ``` 21 | snippet source | anchor 22 | 23 | 24 | or on the command class itself: 25 | 26 | 27 | 28 | ```cs 29 | [Description("Print somebody's name")] 30 | public class NameCommand : OaktonCommand 31 | { 32 | public NameCommand() 33 | { 34 | // The usage pattern definition here is completely 35 | // optional 36 | Usage("Default Color").Arguments(x => x.Name); 37 | Usage("Print name with specified color").Arguments(x => x.Name, x => x.Color); 38 | } 39 | 40 | public override bool Execute(NameInput input) 41 | { 42 | var text = input.Name; 43 | if (!string.IsNullOrEmpty(input.TitleFlag)) 44 | { 45 | text = input.TitleFlag + " " + text; 46 | } 47 | 48 | AnsiConsole.Write($"[{input.Color}]{text}[/]"); 49 | 50 | // Just telling the OS that the command 51 | // finished up okay 52 | return true; 53 | } 54 | } 55 | ``` 56 | snippet source | anchor 57 | 58 | 59 | Also note the explanatory text in the `Usage()` method above in the case of a command that has multiple valid 60 | argument patterns. 61 | 62 | To display a list of all the available commands, you can type either: 63 | 64 | ``` 65 | executable help 66 | ``` 67 | 68 | or 69 | 70 | ``` 71 | executable ? 72 | ``` 73 | 74 | Likewise, to get the usage help for a single command named "clean", use either `executable help clean` or `executable ? clean`. 75 | -------------------------------------------------------------------------------- /docs/guide/host/extensions.md: -------------------------------------------------------------------------------- 1 | # Writing Extension Commands 2 | 3 | Oakton has a strong extensibility model to find and activate commands from external assemblies. If an application uses the `RunOaktonCommands(args)` method, Oakton will look for any Oakton commands in any assembly that has this assembly level attribute: 4 | 5 | 6 | 7 | ```cs 8 | [assembly:Oakton.OaktonCommandAssembly] 9 | ``` 10 | snippet source | anchor 11 | 12 | 13 | ::: tip warning 14 | You will have to explicitly add this attribute to the main assembly of your application to make Oakton discover commands in that assembly. Oakton no longer supports trying to walk the call stack to determine the main application assembly. 15 | ::: 16 | 17 | Extension commands can be either basic `OaktonCommand` or `OaktonAsyncCommand` classes. To add an extension command that uses the `HostBuilder` configuration of the application, the command needs to use the `NetCoreInput` class or a class that inherits from `NetCoreInput`. In this simple example below, I've built a command that just tries to do a "smoke test" by calling the `HostBuilder.Build()` method and seeing if any exceptions happen: 18 | 19 | 20 | 21 | ```cs 22 | [Description("Simply try to build a web host as a smoke test", Name = "smoke")] 23 | public class SmokeCommand : OaktonCommand 24 | { 25 | public override bool Execute(NetCoreInput input) 26 | { 27 | // This method builds out the IWebHost for your 28 | // configured IHostBuilder of the application 29 | using (var host = input.BuildHost()) 30 | { 31 | Console.WriteLine("It's all good"); 32 | } 33 | 34 | return true; 35 | } 36 | } 37 | ``` 38 | snippet source | anchor 39 | 40 | 41 | The `NetCoreInput` carries the `IHostBuilder` of your application, but does **not** start up or build the `IHost` by itself. You would have to explicitly do so, but making that lazy gives you the ability to alter or extend the application configuration before calling `IHostBuilder.Build()` or `IHostBuilder.Start()`. 42 | -------------------------------------------------------------------------------- /docs/guide/host/index.md: -------------------------------------------------------------------------------- 1 | # Integration with IHost 2 | 3 | Just here for Algolia 4 | -------------------------------------------------------------------------------- /docs/guide/host/ioc.md: -------------------------------------------------------------------------------- 1 | # Using IoC Services 2 | 3 | Very frequently, folks have wanted to either use services from their IoC/DI container for their application, or to 4 | have Oakton resolve the command objects from the application's DI container. New in Oakton 6.2 is that very ability. 5 | 6 | ## Injecting Services into Commands 7 | 8 | If you are using [Oakton's IHost integration](/guide/host/integration_with_i_host), you can write commands that 9 | use IoC services by simply decorating a publicly settable property on your Oakton command classes with the 10 | new `[InjectService]` attribute. 11 | 12 | First though, just to make sure you're clear about when and when this isn't applicable, this applies to Oakton used 13 | from an `IHostBuilder` or `ApplicationBuilder` like so: 14 | 15 | snippet: sample_using_ihost_activation 16 | 17 | Then you can decorate your command types something like this: 18 | 19 | snippet: sample_MyDbCommand 20 | 21 | Just know that when you do this and execute a command that has decorated properties for services, Oakton is: 22 | 23 | 1. Building your system's `IHost` 24 | 2. Creating a new `IServiceScope` from your application's DI container, or in other words, a scoped container 25 | 3. Building your command object and setting all the dependencies on your command object by resolving each dependency from the scoped container created in the previous step 26 | 4. Executing the command as normal 27 | 5. Disposing the scoped container and the `IHost`, effectively in a `try/finally` so that Oakton is always cleaning up after the application 28 | 29 | 30 | ## Using IoC Command Creators 31 | 32 | If you would like to just always have Oakton try to use dependency injection in the constructor of the command classes, 33 | you also have this option. First, register Oakton through the application's DI container and run the Oakton commands through 34 | the `IHost.RunHostedOaktonCommands()` extension method as shown below: 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Scenarios 2 | 3 | Just here for Algolia 4 | -------------------------------------------------------------------------------- /docs/guide/opts.md: -------------------------------------------------------------------------------- 1 | # "Opts" Files 2 | 3 | ::: tip warning 4 | This feature is all new in Oakton and was inspired by Javascript tools like Mocha that use "opts" files to make 5 | their tools much easier to use at the command line 6 | ::: 7 | 8 | As a contrived example (that probably violates all kind of security best practices), let's say that your console application exposes several commands, but all of the commands may need 9 | an optional user name and password flag. You might start with a base class for your command inputs like this: 10 | 11 | 12 | 13 | ```cs 14 | public class SecuredInput 15 | { 16 | public string UserName { get; set; } 17 | public string Password { get; set; } 18 | } 19 | ``` 20 | snippet source | anchor 21 | 22 | 23 | To make the tool easier to use, we can take advantage of the "opts" file option by making Oakton look for the presence of an optional text file in the same directory as the command execution with a certain name to pick up default command usages. 24 | 25 | We can set that up by declaring the name of the opts file like this: 26 | 27 | 28 | 29 | ```cs 30 | var executor = CommandExecutor.For(_ => 31 | { 32 | // configure the command discovery 33 | }); 34 | 35 | executor.OptionsFile = "mytool.opts"; 36 | ``` 37 | snippet source | anchor 38 | 39 | 40 | Now, we could have add a file named `mytool.opts` to the directory where we run the command with this sample content: 41 | 42 | ``` 43 | -u MyUserName 44 | -p ~~HeMan2345 45 | ``` 46 | 47 | Now, when we run the command line `mytool somecommand` and that opts file is found in the current directory, Oakton appends the data of each line in that file so that the executed command is really `mytool somecommand -u MyUserName -p ~~HeMan2345`. 48 | 49 | A couple other things to note: 50 | 51 | * The presence of the named opts file is not mandatory 52 | * You can express arguments (maybe not super useful) or more likely any number of flag values 53 | * The opts file can be one or more lines if that aids readability 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | sidebar: false 4 | 5 | title: Oakton 6 | titleTemplate: Parsing and Utilities for Command Line Tools in .Net 7 | 8 | hero: 9 | name: Oakton 10 | text: Parsing and Utilities for Command Line Tools in .Net 11 | image: 12 | src: /oakton.png 13 | actions: 14 | - theme: brand 15 | text: Get Started 16 | link: /guide/ 17 | - theme: alt 18 | text: View on GitHub 19 | link: https://github.com/JasperFx/oakton 20 | 21 | footer: MIT Licensed | Copyright © Jeremy D. Miller and contributors. 22 | --- 23 | -------------------------------------------------------------------------------- /docs/public/oakton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/docs/public/oakton.png -------------------------------------------------------------------------------- /docs/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | fsServe: { 4 | root: '../' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Convention": "InPlaceOverwrite", 3 | "LinkFormat": "GitHub", 4 | "UrlPrefix": "https://github.com/JasperFx/oakton/blob/master", 5 | "ExcludeMarkdownDirectories": ["documentation"], 6 | "ExcludeSnippetDirectories": [], 7 | "TreatMissingAsWarning": false 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "vitepress-dev": "vitepress dev docs --port 5050", 5 | "vitepress-build": "vitepress build docs", 6 | "mdsnippets": "mdsnippets", 7 | "docs": "npm-run-all -s mdsnippets vitepress-dev", 8 | "docs-build": "npm-run-all -s mdsnippets vitepress-build" 9 | }, 10 | "dependencies": { 11 | "vitepress": "^1.0.0-alpha.46" 12 | }, 13 | "devDependencies": { 14 | "npm-run-all": "^4.1.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AspNetCoreExtensionCommands/AspNetCoreExtensionCommands.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/AspNetCoreExtensionCommands/BuildCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton; 3 | 4 | #region sample_using_OaktonCommandAssemblyAttribute 5 | [assembly:Oakton.OaktonCommandAssembly] 6 | #endregion 7 | 8 | namespace AspNetCoreExtensionCommands 9 | { 10 | 11 | #region sample_SmokeCommand 12 | [Description("Simply try to build a web host as a smoke test", Name = "smoke")] 13 | public class SmokeCommand : OaktonCommand 14 | { 15 | public override bool Execute(NetCoreInput input) 16 | { 17 | // This method builds out the IWebHost for your 18 | // configured IHostBuilder of the application 19 | using (var host = input.BuildHost()) 20 | { 21 | Console.WriteLine("It's all good"); 22 | } 23 | 24 | return true; 25 | } 26 | } 27 | #endregion 28 | } 29 | -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyVersion("1.0.0")] 5 | -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/AssemblyInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using JasperFx.Core; 5 | using Oakton; 6 | 7 | namespace CommonAssemblyInfo 8 | { 9 | 10 | public class AssemblyInfoCommand : OaktonCommand 11 | { 12 | public AssemblyInfoCommand() 13 | { 14 | Usage("Generate an AssemblyInfo.cs file").Arguments(x => x.Path); 15 | } 16 | 17 | public override bool Execute(AssemblyInfoInput input) 18 | { 19 | var path = Directory.GetCurrentDirectory().AppendPath(input.Path); 20 | Console.WriteLine("Writing an AssemblyInfo to " + input.Path); 21 | 22 | using (var stream = new FileStream(path, FileMode.Create)) 23 | { 24 | var file = new StreamWriter(stream); 25 | var writer = new CompoundWriter(file, Console.Out); 26 | 27 | input.WriteValues(writer); 28 | 29 | file.Flush(); 30 | } 31 | 32 | return true; 33 | } 34 | 35 | public class CompoundWriter : TextWriter 36 | { 37 | private readonly TextWriter _inner1; 38 | private readonly TextWriter _inner2; 39 | 40 | public CompoundWriter(TextWriter inner1, TextWriter inner2) 41 | { 42 | _inner1 = inner1; 43 | _inner2 = inner2; 44 | } 45 | 46 | public override void Write(char value) 47 | { 48 | _inner1.Write(value); 49 | _inner2.Write(value); 50 | } 51 | 52 | public override void WriteLine(string value) 53 | { 54 | _inner1.WriteLine(value); 55 | _inner2.WriteLine(value); 56 | } 57 | 58 | public override Encoding Encoding => _inner1.Encoding; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/AssemblyInfoInput.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Reflection; 4 | using Oakton; 5 | 6 | namespace CommonAssemblyInfo 7 | { 8 | [Description("Dynamically generates an AssemblyInfo.cs file based on the command inputs")] 9 | public class AssemblyInfoInput 10 | { 11 | private readonly IDictionary _values = new Dictionary(); 12 | 13 | [Description("The relative file path to write the generated file")] public string Path { get; set; } 14 | 15 | [Description("[AssemblyDescription] value")] 16 | public string DescriptionFlag 17 | { 18 | set 19 | { 20 | writeAttribute(value); 21 | } 22 | } 23 | 24 | [Description("[AssemblyProduct] value")] 25 | public string ProductFlag 26 | { 27 | set 28 | { 29 | writeAttribute(value); 30 | } 31 | } 32 | 33 | [Description("[AssemblyCopyright] value")] 34 | public string CopyrightFlag 35 | { 36 | set 37 | { 38 | writeAttribute(value); 39 | } 40 | } 41 | 42 | [Description("[AssemblyTrademark] value")] 43 | public string TrademarkFlag 44 | { 45 | set 46 | { 47 | writeAttribute(value); 48 | } 49 | } 50 | 51 | [Description("[AssemblyVersion] value")] 52 | public string VersionFlag 53 | { 54 | set 55 | { 56 | writeAttribute(value); 57 | } 58 | } 59 | 60 | [Description("[AssemblyFileVersion] value"), FlagAlias("file-alias", 'f')] 61 | public string FileVersionFlag 62 | { 63 | set 64 | { 65 | writeAttribute(value); 66 | } 67 | } 68 | 69 | [Description("[AssemblyInformationalVersion] value")] 70 | public string InformationalVersion 71 | { 72 | set 73 | { 74 | writeAttribute(value); 75 | } 76 | } 77 | 78 | private void writeAttribute(string value) 79 | { 80 | var attName = typeof(T).Name.Replace("Attribute", ""); 81 | if (_values.ContainsKey(attName)) 82 | { 83 | _values[attName] = value; 84 | } 85 | else 86 | { 87 | _values.Add(attName, value); 88 | } 89 | } 90 | 91 | public void WriteValues(TextWriter writer) 92 | { 93 | writer.WriteLine("using System.Reflection;"); 94 | writer.WriteLine("using System.Runtime.InteropServices;"); 95 | writer.WriteLine(""); 96 | 97 | foreach (var pair in _values) 98 | { 99 | writer.WriteLine($"[assembly: {pair.Key}(\"{pair.Value}\")]"); 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/CommonAssemblyInfo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Generate AssemblyInfo.cs files in your CI process 5 | CommonAssemblyInfo 6 | en-US 7 | 1.0.0 8 | Jeremy D. Miller 9 | net6.0;net7.0 10 | dotnet-assemblyinfo 11 | Exe 12 | CommonAssemblyInfo 13 | false 14 | false 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/Program.cs: -------------------------------------------------------------------------------- 1 | using Oakton; 2 | 3 | namespace CommonAssemblyInfo 4 | { 5 | public class Program 6 | { 7 | public static int Main(string[] args) 8 | { 9 | return CommandExecutor.ExecuteCommand(args, "assemblyinfo.opts"); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/CommonAssemblyInfo/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyProduct("CommonAssemblyInfo")] 9 | 10 | -------------------------------------------------------------------------------- /src/DotNet6BootstrappedConsole/DotNet6BootstrappedConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0;net7.0;net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/DotNet6BootstrappedConsole/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using Microsoft.Extensions.Hosting; 4 | using Oakton; 5 | 6 | return Host.CreateDefaultBuilder(args) 7 | .RunOaktonCommandsSynchronously(args); -------------------------------------------------------------------------------- /src/EnvironmentCheckDemonstrator/Describers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Oakton; 5 | using Oakton.Descriptions; 6 | using Spectre.Console; 7 | 8 | namespace EnvironmentCheckDemonstrator 9 | { 10 | public class Describer1 : IDescribedSystemPart 11 | { 12 | public string Title { get; } = "The First Part"; 13 | 14 | public Task Write(TextWriter writer) 15 | { 16 | return writer.WriteLineAsync("Description of the first part"); 17 | } 18 | } 19 | 20 | public class Describer2 : IDescribedSystemPart, IWriteToConsole 21 | { 22 | public string Title { get; } = "The Second Part"; 23 | public string Key { get; } = "part2"; 24 | public Task Write(TextWriter writer) 25 | { 26 | return writer.WriteLineAsync("Description of the second part"); 27 | } 28 | 29 | public Task WriteToConsole() 30 | { 31 | AnsiConsole.MarkupLine("[darkblue]Second part writing in blue[/]"); 32 | return Task.CompletedTask; 33 | } 34 | } 35 | 36 | public class Describer3 : IDescribedSystemPart 37 | { 38 | public string Title { get; } = "The Third Part"; 39 | public string Key { get; } = "part3"; 40 | public Task Write(TextWriter writer) 41 | { 42 | return writer.WriteLineAsync("Description of the third part"); 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /src/EnvironmentCheckDemonstrator/EnvironmentCheckDemonstrator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0;net7.0;net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/EnvironmentCheckDemonstrator/FakeResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Oakton.Resources; 5 | using Spectre.Console; 6 | using Spectre.Console.Rendering; 7 | 8 | namespace EnvironmentCheckDemonstrator 9 | { 10 | public class FakeResource : IStatefulResource 11 | { 12 | private static Random _random = new Random(); 13 | 14 | public FakeResource(string type, string name) 15 | { 16 | Type = type; 17 | Name = name; 18 | } 19 | 20 | public Exception Failure { get; set; } 21 | 22 | public int Delay { get; set; } 23 | 24 | public async Task Check(CancellationToken token) 25 | { 26 | await Task.Delay(_random.Next(100, 1000), token); 27 | await Task.Delay(Delay.Seconds(), token); 28 | if (Failure != null) throw Failure; 29 | } 30 | 31 | public async Task ClearState(CancellationToken token) 32 | { 33 | await Task.Delay(_random.Next(100, 1000), token); 34 | if (Failure != null) throw Failure; 35 | } 36 | 37 | public async Task Teardown(CancellationToken token) 38 | { 39 | await Task.Delay(_random.Next(100, 1000), token); 40 | if (Failure != null) throw Failure; 41 | } 42 | 43 | public async Task Setup(CancellationToken token) 44 | { 45 | await Task.Delay(_random.Next(100, 1000), token); 46 | if (Failure != null) throw Failure; 47 | } 48 | 49 | public async Task DetermineStatus(CancellationToken token) 50 | { 51 | await Task.Delay(_random.Next(100, 1000), token); 52 | 53 | if (Failure != null) throw Failure; 54 | 55 | var table = new Table(); 56 | table.AddColumns("Number", "Value"); 57 | for (var i = 0; i < _random.Next(1, 10); i++) 58 | { 59 | table.AddRow(i.ToString(), Guid.NewGuid().ToString()); 60 | } 61 | 62 | return table; 63 | } 64 | 65 | public string Type { get; } 66 | public string Name { get; } 67 | } 68 | } -------------------------------------------------------------------------------- /src/EnvironmentCheckDemonstrator/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Oakton; 7 | using Oakton.Descriptions; 8 | using Oakton.Environment; 9 | using Oakton.Resources; 10 | 11 | namespace EnvironmentCheckDemonstrator 12 | { 13 | class Program 14 | { 15 | #region sample_extending_describe 16 | static Task Main(string[] args) 17 | { 18 | return Host.CreateDefaultBuilder() 19 | .ConfigureServices(services => 20 | { 21 | 22 | for (int i = 0; i < 5; i++) 23 | { 24 | services.AddSingleton(new GoodEnvironmentCheck(i + 1)); 25 | services.AddSingleton(new BadEnvironmentCheck(i + 1)); 26 | 27 | 28 | services.AddSingleton(new FakeResource("Database", "Db " + (i + 1))); 29 | } 30 | 31 | services.AddSingleton(new FakeResource("Bad", "Blows Up") 32 | { 33 | Failure = new DivideByZeroException() 34 | }); 35 | 36 | services.CheckEnvironment("Inline, async check", async (services, token) => 37 | { 38 | await Task.Delay(1.Milliseconds(), token); 39 | 40 | throw new Exception("I failed!"); 41 | }); 42 | 43 | 44 | // This is an example of adding custom 45 | // IDescriptionSystemPart types to your 46 | // application that can participate in 47 | // the describe output 48 | services.AddDescription(); 49 | services.AddDescription(); 50 | services.AddDescription(); 51 | 52 | }) 53 | .RunOaktonCommands(args); 54 | } 55 | #endregion 56 | } 57 | 58 | public class GoodEnvironmentCheck : IEnvironmentCheck 59 | { 60 | private readonly int _number; 61 | 62 | public GoodEnvironmentCheck(int number) 63 | { 64 | _number = number; 65 | } 66 | 67 | public string Description => "Good #" + _number; 68 | public async Task Assert(IServiceProvider services, CancellationToken cancellation) 69 | { 70 | await Task.Delay(3.Seconds(), cancellation); 71 | } 72 | } 73 | 74 | public class BadEnvironmentCheck : IEnvironmentCheck 75 | { 76 | private readonly int _number; 77 | 78 | public BadEnvironmentCheck(int number) 79 | { 80 | _number = number; 81 | } 82 | 83 | public string Description => "Bad #" + _number; 84 | public async Task Assert(IServiceProvider services, CancellationToken cancellation) 85 | { 86 | await Task.Delay(3.Seconds(), cancellation); 87 | throw new DivideByZeroException("Boom!"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ExtensionCommands/ExtensionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ExtensionCommands; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Oakton; 5 | using Oakton.Resources; 6 | 7 | [assembly:OaktonCommandAssembly(typeof(ExtensionServices))] 8 | 9 | namespace ExtensionCommands 10 | { 11 | public class ExtensionServices : IServiceRegistrations 12 | { 13 | public void Configure(IServiceCollection services) 14 | { 15 | services.AddSingleton(); 16 | services.AddSingleton(new ExtensionResource()); 17 | } 18 | } 19 | 20 | public interface IExtensionService{} 21 | public class ExtensionService : IExtensionService{} 22 | 23 | public class ExtensionInput 24 | { 25 | 26 | } 27 | 28 | public class ExtensionResource : StatefulResourceBase 29 | { 30 | public ExtensionResource() : base("Extension", "The Extension") 31 | { 32 | } 33 | } 34 | 35 | [Description("An extension command loaded from another assembly", Name = "extension")] 36 | public class ExtensionCommand : OaktonCommand 37 | { 38 | public override bool Execute(ExtensionInput input) 39 | { 40 | Console.WriteLine("I'm an extension command"); 41 | return true; 42 | } 43 | } 44 | 45 | [Description("A second extension command loaded from another assembly", Name = "extension2")] 46 | public class Extension2Command : OaktonCommand 47 | { 48 | public override bool Execute(ExtensionInput input) 49 | { 50 | Console.WriteLine("I'm an extension command"); 51 | return true; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/ExtensionCommands/ExtensionCommands.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/MinimalApi/MinimalApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0 5 | enable 6 | 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MinimalApi/Program.cs: -------------------------------------------------------------------------------- 1 | #region sample_bootstrapping_minimal_api 2 | 3 | using Oakton; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // This isn't required, but it "helps" Oakton to enable 8 | // some additional diagnostics for the stateful resource 9 | // model 10 | builder.Host.ApplyOaktonExtensions(); 11 | 12 | var app = builder.Build(); 13 | app.MapGet("/", () => "Hello World!"); 14 | 15 | // Note the usage of await to force the implied 16 | // Program.Main() method to be asynchronous 17 | return await app.RunOaktonCommands(args); 18 | 19 | #endregion -------------------------------------------------------------------------------- /src/MinimalApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:43449", 7 | "sslPort": 44365 8 | } 9 | }, 10 | "profiles": { 11 | "MinimalApi": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7197;http://localhost:5197", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MinimalApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MinimalApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/MultipleCommands/MultipleCommands.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/MultipleCommands/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using Oakton; 5 | 6 | namespace MultipleCommands 7 | { 8 | class Program 9 | { 10 | #region sample_MultipleCommands.Program.Main 11 | static int Main(string[] args) 12 | { 13 | var executor = CommandExecutor.For(_ => 14 | { 15 | // Find and apply all command classes discovered 16 | // in this assembly 17 | _.RegisterCommands(typeof(Program).GetTypeInfo().Assembly); 18 | }); 19 | 20 | return executor.Execute(args); 21 | } 22 | #endregion 23 | 24 | /* 25 | #region sample_MultipleCommands.Program.Main.Async 26 | static Task Main(string[] args) 27 | { 28 | var executor = CommandExecutor.For(_ => 29 | { 30 | // Find and apply all command classes discovered 31 | // in this assembly 32 | _.RegisterCommands(typeof(Program).GetTypeInfo().Assembly); 33 | }); 34 | 35 | return executor.ExecuteAsync(args); 36 | } 37 | #endregion 38 | */ 39 | } 40 | 41 | #region sample_git_commands 42 | [Description("Switch branches or restore working tree files")] 43 | public class CheckoutCommand : OaktonAsyncCommand 44 | { 45 | public override async Task Execute(CheckoutInput input) 46 | { 47 | await Task.CompletedTask; 48 | return true; 49 | } 50 | } 51 | 52 | [Description("Remove untracked files from the working tree")] 53 | public class CleanCommand : OaktonCommand 54 | { 55 | public override bool Execute(CleanInput input) 56 | { 57 | return true; 58 | } 59 | } 60 | #endregion 61 | 62 | #region sample_CheckoutInput 63 | public class CheckoutInput 64 | { 65 | [FlagAlias("create-branch",'b')] 66 | public string CreateBranchFlag { get; set; } 67 | 68 | public bool DetachFlag { get; set; } 69 | 70 | public bool ForceFlag { get; set; } 71 | } 72 | #endregion 73 | 74 | 75 | #region sample_CleanInput 76 | public class CleanInput 77 | { 78 | [Description("Do it now!")] 79 | public bool ForceFlag { get; set; } 80 | 81 | [FlagAlias('d')] 82 | [Description("Remove untracked directories in addition to untracked files")] 83 | public bool RemoveUntrackedDirectoriesFlag { get; set; } 84 | 85 | [FlagAlias('x')] 86 | [Description("Remove only files ignored by Git")] 87 | public bool DoNoUseStandardIgnoreRulesFlag { get; set; } 88 | } 89 | #endregion 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/MvcApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Hosting; 10 | using MvcApp.Models; 11 | using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; 12 | 13 | namespace MvcApp.Controllers 14 | { 15 | public class HomeController : Controller 16 | { 17 | #if NETCOREAPP2_2 18 | private readonly IHostingEnvironment _environment; 19 | #else 20 | private readonly IHostEnvironment _environment; 21 | #endif 22 | private readonly IConfiguration _configuration; 23 | 24 | #if NETCOREAPP2_2 25 | public HomeController(IHostingEnvironment environment, IConfiguration configuration) 26 | #else 27 | public HomeController(IHostEnvironment environment, IConfiguration configuration) 28 | #endif 29 | { 30 | _environment = environment; 31 | _configuration = configuration; 32 | } 33 | 34 | public IActionResult Index() 35 | { 36 | var writer = new StringWriter(); 37 | writer.WriteLine("Hey, it's me!"); 38 | writer.WriteLine($"Environment: {_environment.EnvironmentName}"); 39 | 40 | writer.WriteLine($"Color: {_configuration["color"]}"); 41 | writer.WriteLine($"Number: {_configuration["number"]}"); 42 | 43 | return Content(writer.ToString(), "text/plain"); 44 | } 45 | 46 | public IActionResult Privacy() 47 | { 48 | return View(); 49 | } 50 | 51 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 52 | public IActionResult Error() 53 | { 54 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MvcApp/Describers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Oakton; 5 | using Oakton.Descriptions; 6 | using Spectre.Console; 7 | 8 | namespace MvcApp 9 | { 10 | public class Describer1 : IDescribedSystemPart 11 | { 12 | public string Title { get; } = "The First Part"; 13 | 14 | public Task Write(TextWriter writer) 15 | { 16 | return writer.WriteLineAsync("Description of the first part"); 17 | } 18 | } 19 | 20 | public class Describer2 : IDescribedSystemPart, IWriteToConsole 21 | { 22 | public string Title { get; } = "The Second Part"; 23 | public string Key { get; } = "part2"; 24 | public Task Write(TextWriter writer) 25 | { 26 | return writer.WriteLineAsync("Description of the second part"); 27 | } 28 | 29 | public Task WriteToConsole() 30 | { 31 | AnsiConsole.MarkupLine("[darkblue]Second part writing in blue[/]"); 32 | return Task.CompletedTask; 33 | } 34 | } 35 | 36 | public class Describer3 : IDescribedSystemPart 37 | { 38 | public string Title { get; } = "The Third Part"; 39 | public string Key { get; } = "part3"; 40 | public Task Write(TextWriter writer) 41 | { 42 | return writer.WriteLineAsync("Description of the third part"); 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /src/MvcApp/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MvcApp.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /src/MvcApp/MvcApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0 5 | InProcess 6 | 7.1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/MvcApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using Oakton; 11 | 12 | namespace MvcApp 13 | { 14 | #if NETCOREAPP2_2 15 | /* 16 | #region sample_using_run_oakton_commands 17 | public class Program 18 | { 19 | public static Task Main(string[] args) 20 | { 21 | return CreateWebHostBuilder(args) 22 | 23 | // This extension method replaces the calls to 24 | // IWebHost.Build() and Start() 25 | .RunOaktonCommands(args); 26 | } 27 | 28 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 29 | WebHost.CreateDefaultBuilder(args) 30 | .UseStartup(); 31 | 32 | } 33 | #endregion 34 | */ 35 | #else 36 | #region sample_using_run_oakton_commands_3 37 | public class Program 38 | { 39 | public static Task Main(string[] args) 40 | { 41 | return CreateHostBuilder(args) 42 | 43 | // This extension method replaces the calls to 44 | // IWebHost.Build() and Start() 45 | .RunOaktonCommands(args); 46 | } 47 | 48 | public static IHostBuilder CreateHostBuilder(string[] args) => 49 | Host.CreateDefaultBuilder(args) 50 | .ConfigureWebHostDefaults(x => x.UseStartup()); 51 | 52 | } 53 | #endregion 54 | #endif 55 | } 56 | -------------------------------------------------------------------------------- /src/MvcApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:14428", 7 | "sslPort": 44309 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "MvcApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/MvcApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.HttpsPolicy; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Oakton.Descriptions; 15 | using Oakton.Environment; 16 | using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; 17 | 18 | namespace MvcApp 19 | { 20 | public class Startup 21 | { 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | public IConfiguration Configuration { get; } 28 | 29 | #region sample_ConfigureService_with_EnvironmentCheck 30 | // This method gets called by the runtime. Use this method to add services to the container. 31 | public void ConfigureServices(IServiceCollection services) 32 | { 33 | // Other registrations we don't care about... 34 | 35 | // This extension method is in Oakton.AspNetCore 36 | services.CheckEnvironment("Can connect to the application database", config => 37 | { 38 | var connectionString = config["connectionString"]; 39 | using (var conn = new SqlConnection(connectionString)) 40 | { 41 | // Just attempt to open the connection. If there's anything 42 | // wrong here, it's going to throw an exception 43 | conn.Open(); 44 | } 45 | }); 46 | 47 | // Ignore this please;) 48 | services.AddSingleton(); 49 | services.AddSingleton(); 50 | services.AddSingleton(); 51 | } 52 | #endregion 53 | 54 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 55 | #if NETCOREAPP2_2 56 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 57 | #else 58 | public void Configure(IApplicationBuilder app, IHostEnvironment env) 59 | #endif 60 | { 61 | if (env.IsDevelopment()) 62 | { 63 | app.UseDeveloperExceptionPage(); 64 | } 65 | else 66 | { 67 | app.UseExceptionHandler("/Home/Error"); 68 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 69 | app.UseHsts(); 70 | } 71 | 72 | app.UseHttpsRedirection(); 73 | app.UseStaticFiles(); 74 | app.UseCookiePolicy(); 75 | 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/MvcApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 |
6 |

Welcome

7 |

Learn about building Web apps with ASP.NET Core.

8 |
9 | -------------------------------------------------------------------------------- /src/MvcApp/Views/Home/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Privacy Policy"; 3 | } 4 |

@ViewData["Title"]

5 | 6 |

Use this page to detail your site's privacy policy.

7 | -------------------------------------------------------------------------------- /src/MvcApp/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | -------------------------------------------------------------------------------- /src/MvcApp/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 17 | 25 | } 26 | -------------------------------------------------------------------------------- /src/MvcApp/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/MvcApp/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using MvcApp 2 | @using MvcApp.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /src/MvcApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/MvcApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MvcApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /src/MvcApp/description.html: -------------------------------------------------------------------------------- 1 | 2 |
  1. The First Part
  2. The Second Part
  3. The Third Part

The First Part

Description of the first part
back to top...

The Second Part

Description of the second part
back to top...

The Third Part

The third part Html

back to top...
-------------------------------------------------------------------------------- /src/MvcApp/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Sticky footer styles 11 | -------------------------------------------------- */ 12 | html { 13 | font-size: 14px; 14 | } 15 | @media (min-width: 768px) { 16 | html { 17 | font-size: 16px; 18 | } 19 | } 20 | 21 | .border-top { 22 | border-top: 1px solid #e5e5e5; 23 | } 24 | .border-bottom { 25 | border-bottom: 1px solid #e5e5e5; 26 | } 27 | 28 | .box-shadow { 29 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 30 | } 31 | 32 | button.accept-policy { 33 | font-size: 1rem; 34 | line-height: inherit; 35 | } 36 | 37 | /* Sticky footer styles 38 | -------------------------------------------------- */ 39 | html { 40 | position: relative; 41 | min-height: 100%; 42 | } 43 | 44 | body { 45 | /* Margin bottom by footer height */ 46 | margin-bottom: 60px; 47 | } 48 | .footer { 49 | position: absolute; 50 | bottom: 0; 51 | width: 100%; 52 | white-space: nowrap; 53 | /* Set the fixed height of the footer here */ 54 | height: 60px; 55 | line-height: 60px; /* Vertically center the text there */ 56 | } 57 | -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/src/MvcApp/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/MvcApp/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /src/Net5WebApi/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Net5WebApi.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [HttpGet] 27 | public IEnumerable Get() 28 | { 29 | var rng = new Random(); 30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 31 | { 32 | Date = DateTime.Now.AddDays(index), 33 | TemperatureC = rng.Next(-20, 55), 34 | Summary = Summaries[rng.Next(Summaries.Length)] 35 | }) 36 | .ToArray(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Net5WebApi/Net5WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Net5WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:18717", 8 | "sslPort": 44371 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Net5WebApi": { 21 | "commandName": "Project", 22 | "commandLineArgs": "job1", 23 | "dotnetRunMessages": "true", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Net5WebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.OpenApi.Models; 14 | 15 | namespace Net5WebApi 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllers(); 30 | services.AddSwaggerGen(c => 31 | { 32 | c.SwaggerDoc("v1", new OpenApiInfo {Title = "Net5WebApi", Version = "v1"}); 33 | }); 34 | } 35 | 36 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 37 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 38 | { 39 | if (env.IsDevelopment()) 40 | { 41 | app.UseDeveloperExceptionPage(); 42 | app.UseSwagger(); 43 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Net5WebApi v1")); 44 | } 45 | 46 | app.UseHttpsRedirection(); 47 | 48 | app.UseRouting(); 49 | 50 | app.UseAuthorization(); 51 | 52 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Net5WebApi/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Net5WebApi 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Net5WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Net5WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/Oakton/ActivatorCommandCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | public class ActivatorCommandCreator : ICommandCreator 6 | { 7 | public IOaktonCommand CreateCommand(Type commandType) 8 | { 9 | return (IOaktonCommand)Activator.CreateInstance(commandType); 10 | } 11 | 12 | public object CreateModel(Type modelType) 13 | { 14 | return Activator.CreateInstance(modelType); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Oakton/Argument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using JasperFx.Core; 5 | using JasperFx.Core.Reflection; 6 | using Oakton.Internal.Conversion; 7 | using Oakton.Parsing; 8 | 9 | namespace Oakton; 10 | 11 | public class Argument : TokenHandlerBase 12 | { 13 | private readonly MemberInfo _member; 14 | protected Func _converter; 15 | private bool _isLatched; 16 | private readonly Type _memberType; 17 | 18 | public Argument(MemberInfo member, Conversions conversions) : base(member) 19 | { 20 | _member = member; 21 | _memberType = member.GetMemberType(); 22 | _converter = conversions.FindConverter(_memberType); 23 | } 24 | 25 | public override bool Handle(object input, Queue tokens) 26 | { 27 | if (_isLatched) 28 | { 29 | return false; 30 | } 31 | 32 | if (tokens.NextIsFlag()) 33 | { 34 | if (_memberType.IsNumeric()) 35 | { 36 | if (!decimal.TryParse(tokens.Peek(), out var number)) 37 | { 38 | return false; 39 | } 40 | } 41 | else 42 | { 43 | return false; 44 | } 45 | } 46 | 47 | var value = _converter(tokens.Dequeue()); 48 | setValue(input, value); 49 | 50 | _isLatched = true; 51 | 52 | return true; 53 | } 54 | 55 | public override string ToUsageDescription() 56 | { 57 | var memberType = _member.GetMemberType(); 58 | if (memberType.GetTypeInfo().IsEnum) 59 | { 60 | return Enum.GetNames(memberType).Join("|"); 61 | } 62 | 63 | return $"<{_member.Name.ToLower()}>"; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Oakton/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Tests")] -------------------------------------------------------------------------------- /src/Oakton/CommandFailureException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | public class CommandFailureException : Exception 6 | { 7 | public CommandFailureException(string message) : base(message) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /src/Oakton/CommandRun.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Oakton; 4 | 5 | public class CommandRun 6 | { 7 | public IOaktonCommand Command { get; set; } 8 | public object Input { get; set; } 9 | 10 | public Task Execute() 11 | { 12 | return Command.Execute(Input); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Oakton/Commands/CheckEnvironmentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using JasperFx.Core; 5 | using Oakton.Environment; 6 | using Spectre.Console; 7 | 8 | namespace Oakton.Commands; 9 | 10 | public class CheckEnvironmentInput : NetCoreInput 11 | { 12 | [Description("Use to optionally write the results of the environment checks to a file")] 13 | public string FileFlag { get; set; } 14 | } 15 | 16 | [Description("Execute all environment checks against the application", Name = "check-env")] 17 | public class CheckEnvironmentCommand : OaktonAsyncCommand 18 | { 19 | public override async Task Execute(CheckEnvironmentInput input) 20 | { 21 | AnsiConsole.Write( 22 | new FigletText("Oakton"){Justification = Justify.Left}); 23 | 24 | 25 | using var host = input.BuildHost(); 26 | var results = await EnvironmentChecker.ExecuteAllEnvironmentChecks(host.Services); 27 | 28 | if (input.FileFlag.IsNotEmpty()) 29 | { 30 | results.WriteToFile(input.FileFlag); 31 | Console.WriteLine("Writing environment checks to " + input.FileFlag); 32 | } 33 | 34 | if (results.Failures.Any()) 35 | { 36 | AnsiConsole.MarkupLine("[red]Some environment checks failed![/]"); 37 | return false; 38 | } 39 | 40 | AnsiConsole.MarkupLine("[green]All environment checks are good![/]"); 41 | return true; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Oakton/Commands/RunCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Loader; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | using Oakton.Environment; 10 | 11 | namespace Oakton.Commands; 12 | 13 | public class RunInput : NetCoreInput 14 | { 15 | [Description("Run the environment checks before starting the host")] 16 | public bool CheckFlag { get; set; } 17 | } 18 | 19 | [Description("Start and run this .Net application")] 20 | public class RunCommand : OaktonAsyncCommand 21 | { 22 | public override async Task Execute(RunInput input) 23 | { 24 | using var host = input.BuildHost(); 25 | if (input.CheckFlag) 26 | { 27 | var checks = await EnvironmentChecker.ExecuteAllEnvironmentChecks(host.Services); 28 | checks.Assert(); 29 | } 30 | 31 | var reset = new ManualResetEventSlim(); 32 | // ReSharper disable once PossibleNullReferenceException 33 | AssemblyLoadContext.GetLoadContext(typeof(RunCommand).Assembly).Unloading += 34 | (Action)(context => reset.Set()); 35 | Console.CancelKeyPress += (ConsoleCancelEventHandler)((sender, eventArgs) => 36 | { 37 | reset.Set(); 38 | eventArgs.Cancel = true; 39 | }); 40 | 41 | var lifetime = host.Services.GetService(); 42 | lifetime?.ApplicationStopping.Register(() => reset.Set()); 43 | 44 | await host.StartAsync(); 45 | reset.Wait(); 46 | await host.StopAsync(); 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Oakton/DependencyInjectionCommandCreator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using JasperFx.Core; 6 | using JasperFx.Core.Reflection; 7 | using Oakton.Help; 8 | 9 | namespace Oakton; 10 | 11 | internal class DependencyInjectionCommandCreator : ICommandCreator 12 | { 13 | private readonly IServiceProvider _serviceProvider; 14 | public DependencyInjectionCommandCreator(IServiceProvider serviceProvider) 15 | { 16 | _serviceProvider = serviceProvider; 17 | } 18 | 19 | public IOaktonCommand CreateCommand(Type commandType) 20 | { 21 | if (commandType.GetProperties().Any(x => x.HasAttribute())) 22 | { 23 | return new WrappedOaktonCommand(_serviceProvider, commandType); 24 | } 25 | 26 | return ActivatorUtilities.CreateInstance(_serviceProvider, commandType) as IOaktonCommand; 27 | } 28 | 29 | public object CreateModel(Type modelType) 30 | { 31 | return Activator.CreateInstance(modelType)!; 32 | } 33 | } 34 | 35 | internal class WrappedOaktonCommand : IOaktonCommand 36 | { 37 | private readonly AsyncServiceScope _scope; 38 | private readonly IOaktonCommand _inner; 39 | 40 | public WrappedOaktonCommand(IServiceProvider provider, Type commandType) 41 | { 42 | _scope = provider.CreateAsyncScope(); 43 | _inner = (IOaktonCommand)_scope.ServiceProvider.GetRequiredService(commandType); 44 | } 45 | 46 | public Type InputType => _inner.InputType; 47 | public UsageGraph Usages => _inner.Usages; 48 | public async Task Execute(object input) 49 | { 50 | await using (_scope) 51 | { 52 | // Execute your actual command 53 | return await _inner.Execute(input); 54 | } 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Oakton/DescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Adds a textual description to arguments or flags on input classes, or on a command class 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Property)] 9 | public class DescriptionAttribute : Attribute 10 | { 11 | public DescriptionAttribute(string description) 12 | { 13 | Description = description; 14 | } 15 | 16 | public string Description { get; set; } 17 | 18 | public string Name { get; set; } 19 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/DescribeInput.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton.Descriptions; 2 | 3 | public class DescribeInput : NetCoreInput 4 | { 5 | [Description("Optionally write the description to the given file location")] 6 | public string FileFlag { get; set; } = null; 7 | 8 | [Description("Do not write any output to the console")] 9 | public bool SilentFlag { get; set; } = false; 10 | 11 | [Description("Filter the output to only a single described part")] 12 | public string TitleFlag { get; set; } 13 | 14 | [Description("If set, the command only lists the known part titles")] 15 | public bool ListFlag { get; set; } 16 | 17 | [Description("If set, interactively select which part(s) to preview")] 18 | public bool InteractiveFlag { get; set; } 19 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/IDescribedSystemPart.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace Oakton.Descriptions; 5 | 6 | #region sample_IDescribedSystemPart 7 | 8 | /// 9 | /// Base class for a "described" part of your application. 10 | /// Implementations of this type should be registered in your 11 | /// system's DI container to be exposed through the "describe" 12 | /// command 13 | /// 14 | public interface IDescribedSystemPart 15 | { 16 | /// 17 | /// A descriptive title to be shown in the rendered output 18 | /// 19 | string Title { get; } 20 | 21 | /// 22 | /// Write markdown formatted text to describe this system part 23 | /// 24 | /// 25 | /// 26 | Task Write(TextWriter writer); 27 | } 28 | 29 | #endregion -------------------------------------------------------------------------------- /src/Oakton/Descriptions/IDescribedSystemPartFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton.Descriptions; 2 | 3 | #region sample_IDescribedSystemPartFactory 4 | 5 | /// 6 | /// Register implementations of this service to help 7 | /// the describe command discover additional system parts 8 | /// 9 | public interface IDescribedSystemPartFactory 10 | { 11 | IDescribedSystemPart[] Parts(); 12 | } 13 | 14 | #endregion -------------------------------------------------------------------------------- /src/Oakton/Descriptions/IDescribesProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Descriptions; 4 | 5 | /// 6 | /// Interface to expose key/value pairs to diagnostic output 7 | /// 8 | public interface IDescribesProperties 9 | { 10 | IDictionary DescribeProperties(); 11 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/IRequiresServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton.Descriptions; 4 | 5 | internal interface IRequiresServices 6 | { 7 | void Resolve(IServiceProvider services); 8 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/ITreeDescriber.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console; 2 | 3 | namespace Oakton.Descriptions; 4 | 5 | /// 6 | /// Interface to expose additional diagnostic information to a Spectre tree node 7 | /// 8 | public interface ITreeDescriber 9 | { 10 | void Describe(TreeNode parentNode); 11 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/IWriteToConsole.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Oakton.Descriptions; 4 | 5 | /// 6 | /// Optional interface for exposing specialized console output 7 | /// in the "describe" command 8 | /// 9 | public interface IWriteToConsole 10 | { 11 | Task WriteToConsole(); 12 | } -------------------------------------------------------------------------------- /src/Oakton/Descriptions/LambdaDescribedSystemPart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Oakton.Descriptions; 7 | 8 | internal class LambdaDescribedSystemPart : IDescribedSystemPart, IRequiresServices 9 | { 10 | private readonly Func _write; 11 | private T _service; 12 | 13 | public LambdaDescribedSystemPart(string title, Func write) 14 | { 15 | Title = title; 16 | _write = write; 17 | } 18 | 19 | public string Title { get; } 20 | 21 | public Task Write(TextWriter writer) 22 | { 23 | return _service == null ? default : _write(_service, writer); 24 | } 25 | 26 | public void Resolve(IServiceProvider services) 27 | { 28 | _service = services.GetService(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Oakton/Environment/EnvironmentCheckException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Oakton.Environment; 5 | 6 | public class EnvironmentCheckException : AggregateException 7 | { 8 | public EnvironmentCheckException(EnvironmentCheckResults results) : base(results.ToString(), 9 | results.Failures.Select(x => x.Exception)) 10 | { 11 | Results = results; 12 | } 13 | 14 | public EnvironmentCheckResults Results { get; } 15 | } -------------------------------------------------------------------------------- /src/Oakton/Environment/EnvironmentChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Oakton.Resources; 8 | using Spectre.Console; 9 | 10 | namespace Oakton.Environment; 11 | 12 | /// 13 | /// Executes the environment checks registered in an IoC container 14 | /// 15 | public static class EnvironmentChecker 16 | { 17 | public static async Task ExecuteAllEnvironmentChecks(IServiceProvider services, 18 | CancellationToken token = default) 19 | { 20 | var results = new EnvironmentCheckResults(); 21 | 22 | var checks = services.discoverChecks().ToArray(); 23 | if (!checks.Any()) 24 | { 25 | AnsiConsole.WriteLine("No environment checks."); 26 | return results; 27 | } 28 | 29 | await AnsiConsole.Progress().StartAsync(async c => 30 | { 31 | var task = c.AddTask("[bold]Running Environment Checks[/]", new ProgressTaskSettings 32 | { 33 | MaxValue = checks.Length 34 | }); 35 | 36 | for (var i = 0; i < checks.Length; i++) 37 | { 38 | var check = checks[i]; 39 | 40 | try 41 | { 42 | await check.Assert(services, token); 43 | 44 | AnsiConsole.MarkupLine( 45 | $"[green]{(i + 1).ToString().PadLeft(4)}.) Success: {check.Description}[/]"); 46 | 47 | results.RegisterSuccess(check.Description); 48 | } 49 | catch (Exception e) 50 | { 51 | AnsiConsole.MarkupLine( 52 | $"[red]{(i + 1).ToString().PadLeft(4)}.) Failed: {check.Description}[/]"); 53 | AnsiConsole.WriteException(e); 54 | 55 | results.RegisterFailure(check.Description, e); 56 | } 57 | finally 58 | { 59 | task.Increment(1); 60 | } 61 | } 62 | 63 | task.StopTask(); 64 | }); 65 | 66 | return results; 67 | } 68 | 69 | private static IEnumerable discoverChecks(this IServiceProvider services) 70 | { 71 | foreach (var check in services.GetServices()) yield return check; 72 | 73 | foreach (var factory in services.GetServices()) 74 | { 75 | foreach (var check in factory.Build()) yield return check; 76 | } 77 | 78 | foreach (var resource in services.GetServices()) 79 | yield return new ResourceEnvironmentCheck(resource); 80 | 81 | foreach (var source in services.GetServices()) 82 | { 83 | foreach (var resource in source.FindResources()) yield return new ResourceEnvironmentCheck(resource); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Oakton/Environment/FileExistsCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Oakton.Environment; 7 | 8 | public class FileExistsCheck : IEnvironmentCheck 9 | { 10 | private readonly string _file; 11 | 12 | public FileExistsCheck(string file) 13 | { 14 | _file = file; 15 | } 16 | 17 | 18 | public Task Assert(IServiceProvider services, CancellationToken cancellation) 19 | { 20 | if (!File.Exists(_file)) 21 | { 22 | throw new Exception($"File '{_file}' cannot be found!"); 23 | } 24 | 25 | return Task.CompletedTask; 26 | } 27 | 28 | public string Description => ToString(); 29 | 30 | public override string ToString() 31 | { 32 | return $"File '{_file}' exists"; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Oakton/Environment/IEnvironmentCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Oakton.Environment; 6 | 7 | #region sample_IEnvironmentCheck 8 | 9 | /// 10 | /// Executed during bootstrapping time to carry out environment tests 11 | /// against the application 12 | /// 13 | public interface IEnvironmentCheck 14 | { 15 | /// 16 | /// A textual description for command line output that describes 17 | /// what is being checked 18 | /// 19 | string Description { get; } 20 | 21 | /// 22 | /// Asserts that the current check is valid. Throw an exception 23 | /// to denote a failure 24 | /// 25 | Task Assert(IServiceProvider services, CancellationToken cancellation); 26 | } 27 | 28 | #endregion -------------------------------------------------------------------------------- /src/Oakton/Environment/IEnvironmentCheckFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton.Environment; 2 | 3 | public interface IEnvironmentCheckFactory 4 | { 5 | IEnvironmentCheck[] Build(); 6 | } -------------------------------------------------------------------------------- /src/Oakton/Environment/LambdaCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Oakton.Environment; 6 | 7 | public class LambdaCheck : IEnvironmentCheck 8 | { 9 | private readonly Func _action; 10 | 11 | public LambdaCheck(string description, Func action) 12 | { 13 | Description = description; 14 | _action = action; 15 | } 16 | 17 | public Task Assert(IServiceProvider services, CancellationToken cancellation) 18 | { 19 | return _action(services, cancellation); 20 | } 21 | 22 | public string Description { get; } 23 | 24 | public override string ToString() 25 | { 26 | return Description; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Oakton/FlagAliasAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Use to override the long and/or short flag keys of a property or field 7 | /// 8 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 9 | public class FlagAliasAttribute : Attribute 10 | { 11 | public FlagAliasAttribute(string longAlias, char oneLetterAlias) 12 | { 13 | LongAlias = longAlias; 14 | OneLetterAlias = oneLetterAlias; 15 | } 16 | 17 | public FlagAliasAttribute(char oneLetterAlias) 18 | { 19 | OneLetterAlias = oneLetterAlias; 20 | } 21 | 22 | public FlagAliasAttribute(string longAlias) 23 | { 24 | LongAlias = longAlias; 25 | } 26 | 27 | public FlagAliasAttribute(string longAlias, bool longAliasOnly) 28 | { 29 | LongAlias = longAlias; 30 | LongAliasOnly = longAliasOnly; 31 | } 32 | 33 | public string LongAlias { get; } 34 | 35 | 36 | public char? OneLetterAlias { get; } 37 | 38 | public bool LongAliasOnly { get; } 39 | } -------------------------------------------------------------------------------- /src/Oakton/Help/CommandUsage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using JasperFx.Core; 4 | using Oakton.Parsing; 5 | using Spectre.Console; 6 | 7 | namespace Oakton.Help; 8 | 9 | public class CommandUsage 10 | { 11 | public string Description { get; set; } 12 | public IEnumerable Arguments { get; set; } 13 | public IEnumerable ValidFlags { get; set; } 14 | 15 | public string ToUsage(string appName, string commandName) 16 | { 17 | var arguments = Arguments.Union(ValidFlags) 18 | .Select(x => x.ToUsageDescription()) 19 | .Join(" "); 20 | 21 | return $"{appName} {commandName} {arguments}"; 22 | } 23 | 24 | public void WriteUsage(string appName, string commandName) 25 | { 26 | var arguments = Arguments.Union(ValidFlags) 27 | .Select(x => x.ToUsageDescription()) 28 | .Join(" "); 29 | 30 | AnsiConsole.MarkupLine($"[bold]{appName}[/] [bold]{commandName}[/] [cyan][{arguments}][/]"); 31 | } 32 | 33 | 34 | public bool IsValidUsage(IEnumerable handlers) 35 | { 36 | var actualArgs = handlers.OfType(); 37 | if (actualArgs.Count() != Arguments.Count()) 38 | { 39 | return false; 40 | } 41 | 42 | if (!Arguments.All(x => actualArgs.Contains(x))) 43 | { 44 | return false; 45 | } 46 | 47 | var flags = handlers.Where(x => !(x is Argument)); 48 | return flags.All(x => ValidFlags.Contains(x)); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Oakton/Help/HelpCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Spectre.Console; 4 | 5 | namespace Oakton.Help; 6 | 7 | [Description("List all the available commands", Name = "help")] 8 | public class HelpCommand : OaktonCommand 9 | { 10 | public HelpCommand() 11 | { 12 | Usage("List all the available commands").Arguments(x => x.Name); 13 | Usage("Show all the valid usages for a command"); 14 | } 15 | 16 | public override bool Execute(HelpInput input) 17 | { 18 | if (input.Usage != null) 19 | { 20 | input.Usage.WriteUsages(input.AppName); 21 | return false; 22 | } 23 | 24 | if (input.InvalidCommandName) 25 | { 26 | writeInvalidCommand(input.Name); 27 | listAllCommands(input); 28 | return false; 29 | } 30 | 31 | listAllCommands(input); 32 | return true; 33 | } 34 | 35 | private void listAllCommands(HelpInput input) 36 | { 37 | if (!input.CommandTypes.Any()) 38 | { 39 | Console.WriteLine("There are no known commands in this executable!"); 40 | return; 41 | } 42 | 43 | AnsiConsole.WriteLine(); 44 | AnsiConsole.MarkupLine("[bold]The available commands are:[/]"); 45 | 46 | var table = new Table 47 | { 48 | Border = TableBorder.SimpleHeavy 49 | }; 50 | 51 | table.AddColumns("Alias", "Description"); 52 | foreach (var type in input.CommandTypes.OrderBy(CommandFactory.CommandNameFor)) 53 | table.AddRow(CommandFactory.CommandNameFor(type), CommandFactory.DescriptionFor(type)); 54 | 55 | AnsiConsole.Write(table); 56 | AnsiConsole.WriteLine(); 57 | AnsiConsole.MarkupLine( 58 | "Use [italic]dotnet run -- ? [[command name]][/] or [italic]dotnet run -- help [[command name]][/] to see usage help about a specific command"); 59 | } 60 | 61 | private void writeInvalidCommand(string commandName) 62 | { 63 | AnsiConsole.WriteLine(); 64 | AnsiConsole.MarkupLine($"[red]'{commandName}' is not a command. See available commands.[/]"); 65 | AnsiConsole.WriteLine(); 66 | AnsiConsole.WriteLine(); 67 | } 68 | } -------------------------------------------------------------------------------- /src/Oakton/Help/HelpInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Oakton.Help; 5 | 6 | public class HelpInput 7 | { 8 | [IgnoreOnCommandLine] public IEnumerable CommandTypes { get; set; } 9 | 10 | [Description("A command name")] public string Name { get; set; } 11 | 12 | [IgnoreOnCommandLine] public bool InvalidCommandName { get; set; } 13 | 14 | [IgnoreOnCommandLine] public UsageGraph Usage { get; set; } 15 | 16 | [IgnoreOnCommandLine] public string AppName { get; set; } = "dotnet run --"; 17 | } -------------------------------------------------------------------------------- /src/Oakton/HostWrapperCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Oakton.Help; 7 | 8 | namespace Oakton; 9 | 10 | internal class HostWrapperCommand : IOaktonCommand 11 | { 12 | private readonly IOaktonCommand _inner; 13 | private readonly Func _hostSource; 14 | private readonly PropertyInfo[] _props; 15 | 16 | public HostWrapperCommand(IOaktonCommand inner, Func hostSource, PropertyInfo[] props) 17 | { 18 | _inner = inner; 19 | _hostSource = hostSource; 20 | _props = props; 21 | } 22 | 23 | public Type InputType => _inner.InputType; 24 | public UsageGraph Usages => _inner.Usages; 25 | public async Task Execute(object input) 26 | { 27 | var host = _hostSource(); 28 | try 29 | { 30 | await using var scope = host.Services.CreateAsyncScope(); 31 | foreach (var prop in _props) 32 | { 33 | var serviceType = prop.PropertyType; 34 | var service = scope.ServiceProvider.GetRequiredService(serviceType); 35 | prop.SetValue(_inner, service); 36 | } 37 | 38 | return await _inner.Execute(input); 39 | } 40 | finally 41 | { 42 | if (host is IAsyncDisposable ad) 43 | { 44 | await ad.DisposeAsync(); 45 | } 46 | else 47 | { 48 | host.Dispose(); 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Oakton/ICommandCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Service locator for command types. The default just uses Activator.CreateInstance(). 7 | /// Can be used to plug in IoC construction in Oakton applications 8 | /// 9 | public interface ICommandCreator 10 | { 11 | IOaktonCommand CreateCommand(Type commandType); 12 | object CreateModel(Type modelType); 13 | } -------------------------------------------------------------------------------- /src/Oakton/ICommandFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace Oakton; 6 | 7 | /// 8 | /// Interface that Oakton uses to build command runs during execution. Can be used for custom 9 | /// command activation 10 | /// 11 | public interface ICommandFactory 12 | { 13 | CommandRun BuildRun(string commandLine); 14 | CommandRun BuildRun(IEnumerable args); 15 | void RegisterCommands(Assembly assembly); 16 | 17 | IEnumerable BuildAllCommands(); 18 | 19 | void ApplyExtensions(IHostBuilder builder); 20 | } -------------------------------------------------------------------------------- /src/Oakton/IHostBuilderInput.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Interface used to get access to the HostBuilder from command inputs. 7 | /// 8 | public interface IHostBuilderInput 9 | { 10 | [IgnoreOnCommandLine] IHostBuilder HostBuilder { get; set; } 11 | } -------------------------------------------------------------------------------- /src/Oakton/IOaktonCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Oakton.Help; 4 | 5 | namespace Oakton; 6 | 7 | public interface IOaktonCommand 8 | { 9 | Type InputType { get; } 10 | UsageGraph Usages { get; } 11 | Task Execute(object input); 12 | } 13 | 14 | public interface IOaktonCommand : IOaktonCommand 15 | { 16 | } -------------------------------------------------------------------------------- /src/Oakton/IServiceRegistrations.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Implementations of this interface can be used to define 7 | /// service registrations to be loaded by Oakton command extensions 8 | /// 9 | public interface IServiceRegistrations 10 | { 11 | void Configure(IServiceCollection services); 12 | } -------------------------------------------------------------------------------- /src/Oakton/IgnoreOnCommandLineAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Oakton ignores any fields or properties with this attribute during the binding to the input 7 | /// objects 8 | /// 9 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 10 | public class IgnoreOnCommandLineAttribute : Attribute 11 | { 12 | } -------------------------------------------------------------------------------- /src/Oakton/InjectServiceAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | /// 6 | /// Decorate Oakton commands that are being called by 7 | /// 8 | [AttributeUsage(AttributeTargets.Property)] 9 | public class InjectServiceAttribute : Attribute 10 | { 11 | 12 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/ArgsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Oakton.Internal; 4 | 5 | public static class ArgsExtensions 6 | { 7 | public static string[] FilterLauncherArgs(this string[] args) 8 | { 9 | if (args == null) 10 | { 11 | return new string[0]; 12 | } 13 | 14 | while (args.Any() && args[0].StartsWith("%") && args[0].EndsWith("%")) 15 | { 16 | args = args.Skip(1).ToArray(); 17 | } 18 | 19 | return args; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/ArrayConversion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JasperFx.Core; 3 | 4 | namespace Oakton.Internal.Conversion; 5 | 6 | public class ArrayConversion : IConversionProvider 7 | { 8 | private readonly Conversions _conversions; 9 | 10 | public ArrayConversion(Conversions conversions) 11 | { 12 | _conversions = conversions; 13 | } 14 | 15 | public Func? ConverterFor(Type type) 16 | { 17 | if (!type.IsArray) 18 | { 19 | return null; 20 | } 21 | 22 | var innerType = type.GetElementType()!; 23 | var inner = _conversions.FindConverter(innerType); 24 | 25 | return stringValue => 26 | { 27 | if (stringValue.ToUpper() == "EMPTY" || stringValue.Trim().IsEmpty()) 28 | { 29 | return Array.CreateInstance(innerType, 0); 30 | } 31 | 32 | var strings = stringValue.ToDelimitedArray(); 33 | var array = Array.CreateInstance(innerType, strings.Length); 34 | 35 | for (var i = 0; i < strings.Length; i++) 36 | { 37 | var value = inner?.Invoke(strings[i]); 38 | array.SetValue(value, i); 39 | } 40 | 41 | return array; 42 | }; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/Conversions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using JasperFx.Core; 5 | 6 | namespace Oakton.Internal.Conversion; 7 | 8 | public class Conversions 9 | { 10 | private readonly LightweightCache> _convertors; 11 | private readonly IList _providers = new List(); 12 | 13 | 14 | public Conversions() 15 | { 16 | _convertors = 17 | new LightweightCache?>( 18 | type => { return providers().Select(x => x.ConverterFor(type)).FirstOrDefault(x => x != null); }); 19 | 20 | RegisterConversion(bool.Parse); 21 | RegisterConversion(byte.Parse); 22 | RegisterConversion(sbyte.Parse); 23 | RegisterConversion(char.Parse); 24 | RegisterConversion(decimal.Parse); 25 | RegisterConversion(double.Parse); 26 | RegisterConversion(float.Parse); 27 | RegisterConversion(short.Parse); 28 | RegisterConversion(int.Parse); 29 | RegisterConversion(long.Parse); 30 | RegisterConversion(ushort.Parse); 31 | RegisterConversion(uint.Parse); 32 | RegisterConversion(ulong.Parse); 33 | RegisterConversion(DateTimeConverter.GetDateTime); 34 | RegisterConversion(Guid.Parse); 35 | 36 | RegisterConversion(x => 37 | { 38 | if (x == "EMPTY") 39 | { 40 | return string.Empty; 41 | } 42 | 43 | return x; 44 | }); 45 | } 46 | 47 | 48 | private IEnumerable providers() 49 | { 50 | foreach (var provider in _providers) yield return provider; 51 | 52 | yield return new EnumerationConversion(); 53 | yield return new NullableConvertor(this); 54 | yield return new ArrayConversion(this); 55 | yield return new StringConverterProvider(); 56 | } 57 | 58 | public void RegisterConversionProvider() where T : IConversionProvider, new() 59 | { 60 | _providers.Add(new T()); 61 | } 62 | 63 | public void RegisterConversion(Func convertor) 64 | { 65 | _convertors[typeof(T)] = x => convertor(x); 66 | } 67 | 68 | public Func? FindConverter(Type type) 69 | { 70 | return _convertors[type]; 71 | } 72 | 73 | public object? Convert(Type type, string raw) 74 | { 75 | return _convertors[type]?.Invoke(raw); 76 | } 77 | 78 | public bool Has(Type type) 79 | { 80 | return _convertors.Contains(type) || providers().Any(x => x.ConverterFor(type) != null); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/DateTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Oakton.Internal.Conversion; 7 | 8 | public class DateTimeConverter 9 | { 10 | public const string TODAY = "TODAY"; 11 | 12 | private static readonly Regex iso8601Expression = new( 13 | @"^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$"); 14 | 15 | public static DateTime GetDateTime(string dateString) 16 | { 17 | var trimmedString = dateString.Trim(); 18 | if (trimmedString == TODAY) 19 | { 20 | return DateTime.Today; 21 | } 22 | 23 | if (trimmedString.Contains(TODAY)) 24 | { 25 | var dayString = trimmedString.Substring(5, trimmedString.Length - 5); 26 | var days = int.Parse(dayString); 27 | 28 | return DateTime.Today.AddDays(days); 29 | } 30 | 31 | if (isDayOfWeek(dateString)) 32 | { 33 | return convertToDateFromDayAndTime(dateString); 34 | } 35 | 36 | if (iso8601Expression.IsMatch(trimmedString)) 37 | { 38 | //Thank you jon skeet : http://stackoverflow.com/questions/10029099/datetime-parse2012-09-30t230000-0000000z-always-converts-to-datetimekind-l 39 | DateTime result; 40 | var success = DateTime.TryParseExact(trimmedString, "yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", 41 | CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, 42 | out result); 43 | 44 | if (success) 45 | { 46 | return result; 47 | } 48 | } 49 | 50 | return DateTime.Parse(trimmedString); 51 | } 52 | 53 | private static DateTime convertToDateFromDayAndTime(string dateString) 54 | { 55 | dateString = dateString.Replace(" ", " "); 56 | var parts = dateString.Split(' '); 57 | var day = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), parts[0], true); 58 | var minutes = minutesFrom24HourTime(parts[1]); 59 | 60 | var date = DateTime.Today.AddMinutes(minutes); 61 | while (date.DayOfWeek != day) 62 | { 63 | date = date.AddDays(1); 64 | } 65 | 66 | return date; 67 | } 68 | 69 | private static bool isDayOfWeek(string text) 70 | { 71 | var days = Enum.GetNames(typeof(DayOfWeek)); 72 | return days.FirstOrDefault(x => text.ToLower().StartsWith(x.ToLower())) != null; 73 | } 74 | 75 | 76 | private static int minutesFrom24HourTime(string time) 77 | { 78 | var parts = time.Split(':'); 79 | return 60 * int.Parse(parts[0]) + int.Parse(parts[1]); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/EnumerationConversion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Oakton.Internal.Conversion; 5 | 6 | // SAMPLE: EnumerationConversion 7 | public class EnumerationConversion : IConversionProvider 8 | { 9 | public Func? ConverterFor(Type type) 10 | { 11 | if (type.GetTypeInfo().IsEnum) 12 | { 13 | return x => Enum.Parse(type, x); 14 | } 15 | 16 | return null; 17 | } 18 | } 19 | // ENDSAMPLE -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/IConversionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton.Internal.Conversion; 4 | 5 | // SAMPLE: IConversionProvider 6 | public interface IConversionProvider 7 | { 8 | // Given the type argument, either return a 9 | // Func that can parse a string into that Type 10 | // or return null to let another IConversionProvider 11 | // handle this type 12 | Func? ConverterFor(Type type); 13 | } 14 | // ENDSAMPLE -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/NullableConvertor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using JasperFx.Core.Reflection; 4 | 5 | namespace Oakton.Internal.Conversion; 6 | 7 | public class NullableConvertor : IConversionProvider 8 | { 9 | private readonly Conversions _conversions; 10 | 11 | public NullableConvertor(Conversions conversions) 12 | { 13 | _conversions = conversions; 14 | } 15 | 16 | public Func? ConverterFor(Type type) 17 | { 18 | if (!type.IsNullable()) 19 | { 20 | return null; 21 | } 22 | 23 | 24 | var innerType = type.GetGenericArguments().First(); 25 | var inner = _conversions.FindConverter(innerType); 26 | 27 | return str => str == "NULL" ? null : inner?.Invoke(str); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/StringConverterProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using JasperFx.Core.Reflection; 4 | 5 | namespace Oakton.Internal.Conversion; 6 | 7 | public class StringConverterProvider : IConversionProvider 8 | { 9 | public Func? ConverterFor(Type type) 10 | { 11 | if (!type.IsConcrete()) 12 | { 13 | return null; 14 | } 15 | 16 | var constructor = type.GetConstructor(new[] { typeof(string) }); 17 | if (constructor == null) 18 | { 19 | return null; 20 | } 21 | 22 | var param = Expression.Parameter(typeof(string), "arg"); 23 | var body = Expression.New(constructor, param); 24 | 25 | return Expression.Lambda>(body, param).Compile(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Oakton/Internal/Conversion/TimeSpanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Oakton.Internal.Conversion; 6 | 7 | public class TimeSpanConverter 8 | { 9 | private const string TimespanPattern = 10 | @" 11 | ^(?\d+ # quantity is expressed as some digits 12 | (\.\d+)?) # optionally followed by a decimal point or colon and more digits 13 | \s* # optional whitespace 14 | (?[a-z]*) # units is expressed as a word 15 | $ # match the entire string"; 16 | 17 | 18 | public static TimeSpan GetTimeSpan(string timeString) 19 | { 20 | var match = Regex.Match(timeString.Trim(), TimespanPattern, RegexOptions.IgnorePatternWhitespace); 21 | if (!match.Success) 22 | { 23 | return TimeSpan.Parse(timeString); 24 | } 25 | 26 | 27 | var number = double.Parse(match.Groups["quantity"].Value); 28 | var units = match.Groups["units"].Value.ToLower(); 29 | switch (units) 30 | { 31 | case "s": 32 | case "second": 33 | case "seconds": 34 | return TimeSpan.FromSeconds(number); 35 | 36 | case "m": 37 | case "minute": 38 | case "minutes": 39 | return TimeSpan.FromMinutes(number); 40 | 41 | case "h": 42 | case "hour": 43 | case "hours": 44 | return TimeSpan.FromHours(number); 45 | 46 | case "d": 47 | case "day": 48 | case "days": 49 | return TimeSpan.FromDays(number); 50 | } 51 | 52 | if (timeString.Length == 4 && !timeString.Contains(":")) 53 | { 54 | var hours = int.Parse(timeString.Substring(0, 2)); 55 | var minutes = int.Parse(timeString.Substring(2, 2)); 56 | 57 | return new TimeSpan(hours, minutes, 0); 58 | } 59 | 60 | if (timeString.Length == 5 && timeString.Contains(":")) 61 | { 62 | var parts = timeString.Split(':'); 63 | var hours = int.Parse(parts.ElementAt(0)); 64 | var minutes = int.Parse(parts.ElementAt(1)); 65 | 66 | return new TimeSpan(hours, minutes, 0); 67 | } 68 | 69 | throw new Exception("Time periods must be expressed in seconds, minutes, hours, or days."); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Oakton/InvalidUsageException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | public class InvalidUsageException : Exception 6 | { 7 | public InvalidUsageException() : base(string.Empty) 8 | { 9 | } 10 | 11 | public InvalidUsageException(string message) : base(message) 12 | { 13 | } 14 | 15 | public InvalidUsageException(string message, Exception innerException) : base(message, innerException) 16 | { 17 | } 18 | } -------------------------------------------------------------------------------- /src/Oakton/Oakton.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Command Line Parsing and Execution 4 | Oakton 5 | 6.3.0 6 | Jeremy D. Miller 7 | net6.0;net7.0;net8.0;net9.0 8 | portable 9 | Oakton 10 | 11.0 11 | Library 12 | Oakton 13 | Command Line Parsing 14 | https://jasperfx.github.io/oakton 15 | MIT 16 | git 17 | git://github.com/jasperfx/oakton 18 | https://raw.githubusercontent.com/JasperFx/JasperFx.Core/main/jasperfx-logo.jpg?raw=true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Oakton/OaktonAsyncCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Oakton.Help; 4 | 5 | namespace Oakton; 6 | 7 | /// 8 | /// Base class for all Oakton commands 9 | /// 10 | /// 11 | public abstract class OaktonAsyncCommand : IOaktonCommand 12 | { 13 | protected OaktonAsyncCommand() 14 | { 15 | Usages = new UsageGraph(GetType()); 16 | } 17 | 18 | public UsageGraph Usages { get; } 19 | 20 | public Type InputType => typeof(T); 21 | 22 | Task IOaktonCommand.Execute(object input) 23 | { 24 | return Execute((T)input); 25 | } 26 | 27 | /// 28 | /// If your command has multiple argument usage patterns ala the Git command line, use 29 | /// this method to define the valid combinations of arguments and optionally limit the flags that are valid 30 | /// for each usage 31 | /// 32 | /// The description of this usage to be displayed from the CLI help command 33 | /// 34 | public UsageGraph.UsageExpression Usage(string description) 35 | { 36 | return Usages.AddUsage(description); 37 | } 38 | 39 | /// 40 | /// The actual execution of the command. Return "false" to denote failures 41 | /// or "true" for successes 42 | /// 43 | /// 44 | /// 45 | public abstract Task Execute(T input); 46 | } -------------------------------------------------------------------------------- /src/Oakton/OaktonCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Oakton.Help; 4 | 5 | namespace Oakton; 6 | 7 | /// 8 | /// Base class for all Oakton commands 9 | /// 10 | /// 11 | public abstract class OaktonCommand : IOaktonCommand 12 | { 13 | protected OaktonCommand() 14 | { 15 | Usages = new UsageGraph(GetType()); 16 | } 17 | 18 | public UsageGraph Usages { get; } 19 | 20 | public Type InputType => typeof(T); 21 | 22 | Task IOaktonCommand.Execute(object input) 23 | { 24 | return Task.FromResult(Execute((T)input)); 25 | } 26 | 27 | /// 28 | /// If your command has multiple argument usage patterns ala the Git command line, use 29 | /// this method to define the valid combinations of arguments and optionally limit the flags that are valid 30 | /// for each usage 31 | /// 32 | /// The description of this usage to be displayed from the CLI help command 33 | /// 34 | public UsageGraph.UsageExpression Usage(string description) 35 | { 36 | return Usages.AddUsage(description); 37 | } 38 | 39 | /// 40 | /// The actual execution of the command. Return "false" to denote failures 41 | /// or "true" for successes 42 | /// 43 | /// 44 | /// 45 | public abstract bool Execute(T input); 46 | } -------------------------------------------------------------------------------- /src/Oakton/OaktonCommandAssemblyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JasperFx.Core.Reflection; 3 | 4 | namespace Oakton; 5 | 6 | /// 7 | /// If the CommandExecutor is configured to discover assemblies, 8 | /// this attribute on an assembly will cause Oakton to search for 9 | /// command types within this assembly 10 | /// 11 | [AttributeUsage(AttributeTargets.Assembly)] 12 | public class OaktonCommandAssemblyAttribute : Attribute 13 | { 14 | public OaktonCommandAssemblyAttribute() 15 | { 16 | } 17 | 18 | /// 19 | /// Concrete type implementing the IServiceRegistrations interface that should 20 | /// automatically be applied to hosts during environment checks or resource 21 | /// commands 22 | /// 23 | /// 24 | public OaktonCommandAssemblyAttribute(Type extensionType) 25 | { 26 | if (extensionType.HasDefaultConstructor() && extensionType.CanBeCastTo()) 27 | { 28 | ExtensionType = extensionType; 29 | } 30 | else 31 | { 32 | throw new ArgumentOutOfRangeException(nameof(extensionType), 33 | $"Extension types must have a default, no arg constructor and implement the {nameof(IServiceRegistrations)} interface"); 34 | } 35 | } 36 | 37 | public Type ExtensionType { get; set; } 38 | } -------------------------------------------------------------------------------- /src/Oakton/OaktonEnvironment.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton; 2 | 3 | public static class OaktonEnvironment 4 | { 5 | /// 6 | /// If using Oakton as the run command in .Net Core applications with WebApplication, 7 | /// this will force Oakton to automatically start up the IHost when the Program.Main() 8 | /// method runs. Very useful for WebApplicationFactory testing 9 | /// 10 | public static bool AutoStartHost { get; set; } 11 | } -------------------------------------------------------------------------------- /src/Oakton/OaktonOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton; 4 | 5 | public class OaktonOptions 6 | { 7 | public string OptionsFile { get; set; } 8 | public Action Factory { get; set; } 9 | public string DefaultCommand { get; set; } = "run"; 10 | } 11 | -------------------------------------------------------------------------------- /src/Oakton/Parsing/ArgPreprocessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Parsing; 4 | 5 | public class ArgPreprocessor 6 | { 7 | public static IEnumerable Process(IEnumerable incomingArgs) 8 | { 9 | var newArgs = new List(); 10 | 11 | foreach (var arg in incomingArgs) 12 | { 13 | if (isMultiArg(arg)) 14 | { 15 | foreach (var c in arg.TrimStart('-')) newArgs.Add("-" + c); 16 | } 17 | else 18 | { 19 | newArgs.Add(arg); 20 | } 21 | } 22 | 23 | return newArgs; 24 | } 25 | 26 | private static bool isMultiArg(string arg) 27 | { 28 | // Getting around GH-24 29 | if (decimal.TryParse(arg, out var number)) 30 | { 31 | return false; 32 | } 33 | 34 | // regular short args look like '-a', multi-args are '-abc' which is really '-a -b -c' 35 | return InputParser.IsShortFlag(arg) && arg.Length > 2; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/BooleanFlag.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace Oakton.Parsing; 5 | 6 | public class BooleanFlag : TokenHandlerBase 7 | { 8 | private readonly MemberInfo _member; 9 | 10 | public BooleanFlag(MemberInfo member) : base(member) 11 | { 12 | _member = member; 13 | } 14 | 15 | public override bool Handle(object input, Queue tokens) 16 | { 17 | if (!tokens.NextIsFlagFor(_member)) 18 | { 19 | return false; 20 | } 21 | 22 | tokens.Dequeue(); 23 | setValue(input, true); 24 | 25 | return true; 26 | } 27 | 28 | public override string ToUsageDescription() 29 | { 30 | return $"[{InputParser.ToFlagAliases(_member)}]"; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/DictionaryFlag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using JasperFx.Core.Reflection; 6 | 7 | namespace Oakton.Parsing; 8 | 9 | public class DictionaryFlag : TokenHandlerBase 10 | { 11 | private readonly string _prefix; 12 | 13 | public DictionaryFlag(MemberInfo member) : base(member) 14 | { 15 | if (!member.GetMemberType().CanBeCastTo>()) 16 | { 17 | throw new ArgumentOutOfRangeException("Dictionary flag types have to be IDictionary"); 18 | } 19 | 20 | var flagAliases = InputParser.ToFlagAliases(Member); 21 | 22 | _prefix = flagAliases.LongForm + ":"; 23 | } 24 | 25 | 26 | public override bool Handle(object input, Queue tokens) 27 | { 28 | if (tokens.Peek().StartsWith(_prefix)) 29 | { 30 | var flag = tokens.Dequeue(); 31 | 32 | if (tokens.Count == 0) 33 | { 34 | throw new InvalidUsageException($"No value specified for flag {flag}."); 35 | } 36 | 37 | var key = flag.Split(':').Last().Trim(); 38 | var rawValue = tokens.Dequeue(); 39 | 40 | var dict = getValue(input) as IDictionary; 41 | if (dict == null) 42 | { 43 | dict = new Dictionary(); 44 | setValue(input, dict); 45 | } 46 | 47 | if (dict.ContainsKey(key)) 48 | { 49 | dict[key] = rawValue; 50 | } 51 | else 52 | { 53 | dict.Add(key, rawValue); 54 | } 55 | 56 | return true; 57 | } 58 | 59 | return false; 60 | } 61 | 62 | public override string ToUsageDescription() 63 | { 64 | return $"[{_prefix} ]"; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/EnumerableArgument.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using JasperFx.Core.Reflection; 6 | using Oakton.Internal.Conversion; 7 | 8 | namespace Oakton.Parsing; 9 | 10 | public class EnumerableArgument : Argument 11 | { 12 | private readonly MemberInfo _member; 13 | 14 | public EnumerableArgument(MemberInfo member, Conversions conversions) : base(member, conversions) 15 | { 16 | _member = member; 17 | 18 | _converter = conversions.FindConverter(member.GetMemberType().DetermineElementType()); 19 | } 20 | 21 | public override bool Handle(object input, Queue tokens) 22 | { 23 | var elementType = _member.GetMemberType().GetGenericArguments().First(); 24 | var list = typeof(List<>).CloseAndBuildAs(elementType); 25 | 26 | var wasHandled = false; 27 | while (tokens.Count > 0 && !tokens.NextIsFlag()) 28 | { 29 | var value = _converter(tokens.Dequeue()); 30 | list.Add(value); 31 | 32 | wasHandled = true; 33 | } 34 | 35 | if (wasHandled) 36 | { 37 | setValue(input, list); 38 | } 39 | 40 | return wasHandled; 41 | } 42 | 43 | public override string ToUsageDescription() 44 | { 45 | var name = _member.Name.ToLower(); 46 | return $"<{name}1 {name}2 {name}3 ...>"; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/EnumerableFlag.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using JasperFx.Core.Reflection; 5 | using Oakton.Internal.Conversion; 6 | 7 | namespace Oakton.Parsing; 8 | 9 | public class EnumerableFlag : Flag 10 | { 11 | private readonly MemberInfo _member; 12 | 13 | public EnumerableFlag(MemberInfo member, Conversions conversions) 14 | : base(member, member.GetMemberType().DetermineElementType(), conversions) 15 | { 16 | _member = member; 17 | } 18 | 19 | public override bool Handle(object input, Queue tokens) 20 | { 21 | var elementType = _member.GetMemberType().DetermineElementType(); 22 | var list = typeof(List<>).CloseAndBuildAs(elementType); 23 | 24 | var wasHandled = false; 25 | 26 | if (tokens.NextIsFlagFor(_member)) 27 | { 28 | var flag = tokens.Dequeue(); 29 | while (tokens.Count > 0 && !tokens.NextIsFlag()) 30 | { 31 | var value = Converter(tokens.Dequeue()); 32 | list.Add(value); 33 | 34 | wasHandled = true; 35 | } 36 | 37 | if (!wasHandled) 38 | { 39 | throw new InvalidUsageException($"No values specified for flag {flag}."); 40 | } 41 | 42 | setValue(input, list); 43 | } 44 | 45 | return wasHandled; 46 | } 47 | 48 | public override string ToUsageDescription() 49 | { 50 | var flagAliases = InputParser.ToFlagAliases(_member); 51 | 52 | var name = InputParser.RemoveFlagSuffix(_member.Name).ToLower(); 53 | return $"[{flagAliases} <{name}1 {name}2 {name}3 ...>]"; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/Flag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using JasperFx.Core; 5 | using JasperFx.Core.Reflection; 6 | using Oakton.Internal.Conversion; 7 | 8 | namespace Oakton.Parsing; 9 | 10 | public class Flag : TokenHandlerBase 11 | { 12 | private readonly MemberInfo _member; 13 | protected Func Converter; 14 | 15 | public Flag(MemberInfo member, Conversions conversions) : this(member, member.GetMemberType(), conversions) 16 | { 17 | } 18 | 19 | public Flag(MemberInfo member, Type propertyType, Conversions conversions) : base(member) 20 | { 21 | _member = member; 22 | Converter = conversions.FindConverter(propertyType); 23 | 24 | if (Converter == null) 25 | { 26 | throw new ArgumentOutOfRangeException( 27 | $"Cannot derive a conversion for type {member.GetMemberType()} on property {member.Name}"); 28 | } 29 | } 30 | 31 | public override bool Handle(object input, Queue tokens) 32 | { 33 | if (tokens.NextIsFlagFor(_member)) 34 | { 35 | var flag = tokens.Dequeue(); 36 | 37 | if (tokens.Count == 0) 38 | { 39 | throw new InvalidUsageException($"No value specified for flag {flag}."); 40 | } 41 | 42 | var rawValue = tokens.Dequeue(); 43 | var value = Converter(rawValue); 44 | 45 | setValue(input, value); 46 | 47 | return true; 48 | } 49 | 50 | 51 | return false; 52 | } 53 | 54 | public override string ToUsageDescription() 55 | { 56 | var flagAliases = InputParser.ToFlagAliases(_member); 57 | 58 | if (_member.GetMemberType().GetTypeInfo().IsEnum) 59 | { 60 | var enumValues = Enum.GetNames(_member.GetMemberType()).Join("|"); 61 | return $"[{flagAliases} {enumValues}]"; 62 | } 63 | 64 | var name = InputParser.RemoveFlagSuffix(_member.Name).ToLower(); 65 | return $"[{flagAliases} <{name}>]"; 66 | } 67 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/FlagAliases.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton.Parsing; 2 | 3 | public class FlagAliases 4 | { 5 | public string LongForm { get; set; } 6 | public string ShortForm { get; set; } 7 | 8 | public bool LongFormOnly { get; set; } 9 | 10 | public bool Matches(string token) 11 | { 12 | if (!LongFormOnly && InputParser.IsShortFlag(token)) 13 | { 14 | return token == ShortForm; 15 | } 16 | 17 | var lowerToken = token.ToLower(); 18 | 19 | return lowerToken == LongForm.ToLower(); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return LongFormOnly ? $"{LongForm}" : $"{ShortForm}, {LongForm}"; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/ITokenHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Parsing; 4 | 5 | public interface ITokenHandler 6 | { 7 | string Description { get; } 8 | string MemberName { get; } 9 | bool Handle(object input, Queue tokens); 10 | 11 | string ToUsageDescription(); 12 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/OptionReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using JasperFx.Core; 4 | 5 | namespace Oakton.Parsing; 6 | 7 | public static class OptionReader 8 | { 9 | public static string Read(string file) 10 | { 11 | return File.ReadAllLines(file) 12 | .Select(x => x.Trim()) 13 | .Where(x => x.IsNotEmpty()) 14 | .Join(" "); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/QueueExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace Oakton.Parsing; 5 | 6 | public static class QueueExtensions 7 | { 8 | public static bool NextIsFlag(this Queue queue) 9 | { 10 | return InputParser.IsFlag(queue.Peek()); 11 | } 12 | 13 | public static bool NextIsFlagFor(this Queue queue, MemberInfo property) 14 | { 15 | return InputParser.IsFlagFor(queue.Peek(), property); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/StringTokenizer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Parsing; 4 | 5 | public static class StringTokenizer 6 | { 7 | public static IEnumerable Tokenize(string content) 8 | { 9 | var searchString = content.Trim(); 10 | if (searchString.Length == 0) 11 | { 12 | return new string[0]; 13 | } 14 | 15 | var parser = new TokenParser(); 16 | 17 | foreach (var c in content.ToCharArray()) parser.Read(c); 18 | 19 | // Gotta force the parser to know it's done 20 | parser.Read('\n'); 21 | 22 | return parser.Tokens; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/TokenHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using JasperFx.Core.Reflection; 4 | 5 | namespace Oakton.Parsing; 6 | 7 | public abstract class TokenHandlerBase : ITokenHandler 8 | { 9 | protected TokenHandlerBase(MemberInfo member) 10 | { 11 | Member = member; 12 | } 13 | 14 | public MemberInfo Member { get; } 15 | 16 | public string Description 17 | { 18 | get 19 | { 20 | var name = Member.Name; 21 | Member.ForAttribute(att => name = att.Description); 22 | 23 | return name; 24 | } 25 | } 26 | 27 | public string MemberName => Member.Name; 28 | 29 | public abstract bool Handle(object input, Queue tokens); 30 | public abstract string ToUsageDescription(); 31 | 32 | protected void setValue(object target, object value) 33 | { 34 | (Member as PropertyInfo)?.SetValue(target, value); 35 | (Member as FieldInfo)?.SetValue(target, value); 36 | } 37 | 38 | protected object getValue(object target) 39 | { 40 | return (Member as PropertyInfo)?.GetValue(target) 41 | ?? (Member as FieldInfo)?.GetValue(target); 42 | } 43 | 44 | public bool Equals(TokenHandlerBase other) 45 | { 46 | if (ReferenceEquals(null, other)) 47 | { 48 | return false; 49 | } 50 | 51 | if (ReferenceEquals(this, other)) 52 | { 53 | return true; 54 | } 55 | 56 | return other.Member.Equals(Member); 57 | } 58 | 59 | public override bool Equals(object obj) 60 | { 61 | if (ReferenceEquals(null, obj)) 62 | { 63 | return false; 64 | } 65 | 66 | if (ReferenceEquals(this, obj)) 67 | { 68 | return true; 69 | } 70 | 71 | if (obj.GetType() != typeof(TokenHandlerBase)) 72 | { 73 | return false; 74 | } 75 | 76 | return Equals((TokenHandlerBase)obj); 77 | } 78 | 79 | public override int GetHashCode() 80 | { 81 | return Member != null ? Member.GetHashCode() : 0; 82 | } 83 | } -------------------------------------------------------------------------------- /src/Oakton/Parsing/TokenParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Parsing; 4 | 5 | public class TokenParser 6 | { 7 | private readonly List _tokens = new(); 8 | private List _characters; 9 | private IMode _mode; 10 | 11 | public TokenParser() 12 | { 13 | _mode = new Searching(this); 14 | } 15 | 16 | public IEnumerable Tokens => _tokens; 17 | 18 | public void Read(char c) 19 | { 20 | _mode.Read(c); 21 | } 22 | 23 | private void addChar(char c) 24 | { 25 | _characters.Add(c); 26 | } 27 | 28 | private void startToken(IMode mode) 29 | { 30 | _mode = mode; 31 | _characters = new List(); 32 | } 33 | 34 | private void endToken() 35 | { 36 | var @string = new string(_characters.ToArray()); 37 | _tokens.Add(@string); 38 | 39 | _mode = new Searching(this); 40 | } 41 | 42 | 43 | public interface IMode 44 | { 45 | void Read(char c); 46 | } 47 | 48 | public class Searching : IMode 49 | { 50 | private readonly TokenParser _parent; 51 | 52 | public Searching(TokenParser parent) 53 | { 54 | _parent = parent; 55 | } 56 | 57 | public void Read(char c) 58 | { 59 | if (char.IsWhiteSpace(c)) 60 | { 61 | return; 62 | } 63 | 64 | if (c == '"') 65 | { 66 | _parent.startToken(new InsideQuotedToken(_parent)); 67 | } 68 | else 69 | { 70 | var normalToken = new InsideNormalToken(_parent); 71 | _parent.startToken(normalToken); 72 | normalToken.Read(c); 73 | } 74 | } 75 | } 76 | 77 | public class InsideQuotedToken : IMode 78 | { 79 | private readonly TokenParser _parent; 80 | 81 | public InsideQuotedToken(TokenParser parent) 82 | { 83 | _parent = parent; 84 | } 85 | 86 | 87 | public void Read(char c) 88 | { 89 | if (c == '"') 90 | { 91 | _parent.endToken(); 92 | } 93 | else 94 | { 95 | _parent.addChar(c); 96 | } 97 | } 98 | } 99 | 100 | public class InsideNormalToken : IMode 101 | { 102 | private readonly TokenParser _parent; 103 | 104 | public InsideNormalToken(TokenParser parent) 105 | { 106 | _parent = parent; 107 | } 108 | 109 | public void Read(char c) 110 | { 111 | if (char.IsWhiteSpace(c)) 112 | { 113 | _parent.endToken(); 114 | } 115 | else 116 | { 117 | _parent.addChar(c); 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Oakton/PreBuiltHostBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Spectre.Console; 7 | 8 | namespace Oakton; 9 | 10 | internal class PreBuiltHostBuilder : IHostBuilder 11 | { 12 | private readonly string _notSupportedMessage; 13 | 14 | public PreBuiltHostBuilder(IHost host) 15 | { 16 | Host = host; 17 | _notSupportedMessage = 18 | $"The IHost ({Host}) is already constructed. See https://jasperfx.github.io/oakton for alternative bootstrapping to enable this feature."; 19 | } 20 | 21 | public IHost Host { get; } 22 | 23 | public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) 24 | { 25 | AnsiConsole.MarkupLine("[yellow]Cannot override host configuration when the IHost is already constructed[/]"); 26 | return this; 27 | } 28 | 29 | public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) 30 | { 31 | AnsiConsole.MarkupLine("[yellow]Cannot override app configuration when the IHost is already constructed[/]"); 32 | return this; 33 | } 34 | 35 | public IHostBuilder ConfigureServices(Action configureDelegate) 36 | { 37 | AnsiConsole.MarkupLine("[yellow]Cannot override services when the IHost is already constructed[/]"); 38 | return this; 39 | } 40 | 41 | public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) 42 | { 43 | throw new NotSupportedException(_notSupportedMessage); 44 | } 45 | 46 | public IHostBuilder UseServiceProviderFactory( 47 | Func> factory) 48 | { 49 | throw new NotSupportedException(_notSupportedMessage); 50 | } 51 | 52 | public IHostBuilder ConfigureContainer( 53 | Action configureDelegate) 54 | { 55 | throw new NotSupportedException(_notSupportedMessage); 56 | } 57 | 58 | public IHost Build() 59 | { 60 | return Host; 61 | } 62 | 63 | public IDictionary Properties { get; } = new Dictionary(); 64 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/IStatefulResource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Spectre.Console.Rendering; 5 | 6 | namespace Oakton.Resources; 7 | 8 | #region sample_IStatefulResourceWithDependencies 9 | 10 | /// 11 | /// Use to create dependencies between 12 | /// 13 | public interface IStatefulResourceWithDependencies : IStatefulResource 14 | { 15 | // Given all the known stateful resources in your system -- including the current resource! 16 | // tell Oakton which resources are dependencies of this resource that should be setup first 17 | IEnumerable FindDependencies(IReadOnlyList others); 18 | } 19 | 20 | #endregion 21 | 22 | #region sample_IStatefulResource 23 | 24 | /// 25 | /// Adapter interface used by Oakton enabled applications to allow 26 | /// Oakton to setup/teardown/clear the state/check on stateful external 27 | /// resources of the system like databases or messaging queues 28 | /// 29 | public interface IStatefulResource 30 | { 31 | /// 32 | /// Categorical type name of this resource for filtering 33 | /// 34 | string Type { get; } 35 | 36 | /// 37 | /// Identifier for this resource 38 | /// 39 | string Name { get; } 40 | 41 | /// 42 | /// Check whether the configuration for this resource is valid. An exception 43 | /// should be thrown if the check is invalid 44 | /// 45 | /// 46 | /// 47 | Task Check(CancellationToken token); 48 | 49 | /// 50 | /// Clear any persisted state within this resource 51 | /// 52 | /// 53 | /// 54 | Task ClearState(CancellationToken token); 55 | 56 | /// 57 | /// Tear down the stateful resource represented by this implementation 58 | /// 59 | /// 60 | /// 61 | Task Teardown(CancellationToken token); 62 | 63 | /// 64 | /// Make any necessary configuration to this stateful resource 65 | /// to make the system function correctly 66 | /// 67 | /// 68 | /// 69 | Task Setup(CancellationToken token); 70 | 71 | /// 72 | /// Optionally return a report of the current state of this resource 73 | /// 74 | /// 75 | /// 76 | Task DetermineStatus(CancellationToken token); 77 | } 78 | 79 | #endregion -------------------------------------------------------------------------------- /src/Oakton/Resources/IStatefulResourceSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Oakton.Resources; 4 | 5 | #region sample_IStatefulResourceSource 6 | 7 | /// 8 | /// Expose multiple stateful resources 9 | /// 10 | public interface IStatefulResourceSource 11 | { 12 | IReadOnlyList FindResources(); 13 | } 14 | 15 | #endregion -------------------------------------------------------------------------------- /src/Oakton/Resources/ResourceAction.cs: -------------------------------------------------------------------------------- 1 | namespace Oakton.Resources; 2 | 3 | public enum ResourceAction 4 | { 5 | clear, 6 | teardown, 7 | setup, 8 | statistics, 9 | check, 10 | list 11 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/ResourceEnvironmentCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Oakton.Environment; 5 | 6 | namespace Oakton.Resources; 7 | 8 | internal class ResourceEnvironmentCheck : IEnvironmentCheck 9 | { 10 | private readonly IStatefulResource _resource; 11 | 12 | public ResourceEnvironmentCheck(IStatefulResource resource) 13 | { 14 | _resource = resource; 15 | } 16 | 17 | public string Description => $"Resource {_resource.Name} ({_resource.Type})"; 18 | 19 | public Task Assert(IServiceProvider services, CancellationToken cancellation) 20 | { 21 | return _resource.Check(cancellation); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/ResourceInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Oakton.Resources; 5 | 6 | public class ResourceInput : NetCoreInput 7 | { 8 | private readonly Lazy _cancellation; 9 | 10 | public ResourceInput() 11 | { 12 | _cancellation = 13 | new Lazy(() => new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutFlag))); 14 | } 15 | 16 | [Description("Resource action, default is setup")] 17 | public ResourceAction Action { get; set; } = ResourceAction.setup; 18 | 19 | [Description("Timeout in seconds, default is 60")] 20 | public int TimeoutFlag { get; set; } = 60; 21 | 22 | [IgnoreOnCommandLine] public CancellationTokenSource TokenSource => _cancellation.Value; 23 | 24 | [Description("Optionally filter by resource type")] 25 | public string TypeFlag { get; set; } 26 | 27 | [Description("Optionally filter by resource name")] 28 | public string NameFlag { get; set; } 29 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/ResourceSetupException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Oakton.Resources; 4 | 5 | public class ResourceSetupException : Exception 6 | { 7 | public ResourceSetupException(IStatefulResource resource, Exception ex) : base( 8 | $"Failed to setup resource {resource.Name} of type {resource.Type}", ex) 9 | 10 | { 11 | } 12 | 13 | public ResourceSetupException(IStatefulResourceSource source, Exception ex) : base( 14 | $"Failed to execute resource source {source}", ex) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/ResourceSetupHostService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Oakton.Resources; 10 | 11 | internal class ResourceSetupOptions 12 | { 13 | public StartupAction Action { get; set; } = StartupAction.SetupOnly; 14 | } 15 | 16 | internal class ResourceSetupHostService : IHostedService 17 | { 18 | private readonly ILogger _logger; 19 | private readonly ResourceSetupOptions _options; 20 | private readonly IStatefulResource[] _resources; 21 | private readonly IStatefulResourceSource[] _sources; 22 | 23 | public ResourceSetupHostService(ResourceSetupOptions options, IEnumerable resources, 24 | IEnumerable sources, ILogger logger) 25 | { 26 | _resources = resources.ToArray(); 27 | _sources = sources.ToArray(); 28 | _options = options; 29 | _logger = logger; 30 | } 31 | 32 | public async Task StartAsync(CancellationToken cancellationToken) 33 | { 34 | var list = new List(); 35 | var resources = new List(_resources); 36 | 37 | foreach (var source in _sources) 38 | { 39 | try 40 | { 41 | resources.AddRange(source.FindResources()); 42 | } 43 | catch (Exception e) 44 | { 45 | _logger.LogError(e, "Failed to find resource sources from {Source}", source); 46 | list.Add(new ResourceSetupException(source, e)); 47 | } 48 | } 49 | 50 | async ValueTask execute(IStatefulResource r, CancellationToken t) 51 | { 52 | try 53 | { 54 | await r.Setup(cancellationToken).ConfigureAwait(false); 55 | _logger.LogInformation("Ran Setup() on resource {Name} of type {Type}", r.Name, r.Type); 56 | 57 | if (_options.Action == StartupAction.ResetState) 58 | { 59 | await r.ClearState(cancellationToken).ConfigureAwait(false); 60 | _logger.LogInformation("Ran ClearState() on resource {Name} of type {Type}", r.Name, r.Type); 61 | } 62 | } 63 | catch (Exception e) 64 | { 65 | var wrapped = new ResourceSetupException(r, e); 66 | _logger.LogError(e, "Failed to setup resource {Name} of type {Type}", r.Name, r.Type); 67 | 68 | list.Add(wrapped); 69 | } 70 | } 71 | 72 | foreach (var resource in resources) await execute(resource, cancellationToken).ConfigureAwait(false); 73 | 74 | if (list.Any()) 75 | { 76 | throw new AggregateException(list); 77 | } 78 | } 79 | 80 | public Task StopAsync(CancellationToken cancellationToken) 81 | { 82 | return Task.CompletedTask; 83 | } 84 | } -------------------------------------------------------------------------------- /src/Oakton/Resources/StatefulResourceBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Spectre.Console; 4 | using Spectre.Console.Rendering; 5 | 6 | namespace Oakton.Resources; 7 | 8 | /// 9 | /// Base class with empty implementations for IStatefulResource. 10 | /// 11 | public abstract class StatefulResourceBase : IStatefulResource 12 | { 13 | protected StatefulResourceBase(string type, string name) 14 | { 15 | Type = type; 16 | Name = name; 17 | } 18 | 19 | public virtual Task Check(CancellationToken token) 20 | { 21 | return Task.CompletedTask; 22 | } 23 | 24 | public virtual Task ClearState(CancellationToken token) 25 | { 26 | return Task.CompletedTask; 27 | } 28 | 29 | public virtual Task Teardown(CancellationToken token) 30 | { 31 | return Task.CompletedTask; 32 | } 33 | 34 | public virtual Task Setup(CancellationToken token) 35 | { 36 | return Task.CompletedTask; 37 | } 38 | 39 | public virtual Task DetermineStatus(CancellationToken token) 40 | { 41 | return Task.FromResult((IRenderable)new Markup("Okay")); 42 | } 43 | 44 | public string Type { get; } 45 | public string Name { get; } 46 | } -------------------------------------------------------------------------------- /src/Oakton/jasper-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/src/Oakton/jasper-icon.png -------------------------------------------------------------------------------- /src/OaktonSample/OaktonSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0;net7.0 4 | OaktonSample 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/OaktonSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "OaktonSample": { 4 | "commandName": "Project", 5 | "commandLineArgs": "help" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/OptsFileUsageDemo/HelloCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton; 3 | 4 | namespace OptsFileUsageDemo 5 | { 6 | public class HelloInput 7 | { 8 | public int Times { get; set; } 9 | } 10 | 11 | public class HelloCommand : OaktonCommand 12 | { 13 | public override bool Execute(HelloInput input) 14 | { 15 | for (var i = 0; i < input.Times; i++) 16 | { 17 | Console.WriteLine($"{i + 1}. Hello!"); 18 | } 19 | 20 | Console.WriteLine("Press any key to end."); 21 | Console.ReadLine(); 22 | 23 | return true; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/OptsFileUsageDemo/OptsFileUsageDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/OptsFileUsageDemo/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using Microsoft.Extensions.Hosting; 4 | using Oakton; 5 | 6 | return Host.CreateDefaultBuilder(args) 7 | .RunOaktonCommandsSynchronously(args, "sample.opts"); -------------------------------------------------------------------------------- /src/OptsFileUsageDemo/sample.opts: -------------------------------------------------------------------------------- 1 | hello 5 -------------------------------------------------------------------------------- /src/Tests/ActivatorCommandCreatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests 7 | { 8 | public class ActivatorCommandCreatorTests 9 | { 10 | [Fact] 11 | public void builds_an_instance_with_no_ctor_parameters() 12 | { 13 | var creator = new ActivatorCommandCreator(); 14 | var instance = creator.CreateCommand(typeof (NoParamsCommand)); 15 | 16 | instance.ShouldBeOfType(); 17 | } 18 | 19 | [Fact] 20 | public void throws_if_the_ctor_has_parameters() 21 | { 22 | var creator = new ActivatorCommandCreator(); 23 | 24 | Assert.Throws(() => creator.CreateCommand(typeof (ParamsCommand))); 25 | } 26 | 27 | public class FakeModel 28 | { 29 | } 30 | 31 | private class NoParamsCommand : OaktonCommand 32 | { 33 | public override bool Execute(FakeModel input) 34 | { 35 | throw new System.NotImplementedException(); 36 | } 37 | } 38 | 39 | private class ParamsCommand : OaktonCommand 40 | { 41 | public ParamsCommand(string testArgument) 42 | { 43 | } 44 | 45 | public override bool Execute(FakeModel input) 46 | { 47 | throw new System.NotImplementedException(); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tests/ArgPreprocessorTester.cs: -------------------------------------------------------------------------------- 1 | using Oakton.Parsing; 2 | using Xunit; 3 | 4 | namespace Tests 5 | { 6 | 7 | public class ArgPreprocessorTester 8 | { 9 | [Fact] 10 | public void should_split_multi_args() 11 | { 12 | ArgPreprocessor.Process(new[] {"-abc"}).ShouldHaveTheSameElementsAs("-a", "-b", "-c"); 13 | } 14 | 15 | [Fact] 16 | public void combined_short_flags_should_be_case_sensitive() 17 | { 18 | ArgPreprocessor.Process(new[] { "-aAbBcC" }).ShouldHaveTheSameElementsAs("-a","-A", "-b","-B", "-c","-C"); 19 | } 20 | 21 | [Fact] 22 | public void should_ignore_long_flag_args() 23 | { 24 | ArgPreprocessor.Process(new[] {"--abc"}).ShouldHaveTheSameElementsAs("--abc"); 25 | } 26 | 27 | [Fact] 28 | public void should_support_multiple_types_of_flags() 29 | { 30 | ArgPreprocessor.Process(new[] { "-abc", "--xyz", "b" }).ShouldHaveTheSameElementsAs("-a", "-b", "-c", "--xyz", "b"); 31 | } 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /src/Tests/ArgumentTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using JasperFx.Core.Reflection; 4 | using Oakton; 5 | using Oakton.Internal.Conversion; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace Tests 10 | { 11 | 12 | public class ArgumentTester 13 | { 14 | private Argument argFor(Expression> property) 15 | { 16 | return new Argument(ReflectionHelper.GetProperty(property), new Conversions()); 17 | } 18 | 19 | [Fact] 20 | public void description_is_just_the_property_name_if_no_description_attribute() 21 | { 22 | argFor(x => x.Name).Description.ShouldBe("Name"); 23 | } 24 | 25 | [Fact] 26 | public void description_comes_from_the_attribute_if_it_exists() 27 | { 28 | argFor(x => x.Age).Description.ShouldBe("age of target"); 29 | } 30 | 31 | [Fact] 32 | public void to_usage_description_with_a_simple_string_or_number_type() 33 | { 34 | argFor(x => x.Name).ToUsageDescription().ShouldBe(""); 35 | argFor(x => x.Age).ToUsageDescription().ShouldBe(""); 36 | } 37 | 38 | [Fact] 39 | public void to_usage_description_with_an_enumeration() 40 | { 41 | argFor(x => x.Enum).ToUsageDescription().ShouldBe("red|blue|green"); 42 | } 43 | 44 | 45 | } 46 | 47 | public enum TargetEnum 48 | { 49 | red,blue,green 50 | } 51 | 52 | public class ArgumentTarget 53 | { 54 | public string Name { get; set; } 55 | 56 | public TargetEnum Enum{ get; set;} 57 | 58 | [Description("age of target")] 59 | public int Age { get; set; } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /src/Tests/BooleanFlagTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using JasperFx.Core.Reflection; 4 | using Oakton; 5 | using Oakton.Parsing; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace Tests 10 | { 11 | 12 | public class BooleanFlagTester 13 | { 14 | 15 | private BooleanFlag getFlag(Expression> expression) 16 | { 17 | return new BooleanFlag(ReflectionHelper.GetProperty(expression)); 18 | } 19 | 20 | 21 | [Fact] 22 | public void get_usage_description_with_an_alias() 23 | { 24 | getFlag(x => x.AliasedFlag).ToUsageDescription().ShouldBe("[-a, --aliased]"); 25 | } 26 | 27 | [Fact] 28 | public void get_usage_description_without_an_alias() 29 | { 30 | getFlag(x => x.NormalFlag).ToUsageDescription().ShouldBe("[-n, --normal]"); 31 | } 32 | } 33 | 34 | public class BooleanFlagTarget 35 | { 36 | [FlagAlias("aliased", 'a')] 37 | public bool AliasedFlag { get; set; } 38 | public bool NormalFlag { get; set; } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Tests/Bugs/bug_24_negative_numbers.cs: -------------------------------------------------------------------------------- 1 | using Oakton; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace Tests.Bugs 6 | { 7 | public class bug_24_negative_numbers 8 | { 9 | [Description("Test")] 10 | public class MyInput 11 | { 12 | public int ArgNum { get; set; } 13 | 14 | public int NumFlag { get; set; } 15 | } 16 | 17 | class MyCommand : OaktonCommand 18 | { 19 | 20 | public override bool Execute(MyInput input) 21 | { 22 | return true; 23 | } 24 | } 25 | 26 | [Fact] 27 | public void should_allow_negative_numbers_in_arguments() 28 | { 29 | var factory = new CommandFactory(); 30 | factory.RegisterCommand(); 31 | 32 | factory.BuildRun("my \"-3\"") 33 | .Input.ShouldBeOfType() 34 | .ArgNum.ShouldBe(-3); 35 | } 36 | 37 | [Fact] 38 | public void should_allow_negative_numbes_in_flag() 39 | { 40 | var factory = new CommandFactory(); 41 | factory.RegisterCommand(); 42 | 43 | factory.BuildRun("my \"-3\" --num \"-5\"") 44 | .Input.ShouldBeOfType() 45 | .NumFlag.ShouldBe(-5); 46 | } 47 | 48 | [Theory] 49 | [InlineData(1, 2)] 50 | [InlineData(10, 20)] 51 | [InlineData(1, -2)] 52 | [InlineData(1, -20)] 53 | [InlineData(-1, 2)] 54 | [InlineData(-1, -20)] 55 | [InlineData(-10, -20)] 56 | public void ShouldBeAbleToParseAllNumbersWithQuotes(int argVal, int optVal) 57 | { 58 | var cmd = $"my \"{argVal}\" --num \"{optVal}\""; 59 | 60 | var f = new CommandFactory(); 61 | f.RegisterCommand(); 62 | 63 | var runner = f.BuildRun(cmd); 64 | 65 | var input = runner.Input.ShouldBeOfType(); 66 | 67 | input.ArgNum.ShouldBe(argVal); 68 | input.NumFlag.ShouldBe(optVal); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Tests/Conversion/DateTime_conversion_specs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton.Internal.Conversion; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests.Conversion 7 | { 8 | public class DateTime_conversion_specs 9 | { 10 | [Fact] 11 | public void get_date_time_for_day_and_time() 12 | { 13 | var date = DateTimeConverter.GetDateTime("Saturday 14:30"); 14 | 15 | date.DayOfWeek.ShouldBe(DayOfWeek.Saturday); 16 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date); 17 | (date >= DateTime.Today).ShouldBe(true); 18 | } 19 | 20 | [Fact] 21 | public void get_date_time_for_day_and_time_2() 22 | { 23 | var date = DateTimeConverter.GetDateTime("Monday 14:30"); 24 | 25 | date.DayOfWeek.ShouldBe(DayOfWeek.Monday); 26 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date); 27 | (date >= DateTime.Today).ShouldBe(true); 28 | } 29 | 30 | [Fact] 31 | public void get_date_time_for_day_and_time_3() 32 | { 33 | var date = DateTimeConverter.GetDateTime("Wednesday 14:30"); 34 | 35 | date.DayOfWeek.ShouldBe(DayOfWeek.Wednesday); 36 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date); 37 | (date >= DateTime.Today).ShouldBe(true); 38 | } 39 | 40 | [Fact] 41 | public void get_date_time_from_full_iso_8601_should_be_a_utc_datetime() 42 | { 43 | var date = DateTimeConverter.GetDateTime("2012-06-01T14:52:35.0000000Z"); 44 | 45 | date.ShouldBe(new DateTime(2012, 06, 01, 14, 52, 35, DateTimeKind.Utc)); 46 | } 47 | 48 | [Fact] 49 | public void get_date_time_from_partial_iso_8601_uses_default_parser_and_is_local() 50 | { 51 | var date = DateTimeConverter.GetDateTime("2012-06-01T12:52:35Z"); 52 | 53 | var gmtOffsetInHours = TimeZoneInfo.Local.GetUtcOffset(date).TotalHours; 54 | date.ShouldBe(new DateTime(2012, 06, 01, 12, 52, 35, DateTimeKind.Local).AddHours(gmtOffsetInHours)); 55 | } 56 | 57 | [Fact] 58 | public void get_date_time_from_24_hour_time() 59 | { 60 | DateTimeConverter.GetDateTime("14:30").ShouldBe(DateTime.Today.AddHours(14).AddMinutes(30)); 61 | } 62 | 63 | [Fact] 64 | public void parse_today() 65 | { 66 | DateTimeConverter.GetDateTime("TODAY").ShouldBe(DateTime.Today); 67 | } 68 | 69 | [Fact] 70 | public void parse_today_minus_date() 71 | { 72 | DateTimeConverter.GetDateTime("TODAY-3").ShouldBe(DateTime.Today.AddDays(-3)); 73 | } 74 | 75 | [Fact] 76 | public void parse_today_plus_date() 77 | { 78 | DateTimeConverter.GetDateTime("TODAY+5").ShouldBe(DateTime.Today.AddDays(5)); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Tests/Conversion/StringConverterProviderTester.cs: -------------------------------------------------------------------------------- 1 | using Oakton.Internal.Conversion; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace Tests.Conversion 6 | { 7 | public class StringConstrucutorConversionProviderTester 8 | { 9 | private readonly StringConverterProvider provider = new StringConverterProvider(); 10 | 11 | [Fact] 12 | public void provide_instance_with_string_constructor() 13 | { 14 | var @object = provider.ConverterFor(typeof (TestKlass)); 15 | @object.ShouldNotBeNull(); 16 | 17 | var result = @object("Sample"); 18 | 19 | result.ShouldNotBeNull(); 20 | result.ShouldBeOfType() 21 | .S.ShouldBe("Sample"); 22 | } 23 | 24 | [Fact] 25 | public void return_null_for_invalid_class() 26 | { 27 | var @object = provider.ConverterFor(typeof (TestKlass2)); 28 | @object.ShouldBeNull(); 29 | } 30 | 31 | 32 | public class TestKlass 33 | { 34 | public string S; 35 | 36 | public TestKlass(string s) 37 | { 38 | S = s; 39 | } 40 | } 41 | 42 | public class TestKlass2 43 | { 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Tests/Conversion/TimeSpanConverterTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton.Internal.Conversion; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests.Conversion 7 | { 8 | public class TimeSpanConverterTester 9 | { 10 | [Fact] 11 | public void happily_converts_timespans_in_4_digit_format() 12 | { 13 | TimeSpanConverter.GetTimeSpan("1230").ShouldBe(new TimeSpan(12, 30, 0)); 14 | } 15 | 16 | [Fact] 17 | public void happily_converts_timespans_in_5_digit_format() 18 | { 19 | TimeSpanConverter.GetTimeSpan("12:30").ShouldBe(new TimeSpan(12, 30, 0)); 20 | } 21 | 22 | [Fact] 23 | public void converts_timespans_for_seconds() 24 | { 25 | TimeSpanConverter.GetTimeSpan("3.5s").ShouldBe(TimeSpan.FromSeconds(3.5)); 26 | TimeSpanConverter.GetTimeSpan("5 s").ShouldBe(TimeSpan.FromSeconds(5)); 27 | TimeSpanConverter.GetTimeSpan("1 second").ShouldBe(TimeSpan.FromSeconds(1)); 28 | TimeSpanConverter.GetTimeSpan("12 seconds").ShouldBe(TimeSpan.FromSeconds(12)); 29 | } 30 | 31 | [Fact] 32 | public void converts_timespans_for_minutes() 33 | { 34 | TimeSpanConverter.GetTimeSpan("10m").ShouldBe(TimeSpan.FromMinutes(10)); 35 | TimeSpanConverter.GetTimeSpan("2.1 m").ShouldBe(TimeSpan.FromMinutes(2.1)); 36 | TimeSpanConverter.GetTimeSpan("1 minute").ShouldBe(TimeSpan.FromMinutes(1)); 37 | TimeSpanConverter.GetTimeSpan("5 minutes").ShouldBe(TimeSpan.FromMinutes(5)); 38 | } 39 | 40 | [Fact] 41 | public void converts_timespans_for_hours() 42 | { 43 | TimeSpanConverter.GetTimeSpan("24h").ShouldBe(TimeSpan.FromHours(24)); 44 | TimeSpanConverter.GetTimeSpan("4 h").ShouldBe(TimeSpan.FromHours(4)); 45 | TimeSpanConverter.GetTimeSpan("1 hour").ShouldBe(TimeSpan.FromHours(1)); 46 | TimeSpanConverter.GetTimeSpan("12.5 hours").ShouldBe(TimeSpan.FromHours(12.5)); 47 | } 48 | 49 | [Fact] 50 | public void converts_timespans_for_days() 51 | { 52 | TimeSpanConverter.GetTimeSpan("3d").ShouldBe(TimeSpan.FromDays(3)); 53 | TimeSpanConverter.GetTimeSpan("2 d").ShouldBe(TimeSpan.FromDays(2)); 54 | TimeSpanConverter.GetTimeSpan("1 day").ShouldBe(TimeSpan.FromDays(1)); 55 | TimeSpanConverter.GetTimeSpan("7 days").ShouldBe(TimeSpan.FromDays(7)); 56 | } 57 | 58 | [Fact] 59 | public void can_convert_from_standard_format() 60 | { 61 | TimeSpanConverter.GetTimeSpan("00:00:01").ShouldBe(new TimeSpan(0, 0, 1)); 62 | TimeSpanConverter.GetTimeSpan("00:10:00").ShouldBe(new TimeSpan(0, 10, 0)); 63 | TimeSpanConverter.GetTimeSpan("01:30:00").ShouldBe(new TimeSpan(1, 30, 0)); 64 | TimeSpanConverter.GetTimeSpan("1.01:30:00").ShouldBe(new TimeSpan(1, 1, 30, 0)); 65 | TimeSpanConverter.GetTimeSpan("-00:10:00").ShouldBe(new TimeSpan(0, -10, 0)); 66 | TimeSpanConverter.GetTimeSpan("12:34:56.789").ShouldBe(new TimeSpan(0, 12, 34, 56, 789)); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/Tests/CustomCommandCreatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests 7 | { 8 | public class CustomCommandCreatorTests 9 | { 10 | [Fact] 11 | public void command_factory_creates_with_custom_creator() 12 | { 13 | TestCommandCreator creator = new TestCommandCreator(); 14 | ICommandFactory factory = new CommandFactory(creator); 15 | var executor = CommandExecutor.For(_ => 16 | { 17 | _.RegisterCommand(); 18 | }, creator); 19 | 20 | executor.Execute("test"); 21 | 22 | creator.LastCreatedModel.ShouldNotBeNull(); 23 | } 24 | 25 | [Fact] 26 | public void help_displays_with_custom_creator() 27 | { 28 | TestCommandCreator creator = new TestCommandCreator(); 29 | CommandFactory factory = new CommandFactory(creator); 30 | var executor = CommandExecutor.For(_ => 31 | { 32 | _.RegisterCommand(); 33 | }, creator); 34 | 35 | executor.Execute(""); 36 | 37 | creator.LastCreatedModel.ShouldNotBeNull(); 38 | } 39 | 40 | private class TestCommand : OaktonCommand 41 | { 42 | public TestCommand(string message) 43 | { 44 | Message = message; 45 | } 46 | 47 | public string Message { get; } 48 | 49 | public override bool Execute(object input) 50 | { 51 | return true; 52 | } 53 | } 54 | 55 | private class TestOptions 56 | { 57 | public TestOptions(string message) 58 | { 59 | Message = message; 60 | } 61 | 62 | public string Message { get; } 63 | } 64 | 65 | private class TestCommandCreator : ICommandCreator 66 | { 67 | public TestCommand LastCreatedCommand { get; private set; } 68 | 69 | public TestOptions LastCreatedModel { get; private set; } 70 | 71 | public IOaktonCommand CreateCommand(Type commandType) 72 | { 73 | LastCreatedCommand = new TestCommand("created command"); 74 | return LastCreatedCommand; 75 | } 76 | 77 | public object CreateModel(Type modelType) 78 | { 79 | LastCreatedModel = new TestOptions("created options"); 80 | return LastCreatedModel; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tests/Descriptions/ConfigurationPreviewTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using JasperFx.Core; 6 | using Microsoft.Extensions.Configuration; 7 | using Oakton.Descriptions; 8 | using Xunit; 9 | 10 | namespace Tests.Descriptions; 11 | 12 | [Collection("SetConsoleOutput")] 13 | public class ConfigurationPreviewTests 14 | { 15 | [Fact] 16 | public async Task write_configuration_value_with_bracket_to_console() 17 | { 18 | // Arrange 19 | var configBuilder = new ConfigurationBuilder(); 20 | configBuilder.AddInMemoryCollection(new Dictionary 21 | { 22 | { "NoBracketKey", "hello world" }, 23 | { "BracketKey", "value with bracket [hello]" } 24 | }); 25 | var config = configBuilder.Build(); 26 | 27 | 28 | var original = Console.Out; 29 | var output = new StringWriter(); 30 | Console.SetOut(output); 31 | 32 | try 33 | { 34 | // Act 35 | var preview = new ConfigurationPreview(config); 36 | await preview.WriteToConsole(); 37 | 38 | 39 | // Assert 40 | var text = output.ToString().ReadLines(); 41 | 42 | } 43 | finally 44 | { 45 | Console.SetOut(original); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Tests/DictionaryFlagTester.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Oakton; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace Tests 8 | { 9 | public class DictionaryFlagTester 10 | { 11 | private CommandFactory theFactory; 12 | 13 | public DictionaryFlagTester() 14 | { 15 | theFactory = new CommandFactory(); 16 | theFactory.RegisterCommands(GetType().GetTypeInfo().Assembly); 17 | } 18 | 19 | private CommandRun forArgs(string args) 20 | { 21 | return theFactory.BuildRun(args); 22 | } 23 | 24 | [Fact] 25 | public void use_prop_flags() 26 | { 27 | var run = forArgs("dict --prop:color red --prop:age 43"); 28 | 29 | var input = run.Input.ShouldBeOfType(); 30 | input.PropFlag["color"].ShouldBe("red"); 31 | input.PropFlag["age"].ShouldBe("43"); 32 | } 33 | 34 | [Fact] 35 | public void use_prop_flags_when_dict_has_to_be_built() 36 | { 37 | var run = forArgs("missingdict --prop:color red --prop:age 43"); 38 | 39 | var input = run.Input.ShouldBeOfType(); 40 | input.PropFlag["color"].ShouldBe("red"); 41 | input.PropFlag["age"].ShouldBe("43"); 42 | } 43 | } 44 | 45 | #region sample_DictInput 46 | public class DictInput 47 | { 48 | public Dictionary PropFlag = new Dictionary(); 49 | } 50 | #endregion 51 | 52 | public class DictCommand : OaktonCommand 53 | { 54 | public override bool Execute(DictInput input) 55 | { 56 | return true; 57 | } 58 | } 59 | 60 | public class MissingInput 61 | { 62 | public Dictionary PropFlag; 63 | } 64 | 65 | [Description("SOMETHING", Name = "missingdict")] 66 | public class MissingDictCommand : OaktonCommand 67 | { 68 | public override bool Execute(MissingInput input) 69 | { 70 | return true; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Tests/EnumerableArgumentTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using JasperFx.Core.Reflection; 5 | using Oakton; 6 | using Oakton.Internal.Conversion; 7 | using Oakton.Parsing; 8 | using Shouldly; 9 | using Xunit; 10 | 11 | namespace Tests 12 | { 13 | 14 | public class EnumerableArgumentTester 15 | { 16 | private EnumerableArgument argFor(Expression> property) 17 | { 18 | return new EnumerableArgument(ReflectionHelper.GetProperty(property), new Conversions()); 19 | } 20 | 21 | [Fact] 22 | public void description_is_just_the_property_name_if_no_description_attribute() 23 | { 24 | argFor(x => x.Names).Description.ShouldBe("Names"); 25 | } 26 | 27 | [Fact] 28 | public void description_comes_from_the_attribute_if_it_exists() 29 | { 30 | argFor(x => x.Ages).Description.ShouldBe("ages of target"); 31 | } 32 | 33 | [Fact] 34 | public void to_usage_description_with_a_simple_string_or_number_type() 35 | { 36 | argFor(x => x.Names).ToUsageDescription().ShouldBe(""); 37 | argFor(x => x.Ages).ToUsageDescription().ShouldBe(""); 38 | } 39 | 40 | [Fact] 41 | public void x() 42 | { 43 | Console.WriteLine(argFor(x => x.OptionalFlag).ToUsageDescription()); 44 | 45 | } 46 | 47 | [Fact] 48 | public void handle() 49 | { 50 | var target = new EnumerableArgumentInput(); 51 | var queue = new Queue(); 52 | queue.Enqueue("a"); 53 | queue.Enqueue("b"); 54 | queue.Enqueue("c"); 55 | 56 | argFor(x => x.Names).Handle(target, queue); 57 | 58 | target.Names.ShouldHaveTheSameElementsAs("a", "b", "c"); 59 | } 60 | 61 | } 62 | 63 | #region sample_EnumerableArguments 64 | public class EnumerableArgumentInput 65 | { 66 | public IEnumerable Names { get; set; } 67 | 68 | public IEnumerable OptionalFlag { get; set; } 69 | 70 | public IEnumerable Enums { get; set; } 71 | 72 | [Description("ages of target")] 73 | public IEnumerable Ages { get; set; } 74 | 75 | } 76 | #endregion 77 | } 78 | -------------------------------------------------------------------------------- /src/Tests/Environment/EnvironmentCheckResultsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Oakton.Environment; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests.Environment 7 | { 8 | public class EnvironmentCheckResultsTests 9 | { 10 | [Fact] 11 | public void empty_results_asserts_just_fine() 12 | { 13 | new EnvironmentCheckResults().Assert(); 14 | } 15 | 16 | [Fact] 17 | public void assert_with_only_successes() 18 | { 19 | var checkResults = new EnvironmentCheckResults(); 20 | checkResults.RegisterSuccess("Okay"); 21 | checkResults.RegisterSuccess("Still Okay"); 22 | 23 | checkResults.Assert(); 24 | } 25 | 26 | [Fact] 27 | public void assert_with_failures() 28 | { 29 | var checkResults = new EnvironmentCheckResults(); 30 | 31 | checkResults.RegisterSuccess("Okay"); 32 | 33 | checkResults.RegisterSuccess("Still Okay"); 34 | 35 | checkResults.RegisterFailure("bad!", new DivideByZeroException()); 36 | 37 | var ex = Should.Throw(() => checkResults.Assert()); 38 | 39 | ex.Results.ShouldBeSameAs(checkResults); 40 | } 41 | 42 | [Fact] 43 | public void succeeded() 44 | { 45 | var checkResults = new EnvironmentCheckResults(); 46 | checkResults.Succeeded().ShouldBeTrue(); 47 | 48 | checkResults.RegisterSuccess("Okay"); 49 | checkResults.Succeeded().ShouldBeTrue(); 50 | 51 | checkResults.RegisterSuccess("Still Okay"); 52 | checkResults.Succeeded().ShouldBeTrue(); 53 | 54 | checkResults.RegisterFailure("bad!", new DivideByZeroException()); 55 | checkResults.Succeeded().ShouldBeFalse(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Tests/Environment/LambdaCheckTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Oakton.Environment; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace Tests.Environment 8 | { 9 | public class LambdaCheckTests 10 | { 11 | [Fact] 12 | public void description() 13 | { 14 | var check = new LambdaCheck("it's okay", (s, t) => Task.CompletedTask); 15 | check.Description.ShouldBe("it's okay"); 16 | } 17 | 18 | [Fact] 19 | public async Task call_the_assert() 20 | { 21 | bool wasCalled = false; 22 | var check = new LambdaCheck("it's okay", (s, t) => 23 | { 24 | wasCalled = true; 25 | return Task.CompletedTask; 26 | }); 27 | 28 | await check.Assert(null, default(CancellationToken)); 29 | 30 | wasCalled.ShouldBeTrue(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Tests/HostedCommandsTester.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Oakton; 4 | using System; 5 | using Xunit; 6 | 7 | namespace Tests; 8 | 9 | public class HostedCommandsTester 10 | { 11 | [Fact] 12 | public void CanInjectServicesIntoCommands() 13 | { 14 | var builder = Host.CreateDefaultBuilder() 15 | .ConfigureServices(services => 16 | { 17 | services.AddScoped(); 18 | services.AddOakton(options => 19 | { 20 | options.Factory = factory => 21 | { 22 | factory.RegisterCommand(); 23 | }; 24 | options.DefaultCommand = "TestDI"; 25 | }); 26 | }); 27 | 28 | var app = builder.Build(); 29 | 30 | app.RunHostedOaktonCommands(Array.Empty()); 31 | 32 | Assert.Equal(1, TestDICommand.Value); 33 | } 34 | 35 | public class TestInput 36 | { 37 | } 38 | 39 | public class TestDependency : IDisposable 40 | { 41 | public int Value { get; private set; } 42 | 43 | public TestDependency() 44 | { 45 | Value = 1; 46 | } 47 | 48 | public void Dispose() 49 | { 50 | Value = 0; 51 | GC.SuppressFinalize(this); 52 | } 53 | } 54 | 55 | public class TestDICommand : OaktonCommand 56 | { 57 | public static int Value { get; set; } = 0; 58 | private readonly TestDependency _dep; 59 | public TestDICommand(TestDependency dep) 60 | { 61 | _dep = dep; 62 | } 63 | 64 | public override bool Execute(TestInput input) 65 | { 66 | Value = _dep.Value; 67 | return true; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Tests/NoXUnitParallelization.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] 4 | -------------------------------------------------------------------------------- /src/Tests/OptionReaderTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using JasperFx.Core; 4 | using Oakton.Parsing; 5 | using Shouldly; 6 | using Xunit; 7 | 8 | namespace Tests 9 | { 10 | public class OptionReaderTester 11 | { 12 | #if NET451 13 | private string directory = AppDomain.CurrentDomain.BaseDirectory; 14 | #else 15 | private string directory = AppContext.BaseDirectory; 16 | #endif 17 | 18 | [Fact] 19 | public void read_from_one_line() 20 | { 21 | var path = directory.AppendPath("opts1.txt"); 22 | File.WriteAllText(path, "-f -a -b"); 23 | 24 | OptionReader.Read(path) 25 | .ShouldBe("-f -a -b"); 26 | } 27 | 28 | [Fact] 29 | public void read_from_multiple_lines() 30 | { 31 | var path = directory.AppendPath("opts2.txt"); 32 | 33 | using (var stream = new FileStream(path, FileMode.Create)) 34 | { 35 | var writer = new StreamWriter(stream); 36 | 37 | writer.WriteLine("--color Blue"); 38 | writer.WriteLine("--size Medium "); 39 | writer.WriteLine(" --direction East"); 40 | 41 | writer.Flush(); 42 | 43 | writer.Dispose(); 44 | } 45 | 46 | OptionReader.Read(path) 47 | .ShouldBe("--color Blue --size Medium --direction East"); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Tests/OptionsSamples.cs: -------------------------------------------------------------------------------- 1 | using Oakton; 2 | 3 | namespace Tests 4 | { 5 | public class OptionsSamples 6 | { 7 | public static void go() 8 | { 9 | #region sample_configuring_opts_file 10 | var executor = CommandExecutor.For(_ => 11 | { 12 | // configure the command discovery 13 | }); 14 | 15 | executor.OptionsFile = "mytool.opts"; 16 | #endregion 17 | } 18 | } 19 | 20 | #region sample_SecuredInput 21 | public class SecuredInput 22 | { 23 | public string UserName { get; set; } 24 | public string Password { get; set; } 25 | } 26 | #endregion 27 | } 28 | -------------------------------------------------------------------------------- /src/Tests/Resources/resource_filtering.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests.Resources 7 | { 8 | public class resource_filtering : ResourceCommandContext 9 | { 10 | [Fact] 11 | public void uses_resource_source() 12 | { 13 | var blue = AddResource("blue", "color"); 14 | var red = AddResource("red", "color"); 15 | 16 | AddSource(col => 17 | { 18 | col.Add("purple", "color"); 19 | col.Add("orange", "color"); 20 | }); 21 | 22 | AddSource(col => 23 | { 24 | col.Add("green", "color"); 25 | col.Add("white", "color"); 26 | }); 27 | 28 | var resources = applyTheResourceFiltering(); 29 | 30 | var colors = resources.Select(x => x.Name).OrderBy(x => x) 31 | .ToList(); 32 | 33 | colors.ShouldHaveTheSameElementsAs("blue", "green", "orange", "purple", "red", "white"); 34 | } 35 | 36 | [Fact] 37 | public void no_filtering() 38 | { 39 | var blue = AddResource("blue", "color"); 40 | var red = AddResource("red", "color"); 41 | 42 | var tx = AddResource("tx", "state"); 43 | var ar = AddResource("ar", "state"); 44 | 45 | var resources = applyTheResourceFiltering(); 46 | 47 | resources.Count.ShouldBe(4); 48 | 49 | resources.ShouldContain(blue); 50 | resources.ShouldContain(red); 51 | resources.ShouldContain(tx); 52 | resources.ShouldContain(ar); 53 | } 54 | 55 | [Fact] 56 | public void filter_by_name() 57 | { 58 | var blue = AddResource("blue", "color"); 59 | var red = AddResource("red", "color"); 60 | 61 | var tx = AddResource("tx", "state"); 62 | var ar = AddResource("ar", "state"); 63 | 64 | theInput.NameFlag = "tx"; 65 | 66 | var resources = applyTheResourceFiltering(); 67 | resources.Single() 68 | .ShouldBe(tx); 69 | } 70 | 71 | [Fact] 72 | public void filter_by_type() 73 | { 74 | var blue = AddResource("blue", "color"); 75 | var red = AddResource("red", "color"); 76 | var green = AddResource("green", "color"); 77 | 78 | var tx = AddResource("tx", "state"); 79 | var ar = AddResource("ar", "state"); 80 | var mo = AddResource("mo", "state"); 81 | 82 | theInput.TypeFlag = "color"; 83 | var resources = applyTheResourceFiltering(); 84 | 85 | resources.Count.ShouldBe(3); 86 | resources.ShouldContain(blue); 87 | resources.ShouldContain(red); 88 | resources.ShouldContain(green); 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/Tests/Resources/resource_ordering.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace Tests.Resources; 6 | 7 | public class resource_ordering : ResourceCommandContext 8 | { 9 | [Fact] 10 | public void respect_ordering_by_dependencies() 11 | { 12 | var one = AddResourceWithDependencies("one", "system", "blue", "red"); 13 | var two = AddResourceWithDependencies("two", "system", "blue", "red", "one"); 14 | 15 | var blue = AddResource("blue", "color"); 16 | var red = AddResource("red", "color"); 17 | 18 | var tx = AddResource("tx", "state"); 19 | var ar = AddResource("ar", "state"); 20 | 21 | var resources = applyTheResourceFiltering(); 22 | 23 | resources.Count.ShouldBe(6); 24 | 25 | resources.Last().ShouldBe(two); 26 | 27 | resources.Select(x => x.Name).ShouldBe(new string[] { "blue", "red", "ar", "tx", "one", "two" }); 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/Tests/SpecificationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using Shouldly; 7 | 8 | namespace Tests 9 | { 10 | public static class SpecificationExtensions 11 | { 12 | public static void ShouldHaveTheSameElementsAs(this IList actual, IList expected) 13 | { 14 | try 15 | { 16 | actual.ShouldNotBeNull(); 17 | expected.ShouldNotBeNull(); 18 | 19 | actual.Count.ShouldBe(expected.Count); 20 | 21 | for (int i = 0; i < actual.Count; i++) 22 | { 23 | actual[i].ShouldBe(expected[i]); 24 | } 25 | } 26 | catch (Exception) 27 | { 28 | Debug.WriteLine("Actual values were:"); 29 | foreach (var x in actual) 30 | { 31 | Debug.WriteLine((object)x); 32 | } 33 | throw; 34 | } 35 | } 36 | 37 | public static void ShouldHaveTheSameElementsAs(this IEnumerable actual, params T[] expected) 38 | { 39 | ShouldHaveTheSameElementsAs(actual, (IEnumerable)expected); 40 | } 41 | 42 | public static void ShouldHaveTheSameElementsAs(this IEnumerable actual, IEnumerable expected) 43 | { 44 | IList actualList = (actual is IList) ? (IList)actual : actual.ToList(); 45 | IList expectedList = (expected is IList) ? (IList)expected : expected.ToList(); 46 | 47 | ShouldHaveTheSameElementsAs(actualList, expectedList); 48 | } 49 | 50 | public static void ShouldHaveTheSameElementKeysAs(this IEnumerable actual, 51 | IEnumerable expected, 52 | Func keySelector) 53 | { 54 | actual.ShouldNotBeNull(); 55 | expected.ShouldNotBeNull(); 56 | 57 | ELEMENT[] actualArray = actual.ToArray(); 58 | object[] expectedArray = expected.Cast().ToArray(); 59 | 60 | actualArray.Length.ShouldBe(expectedArray.Length); 61 | 62 | for (int i = 0; i < actual.Count(); i++) 63 | { 64 | keySelector(actualArray[i]).ShouldBe(expectedArray[i]); 65 | } 66 | } 67 | 68 | } 69 | 70 | public static class Exception where T : Exception 71 | { 72 | public static T ShouldBeThrownBy(Action action) 73 | { 74 | T exception = null; 75 | 76 | try 77 | { 78 | action(); 79 | } 80 | catch (Exception e) 81 | { 82 | exception = e.ShouldBeOfType(); 83 | } 84 | 85 | if (exception == null) throw new Exception("An exception was expected, but not thrown by the given action."); 86 | 87 | return exception; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/Tests/StringTokenizerTester.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Oakton.Parsing; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace Tests 7 | { 8 | 9 | public class StringTokenizerTester 10 | { 11 | [Fact] 12 | public void empty_array_for_all_whitespace() 13 | { 14 | StringTokenizer.Tokenize("").Any().ShouldBeFalse(); 15 | StringTokenizer.Tokenize(" ").Any().ShouldBeFalse(); 16 | StringTokenizer.Tokenize("\t").Any().ShouldBeFalse(); 17 | StringTokenizer.Tokenize("\n").Any().ShouldBeFalse(); 18 | } 19 | 20 | [Fact] 21 | public void tokenize_simple_strings() 22 | { 23 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state"); 24 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state"); 25 | StringTokenizer.Tokenize("name age\nstate").ShouldHaveTheSameElementsAs("name", "age", "state"); 26 | StringTokenizer.Tokenize("name\tage state").ShouldHaveTheSameElementsAs("name", "age", "state"); 27 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state"); 28 | } 29 | 30 | [Fact] 31 | public void tokenize_string_with_only_one_token() 32 | { 33 | StringTokenizer.Tokenize("name").ShouldHaveTheSameElementsAs("name"); 34 | StringTokenizer.Tokenize(" name ").ShouldHaveTheSameElementsAs("name"); 35 | StringTokenizer.Tokenize("\nname").ShouldHaveTheSameElementsAs("name"); 36 | StringTokenizer.Tokenize("name\n").ShouldHaveTheSameElementsAs("name"); 37 | } 38 | 39 | [Fact] 40 | public void tokenize_string_marked_with_parantheses() 41 | { 42 | StringTokenizer.Tokenize("name \"jeremy miller\" age").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age"); 43 | StringTokenizer.Tokenize("name \" jeremy miller\" age").ShouldHaveTheSameElementsAs("name", " jeremy miller", "age"); 44 | StringTokenizer.Tokenize("name \"jeremy miller\" age").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age"); 45 | StringTokenizer.Tokenize("name \"jeremy miller\" age \"Texas\"").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age", "Texas"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Tests/can_use_fields_as_arguments_and_flags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Oakton; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace Tests 8 | { 9 | public class can_use_fields_as_arguments_and_flags 10 | { 11 | [Fact] 12 | public void integrated_test_arguments_only() 13 | { 14 | var input = build("file1", "red"); 15 | input.File.ShouldBe("file1"); 16 | input.Color.ShouldBe(Color.red); 17 | 18 | // default is not touched 19 | input.OrderFlag.ShouldBe(0); 20 | } 21 | 22 | [Fact] 23 | public void integrated_test_with_mix_of_flags() 24 | { 25 | var input = build("file1", "--color", "green", "blue", "--order", "12"); 26 | input.File.ShouldBe("file1"); 27 | input.Color.ShouldBe(Color.blue); 28 | input.ColorFlag.ShouldBe(Color.green); 29 | input.OrderFlag.ShouldBe(12); 30 | } 31 | 32 | [Fact] 33 | public void integrated_test_with_a_boolean_flag() 34 | { 35 | var input = build("file1", "blue", "--true-false"); 36 | input.TrueFalseFlag.ShouldBeTrue(); 37 | 38 | build("file1", "blue").TrueFalseFlag.ShouldBeFalse(); 39 | } 40 | 41 | [Fact] 42 | public void long_flag_with_dashes_should_pass() 43 | { 44 | var input = build("file1", "blue", "--herp-derp"); 45 | input.HerpDerpFlag.ShouldBeTrue(); 46 | 47 | build("file1", "blue").HerpDerpFlag.ShouldBeFalse(); 48 | } 49 | 50 | 51 | private FieldModel build(params string[] tokens) 52 | { 53 | var queue = new Queue(tokens); 54 | var graph = new FieldCommand().Usages; 55 | var creator = new ActivatorCommandCreator(); 56 | 57 | return (FieldModel)graph.BuildInput(queue, creator); 58 | } 59 | 60 | public class FieldModel 61 | { 62 | public string File ; 63 | public Color ColorFlag ; 64 | 65 | public Color Color ; 66 | public int OrderFlag ; 67 | public bool TrueFalseFlag ; 68 | [FlagAlias('T')] 69 | public bool TrueOrFalseFlag ; 70 | 71 | public IEnumerable SillyFlag ; 72 | 73 | public bool HerpDerpFlag ; 74 | 75 | [FlagAlias("makesuckmode")] 76 | public bool MakeSuckModeFlag ; 77 | 78 | public IEnumerable Ages ; 79 | 80 | [FlagAlias("aliased", 'a')] 81 | public string AliasedFlag ; 82 | } 83 | 84 | public class FieldCommand : OaktonCommand 85 | { 86 | public FieldCommand() 87 | { 88 | Usage("default").Arguments(x => x.File, x => x.Color); 89 | Usage("ages").Arguments(x => x.File, x => x.Color, x => x.Ages); 90 | } 91 | 92 | public override bool Execute(FieldModel input) 93 | { 94 | throw new NotImplementedException(); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Tests/filtering_launcher_args.cs: -------------------------------------------------------------------------------- 1 | using Oakton.Internal; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace Tests 6 | { 7 | public class filtering_launcher_args 8 | { 9 | [Fact] 10 | public void nothing_for_an_empty_array() 11 | { 12 | new string[0].FilterLauncherArgs() 13 | .Length.ShouldBe(0); 14 | } 15 | 16 | [Fact] 17 | public void just_the_launcher_args() 18 | { 19 | new string[]{"%launcher_args%"}.FilterLauncherArgs() 20 | .Length.ShouldBe(0); 21 | } 22 | 23 | [Fact] 24 | public void extra_args() 25 | { 26 | new string[]{"%launcher_args%", "command"}.FilterLauncherArgs() 27 | .ShouldHaveSingleItem("command"); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Tests/using_injected_services.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Oakton; 6 | using Oakton.Help; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | [assembly: OaktonCommandAssembly] 11 | 12 | namespace Tests; 13 | 14 | public class using_injected_services 15 | { 16 | [Fact] 17 | public async Task can_use_injected_services() 18 | { 19 | var success = await Host.CreateDefaultBuilder() 20 | .ConfigureServices(services => 21 | { 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | }) 25 | .RunOaktonCommands(new string[] { "injected", "Bob Marley" }); 26 | 27 | success.ShouldBe(0); 28 | 29 | MyService.WasCalled.ShouldBeTrue(); 30 | MyService.Name.ShouldBe("Bob Marley"); 31 | MyService.WasDisposed.ShouldBeTrue(); 32 | 33 | OtherService.WasCalled.ShouldBeTrue(); 34 | OtherService.Name.ShouldBe("Bob Marley"); 35 | OtherService.WasDisposed.ShouldBeTrue(); 36 | 37 | } 38 | } 39 | 40 | public class InjectedInput 41 | { 42 | public string Name { get; set; } 43 | } 44 | 45 | [Description("Injected command", Name = "injected")] 46 | public class InjectedCommand : OaktonCommand 47 | { 48 | [InjectService] 49 | public MyService One { get; set; } 50 | 51 | [InjectService] 52 | public OtherService Two { get; set; } 53 | 54 | public override bool Execute(InjectedInput input) 55 | { 56 | One.DoStuff(input.Name); 57 | Two.DoStuff(input.Name); 58 | 59 | return true; 60 | } 61 | } 62 | 63 | public class MyService : IDisposable 64 | { 65 | public static bool WasCalled; 66 | public static string Name; 67 | 68 | public static bool WasDisposed; 69 | 70 | public void DoStuff(string name) 71 | { 72 | WasCalled = true; 73 | Name = name; 74 | } 75 | 76 | public void Dispose() 77 | { 78 | WasDisposed = true; 79 | } 80 | } 81 | 82 | public class OtherService : IDisposable 83 | { 84 | public static bool WasCalled; 85 | public static string Name; 86 | 87 | public static bool WasDisposed; 88 | 89 | public void DoStuff(string name) 90 | { 91 | WasCalled = true; 92 | Name = name; 93 | } 94 | 95 | public void Dispose() 96 | { 97 | WasDisposed = true; 98 | } 99 | } 100 | 101 | public class MyDbContext{} 102 | 103 | public class MyInput 104 | { 105 | 106 | } 107 | 108 | #region sample_MyDbCommand 109 | 110 | public class MyDbCommand : OaktonAsyncCommand 111 | { 112 | [InjectService] 113 | public MyDbContext DbContext { get; set; } 114 | 115 | public override Task Execute(MyInput input) 116 | { 117 | // do stuff with DbContext from up above 118 | return Task.FromResult(true); 119 | } 120 | } 121 | 122 | #endregion -------------------------------------------------------------------------------- /src/WorkerService/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Oakton; 7 | 8 | namespace WorkerService 9 | { 10 | #region sample_using_ihost_activation 11 | 12 | public class Program 13 | { 14 | public static Task Main(string[] args) 15 | { 16 | return CreateHostBuilder(args) 17 | .RunOaktonCommands(args); 18 | } 19 | 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | // This is a little old-fashioned, but still valid .NET core code: 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureServices((hostContext, services) => { services.AddHostedService(); }); 24 | } 25 | 26 | #endregion 27 | } -------------------------------------------------------------------------------- /src/WorkerService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WorkerService": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/WorkerService/Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace WorkerService 9 | { 10 | public class Worker : BackgroundService 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public Worker(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 20 | { 21 | while (!stoppingToken.IsCancellationRequested) 22 | { 23 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 24 | await Task.Delay(1000, stoppingToken); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WorkerService/WorkerService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0 5 | dotnet-WorkerService-88331894-5452-4449-92B7-95E78F129B94 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/WorkerService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/WorkerService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/quickstart/quickstart.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net6.0 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------