├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── build.yml │ ├── changelog.config │ ├── changelog.yml │ ├── combine-prs.yml │ ├── docs.yml │ ├── dotnet-file-core.yml │ ├── dotnet-file.yml │ ├── includes.yml │ ├── publish.yml │ └── triage.yml ├── .gitignore ├── .netconfig ├── Config.sln ├── Directory.Build.rsp ├── Gemfile ├── _config.yml ├── assets └── css │ └── style.scss ├── changelog.md ├── contributing.md ├── docs ├── api │ ├── .gitignore │ └── index.md ├── docfx.json ├── favicon.ico ├── img │ ├── icon-128.png │ ├── icon-300.png │ ├── icon-32-flip.png │ ├── icon-32.png │ ├── icon-64.png │ ├── icon.ico │ ├── icon.png │ ├── icon.svg │ ├── noun_Command Line_915990.svg │ ├── noun_Settings_3120278.svg │ └── readme.txt ├── index.md ├── logo.svg ├── styles │ ├── main.css │ └── main.js ├── template │ └── partials │ │ ├── head.tmpl.partial │ │ └── scripts.tmpl.partial ├── toc.yml └── who │ ├── dotnet-eventgrid.md │ ├── dotnet-file.md │ ├── dotnet-serve.md │ ├── dotnet-vs.md │ ├── index.md │ ├── reportgenerator.md │ ├── sleet.md │ └── toc.yml ├── license.txt ├── readme.md └── src ├── CommandLine ├── CommandLine.csproj └── CommandLineExtensions.cs ├── Config.Tests ├── .editorconfig ├── CommandLineTests.cs ├── Config.Tests.csproj ├── ConfigDocumentTests.cs ├── ConfigParserTests.cs ├── ConfigReaderTests.cs ├── ConfigTests.cs ├── ConfigurationTests.cs ├── Constants.cs ├── Content │ ├── .netconfig │ ├── ConfigParserTests.txt │ ├── global.netconfig │ ├── local │ │ └── .netconfig │ ├── system.netconfig │ └── web │ │ └── .netconfig ├── FlakyFactAttribute.cs ├── TextRulesTests.cs └── xunit.runner.json ├── Config.Tool ├── Config.Tool.csproj ├── ConfigAction.cs ├── Program.cs ├── Properties │ └── .gitignore ├── ValueKind.cs └── readme.md ├── Config ├── AggregateConfig.cs ├── AssemblyInfo.cs ├── Config.cs ├── Config.csproj ├── ConfigDocument.cs ├── ConfigEntry.cs ├── ConfigExtensions.cs ├── ConfigLevel.cs ├── ConfigReader.cs ├── ConfigSection.cs ├── ConfigSectionExtensions.cs ├── FileConfig.cs ├── Line.cs ├── LineKind.cs ├── Position.cs ├── TextRules.cs ├── TextSpan.cs ├── ValueMatcher.cs └── readme.md ├── Configuration ├── Configuration.csproj ├── DotNetConfigExtensions.cs ├── DotNetConfigProvider.cs ├── DotNetConfigSource.cs └── readme.md ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.props ├── dotnet-config.snk └── icon.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | 15 | # Xml project files 16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 17 | indent_size = 2 18 | 19 | # Xml config files 20 | [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct}] 21 | indent_size = 2 22 | 23 | # YAML files 24 | [*.{yaml,yml}] 25 | indent_size = 2 26 | 27 | # JSON files 28 | [*.json] 29 | indent_size = 2 30 | 31 | # Dotnet code style settings: 32 | [*.{cs,vb}] 33 | # Sort using and Import directives with System.* appearing first 34 | dotnet_sort_system_directives_first = true 35 | # Avoid "this." and "Me." if not necessary 36 | dotnet_style_qualification_for_field = false:suggestion 37 | dotnet_style_qualification_for_property = false:suggestion 38 | dotnet_style_qualification_for_method = false:suggestion 39 | dotnet_style_qualification_for_event = false:suggestion 40 | 41 | # Use language keywords instead of framework type names for type references 42 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 43 | dotnet_style_predefined_type_for_member_access = true:suggestion 44 | 45 | # Suggest more modern language features when available 46 | dotnet_style_object_initializer = true:suggestion 47 | dotnet_style_collection_initializer = true:suggestion 48 | dotnet_style_coalesce_expression = true:suggestion 49 | dotnet_style_null_propagation = true:suggestion 50 | dotnet_style_explicit_tuple_names = true:suggestion 51 | 52 | # CSharp code style settings: 53 | 54 | # IDE0040: Add accessibility modifiers 55 | dotnet_style_require_accessibility_modifiers = omit_if_default:error 56 | 57 | # IDE0040: Add accessibility modifiers 58 | dotnet_diagnostic.IDE0040.severity = error 59 | 60 | [*.cs] 61 | # Top-level files are definitely OK 62 | csharp_using_directive_placement = outside_namespace:silent 63 | csharp_style_namespace_declarations = block_scoped:silent 64 | csharp_prefer_simple_using_statement = true:suggestion 65 | csharp_prefer_braces = true:silent 66 | 67 | # Prefer "var" everywhere 68 | csharp_style_var_for_built_in_types = true:suggestion 69 | csharp_style_var_when_type_is_apparent = true:suggestion 70 | csharp_style_var_elsewhere = true:suggestion 71 | 72 | # Prefer method-like constructs to have an expression-body 73 | csharp_style_expression_bodied_methods = true:none 74 | csharp_style_expression_bodied_constructors = true:none 75 | csharp_style_expression_bodied_operators = true:none 76 | 77 | # Prefer property-like constructs to have an expression-body 78 | csharp_style_expression_bodied_properties = true:none 79 | csharp_style_expression_bodied_indexers = true:none 80 | csharp_style_expression_bodied_accessors = true:none 81 | 82 | # Suggest more modern language features when available 83 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 84 | csharp_style_pattern_matching_over_as_with_null_check = true:error 85 | csharp_style_inlined_variable_declaration = true:suggestion 86 | csharp_style_throw_expression = true:suggestion 87 | csharp_style_conditional_delegate_call = true:suggestion 88 | 89 | # Newline settings 90 | csharp_new_line_before_open_brace = all 91 | csharp_new_line_before_else = true 92 | csharp_new_line_before_catch = true 93 | csharp_new_line_before_finally = true 94 | csharp_new_line_before_members_in_object_initializers = true 95 | csharp_new_line_before_members_in_anonymous_types = true 96 | 97 | # Test settings 98 | [**/*Tests*/**{.cs,.vb}] 99 | # xUnit1013: Public method should be marked as test. Allows using records as test classes 100 | dotnet_diagnostic.xUnit1013.severity = none 101 | 102 | # CS9113: Parameter is unread (usually, ITestOutputHelper) 103 | dotnet_diagnostic.CS9113.severity = none 104 | 105 | # Default severity for analyzer diagnostics with category 'Style' 106 | dotnet_analyzer_diagnostic.category-Style.severity = none 107 | 108 | # VSTHRD200: Use "Async" suffix for async methods 109 | dotnet_diagnostic.VSTHRD200.severity = none 110 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # normalize by default 2 | * text=auto encoding=UTF-8 3 | *.sh text eol=lf 4 | 5 | # These are windows specific files which we may as well ensure are 6 | # always crlf on checkout 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: nuget 7 | directory: / 8 | schedule: 9 | interval: daily 10 | groups: 11 | Azure: 12 | patterns: 13 | - "Azure*" 14 | - "Microsoft.Azure*" 15 | Identity: 16 | patterns: 17 | - "System.IdentityModel*" 18 | - "Microsoft.IdentityModel*" 19 | System: 20 | patterns: 21 | - "System*" 22 | exclude-patterns: 23 | - "System.IdentityModel*" 24 | Extensions: 25 | patterns: 26 | - "Microsoft.Extensions*" 27 | Web: 28 | patterns: 29 | - "Microsoft.AspNetCore*" 30 | Tests: 31 | patterns: 32 | - "Microsoft.NET.Test*" 33 | - "xunit*" 34 | - "coverlet*" 35 | ThisAssembly: 36 | patterns: 37 | - "ThisAssembly*" 38 | ProtoBuf: 39 | patterns: 40 | - "protobuf-*" 41 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - bydesign 5 | - dependencies 6 | - duplicate 7 | - question 8 | - invalid 9 | - wontfix 10 | - need info 11 | - techdebt 12 | authors: 13 | - devlooped-bot 14 | - dependabot 15 | - github-actions 16 | categories: 17 | - title: ✨ Implemented enhancements 18 | labels: 19 | - enhancement 20 | - title: 🐛 Fixed bugs 21 | labels: 22 | - bug 23 | - title: 📝 Documentation updates 24 | labels: 25 | - docs 26 | - documentation 27 | - title: 🔨 Other 28 | labels: 29 | - '*' 30 | exclude: 31 | labels: 32 | - dependencies 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Builds and runs tests in all three supported OSes 2 | # Pushes CI feed if secrets.SLEET_CONNECTION is provided 3 | 4 | name: build 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | configuration: 9 | type: choice 10 | description: Configuration 11 | options: 12 | - Release 13 | - Debug 14 | push: 15 | branches: [ main, dev, 'dev/*', 'feature/*', 'rel/*' ] 16 | paths-ignore: 17 | - changelog.md 18 | - readme.md 19 | pull_request: 20 | types: [opened, synchronize, reopened] 21 | 22 | env: 23 | DOTNET_NOLOGO: true 24 | PackOnBuild: true 25 | GeneratePackageOnBuild: true 26 | VersionPrefix: 42.42.${{ github.run_number }} 27 | VersionLabel: ${{ github.ref }} 28 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 29 | MSBUILDTERMINALLOGGER: auto 30 | Configuration: ${{ github.event.inputs.configuration || 'Release' }} 31 | 32 | defaults: 33 | run: 34 | shell: bash 35 | 36 | jobs: 37 | os-matrix: 38 | runs-on: ubuntu-latest 39 | outputs: 40 | matrix: ${{ steps.lookup.outputs.matrix }} 41 | steps: 42 | - name: 🤘 checkout 43 | uses: actions/checkout@v4 44 | 45 | - name: 🔎 lookup 46 | id: lookup 47 | shell: pwsh 48 | run: | 49 | $path = './.github/workflows/os-matrix.json' 50 | $os = if (test-path $path) { cat $path } else { '["ubuntu-latest"]' } 51 | echo "matrix=$os" >> $env:GITHUB_OUTPUT 52 | 53 | build: 54 | needs: os-matrix 55 | name: build-${{ matrix.os }} 56 | runs-on: ${{ matrix.os }} 57 | strategy: 58 | matrix: 59 | os: ${{ fromJSON(needs.os-matrix.outputs.matrix) }} 60 | steps: 61 | - name: 🤘 checkout 62 | uses: actions/checkout@v4 63 | with: 64 | submodules: recursive 65 | fetch-depth: 0 66 | 67 | - name: ⚙ dotnet 68 | uses: actions/setup-dotnet@v4 69 | with: 70 | dotnet-version: | 71 | 6.x 72 | 8.x 73 | 9.x 74 | 75 | - name: 🙏 build 76 | run: dotnet build -m:1 -bl:build.binlog 77 | 78 | - name: 🧪 test 79 | run: | 80 | dotnet tool update -g dotnet-retest 81 | dotnet retest -- --no-build 82 | 83 | - name: 🐛 logs 84 | uses: actions/upload-artifact@v4 85 | if: runner.debug && always() 86 | with: 87 | name: logs 88 | path: '*.binlog' 89 | 90 | - name: 🚀 sleet 91 | env: 92 | SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} 93 | if: env.SLEET_CONNECTION != '' 94 | run: | 95 | dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r) 96 | sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" 97 | 98 | dotnet-format: 99 | runs-on: ubuntu-latest 100 | steps: 101 | - name: 🤘 checkout 102 | uses: actions/checkout@v4 103 | with: 104 | submodules: recursive 105 | fetch-depth: 0 106 | 107 | - name: ✓ ensure format 108 | run: | 109 | dotnet format whitespace --verify-no-changes -v:diag --exclude ~/.nuget 110 | dotnet format style --verify-no-changes -v:diag --exclude ~/.nuget 111 | -------------------------------------------------------------------------------- /.github/workflows/changelog.config: -------------------------------------------------------------------------------- 1 | usernames-as-github-logins=true 2 | issues_wo_labels=true 3 | pr_wo_labels=true 4 | exclude-labels=bydesign,dependencies,duplicate,discussion,question,invalid,wontfix,need info,docs 5 | enhancement-label=:sparkles: Implemented enhancements: 6 | bugs-label=:bug: Fixed bugs: 7 | issues-label=:hammer: Other: 8 | pr-label=:twisted_rightwards_arrows: Merged: 9 | unreleased=false 10 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: changelog 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 🤖 defaults 12 | uses: devlooped/actions-bot@v1 13 | with: 14 | name: ${{ secrets.BOT_NAME }} 15 | email: ${{ secrets.BOT_EMAIL }} 16 | gh_token: ${{ secrets.GH_TOKEN }} 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: 🤘 checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | ref: main 24 | token: ${{ env.GH_TOKEN }} 25 | 26 | - name: ⚙ ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.0.3 30 | 31 | - name: ⚙ changelog 32 | run: | 33 | gem install github_changelog_generator 34 | github_changelog_generator --user ${GITHUB_REPOSITORY%/*} --project ${GITHUB_REPOSITORY##*/} --token $GH_TOKEN --o changelog.md --config-file .github/workflows/changelog.config 35 | 36 | - name: 🚀 changelog 37 | run: | 38 | git add changelog.md 39 | (git commit -m "🖉 Update changelog with ${GITHUB_REF#refs/*/}" && git push) || echo "Done" -------------------------------------------------------------------------------- /.github/workflows/combine-prs.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/hrvey/combine-prs-workflow 2 | # Tweaks: regex support for branch 3 | 4 | name: '⛙ combine-prs' 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | branchExpression: 10 | description: 'Regular expression to match against PR branches to find combinable PRs' 11 | required: true 12 | default: 'dependabot' 13 | mustBeGreen: 14 | description: 'Only combine PRs that are green (status is success)' 15 | required: true 16 | default: true 17 | combineTitle: 18 | description: 'Title of the combined PR' 19 | required: true 20 | default: '⬆️ Bump dependencies' 21 | combineBranchName: 22 | description: 'Name of the branch to combine PRs into' 23 | required: true 24 | default: 'combine-prs' 25 | ignoreLabel: 26 | description: 'Exclude PRs with this label' 27 | required: true 28 | default: 'nocombine' 29 | 30 | jobs: 31 | combine-prs: 32 | name: ${{ github.event.inputs.combineBranchName }} 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/github-script@v6 36 | with: 37 | github-token: ${{secrets.GITHUB_TOKEN}} 38 | script: | 39 | const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { 40 | owner: context.repo.owner, 41 | repo: context.repo.repo 42 | }); 43 | const branchRegExp = new RegExp(`${{github.event.inputs.branchExpression}}`); 44 | let branchesAndPRStrings = []; 45 | let baseBranch = null; 46 | let baseBranchSHA = null; 47 | for (const pull of pulls) { 48 | const branch = pull['head']['ref']; 49 | console.log('Pull for branch: ' + branch); 50 | if (branchRegExp.test(branch)) { 51 | console.log('Branch matched: ' + branch); 52 | let statusOK = true; 53 | if(${{ github.event.inputs.mustBeGreen }}) { 54 | console.log('Checking green status: ' + branch); 55 | const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) { 56 | repository(owner: $owner, name: $repo) { 57 | pullRequest(number:$pull_number) { 58 | commits(last: 1) { 59 | nodes { 60 | commit { 61 | statusCheckRollup { 62 | state 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }` 70 | const vars = { 71 | owner: context.repo.owner, 72 | repo: context.repo.repo, 73 | pull_number: pull['number'] 74 | }; 75 | const result = await github.graphql(stateQuery, vars); 76 | const [{ commit }] = result.repository.pullRequest.commits.nodes; 77 | const state = commit.statusCheckRollup.state 78 | console.log('Validating status: ' + state); 79 | if(state != 'SUCCESS') { 80 | console.log('Discarding ' + branch + ' with status ' + state); 81 | statusOK = false; 82 | } 83 | } 84 | console.log('Checking labels: ' + branch); 85 | const labels = pull['labels']; 86 | for(const label of labels) { 87 | const labelName = label['name']; 88 | console.log('Checking label: ' + labelName); 89 | if(labelName == '${{ github.event.inputs.ignoreLabel }}') { 90 | console.log('Discarding ' + branch + ' with label ' + labelName); 91 | statusOK = false; 92 | } 93 | } 94 | if (statusOK) { 95 | console.log('Adding branch to array: ' + branch); 96 | const prString = '#' + pull['number'] + ' ' + pull['title']; 97 | branchesAndPRStrings.push({ branch, prString }); 98 | baseBranch = pull['base']['ref']; 99 | baseBranchSHA = pull['base']['sha']; 100 | } 101 | } 102 | } 103 | if (branchesAndPRStrings.length == 0) { 104 | core.setFailed('No PRs/branches matched criteria'); 105 | return; 106 | } 107 | if (branchesAndPRStrings.length == 1) { 108 | core.setFailed('Only one PR/branch matched criteria'); 109 | return; 110 | } 111 | 112 | try { 113 | await github.rest.git.createRef({ 114 | owner: context.repo.owner, 115 | repo: context.repo.repo, 116 | ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}', 117 | sha: baseBranchSHA 118 | }); 119 | } catch (error) { 120 | console.log(error); 121 | core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); 122 | return; 123 | } 124 | 125 | let combinedPRs = []; 126 | let mergeFailedPRs = []; 127 | for(const { branch, prString } of branchesAndPRStrings) { 128 | try { 129 | await github.rest.repos.merge({ 130 | owner: context.repo.owner, 131 | repo: context.repo.repo, 132 | base: '${{ github.event.inputs.combineBranchName }}', 133 | head: branch, 134 | }); 135 | console.log('Merged branch ' + branch); 136 | combinedPRs.push(prString); 137 | } catch (error) { 138 | console.log('Failed to merge branch ' + branch); 139 | mergeFailedPRs.push(prString); 140 | } 141 | } 142 | 143 | console.log('Creating combined PR'); 144 | const combinedPRsString = combinedPRs.join('\n'); 145 | let body = '⛙ Combined PRs:\n' + combinedPRsString; 146 | if(mergeFailedPRs.length > 0) { 147 | const mergeFailedPRsString = mergeFailedPRs.join('\n'); 148 | body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString 149 | } 150 | await github.rest.pulls.create({ 151 | owner: context.repo.owner, 152 | repo: context.repo.repo, 153 | title: '⛙ ${{github.event.inputs.combineTitle}}', 154 | head: '${{ github.event.inputs.combineBranchName }}', 155 | base: baseBranch, 156 | body: body 157 | }); 158 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | docs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v2 13 | 14 | - name: 🙏 build 15 | run: dotnet build -p:BuildDocFx=true -p:DOCFX_SOURCE_BRANCH_NAME=main 16 | 17 | - name: 🚀 deploy 18 | uses: JamesIves/github-pages-deploy-action@releases/v3 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | BRANCH: gh-pages 22 | FOLDER: _site -------------------------------------------------------------------------------- /.github/workflows/dotnet-file-core.yml: -------------------------------------------------------------------------------- 1 | # Synchronizes .netconfig-configured files with dotnet-file 2 | name: dotnet-file-core 3 | on: 4 | workflow_call: 5 | 6 | env: 7 | DOTNET_NOLOGO: true 8 | 9 | defaults: 10 | run: 11 | shell: pwsh 12 | 13 | jobs: 14 | sync: 15 | runs-on: ubuntu-latest 16 | continue-on-error: true 17 | steps: 18 | - name: 🤖 defaults 19 | uses: devlooped/actions-bot@v1 20 | with: 21 | name: ${{ secrets.BOT_NAME }} 22 | email: ${{ secrets.BOT_EMAIL }} 23 | gh_token: ${{ secrets.GH_TOKEN }} 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: 🤘 checkout 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | ref: main 31 | token: ${{ env.GH_TOKEN }} 32 | 33 | - name: ⌛ rate 34 | if: github.event_name != 'workflow_dispatch' 35 | run: | 36 | # add random sleep since we run on fixed schedule 37 | sleep (get-random -max 60) 38 | # get currently authenticated user rate limit info 39 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 40 | # if we don't have at least 100 requests left, wait until reset 41 | if ($rate.remaining -lt 10) { 42 | $wait = ($rate.reset - (Get-Date (Get-Date).ToUniversalTime() -UFormat %s)) 43 | echo "Rate limit remaining is $($rate.remaining), waiting for $($wait / 1000) seconds to reset" 44 | sleep $wait 45 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 46 | echo "Rate limit has reset to $($rate.remaining) requests" 47 | } 48 | 49 | - name: 🔄 sync 50 | run: | 51 | dotnet tool update -g dotnet-gcm 52 | # store credentials in plaintext for linux compat 53 | git config --local credential.credentialStore plaintext 54 | dotnet gcm store --protocol=https --host=github.com --username=$env:GITHUB_ACTOR --password=$env:GH_TOKEN 55 | gh auth status 56 | 57 | dotnet tool update -g dotnet-file 58 | $changelog = "$([System.IO.Path]::GetTempPath())dotnet-file.md" 59 | dotnet file sync -c:$changelog 60 | if (test-path $changelog) { 61 | echo 'CHANGES<> $env:GITHUB_ENV 62 | cat $changelog >> $env:GITHUB_ENV 63 | echo 'EOF' >> $env:GITHUB_ENV 64 | cat $changelog 65 | } else { 66 | echo 'No changelog was generated' 67 | } 68 | 69 | - name: +Mᐁ includes 70 | uses: devlooped/actions-includes@v1 71 | with: 72 | validate: false 73 | 74 | - name: ✍ pull request 75 | uses: peter-evans/create-pull-request@v7 76 | with: 77 | base: main 78 | branch: dotnet-file-sync 79 | delete-branch: true 80 | labels: dependencies 81 | author: ${{ env.BOT_AUTHOR }} 82 | committer: ${{ env.BOT_AUTHOR }} 83 | commit-message: ⬆️ Bump files with dotnet-file sync 84 | 85 | ${{ env.CHANGES }} 86 | title: "⬆️ Bump files with dotnet-file sync" 87 | body: ${{ env.CHANGES }} 88 | token: ${{ env.GH_TOKEN }} 89 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-file.yml: -------------------------------------------------------------------------------- 1 | # Synchronizes .netconfig-configured files with dotnet-file 2 | name: dotnet-file 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: [ 'dotnet-file' ] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | 13 | jobs: 14 | run: 15 | uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main 16 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/includes.yml: -------------------------------------------------------------------------------- 1 | name: +Mᐁ includes 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '**.md' 9 | - '!changelog.md' 10 | 11 | jobs: 12 | includes: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | steps: 18 | - name: 🤖 defaults 19 | uses: devlooped/actions-bot@v1 20 | with: 21 | name: ${{ secrets.BOT_NAME }} 22 | email: ${{ secrets.BOT_EMAIL }} 23 | gh_token: ${{ secrets.GH_TOKEN }} 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: 🤘 checkout 27 | uses: actions/checkout@v4 28 | with: 29 | token: ${{ env.GH_TOKEN }} 30 | 31 | - name: +Mᐁ includes 32 | uses: devlooped/actions-includes@v1 33 | 34 | - name: ✍ pull request 35 | uses: peter-evans/create-pull-request@v6 36 | with: 37 | add-paths: '**.md' 38 | base: main 39 | branch: markdown-includes 40 | delete-branch: true 41 | labels: docs 42 | author: ${{ env.BOT_AUTHOR }} 43 | committer: ${{ env.BOT_AUTHOR }} 44 | commit-message: +Mᐁ includes 45 | title: +Mᐁ includes 46 | body: +Mᐁ includes 47 | token: ${{ env.GH_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Builds a final release version and pushes to nuget.org 2 | # whenever a release is published. 3 | # Requires: secrets.NUGET_API_KEY 4 | 5 | name: publish 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | Configuration: Release 13 | PackOnBuild: true 14 | GeneratePackageOnBuild: true 15 | VersionLabel: ${{ github.ref }} 16 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 17 | MSBUILDTERMINALLOGGER: auto 18 | 19 | jobs: 20 | publish: 21 | runs-on: ${{ vars.PUBLISH_AGENT || 'ubuntu-latest' }} 22 | steps: 23 | - name: 🤘 checkout 24 | uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | fetch-depth: 0 28 | 29 | - name: ⚙ dotnet 30 | uses: actions/setup-dotnet@v4 31 | with: 32 | dotnet-version: | 33 | 6.x 34 | 8.x 35 | 9.x 36 | 37 | - name: 🙏 build 38 | run: dotnet build -m:1 -bl:build.binlog 39 | 40 | - name: 🧪 test 41 | run: | 42 | dotnet tool update -g dotnet-retest 43 | dotnet retest -- --no-build 44 | 45 | - name: 🐛 logs 46 | uses: actions/upload-artifact@v4 47 | if: runner.debug && always() 48 | with: 49 | name: logs 50 | path: '*.binlog' 51 | 52 | - name: 🚀 nuget 53 | env: 54 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 55 | if: ${{ env.NUGET_API_KEY != '' && github.event.action != 'prereleased' }} 56 | working-directory: bin 57 | run: dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate 58 | 59 | - name: 🚀 sleet 60 | env: 61 | SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }} 62 | if: env.SLEET_CONNECTION != '' 63 | run: | 64 | dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r) 65 | sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found" 66 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: 'triage' 2 | on: 3 | schedule: 4 | - cron: '42 0 * * *' 5 | 6 | workflow_dispatch: 7 | # Manual triggering through the GitHub UI, API, or CLI 8 | inputs: 9 | daysBeforeClose: 10 | description: "Days before closing stale or need info issues" 11 | required: true 12 | default: "30" 13 | daysBeforeStale: 14 | description: "Days before labeling stale" 15 | required: true 16 | default: "180" 17 | daysSinceClose: 18 | description: "Days since close to lock" 19 | required: true 20 | default: "30" 21 | daysSinceUpdate: 22 | description: "Days since update to lock" 23 | required: true 24 | default: "30" 25 | 26 | permissions: 27 | actions: write # For managing the operation state cache 28 | issues: write 29 | contents: read 30 | 31 | jobs: 32 | stale: 33 | # Do not run on forks 34 | if: github.repository_owner == 'devlooped' 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: ⌛ rate 38 | shell: pwsh 39 | if: github.event_name != 'workflow_dispatch' 40 | env: 41 | GH_TOKEN: ${{ secrets.DEVLOOPED_TOKEN }} 42 | run: | 43 | # add random sleep since we run on fixed schedule 44 | $wait = get-random -max 180 45 | echo "Waiting random $wait seconds to start" 46 | sleep $wait 47 | # get currently authenticated user rate limit info 48 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 49 | # if we don't have at least 100 requests left, wait until reset 50 | if ($rate.remaining -lt 100) { 51 | $wait = ($rate.reset - (Get-Date (Get-Date).ToUniversalTime() -UFormat %s)) 52 | echo "Rate limit remaining is $($rate.remaining), waiting for $($wait / 1000) seconds to reset" 53 | sleep $wait 54 | $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate 55 | echo "Rate limit has reset to $($rate.remaining) requests" 56 | } 57 | 58 | - name: ✏️ stale labeler 59 | # pending merge: https://github.com/actions/stale/pull/1176 60 | uses: kzu/stale@c8450312ba97b204bf37545cb249742144d6ca69 61 | with: 62 | ascending: true # Process the oldest issues first 63 | stale-issue-label: 'stale' 64 | stale-issue-message: | 65 | Due to lack of recent activity, this issue has been labeled as 'stale'. 66 | It will be closed if no further activity occurs within ${{ fromJson(inputs.daysBeforeClose || 30 ) }} more days. 67 | Any new comment will remove the label. 68 | close-issue-message: | 69 | This issue will now be closed since it has been labeled 'stale' without activity for ${{ fromJson(inputs.daysBeforeClose || 30 ) }} days. 70 | days-before-stale: ${{ fromJson(inputs.daysBeforeStale || 180) }} 71 | days-before-close: ${{ fromJson(inputs.daysBeforeClose || 30 ) }} 72 | days-before-pr-close: -1 # Do not close PRs labeled as 'stale' 73 | exempt-all-milestones: true 74 | exempt-all-assignees: true 75 | exempt-issue-labels: priority,sponsor,backed 76 | exempt-authors: kzu 77 | 78 | - name: 🤘 checkout actions 79 | uses: actions/checkout@v4 80 | with: 81 | repository: 'microsoft/vscode-github-triage-actions' 82 | ref: v42 83 | 84 | - name: ⚙ install actions 85 | run: npm install --production 86 | 87 | - name: 🔒 issues locker 88 | uses: ./locker 89 | with: 90 | token: ${{ secrets.DEVLOOPED_TOKEN }} 91 | ignoredLabel: priority 92 | daysSinceClose: ${{ fromJson(inputs.daysSinceClose || 30) }} 93 | daysSinceUpdate: ${{ fromJson(inputs.daysSinceUpdate || 30) }} 94 | 95 | - name: 🔒 need info closer 96 | uses: ./needs-more-info-closer 97 | with: 98 | token: ${{ secrets.DEVLOOPED_TOKEN }} 99 | label: 'need info' 100 | closeDays: ${{ fromJson(inputs.daysBeforeClose || 30) }} 101 | closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity.\n\nHappy Coding!" 102 | pingDays: 80 103 | pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | artifacts 4 | pack 5 | TestResults 6 | results 7 | BenchmarkDotNet.Artifacts 8 | /app 9 | .vs 10 | .vscode 11 | .genaiscript 12 | .idea 13 | local.settings.json 14 | 15 | *.suo 16 | *.sdf 17 | *.userprefs 18 | *.user 19 | *.nupkg 20 | *.metaproj 21 | *.tmp 22 | *.log 23 | *.cache 24 | *.binlog 25 | *.zip 26 | __azurite*.* 27 | __*__ 28 | 29 | .nuget 30 | *.lock.json 31 | *.nuget.props 32 | *.nuget.targets 33 | 34 | node_modules 35 | _site 36 | .jekyll-metadata 37 | .jekyll-cache 38 | .sass-cache 39 | Gemfile.lock 40 | package-lock.json 41 | -------------------------------------------------------------------------------- /.netconfig: -------------------------------------------------------------------------------- 1 | [file] 2 | url = https://github.com/kzu/oss 3 | url = https://github.com/clarius/pages 4 | [file ".netconfig"] 5 | url = https://github.com/devlooped/oss/blob/main/.netconfig 6 | skip 7 | [file "src/kzu.snk"] 8 | url = https://github.com/devlooped/oss/blob/main/src/kzu.snk 9 | skip 10 | [file "readme.md"] 11 | url = https://github.com/devlooped/oss/blob/main/readme.md 12 | skip 13 | [file ".editorconfig"] 14 | url = https://github.com/devlooped/oss/blob/main/.editorconfig 15 | sha = c779d3d4e468358106dea03e93ba2cd35bb01ecb 16 | etag = 7298c6450967975a8782b5c74f3071e1910fc59686e48f9c9d5cd7c68213cf59 17 | weak 18 | [file ".gitattributes"] 19 | url = https://github.com/devlooped/oss/blob/main/.gitattributes 20 | sha = 5f92a68e302bae675b394ef343114139c075993e 21 | etag = 338ba6d92c8d1774363396739c2be4257bfc58026f4b0fe92cb0ae4460e1eff7 22 | weak 23 | [file ".github/dependabot.yml"] 24 | url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml 25 | sha = 49661dbf0720cde93eb5569be7523b5912351560 26 | etag = c147ea2f3431ca0338c315c4a45b56ee233c4d30f8d6ab698d0e1980a257fd6a 27 | weak 28 | [file ".github/workflows/build.yml"] 29 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml 30 | sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a 31 | etag = 0a4b3f0a875cd8c9434742b4046558aecf610d3fa3d490cfd2099266e95e9195 32 | weak 33 | [file ".gitignore"] 34 | url = https://github.com/devlooped/oss/blob/main/.gitignore 35 | sha = e0be248fff1d39133345283b8227372b36574b75 36 | etag = c449ec6f76803e1891357ca2b8b4fcb5b2e5deeff8311622fd92ca9fbf1e6575 37 | weak 38 | [file "Directory.Build.rsp"] 39 | url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp 40 | sha = 0f7f7f7e8a29de9b535676f75fe7c67e629a5e8c 41 | etag = 0ccae83fc51f400bfd7058170bfec7aba11455e24a46a0d7e6a358da6486e255 42 | weak 43 | [file "_config.yml"] 44 | url = https://github.com/devlooped/oss/blob/main/_config.yml 45 | sha = fa83a5161ba52bc5d510ce0ba75ee0b1f8d4bc63 46 | etag = 9139148f845adf503fd3c3c140eb64421fc476a1f9c027fc50825c0efb05f557 47 | weak 48 | [file "license.txt"] 49 | url = https://github.com/devlooped/oss/blob/main/license.txt 50 | sha = 0683ee777d7d878d4bf013d7deea352685135a05 51 | etag = 2c6335b37e4ae05eea7c01f5d0c9d82b49c488f868a8b5ba7bff7c6ff01f3994 52 | weak 53 | [file "SponsorLink.sln"] 54 | url = https://github.com/devlooped/oss/blob/main/SponsorLink.sln 55 | skip 56 | [file "src/Directory.Build.props"] 57 | url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props 58 | sha = 2fff747a9673b499c99f2da183cdd5263fdc9333 59 | etag = 0fccddf04f282fe98122ab2610dc2972c205a521254559bf013655c6271b0017 60 | weak 61 | [file "src/Directory.Build.targets"] 62 | url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets 63 | sha = a8b208093599263b7f2d1fe3854634c588ea5199 64 | etag = 19087699f05396205e6b050d999a43b175bd242f6e8fac86f6df936310178b03 65 | weak 66 | [file ".github/workflows/changelog.yml"] 67 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml 68 | sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9 69 | etag = ad1efa56d6024ee1add2bcda81a7e4e38d0e9069473c6ff70374d5ce06af1f5a 70 | weak 71 | [file ".github/workflows/publish.yml"] 72 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml 73 | sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a 74 | etag = 2f64f75ad01f735fd05290370fb8a826111ac8dd7e74ce04226bb627a54a62ba 75 | weak 76 | [file "assets/css/style.scss"] 77 | url = https://github.com/devlooped/oss/blob/main/assets/css/style.scss 78 | sha = 9db26e2710b084d219d6355339d822f159bf5780 79 | etag = f710d8919abfd5a8d00050b74ba7d0bb05c6d02e40842a3012eb96555c208504 80 | weak 81 | [file ".github/workflows/pages.yml"] 82 | url = https://github.com/clarius/pages/blob/main/.github/workflows/pages.yml 83 | skip 84 | [file "Gemfile"] 85 | url = https://github.com/clarius/pages/blob/main/Gemfile 86 | sha = 90fa16ed0e7300a78a38ee1d23c34a7e875aab27 87 | etag = 3dd7febc8ae6760f19abfe787711f469c288cd803a6f1c545edec34264d48e71 88 | weak 89 | [file ".github/workflows/dotnet-file.yml"] 90 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml 91 | sha = 59aaf432369b5ea597831d4feec5a6ac4024c2e3 92 | etag = 1374e3f8c9b7af69c443605c03f7262300dcb7d783738d9eb9fe84268ed2d10c 93 | weak 94 | [file "src/nuget.config"] 95 | url = https://github.com/devlooped/oss/blob/main/src/nuget.config 96 | skip 97 | [file ".github/workflows/includes.yml"] 98 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml 99 | sha = 85829f2510f335f4a411867f3dbaaa116c3ab3de 100 | etag = 086f6b6316cc6ea7089c0dcc6980be519e6ed6e6201e65042ef41b82634ec0ee 101 | weak 102 | [file ".github/release.yml"] 103 | url = https://github.com/devlooped/oss/blob/main/.github/release.yml 104 | sha = 0c23e24704625cf75b2cb1fdc566cef7e20af313 105 | etag = 310df162242c95ed19ed12e3c96a65f77e558b46dced676ad5255eb12caafe75 106 | weak 107 | [file ".github/workflows/changelog.config"] 108 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.config 109 | sha = 08d83cb510732f861416760d37702f9f55bd7f9e 110 | etag = 556a28914eeeae78ca924b1105726cdaa211af365671831887aec81f5f4301b4 111 | weak 112 | [file ".github/workflows/combine-prs.yml"] 113 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/combine-prs.yml 114 | sha = c1610886eba42cb250e3894aed40c0a258cd383d 115 | etag = 598ee294649a44d4c5d5934416c01183597d08aa7db7938453fd2bbf52a4e64d 116 | weak 117 | [file "SponsorLink.sln"] 118 | url = https://github.com/devlooped/oss/blob/main/SponsorLink.sln 119 | sha = e732f6a2c44a2f7940a1868a92cd66523f74ed24 120 | etag = 5f4cdd4e73a4ac03a41b6f11ec5c576f7a0e7ecef95fcdae04006abc39ea729b 121 | weak 122 | [file ".github/workflows/dotnet-file-core.yml"] 123 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file-core.yml 124 | sha = 875284ba5d565f529aba2f5d24ab8ed27c1d1c79 125 | etag = 8de1d974bf73b1945b5c8be684c3a0b7364114a0d795c9d68837aed9b3aff331 126 | weak 127 | [file ".github/workflows/triage.yml"] 128 | url = https://github.com/devlooped/oss/blob/main/.github/workflows/triage.yml 129 | sha = 33000c0c4ab4eb4e0e142fa54515b811a189d55c 130 | etag = 013a47739e348f06891f37c45164478cca149854e6cd5c5158e6f073f852b61a 131 | weak 132 | -------------------------------------------------------------------------------- /Config.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8722AC17-A90B-406B-850A-C1B8EB89056D}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | src\Directory.props = src\Directory.props 10 | readme.md = readme.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Config", "src\Config\Config.csproj", "{D746131A-1AC4-476F-B075-0534069DCEA0}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Config.Tool", "src\Config.Tool\Config.Tool.csproj", "{8CB7B794-E8B4-4899-AC6C-E45AC254D17E}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Config.Tests", "src\Config.Tests\Config.Tests.csproj", "{B1776FB0-DF19-421E-8369-6D16D482A8BD}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{BC7430A7-57FD-47C9-81CC-D0027FE0C4B5}" 20 | ProjectSection(SolutionItems) = preProject 21 | .github\workflows\build.yml = .github\workflows\build.yml 22 | .github\workflows\docs.yml = .github\workflows\docs.yml 23 | .github\workflows\release.yml = .github\workflows\release.yml 24 | .github\workflows\tag.yml = .github\workflows\tag.yml 25 | EndProjectSection 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{BDFC791F-8AA0-481F-ADDC-F6B8EFBD00F1}" 28 | ProjectSection(SolutionItems) = preProject 29 | docs\docfx.json = docs\docfx.json 30 | docs\favicon.ico = docs\favicon.ico 31 | docs\index.md = docs\index.md 32 | docs\logo.svg = docs\logo.svg 33 | docs\toc.yml = docs\toc.yml 34 | EndProjectSection 35 | EndProject 36 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{5801C1E5-6570-496C-A0F5-A4C6801C6AF3}" 37 | ProjectSection(SolutionItems) = preProject 38 | docs\api\.gitignore = docs\api\.gitignore 39 | docs\api\index.md = docs\api\index.md 40 | EndProjectSection 41 | EndProject 42 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "styles", "styles", "{9062F290-96B8-4B15-A4E8-423424BBD3A9}" 43 | ProjectSection(SolutionItems) = preProject 44 | docs\styles\main.css = docs\styles\main.css 45 | docs\styles\main.js = docs\styles\main.js 46 | EndProjectSection 47 | EndProject 48 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{E07D933F-2E4F-4770-9ED1-93C1AB0F9B7D}" 49 | EndProject 50 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "who", "who", "{A6A9B3BF-7F2A-4A47-AB4B-1ADEB80A45F1}" 51 | ProjectSection(SolutionItems) = preProject 52 | docs\who\dotnet-eventgrid.md = docs\who\dotnet-eventgrid.md 53 | docs\who\dotnet-file.md = docs\who\dotnet-file.md 54 | docs\who\dotnet-serve.md = docs\who\dotnet-serve.md 55 | docs\who\dotnet-vs.md = docs\who\dotnet-vs.md 56 | docs\who\index.md = docs\who\index.md 57 | docs\who\reportgenerator.md = docs\who\reportgenerator.md 58 | docs\who\sleet.md = docs\who\sleet.md 59 | docs\who\toc.yml = docs\who\toc.yml 60 | EndProjectSection 61 | EndProject 62 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "partials", "partials", "{61FC2991-D7D8-4CD9-B399-82AA2CDC482D}" 63 | ProjectSection(SolutionItems) = preProject 64 | docs\template\partials\head.tmpl.partial = docs\template\partials\head.tmpl.partial 65 | docs\template\partials\scripts.tmpl.partial = docs\template\partials\scripts.tmpl.partial 66 | EndProjectSection 67 | EndProject 68 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Configuration", "src\Configuration\Configuration.csproj", "{27257308-72DF-4C4E-8B22-E7CD08984ABE}" 69 | EndProject 70 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "src\CommandLine\CommandLine.csproj", "{545759D0-0144-4CEA-8AFF-73879FEFA918}" 71 | EndProject 72 | Global 73 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 74 | Debug|Any CPU = Debug|Any CPU 75 | Release|Any CPU = Release|Any CPU 76 | EndGlobalSection 77 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 78 | {D746131A-1AC4-476F-B075-0534069DCEA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 79 | {D746131A-1AC4-476F-B075-0534069DCEA0}.Debug|Any CPU.Build.0 = Debug|Any CPU 80 | {D746131A-1AC4-476F-B075-0534069DCEA0}.Release|Any CPU.ActiveCfg = Release|Any CPU 81 | {D746131A-1AC4-476F-B075-0534069DCEA0}.Release|Any CPU.Build.0 = Release|Any CPU 82 | {8CB7B794-E8B4-4899-AC6C-E45AC254D17E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 83 | {8CB7B794-E8B4-4899-AC6C-E45AC254D17E}.Debug|Any CPU.Build.0 = Debug|Any CPU 84 | {8CB7B794-E8B4-4899-AC6C-E45AC254D17E}.Release|Any CPU.ActiveCfg = Release|Any CPU 85 | {8CB7B794-E8B4-4899-AC6C-E45AC254D17E}.Release|Any CPU.Build.0 = Release|Any CPU 86 | {B1776FB0-DF19-421E-8369-6D16D482A8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 87 | {B1776FB0-DF19-421E-8369-6D16D482A8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 88 | {B1776FB0-DF19-421E-8369-6D16D482A8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 89 | {B1776FB0-DF19-421E-8369-6D16D482A8BD}.Release|Any CPU.Build.0 = Release|Any CPU 90 | {27257308-72DF-4C4E-8B22-E7CD08984ABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 91 | {27257308-72DF-4C4E-8B22-E7CD08984ABE}.Debug|Any CPU.Build.0 = Debug|Any CPU 92 | {27257308-72DF-4C4E-8B22-E7CD08984ABE}.Release|Any CPU.ActiveCfg = Release|Any CPU 93 | {27257308-72DF-4C4E-8B22-E7CD08984ABE}.Release|Any CPU.Build.0 = Release|Any CPU 94 | {545759D0-0144-4CEA-8AFF-73879FEFA918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 95 | {545759D0-0144-4CEA-8AFF-73879FEFA918}.Debug|Any CPU.Build.0 = Debug|Any CPU 96 | {545759D0-0144-4CEA-8AFF-73879FEFA918}.Release|Any CPU.ActiveCfg = Release|Any CPU 97 | {545759D0-0144-4CEA-8AFF-73879FEFA918}.Release|Any CPU.Build.0 = Release|Any CPU 98 | EndGlobalSection 99 | GlobalSection(SolutionProperties) = preSolution 100 | HideSolutionNode = FALSE 101 | EndGlobalSection 102 | GlobalSection(NestedProjects) = preSolution 103 | {BC7430A7-57FD-47C9-81CC-D0027FE0C4B5} = {8722AC17-A90B-406B-850A-C1B8EB89056D} 104 | {5801C1E5-6570-496C-A0F5-A4C6801C6AF3} = {BDFC791F-8AA0-481F-ADDC-F6B8EFBD00F1} 105 | {9062F290-96B8-4B15-A4E8-423424BBD3A9} = {BDFC791F-8AA0-481F-ADDC-F6B8EFBD00F1} 106 | {E07D933F-2E4F-4770-9ED1-93C1AB0F9B7D} = {BDFC791F-8AA0-481F-ADDC-F6B8EFBD00F1} 107 | {A6A9B3BF-7F2A-4A47-AB4B-1ADEB80A45F1} = {BDFC791F-8AA0-481F-ADDC-F6B8EFBD00F1} 108 | {61FC2991-D7D8-4CD9-B399-82AA2CDC482D} = {E07D933F-2E4F-4770-9ED1-93C1AB0F9B7D} 109 | EndGlobalSection 110 | GlobalSection(ExtensibilityGlobals) = postSolution 111 | SolutionGuid = {19B804F2-469A-40CA-9387-C9C1F7D231E8} 112 | EndGlobalSection 113 | EndGlobal 114 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | # See https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files 2 | -nr:false 3 | -m:1 4 | -v:m 5 | -clp:Summary;ForceNoAlign 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'github-pages', '~> 231', group: :jekyll_plugins 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | 3 | exclude: [ 'src/', '*.sln', 'Gemfile*', '*.rsp' ] -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-slate"; 5 | 6 | .inner { 7 | max-width: 960px; 8 | } 9 | 10 | pre, code { 11 | background-color: unset; 12 | font-size: unset; 13 | } 14 | 15 | code { 16 | font-size: 0.80em; 17 | } 18 | 19 | h1 > img { 20 | border: unset; 21 | box-shadow: unset; 22 | vertical-align: middle; 23 | -moz-box-shadow: unset; 24 | -o-box-shadow: unset; 25 | -ms-box-shadow: unset; 26 | } 27 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.2.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.2.0) (2024-07-07) 4 | 5 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.1.1...v1.2.0) 6 | 7 | :sparkles: Implemented enhancements: 8 | 9 | - Drop immutability which doesn't add any value [\#155](https://github.com/dotnetconfig/dotnet-config/pull/155) (@kzu) 10 | 11 | :hammer: Other: 12 | 13 | - Tabs vs spaces [\#122](https://github.com/dotnetconfig/dotnet-config/issues/122) 14 | 15 | :twisted_rightwards_arrows: Merged: 16 | 17 | - Add test that ensures current tab-based behavior [\#153](https://github.com/dotnetconfig/dotnet-config/pull/153) (@kzu) 18 | 19 | ## [v1.1.1](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.1) (2024-06-25) 20 | 21 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.1.0...v1.1.1) 22 | 23 | :sparkles: Implemented enhancements: 24 | 25 | - Improve docs, add package readme for extensions [\#152](https://github.com/dotnetconfig/dotnet-config/pull/152) (@kzu) 26 | - Make options and note high compat level with git config [\#151](https://github.com/dotnetconfig/dotnet-config/pull/151) (@kzu) 27 | 28 | :bug: Fixed bugs: 29 | 30 | - DotNetConfig.CommandLine not compatible with latest prerelease of System.Commandline [\#105](https://github.com/dotnetconfig/dotnet-config/issues/105) 31 | 32 | ## [v1.1.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.1.0) (2024-06-25) 33 | 34 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.6...v1.1.0) 35 | 36 | :sparkles: Implemented enhancements: 37 | 38 | - Use nugetizer for packing [\#150](https://github.com/dotnetconfig/dotnet-config/pull/150) (@kzu) 39 | - Bump to .net6/8 for the CLI [\#148](https://github.com/dotnetconfig/dotnet-config/pull/148) (@kzu) 40 | 41 | :hammer: Other: 42 | 43 | - .Net 5 reached EOL, please upgrade this tool to use .Net 6 or 8 [\#146](https://github.com/dotnetconfig/dotnet-config/issues/146) 44 | - Can't save empty/blank values [\#145](https://github.com/dotnetconfig/dotnet-config/issues/145) 45 | 46 | :twisted_rightwards_arrows: Merged: 47 | 48 | - Add how to work with array of complex objects [\#98](https://github.com/dotnetconfig/dotnet-config/pull/98) (@PadreSVK) 49 | 50 | ## [v1.0.6](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.6) (2021-07-30) 51 | 52 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.5...v1.0.6) 53 | 54 | :sparkles: Implemented enhancements: 55 | 56 | - Update configuration to local/user level if original entry is from local too [\#79](https://github.com/dotnetconfig/dotnet-config/issues/79) 57 | 58 | ## [v1.0.5](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.5) (2021-07-30) 59 | 60 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.4...v1.0.5) 61 | 62 | :sparkles: Implemented enhancements: 63 | 64 | - Include readme in package [\#77](https://github.com/dotnetconfig/dotnet-config/issues/77) 65 | 66 | :bug: Fixed bugs: 67 | 68 | - Configuration extension AddDotNetConfig inverts settings hierarchy/priority [\#78](https://github.com/dotnetconfig/dotnet-config/issues/78) 69 | - Dependency on Configuration.Abstractions doesn't work with Functions v3 [\#73](https://github.com/dotnetconfig/dotnet-config/issues/73) 70 | 71 | ## [v1.0.4](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.4) (2021-06-12) 72 | 73 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.3...v1.0.4) 74 | 75 | :sparkles: Implemented enhancements: 76 | 77 | - Expose Section and Subsection for a ConfigSection [\#70](https://github.com/dotnetconfig/dotnet-config/issues/70) 78 | 79 | :twisted_rightwards_arrows: Merged: 80 | 81 | - Bump files with dotnet-file sync [\#72](https://github.com/dotnetconfig/dotnet-config/pull/72) (@kzu) 82 | 83 | ## [v1.0.3](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.3) (2021-04-29) 84 | 85 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.0...v1.0.3) 86 | 87 | :sparkles: Implemented enhancements: 88 | 89 | - ≥ Add System.CommandLine support [\#65](https://github.com/dotnetconfig/dotnet-config/pull/65) (@kzu) 90 | 91 | :bug: Fixed bugs: 92 | 93 | - Position class should not be public, it's an internal implementation detail [\#66](https://github.com/dotnetconfig/dotnet-config/issues/66) 94 | - dotnet-config tool is missing repository/project properties [\#64](https://github.com/dotnetconfig/dotnet-config/issues/64) 95 | - ConfigSection facade over Config is missing immutability feature [\#61](https://github.com/dotnetconfig/dotnet-config/issues/61) 96 | 97 | ## [v1.0.0](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.0) (2021-04-27) 98 | 99 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.0-rc.3...v1.0.0) 100 | 101 | :sparkles: Implemented enhancements: 102 | 103 | - Disable async IO since configuration loading is synchronous [\#57](https://github.com/dotnetconfig/dotnet-config/issues/57) 104 | - Add Microsoft.Extensions.Configuration support [\#3](https://github.com/dotnetconfig/dotnet-config/issues/3) 105 | 106 | :twisted_rightwards_arrows: Merged: 107 | 108 | - ⚙ Account for test flakyness in CI [\#59](https://github.com/dotnetconfig/dotnet-config/pull/59) (@kzu) 109 | - ⚙ Add basic Microsoft.Extensions.Configuration support [\#58](https://github.com/dotnetconfig/dotnet-config/pull/58) (@kzu) 110 | 111 | ## [v1.0.0-rc.3](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.0-rc.3) (2021-04-26) 112 | 113 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.0-rc.2...v1.0.0-rc.3) 114 | 115 | :sparkles: Implemented enhancements: 116 | 117 | - Switch to an immutable internal structure and public API [\#54](https://github.com/dotnetconfig/dotnet-config/issues/54) 118 | - When writing initial file, add header line [\#53](https://github.com/dotnetconfig/dotnet-config/issues/53) 119 | 120 | :bug: Fixed bugs: 121 | 122 | - When concurrently reading from config file, IO exception may be thrown [\#55](https://github.com/dotnetconfig/dotnet-config/issues/55) 123 | - Fails to save variables in global dir [\#51](https://github.com/dotnetconfig/dotnet-config/issues/51) 124 | - Fix \(code\) editor launch on Windows [\#35](https://github.com/dotnetconfig/dotnet-config/issues/35) 125 | 126 | :twisted_rightwards_arrows: Merged: 127 | 128 | - Make model immutable to avoid concurrency issues [\#56](https://github.com/dotnetconfig/dotnet-config/pull/56) (@kzu) 129 | - When saving to an aggregate config, use fallback locations [\#52](https://github.com/dotnetconfig/dotnet-config/pull/52) (@kzu) 130 | - Bump files with dotnet-file sync [\#48](https://github.com/dotnetconfig/dotnet-config/pull/48) (@kzu) 131 | - Bump files with dotnet-file sync [\#46](https://github.com/dotnetconfig/dotnet-config/pull/46) (@kzu) 132 | - 🔄 dotnet-file sync [\#42](https://github.com/dotnetconfig/dotnet-config/pull/42) (@kzu) 133 | - 🔄 dotnet-file sync [\#41](https://github.com/dotnetconfig/dotnet-config/pull/41) (@kzu) 134 | - 🖆 Apply devlooped/oss template via dotnet-file [\#38](https://github.com/dotnetconfig/dotnet-config/pull/38) (@kzu) 135 | 136 | ## [v1.0.0-rc.2](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.0-rc.2) (2020-12-21) 137 | 138 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.0-rc.1...v1.0.0-rc.2) 139 | 140 | :bug: Fixed bugs: 141 | 142 | - When reading hierarchical configurations, ensure files are read only once [\#31](https://github.com/dotnetconfig/dotnet-config/issues/31) 143 | 144 | :twisted_rightwards_arrows: Merged: 145 | 146 | - Fix \(code\) editor launch on Windows [\#33](https://github.com/dotnetconfig/dotnet-config/pull/33) (@atifaziz) 147 | - ☝ Ensure files are read only once when building Config [\#32](https://github.com/dotnetconfig/dotnet-config/pull/32) (@kzu) 148 | 149 | ## [v1.0.0-rc.1](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.0-rc.1) (2020-12-15) 150 | 151 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/v1.0.0-rc...v1.0.0-rc.1) 152 | 153 | :sparkles: Implemented enhancements: 154 | 155 | - 🖐 Add native .NET5 support to dotnet-config tool [\#30](https://github.com/dotnetconfig/dotnet-config/pull/30) (@kzu) 156 | 157 | :hammer: Other: 158 | 159 | - 🖐 Add native .NET5 support to dotnet-config tool [\#29](https://github.com/dotnetconfig/dotnet-config/issues/29) 160 | 161 | :twisted_rightwards_arrows: Merged: 162 | 163 | - Add ReportGenerator tool [\#16](https://github.com/dotnetconfig/dotnet-config/pull/16) (@kzu) 164 | 165 | ## [v1.0.0-rc](https://github.com/dotnetconfig/dotnet-config/tree/v1.0.0-rc) (2020-09-06) 166 | 167 | [Full Changelog](https://github.com/dotnetconfig/dotnet-config/compare/392c1087a84a2cb49a280b30d638213fa6b36c7d...v1.0.0-rc) 168 | 169 | :hammer: Other: 170 | 171 | - Allow resolving file path variable values relative to their declaring file [\#5](https://github.com/dotnetconfig/dotnet-config/issues/5) 172 | 173 | :twisted_rightwards_arrows: Merged: 174 | 175 | - Run CI validation via GH actions for speed [\#13](https://github.com/dotnetconfig/dotnet-config/pull/13) (@kzu) 176 | - Build and test in all supported platforms [\#12](https://github.com/dotnetconfig/dotnet-config/pull/12) (@kzu) 177 | - Get normalized and resolved relative paths from config [\#11](https://github.com/dotnetconfig/dotnet-config/pull/11) (@kzu) 178 | - Rename dotnet-config-lib to DotNetConfig [\#10](https://github.com/dotnetconfig/dotnet-config/pull/10) (@kzu) 179 | - Fix inclusion of build metadata in package id in non-main builds [\#9](https://github.com/dotnetconfig/dotnet-config/pull/9) (@kzu) 180 | - Add support for .netconfig.user files [\#8](https://github.com/dotnetconfig/dotnet-config/pull/8) (@kzu) 181 | - Add source link to library to support debugging [\#7](https://github.com/dotnetconfig/dotnet-config/pull/7) (@kzu) 182 | - Replace the parsing from Superpower to manual [\#6](https://github.com/dotnetconfig/dotnet-config/pull/6) (@kzu) 183 | - Rename Microsoft.DotNet \> DotNetConfig [\#4](https://github.com/dotnetconfig/dotnet-config/pull/4) (@kzu) 184 | - Initial docfx-based site and corresponding GH actions [\#2](https://github.com/dotnetconfig/dotnet-config/pull/2) (@kzu) 185 | - API improvements based on feedback from dotnet-vs [\#1](https://github.com/dotnetconfig/dotnet-config/pull/1) (@kzu) 186 | 187 | 188 | 189 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 190 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | One of the easiest ways to contribute is to participate in discussions on GitHub issues. You can also contribute by submitting pull requests with code changes. We also have a [discord channel](https://discord.gg/x4qhjYd) for more immediate discussions with the community. 4 | 5 | ## Contributing code 6 | 7 | Simply fork the repo, open the main solution and build it with your IDE of choice. You can build and run all tests with the *dotnet* CLI: 8 | 9 | ``` 10 | > dotnet build 11 | > dotnet test 12 | ``` 13 | 14 | This works across Windows, macOS and Linux, our [PR validation CI](https://github.com/dotnetconfig/dotnet-config/blob/dev/.github/workflows/pr.yml) ensures that any contributed PR runs in all supported platforms, so you're free to just build and test on your favorite one :). 15 | 16 | The repository provides a `.editorconfig` that should take care of formatting code appropriately on most code editors so you don't have to do that manually. The only requirement for code styling is that you try to follow the style of the surrounding code so your contribution blends in as much as possible. We believe in common sense, tooling and direct PR feedback rather than lengthy convention docs. 17 | 18 | ## Contributing documentation 19 | 20 | The project uses [docfx](https://dotnet.github.io/docfx/) to generate the https://dotnetconfig.org site. The *docs* folder containings the main article contents, and the root *README.md* is included in its entirety to make up the [main page](docs/index.md). 21 | 22 | Documentation is published automatically from the *main* branch, which is also used for building releases. 23 | 24 | The most common type of doc contribution would be adding a third-party tool that leverages this project for configuration, by adding an entry to the [toc.yml](docs/who/toc.yml) and a corresponding showcase doc for it. Use the existing docs a guideline. -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | There are three main ways to access *.netconfig* values: 4 | 5 | * [Native API](#native-api) for direct access to .netconfig values 6 | * [Microsoft.Extensions.Configuration](#microsoftextensionsconfiguration) provider 7 | * [System.CommandLine](#systemcommandline) for CLI apps 8 | 9 | ### Native API 10 | 11 | [![Version](https://img.shields.io/nuget/v/DotNetConfig.svg?color=royalblue)](https://www.nuget.org/packages/DotNetConfig) 12 | [![Downloads](https://img.shields.io/nuget/dt/DotNetConfig.svg?color=darkmagenta)](https://www.nuget.org/packages/DotNetConfig) 13 | 14 | ``` 15 | PM> Install-Package DotNetConfig 16 | ``` 17 | 18 | The main usage for .NET tool authors consuming the [DotNetConfig](https://www.nuget.org/packages/DotNetConfig) 19 | API is to first build a configuration from a specific path (will assume current directory 20 | if omitted): 21 | 22 | ```csharp 23 | var config = Config.Build(); 24 | ``` 25 | 26 | The resulting configuration will contain the hierarchical variables set in the 27 | current directory (or the given path), all its ancestor directories, plus global 28 | and system locations. 29 | 30 | When getting values, the supported primitives are exposed as first-class methods 31 | for `Add`, `Get` and `Set`, so you get quite a few usability overloads for 32 | each of `Boolean`, `DateTime`, `Number` and `String`, such as `AddBoolean`, 33 | `GetDateTime`, `GetString` or `SetNumber`: 34 | 35 | ```csharp 36 | // reads from: 37 | // [mytool] 38 | // enabled = true 39 | 40 | bool? enabled = config.GetBoolean("mytool", "enabled"); 41 | 42 | // reads from: 43 | // [mytool.editor] 44 | // path = code.exe 45 | 46 | string? path = config.GetString("mytool.editor", "path"); 47 | 48 | 49 | // reads from: 50 | // [mytool "src/file.txt"] 51 | // createdOn = 2020-08-23T12:00:00Z 52 | 53 | DateTime? created = config.GetDateTime("mytool", "src/file.txt", "createdOn"); 54 | // If value was not found, set it to the current datetime 55 | if (created == null) 56 | // Would create the section if it did not previously exist, and add the variable 57 | config.SetDateTime("mytool", "src/file.txt", "createdOn", DateTime.Now); 58 | ``` 59 | 60 | Alternatively you can use the `TryGetXXX` methods instead, to avoid checking for 61 | null return values in cases where the variable (in the requested section and 62 | optional subsection) is not found: 63 | 64 | ```csharp 65 | if (!config.TryGetDateTime("mytool", "src/file.txt", "createdOn", out created)) 66 | config.SetDateTime("mytool", "src/file.txt", "createdOn", DateTime.Now); 67 | ``` 68 | 69 | 70 | Since `.netconfig` supports multi-valued variables, you can retrieve all entries as 71 | `ConfigEntry` and inspect or manipulate them granularly: 72 | 73 | ```csharp 74 | foreach (ConfigEntry entry in config.GetAll("proxy", "url")) 75 | { 76 | // entry.Level allows inspecting the location where the entry was read from 77 | if (entry.Level == ConfigLevel.System) 78 | // entry came from Environment.SpecialFolder.System 79 | else if (entry.Level == ConfigLevel.Global) 80 | // entry came from Environment.SpecialFolder.UserProfile 81 | else if (entry.Level == ConfigLevel.Local) 82 | // entry came from .netconfig.user file in the current dir or an ancestor directory 83 | else 84 | // local entry from current dir .netconfig or an ancestor directory 85 | 86 | Console.WriteLine(entry.GetString()); 87 | // entry.GetBoolean(), entry.GetDateTime(), entry.GetNumber() 88 | } 89 | ``` 90 | 91 | When writing values (via `AddXXX` or `SetXXX`) you can optionally specify the 92 | configuration level to use for persisting the value, by passing a `ConfigLevel`: 93 | 94 | ```csharp 95 | // writes on the global .netconfig in the user's profile 96 | //[vs "alias"] 97 | // comexp = run|community|exp 98 | 99 | config.AddString("vs", "alias", "comexp", "run|community|exp", ConfigLevel.Global); 100 | ``` 101 | 102 | You can explore the entire API in the [docs site](https://dotnetconfig.org/api/). 103 | 104 | ### Microsoft.Extensions.Configuration 105 | 106 | [![Version](https://img.shields.io/nuget/v/DotNetConfig.Configuration.svg?color=royalblue)](https://www.nuget.org/packages/DotNetConfig.Configuration) 107 | [![Downloads](https://img.shields.io/nuget/dt/DotNetConfig.Configuration.svg?color=darkmagenta)](https://www.nuget.org/packages/DotNetConfig.Configuration) 108 | 109 | ``` 110 | PM> Install-Package DotNetConfig.Configuration 111 | ``` 112 | 113 | Usage (in this example, also chaining other providers): 114 | 115 | ```csharp 116 | var config = new ConfigurationBuilder() 117 | .AddJsonFile(...) 118 | .AddEnvironmentVariables() 119 | .AddIniFile(...) 120 | .AddDotNetConfig(); 121 | ``` 122 | 123 | Given the following .netconfig: 124 | 125 | ```gitconfig 126 | [serve] 127 | port = 8080 128 | 129 | [security "admin"] 130 | timeout = 60 131 | ``` 132 | 133 | You can read both values with: 134 | 135 | ```csharp 136 | string port = config["serve:port"]; // == "8080"; 137 | string timeout = config["security:admin:timeout"]; // == "60"; 138 | ``` 139 | 140 | ### System.CommandLine 141 | 142 | [![Version](https://img.shields.io/nuget/v/DotNetConfig.CommandLine.svg?color=royalblue)](https://www.nuget.org/packages/DotNetConfig.CommandLine) 143 | [![Downloads](https://img.shields.io/nuget/dt/DotNetConfig.CommandLine.svg?color=darkmagenta)](https://www.nuget.org/packages/DotNetConfig.CommandLine) 144 | 145 | Given the explicit goal of making **.netconfig** a first-class citizen among dotnet (global) tools, it offers 146 | excelent and seamless integration with [System.CommandLine](https://www.nuget.org/packages/System.CommandLine). 147 | 148 | Let's asume you create a CLI app named `package` which manages your local cache of packages (i.e. NuGet). 149 | You might have a couple commands, like `download` and `prune`, like so: 150 | 151 | ```csharp 152 | var root = new RootCommand 153 | { 154 | new Command("download") 155 | { 156 | new Argument("id") 157 | }, 158 | new Command("prune") 159 | { 160 | new Argument("id"), 161 | new Option("days") 162 | }, 163 | }.WithConfigurableDefaults("package"); 164 | ``` 165 | 166 | The added `WithConfigurableDefaults` invocation means that now all arguments and options can have 167 | their default values specified in config, such as: 168 | 169 | ```gitconfig 170 | [package] 171 | id = DotNetConfig 172 | 173 | [package "prune"] 174 | days = 30 175 | ``` 176 | 177 | Note how the `id` can be specified at the top level too. The integration will automatically promote 178 | configurable values to ancestor sections as long as they have compatible types (both `id` in `download` 179 | and `prune` commands are defined as `string`). 180 | 181 | Running `package -?` from the command line will now pull the rendered default values from config, so 182 | you can see what will actually be used if the command is run with no values: 183 | 184 | ``` 185 | Usage: 186 | package [options] [command] 187 | 188 | Options: 189 | --version Show version information 190 | -?, -h, --help Show help and usage information 191 | 192 | Commands: 193 | download [default: DotNetConfig] 194 | prune [default: DotNetConfig] 195 | ``` 196 | 197 | And `package prune -?` would show: 198 | 199 | ``` 200 | Usage: 201 | package [options] prune [] 202 | 203 | Arguments: 204 | [default: DotNetConfig] 205 | 206 | Options: 207 | --days [default: 30] 208 | -?, -h, --help Show help and usage information 209 | ``` 210 | 211 | Since **.netconfig** supports multi-valued variables, it's great for populating default 212 | values for arguments or options that can be specified more than once. By making this 213 | simple change to the argument above: 214 | 215 | ```csharp 216 | new Argument("id") 217 | ``` 218 | 219 | We can now support a configuration like the following: 220 | 221 | ```gitconfig 222 | [package] 223 | id = DotNetConfig 224 | id = Moq 225 | id = ThisAssembly 226 | ``` 227 | 228 | And running the command with no `id` argument will now cause the handler to receive all three. You 229 | can also verify that this is the case via `download -?`, for example: 230 | 231 | ``` 232 | Usage: 233 | package [options] download [...] 234 | 235 | Arguments: 236 | [default: DotNetConfig|Moq|ThisAssembly] 237 | 238 | Options: 239 | -?, -h, --help Show help and usage information 240 | ``` 241 | 242 | All the types supported by System.CommandLine for multiple artity arguments and options are 243 | automatically populated: arrays, `IEnumerable`, `ICollection`, `IList` and `List`. 244 | 245 | For numbers, the argument/option can be either `long` or `int`. Keep in mind that since numbers in 246 | **.netconfig** are always `long`, truncation may occur in the latter case. 247 | -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/docfx", 3 | "metadata": [ 4 | { 5 | "src": [ 6 | { 7 | "files": [ "*.cs" ], 8 | "src": "../src/Config" 9 | }, 10 | { 11 | "files": [ "*.cs" ], 12 | "src": "../src/Configuration" 13 | }, 14 | { 15 | "files": [ "*.cs" ], 16 | "src": "../src/CommandLine" 17 | } 18 | ], 19 | "dest": "docs/api" 20 | } 21 | ], 22 | "build": { 23 | "content": [ 24 | { 25 | "files": [ 26 | "*.yml", 27 | "index.md" 28 | ], 29 | "src": "api", 30 | "dest": "api" 31 | }, 32 | { 33 | "files": [ 34 | "*.yml", 35 | "*.md" 36 | ], 37 | "src": "who", 38 | "dest": "who" 39 | }, 40 | { 41 | "files": [ 42 | "*.md", 43 | "toc.yml" 44 | ] 45 | } 46 | ], 47 | "resource": [ 48 | { 49 | "files": [ 50 | "logo.svg", 51 | "favicon.ico", 52 | "styles/*.css", 53 | "styles/*.js" 54 | ] 55 | } 56 | ], 57 | "globalMetadata": { 58 | "_appTitle": ".netconfig", 59 | "_enableSearch": true 60 | }, 61 | "template": [ 62 | "default", 63 | "./template" 64 | ], 65 | "dest": "_site" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/favicon.ico -------------------------------------------------------------------------------- /docs/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon-128.png -------------------------------------------------------------------------------- /docs/img/icon-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon-300.png -------------------------------------------------------------------------------- /docs/img/icon-32-flip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon-32-flip.png -------------------------------------------------------------------------------- /docs/img/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon-32.png -------------------------------------------------------------------------------- /docs/img/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon-64.png -------------------------------------------------------------------------------- /docs/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon.ico -------------------------------------------------------------------------------- /docs/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/img/icon.png -------------------------------------------------------------------------------- /docs/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/img/noun_Command Line_915990.svg: -------------------------------------------------------------------------------- 1 | 2 | inspect-icons-liny-export 3 | 4 | 5 | Created by Vicons Design 6 | from the Noun Project 7 | 8 | -------------------------------------------------------------------------------- /docs/img/noun_Settings_3120278.svg: -------------------------------------------------------------------------------- 1 | 2 | Setting 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Created by Rois Faozi 15 | from the Noun Project 16 | 17 | -------------------------------------------------------------------------------- /docs/img/readme.txt: -------------------------------------------------------------------------------- 1 | Icon mashup/edit from: 2 | 3 | - Settings by Rois Faozi from the Noun Project: https://thenounproject.com/term/settings/3120278/ 4 | - Command Line by Vicons Design from the Noun Project: https://thenounproject.com/term/command-line/915990/ -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | [!include[Readme](../readme.md)] -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/styles/main.css: -------------------------------------------------------------------------------- 1 | @media (min-width:1400px) { 2 | .container{width:1300px;} 3 | } 4 | 5 | .table { 6 | width: auto 7 | } 8 | -------------------------------------------------------------------------------- /docs/styles/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/docs/styles/main.js -------------------------------------------------------------------------------- /docs/template/partials/head.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | 4 | 5 | 6 | {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} 7 | 8 | 9 | 10 | {{#_description}}{{/_description}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{#_noindex}}{{/_noindex}} 19 | {{#_enableSearch}}{{/_enableSearch}} 20 | {{#_enableNewTab}}{{/_enableNewTab}} 21 | 22 | -------------------------------------------------------------------------------- /docs/template/partials/scripts.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Home 2 | href: index.md 3 | - name: Api 4 | href: api/ 5 | homepage: api/index.md 6 | - name: Who 7 | href: who/ 8 | homepage: who/index.md 9 | -------------------------------------------------------------------------------- /docs/who/dotnet-eventgrid.md: -------------------------------------------------------------------------------- 1 | # dotnet-eventgrid 2 | 3 | [![dotnet-eventgrid](https://img.shields.io/nuget/v/dotnet-eventgrid.svg?color=royalblue&label=dotnet-eventgrid)](https://nuget.org/packages/dotnet-eventgrid) 4 | 5 | [dotnet-eventgrid](https://github.com/kzu/dotnet-eventgrid) is a dotnet global tool to 6 | monitor and filter real-time event from Azure EventGrid, delivered through Azure SignalR. 7 | 8 | The tool allows configuring filters so that you can monitor the specific events comming through 9 | the SignalR connection like: 10 | 11 | ```gitconfig 12 | [eventgrid] 13 | url = https://events.mygrid.com 14 | filter = path=MyApp/**/Login 15 | filter = eventType=*System* 16 | ``` 17 | 18 | You can also specify whether you want to see certain event properties rendered or not in the console: 19 | 20 | ```gitconfig 21 | [eventgrid] 22 | # properties to include in the event rendering 23 | include = EventType 24 | include = Subject 25 | 26 | # properties to exclude from event rendering 27 | exclude = data 28 | ``` 29 | 30 | The tool is quite awesome in action, and after you have your desired *.netconfig* in place, you can 31 | just run `eventgrid` without parameters and enjoy it right-away: 32 | 33 | ![EventGrid tool in action](https://raw.github.com/kzu/dotnet-eventgrid/master/img/eventgrid.gif) -------------------------------------------------------------------------------- /docs/who/dotnet-file.md: -------------------------------------------------------------------------------- 1 | # dotnet-file 2 | 3 | [![dotnet-file](https://img.shields.io/nuget/v/dotnet-file.svg?color=royalblue&label=dotnet-file)](https://nuget.org/packages/dotnet-file) 4 | 5 | The [dotnet-file](https://github.com/kzu/dotnet-file) is a dotnet global tool for 6 | downloading and updating loose files from arbitrary URLs. It uses `dotnet-config` to 7 | persist the remove URLs and the associated ETags for downloaded files so that performing 8 | a `dotnet file update` can only download the necessary changes (if any). 9 | 10 | Sample configuration: 11 | 12 | ```gitconfig 13 | # dotnet-file GH repo download/sync for specific subfolders 14 | [file "docs"] 15 | url = https://github.com/dotnet/aspnetcore/tree/master/docs 16 | url = https://github.com/dotnet/runtime/tree/master/docs/design/features 17 | 18 | [file "docs/APIReviewProcess.md"] 19 | url = https://github.com/dotnet/aspnetcore/blob/master/docs/APIReviewProcess.md 20 | etag = 1e4acd7e1ac446f0c6d397e1ed517c54507700b85826f64745559dfb50f2acbd 21 | [file "docs/Artifacts.md"] 22 | url = https://github.com/dotnet/aspnetcore/blob/master/docs/Artifacts.md 23 | etag = d663b7b460e871c6af17fc288d8bd2d893e29127acf417030254dd239ef42a68 24 | ... 25 | [file "docs/design/features/tiered-compilation.md"] 26 | url = https://github.com/dotnet/runtime/blob/master/docs/design/features/tiered-compilation.md 27 | etag = 8c2706b687ea4bdaac7ba4caccf29fa529856623e292195dda4aa506e39c3d7d 28 | [file "docs/design/features/unloadability.md"] 29 | url = https://github.com/dotnet/runtime/blob/master/docs/design/features/unloadability.md 30 | etag = 4424103e00e2fae42e6a6a8157d139de18026f2acd5d1afd6c727af03c5affeb 31 | ``` -------------------------------------------------------------------------------- /docs/who/dotnet-serve.md: -------------------------------------------------------------------------------- 1 | # dotnet-serve 2 | 3 | [![dotnet-serve](https://img.shields.io/nuget/v/dotnet-serve.svg?color=royalblue&label=dotnet-serve)](https://nuget.org/packages/dotnet-serve) 4 | 5 | The [dotnet-serve](https://github.com/natemcmaster/dotnet-serve) is a simple 6 | command-line HTTP server. 7 | 8 | It leverages `dotnet-config` to [augment and reuse options](https://github.com/natemcmaster/dotnet-serve#reusing-options-with-netconfig) 9 | so they don't have to be passed in constantly via the command line as arguments. 10 | The hierarchical nature of `.netconfig` makes it very convenient to centralize 11 | settings related to HTTPS, certificates and others globally (either at the user 12 | or system level), so that in most cases you just have to run `dotnet serve` from 13 | any folder and get a consistent behavior throughout your machine. 14 | 15 | To save the options used in a particular run to the current directory's `.netconfig`, just append 16 | `--save-options`: 17 | 18 | ``` 19 | dotnet serve -p 8080 --gzip --cors --quiet --save-options 20 | ``` 21 | 22 | After running that command, a new `.netconfig` will be created (if there isn't one already there) 23 | with the following section for `dotnet-serve`: 24 | 25 | ```gitconfig 26 | [serve] 27 | port = 8080 28 | gzip 29 | cors 30 | quiet 31 | ``` 32 | 33 | You can also add multiple `header`, `mime` type mappings and `exclude-file` entries can be provided as 34 | individual variables, such as: 35 | 36 | ```gitconfig 37 | [serve] 38 | port = 8080 39 | header = X-H1: value 40 | header = X-H2: value 41 | mime = .cs=text/plain 42 | mime = .vb=text/plain 43 | mime = .fs=text/plain 44 | exclude-file = app.config 45 | exclude-file = appsettings.json 46 | ``` 47 | 48 | You can place those settings in any parent folder and it will be reused across all descendent 49 | folders, or they can also be saved to the global (user profile) or system locations. To easily 50 | configure these options at those levels, use the `dotnet-config` tool itself: 51 | 52 | ``` 53 | dotnet config --global --set serve.port 8000 54 | dotnet config --global --add serve.mime .csproj=text/plain 55 | ``` 56 | 57 | This will default the port to `8000` whenever a port is not specified in the command line, 58 | and it will always add the given mime type mapping. You can open the saved `.netconfig` 59 | at `%USERPROFILE%\.netconfig` or `~/.netconfig`. -------------------------------------------------------------------------------- /docs/who/dotnet-vs.md: -------------------------------------------------------------------------------- 1 | # dotnet-vs 2 | 3 | [![dotnet-vs](https://img.shields.io/nuget/v/dotnet-vs.svg?color=royalblue&label=dotnet-vs)](https://nuget.org/packages/dotnet-vs) 4 | 5 | The [dotnet-vs](https://github.com/kzu/dotnet-vs) tool uses `dotnet-config` to persist command aliases, 6 | just like GIT aliases, that run `Visual Studio` (or its installer) with various switches. 7 | 8 | Any of the commands supported by the `vs` global tool can be saved as an alias by simply appending 9 | `--save [alias]` at the end of the command line arguments. Next time you need to execute the same 10 | command, you can just use `vs [alias]` instead. 11 | 12 | Example: 13 | 14 | * Run Visual Studio Community edition's experimental instance, with activity logging enabled, save as `test` 15 | 16 | vs run com exp /log --save=test 17 | 18 | From that point on, the same command can be run simply with: 19 | 20 | vs test 21 | 22 | 23 | Sample configuration: 24 | 25 | ```gitconfig 26 | [vs "alias"] 27 | comexp = run|community|exp 28 | preexp = run|preview|exp 29 | test = run|com|exp|/log 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /docs/who/index.md: -------------------------------------------------------------------------------- 1 | # Who uses .netconfig? 2 | 3 | This section highlights some of the known usages of `dotnet-config`. 4 | 5 | If you created or use a tool that leverages `dotnet-config`, please consider 6 | sending a PR to add it to the [who](https://github.com/dotnetconfig/dotnet-config/tree/main/docs/who) 7 | directory. In addition to a `.md` doc showcasing the usage, don't forget to add 8 | a corresponding entry to the [toc.yml](https://github.com/dotnetconfig/dotnet-config/tree/main/docs/who/toc.yml). 9 | 10 | Thanks! 11 | 12 | -------------------------------------------------------------------------------- /docs/who/reportgenerator.md: -------------------------------------------------------------------------------- 1 | # ReportGenerator 2 | 3 | [![reportgenerator](https://img.shields.io/nuget/v/dotnet-reportgenerator-globaltool.svg?color=royalblue&label=reportgenerator)](https://nuget.org/packages/dotnet-reportgenerator-globaltool) 4 | 5 | [ReportGenerator](https://github.com/danielpalme/ReportGenerator) converts coverage reports generated by OpenCover, dotCover, Visual Studio, NCover, Cobertura, JaCoCo, Clover, gcov or lcov into human readable reports in various formats. 6 | 7 | It provides a dotnet [global tool](https://www.nuget.org/packages/dotnet-reportgenerator-globaltool/) 8 | which supports `.netconfig` to configure the multitude of options that generating complex reports 9 | require, for example: 10 | 11 | ```gitconfig 12 | [ReportGenerator] 13 | reports = coverage.xml 14 | targetdir = "C:\report" 15 | reporttypes = Latex;HtmlSummary 16 | assemblyfilters = +Test;-Test 17 | classfilters = +Test2;-Test2 18 | ``` 19 | 20 | All the plural options can also be specified as multiple singular entries, like: 21 | 22 | ```gitconfig 23 | [ReportGenerator] 24 | report = coverage1.xml 25 | report = coverage2.xml 26 | reporttype = Latex 27 | reporttype = HtmlSummary 28 | assemblyfilter = +Test 29 | assemblyfilter = -Test 30 | classfilter = +Test2 31 | classfilter = -Test2 32 | filefilter = +cs 33 | filefilter = -vb 34 | sourcedir = src 35 | sourcedir = test 36 | ``` -------------------------------------------------------------------------------- /docs/who/sleet.md: -------------------------------------------------------------------------------- 1 | # Sleet 2 | 3 | [![sleet](https://img.shields.io/nuget/v/sleet.svg?color=royalblue&label=sleet)](https://nuget.org/packages/sleet) 4 | 5 | [Sleet](https://github.com/emgarten/Sleet) is a static (serverless) NuGet package 6 | feed generator, which supports configuration via a JSON file as well as `.netconfig`. 7 | 8 | All the available [settings](https://github.com/emgarten/Sleet/blob/master/doc/client-settings.md) 9 | are supported via `.netconfig`. 10 | 11 | Some examples are: 12 | 13 | 1. Azure Blob Storage-based feed: 14 | 15 | ```gitconfig 16 | [sleet "feed"] 17 | type = azure 18 | container = feed 19 | connectionString = "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;BlobEndpoint=" 20 | path = https://yourStorageAccount.blob.core.windows.net/feed/ 21 | ``` 22 | 23 | 2. AWS S3-based feed: 24 | 25 | ```gitconfig 26 | [sleet "feed"] 27 | type = s3 28 | path = https://s3.amazonaws.com/my-bucket-feed/ 29 | bucketName = my-bucket-feed 30 | region = us-west-2 31 | accessKeyId = IAM_ACCESS_KEY_ID 32 | secretAccessKey = IAM_SECRET_ACCESS_KEY 33 | ``` 34 | 35 | 3. Local directory feed: 36 | ```gitconfig 37 | [sleet "myLocalFeed"] 38 | type = local 39 | path = C:\\myFeed 40 | ``` 41 | 42 | See the [docs](https://github.com/emgarten/Sleet/blob/master/doc/index.md) for all 43 | available settings. 44 | -------------------------------------------------------------------------------- /docs/who/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Overview 2 | href: index.md 3 | - name: dotnet-eventgrid 4 | href: dotnet-eventgrid.md 5 | - name: dotnet-file 6 | href: dotnet-file.md 7 | - name: dotnet-serve 8 | href: dotnet-serve.md 9 | - name: dotnet-vs 10 | href: dotnet-vs.md 11 | - name: ReportGenerator 12 | href: reportgenerator.md 13 | - name: Sleet 14 | href: sleet.md -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daniel Cazzulino and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/CommandLine/CommandLine.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DotNetConfig.CommandLine 5 | Extensions to System.CommandLine to automatically read default values from .netconfig files. 6 | 7 | Usage: 8 | var root = new RootCommand 9 | { 10 | // child commands, arguments, options 11 | }.WithConfigurableDefaults(); 12 | 13 | 14 | The following heuristics are applied when providing default values: 15 | 16 | * Only arguments/options without a default value are processed. 17 | * Section matches root command name, subsection (dot-separated) for each additional nested 18 | command level (i.e. `[mytool "mycommand.myverb"]`). 19 | * Compatible arguments/options (same name/type) can be placed in ancestor section/subsection to affect 20 | default value of entire subtree. 21 | * All the types supported by System.CommandLine for multiple artity arguments and options are 22 | automatically populated: arrays, `IEnumerable{T}`, `ICollection{T}`, `IList{T}` and `List{T}`: 23 | .netconfig can provide multi-valued variables for those. 24 | * Numbers can be either `int` or `long`. 25 | 26 | 27 | 28 | netstandard2.0 29 | DotNetConfig.CommandLine 30 | DotNetConfig 31 | true 32 | true 33 | true 34 | readme.md 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Config.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE1006: Naming Styles 4 | dotnet_diagnostic.IDE1006.severity = none 5 | -------------------------------------------------------------------------------- /src/Config.Tests/CommandLineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.CommandLine; 4 | using System.CommandLine.Builder; 5 | using System.CommandLine.Invocation; 6 | using System.CommandLine.Parsing; 7 | using System.IO; 8 | using System.Linq; 9 | using Medallion.Collections; 10 | using Xunit; 11 | 12 | namespace DotNetConfig 13 | { 14 | public class CommandLineTests 15 | { 16 | internal static string[] SeverityLevels => new[] { "info", "warn", "error" }; 17 | 18 | [Fact] 19 | public void when_argument_no_default_then_sets_default() 20 | { 21 | var command = new Command("foo") 22 | { 23 | new Argument("string"), 24 | new Argument("long"), 25 | new Argument("int"), 26 | new Argument("date"), 27 | new Argument("bool"), 28 | new Argument("string2"), 29 | new Argument("long2"), 30 | new Argument("int2"), 31 | new Argument("date2"), 32 | new Argument("bool2"), 33 | }.WithConfigurableDefaults(); 34 | 35 | Assert.True(command.Arguments.All(x => x.HasDefaultValue)); 36 | } 37 | 38 | [Fact] 39 | public void given_command_tree_sets_all_default() 40 | { 41 | var config = Config.Build(Path.GetTempFileName()) 42 | .SetString("foo", "name", "foo") 43 | .SetString("foo", "bar", "name", "bar") 44 | .SetBoolean("foo", "baz", "force", true) 45 | .SetString("foo", "baz.update", "name", "baz"); 46 | 47 | var command = new Command("foo") 48 | { 49 | new Command("bar") 50 | { 51 | new Argument("name"), 52 | }, 53 | new Command("baz") 54 | { 55 | new Command("update") 56 | { 57 | new Argument("name"), 58 | }, 59 | new Command("install") 60 | { 61 | new Argument("name"), 62 | }, 63 | new Command("uninstall") 64 | { 65 | new Argument("name"), 66 | new Option("force"), 67 | }, 68 | }, 69 | }.WithConfigurableDefaults(configuration: config); 70 | 71 | Assert.True(Traverse 72 | .BreadthFirst(command, c => c.Children.OfType()) 73 | .SelectMany(x => x.Arguments).All(x => x.HasDefaultValue)); 74 | 75 | // Gets from the command-specific section 76 | Assert.Equal("bar", command.Children.OfType().First().Arguments.First().GetDefaultValue()); 77 | Assert.Equal("baz", Traverse 78 | .DepthFirst(command, c => c.Children.OfType()) 79 | .First(c => c.Name == "update") 80 | .Arguments.First().GetDefaultValue()); 81 | 82 | // Uses default from lifted top-level section for shared "name" argument 83 | Assert.Equal("foo", Traverse 84 | .DepthFirst(command, c => c.Children.OfType()) 85 | .First(c => c.Name == "install") 86 | .Arguments.First().GetDefaultValue()); 87 | 88 | // Non-shared but still lifted since no conflicts 89 | Assert.True(Traverse 90 | .DepthFirst(command, c => c.Children.OfType()) 91 | .First(c => c.Name == "uninstall") 92 | .Options.OfType().First().Argument.GetDefaultValue() as bool?); 93 | } 94 | 95 | [Fact] 96 | public void given_string_array_sets_all_values() 97 | { 98 | var config = Config.Build(Path.GetTempFileName()) 99 | .AddString("cli", "include", "foo") 100 | .AddString("cli", "include", "bar") 101 | .AddString("cli", "include", "baz"); 102 | 103 | var command = new RootCommand() 104 | { 105 | new Argument("include"), 106 | }.WithConfigurableDefaults("cli", configuration: config); 107 | 108 | Assert.Equal(new[] { "foo", "bar", "baz" }, (string[]?)command.Arguments.First().GetDefaultValue()); 109 | 110 | command.Handler = CommandHandler.Create(include => 111 | Assert.Equal(new[] { "foo", "bar", "baz" }, include)); 112 | 113 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 114 | } 115 | 116 | [Fact] 117 | public void given_string_list_sets_all_values() 118 | { 119 | var config = Config.Build(Path.GetTempFileName()) 120 | .AddString("cli", "include", "foo") 121 | .AddString("cli", "include", "bar") 122 | .AddString("cli", "include", "baz"); 123 | 124 | var command = new RootCommand() 125 | { 126 | new Argument>("include"), 127 | }.WithConfigurableDefaults("cli", configuration: config); 128 | 129 | Assert.NotNull(command.Arguments.First().GetDefaultValue()); 130 | Assert.Equal(new[] { "foo", "bar", "baz" }, (IEnumerable)command.Arguments.First().GetDefaultValue()!); 131 | 132 | command.Handler = CommandHandler.Create>(include => 133 | Assert.Equal(new[] { "foo", "bar", "baz" }, include)); 134 | 135 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 136 | } 137 | 138 | [Fact] 139 | public void given_string_ilist_sets_all_values() 140 | { 141 | var config = Config.Build(Path.GetTempFileName()) 142 | .AddString("cli", "include", "foo") 143 | .AddString("cli", "include", "bar") 144 | .AddString("cli", "include", "baz"); 145 | 146 | var command = new RootCommand() 147 | { 148 | new Argument>("include"), 149 | }.WithConfigurableDefaults("cli", configuration: config); 150 | 151 | Assert.NotNull(command.Arguments.First().GetDefaultValue()); 152 | Assert.Equal(new[] { "foo", "bar", "baz" }, (IEnumerable)command.Arguments.First().GetDefaultValue()!); 153 | 154 | command.Handler = CommandHandler.Create>(include => 155 | Assert.Equal(new[] { "foo", "bar", "baz" }, include)); 156 | 157 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 158 | } 159 | 160 | [Fact] 161 | public void given_string_icollection_sets_all_values() 162 | { 163 | var config = Config.Build(Path.GetTempFileName()) 164 | .AddString("cli", "include", "foo") 165 | .AddString("cli", "include", "bar") 166 | .AddString("cli", "include", "baz"); 167 | 168 | var command = new RootCommand() 169 | { 170 | new Argument>("include"), 171 | }.WithConfigurableDefaults("cli", configuration: config); 172 | 173 | Assert.NotNull(command.Arguments.First().GetDefaultValue()); 174 | Assert.Equal(new[] { "foo", "bar", "baz" }, (IEnumerable)command.Arguments.First().GetDefaultValue()!); 175 | 176 | command.Handler = CommandHandler.Create>(include => 177 | Assert.Equal(new[] { "foo", "bar", "baz" }, include)); 178 | 179 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 180 | } 181 | 182 | [Fact] 183 | public void given_int_array_sets_all_values() 184 | { 185 | var config = Config.Build(Path.GetTempFileName()) 186 | .AddNumber("cli", "include", 25) 187 | .AddNumber("cli", "include", 50) 188 | .AddNumber("cli", "include", 100); 189 | 190 | var command = new RootCommand() 191 | { 192 | new Argument("include"), 193 | }.WithConfigurableDefaults("cli", configuration: config); 194 | 195 | command.Handler = CommandHandler.Create(include => 196 | Assert.Equal(new[] { 25, 50, 100 }, include)); 197 | 198 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 199 | } 200 | 201 | [Fact] 202 | public void given_long_array_sets_all_values() 203 | { 204 | var config = Config.Build(Path.GetTempFileName()) 205 | .AddNumber("cli", "include", 25) 206 | .AddNumber("cli", "include", 50) 207 | .AddNumber("cli", "include", 100); 208 | 209 | var command = new RootCommand() 210 | { 211 | new Argument("include"), 212 | }.WithConfigurableDefaults("cli", configuration: config); 213 | 214 | command.Handler = CommandHandler.Create(include => 215 | Assert.Equal(new long[] { 25, 50, 100 }, include)); 216 | 217 | new CommandLineBuilder(command).Build().Invoke(new string[] { }); 218 | } 219 | 220 | [Fact] 221 | public void SetDefaults() 222 | { 223 | // Sync changes to option and argument names with the FormatCommant.Handler above. 224 | var rootCommand = new RootCommand 225 | { 226 | new Argument("workspace") 227 | { 228 | Arity = ArgumentArity.ZeroOrOne, 229 | }.LegalFilePathsOnly(), 230 | new Option(new[] { "--no-restore" }), 231 | new Option(new[] { "--folder", "-f" }), 232 | new Option(new[] { "--fix-whitespace", "-w" }), 233 | new Option(new[] { "--fix-style", "-s" }) { Name = "severity" }.FromAmong(SeverityLevels), 234 | new Option(new[] { "--fix-analyzers", "-a" }).FromAmong(SeverityLevels), 235 | //{ 236 | //Argument = new Argument("severity") { Arity = ArgumentArity.ZeroOrOne }.FromAmong(SeverityLevels) 237 | //}, 238 | new Option(new[] { "--diagnostics" }, () => Array.Empty()) 239 | { 240 | //Argument = new Argument(() => Array.Empty()) 241 | }, 242 | new Option(new[] { "--include" }) 243 | { 244 | //Argument = new Argument(() => Array.Empty()) 245 | }, 246 | new Option(new[] { "--exclude" }) 247 | { 248 | //Argument = new Argument(() => Array.Empty()) 249 | }, 250 | new Option(new[] { "--check" }), 251 | new Option(new[] { "--report" }) 252 | { 253 | //Argument = new Argument(() => null) { Name = "report-path" }.LegalFilePathsOnly() 254 | }, 255 | new Option(new[] { "--verbosity", "-v" }) 256 | { 257 | //Argument = new Argument() { Arity = ArgumentArity.ExactlyOne }.FromAmong(VerbosityLevels) 258 | }, 259 | new Option(new[] { "--include-generated" }) 260 | { 261 | IsHidden = true 262 | }, 263 | new Option(new[] { "--binarylog" }) 264 | { 265 | //Argument = new Argument(() => null) { Name = "binary-log-path", Arity = ArgumentArity.ZeroOrOne }.LegalFilePathsOnly() 266 | }, 267 | }; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Config.Tests/Config.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | DotNetConfig.Tests 6 | DotNetConfig 7 | CS1685;$(NoWarn) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Config.Tests/ConfigParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace DotNetConfig.Tests 8 | { 9 | public class ConfigParserTests 10 | { 11 | ITestOutputHelper output; 12 | 13 | public ConfigParserTests(ITestOutputHelper output) => this.output = output; 14 | 15 | [Theory] 16 | [InlineData("[foo\\bar]")] 17 | [InlineData("[foo\"]")] 18 | [InlineData("[foo bar")] 19 | public void cannot_parse_invalid_section(string input) 20 | { 21 | Assert.False(ConfigParser.TryParseSectionLine(input, out var _, out var __, out var ___)); 22 | } 23 | 24 | [Theory] 25 | [InlineData("[foo]", "foo", null)] 26 | [InlineData("[foo.bar]", "foo.bar", null)] 27 | [InlineData("[foo \"bar\"]", "foo", "bar")] 28 | [InlineData("[foo-bar]", "foo-bar", null)] 29 | [InlineData("[foo \"bar-baz\"]", "foo", "bar-baz")] 30 | [InlineData("[foo.bar \"baz\"]", "foo.bar", "baz")] 31 | [InlineData("[foo.bar \"baz \\\"quoted\\\"\"]", "foo.bar", "baz \"quoted\"")] 32 | public void can_parse_section(string input, string section, string subsection) 33 | { 34 | if (ConfigParser.TryParseSectionLine(input, out var result, out var error, out var position)) 35 | { 36 | Assert.Equal(section, result.Section); 37 | Assert.Equal(subsection, result.Subsection); 38 | } 39 | else 40 | { 41 | Assert.True(false, $"at {position.Column}: {error}"); 42 | } 43 | } 44 | 45 | [Theory] 46 | [InlineData("foo", "foo", "true")] 47 | [InlineData("foo=bar", "foo", "bar")] 48 | [InlineData("foo-bar=baz", "foo-bar", "baz")] 49 | [InlineData("foo = bar", "foo", "bar")] 50 | [InlineData("foo = \"with spaces\"", "foo", "with spaces")] 51 | [InlineData(" foo = bar", "foo", "bar")] 52 | [InlineData("\tfoo=bar", "foo", "bar")] 53 | [InlineData("foo= value has spaces ", "foo", "value has spaces")] 54 | [InlineData("foo= value: has colon", "foo", "value: has colon")] 55 | [InlineData("foo=\"+A;-B\"", "foo", "+A;-B")] 56 | [InlineData("foo=\"A#B\"", "foo", "A#B")] 57 | [InlineData("glob = targets\\\\*\\\\*.xml", "glob", "targets\\*\\*.xml")] 58 | [InlineData("file=a.xml", "file", "a.xml")] 59 | [InlineData("foo= .txt=text/plain", "foo", ".txt=text/plain")] 60 | [InlineData("quoted=\"this is an \\\"example\\\" of a nested quote\"", "quoted", "this is an \"example\" of a nested quote")] 61 | [InlineData(" key = value # this is a comment", "key", "value")] 62 | [InlineData("key= \"value has spaces \" ", "key", "value has spaces ")] 63 | [InlineData("connection=\"DefaultEndpointsProtocol=https;AccountName=;AccountKey=;BlobEndpoint=\"", "connection", "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;BlobEndpoint=")] 64 | [InlineData("enabled=1", "enabled", "true")] 65 | [InlineData("enabled=true", "enabled", "true")] 66 | [InlineData("enabled=True", "enabled", "true")] 67 | [InlineData("enabled=TRUE", "enabled", "true")] 68 | [InlineData("enabled=yes", "enabled", "true")] 69 | [InlineData("enabled=Yes", "enabled", "true")] 70 | [InlineData("enabled=YES", "enabled", "true")] 71 | [InlineData("enabled=on", "enabled", "true")] 72 | [InlineData("enabled=On", "enabled", "true")] 73 | [InlineData("enabled=ON", "enabled", "true")] 74 | [InlineData("enabled=0", "enabled", "false")] 75 | [InlineData("enabled=false", "enabled", "false")] 76 | [InlineData("enabled=False", "enabled", "false")] 77 | [InlineData("enabled=FALSE", "enabled", "false")] 78 | [InlineData("enabled=no", "enabled", "false")] 79 | [InlineData("enabled=No", "enabled", "false")] 80 | [InlineData("enabled=NO", "enabled", "false")] 81 | [InlineData("enabled=off", "enabled", "false")] 82 | [InlineData("enabled=Off", "enabled", "false")] 83 | [InlineData("enabled=OFF", "enabled", "false")] 84 | [InlineData("size=10", "size", "10")] 85 | [InlineData("size=2k", "size", "2048")] 86 | [InlineData("size=2kb", "size", "2048")] 87 | [InlineData("size=2K", "size", "2048")] 88 | [InlineData("size=2KB", "size", "2048")] 89 | [InlineData("size=5m", "size", "5242880")] 90 | [InlineData("size=5mb", "size", "5242880")] 91 | [InlineData("size=5M", "size", "5242880")] 92 | [InlineData("size=5MB", "size", "5242880")] 93 | [InlineData("size=500m", "size", "524288000")] 94 | [InlineData("size=1g", "size", "1073741824")] 95 | [InlineData("size=1gb", "size", "1073741824")] 96 | [InlineData("size=1G", "size", "1073741824")] 97 | [InlineData("size=1GB", "size", "1073741824")] 98 | [InlineData("size=5G", "size", "5368709120")] 99 | [InlineData("size=2T", "size", "2199023255552")] 100 | [InlineData("size=2Tb", "size", "2199023255552")] 101 | [InlineData("size=2t", "size", "2199023255552")] 102 | [InlineData("size=2tb", "size", "2199023255552")] 103 | public void can_parse_variable(string input, string name, string value) 104 | { 105 | if (ConfigParser.TryParseVariableLine(input, out var result, out var error, out var position)) 106 | { 107 | Assert.Equal(name, result.Name); 108 | Assert.Equal(value, result.Value); 109 | } 110 | else 111 | { 112 | Assert.True(false, $"at {position.Column}: {error}"); 113 | } 114 | } 115 | 116 | [Theory] 117 | [InlineData("")] 118 | [InlineData("\t")] 119 | [InlineData("variable with spaces = 1")] 120 | [InlineData("variable_with_underscore = 1")] 121 | [InlineData("missing-end-quote = \"")] 122 | [InlineData("must-quote-backslash = \\")] 123 | [InlineData("missing-value = ")] 124 | [InlineData("1variable=1")] 125 | [InlineData("foo= value \"has single quote ")] 126 | [InlineData("foo= value \\has backslash")] 127 | public void cannot_parse_invalid_variable(string input) 128 | { 129 | Assert.False(ConfigParser.TryParseVariableLine(input, out _, out _, out _)); 130 | } 131 | 132 | [Theory] 133 | [InlineData("# this is a comment")] 134 | [InlineData(" # this is a comment")] 135 | [InlineData("\t\t# this is a comment")] 136 | [InlineData("; this is a comment")] 137 | [InlineData(" ; this is a comment")] 138 | [InlineData("\t\t; this is a comment")] 139 | public void can_parse_comment(string input) 140 | { 141 | if (ConfigParser.TryParseCommentLine(input, out var result, out var error, out var position)) 142 | { 143 | Assert.Equal(input, result.Text); 144 | } 145 | else 146 | { 147 | Assert.True(false, $"at {position.Column}: {error}"); 148 | } 149 | } 150 | 151 | [Fact] 152 | public void cannot_parse_non_comment() 153 | { 154 | Assert.False(ConfigParser.TryParseCommentLine("not a comment", out _, out _, out _)); 155 | } 156 | 157 | [Theory] 158 | [InlineData("file.app.config.url", "file.app", "config", "url")] 159 | [InlineData("file.url", "file", null, "url")] 160 | [InlineData("file.\"app.config\".url", "file", "app.config", "url")] 161 | [InlineData("file.\"with spaces\".url", "file", "with spaces", "url")] 162 | [InlineData("file.\"src\\\\app.config\".url", "file", "src\\app.config", "url")] 163 | public void can_parse_key(string key, string section, string subsection, string variable) 164 | { 165 | Assert.True(ConfigParser.TryParseKey(key, out var s, out var ss, out var v, out _)); 166 | Assert.Equal(section, section); 167 | Assert.Equal(subsection, ss); 168 | Assert.Equal(variable, v); 169 | } 170 | 171 | [Theory] 172 | [InlineData("file.app.config", "file.app", "config")] 173 | [InlineData("file", "file", null)] 174 | [InlineData("file.\"app.config\"", "file", "app.config")] 175 | [InlineData("foo.\"bar or baz\"", "file", "bar or baz")] 176 | public void can_parse_section_key(string key, string section, string subsection) 177 | { 178 | Assert.True(ConfigParser.TryParseSection(key, out var s, out var ss, out _)); 179 | Assert.Equal(section, section); 180 | Assert.Equal(subsection, ss); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Config.Tests/ConfigReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace DotNetConfig 9 | { 10 | public class ConfigReaderTests 11 | { 12 | [Theory] 13 | [InlineData("[foo]", "foo", null)] 14 | [InlineData("[foo-bar]", "foo-bar", null)] 15 | [InlineData("[foo \"bar\"]", "foo", "bar")] 16 | [InlineData("[foo] # some comment", "foo", null, "# some comment")] 17 | [InlineData("[foo \"bar\"] # some comment", "foo", "bar", "# some comment")] 18 | [InlineData("[foo \"bar\\baz\"]", "foo", "barbaz")] 19 | [InlineData("# comment", null, null, "# comment")] 20 | [InlineData(" # comment", null, null, "# comment")] 21 | public void can_read_section(string input, string section, string subsection, string? comment = null) 22 | { 23 | using var reader = new ConfigReader(new StringReader(input)); 24 | var line = reader.ReadLine(); 25 | 26 | Assert.NotNull(line); 27 | 28 | if (section != null) 29 | { 30 | Assert.Equal(LineKind.Section, line!.Kind); 31 | Assert.Equal(section, line.Section); 32 | } 33 | 34 | if (subsection != null) 35 | Assert.Equal(subsection, line!.Subsection); 36 | 37 | if (comment != null) 38 | Assert.Equal(comment, line!.Comment); 39 | } 40 | 41 | [Theory] 42 | [InlineData("[foo]", "bar", null, "[bar]")] 43 | [InlineData("[foo \"bar\"]", "bar", "baz", "[bar \"baz\"]")] 44 | [InlineData("[foo] # some comment", "bar", null, "[bar] # some comment")] 45 | [InlineData("[foo \"bar\"] # some comment", "bar", "baz", "[bar \"baz\"] # some comment")] 46 | public void can_replace_section(string input, string section, string subsection, string expected) 47 | { 48 | using var reader = new ConfigReader(new StringReader(input)); 49 | var line = reader.ReadLine(); 50 | 51 | Assert.NotNull(line); 52 | 53 | var updated = line!.WithSection(section, subsection); 54 | 55 | Assert.Equal(expected, updated.LineText); 56 | } 57 | 58 | [Theory] 59 | [InlineData("[", 2, "Expected section name.")] 60 | [InlineData("[1foo]", 2, "Section name must start with a letter.")] 61 | [InlineData("[foo_bar]", 5, "Section name can only contain letters, digits or '-'.")] 62 | [InlineData("[foo;", 5, "Section name can only contain letters, digits or '-'.")] 63 | [InlineData("[foo", 5, "Expected end of section ']' or whitespace followed by quoted subsection name.")] 64 | [InlineData("[foo ", 6, "Expected quoted subsection name.")] 65 | [InlineData("[foo \"bar\\", 11, "Expected closing quote.")] 66 | [InlineData("[foo \"bar", 10, "Expected closing quote and end of section ']'.")] 67 | [InlineData("[foo \"bar\"", 11, "Expected end of section ']'.")] 68 | [InlineData("[foo \"bar\" ", 11, "Expected end of section ']'.")] 69 | public void reports_section_error(string input, int column, string error, params object[] args) 70 | { 71 | using var reader = new ConfigReader(new StringReader(input)); 72 | var line = reader.ReadLine(); 73 | 74 | Assert.NotNull(line); 75 | Assert.Equal(LineKind.Error, line!.Kind); 76 | Assert.Equal(string.Format(error, args), line!.Error); 77 | Assert.Equal(column, line!.ErrorPosition?.Column); 78 | } 79 | 80 | [Theory] 81 | [InlineData("foo", "foo", null)] 82 | [InlineData("foo ", "foo", null)] 83 | [InlineData(" foo ", "foo", null)] 84 | [InlineData("foo # comment", "foo", null, "# comment")] 85 | [InlineData("foo=bar", "foo", "bar")] 86 | [InlineData("foo = bar", "foo", "bar")] 87 | [InlineData("foo-bar=baz", "foo-bar", "baz")] 88 | [InlineData(" foo = bar", "foo", "bar")] 89 | [InlineData("\tfoo=bar", "foo", "bar")] 90 | [InlineData(" foo=ba\\tr", "foo", "ba\tr")] 91 | [InlineData(" foo=bar\\nbaz", "foo", @"bar 92 | baz")] 93 | [InlineData("foo = \"with spaces\"", "foo", "with spaces")] 94 | [InlineData("foo= value has spaces ", "foo", "value has spaces")] 95 | [InlineData("key= \"value has spaces \" ", "key", "value has spaces ")] 96 | [InlineData("foo= value: has colon", "foo", "value: has colon")] 97 | [InlineData("foo=\"+A;-B\"", "foo", "+A;-B")] 98 | [InlineData("foo=\"A#B\"", "foo", "A#B")] 99 | [InlineData("glob = targets\\\\*\\\\*.xml", "glob", "targets\\*\\*.xml")] 100 | [InlineData("file=a.xml", "file", "a.xml")] 101 | [InlineData("foo= .txt=text/plain", "foo", ".txt=text/plain")] 102 | [InlineData("quoted=\"this is \\\"great\\\".\"", "quoted", "this is \"great\".")] 103 | [InlineData(" key = value # this is a comment", "key", "value", "# this is a comment")] 104 | [InlineData("connection=DefaultEndpointsProtocol=https;AccountName=", "connection", "DefaultEndpointsProtocol=https")] 105 | [InlineData("connection=\"DefaultEndpointsProtocol=https;AccountName=;\"", "connection", "DefaultEndpointsProtocol=https;AccountName=;")] 106 | 107 | //[InlineData("foo", "foo", "true")] 108 | //[InlineData("enabled=1", "enabled", "true")] 109 | //[InlineData("enabled=true", "enabled", "true")] 110 | //[InlineData("enabled=True", "enabled", "true")] 111 | //[InlineData("enabled=TRUE", "enabled", "true")] 112 | //[InlineData("enabled=yes", "enabled", "true")] 113 | //[InlineData("enabled=Yes", "enabled", "true")] 114 | //[InlineData("enabled=YES", "enabled", "true")] 115 | //[InlineData("enabled=on", "enabled", "true")] 116 | //[InlineData("enabled=On", "enabled", "true")] 117 | //[InlineData("enabled=ON", "enabled", "true")] 118 | //[InlineData("enabled=0", "enabled", "false")] 119 | //[InlineData("enabled=false", "enabled", "false")] 120 | //[InlineData("enabled=False", "enabled", "false")] 121 | //[InlineData("enabled=FALSE", "enabled", "false")] 122 | //[InlineData("enabled=no", "enabled", "false")] 123 | //[InlineData("enabled=No", "enabled", "false")] 124 | //[InlineData("enabled=NO", "enabled", "false")] 125 | //[InlineData("enabled=off", "enabled", "false")] 126 | //[InlineData("enabled=Off", "enabled", "false")] 127 | //[InlineData("enabled=OFF", "enabled", "false")] 128 | //[InlineData("size=10", "size", "10")] 129 | //[InlineData("size=2k", "size", "2048")] 130 | //[InlineData("size=2kb", "size", "2048")] 131 | //[InlineData("size=2K", "size", "2048")] 132 | //[InlineData("size=2KB", "size", "2048")] 133 | //[InlineData("size=5m", "size", "5242880")] 134 | //[InlineData("size=5mb", "size", "5242880")] 135 | //[InlineData("size=5M", "size", "5242880")] 136 | //[InlineData("size=5MB", "size", "5242880")] 137 | //[InlineData("size=500m", "size", "524288000")] 138 | //[InlineData("size=1g", "size", "1073741824")] 139 | //[InlineData("size=1gb", "size", "1073741824")] 140 | //[InlineData("size=1G", "size", "1073741824")] 141 | //[InlineData("size=1GB", "size", "1073741824")] 142 | //[InlineData("size=5G", "size", "5368709120")] 143 | //[InlineData("size=2T", "size", "2199023255552")] 144 | //[InlineData("size=2Tb", "size", "2199023255552")] 145 | //[InlineData("size=2t", "size", "2199023255552")] 146 | //[InlineData("size=2tb", "size", "2199023255552")] 147 | public void can_read_variable(string input, string name, string value, string? comment = null) 148 | { 149 | using var reader = new ConfigReader(new StringReader("[section]" + Environment.NewLine + input)); 150 | Assert.NotNull(reader.ReadLine()); 151 | 152 | var line = reader.ReadLine(); 153 | 154 | Assert.NotNull(line); 155 | Assert.Equal(LineKind.Variable, line!.Kind); 156 | Assert.Equal(name, line.Variable); 157 | 158 | if (value == null) 159 | Assert.Null(line.Value); 160 | else 161 | Assert.Equal(value, line.Value); 162 | 163 | if (comment != null) 164 | Assert.Equal(comment, line.Comment); 165 | } 166 | 167 | [Theory] 168 | [InlineData("\tfoo", "bar", "\tfoo = bar")] 169 | [InlineData("\tfoo=bar", null, "\tfoo")] 170 | [InlineData("\tfoo # comment", "bar", "\tfoo = bar # comment")] 171 | [InlineData("\tfoo=bar", "baz", "\tfoo = baz")] 172 | [InlineData("\tfoo = bar", "baz", "\tfoo = baz")] 173 | [InlineData("\tfoo-bar=baz", "foo", "\tfoo-bar = foo")] 174 | [InlineData("\tfoo = bar", "baz", "\tfoo = baz")] 175 | [InlineData("\tfoo=b\\tar", "ba\tr", "\tfoo = ba\tr")] 176 | public void can_replace_variable(string input, string value, string expected) 177 | { 178 | using var reader = new ConfigReader(new StringReader("[section]" + Environment.NewLine + input)); 179 | Assert.NotNull(reader.ReadLine()); 180 | 181 | var line = reader.ReadLine(); 182 | 183 | Assert.NotNull(line); 184 | 185 | var updated = line!.WithValue(value); 186 | 187 | Assert.Equal(expected, updated.LineText); 188 | } 189 | 190 | [Fact] 191 | public void reports_required_section() 192 | { 193 | using var reader = new ConfigReader(new StringReader("foo = bar")); 194 | var line = reader.ReadLine(); 195 | 196 | Assert.NotNull(line); 197 | Assert.Equal(LineKind.Error, line!.Kind); 198 | Assert.Equal("Variables must be declared within a section.", line.Error); 199 | } 200 | 201 | [Theory] 202 | [InlineData("1foo", 1, "Variable name must start with a letter.")] 203 | [InlineData("foo_bar", 4, "Variable name can only contain letters, digits or '-'.")] 204 | [InlineData("foo =", 6, "Expected variable value after '='.")] 205 | [InlineData("foo=\"", 5, "Double quotes must be properly balanced or escaped with a backslash.")] 206 | [InlineData("foo=\\", 6, "Incomplete character escape.")] 207 | [InlineData("foo=\"bar\\", 10, "Incomplete character escape.")] 208 | [InlineData("foo = bar \" baz", 11, "Double quotes must be properly balanced or escaped with a backslash.")] 209 | [InlineData("v.1 = 1", 2, "Variable name can only contain letters, digits or '-'.")] 210 | [InlineData("variable with spaces = 1", 9, "Variable name can only contain letters, digits or '-'.")] 211 | [InlineData("variable_with_underscore = 1", 9, "Variable name can only contain letters, digits or '-'.")] 212 | [InlineData("foo=\"bar\\baz\"", 10, "Invalid escape sequence '\\b'.")] 213 | public void reports_variable_error(string input, int column, string error, params object[] args) 214 | { 215 | using var reader = new ConfigReader(new StringReader("[section]" + Environment.NewLine + input)); 216 | Assert.NotNull(reader.ReadLine()); 217 | 218 | var line = reader.ReadLine(); 219 | 220 | Assert.NotNull(line); 221 | Assert.Equal(LineKind.Error, line!.Kind); 222 | Assert.Equal(string.Format(error, args), line.Error); 223 | Assert.Equal(column, line.ErrorPosition?.Column); 224 | } 225 | 226 | [Fact] 227 | public async Task reading_from_multiple_threads_succeeds() 228 | { 229 | Config.GlobalLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "global.netconfig"); 230 | Config.SystemLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "system.netconfig"); 231 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 232 | 233 | var path = Path.Combine(Directory.GetCurrentDirectory(), "Content", ".netconfig"); 234 | 235 | await Task.WhenAll(Enumerable 236 | .Range(0, 5) 237 | .Select(_ => Task.Run(() => Config.Build(path))) 238 | .ToArray()); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/Config.Tests/ConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.IO; 3 | using System.Runtime.CompilerServices; 4 | using Microsoft.Extensions.Configuration; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace DotNetConfig 9 | { 10 | public class ConfigurationTests 11 | { 12 | readonly ITestOutputHelper output; 13 | 14 | public ConfigurationTests(ITestOutputHelper output) 15 | { 16 | Config.GlobalLocation = Path.Combine(Constants.CurrentDirectory, "Content", "global.netconfig"); 17 | Config.SystemLocation = Path.Combine(Constants.CurrentDirectory, "Content", "system.netconfig"); 18 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 19 | this.output = output; 20 | } 21 | 22 | [Fact] 23 | public void can_load_hierarchical_values() 24 | { 25 | var config = BuildConfiguration(); 26 | 27 | Assert.Equal("on", config["core:parent"]); 28 | Assert.Equal("true", config["http:sslVerify"]); 29 | Assert.Equal("false", config["http:https://weak.example.com:sslVerify"]); 30 | Assert.Equal("yay", config["foo:bar:baz"]); 31 | } 32 | 33 | [Fact] 34 | public void can_save_values() 35 | { 36 | var config = BuildConfiguration(); 37 | 38 | config["foo:enabled"] = "true"; 39 | config["foo:bar:baz"] = "bye"; 40 | config["http:https://weaker.example.com:sslVerify"] = "false"; 41 | 42 | var dotnet = Config.Build(Path.Combine(Constants.CurrentDirectory, "Content", "web", nameof(can_save_values))); 43 | 44 | Assert.Equal("true", dotnet.GetString("foo", "enabled")); 45 | Assert.Equal("bye", dotnet.GetString("foo", "bar", "baz")); 46 | Assert.Equal("false", dotnet.GetString("http", "https://weaker.example.com", "sslVerify")); 47 | } 48 | 49 | [Fact] 50 | public void local_values_override_system_values() 51 | { 52 | var config = BuildConfiguration(); 53 | 54 | Assert.Equal("123", config["local:value"]); 55 | } 56 | 57 | [Fact] 58 | public void saves_to_same_level() 59 | { 60 | var dir = Path.Combine(Constants.CurrentDirectory, "Content", "web", nameof(saves_to_same_level)); 61 | Directory.CreateDirectory(dir); 62 | var usr = Path.Combine(dir, Config.FileName + Config.UserExtension); 63 | 64 | Config.Build(usr).SetNumber("local", "value", 999, ConfigLevel.Local); 65 | 66 | var config = BuildConfiguration(); 67 | 68 | Assert.Equal("999", config["local:value"]); 69 | 70 | config["local:value"] = "888"; 71 | 72 | Assert.Equal("888", config["local:value"]); 73 | Assert.Contains("888", File.ReadAllText(usr)); 74 | } 75 | 76 | IConfiguration BuildConfiguration([CallerMemberName] string? methodName = null) 77 | { 78 | var dir = Path.Combine(Constants.CurrentDirectory, "Content", "web", methodName!); 79 | Directory.CreateDirectory(dir); 80 | 81 | return new ConfigurationBuilder().AddDotNetConfig(dir).Build(); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/Config.Tests/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace DotNetConfig 4 | { 5 | #pragma warning disable CS0436 // ThisAssembly Type conflicts with imported type (IVT) 6 | static class Constants 7 | { 8 | public static string CurrentDirectory { get; } = Path.Combine( 9 | ThisAssembly.Project.MSBuildProjectDirectory, 10 | ThisAssembly.Project.OutputPath); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Config.Tests/Content/.netconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | timeout = 60 3 | parent = on 4 | 5 | [foo "bar"] 6 | baz = yay -------------------------------------------------------------------------------- /src/Config.Tests/Content/ConfigParserTests.txt: -------------------------------------------------------------------------------- 1 | [foo.bar "works on c:\\ \"baz\""] ; some / "comment can ; have # anything \" in it. 2 | foo = bar ; this is a comment 3 | # pure comment line 4 | bar = "a \"quoted\" value" 5 | path="c:\\root\\path\\" 6 | timeout=120 7 | length=60kb 8 | enabled = true 9 | valueless 10 | -------------------------------------------------------------------------------- /src/Config.Tests/Content/global.netconfig: -------------------------------------------------------------------------------- 1 | [http "https://weak.example.com"] 2 | sslVerify = false 3 | [core] 4 | global = yes 5 | [local] 6 | value = 456 -------------------------------------------------------------------------------- /src/Config.Tests/Content/local/.netconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | filemode = true 3 | local = 1 4 | -------------------------------------------------------------------------------- /src/Config.Tests/Content/system.netconfig: -------------------------------------------------------------------------------- 1 | [http] 2 | sslVerify 3 | [core] 4 | system = yes 5 | -------------------------------------------------------------------------------- /src/Config.Tests/Content/web/.netconfig: -------------------------------------------------------------------------------- 1 | [local] 2 | value = 123 -------------------------------------------------------------------------------- /src/Config.Tests/FlakyFactAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace DotNetConfig 5 | { 6 | public class FlakyFactAttribute : FactAttribute 7 | { 8 | public FlakyFactAttribute() 9 | { 10 | if (bool.TryParse(Environment.GetEnvironmentVariable("CI"), out var ci) && ci) 11 | Skip = "Flaky test that is not run on CI"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Config.Tests/TextRulesTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace DotNetConfig 4 | { 5 | public class TextRulesTests 6 | { 7 | [Theory] 8 | [InlineData("1", true)] 9 | [InlineData("true", true)] 10 | [InlineData("True", true)] 11 | [InlineData("TRUE", true)] 12 | [InlineData("yes", true)] 13 | [InlineData("Yes", true)] 14 | [InlineData("YES", true)] 15 | [InlineData("on", true)] 16 | [InlineData("On", true)] 17 | [InlineData("ON", true)] 18 | [InlineData("0", false)] 19 | [InlineData("false", false)] 20 | [InlineData("False", false)] 21 | [InlineData("FALSE", false)] 22 | [InlineData("no", false)] 23 | [InlineData("No", false)] 24 | [InlineData("NO", false)] 25 | [InlineData("off", false)] 26 | [InlineData("Off", false)] 27 | [InlineData("OFF", false)] 28 | public void can_parse_boolean(string value, bool expected) 29 | { 30 | Assert.Equal(expected, TextRules.ParseBoolean(value)); 31 | } 32 | 33 | [Theory] 34 | [InlineData("10", "10")] 35 | [InlineData("2k", "2048")] 36 | [InlineData("2kb", "2048")] 37 | [InlineData("2K", "2048")] 38 | [InlineData("2KB", "2048")] 39 | [InlineData("5m", "5242880")] 40 | [InlineData("5mb", "5242880")] 41 | [InlineData("5M", "5242880")] 42 | [InlineData("5MB", "5242880")] 43 | [InlineData("500m", "524288000")] 44 | [InlineData("1g", "1073741824")] 45 | [InlineData("1gb", "1073741824")] 46 | [InlineData("1G", "1073741824")] 47 | [InlineData("1GB", "1073741824")] 48 | [InlineData("5G", "5368709120")] 49 | [InlineData("2T", "2199023255552")] 50 | [InlineData("2Tb", "2199023255552")] 51 | [InlineData("2t", "2199023255552")] 52 | [InlineData("2tb", "2199023255552")] 53 | public void can_parse_number(string value, string expected) 54 | { 55 | Assert.Equal(long.Parse(expected), TextRules.ParseNumber(value)); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Config.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "appDomain": "denied", 3 | "maxParallelThreads": "1" 4 | } 5 | -------------------------------------------------------------------------------- /src/Config.Tool/Config.Tool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | A global tool for managing hierarchical configurations for dotnet tools, using git config format. 5 | 6 | Exe 7 | net6.0;net8.0 8 | 9 | dotnet-config 10 | DotNetConfig 11 | 12 | dotnet-config 13 | dotnet-config 14 | true 15 | enable 16 | 17 | $(NoWarn);CA1831 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Config.Tool/ConfigAction.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetConfig 2 | { 3 | enum ConfigAction 4 | { 5 | None, 6 | Add, 7 | Get, 8 | GetAll, 9 | GetRegexp, 10 | Set, 11 | SetAll, // aka Replace-All 12 | Unset, 13 | UnsetAll, 14 | 15 | List, 16 | Edit, 17 | 18 | // TODO: 19 | RenameSection, 20 | RemoveSection, 21 | /* 22 | --get get value: name [value-regex] 23 | --get-all get all values: key [value-regex] 24 | --get-regexp get values for regexp: name-regex [value-regex] 25 | --get-urlmatch get value specific for the URL: section[.var] URL 26 | --replace-all replace all matching variables: name value [value_regex] 27 | --add add a new variable: name value 28 | --unset remove a variable: name [value-regex] 29 | --unset-all remove all matches: name [value-regex] 30 | --rename-section rename section: old-name new-name 31 | --remove-section remove a section: name 32 | -l, --list list all 33 | -e, --edit open an editor 34 | */ 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Config.Tool/Properties/.gitignore: -------------------------------------------------------------------------------- 1 | launchSettings.json -------------------------------------------------------------------------------- /src/Config.Tool/ValueKind.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetConfig 2 | { 3 | enum ValueKind 4 | { 5 | Boolean, 6 | DateTime, 7 | Number, 8 | String, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Config.Tool/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Config/AggregateConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DotNetConfig 6 | { 7 | class AggregateConfig : Config 8 | { 9 | public AggregateConfig(params Config[] configs) 10 | : base(configs.SkipWhile(c => c.Level == ConfigLevel.Local).FirstOrDefault()?.FilePath ?? throw new ArgumentException()) 11 | => Files = configs.ToList(); 12 | 13 | public List Files { get; } 14 | 15 | public override Config AddBoolean(string section, string? subsection, string variable, bool value) 16 | => GetConfig().AddBoolean(section, subsection, variable, value); 17 | 18 | public override Config AddDateTime(string section, string? subsection, string variable, DateTime value) 19 | => GetConfig().AddDateTime(section, subsection, variable, value); 20 | 21 | public override Config AddNumber(string section, string? subsection, string variable, long value) 22 | => GetConfig().AddNumber(section, subsection, variable, value); 23 | 24 | public override Config AddString(string section, string? subsection, string variable, string value) 25 | => GetConfig().AddString(section, subsection, variable, value); 26 | 27 | public override IEnumerable GetAll(string section, string? subsection, string variable, string? valueRegex) 28 | => Files.SelectMany(x => x.GetAll(section, subsection, variable, valueRegex)); 29 | 30 | public override IEnumerable GetRegex(string nameRegex, string? valueRegex = null) 31 | => Files.SelectMany(x => x.GetRegex(nameRegex, valueRegex)); 32 | 33 | public override string? GetNormalizedPath(string section, string? subsection, string variable) 34 | => Files.Select(config => config.GetNormalizedPath(section, subsection, variable)).Where(x => x != null).FirstOrDefault(); 35 | 36 | public override Config RemoveSection(string section, string? subsection = null) 37 | => GetConfig().RemoveSection(section, subsection); 38 | 39 | public override Config RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection) 40 | => GetConfig().RenameSection(oldSection, oldSubsection, newSection, newSubsection); 41 | 42 | public override Config SetAllBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) 43 | => GetConfig().SetAllBoolean(section, subsection, variable, value, valueRegex); 44 | 45 | public override Config SetAllDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) 46 | => GetConfig().SetAllDateTime(section, subsection, variable, value, valueRegex); 47 | 48 | public override Config SetAllNumber(string section, string? subsection, string variable, long value, string? valueRegex) 49 | => GetConfig().SetAllNumber(section, subsection, variable, value, valueRegex); 50 | 51 | public override Config SetAllString(string section, string? subsection, string variable, string value, string? valueRegex) 52 | => GetConfig().SetAllString(section, subsection, variable, value, valueRegex); 53 | 54 | public override Config SetBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) 55 | => GetConfig().SetBoolean(section, subsection, variable, value, valueRegex); 56 | 57 | public override Config SetDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) 58 | => GetConfig().SetDateTime(section, subsection, variable, value, valueRegex); 59 | 60 | public override Config SetNumber(string section, string? subsection, string variable, long value, string? valueRegex) 61 | => GetConfig().SetNumber(section, subsection, variable, value, valueRegex); 62 | 63 | public override Config SetString(string section, string? subsection, string variable, string value, string? valueRegex) 64 | => GetConfig().SetString(section, subsection, variable, value, valueRegex); 65 | 66 | public override bool TryGetBoolean(string section, string? subsection, string variable, out bool value) 67 | { 68 | foreach (var config in Files) 69 | { 70 | if (config.TryGetBoolean(section, subsection, variable, out value)) 71 | return true; 72 | } 73 | 74 | value = false; 75 | return false; 76 | } 77 | 78 | public override bool TryGetDateTime(string section, string? subsection, string variable, out DateTime value) 79 | { 80 | foreach (var config in Files) 81 | { 82 | if (config.TryGetDateTime(section, subsection, variable, out value)) 83 | return true; 84 | } 85 | 86 | value = DateTime.MinValue; 87 | return false; 88 | } 89 | 90 | public override bool TryGetNumber(string section, string? subsection, string variable, out long value) 91 | { 92 | foreach (var config in Files) 93 | { 94 | if (config.TryGetNumber(section, subsection, variable, out value)) 95 | return true; 96 | } 97 | 98 | value = 0; 99 | return false; 100 | } 101 | 102 | public override bool TryGetString(string section, string? subsection, string variable, out string value) 103 | { 104 | foreach (var config in Files) 105 | { 106 | if (config.TryGetString(section, subsection, variable, out value)) 107 | return true; 108 | } 109 | 110 | value = ""; 111 | return false; 112 | } 113 | 114 | public override Config Unset(string section, string? subsection, string variable) 115 | => GetConfig().Unset(section, subsection, variable); 116 | 117 | public override Config UnsetAll(string section, string? subsection, string variable, string? valueRegex) 118 | => GetConfig().UnsetAll(section, subsection, variable, valueRegex); 119 | 120 | // When writing via the aggregate config without specifying a ConfigLevel, we first try the default 121 | // .netconfig location, followed by a non-local one (i.e. global/system if that's the first we find), 122 | // followed by whatever is first last. 123 | Config GetConfig() 124 | => Files.FirstOrDefault(x => x.Level == null) ?? 125 | Files.FirstOrDefault(x => x.Level != ConfigLevel.Local) ?? 126 | Files.First(); 127 | 128 | protected override IEnumerable GetEntries() => Files.SelectMany(x => x); 129 | 130 | public override string ToString() => string.Join(", ", Files.Select(x => x.FilePath)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Config/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DotNetConfig.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010071207e0121c41cd25ecdf4dffe275b3a055b03e9f009f778b6bd0f0fe6643ac89ca3eeddf6d136496c4cd0defa1fcff361cc2c2c0d0a8f1b6ff92c15e661dee0acde682c4dcf78b7a30edd65737b54da568f4ec76b66827ce019093b9dedf80214b1a3d63d5289d542b3b218d7fe537d6da628d2718307190a5993d7fca0e3b1")] 4 | [assembly: InternalsVisibleTo("dotnet-config, PublicKey=002400000480000094000000060200000024000052534131000400000100010071207e0121c41cd25ecdf4dffe275b3a055b03e9f009f778b6bd0f0fe6643ac89ca3eeddf6d136496c4cd0defa1fcff361cc2c2c0d0a8f1b6ff92c15e661dee0acde682c4dcf78b7a30edd65737b54da568f4ec76b66827ce019093b9dedf80214b1a3d63d5289d542b3b218d7fe537d6da628d2718307190a5993d7fca0e3b1")] 5 | -------------------------------------------------------------------------------- /src/Config/Config.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | APIs for handling dotnet-config compatible settings for any dotnet tool. 5 | 6 | Usage: 7 | var config = Config.Build(); 8 | var value = config.GetString("section", "subsection", "variable"); 9 | 10 | // Setting values 11 | config 12 | .SetString("section", "subsection", "variable", value) 13 | .SetBoolean("section", "subsection", "enabled", true); 14 | 15 | 16 | netstandard2.0 17 | DotNetConfig 18 | DotNetConfig 19 | DotNetConfig 20 | true 21 | true 22 | true 23 | 24 | 25 | 26 | ../../docs/docfx.json 27 | ../.. 28 | ../../docs/_site 29 | $(MSBuildProjectExtensionsPath)/obj/docfx.log 30 | Info 31 | false 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Config/ConfigEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Globalization; 4 | 5 | namespace DotNetConfig 6 | { 7 | /// 8 | /// Represents a configuration option. 9 | /// 10 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 11 | public record ConfigEntry 12 | { 13 | /// 14 | /// Initializes a new instance of the class with a given key, value and store level. 15 | /// 16 | /// The section containing the variable. 17 | /// Optional subsection containing the variable. 18 | /// The variable name. 19 | /// The variable value. 20 | /// The origin store. 21 | public ConfigEntry(string section, string? subsection, string name, string? value, ConfigLevel? level) 22 | { 23 | Section = section; 24 | Subsection = subsection; 25 | Variable = name; 26 | RawValue = value; 27 | Level = level; 28 | } 29 | 30 | /// 31 | /// Gets the section containing the entry. 32 | /// 33 | public string Section { get; } 34 | 35 | /// 36 | /// Gets the optional subsection containing the entry. 37 | /// 38 | public string? Subsection { get; } 39 | 40 | /// 41 | /// Gets the variable name. 42 | /// 43 | public string Variable { get; } 44 | 45 | /// 46 | /// Gets the variable raw value. 47 | /// 48 | public string? RawValue { get; init; } 49 | 50 | /// 51 | /// Gets the origin store. if not either 52 | /// or . 53 | /// 54 | public ConfigLevel? Level { get; } 55 | 56 | /// 57 | /// Gets or sets the optional comment. 58 | /// 59 | public string? Comment { get; init; } 60 | 61 | /// 62 | /// Gets the key for the entry. 63 | /// 64 | public string Key => TextRules.ToKey(Section, Subsection, Variable); 65 | 66 | /// 67 | /// Gets the typed value for the entry. 68 | /// 69 | /// The corresponding to the . 70 | /// The cannot be converted to . 71 | public bool GetBoolean() => TextRules.ParseBoolean(RawValue); 72 | 73 | /// 74 | /// Gets the typed value for the entry. 75 | /// 76 | /// The corresponding to the . 77 | /// The cannot be converted to . 78 | public DateTime GetDateTime() => DateTime.Parse(RawValue ?? throw new FormatException(Key), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); 79 | 80 | /// 81 | /// Gets the typed value for the entry. 82 | /// 83 | /// The corresponding to the . 84 | /// The cannot be converted to . 85 | public long GetNumber() => TextRules.ParseNumber(RawValue ?? throw new FormatException(Key)); 86 | 87 | /// 88 | /// Gets the value for the entry. 89 | /// 90 | /// The from the . 91 | /// The cannot be converted to , because it does not have a value. 92 | public string GetString() => RawValue ?? throw new FormatException(Key); 93 | 94 | string DebuggerDisplay => $"{Key} = {RawValue}"; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Config/ConfigLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetConfig 4 | { 5 | /// 6 | /// Specifies the level of configuration to use. 7 | /// 8 | /// 9 | /// If not provided, the default .netconfig location will be used, 10 | /// which is typically the current directory unless building configuration 11 | /// from a specific file. 12 | /// 13 | public enum ConfigLevel 14 | { 15 | /// 16 | /// Use a .netconfig.user file, instead of the default .netconfig, 17 | /// which allows separating local-only settings from potentially 18 | /// team-wide configuration files that can be checked-in source control. 19 | /// 20 | Local, 21 | 22 | /// 23 | /// The global ~/.netconfig for the current user, from the 24 | /// location. 25 | /// 26 | /// 27 | Global, 28 | 29 | /// 30 | /// The system wide .netconfig, from the 31 | /// location. 32 | /// 33 | /// 34 | System, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Config/ConfigSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DotNetConfig 5 | { 6 | /// 7 | /// Provides access to a specific section and optional subsection. 8 | /// 9 | public record ConfigSection 10 | { 11 | internal ConfigSection(Config config, string section, string? subsection) 12 | => (Config, Section, Subsection) 13 | = (config, section, subsection); 14 | 15 | internal Config Config { get; init; } 16 | 17 | /// 18 | /// Gets the section name. 19 | /// 20 | public string Section { get; } 21 | 22 | /// 23 | /// Gets the subsection name (if any). 24 | /// 25 | public string? Subsection { get; } 26 | 27 | /// 28 | /// Adds a value to a multi-valued variable in the current section/subsection. 29 | /// 30 | /// The variable to assign. 31 | /// Value add to the variable. 32 | public ConfigSection AddBoolean(string variable, bool value) 33 | => this with { Config = Config.AddBoolean(Section, Subsection, variable, value) }; 34 | 35 | /// 36 | /// Adds a value to a multi-valued variable in the current section/subsection. 37 | /// 38 | /// The variable to assign. 39 | /// Value add to the variable. 40 | public ConfigSection AddDateTime(string variable, DateTime value) 41 | => this with { Config = Config.AddDateTime(Section, Subsection, variable, value) }; 42 | 43 | /// 44 | /// Adds a value to a multi-valued variable in the current section/subsection. 45 | /// 46 | /// The variable to assign. 47 | /// Value add to the variable. 48 | public ConfigSection AddNumber(string variable, long value) 49 | => this with { Config = Config.AddNumber(Section, Subsection, variable, value) }; 50 | 51 | /// 52 | /// Adds a value to a multi-valued variable in the current section/subsection. 53 | /// 54 | /// The variable to assign. 55 | /// Value add to the variable. 56 | public ConfigSection AddString(string variable, string value) 57 | => this with { Config = Config.AddString(Section, Subsection, variable, value) }; 58 | 59 | /// 60 | /// Gets all values from a multi-valued variable from the current section/subsection, 61 | /// which optionally match the given value regular expression. 62 | /// 63 | /// The variable to remove. 64 | /// Filter returned entries to those where the value matches the given expression. 65 | public IEnumerable GetAll(string variable, string? valueRegex = null) 66 | => Config.GetAll(Section, Subsection, variable, valueRegex); 67 | 68 | /// 69 | /// Gets a string variable and applies path normalization to it, resolving 70 | /// relative paths and normalizing directory separator characters to the 71 | /// current platform. 72 | /// 73 | /// The variable to retrieve as a resolved path. 74 | /// if the value was found, otherwise. 75 | public string? GetNormalizedPath(string variable) => Config.GetNormalizedPath(Section, Subsection, variable); 76 | 77 | /// 78 | /// Sets the value of all matching variables in the current section/subsection. 79 | /// 80 | /// The variable to assign. 81 | /// Value to assign to the matching variables. 82 | /// Filter returned entries to those where the value matches the given expression. 83 | public ConfigSection SetAllBoolean(string variable, bool value, string? valueRegex = null) 84 | => this with { Config = Config.SetAllBoolean(Section, Subsection, variable, value, valueRegex) }; 85 | 86 | /// 87 | /// Sets the value of all matching variables in the current section/subsection. 88 | /// 89 | /// The variable to assign. 90 | /// Value to assign to the matching variables. 91 | /// Filter returned entries to those where the value matches the given expression. 92 | public ConfigSection SetAllDateTime(string variable, DateTime value, string? valueRegex = null) 93 | => this with { Config = Config.SetAllDateTime(Section, Subsection, variable, value, valueRegex) }; 94 | 95 | /// 96 | /// Sets the value of all matching variables in the current section/subsection. 97 | /// 98 | /// The variable to assign. 99 | /// Value to assign to the matching variables. 100 | /// Filter returned entries to those where the value matches the given expression. 101 | public ConfigSection SetAllNumber(string variable, long value, string? valueRegex = null) 102 | => this with { Config = Config.SetAllNumber(Section, Subsection, variable, value, valueRegex) }; 103 | 104 | /// 105 | /// Sets the value of all matching variables in the current section/subsection. 106 | /// 107 | /// The variable to assign. 108 | /// Value to assign to the matching variables. 109 | /// Filter returned entries to those where the value matches the given expression. 110 | public ConfigSection SetAllString(string variable, string value, string? valueRegex = null) 111 | => this with { Config = Config.SetAllString(Section, Subsection, variable, value, valueRegex) }; 112 | 113 | /// 114 | /// Sets the value of a variable in the current section/subsection. 115 | /// 116 | /// The variable to assign. 117 | /// Value to assign to the variable. 118 | /// Filter returned entries to those where the value matches the given expression. 119 | public ConfigSection SetBoolean(string variable, bool value, string? valueRegex = null) 120 | => this with { Config = Config.SetBoolean(Section, Subsection, variable, value, valueRegex) }; 121 | 122 | /// 123 | /// Sets the value of a variable in the current section/subsection. 124 | /// 125 | /// The variable to assign. 126 | /// Value to assign to the variable. 127 | /// Filter returned entries to those where the value matches the given expression. 128 | public ConfigSection SetDateTime(string variable, DateTime value, string? valueRegex = null) 129 | => this with { Config = Config.SetDateTime(Section, Subsection, variable, value, valueRegex) }; 130 | 131 | /// 132 | /// Sets the value of a variable in the current section/subsection. 133 | /// 134 | /// The variable to assign. 135 | /// Value to assign to the variable. 136 | /// Filter returned entries to those where the value matches the given expression. 137 | public ConfigSection SetNumber(string variable, long value, string? valueRegex = null) 138 | => this with { Config = Config.SetNumber(Section, Subsection, variable, value, valueRegex) }; 139 | 140 | /// 141 | /// Sets the value of a variable in the current section/subsection. 142 | /// 143 | /// The variable to assign. 144 | /// Value to assign to the variable. 145 | /// Filter returned entries to those where the value matches the given expression. 146 | public ConfigSection SetString(string variable, string value, string? valueRegex = null) 147 | => this with { Config = Config.SetString(Section, Subsection, variable, value, valueRegex) }; 148 | 149 | /// 150 | /// Tries to retrieve a variable value from configuration. 151 | /// 152 | /// The variable to retrieve. 153 | /// The variable value if found. 154 | /// if the value was found, otherwise. 155 | public bool TryGetBoolean(string variable, out bool value) => Config.TryGetBoolean(Section, Subsection, variable, out value); 156 | 157 | /// 158 | /// Tries to retrieve a variable value from configuration. 159 | /// 160 | /// The variable to retrieve. 161 | /// The variable value if found. 162 | /// if the value was found, otherwise. 163 | public bool TryGetDateTime(string variable, out DateTime value) => Config.TryGetDateTime(Section, Subsection, variable, out value); 164 | 165 | /// 166 | /// Tries to retrieve a variable value from configuration. 167 | /// 168 | /// The variable to retrieve. 169 | /// The variable value if found. 170 | /// if the value was found, otherwise. 171 | public bool TryGetNumber(string variable, out long value) => Config.TryGetNumber(Section, Subsection, variable, out value); 172 | 173 | /// 174 | /// Tries to retrieve a variable value from configuration. 175 | /// 176 | /// The variable to retrieve. 177 | /// The variable value if found. 178 | /// if the value was found, otherwise. 179 | public bool TryGetString(string variable, out string value) => Config.TryGetString(Section, Subsection, variable, out value); 180 | 181 | /// 182 | /// Removes a variable from the current section/subsection. 183 | /// 184 | /// The variable to remove. 185 | public ConfigSection Unset(string variable) 186 | => this with { Config = Config.Unset(Section, Subsection, variable) }; 187 | 188 | /// 189 | /// Removes all values from a multi-valued variable from the current section/subsection. 190 | /// 191 | /// The variable to remove. 192 | /// Filter returned entries to those where the value matches the given expression. 193 | public ConfigSection UnsetAll(string variable, string? valueRegex = null) 194 | => this with { Config = Config.UnsetAll(Section, Subsection, variable, valueRegex) }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Config/FileConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace DotNetConfig 7 | { 8 | class FileConfig : Config 9 | { 10 | ConfigDocument document; 11 | 12 | public FileConfig(string filePath, ConfigLevel? level = null) 13 | : base(filePath) 14 | => document = ConfigDocument.FromFile(filePath, level); 15 | 16 | FileConfig(string filePath, ConfigDocument document) : base(filePath) => this.document = document; 17 | 18 | public override Config AddBoolean(string section, string? subsection, string variable, bool value) 19 | { 20 | if (value) 21 | { 22 | // Shortcut notation. 23 | document.Add(section, subsection, variable, null).Save(); 24 | } 25 | else 26 | { 27 | document.Add(section, subsection, variable, "false").Save(); 28 | } 29 | 30 | return this; 31 | } 32 | 33 | public override Config AddDateTime(string section, string? subsection, string variable, DateTime value) 34 | { 35 | document.Add(section, subsection, variable, value.ToString("O")).Save(); 36 | return this; 37 | } 38 | 39 | public override Config AddNumber(string section, string? subsection, string variable, long value) 40 | { 41 | document.Add(section, subsection, variable, value.ToString()).Save(); 42 | return this; 43 | } 44 | 45 | public override Config AddString(string section, string? subsection, string variable, string value) 46 | { 47 | document.Add(section, subsection, variable, value).Save(); 48 | return this; 49 | } 50 | 51 | public override IEnumerable GetAll(string section, string? subsection, string variable, string? valueRegex) 52 | => document.GetAll(section, subsection, variable, valueRegex); 53 | 54 | public override IEnumerable GetRegex(string nameRegex, string? valueRegex = null) 55 | => document.GetAll(nameRegex, valueRegex); 56 | 57 | public override string? GetNormalizedPath(string section, string? subsection, string variable) 58 | { 59 | if (!TryGetString(section, subsection, variable, out var value)) 60 | return null; 61 | 62 | // For Windows, FileInfo.FullName takes care of converting \ to / already. 63 | if (Environment.OSVersion.Platform != PlatformID.Win32NT) 64 | value = value.Replace('\\', '/'); 65 | 66 | if (Path.IsPathRooted(value)) 67 | return new FileInfo(value).FullName; 68 | 69 | return new FileInfo(Path.Combine(Path.GetDirectoryName(FilePath), value)).FullName; 70 | } 71 | 72 | public override Config RemoveSection(string section, string? subsection) 73 | { 74 | document.RemoveSection(section, subsection).Save(); 75 | return this; 76 | } 77 | 78 | public override Config RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection) 79 | { 80 | document.RenameSection(oldSection, oldSubsection, newSection, newSubsection).Save(); 81 | return this; 82 | } 83 | 84 | public override Config SetAllBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) 85 | { 86 | if (value) 87 | { 88 | // Shortcut notation. 89 | document.SetAll(section, subsection, variable, null, valueRegex).Save(); 90 | } 91 | else 92 | { 93 | document.SetAll(section, subsection, variable, "false", valueRegex).Save(); 94 | } 95 | 96 | return this; 97 | } 98 | 99 | public override Config SetAllDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) 100 | { 101 | document.SetAll(section, subsection, variable, value.ToString("O"), valueRegex).Save(); 102 | return this; 103 | } 104 | 105 | public override Config SetAllNumber(string section, string? subsection, string variable, long value, string? valueRegex) 106 | { 107 | document.SetAll(section, subsection, variable, value.ToString(), valueRegex).Save(); 108 | return this; 109 | } 110 | 111 | public override Config SetAllString(string section, string? subsection, string variable, string value, string? valueRegex) 112 | { 113 | document.SetAll(section, subsection, variable, value, valueRegex).Save(); 114 | return this; 115 | } 116 | 117 | public override Config SetBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) 118 | { 119 | if (value) 120 | { 121 | // Shortcut notation. 122 | document.Set(section, subsection, variable, null, valueRegex).Save(); 123 | } 124 | else 125 | { 126 | document.Set(section, subsection, variable, "false", valueRegex).Save(); 127 | } 128 | 129 | return this; 130 | } 131 | 132 | public override Config SetDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) 133 | { 134 | document.Set(section, subsection, variable, value.ToString("O"), valueRegex).Save(); 135 | return this; 136 | } 137 | 138 | public override Config SetNumber(string section, string? subsection, string variable, long value, string? valueRegex) 139 | { 140 | document.Set(section, subsection, variable, value.ToString(), valueRegex).Save(); 141 | return this; 142 | } 143 | 144 | public override Config SetString(string section, string? subsection, string variable, string value, string? valueRegex) 145 | { 146 | document.Set(section, subsection, variable, value, valueRegex).Save(); 147 | return this; 148 | } 149 | 150 | public override bool TryGetBoolean(string section, string? subsection, string variable, out bool value) 151 | { 152 | var entry = document.FirstOrDefault(x => 153 | string.Equals(section, x.Section, StringComparison.OrdinalIgnoreCase) && 154 | string.Equals(x.Subsection, subsection) && 155 | variable.Equals(x.Variable)); 156 | 157 | if (entry == null) 158 | { 159 | value = false; 160 | return false; 161 | } 162 | 163 | value = entry.GetBoolean(); 164 | return true; 165 | } 166 | 167 | public override bool TryGetDateTime(string section, string? subsection, string variable, out DateTime value) 168 | { 169 | var entry = document.FirstOrDefault(x => 170 | string.Equals(section, x.Section, StringComparison.OrdinalIgnoreCase) && 171 | string.Equals(x.Subsection, subsection) && 172 | variable.Equals(x.Variable)); 173 | 174 | if (entry == null) 175 | { 176 | value = DateTime.MinValue; 177 | return false; 178 | } 179 | 180 | value = entry.GetDateTime(); 181 | return true; 182 | } 183 | 184 | public override bool TryGetNumber(string section, string? subsection, string variable, out long value) 185 | { 186 | var entry = document.FirstOrDefault(x => 187 | string.Equals(section, x.Section, StringComparison.OrdinalIgnoreCase) && 188 | string.Equals(x.Subsection, subsection) && 189 | variable.Equals(x.Variable)); 190 | 191 | if (entry == null) 192 | { 193 | value = 0; 194 | return false; 195 | } 196 | 197 | value = entry.GetNumber(); 198 | return true; 199 | } 200 | 201 | public override bool TryGetString(string section, string? subsection, string variable, out string value) 202 | { 203 | var entry = document.FirstOrDefault(x => 204 | string.Equals(section, x.Section, StringComparison.OrdinalIgnoreCase) && 205 | string.Equals(x.Subsection, subsection) && 206 | variable.Equals(x.Variable)); 207 | 208 | if (entry == null) 209 | { 210 | value = ""; 211 | return false; 212 | } 213 | 214 | value = entry.RawValue ?? throw new ArgumentNullException(entry.Key); 215 | return true; 216 | } 217 | 218 | public override Config Unset(string section, string? subsection, string variable) 219 | { 220 | document.Unset(section, subsection, variable).Save(); 221 | return this; 222 | } 223 | 224 | public override Config UnsetAll(string section, string? subsection, string variable, string? valueMatcher) 225 | { 226 | document.UnsetAll(section, subsection, variable, valueMatcher).Save(); 227 | return this; 228 | } 229 | 230 | protected override IEnumerable GetEntries() => document; 231 | 232 | public override string ToString() => FilePath; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Config/Line.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace DotNetConfig 5 | { 6 | record Line 7 | { 8 | public static Line CreateSection(string? filePath, int lineNumber, string section, string? subsection) 9 | { 10 | var lineText = subsection == null ? 11 | "[" + section + "]" : 12 | "[" + section + " \"" + TextRules.SerializeSubsection(subsection) + "\"]"; 13 | 14 | return ConfigReader.ParseSection(filePath, lineText, lineNumber); 15 | } 16 | 17 | public static Line CreateVariable(string? filePath, int lineNumber, TextSpan? section, TextSpan? subsection, string name, string? value) 18 | { 19 | var lineText = value == null ? 20 | "\t" + name : 21 | "\t" + name + " = " + TextRules.SerializeValue(value); 22 | 23 | return ConfigReader.ParseVariable(filePath, lineText, lineNumber, section, subsection); 24 | } 25 | 26 | public Line( 27 | string? filePath, LineKind kind, 28 | int lineNumber, string lineText, 29 | TextSpan? section = null, TextSpan? subsection = null, 30 | TextSpan? name = null, TextSpan? value = null, 31 | TextSpan? comment = null, 32 | string? error = null, 33 | Position? errorPosition = null) 34 | { 35 | FilePath = filePath; 36 | Kind = kind; 37 | LineNumber = lineNumber; 38 | LineText = lineText; 39 | Section = section; 40 | Subsection = subsection; 41 | Variable = name; 42 | Value = value; 43 | Comment = comment; 44 | Error = error; 45 | ErrorPosition = errorPosition; 46 | } 47 | 48 | public string? FilePath { get; } 49 | 50 | public LineKind Kind { get; } 51 | 52 | public int LineNumber { get; } 53 | 54 | public string LineText { get; private init; } 55 | 56 | public TextSpan? Section { get; private init; } 57 | 58 | public TextSpan? Subsection { get; private init; } 59 | 60 | public TextSpan? Variable { get; private init; } 61 | 62 | public TextSpan? Value { get; private init; } 63 | 64 | public TextSpan? Comment { get; private init; } 65 | 66 | public string? Error { get; } 67 | 68 | public Position? ErrorPosition { get; } 69 | 70 | public override string ToString() => LineText; 71 | 72 | internal Line WithSection(string section, string? subsection) 73 | { 74 | var original = LineText; 75 | var builder = new StringBuilder(original.Length + section.Length + (subsection == null ? 0 : subsection.Length)); 76 | 77 | if (Section == null) 78 | throw new InvalidOperationException("Expected non-null Section"); 79 | 80 | builder.Append(original.Substring(0, Section.Position.Absolute)); 81 | builder.Append(section); 82 | 83 | if (Subsection != null) 84 | { 85 | var start = Section.Position.Absolute + Section.Length; 86 | var length = Subsection.Position.Absolute - start; 87 | 88 | builder.Append(original.Substring(start, length)); 89 | 90 | if (subsection != null) 91 | builder.Append('\"').Append(TextRules.SerializeSubsection(subsection)).Append('\"'); 92 | 93 | builder.Append(original.Substring(Subsection.Position.Absolute + Subsection.Length)); 94 | } 95 | else 96 | { 97 | if (subsection != null) 98 | builder.Append(" \"").Append(TextRules.SerializeSubsection(subsection)).Append('\"'); 99 | 100 | builder.Append(original.Substring(Section.Position.Absolute + Section.Length)); 101 | } 102 | 103 | var lineText = builder.ToString(); 104 | var parsed = ConfigReader.ParseSection(FilePath, lineText, LineNumber); 105 | 106 | return this with 107 | { 108 | LineText = lineText, 109 | Section = parsed.Section, 110 | Subsection = parsed.Subsection, 111 | Comment = parsed.Comment, 112 | }; 113 | } 114 | 115 | internal Line WithSection(TextSpan section, TextSpan? subsection) 116 | => this with 117 | { 118 | Section = section, 119 | Subsection = subsection, 120 | }; 121 | 122 | internal Line WithValue(string? value) 123 | { 124 | if (Kind != LineKind.Variable) 125 | throw new NotSupportedException(); 126 | 127 | var original = LineText; 128 | var builder = new StringBuilder(original.Length + (value == null ? 0 : value.Length)); 129 | 130 | builder.Append(original.Substring(0, Variable!.Position.Absolute + Variable!.Length)); 131 | 132 | if (value != null) 133 | { 134 | builder.Append(" = "); 135 | builder.Append(TextRules.SerializeValue(value)); 136 | } 137 | 138 | if (Comment != null) 139 | builder.Append(" ").Append(Comment.Text); 140 | 141 | var lineText = builder.ToString(); 142 | var parsed = ConfigReader.ParseVariable(FilePath, lineText, LineNumber, Section, Subsection); 143 | 144 | return this with 145 | { 146 | LineText = lineText, 147 | Variable = parsed.Variable, 148 | Value = parsed.Value, 149 | Comment = parsed.Comment, 150 | }; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Config/LineKind.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetConfig 2 | { 3 | enum LineKind 4 | { 5 | None, 6 | Comment, 7 | Error, 8 | Section, 9 | Variable 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Config/Position.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetConfig 2 | { 3 | /// 4 | /// A position within a line of configuration. 5 | /// 6 | class Position 7 | { 8 | /// 9 | /// The position corresponding to the zero index. 10 | /// 11 | public static Position Zero { get; } = new Position(0, 0, 1); 12 | 13 | /// 14 | /// Creates a new position. 15 | /// 16 | /// The line number. 17 | /// The absolute position (zero-based). 18 | /// The column number (one-based). 19 | public Position(int line, int absolute, int? column = default) 20 | { 21 | Line = line; 22 | Absolute = absolute; 23 | Column = column ?? absolute + 1; 24 | } 25 | 26 | /// 27 | /// The zero-based absolute index of the position. 28 | /// 29 | public int Absolute { get; } 30 | 31 | /// 32 | /// Gets the one-based line number. 33 | /// 34 | public int Line { get; } 35 | 36 | /// 37 | /// Gets the one-based column number. 38 | /// 39 | public int Column { get; } 40 | 41 | /// 42 | /// if the position has a value. 43 | /// 44 | public bool HasValue => Line > 0; 45 | 46 | /// 47 | /// Gets the position. 48 | /// 49 | /// 50 | public override string ToString() => Absolute.ToString(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/TextRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace DotNetConfig 5 | { 6 | static class TextRules 7 | { 8 | const long KB = 1024; 9 | 10 | public static void ParseKey(string key, out string section, out string? subsection, out string variable) 11 | { 12 | var parts = key.Split('.'); 13 | if (parts.Length < 2) 14 | throw new ArgumentException("Expected: SECTION.[SUBSECTION.]VARIABLE"); 15 | 16 | if (parts.Length == 2) 17 | { 18 | // NO SUBS 19 | var sl = Line.CreateSection(null, 0, parts[0], null); 20 | var vl = Line.CreateVariable(null, 1, sl.Section, null, parts[1], null); 21 | section = sl.Section!; 22 | subsection = null; 23 | variable = vl.Variable!; 24 | } 25 | else 26 | { 27 | var sl = Line.CreateSection(null, 0, parts[0], string.Join(".", parts[1..^1]).Trim('"')); 28 | var vl = Line.CreateVariable(null, 1, sl.Section, null, parts[^1], null); 29 | section = sl.Section!; 30 | subsection = sl.Subsection; 31 | variable = vl.Variable!; 32 | } 33 | } 34 | 35 | public static void ParseSection(string key, out string section, out string? subsection) 36 | { 37 | var parts = key.Split('.'); 38 | 39 | if (parts.Length == 1) 40 | { 41 | var sl = Line.CreateSection(null, 0, parts[0], null); 42 | section = sl.Section!; 43 | subsection = null; 44 | } 45 | else 46 | { 47 | var sl = Line.CreateSection(null, 0, parts[0], string.Join(".", parts[1..^1]).Trim('"')); 48 | section = sl.Section!; 49 | subsection = sl.Subsection; 50 | } 51 | } 52 | 53 | public static bool ParseBoolean(string? value) 54 | { 55 | // Empty or null variable value is true for a boolean 56 | if (string.IsNullOrWhiteSpace(value)) 57 | return true; 58 | 59 | // Regular bool parsing 60 | if (bool.TryParse(value, out var result)) 61 | return result; 62 | 63 | // Special cases for common variants of boolean users can use. 64 | return value!.ToLowerInvariant() switch 65 | { 66 | "yes" or "on" or "1" => true, 67 | "no" or "off" or "0" => false, 68 | _ => throw new FormatException($"Invalid value '{value}' for a boolean. Expected yes, on, 1 or true, or no, off, 0 or false (case insensitive)."), 69 | }; 70 | } 71 | 72 | public static long ParseNumber(string value) 73 | { 74 | // If last two are non-digits, attempt the two-letter suffix 75 | if (value.Length > 1 && !char.IsDigit(value[^1])) 76 | { 77 | var suffix = char.ToLowerInvariant(value[^1]); 78 | if (value.Length > 2 && !char.IsDigit(value[^2])) 79 | { 80 | if (suffix != 'b') 81 | throw new FormatException("Invalid number suffix. Expected k, m, g or t, or kb, mb, gb or tb (case insensitive)."); 82 | 83 | return long.Parse(value[..^2]) * char.ToLowerInvariant(value[^2]) switch 84 | { 85 | 'k' => KB, 86 | 'm' => KB * KB, 87 | 'g' => KB * KB * KB, 88 | 't' => KB * KB * KB * KB, 89 | _ => throw new FormatException("Invalid number suffix. Expected k, m, g or t, or kb, mb, gb or tb (case insensitive)."), 90 | }; 91 | } 92 | 93 | return long.Parse(value[..^1]) * suffix switch 94 | { 95 | 'k' => KB, 96 | 'm' => KB * KB, 97 | 'g' => KB * KB * KB, 98 | 't' => KB * KB * KB * KB, 99 | _ => throw new FormatException("Invalid number suffix. Expected k, m, g or t, or kb, mb, gb or tb (case insensitive)."), 100 | }; 101 | } 102 | 103 | return long.Parse(value); 104 | } 105 | 106 | public static string SerializeSubsection(string subsection) 107 | => subsection.Replace("\\", "\\\\").Replace("\"", "\\\""); 108 | 109 | public static string SerializeValue(string value) 110 | { 111 | value = value.Replace(Environment.NewLine, "\\n"); 112 | 113 | // Escaping backslashes is applied first since it does not 114 | // require adding quotes to the string. 115 | if (value.IndexOf('\\') != -1) 116 | value = value.Replace("\\", "\\\\"); 117 | 118 | if (value.IndexOfAny(new[] { '#', ';', '"', '=' }) == -1) 119 | return value; 120 | 121 | return "\"" + value.Trim('"').Replace("\"", "\\\"") + "\""; 122 | } 123 | 124 | public static string ToKey(string section, string? subsection, string variable) 125 | { 126 | var sb = new StringBuilder(section); 127 | if (subsection != null) 128 | { 129 | sb = sb.Append('.'); 130 | if (subsection.IndexOfAny(new[] { ' ', '\\', '"', '.' }) == -1) 131 | sb = sb.Append(subsection); 132 | else 133 | sb = sb.Append("\"").Append(SerializeSubsection(subsection)).Append("\""); 134 | } 135 | 136 | return sb.Append('.').Append(variable).ToString(); 137 | } 138 | 139 | public static bool TryValidateBoolean(string value, out string? error) 140 | { 141 | error = default; 142 | try 143 | { 144 | ParseBoolean(value); 145 | return true; 146 | } 147 | catch (FormatException fe) 148 | { 149 | error = fe.Message; 150 | return false; 151 | } 152 | } 153 | 154 | public static bool TryValidateNumber(string value, out string? error) 155 | { 156 | error = default; 157 | try 158 | { 159 | ParseNumber(value); 160 | return true; 161 | } 162 | catch (FormatException fe) 163 | { 164 | error = fe.Message; 165 | return false; 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Config/TextSpan.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetConfig 2 | { 3 | /// 4 | /// A span of configuration text within an entire line. 5 | /// 6 | class TextSpan 7 | { 8 | string? text; 9 | 10 | /// 11 | /// Construct a span encompassing an entire string. 12 | /// 13 | /// The source line. 14 | public TextSpan(string line) 15 | : this(line, Position.Zero, line.Length) 16 | { 17 | } 18 | 19 | /// 20 | /// Construct a string span for a substring of . 21 | /// 22 | /// The source line. 23 | /// The start of the span. 24 | /// The length of the span. 25 | /// Optional pre-calculated text from the span. 26 | public TextSpan(string line, Position position, int length, string? text = null) 27 | { 28 | Line = line; 29 | Position = position; 30 | Length = length; 31 | this.text = text; 32 | } 33 | 34 | /// 35 | /// The line of text text containing the span. 36 | /// 37 | public string Line { get; } 38 | 39 | /// 40 | /// The position of the start of the span within the string. 41 | /// 42 | public Position Position { get; } 43 | 44 | /// 45 | /// The length of the span. 46 | /// 47 | public int Length { get; } 48 | 49 | /// 50 | /// The text value of the span. 51 | /// 52 | public string Text 53 | { 54 | get 55 | { 56 | if (text == null) 57 | text = Line == null ? "" : Line.Substring(Position.Absolute, Length); 58 | 59 | return text; 60 | } 61 | } 62 | 63 | /// 64 | /// Gets the represented by this span. 65 | /// 66 | public override string ToString() => Text; 67 | 68 | /// 69 | /// Gets the text value of the span. 70 | /// 71 | public static implicit operator string?(TextSpan? span) => span?.Text; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Config/ValueMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace DotNetConfig 5 | { 6 | /// 7 | /// Allows matching values by using a regular expression. 8 | /// 9 | class ValueMatcher 10 | { 11 | readonly Func matcher; 12 | 13 | /// 14 | /// A that always matches regardless of the 15 | /// specified value. 16 | /// 17 | public static ValueMatcher All { get; } = new ValueMatcher(_ => true); 18 | 19 | /// 20 | /// An optional regular expression to use for value matching/filtering. 21 | /// 22 | /// Regular expression, optionally starting with ! to negate the match expression. 23 | public static ValueMatcher From(string? expression) 24 | { 25 | if (string.IsNullOrWhiteSpace(expression)) 26 | return All; 27 | 28 | if (expression![0] == '!') 29 | return new ValueMatcher(v => v != null && !Regex.IsMatch(v, expression.Substring(1))); 30 | 31 | return new ValueMatcher(v => v != null && Regex.IsMatch(v, expression)); 32 | 33 | } 34 | 35 | /// 36 | /// Converts a string to a . 37 | /// 38 | /// Regular expression, optionally starting with ! to negate the match expression. 39 | public static implicit operator ValueMatcher?(string? expression) => From(expression); 40 | 41 | ValueMatcher(Func matcher) => this.matcher = matcher; 42 | 43 | /// 44 | /// Checks whether the given matches the expression 45 | /// specified for the when constructed. 46 | /// 47 | /// if the value is not null and matches the expression. 48 | public bool Matches(string? value) => matcher(value); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Config/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Configuration/Configuration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DotNetConfig.Configuration 5 | DotNetConfig configuration provider implementation for Microsoft.Extensions.Configuration. 6 | 7 | Usage: 8 | var config = new ConfigurationBuilder().AddDotNetConfig().Build(); 9 | var value = config["section:subsection:variable"]); 10 | 11 | Note: section is required and subsection is optional, just like in dotnet-config. 12 | 13 | 14 | netstandard2.0 15 | DotNetConfig.Configuration 16 | DotNetConfig 17 | true 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | %(Filename)%(Extension) 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Configuration/DotNetConfigExtensions.cs: -------------------------------------------------------------------------------- 1 | using DotNetConfig; 2 | 3 | namespace Microsoft.Extensions.Configuration 4 | { 5 | /// 6 | /// Extension methods for adding DotNetConfig support to Microsoft.Extensions.Configuration. 7 | /// 8 | public static class DotNetConfigExtensions 9 | { 10 | /// 11 | /// Adds the DotNetConfig configuration provider to the . 12 | /// 13 | /// The to add dotnet config support to. 14 | /// The . 15 | /// 16 | /// Simply invoke this method on a builder to add hierarchical dotnet-config support 17 | /// to your app, for example: 18 | /// 19 | /// var config = new ConfigurationBuilder().AddDotNetConfig().Build(); 20 | /// 21 | /// var ssl = config["http:sslVerify"]; 22 | /// 23 | /// 24 | public static IConfigurationBuilder AddDotNetConfig(this IConfigurationBuilder builder) 25 | => AddDotNetConfig(builder, null); 26 | 27 | /// 28 | /// Adds the DotNetConfig configuration provider to the . 29 | /// 30 | /// The to add dotnet config support to. 31 | /// Optional path to use when building the configuration. See . 32 | /// If not provided, the current directory will be used. 33 | /// The . 34 | /// 35 | /// Simply invoke this method on a builder to add hierarchical dotnet-config support 36 | /// to your app, for example: 37 | /// 38 | /// var config = new ConfigurationBuilder().AddDotNetConfig().Build(); 39 | /// 40 | /// var ssl = config["http:sslVerify"]; 41 | /// 42 | /// 43 | public static IConfigurationBuilder AddDotNetConfig(this IConfigurationBuilder builder, string? path = null) 44 | => builder.Add(new DotNetConfigSource(path)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Configuration/DotNetConfigProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace DotNetConfig 7 | { 8 | class DotNetConfigProvider : ConfigurationProvider 9 | { 10 | Config configuration; 11 | 12 | public DotNetConfigProvider(string? path = null) => configuration = Config.Build(path); 13 | 14 | public override void Load() 15 | { 16 | var data = new Dictionary(StringComparer.OrdinalIgnoreCase); 17 | foreach (var entry in configuration) 18 | { 19 | // NOTES: 20 | // 1. Microsoft.Extensions.Configuration does not support multi-valued variables 21 | // so last value wins. 22 | // 2. a null/empty value is equal to "true" in dotnet-config/git-config. 23 | // 3. The hierarchical keys are separated by : instead of . 24 | 25 | var key = entry.Section; 26 | if (entry.Subsection != null) 27 | key += ConfigurationPath.KeyDelimiter + entry.Subsection; 28 | 29 | key += ConfigurationPath.KeyDelimiter + entry.Variable; 30 | 31 | if (!data.ContainsKey(key)) 32 | data[key] = string.IsNullOrWhiteSpace(entry.RawValue) ? "true" : entry.RawValue!.Trim('"'); 33 | } 34 | 35 | Data = data; 36 | } 37 | 38 | public override void Set(string key, string value) 39 | { 40 | var first = key.IndexOf(ConfigurationPath.KeyDelimiter); 41 | if (first == -1) 42 | throw new ArgumentException(key, nameof(key)); 43 | 44 | var section = key.Substring(0, first); 45 | string? subsection = default; 46 | string variable; 47 | 48 | var last = key.LastIndexOf(ConfigurationPath.KeyDelimiter); 49 | 50 | if (first == last) 51 | { 52 | variable = key.Substring(first + 1); 53 | } 54 | else 55 | { 56 | subsection = key.Substring(first + 1, last - first - 1); 57 | variable = key.Substring(last + 1); 58 | } 59 | 60 | var entry = configuration.FirstOrDefault(x => x.Section == section && x.Subsection == subsection && x.Variable == variable); 61 | if (entry?.Level == ConfigLevel.Local) 62 | // Honor the local level if specified. 63 | configuration.SetString(section, subsection, variable, value, ConfigLevel.Local); 64 | else 65 | // Default saves to the target dir .netconfig instead. 66 | configuration = configuration.SetString(section, subsection, variable, value); 67 | 68 | base.Set(key, value); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Configuration/DotNetConfigSource.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace DotNetConfig 4 | { 5 | class DotNetConfigSource : IConfigurationSource 6 | { 7 | readonly string? path; 8 | 9 | public DotNetConfigSource(string? path = null) => this.path = path; 10 | 11 | public IConfigurationProvider Build(IConfigurationBuilder builder) => new DotNetConfigProvider(path); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Configuration/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | true 14 | 15 | 16 | 17 | 18 | $(CI) 19 | 20 | 21 | 22 | Daniel Cazzulino 23 | Copyright (C) Daniel Cazzulino and Contributors. All rights reserved. 24 | false 25 | MIT 26 | 27 | 28 | icon.png 29 | readme.md 30 | 31 | icon.png 32 | readme.md 33 | 34 | true 35 | true 36 | 37 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\bin')) 38 | 39 | 40 | true 41 | true 42 | 43 | 44 | true 45 | 46 | 47 | 48 | Release 49 | Latest 50 | 51 | 52 | false 53 | 54 | embedded 55 | true 56 | enable 57 | 58 | strict 59 | 60 | 61 | $(MSBuildProjectName) 62 | $(MSBuildProjectName.IndexOf('.')) 63 | $(MSBuildProjectName.Substring(0, $(RootNamespaceDot))) 64 | 65 | 66 | $(DefaultItemExcludes);*.binlog;*.zip;*.rsp;*.items;**/TestResults/**/*.* 67 | 68 | true 69 | true 70 | true 71 | true 72 | 73 | 74 | true 75 | 76 | 77 | false 78 | 79 | 80 | NU5105;$(NoWarn) 81 | 82 | true 83 | 84 | 85 | true 86 | 87 | 88 | LatestMinor 89 | 90 | 91 | 92 | 93 | $(MSBuildThisFileDirectory)kzu.snk 94 | 100 | 002400000480000094000000060200000024000052534131000400000100010051155fd0ee280be78d81cc979423f1129ec5dd28edce9cd94fd679890639cad54c121ebdb606f8659659cd313d3b3db7fa41e2271158dd602bb0039a142717117fa1f63d93a2d288a1c2f920ec05c4858d344a45d48ebd31c1368ab783596b382b611d8c92f9c1b3d338296aa21b12f3bc9f34de87756100c172c52a24bad2db 101 | 00352124762f2aa5 102 | true 103 | 104 | 105 | 106 | 114 | 42.42.42 115 | 116 | 117 | 118 | <_VersionLabel>$(VersionLabel.Replace('refs/heads/', '')) 119 | <_VersionLabel>$(_VersionLabel.Replace('refs/tags/v', '')) 120 | 121 | 122 | <_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789')) 123 | 124 | <_VersionLabel>$(_VersionLabel.Replace('refs/pull/', 'pr')) 125 | 126 | <_VersionLabel>$(_VersionLabel.Replace('/merge', '')) 127 | 128 | <_VersionLabel>$(_VersionLabel.Replace('/', '-')) 129 | 130 | 131 | $(_VersionLabel) 132 | 133 | $(_VersionLabel) 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 162 | 163 | 1.0.0 164 | $(VersionPrefix)-$(VersionSuffix) 165 | $(VersionPrefix) 166 | 167 | 168 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/Directory.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | dotnet config 6 | 7 | 8 | 9 | 10 | true 11 | $(PackOnBuild) 12 | 13 | $(MSBuildThisFileDirectory)dotnet-config.snk 14 | 002400000480000094000000060200000024000052534131000400000100010071207e0121c41cd25ecdf4dffe275b3a055b03e9f009f778b6bd0f0fe6643ac89ca3eeddf6d136496c4cd0defa1fcff361cc2c2c0d0a8f1b6ff92c15e661dee0acde682c4dcf78b7a30edd65737b54da568f4ec76b66827ce019093b9dedf80214b1a3d63d5289d542b3b218d7fe537d6da628d2718307190a5993d7fca0e3b1 15 | 41dc05ca892b85e5 16 | true 17 | 18 | true 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/dotnet-config.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/src/dotnet-config.snk -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnetconfig/dotnet-config/22617f7182b0d123b411e2695970fe503fa7bc96/src/icon.png --------------------------------------------------------------------------------