├── .config └── dotnet-tools.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── build.fsx ├── logo.png ├── paket.dependencies ├── paket.lock └── src ├── Fli.Tests ├── ExecContext │ ├── ExecCommandConfigureUnixTests.fs │ ├── ExecCommandConfigureWindowsTests.fs │ ├── ExecCommandExecuteUnixTests.fs │ ├── ExecCommandExecuteWindowsTests.fs │ ├── ExecCommandToStringTests.fs │ └── ExecConfigTests.fs ├── Fli.Tests.fsproj ├── ShellContext │ ├── ShellCommandConfigureUnixTests.fs │ ├── ShellCommandConfigureWindowsTests.fs │ ├── ShellCommandExecuteUnixTests.fs │ ├── ShellCommandExecuteWindowsTests.fs │ ├── ShellCommandToStringUnixTests.fs │ ├── ShellCommandToStringWindowsTests.fs │ └── ShellConfigTests.fs └── paket.references ├── Fli.sln ├── Fli ├── AssemblyInfo.fs ├── CE.fs ├── Command.fs ├── Domain.fs ├── Dsl.fs ├── Extensions.fs ├── Fli.fsproj ├── Helpers.fs ├── Output.fs ├── paket.references └── paket.template └── install.ps1 /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "9.0.2", 7 | "commands": [ 8 | "paket" 9 | ], 10 | "rollForward": false 11 | }, 12 | "fantomas": { 13 | "version": "7.0.1", 14 | "commands": [ 15 | "fantomas" 16 | ], 17 | "rollForward": false 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: CaptnCodr 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: bug, triage 6 | assignees: CaptnCodr 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Repro steps** 14 | Please provide the steps required to reproduce the problem 15 | 1. Abstract code you wrote 16 | 2. Executed code 17 | 3. See error 18 | 19 | **Expected behavior** 20 | Please provide a description of the behavior you expect. 21 | 22 | **Errors or actual behavior** 23 | If applicable, add error messages 24 | 25 | **Related information (please complete the following information):** 26 | - OS: [e.g.: Windows, Linux, MacOS] 27 | - .NET version 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [windows-latest, macOS-latest, ubuntu-latest] 18 | dotnet: [8.0.100] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: ${{ matrix.dotnet }} 27 | 28 | - name: Install local tools 29 | run: dotnet tool restore 30 | 31 | - name: Paket Restore 32 | run: dotnet paket restore 33 | 34 | - name: Build 35 | run: dotnet build ./src/ 36 | 37 | - name: Test 38 | run: dotnet test ./src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Rider's working folder. 353 | .idea/ -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[2]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) 242 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) 243 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[8]) 244 | 245 | 246 | %(PaketReferencesFileLinesInfo.PackageVersion) 247 | All 248 | runtime 249 | $(ExcludeAssets);contentFiles 250 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive 251 | %(PaketReferencesFileLinesInfo.Aliases) 252 | true 253 | true 254 | 255 | 256 | 257 | 258 | %(PaketReferencesFileLinesInfo.PackageVersion) 259 | 260 | 261 | 262 | 263 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 273 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 274 | 275 | 276 | %(PaketCliToolFileLinesInfo.PackageVersion) 277 | 278 | 279 | 280 | 284 | 285 | 286 | 287 | 288 | 289 | false 290 | 291 | 292 | 293 | 294 | 295 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 296 | 297 | 298 | 299 | 300 | 301 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 302 | true 303 | false 304 | true 305 | false 306 | true 307 | false 308 | true 309 | false 310 | true 311 | false 312 | true 313 | $(PaketIntermediateOutputPath)\$(Configuration) 314 | $(PaketIntermediateOutputPath) 315 | 316 | 317 | 318 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 378 | 379 | 428 | 429 | 474 | 475 | 519 | 520 | 563 | 564 | 565 | 566 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Constantin Tews 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fli 2 | [![build](https://github.com/CaptnCodr/Fli/actions/workflows/build.yml/badge.svg)](https://github.com/CaptnCodr/Fli/actions/workflows/build.yml) 3 | [![Nuget](https://img.shields.io/nuget/v/fli?color=33cc56)](https://www.nuget.org/packages/Fli/) 4 | 5 | 6 | Execute CLI commands from your F# code in F# style! 7 | 8 | **Fli is part of the F# Advent Calendar 2022: [A little story about Fli](https://gist.github.com/CaptnCodr/d709b30eb1191bedda090623d04bf738)** 9 | 10 | ### Features 11 | - Starting processes easily 12 | - Execute CLI commands in your favourite shell 13 | - F# computation expression syntax 14 | - Wrap authenticated CLI tools 15 | - No external dependencies 16 | 17 | ### Install 18 | Get it from [Nuget](https://www.nuget.org/packages/Fli/): `dotnet add package Fli` 19 | 20 | ### Usage 21 | `open Fli` and start 22 | 23 | For example: 24 | ```fsharp 25 | cli { 26 | Shell CMD 27 | Command "echo Hello World!" 28 | } 29 | |> Command.execute 30 | ``` 31 | that starts `CMD.exe` as Shell and `echo Hello World!` is the command to execute. 32 | 33 | Run a file with PowerShell from a specific directory: 34 | ```fsharp 35 | cli { 36 | Shell PWSH 37 | Command "test.bat" 38 | WorkingDirectory (Environment.GetFolderPath Environment.SpecialFolder.UserProfile) 39 | } 40 | |> Command.execute 41 | ``` 42 | 43 | Executing programs with arguments: 44 | ```fsharp 45 | cli { 46 | Exec "path/to/executable" 47 | Arguments "--info" 48 | } 49 | |> Command.execute 50 | ``` 51 | 52 | an example with `git`: 53 | ```fsharp 54 | cli { 55 | Exec "git" 56 | Arguments ["commit"; "-m"; "\"Fixing issue #1337.\""] 57 | } 58 | |> Command.execute 59 | ``` 60 | 61 | Add a verb to your executing program: 62 | ```fsharp 63 | cli { 64 | Exec "adobe.exe" 65 | Arguments (Path.Combine ((Environment.GetFolderPath Environment.SpecialFolder.UserProfile), "test.pdf")) 66 | Verb "open" 67 | } 68 | |> Command.execute 69 | ``` 70 | or open a file in the default/assigned program: 71 | ```fsharp 72 | cli { 73 | Exec "test.pdf" 74 | } 75 | |> Command.execute 76 | ``` 77 | (Hint: if file extension is not assigned to any installed program, it will throw a `System.NullReferenceException`) 78 | 79 | Write output to a specific file: 80 | ```fsharp 81 | cli { 82 | Exec "dotnet" 83 | Arguments "--list-sdks" 84 | Output @"absolute\path\to\dotnet-sdks.txt" 85 | } 86 | |> Command.execute 87 | ``` 88 | 89 | Write output to a function (logging, printing, etc.): 90 | ```fsharp 91 | let log (output: string) = Debug.Log($"CLI log: {output}") 92 | 93 | cli { 94 | Exec "dotnet" 95 | Arguments "--list-sdks" 96 | Output log 97 | } 98 | |> Command.execute 99 | ``` 100 | 101 | Add environment variables for the executing program: 102 | ```fsharp 103 | cli { 104 | Exec "git" 105 | EnvironmentVariables [("GIT_AUTHOR_NAME", "Jon Doe"); ("GIT_AUTHOR_EMAIL", "jon.doe@domain.com")] 106 | Output "" 107 | } 108 | |> Command.execute 109 | ``` 110 | Hint: `Output ""` will be ignored. This is for conditional cases, e.g.: `Output (if true then logFilePath else "")`. 111 | 112 | Add credentials to program: 113 | ```fsharp 114 | cli { 115 | Exec "program" 116 | Credentials ("domain", "bobk", "password123") 117 | } 118 | |> Command.execute 119 | ``` 120 | Hint: Running a process as a different user is supported on all platforms. Other options (Domain, Password) are only available on Windows. As an alternative for not Windows based systems there is: 121 | ```fsharp 122 | cli { 123 | Exec "path/to/program" 124 | Username "admin" 125 | } 126 | |> Command.execute 127 | ``` 128 | 129 | For Windows applications it's possible to set their visibility. There are four possible values: `Hidden`, `Maximized`, `Minimized` and `Normal`. The default is `Hidden`. 130 | ```fsharp 131 | cli { 132 | Exec @"C:\Windows\regedit.exe" 133 | WindowStyle Normal 134 | } 135 | |> Command.execute 136 | ``` 137 | 138 | #### `Command.execute` 139 | `Command.execute` returns record: `type Output = { Id: int; Text: string option; ExitCode: int; Error: string option }` 140 | which has getter methods to get only one value: 141 | ```fsharp 142 | toId: Output -> int 143 | toText: Output -> string 144 | toExitCode: Output -> int 145 | toError: Output -> string 146 | ``` 147 | example: 148 | ```fsharp 149 | cli { 150 | Shell CMD 151 | Command "echo Hello World!" 152 | } 153 | |> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None } 154 | |> Output.toText // "Hello World!" 155 | 156 | // same with Output.toId: 157 | cli { ... } 158 | |> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None } 159 | |> Output.toId // 123 160 | 161 | // same with Output.toExitCode: 162 | cli { ... } 163 | |> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None } 164 | |> Output.toExitCode // 0 165 | 166 | // in case of an error: 167 | cli { ... } 168 | |> Command.execute // { Id = 123; Text = None; ExitCode = 1; Error = Some "This is an error!" } 169 | |> Output.toError // "This is an error!" 170 | ``` 171 | 172 | #### `Output` functions 173 | ```fsharp 174 | throwIfErrored: Output -> Output 175 | throw: (Output -> bool) -> Output -> Output 176 | ``` 177 | 178 | `Output.throw` and `Output.throwIfErrored` are assertion functions that if something's not right it will throw an exception. 179 | That is useful for build scripts to stop the execution immediately, here is an example: 180 | ```fsharp 181 | cli { 182 | Exec "dotnet" 183 | Arguments [| "build"; "-c"; "Release" |] 184 | WorkingDirectory "src/" 185 | } 186 | |> Command.execute // returns { Id = 123; Text = None; ExitCode = 1; Error = Some "This is an error!" } 187 | |> Output.throwIfErrored // <- Exception thrown! 188 | |> Output.toError 189 | ``` 190 | 191 | or, you can define when to "fail": 192 | ```fsharp 193 | cli { ... } 194 | |> Command.execute // returns { Id = 123; Text = "An error occured: ..."; ExitCode = 1; Error = Some "Error detail." } 195 | |> Output.throw (fun output -> output.Text.Contains("error")) // <- Exception thrown! 196 | |> Output.toError 197 | ``` 198 | 199 | #### Printing `Output` fields 200 | There are printing methods in `Output` too: 201 | ```fsharp 202 | printId: Output -> unit 203 | printText: Output -> unit 204 | printExitCode: Output -> unit 205 | printError: Output -> unit 206 | ``` 207 | 208 | Instead of writing: 209 | ```fsharp 210 | cli { ... } 211 | |> Command.execute 212 | |> Output.toText 213 | |> printfn "%s" 214 | ``` 215 | For a little shorter code you can use: 216 | ```fsharp 217 | cli { ... } 218 | |> Command.execute 219 | |> Output.printText 220 | ``` 221 | 222 | #### `Command.toString` 223 | `Command.toString` concatenates only the executing shell/program + the given commands/arguments: 224 | ```fsharp 225 | cli { 226 | Shell PS 227 | Command "Write-Host Hello World!" 228 | } 229 | |> Command.toString // "powershell.exe -Command Write-Host Hello World!" 230 | ``` 231 | and: 232 | ```fsharp 233 | cli { 234 | Exec "cmd.exe" 235 | Arguments [ "/C"; "echo"; "Hello World!" ] 236 | } 237 | |> Command.toString // "cmd.exe /C echo Hello World!" 238 | ``` 239 | 240 | #### Builder operations: 241 | 242 | `ShellContext` operations (`cli { Shell ... }`): 243 | | Operation | Type | 244 | |------------------------|----------------------------| 245 | | `Shell` | `Fli.Shells` | 246 | | `Command` | `string` | 247 | | `Input` | `string` | 248 | | `Output` | `Fli.Outputs` | 249 | | `WorkingDirectory` | `string` | 250 | | `WindowStyle` | `Fli.WindowStyle` | 251 | | `EnvironmentVariable` | `string * string` | 252 | | `EnvironmentVariables` | `(string * string) list` | 253 | | `Encoding` | `System.Text.Encoding` | 254 | | `CancelAfter` | `int` | 255 | 256 | `ExecContext` operations (`cli { Exec ... }`): 257 | | Operation | Type | 258 | |------------------------|----------------------------------------------------------| 259 | | `Exec` | `string` | 260 | | `Arguments` | `string` / `string seq` / `string list` / `string array` | 261 | | `Input` | `string` | 262 | | `Output` | `Fli.Outputs` | 263 | | `Verb` | `string` | 264 | | `Username` | `string` | 265 | | `Credentials` | `string * string * string` | 266 | | `WorkingDirectory` | `string` | 267 | | `WindowStyle` | `Fli.WindowStyle` | 268 | | `EnvironmentVariable` | `string * string` | 269 | | `EnvironmentVariables` | `(string * string) list` | 270 | | `Encoding` | `System.Text.Encoding` | 271 | | `CancelAfter` | `int` | 272 | 273 | Currently provided `Fli.Shells`: 274 | - `CMD` runs either `cmd.exe /c ...` or `cmd.exe /k ...` (if `Input` is provided) 275 | - `PS` runs `powershell.exe -Command ...` 276 | - `PWSH` runs `pwsh.exe -Command ...` 277 | - `WSL` runs `wsl.exe -- ...` 278 | - `SH` runs `sh -c ...` 279 | - `BASH` runs `bash -c ...` 280 | - `ZSH` runs `zsh -c ...` 281 | - `CUSTOM (shell: string * flag: string)` runs the specified `shell` with the specified starting argument (`flag`) 282 | 283 | Provided `Fli.Outputs`: 284 | - `File of string` a string with an absolute path of the output file. 285 | - `StringBuilder of StringBuilder` a StringBuilder which will be filled with the output text. 286 | - `Custom of Func` a custom function (`string -> unit`) that will be called with the output string (logging, printing etc.). 287 | 288 | Provided `Fli.WindowStyle`: 289 | - `Hidden` (default) 290 | - `Maximized` 291 | - `Minimized` 292 | - `Normal` 293 | 294 | ### Do you miss something? 295 | Open an [issue](https://github.com/CaptnCodr/Fli/issues) or start a [discussion](https://github.com/CaptnCodr/Fli/discussions). 296 | 297 | ### Contributing 298 | After cloning this repository, there are some steps to start: 299 | 1. `dotnet tool restore` 300 | 2. `dotnet paket restore` 301 | 3. `dotnet restore` 302 | 4. `dotnet paket install` 303 | 5. `dotnet build` 304 | 305 | After that, you can start coding, build and test. 306 | 307 | Every contribution is welcome. :) 308 | 309 | ### Inspiration 310 | Use CE's for CLI commands came in mind while using [FsHttp](https://github.com/fsprojects/FsHttp). 311 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | v1.111.11.0 - May 27, 2025 2 | - Improve ArgumentList in `Exec.Arguments`, this will split arguments more precise especially for python commands. (https://github.com/CaptnCodr/Fli/pull/76) 3 | - Improve documentations. 4 | 5 | v1.111.10.0 - May 31, 2024 6 | - Empty file output will not write to file. (https://github.com/CaptnCodr/Fli/pull/72) 7 | 8 | v1.111.1.0 - May 24, 2024 9 | - Ensure StandardIn is always redirected when there is `Input`. (https://github.com/CaptnCodr/Fli/pull/71) 10 | 11 | v1.111.0.0 - Apr 16, 2024 12 | - Add SH and ZSH 13 | - Add Linux's PWSH, always quote default shell (https://github.com/CaptnCodr/Fli/pull/68) 14 | 15 | v1.110.0.0 - Apr 12, 2024 16 | - Add WindowStyle to CE. (https://github.com/CaptnCodr/Fli/pull/67) 17 | 18 | v1.101.0.0 - Jan 09, 2024 19 | - New year's work. 20 | 21 | v1.100.10.0 - Nov 24, 2023 22 | - Tiny logo fix. 23 | 24 | v1.100.1.0 - Nov 24, 2023 25 | - Tiny fix. 26 | 27 | v1.100.0.0 - Nov 24, 2023 28 | - Add .NET8 support (https://github.com/CaptnCodr/Fli/pull/60). 29 | - Add new logo (https://github.com/CaptnCodr/Fli/pull/61). 30 | 31 | v1.11.0.0 - Oct 06, 2023 32 | - Add `throw` and `throwIfErrored` functions to interrupt pipeline on error (https://github.com/CaptnCodr/Fli/pull/56). 33 | 34 | v1.10.1.0 - Sep 01, 2023 35 | - `Command.executeAsync` stdout and stderr content now gets returned properly (https://github.com/CaptnCodr/Fli/pull/58). 36 | 37 | v1.10.0.0 - Aug 11, 2023 38 | - Open files in default/assigned program in `Exec` (https://github.com/CaptnCodr/Fli/pull/56). 39 | 40 | v1.1.1.0 - Jul 25, 2023 41 | - No need to quote the bash command anymore (https://github.com/CaptnCodr/Fli/pull/53) to have a consistent using between `Shells`. 42 | 43 | v1.1.0.0 - May 29, 2023 44 | - Slightly change signature of `Output` CustomOperation. (https://github.com/CaptnCodr/Fli/pull/51) 45 | - Add `CancelAfter` CustomOperation for `executeAsync` to cancel after a specific amount of time (milliseconds). (https://github.com/CaptnCodr/Fli/pull/50) 46 | 47 | v1.0.1.0 - Feb 02, 2023 48 | - Fix FSharp.Core to v6.0. (https://github.com/CaptnCodr/Fli/issues/47) 49 | 50 | v1.0.0.0 - Dec 18, 2022 51 | - Add `CUSTOM` shell in `ShellContext`. (https://github.com/CaptnCodr/Fli/pull/46) 52 | - Add printing methods for each field in `Output` for shorter code. (https://github.com/CaptnCodr/Fli/pull/45) 53 | - Update dependencies. 54 | 55 | v0.11.0.0 - Nov 11, 2022 56 | - Add `Output` as CustomOperation with different types (s. `Fli.Outputs`). (https://github.com/CaptnCodr/Fli/pull/37 https://github.com/CaptnCodr/Fli/pull/39 https://github.com/CaptnCodr/Fli/pull/40 https://github.com/CaptnCodr/Fli/pull/41) 57 | - Add .NET7 support and drop .MET5. (https://github.com/CaptnCodr/Fli/pull/42) 58 | - Update dependencies. 59 | 60 | v0.9.0.0 - Oct 18, 2022 61 | - Add `Output.Id` from `Process.Id`. (https://github.com/CaptnCodr/Fli/pull/27) 62 | - Add `WSL` to provided `Shells`. (https://github.com/CaptnCodr/Fli/pull/31) 63 | - Enhencement: Trim output texts at the end. (https://github.com/CaptnCodr/Fli/pull/32) 64 | - Add logo for Nuget package. (https://github.com/CaptnCodr/Fli/pull/34) 65 | - Update dependencies. 66 | 67 | v0.8.0.0 - Oct 12, 2022 68 | - Add `Command.executeAsync` for `NET5` and up! (https://github.com/CaptnCodr/Fli/pull/19) 69 | - Add `Error` from `StandardError`. (https://github.com/CaptnCodr/Fli/pull/20) 70 | - Add `Input` for `StandardInput`. (https://github.com/CaptnCodr/Fli/pull/21) 71 | - Fix some build warnings. (https://github.com/CaptnCodr/Fli/pull/22) 72 | 73 | v0.7.0.0 - Oct 07, 2022 74 | - Add `Encoding` to contexts. (https://github.com/CaptnCodr/Fli/pull/11) 75 | - Renaming of contexts. 76 | - Update docs with more snippets etc. 77 | - **BREAKING** Add output type with exit code: `type Output = { Text: string; ExitCode: int }` (https://github.com/CaptnCodr/Fli/pull/14) 78 | 79 | v0.6.1.0 - Oct 04, 2022 80 | - Fix: Wrong (old) content in Release 0.6.0 Nuget package. 81 | 82 | v0.6.0.0 - Oct 04, 2022 83 | - Add `WorkingDirectory` to both ShellContext & ProgramContext. (https://github.com/CaptnCodr/Fli/pull/4) 84 | - Add `Verb` to ProgramContext. (https://github.com/CaptnCodr/Fli/pull/6) 85 | - Add `Username` to ProgramContext. (https://github.com/CaptnCodr/Fli/pull/5) 86 | + `Credentials` added later (for windows only) 87 | - Add `EnvironmentVariables` to contexts. (https://github.com/CaptnCodr/Fli/pull/8) 88 | - Add internal method `configureProcess` for better testing. (https://github.com/CaptnCodr/Fli/pull/9) 89 | 90 | v0.0.2.0 - "Hello World!"-Release Sep 29, 2022 91 | - Initial release -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: Fli" 2 | 3 | open Fli 4 | 5 | // Empty output directory 6 | cli { 7 | Shell PWSH 8 | Command "Remove-Item * -Include *.nupkg" 9 | WorkingDirectory "src/.nupkg/" 10 | } 11 | |> Command.execute 12 | |> Output.toText 13 | |> printfn "%s" 14 | 15 | // Build in Release configuration 16 | cli { 17 | Exec "dotnet" 18 | Arguments [| "build"; "-c"; "Release" |] 19 | WorkingDirectory "src/" 20 | } 21 | |> Command.execute 22 | |> Output.throwIfErrored 23 | |> Output.toText 24 | |> printfn "%s" 25 | 26 | // Pack Nuget Package 27 | cli { 28 | Shell CMD 29 | Command "paket pack .nupkg" 30 | WorkingDirectory "src/" 31 | } 32 | |> Command.execute 33 | |> Output.throwIfErrored 34 | |> Output.toText 35 | |> printfn "%s" 36 | 37 | // Push Nuget Package 38 | cli { 39 | Shell PWSH 40 | Command "paket push (Get-Item * -Include *.nupkg).Name" 41 | WorkingDirectory "src/.nupkg/" 42 | } 43 | |> Command.execute 44 | |> Output.toText 45 | |> printfn "%s" 46 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptnCodr/Fli/334b1bc12456bae53cc59db22dd23ed9434343a9/logo.png -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | framework: net6.0, net7.0, net8.0, netstandard2.0, netstandard2.1 3 | 4 | nuget FSharp.Core ~> 6.0.0 5 | 6 | nuget FsUnit 7 | nuget Microsoft.NET.Test.Sdk 8 | nuget NUnit 9 | nuget NUnit3TestAdapter 10 | nuget NUnit.Analyzers -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | RESTRICTION: || (== net6.0) (== net7.0) (== net8.0) (== netstandard2.0) (== netstandard2.1) 2 | NUGET 3 | remote: https://api.nuget.org/v3/index.json 4 | FSharp.Core (6.0.7) 5 | FsUnit (7.0.1) 6 | FSharp.Core (>= 5.0.2) 7 | NUnit (>= 4.0.1) 8 | Microsoft.ApplicationInsights (2.23) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 9 | System.Diagnostics.DiagnosticSource (>= 5.0) 10 | Microsoft.CodeCoverage (17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 11 | Microsoft.NET.Test.Sdk (17.13) 12 | Microsoft.CodeCoverage (>= 17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 13 | Microsoft.TestPlatform.TestHost (>= 17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 14 | Microsoft.Testing.Extensions.Telemetry (1.6.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 15 | Microsoft.ApplicationInsights (>= 2.22) 16 | Microsoft.Testing.Platform (>= 1.6.3) 17 | Microsoft.Testing.Extensions.TrxReport.Abstractions (1.6.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 18 | Microsoft.Testing.Platform (>= 1.6.3) 19 | Microsoft.Testing.Extensions.VSTestBridge (1.6.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 20 | Microsoft.Testing.Extensions.Telemetry (>= 1.6.3) 21 | Microsoft.Testing.Extensions.TrxReport.Abstractions (>= 1.6.3) 22 | Microsoft.Testing.Platform (>= 1.6.3) 23 | Microsoft.TestPlatform.ObjectModel (>= 17.13) 24 | Microsoft.Testing.Platform (1.6.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 25 | Microsoft.Testing.Platform.MSBuild (1.6.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 26 | Microsoft.Testing.Platform (>= 1.6.3) 27 | Microsoft.TestPlatform.ObjectModel (17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 28 | System.Reflection.Metadata (>= 1.6) 29 | Microsoft.TestPlatform.TestHost (17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 30 | Microsoft.TestPlatform.ObjectModel (>= 17.13) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 31 | Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 32 | Newtonsoft.Json (13.0.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 33 | NUnit (4.3.2) 34 | NUnit.Analyzers (4.7) 35 | NUnit3TestAdapter (5.0) 36 | Microsoft.Testing.Extensions.VSTestBridge (>= 1.5.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 37 | Microsoft.Testing.Platform.MSBuild (>= 1.5.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 38 | System.Collections.Immutable (9.0.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 39 | System.Memory (>= 4.5.5) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (>= net462)) (== netstandard2.0) (== netstandard2.1) 40 | System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (>= net462)) (== netstandard2.0) (== netstandard2.1) 41 | System.Diagnostics.DiagnosticSource (9.0.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 42 | System.Memory (>= 4.5.5) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (>= net462)) (== netstandard2.0) (== netstandard2.1) 43 | System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (>= net462)) (== netstandard2.0) (== netstandard2.1) 44 | System.Memory (4.6.3) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (== netstandard2.0)) (&& (== net8.0) (== netstandard2.1)) (&& (== net8.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 45 | System.Reflection.Metadata (9.0.3) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 46 | System.Collections.Immutable (>= 9.0.3) 47 | System.Memory (>= 4.5.5) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (>= net462)) (== netstandard2.0) (== netstandard2.1) 48 | System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (== net6.0) (== net7.0) (&& (== net8.0) (== netstandard2.0)) (&& (== net8.0) (== netstandard2.1)) (&& (== net8.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) 49 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecCommandConfigureUnixTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandConfigureUnixTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Collections.Generic 8 | open System.Text 9 | 10 | [] 11 | [] 12 | let ``Check FileName in ProcessStartInfo Exec program`` () = 13 | cli { Exec "bash" } |> Command.buildProcess |> _.FileName |> should equal "bash" 14 | 15 | [] 16 | [] 17 | let ``Check Arguments in ProcessStartInfo with Arguments`` () = 18 | cli { 19 | Exec "bash" 20 | Arguments "-c echo Hello World!" 21 | } 22 | |> Command.buildProcess 23 | |> _.Arguments 24 | |> should equal "-c echo Hello World!" 25 | 26 | [] 27 | [] 28 | let ``Check WorkingDirectory in ProcessStartInfo with WorkingDirectory`` () = 29 | cli { 30 | Exec "bash" 31 | WorkingDirectory @"/etc/" 32 | } 33 | |> Command.buildProcess 34 | |> _.WorkingDirectory 35 | |> should equal @"/etc/" 36 | 37 | [] 38 | [] 39 | let ``Check WindowStyle in ProcessStartInfo with WorkingDirectory`` () = 40 | cli { 41 | Exec "bash" 42 | WindowStyle Normal 43 | } 44 | |> Command.buildProcess 45 | |> _.WindowStyle 46 | |> should equal Diagnostics.ProcessWindowStyle.Normal 47 | 48 | [] 49 | [] 50 | let ``Check UserName in ProcessStartInfo with Username`` () = 51 | cli { 52 | Exec "bash" 53 | Username "root" 54 | } 55 | |> Command.buildProcess 56 | |> _.UserName 57 | |> should equal "root" 58 | 59 | [] 60 | [] 61 | let ``Check Environment in ProcessStartInfo with single environment variable`` () = 62 | cli { 63 | Exec "bash" 64 | EnvironmentVariable("Fli", "test") 65 | } 66 | |> Command.buildProcess 67 | |> _.Environment.Contains(KeyValuePair("Fli", "test")) 68 | |> should be True 69 | 70 | [] 71 | [] 72 | let ``Check Environment in ProcessStartInfo with multiple environment variables`` () = 73 | let config = 74 | cli { 75 | Exec "bash" 76 | EnvironmentVariables [ ("Fli", "test"); ("Fli.Test", "test") ] 77 | } 78 | |> Command.buildProcess 79 | 80 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 81 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 82 | 83 | [] 84 | [] 85 | let ``Check StandardOutputEncoding & StandardErrorEncoding with setting Encoding`` () = 86 | let config = 87 | cli { 88 | Exec "bash" 89 | Encoding Encoding.UTF8 90 | } 91 | |> Command.buildProcess 92 | 93 | config.StandardOutputEncoding |> should equal Encoding.UTF8 94 | config.StandardErrorEncoding |> should equal Encoding.UTF8 95 | 96 | [] 97 | [] 98 | let ``Check all possible values in ProcessStartInfo`` () = 99 | let config = 100 | cli { 101 | Exec "bash" 102 | Arguments "--help" 103 | Output "./Users/test.txt" 104 | WorkingDirectory "./Users" 105 | Username "admin" 106 | EnvironmentVariable("Fli", "test") 107 | EnvironmentVariables [ ("Fli.Test", "test") ] 108 | Encoding Encoding.UTF8 109 | } 110 | |> Command.buildProcess 111 | 112 | config.FileName |> should equal "bash" 113 | config.Arguments |> should equal "--help" 114 | config.WorkingDirectory |> should equal "./Users" 115 | config.Verb |> should equal String.Empty 116 | config.UserName |> should equal "admin" 117 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 118 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 119 | config.StandardOutputEncoding |> should equal Encoding.UTF8 120 | config.StandardErrorEncoding |> should equal Encoding.UTF8 121 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecCommandConfigureWindowsTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandConfigureWindowsTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Collections.Generic 8 | open System.Text 9 | 10 | [] 11 | [] 12 | let ``Check FileName in ProcessStartInfo Exec program`` () = 13 | cli { Exec "cmd.exe" } 14 | |> Command.buildProcess 15 | |> _.FileName 16 | |> should equal "cmd.exe" 17 | 18 | [] 19 | [] 20 | let ``Check Arguments in ProcessStartInfo with Arguments`` () = 21 | cli { 22 | Exec "cmd.exe" 23 | Arguments "-c echo Hello World!" 24 | } 25 | |> Command.buildProcess 26 | |> _.Arguments 27 | |> should equal "-c echo Hello World!" 28 | 29 | #if NET 30 | [] 31 | [] 32 | let ``Check Arguments in ProcessStartInfo with ArgumentList`` () = 33 | cli { 34 | Exec "cmd.exe" 35 | Arguments [ "-c"; "echo Hello World!" ] 36 | } 37 | |> Command.buildProcess 38 | |> _.ArgumentList 39 | :> seq<_> 40 | |> should 41 | equal 42 | (seq { 43 | "-c" 44 | "echo Hello World!" 45 | }) 46 | #endif 47 | 48 | [] 49 | [] 50 | let ``Check WorkingDirectory in ProcessStartInfo with WorkingDirectory`` () = 51 | cli { 52 | Exec "cmd.exe" 53 | WorkingDirectory @"C:\Users" 54 | } 55 | |> Command.buildProcess 56 | |> _.WorkingDirectory 57 | |> should equal @"C:\Users" 58 | 59 | [] 60 | [] 61 | let ``Check WindowStyle in ProcessStartInfo with WorkingDirectory`` () = 62 | cli { 63 | Exec "cmd.exe" 64 | WindowStyle Normal 65 | } 66 | |> Command.buildProcess 67 | |> _.WindowStyle 68 | |> should equal Diagnostics.ProcessWindowStyle.Normal 69 | 70 | [] 71 | [] 72 | let ``Check Verb in ProcessStartInfo with Verb`` () = 73 | cli { 74 | Exec "cmd.exe" 75 | Verb "open" 76 | } 77 | |> Command.buildProcess 78 | |> _.Verb 79 | |> should equal "open" 80 | 81 | [] 82 | [] 83 | let ``Check UserName in ProcessStartInfo with Username`` () = 84 | cli { 85 | Exec "cmd.exe" 86 | Username "admin" 87 | } 88 | |> Command.buildProcess 89 | |> _.UserName 90 | |> should equal "admin" 91 | 92 | [] 93 | [] 94 | let ``Check Domain, UserName, Password in ProcessStartInfo with Credentials for windows (overwrites Username)`` () = 95 | let config = 96 | cli { 97 | Exec "cmd.exe" 98 | Username "admin" 99 | Credentials("domain", "user", "password") 100 | } 101 | |> Command.buildProcess 102 | 103 | config.Domain |> should equal "domain" 104 | config.UserName |> should equal "user" 105 | config.Password |> should not' (equal "password") // stored as SecureString 106 | 107 | [] 108 | [] 109 | let ``Check Environment in ProcessStartInfo with single environment variable`` () = 110 | cli { 111 | Exec "cmd.exe" 112 | EnvironmentVariable("Fli", "test") 113 | } 114 | |> Command.buildProcess 115 | |> _.Environment.Contains(KeyValuePair("Fli", "test")) 116 | |> should be True 117 | 118 | [] 119 | [] 120 | let ``Check Environment in ProcessStartInfo with multiple environment variables`` () = 121 | let config = 122 | cli { 123 | Exec "cmd.exe" 124 | EnvironmentVariables [ ("Fli", "test"); ("Fli.Test", "test") ] 125 | } 126 | |> Command.buildProcess 127 | 128 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 129 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 130 | 131 | [] 132 | [] 133 | let ``Check StandardOutputEncoding & StandardErrorEncoding with setting Encoding`` () = 134 | let config = 135 | cli { 136 | Exec "cmd.exe" 137 | Encoding Encoding.UTF8 138 | } 139 | |> Command.buildProcess 140 | 141 | config.StandardOutputEncoding |> should equal Encoding.UTF8 142 | config.StandardErrorEncoding |> should equal Encoding.UTF8 143 | 144 | [] 145 | [] 146 | let ``Check all possible values in ProcessStartInfo for windows`` () = 147 | let config = 148 | cli { 149 | Exec "cmd.exe" 150 | Arguments "--help" 151 | Input "Test" 152 | Output @"C:\Users\test.txt" 153 | WorkingDirectory @"C:\Users" 154 | Verb "open" 155 | Username "admin" 156 | Credentials("domain", "admin", "password") 157 | EnvironmentVariable("Fli", "test") 158 | EnvironmentVariables [ ("Fli.Test", "test") ] 159 | Encoding Encoding.UTF8 160 | WindowStyle Normal 161 | } 162 | |> Command.buildProcess 163 | 164 | config.FileName |> should equal "cmd.exe" 165 | config.Arguments |> should equal "--help" 166 | config.WorkingDirectory |> should equal @"C:\Users" 167 | config.Verb |> should equal "open" 168 | config.Domain |> should equal "domain" 169 | config.UserName |> should equal "admin" 170 | config.Password |> should not' (equal "password") 171 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 172 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 173 | config.StandardOutputEncoding |> should equal Encoding.UTF8 174 | config.StandardErrorEncoding |> should equal Encoding.UTF8 175 | config.WindowStyle |> should equal Diagnostics.ProcessWindowStyle.Normal 176 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecCommandExecuteUnixTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandExecuteLinuxTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Text 8 | 9 | [] 10 | [] 11 | let ``Hello World with executing program`` () = 12 | cli { 13 | Exec "bash" 14 | Arguments "-c \"echo Hello World!\"" 15 | } 16 | |> Command.execute 17 | |> Output.toText 18 | |> should equal "Hello World!" 19 | 20 | [] 21 | [] 22 | let ``Get process Id`` () = 23 | cli { 24 | Exec "bash" 25 | Arguments "-c \"echo Test\"" 26 | } 27 | |> Command.execute 28 | |> Output.toId 29 | |> should not' (equal 0) 30 | 31 | [] 32 | [] 33 | let ``print text with Input with executing program`` () = 34 | cli { 35 | Exec "bash" 36 | Arguments "-c \"echo Test\"" 37 | Input "echo Hello World!" 38 | WorkingDirectory @"/etc/" 39 | } 40 | |> Command.execute 41 | |> Output.toText 42 | |> should equal "Test" 43 | 44 | [] 45 | [] 46 | let ``Get output in StringBuilder`` () = 47 | let sb = StringBuilder() 48 | 49 | cli { 50 | Exec "bash" 51 | Arguments "-c \"echo Test\"" 52 | Output sb 53 | } 54 | |> Command.execute 55 | |> ignore 56 | 57 | sb.ToString() |> should equal "Test\n" 58 | 59 | [] 60 | [] 61 | let ``Call custom function in output`` () = 62 | let testFunc (test: string) (s: string) = s |> should equal test 63 | 64 | cli { 65 | Exec "bash" 66 | Arguments "-c \"echo Test\"" 67 | Output(testFunc "Test\n") 68 | } 69 | |> Command.execute 70 | |> ignore 71 | 72 | [] 73 | [] 74 | let ``Hello World with executing program async`` () = 75 | async { 76 | let! output = 77 | cli { 78 | Exec "bash" 79 | Arguments "-c \"echo Hello World!\"" 80 | } 81 | |> Command.executeAsync 82 | 83 | output |> Output.toText |> should equal "Hello World!" 84 | } 85 | 86 | [] 87 | [] 88 | let ``Passing data to a progrma on stdin`` () = 89 | cli { 90 | Exec "cat" 91 | Input "Test" 92 | } 93 | |> Command.execute 94 | |> Output.toText 95 | |> should equal "Test" 96 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecCommandExecuteWindowsTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandExecuteWindowsTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Text 8 | 9 | 10 | [] 11 | [] 12 | let ``Hello World with executing program`` () = 13 | cli { 14 | Exec "cmd.exe" 15 | Arguments "/C echo Hello World!" 16 | } 17 | |> Command.execute 18 | |> Output.toText 19 | |> should equal "Hello World!" 20 | 21 | [] 22 | [] 23 | let ``Print "Test" properly with ArgumentList`` () = 24 | cli { 25 | Exec "pwsh.exe" 26 | Arguments [ "/C"; "Write-Host \"Test\"" ] 27 | } 28 | |> Command.execute 29 | |> Output.toText 30 | |> should equal "Test" 31 | 32 | [] 33 | [] 34 | let ``Get process Id`` () = 35 | cli { 36 | Exec "cmd.exe" 37 | Arguments "/C echo Test" 38 | } 39 | |> Command.execute 40 | |> Output.toId 41 | |> should not' (equal 0) 42 | 43 | [] 44 | [] 45 | let ``print text with Input with executing program`` () = 46 | cli { 47 | Exec "cmd.exe" 48 | Arguments "/k echo Test" 49 | Input "echo Hello World!" 50 | WorkingDirectory @"C:\" 51 | } 52 | |> Command.execute 53 | |> Output.toText 54 | |> should equal "Test\r\n\r\nC:\\>echo Hello World!\r\nHello World!\r\n\r\nC:\\>" 55 | 56 | [] 57 | [] 58 | let ``Get output in StringBuilder`` () = 59 | let sb = StringBuilder() 60 | 61 | cli { 62 | Exec "cmd.exe" 63 | Arguments "/c echo Test" 64 | Output sb 65 | } 66 | |> Command.execute 67 | |> ignore 68 | 69 | sb.ToString() |> should equal "Test\r\n" 70 | 71 | [] 72 | [] 73 | let ``Call custom function in output`` () = 74 | let testFunc (test: string) (s: string) = s |> should equal test 75 | 76 | cli { 77 | Exec "cmd.exe" 78 | Arguments "/c echo Test" 79 | Output(testFunc "Test\r\n") 80 | } 81 | |> Command.execute 82 | |> ignore 83 | 84 | [] 85 | [] 86 | let ``Hello World with executing program async`` () = 87 | async { 88 | let! output = 89 | cli { 90 | Exec "cmd.exe" 91 | Arguments "/C echo Hello World!" 92 | } 93 | |> Command.executeAsync 94 | 95 | output |> Output.toText |> should equal "Hello World!" 96 | } 97 | 98 | 99 | [] 100 | [] 101 | let ``Hello World with executing program with Verb`` () = 102 | cli { 103 | Exec "cmd.exe" 104 | Verb "open" 105 | Arguments "/C echo Hello World!" 106 | } 107 | |> Command.execute 108 | |> Output.toText 109 | |> should equal "Hello World!" 110 | 111 | 112 | [] 113 | [] 114 | let ``Hello World with executing program throws exception with unknown Verb`` () = 115 | try 116 | cli { 117 | Exec "cmd.exe" 118 | Verb "print" 119 | } 120 | |> Command.execute 121 | |> ignore 122 | with :? ArgumentException as ex -> 123 | ex.Message 124 | |> should equal ("Unknown verb 'print'. Possible verbs on 'cmd.exe': open, runas, runasuser") 125 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecCommandToStringTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandToStringTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | 7 | 8 | [] 9 | let ``Hello World with executing program`` () = 10 | cli { 11 | Exec "cmd.exe" 12 | Arguments "/C echo Hello World!" 13 | } 14 | |> Command.toString 15 | |> should equal "cmd.exe /C echo Hello World!" 16 | 17 | [] 18 | let ``Hello World with an argument list`` () = 19 | cli { 20 | Exec "cmd.exe" 21 | Arguments [ "/C"; "echo"; "Hello World!" ] 22 | } 23 | |> Command.toString 24 | |> should equal "cmd.exe /C echo Hello World!" 25 | 26 | [] 27 | let ``Hello World with an argument array`` () = 28 | cli { 29 | Exec "cmd.exe" 30 | Arguments [| "/C"; "echo"; "Hello World!" |] 31 | } 32 | |> Command.toString 33 | |> should equal "cmd.exe /C echo Hello World!" 34 | 35 | [] 36 | let ``Hello World with an argument seq`` () = 37 | cli { 38 | Exec "cmd.exe" 39 | 40 | Arguments( 41 | seq { 42 | "/C" 43 | "echo" 44 | "Hello World!" 45 | } 46 | ) 47 | } 48 | |> Command.toString 49 | |> should equal "cmd.exe /C echo Hello World!" 50 | -------------------------------------------------------------------------------- /src/Fli.Tests/ExecContext/ExecConfigTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ExecContext.ExecCommandConfigTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | 7 | 8 | [] 9 | let ``Check executable name config for executing program`` () = 10 | cli { Exec "cmd.exe" } |> (fun c -> c.config.Program) |> should equal "cmd.exe" 11 | 12 | [] 13 | let ``Check arguments config for executing program`` () = 14 | cli { 15 | Exec "cmd.exe" 16 | Arguments "echo Hello World!" 17 | } 18 | |> _.config.Arguments 19 | |> should equal (Some(Arguments(Some "echo Hello World!"))) 20 | 21 | [] 22 | let ``Check arguments list config for executing program`` () = 23 | cli { 24 | Exec "cmd.exe" 25 | Arguments [ "echo"; "Hello World!" ] 26 | } 27 | |> _.config.Arguments 28 | |> should equal (Some(ArgumentList(Some [| "echo"; "Hello World!" |]))) 29 | 30 | [] 31 | let ``Check Input config for executing program`` () = 32 | cli { 33 | Exec "cmd.exe" 34 | Input "echo 123\r\necho Test" 35 | } 36 | |> _.config.Input 37 | |> should equal (Some "echo 123\r\necho Test") 38 | 39 | [] 40 | let ``Check Output config for executing program`` () = 41 | cli { 42 | Exec "cmd.exe" 43 | Output @"C:\Users\test.txt" 44 | } 45 | |> _.config.Output 46 | |> should equal (Some(File @"C:\Users\test.txt")) 47 | 48 | [] 49 | let ``Empty string in Output ends up as None`` () = 50 | cli { 51 | Exec "cmd.exe" 52 | Output "" 53 | } 54 | |> _.config.Output 55 | |> should be (ofCase <@ None @>) 56 | 57 | [] 58 | let ``Nullable file path in Output ends up as None`` () = 59 | cli { 60 | Exec "cmd.exe" 61 | Output(File(null)) 62 | } 63 | |> _.config.Output 64 | |> should be (ofCase <@ None @>) 65 | 66 | [] 67 | let ``Check working directory config for executing program`` () = 68 | cli { 69 | Exec "cmd.exe" 70 | WorkingDirectory @"C:\Users" 71 | } 72 | |> _.config.WorkingDirectory 73 | |> should equal (Some @"C:\Users") 74 | 75 | [] 76 | let ``Check WindowStyle config for executing program`` () = 77 | cli { 78 | Exec "cmd.exe" 79 | WindowStyle Normal 80 | } 81 | |> _.config.WindowStyle 82 | |> should equal (Some Normal) 83 | 84 | [] 85 | let ``Check Verb config for executing program`` () = 86 | cli { 87 | Exec "cmd.exe" 88 | Verb "runas" 89 | } 90 | |> _.config.Verb 91 | |> should equal (Some "runas") 92 | 93 | [] 94 | let ``Check credentials config for executing program`` () = 95 | cli { 96 | Exec "cmd.exe" 97 | WorkingDirectory @"C:\Users" 98 | Username "admin" 99 | } 100 | |> _.config.UserName.Value 101 | |> should equal "admin" 102 | 103 | [] 104 | let ``Check credentials config for executing program with NetworkCredentials`` () = 105 | cli { 106 | Exec "cmd.exe" 107 | WorkingDirectory @"C:\Users" 108 | Username "admin@company" 109 | } 110 | |> _.config.UserName.Value 111 | |> should equal "admin@company" 112 | 113 | [] 114 | let ``Check EnvironmentVariables with single KeyValue config`` () = 115 | cli { 116 | Exec "cmd.exe" 117 | EnvironmentVariable("user", "admin") 118 | } 119 | |> _.config.EnvironmentVariables.Value 120 | |> should equal [ ("user", "admin") ] 121 | 122 | [] 123 | let ``Check EnvironmentVariables with multiple KeyValues config`` () = 124 | cli { 125 | Exec "cmd.exe" 126 | EnvironmentVariables [ ("user", "admin"); ("path", "path/to/file") ] 127 | } 128 | |> _.config.EnvironmentVariables.Value 129 | |> should equal [ ("user", "admin"); ("path", "path/to/file") ] 130 | 131 | [] 132 | let ``Check Credentials with domain, username and password`` () = 133 | cli { 134 | Exec "cmd.exe" 135 | Credentials("domain", "user", "password123") 136 | } 137 | |> _.config.Credentials.Value 138 | |> should equal (Credentials("domain", "user", "password123")) 139 | 140 | [] 141 | let ``Check Encoding with setting Encoding`` () = 142 | cli { 143 | Exec "cmd.exe" 144 | Encoding System.Text.Encoding.UTF8 145 | } 146 | |> _.config.Encoding.Value 147 | |> should equal System.Text.Encoding.UTF8 148 | -------------------------------------------------------------------------------- /src/Fli.Tests/Fli.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandConfigureUnixTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandConfigureUnixTests 2 | 3 | open Fli 4 | open NUnit.Framework 5 | open FsUnit 6 | open System.Collections.Generic 7 | open System.Text 8 | 9 | 10 | [] 11 | [] 12 | let ``Check FileName in ProcessStartInfo with CMD Shell`` () = 13 | cli { Shell BASH } |> Command.buildProcess |> _.FileName |> should equal "bash" 14 | 15 | [] 16 | [] 17 | let ``Check Argument in ProcessStartInfo with Command`` () = 18 | cli { 19 | Shell BASH 20 | Command "echo Hello World!" 21 | } 22 | |> Command.buildProcess 23 | |> _.Arguments 24 | |> should equal "-c \"echo Hello World!\"" 25 | 26 | [] 27 | [] 28 | let ``Check WorkingDirectory in ProcessStartInfo with WorkingDirectory`` () = 29 | cli { 30 | Shell BASH 31 | WorkingDirectory @"/etc/" 32 | } 33 | |> Command.buildProcess 34 | |> _.WorkingDirectory 35 | |> should equal @"/etc/" 36 | 37 | [] 38 | [] 39 | let ``Check Environment in ProcessStartInfo with single environment variable`` () = 40 | cli { 41 | Shell BASH 42 | EnvironmentVariable("Fli", "test") 43 | } 44 | |> Command.buildProcess 45 | |> _.Environment.Contains(KeyValuePair("Fli", "test")) 46 | |> should be True 47 | 48 | [] 49 | [] 50 | let ``Check Environment in ProcessStartInfo with multiple environment variables`` () = 51 | let config = 52 | cli { 53 | Shell BASH 54 | EnvironmentVariables [ ("Fli", "test"); ("Fli.Test", "test") ] 55 | } 56 | |> Command.buildProcess 57 | 58 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 59 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 60 | 61 | [] 62 | [] 63 | let ``Check StandardOutputEncoding & StandardErrorEncoding with setting Encoding`` () = 64 | let config = 65 | cli { 66 | Shell BASH 67 | Encoding Encoding.UTF8 68 | } 69 | |> Command.buildProcess 70 | 71 | config.StandardOutputEncoding |> should equal Encoding.UTF8 72 | config.StandardErrorEncoding |> should equal Encoding.UTF8 73 | 74 | [] 75 | [] 76 | let ``Check all possible values in ProcessStartInfo`` () = 77 | let config = 78 | cli { 79 | Shell BASH 80 | Command "echo Hello World! €" 81 | Input "echo TestInput" 82 | WorkingDirectory @"C:\Users" 83 | EnvironmentVariable("Fli", "test") 84 | EnvironmentVariables [ ("Fli.Test", "test") ] 85 | Encoding Encoding.UTF8 86 | } 87 | |> Command.buildProcess 88 | 89 | config.FileName |> should equal "bash" 90 | config.Arguments |> should equal "-c \"echo Hello World! €\"" 91 | config.WorkingDirectory |> should equal @"C:\Users" 92 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 93 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 94 | config.StandardOutputEncoding |> should equal Encoding.UTF8 95 | config.StandardErrorEncoding |> should equal Encoding.UTF8 96 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandConfigureWindowsTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandConfigureWindowsTests 2 | 3 | open Fli 4 | open NUnit.Framework 5 | open FsUnit 6 | open System.Collections.Generic 7 | open System.Text 8 | 9 | 10 | [] 11 | [] 12 | let ``Check FileName in ProcessStartInfo with CMD Shell`` () = 13 | cli { Shell CMD } 14 | |> Command.buildProcess 15 | |> _.FileName 16 | |> should equal "cmd.exe" 17 | 18 | [] 19 | [] 20 | let ``Check Argument in ProcessStartInfo with Command`` () = 21 | cli { 22 | Shell PS 23 | Command "echo Hello World!" 24 | } 25 | |> Command.buildProcess 26 | |> _.Arguments 27 | |> should equal "-Command echo Hello World!" 28 | 29 | [] 30 | [] 31 | let ``Check WorkingDirectory in ProcessStartInfo with WorkingDirectory`` () = 32 | cli { 33 | Shell CMD 34 | WorkingDirectory @"C:\Users" 35 | } 36 | |> Command.buildProcess 37 | |> _.WorkingDirectory 38 | |> should equal @"C:\Users" 39 | 40 | [] 41 | [] 42 | let ``Check Environment in ProcessStartInfo with single environment variable`` () = 43 | cli { 44 | Shell CMD 45 | EnvironmentVariable("Fli", "test") 46 | } 47 | |> Command.buildProcess 48 | |> _.Environment.Contains(KeyValuePair("Fli", "test")) 49 | |> should be True 50 | 51 | [] 52 | [] 53 | let ``Check Environment in ProcessStartInfo with multiple environment variables`` () = 54 | let config = 55 | cli { 56 | Shell CMD 57 | EnvironmentVariables [ ("Fli", "test"); ("Fli.Test", "test") ] 58 | } 59 | |> Command.buildProcess 60 | 61 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 62 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 63 | 64 | [] 65 | [] 66 | let ``Check StandardOutputEncoding & StandardErrorEncoding with setting Encoding`` () = 67 | let config = 68 | cli { 69 | Shell CMD 70 | Encoding Encoding.UTF8 71 | } 72 | |> Command.buildProcess 73 | 74 | config.StandardOutputEncoding |> should equal Encoding.UTF8 75 | config.StandardErrorEncoding |> should equal Encoding.UTF8 76 | 77 | [] 78 | [] 79 | let ``Check all possible values in ProcessStartInfo`` () = 80 | let config = 81 | cli { 82 | Shell CMD 83 | Command "echo Hello World! €" 84 | Input "echo TestInput" 85 | WorkingDirectory @"C:\Users" 86 | EnvironmentVariable("Fli", "test") 87 | EnvironmentVariables [ ("Fli.Test", "test") ] 88 | Encoding Encoding.UTF8 89 | } 90 | |> Command.buildProcess 91 | 92 | config.FileName |> should equal "cmd.exe" 93 | config.Arguments |> should equal "/k echo Hello World! €" 94 | config.WorkingDirectory |> should equal @"C:\Users" 95 | config.Environment.Contains(KeyValuePair("Fli", "test")) |> should be True 96 | config.Environment.Contains(KeyValuePair("Fli.Test", "test")) |> should be True 97 | config.StandardOutputEncoding |> should equal Encoding.UTF8 98 | config.StandardErrorEncoding |> should equal Encoding.UTF8 99 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandExecuteUnixTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandExecuteUnixTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Text 8 | open System.Diagnostics 9 | 10 | [] 11 | [] 12 | let ``Hello World with CUSTOM shell`` () = 13 | cli { 14 | Shell(CUSTOM("bash", "-c")) 15 | Command "echo Hello World!" 16 | } 17 | |> Command.execute 18 | |> Output.toText 19 | |> should equal "Hello World!" 20 | 21 | [] 22 | [] 23 | let ``Get output in StringBuilder`` () = 24 | let sb = StringBuilder() 25 | 26 | cli { 27 | Shell BASH 28 | Command "echo Test" 29 | Output sb 30 | } 31 | |> Command.execute 32 | |> ignore 33 | 34 | sb.ToString() |> should equal "Test\n" 35 | 36 | [] 37 | [] 38 | let ``Hello World with SH`` () = 39 | cli { 40 | Shell SH 41 | Command "echo Hello World!" 42 | } 43 | |> Command.execute 44 | |> Output.toText 45 | |> should equal "Hello World!" 46 | 47 | [] 48 | [] 49 | let ``Hello World with BASH`` () = 50 | cli { 51 | Shell BASH 52 | Command "echo Hello World!" 53 | } 54 | |> Command.execute 55 | |> Output.toText 56 | |> should equal "Hello World!" 57 | 58 | [] 59 | [] 60 | let ``Hello World with ZSH`` () = 61 | cli { 62 | Shell ZSH 63 | Command "echo Hello World!" 64 | } 65 | |> Command.execute 66 | |> Output.toText 67 | |> should equal "Hello World!" 68 | 69 | [] 70 | [] 71 | let ``Input text in BASH`` () = 72 | cli { 73 | Shell BASH 74 | Command "echo Hello World!" 75 | Input "echo Test" 76 | } 77 | |> Command.execute 78 | |> Output.toText 79 | |> should equal "Hello World!" 80 | 81 | [] 82 | [] 83 | let ``Hello World with BASH async`` () = 84 | async { 85 | let! output = 86 | cli { 87 | Shell BASH 88 | Command "echo Hello World!" 89 | } 90 | |> Command.executeAsync 91 | 92 | output |> Output.toText |> should equal "Hello World!" 93 | } 94 | 95 | [] 96 | [] 97 | let ``BASH returning non zero ExitCode`` () = 98 | cli { 99 | Shell BASH 100 | Command "echl Test" 101 | } 102 | |> Command.execute 103 | |> Output.toExitCode 104 | |> should equal 127 105 | 106 | [] 107 | [] 108 | let ``BASH returning non zero process id`` () = 109 | cli { 110 | Shell BASH 111 | Command "echo Test" 112 | } 113 | |> Command.execute 114 | |> Output.toId 115 | |> should not' (equal 0) 116 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandExecuteWindowsTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | open System.Text 8 | open System.Diagnostics 9 | 10 | 11 | [] 12 | [] 13 | let ``Hello World with CMD`` () = 14 | let operation = 15 | cli { 16 | Shell CMD 17 | Command "echo Hello World!" 18 | } 19 | |> Command.execute 20 | 21 | operation |> Output.toText |> should equal "Hello World!" 22 | operation |> Output.toError |> should equal "" 23 | 24 | [] 25 | [] 26 | let ``Hello World with CMD waiting async`` () = 27 | async { 28 | let stopwatch = new Stopwatch() 29 | stopwatch.Start() 30 | 31 | try 32 | let! operation = 33 | cli { 34 | Shell(CUSTOM("cmd.exe", "/K")) 35 | Command "Hello World!" 36 | CancelAfter 3000 37 | } 38 | |> Command.executeAsync 39 | 40 | () 41 | with :? AggregateException as e -> 42 | e.GetType() |> should equal typeof 43 | 44 | stopwatch.Stop() 45 | stopwatch.Elapsed.TotalSeconds |> should be (inRange 2.9 3.2) 46 | } 47 | 48 | [] 49 | [] 50 | let ``Hello World with CUSTOM shell`` () = 51 | cli { 52 | Shell(CUSTOM("cmd.exe", "/c")) 53 | Command "echo Hello World!" 54 | } 55 | |> Command.execute 56 | |> Output.toText 57 | |> should equal "Hello World!" 58 | 59 | [] 60 | [] 61 | let ``CMD returning non zero ExitCode`` () = 62 | cli { 63 | Shell CMD 64 | Command "echl Test" 65 | } 66 | |> Command.execute 67 | |> Output.toExitCode 68 | |> should equal 1 69 | 70 | [] 71 | [] 72 | let ``Get output in StringBuilder`` () = 73 | let sb = StringBuilder() 74 | 75 | cli { 76 | Shell CMD 77 | Command "echo Test" 78 | Output sb 79 | } 80 | |> Command.execute 81 | |> ignore 82 | 83 | sb.ToString() |> should equal "Test\r\n" 84 | 85 | [] 86 | [] 87 | let ``CMD returning non zero process id`` () = 88 | cli { 89 | Shell CMD 90 | Command "echo Test" 91 | } 92 | |> Command.execute 93 | |> Output.toId 94 | |> should not' (equal 0) 95 | 96 | [] 97 | [] 98 | let ``Text in Input with CMD`` () = 99 | cli { 100 | Shell CMD 101 | Input "echo 123\r\necho 345" 102 | WorkingDirectory @"C:\" 103 | } 104 | |> Command.execute 105 | |> Output.toText 106 | |> should equal "C:\\>echo 123\r\n123\r\n\r\nC:\\>echo 345\r\n345\r\n\r\nC:\\>" 107 | 108 | [] 109 | [] 110 | let ``CMD returning error message`` () = 111 | cli { 112 | Shell CMD 113 | Command "echl Test" 114 | } 115 | |> Command.execute 116 | |> Output.toError 117 | |> should not' (equal None) 118 | 119 | [] 120 | [] 121 | let ``CMD returning error message without text`` () = 122 | let operation = 123 | cli { 124 | Shell CMD 125 | Command "echl Test" 126 | } 127 | |> Command.execute 128 | 129 | operation |> Output.toError |> should not' (equal None) 130 | operation |> Output.toText |> should equal "" 131 | 132 | [] 133 | [] 134 | let ``Hello World with PS`` () = 135 | cli { 136 | Shell PS 137 | Command "Write-Host Hello World!" 138 | } 139 | |> Command.execute 140 | |> Output.toText 141 | |> should equal "Hello World!" 142 | 143 | [] 144 | [] 145 | let ``Hello World with PWSH`` () = 146 | cli { 147 | Shell PWSH 148 | Command "Write-Host Hello World!" 149 | } 150 | |> Command.execute 151 | |> Output.toText 152 | |> should equal "Hello World!" 153 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandToStringUnixTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandToStringUnixTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | 8 | [] 9 | [] 10 | let ``PWSH command toString returns full line`` () = 11 | cli { 12 | Shell PWSH 13 | Command "Write-Host Hello World!" 14 | } 15 | |> Command.toString 16 | |> should equal "pwsh -Command Write-Host Hello World!" 17 | 18 | [] 19 | [] 20 | let ``BASH command toString returns full line`` () = 21 | cli { 22 | Shell BASH 23 | Command "echo Hello World!" 24 | } 25 | |> Command.toString 26 | |> should equal "bash -c \"echo Hello World!\"" 27 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellCommandToStringWindowsTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellCommandToStringWindowsTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | open System 7 | 8 | 9 | [] 10 | [] 11 | let ``CMD command toString returns full line`` () = 12 | cli { 13 | Shell CMD 14 | Command "echo Hello World!" 15 | } 16 | |> Command.toString 17 | |> should equal "cmd.exe /c echo Hello World!" 18 | 19 | [] 20 | [] 21 | let ``CMD command toString returns full line in interactive mode`` () = 22 | cli { 23 | Shell CMD 24 | Command "echo Hello World!" 25 | Input "echo Hello World!" 26 | } 27 | |> Command.toString 28 | |> should equal "cmd.exe /k echo Hello World!" 29 | 30 | [] 31 | [] 32 | let ``PS command toString returns full line`` () = 33 | cli { 34 | Shell PS 35 | Command "Write-Host Hello World!" 36 | } 37 | |> Command.toString 38 | |> should equal "powershell.exe -Command Write-Host Hello World!" 39 | 40 | [] 41 | [] 42 | let ``PWSH command toString returns full line`` () = 43 | cli { 44 | Shell PWSH 45 | Command "Write-Host Hello World!" 46 | } 47 | |> Command.toString 48 | |> should equal "pwsh.exe -Command Write-Host Hello World!" 49 | 50 | [] 51 | [] 52 | let ``WSL command toString returns full line`` () = 53 | cli { 54 | Shell WSL 55 | Command "echo Hello World!" 56 | } 57 | |> Command.toString 58 | |> should equal "wsl.exe -- echo Hello World!" 59 | -------------------------------------------------------------------------------- /src/Fli.Tests/ShellContext/ShellConfigTests.fs: -------------------------------------------------------------------------------- 1 | module Fli.Tests.ShellContext.ShellConfigTests 2 | 3 | open NUnit.Framework 4 | open FsUnit 5 | open Fli 6 | 7 | 8 | [] 9 | let ``Check Shell config with CMD`` () = 10 | cli { Shell CMD } |> _.config.Shell |> should equal CMD 11 | 12 | [] 13 | let ``Check Command config`` () = 14 | cli { 15 | Shell PS 16 | Command "echo test" 17 | } 18 | |> _.config.Command 19 | |> should equal (Some "echo test") 20 | 21 | [] 22 | let ``Check Input config for CMD`` () = 23 | cli { 24 | Shell CMD 25 | Input "echo 123\r\necho Test" 26 | } 27 | |> _.config.Input 28 | |> should equal (Some "echo 123\r\necho Test") 29 | 30 | [] 31 | let ``Check Output config for CMD`` () = 32 | cli { 33 | Shell CMD 34 | Output @"C:\Users\test.txt" 35 | } 36 | |> _.config.Output 37 | |> should equal (Some(File @"C:\Users\test.txt")) 38 | 39 | [] 40 | let ``Empty string in Output ends up as None`` () = 41 | cli { 42 | Shell CMD 43 | Output "" 44 | } 45 | |> _.config.Output 46 | |> should be (ofCase <@ None @>) 47 | 48 | [] 49 | let ``Nullable file path in Output ends up as None`` () = 50 | cli { 51 | Shell CMD 52 | Output(File(null)) 53 | } 54 | |> _.config.Output 55 | |> should be (ofCase <@ None @>) 56 | 57 | [] 58 | let ``Check WorkingDirectory config`` () = 59 | cli { 60 | Shell BASH 61 | WorkingDirectory @"C:\Users" 62 | } 63 | |> _.config.WorkingDirectory 64 | |> should equal (Some @"C:\Users") 65 | 66 | [] 67 | let ``Check EnvironmentVariables with single KeyValue config`` () = 68 | cli { 69 | Shell BASH 70 | EnvironmentVariable("user", "admin") 71 | } 72 | |> _.config.EnvironmentVariables.Value 73 | |> should equal [ ("user", "admin") ] 74 | 75 | [] 76 | let ``Check EnvironmentVariables with multiple KeyValues config`` () = 77 | cli { 78 | Shell BASH 79 | EnvironmentVariables [ ("user", "admin"); ("path", "path/to/file") ] 80 | } 81 | |> _.config.EnvironmentVariables.Value 82 | |> should equal [ ("user", "admin"); ("path", "path/to/file") ] 83 | 84 | [] 85 | let ``Check Encoding with setting Encoding`` () = 86 | cli { 87 | Shell BASH 88 | Encoding System.Text.Encoding.UTF8 89 | } 90 | |> _.config.Encoding.Value 91 | |> should equal System.Text.Encoding.UTF8 92 | -------------------------------------------------------------------------------- /src/Fli.Tests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | 3 | FsUnit 4 | Microsoft.NET.Test.Sdk 5 | NUnit 6 | NUnit3TestAdapter 7 | NUnit.Analyzers -------------------------------------------------------------------------------- /src/Fli.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32901.215 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fli", "Fli\Fli.fsproj", "{401B7916-2350-49CC-96F8-A0280F06B0AE}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fli.Tests", "Fli.Tests\Fli.Tests.fsproj", "{AD2B83A4-C36A-4963-B15C-5C6F8E5A0915}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {401B7916-2350-49CC-96F8-A0280F06B0AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {401B7916-2350-49CC-96F8-A0280F06B0AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {401B7916-2350-49CC-96F8-A0280F06B0AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {401B7916-2350-49CC-96F8-A0280F06B0AE}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {AD2B83A4-C36A-4963-B15C-5C6F8E5A0915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {AD2B83A4-C36A-4963-B15C-5C6F8E5A0915}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {AD2B83A4-C36A-4963-B15C-5C6F8E5A0915}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {AD2B83A4-C36A-4963-B15C-5C6F8E5A0915}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {C56166F4-FF51-4A92-82DB-AC3A08EA113A} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Fli/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | module AssemblyInfo 2 | 3 | open System.Runtime.CompilerServices 4 | 5 | [] 6 | do () 7 | -------------------------------------------------------------------------------- /src/Fli/CE.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | [] 4 | module CE = 5 | 6 | open System.Text 7 | open Domain 8 | 9 | type ICommandContext<'a> with 10 | 11 | member this.Yield _ = this 12 | 13 | type StartingContext = 14 | { config: Config option } 15 | 16 | member this.CurrentConfig = this.config |> Option.defaultValue Defaults 17 | 18 | interface ICommandContext with 19 | member this.Context = this 20 | 21 | let cli = { config = None } 22 | 23 | /// Extensions for Shell context. 24 | type ICommandContext<'a> with 25 | 26 | /// `Shell` opening keyword, which `Fli.Shell` shall be used. 27 | [] 28 | member _.Shell(context: ICommandContext, shell) = 29 | Cli.shell shell context.Context.CurrentConfig 30 | 31 | /// Which `Command` should be executed in the `Shell`. 32 | [] 33 | member _.Command(context: ICommandContext, command) = Cli.command command context.Context 34 | 35 | /// `Input` string(s) that can be used to interact with the shell. 36 | [] 37 | member _.Input(context: ICommandContext, input) = Cli.input input context.Context 38 | 39 | /// Extra `Output` that is being executed immediately after getting output from execution. 40 | [] 41 | member _.Output(context: ICommandContext, output: Outputs) = Cli.output output context.Context 42 | 43 | /// Extra `Output` that is being executed immediately after getting output from execution. 44 | [] 45 | member _.Output(context: ICommandContext, filePath: string) = 46 | Cli.output (File filePath) context.Context 47 | 48 | /// Extra `Output` that is being executed immediately after getting output from execution. 49 | [] 50 | member _.Output(context: ICommandContext, stringBuilder: StringBuilder) = 51 | Cli.output (StringBuilder stringBuilder) context.Context 52 | 53 | /// Extra `Output` that is being executed immediately after getting output from execution. 54 | [] 55 | member _.Output(context: ICommandContext, func: string -> unit) = 56 | Cli.output (Custom func) context.Context 57 | 58 | /// Current executing `working directory`. 59 | [] 60 | member _.WorkingDirectory(context: ICommandContext, workingDirectory) = 61 | Cli.workingDirectory workingDirectory context.Context 62 | 63 | /// The `WindowStyle` for newly created windows. 64 | /// Hint: Hidden, Maximized, Minimized or Normal. 65 | [] 66 | member _.WindowStyle(context: ICommandContext, windowStyle) = 67 | Cli.windowStyle windowStyle context.Context 68 | 69 | /// One tupled `EnvironmentVariable`. 70 | [] 71 | member _.EnvironmentVariable(context: ICommandContext, environmentVariable) = 72 | Cli.environmentVariables [ environmentVariable ] context.Context 73 | 74 | /// A list of tupled `EnvironmentVariables`. 75 | [] 76 | member _.EnvironmentVariables(context: ICommandContext, environmentVariables) = 77 | Cli.environmentVariables environmentVariables context.Context 78 | 79 | /// `Encoding` that'll be used for `Input`, 'Output.Text' and `Output.Error`. 80 | [] 81 | member _.Encoding(context: ICommandContext, encoding) = Cli.encoding encoding context.Context 82 | 83 | /// Cancel after a period of time in milliseconds for async operations. 84 | [] 85 | member _.CancelAfter(context: ICommandContext, cancelAfter) = 86 | Cli.cancelAfter cancelAfter context.Context 87 | 88 | /// Extensions for Exec context. 89 | type ICommandContext<'a> with 90 | 91 | /// `Exec` opening keyword, which binary/executable shall be started. 92 | [] 93 | member _.Exec(context: ICommandContext, program) = 94 | Program.program program context.Context.CurrentConfig 95 | 96 | /// `Arguments` that will be passed into the executable. 97 | [] 98 | member _.Arguments(context: ICommandContext, arguments) = 99 | let matchArguments arguments = 100 | match box arguments with 101 | | :? string as s -> s |> Some |> Arguments 102 | | :? seq as s -> s |> Array.ofSeq |> Some |> ArgumentList 103 | | _ -> failwith "Cannot convert arguments to a string!" 104 | 105 | Program.arguments (matchArguments arguments) context.Context 106 | 107 | /// `Input` string(s) that can be used to interact with the executable. 108 | [] 109 | member _.Input(context: ICommandContext, input) = Program.input input context.Context 110 | 111 | /// Extra `Output` that is being executed immediately after getting output from execution. 112 | [] 113 | member _.Output(context: ICommandContext, output: Outputs) = Program.output output context.Context 114 | 115 | /// Extra `Output` that is being executed immediately after getting output from execution. 116 | [] 117 | member _.Output(context: ICommandContext, filePath: string) = 118 | Program.output (File filePath) context.Context 119 | 120 | /// Extra `Output` that is being executed immediately after getting output from execution. 121 | [] 122 | member _.Output(context: ICommandContext, stringBuilder: StringBuilder) = 123 | Program.output (StringBuilder stringBuilder) context.Context 124 | 125 | /// Extra `Output` that is being executed immediately after getting output from execution. 126 | [] 127 | member _.Output(context: ICommandContext, func: string -> unit) = 128 | Program.output (Custom func) context.Context 129 | 130 | /// Current executing `working directory`. 131 | [] 132 | member _.WorkingDirectory(context: ICommandContext, workingDirectory) = 133 | Program.workingDirectory workingDirectory context.Context 134 | 135 | /// The `WindowStyle` for newly created windows. 136 | /// Hint: Hidden, Maximized, Minimized or Normal. 137 | [] 138 | member _.WindowStyle(context: ICommandContext, windowStyle) = 139 | Program.windowStyle windowStyle context.Context 140 | 141 | /// `Verb` keyword that can be used to start the executable. 142 | [] 143 | member _.Verb(context: ICommandContext, verb) = Program.verb verb context.Context 144 | 145 | /// Start the executable with another `Username`. 146 | [] 147 | member _.UserName(context: ICommandContext, userName) = 148 | Program.userName userName context.Context 149 | 150 | /// Start the executable with other `Credentials`. 151 | /// Hint: `Domain` and `Password` are available for Windows systems only. 152 | [] 153 | member _.Credentials(context: ICommandContext, credentials) = 154 | let domain, user, pw = credentials in Program.credentials (Credentials(domain, user, pw)) context.Context 155 | 156 | /// One tupled `EnvironmentVariable`. 157 | [] 158 | member _.EnvironmentVariable(context: ICommandContext, environmentVariable) = 159 | Program.environmentVariables [ environmentVariable ] context.Context 160 | 161 | /// A list of tupled `EnvironmentVariables`. 162 | [] 163 | member _.EnvironmentVariables(context: ICommandContext, environmentVariables) = 164 | Program.environmentVariables environmentVariables context.Context 165 | 166 | /// `Encoding` that'll be used for `Input`, 'Output.Text' and `Output.Error`. 167 | [] 168 | member _.Encoding(context: ICommandContext, encoding) = 169 | Program.encoding encoding context.Context 170 | 171 | /// Cancel after a period of time in milliseconds for async operations. 172 | [] 173 | member _.CancelAfter(context: ICommandContext, cancelAfter) = 174 | Program.cancelAfter cancelAfter context.Context 175 | -------------------------------------------------------------------------------- /src/Fli/Command.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | [] 4 | module Command = 5 | 6 | open Domain 7 | open Helpers 8 | open Extensions 9 | open System 10 | open System.IO 11 | open System.Text 12 | open System.Diagnostics 13 | open System.Runtime.InteropServices 14 | open System.Threading 15 | 16 | let private getAvailablePwshExe () = 17 | match RuntimeInformation.IsOSPlatform(OSPlatform.Windows) with 18 | | true -> "pwsh.exe" 19 | | false -> "pwsh" 20 | 21 | let private shellToProcess (shell: Shells) (input: string option) = 22 | match shell with 23 | | CMD -> "cmd.exe", (if input.IsNone then "/c" else "/k") 24 | | PS -> "powershell.exe", "-Command" 25 | | PWSH -> getAvailablePwshExe (), "-Command" 26 | | WSL -> "wsl.exe", "--" 27 | | SH -> "sh", "-c" 28 | | BASH -> "bash", "-c" 29 | | ZSH -> "zsh", "-c" 30 | | CUSTOM(shell, flag) -> shell, flag 31 | 32 | let private getArguments arguments executable = 33 | match arguments with 34 | | Some(Arguments a) -> 35 | let args = (a |> Option.defaultValue "") 36 | ProcessStartInfo(executable, args) 37 | | Some(ArgumentList list) -> 38 | #if NET 39 | ProcessStartInfo(executable, (list |> Option.defaultValue [||])) 40 | #else 41 | ProcessStartInfo(executable, ArgumentList(list).toString()) 42 | #endif 43 | | None -> ProcessStartInfo(executable, "") 44 | 45 | let private createProcess executable arguments openDefault = 46 | let processInfo = getArguments arguments executable 47 | processInfo.CreateNoWindow <- true 48 | processInfo.UseShellExecute <- openDefault 49 | processInfo.RedirectStandardInput <- not openDefault 50 | processInfo.RedirectStandardOutput <- not openDefault 51 | processInfo.RedirectStandardError <- not openDefault 52 | processInfo 53 | 54 | let private trim (s: string) = s.TrimEnd([| '\r'; '\n' |]) 55 | 56 | let returnOr (sb: StringBuilder) (output: string) = 57 | match (sb.ToString(), output) with 58 | | t, _ when t.Length > 0 -> t 59 | | _, o when o.Length > 0 -> o 60 | | _, _ -> "" 61 | 62 | #if NET 63 | let private startProcessAsync 64 | (inFunc: Process -> Tasks.Task) 65 | (outFunc: string -> unit) 66 | cancellationToken 67 | psi 68 | = 69 | async { 70 | let proc = Process.Start(startInfo = psi) 71 | do! proc |> inFunc |> Async.AwaitTask 72 | 73 | let sbStd = StringBuilder() 74 | let sbErr = StringBuilder() 75 | 76 | proc.OutputDataReceived.AddHandler( 77 | new DataReceivedEventHandler(fun s e -> 78 | use o = proc.StandardOutput 79 | sbStd.Append(o.ReadToEnd()) |> ignore) 80 | ) 81 | 82 | proc.ErrorDataReceived.AddHandler( 83 | new DataReceivedEventHandler(fun s e -> 84 | use o = proc.StandardError 85 | sbErr.Append(o.ReadToEnd()) |> ignore) 86 | ) 87 | 88 | try 89 | do! proc.WaitForExitAsync(cancellationToken) |> Async.AwaitTask 90 | with :? OperationCanceledException -> 91 | () 92 | 93 | cancellationToken.ThrowIfCancellationRequested() 94 | let! stdo = proc.StandardOutput.ReadToEndAsync() |> Async.AwaitTask 95 | let text = returnOr sbStd stdo 96 | 97 | let! stde = proc.StandardError.ReadToEndAsync() |> Async.AwaitTask 98 | let error = returnOr sbErr stde 99 | 100 | do text |> outFunc 101 | 102 | return 103 | { Id = proc.Id 104 | Text = text |> trim |> toOption 105 | ExitCode = proc.ExitCode 106 | Error = error |> trim |> toOption } 107 | } 108 | |> Async.StartAsTask 109 | |> Async.AwaitTask 110 | #endif 111 | 112 | let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) psi = 113 | let proc = Process.Start(startInfo = psi) 114 | proc |> inputFunc 115 | 116 | let text = 117 | if psi.UseShellExecute |> not then 118 | proc.StandardOutput.ReadToEnd() 119 | else 120 | "" 121 | 122 | let error = 123 | if psi.UseShellExecute |> not then 124 | proc.StandardError.ReadToEnd() 125 | else 126 | "" 127 | 128 | proc.WaitForExit() 129 | 130 | text |> outputFunc 131 | 132 | { Id = proc.Id 133 | Text = text |> trim |> toOption 134 | ExitCode = proc.ExitCode 135 | Error = error |> trim |> toOption } 136 | 137 | 138 | let private checkVerb (verb: string option) (executable: string) = 139 | match verb with 140 | | Some v -> 141 | let verbs = ProcessStartInfo(executable).Verbs 142 | 143 | if not (verbs |> Array.contains v) then 144 | $"""Unknown verb '{v}'. Possible verbs on '{executable}': {verbs |> String.concat ", "}""" 145 | |> ArgumentException 146 | |> raise 147 | | None -> () 148 | 149 | let private addEnvironmentVariables (variables: (string * string) list option) (psi: ProcessStartInfo) = 150 | if psi.UseShellExecute |> not then 151 | ((variables |> Option.defaultValue [] |> List.iter psi.Environment.Add), psi) 152 | |> snd 153 | else 154 | psi 155 | 156 | let private addCredentials (credentials: Credentials option) (psi: ProcessStartInfo) = 157 | match credentials with 158 | | Some(Credentials(domain, username, password)) -> 159 | psi.UserName <- username 160 | 161 | if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then 162 | psi.Domain <- domain 163 | psi.Password <- (password |> toSecureString) 164 | 165 | psi 166 | | None -> psi 167 | 168 | let private writeInput (input: string option) (encoding: Encoding option) (p: Process) = 169 | match input with 170 | | Some inputText -> 171 | try 172 | use sw = p.StandardInput 173 | sw.WriteLine(inputText, encoding) 174 | sw.Flush() 175 | sw.Close() 176 | with :? IOException as ex when ex.GetType() = typedefof -> 177 | () 178 | | None -> () 179 | 180 | let private writeInputAsync (input: string option) (p: Process) = 181 | async { 182 | match input with 183 | | Some inputText -> 184 | try 185 | use sw = p.StandardInput 186 | do! inputText |> sw.WriteLineAsync |> Async.AwaitTask 187 | do! sw.FlushAsync() |> Async.AwaitTask 188 | sw.Close() 189 | with :? IOException as ex when ex.GetType() = typedefof -> 190 | () 191 | | None -> () 192 | } 193 | |> Async.StartAsTask 194 | 195 | let private writeOutput (outputType: Outputs option) (output: string) = 196 | match outputType with 197 | | Some(o) -> 198 | match o with 199 | | Outputs.File(file) -> File.WriteAllText(file, output) 200 | | Outputs.StringBuilder(stringBuilder) -> output |> stringBuilder.Append |> ignore 201 | | Outputs.Custom(func) -> func.Invoke(output) 202 | | None -> () 203 | 204 | let private setupCancellationToken (cancelAfter: int option) = 205 | let cts = new CancellationTokenSource() 206 | 207 | match cancelAfter with 208 | | None -> () 209 | | Some ca -> cts.CancelAfter(ca) 210 | 211 | cts.Token 212 | 213 | let private quoteBashCommand (context: ShellContext) = 214 | let noQuoteNeeded = [| Shells.CMD; Shells.PWSH; Shells.PS; Shells.WSL |] 215 | 216 | match Array.contains context.config.Shell noQuoteNeeded with 217 | | true -> context.config.Command |> Option.defaultValue "" 218 | | false -> context.config.Command |> Option.defaultValue "" |> (fun s -> $"\"{s}\"") 219 | 220 | let private getProcessWindowStyle (windowStyle: WindowStyle) = 221 | match windowStyle with 222 | | Hidden -> ProcessWindowStyle.Hidden 223 | | Maximized -> ProcessWindowStyle.Maximized 224 | | Minimized -> ProcessWindowStyle.Minimized 225 | | Normal -> ProcessWindowStyle.Normal 226 | 227 | type Command = 228 | static member internal buildProcess(context: ShellContext) = 229 | let proc, flag = (context.config.Shell, context.config.Input) ||> shellToProcess 230 | let command = context |> quoteBashCommand 231 | let args = Arguments(Some $"""{flag} {command}""") 232 | 233 | (createProcess proc (Some args) false) 234 | .With(WorkingDirectory = (context.config.WorkingDirectory |> Option.defaultValue "")) 235 | .With(StandardOutputEncoding = (context.config.Encoding |> Option.defaultValue null)) 236 | .With(StandardErrorEncoding = (context.config.Encoding |> Option.defaultValue null)) 237 | .With(WindowStyle = getProcessWindowStyle (context.config.WindowStyle |> Option.defaultValue Hidden)) 238 | |> addEnvironmentVariables context.config.EnvironmentVariables 239 | 240 | static member internal buildProcess(context: ExecContext) = 241 | checkVerb context.config.Verb context.config.Program 242 | 243 | let openDefault = 244 | context.config.Arguments.IsNone 245 | && context.config.EnvironmentVariables.IsNone 246 | && context.config.Input.IsNone 247 | 248 | (createProcess context.config.Program context.config.Arguments openDefault) 249 | .With(Verb = (context.config.Verb |> Option.defaultValue null)) 250 | .With(WorkingDirectory = (context.config.WorkingDirectory |> Option.defaultValue "")) 251 | .With(UserName = (context.config.UserName |> Option.defaultValue "")) 252 | .With(StandardOutputEncoding = (context.config.Encoding |> Option.defaultValue null)) 253 | .With(StandardErrorEncoding = (context.config.Encoding |> Option.defaultValue null)) 254 | .With(WindowStyle = getProcessWindowStyle (context.config.WindowStyle |> Option.defaultValue Hidden)) 255 | |> addCredentials context.config.Credentials 256 | |> addEnvironmentVariables context.config.EnvironmentVariables 257 | 258 | /// Stringifies shell + opening flag and given command. 259 | static member toString(context: ShellContext) = 260 | let proc, flag = (context.config.Shell, context.config.Input) ||> shellToProcess 261 | let command = context |> quoteBashCommand 262 | $"""{proc} {flag} {command}""" 263 | 264 | /// Stringifies executable + arguments. 265 | static member toString(context: ExecContext) = 266 | let args = 267 | if context.config.Arguments.IsSome then 268 | context.config.Arguments.Value.toString () 269 | else 270 | "" 271 | 272 | $"""{context.config.Program} {args}""" 273 | 274 | /// Executes the given context as a new process. 275 | static member execute(context: ShellContext) = 276 | context 277 | |> Command.buildProcess 278 | |> startProcess 279 | (writeInput context.config.Input context.config.Encoding) 280 | (writeOutput context.config.Output) 281 | 282 | /// Executes the given context as a new process. 283 | static member execute(context: ExecContext) = 284 | context 285 | |> Command.buildProcess 286 | |> startProcess 287 | (writeInput context.config.Input context.config.Encoding) 288 | (writeOutput context.config.Output) 289 | 290 | #if NET 291 | /// Executes the given context as a new process asynchronously. 292 | static member executeAsync(context: ShellContext) = 293 | context 294 | |> Command.buildProcess 295 | |> startProcessAsync 296 | (writeInputAsync context.config.Input) 297 | (writeOutput context.config.Output) 298 | (setupCancellationToken context.config.CancelAfter) 299 | 300 | /// Executes the given context as a new process asynchronously. 301 | static member executeAsync(context: ExecContext) = 302 | context 303 | |> Command.buildProcess 304 | |> startProcessAsync 305 | (writeInputAsync context.config.Input) 306 | (writeOutput context.config.Output) 307 | (setupCancellationToken context.config.CancelAfter) 308 | #endif 309 | -------------------------------------------------------------------------------- /src/Fli/Domain.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | [] 4 | module Domain = 5 | 6 | open System 7 | open System.Text 8 | 9 | type ICommandContext<'a> = 10 | abstract member Context: 'a 11 | 12 | type ShellConfig = 13 | { Shell: Shells 14 | Command: string option 15 | Input: string option 16 | Output: Outputs option 17 | WorkingDirectory: string option 18 | EnvironmentVariables: (string * string) list option 19 | Encoding: Encoding option 20 | CancelAfter: int option 21 | WindowStyle: WindowStyle option } 22 | 23 | and Shells = 24 | | CMD 25 | | PS 26 | | PWSH 27 | | WSL 28 | | SH 29 | | BASH 30 | | ZSH 31 | | CUSTOM of shell: string * flag: string 32 | 33 | and Outputs = 34 | | File of string 35 | | StringBuilder of StringBuilder 36 | | Custom of Func 37 | 38 | and WindowStyle = 39 | | Hidden 40 | | Maximized 41 | | Minimized 42 | | Normal 43 | 44 | type ExecConfig = 45 | { Program: string 46 | Arguments: Arguments option 47 | Input: string option 48 | Output: Outputs option 49 | WorkingDirectory: string option 50 | Verb: string option 51 | UserName: string option 52 | Credentials: Credentials option 53 | EnvironmentVariables: (string * string) list option 54 | Encoding: Encoding option 55 | CancelAfter: int option 56 | WindowStyle: WindowStyle option } 57 | 58 | and Credentials = Credentials of Domain: string * UserName: string * Password: string 59 | 60 | and Arguments = 61 | | Arguments of string option 62 | | ArgumentList of string array option 63 | 64 | member x.toString() = 65 | match x with 66 | | Arguments(Some s) -> s 67 | | ArgumentList(Some s) -> 68 | let escapeString (str: string) = 69 | if str.Contains("\"") then 70 | str.Replace("\"", "\"\"") |> fun s -> $"\"{s}\"" 71 | else 72 | str 73 | 74 | s |> Seq.map escapeString |> String.concat " " 75 | | _ -> "" 76 | 77 | type Config = 78 | { ShellConfig: ShellConfig 79 | ExecConfig: ExecConfig } 80 | 81 | type ShellContext = 82 | { config: ShellConfig } 83 | 84 | interface ICommandContext with 85 | member this.Context = this 86 | 87 | type ExecContext = 88 | { config: ExecConfig } 89 | 90 | interface ICommandContext with 91 | member this.Context = this 92 | 93 | let Defaults = 94 | { ShellConfig = 95 | { Shell = CMD 96 | Command = None 97 | Input = None 98 | Output = None 99 | WorkingDirectory = None 100 | EnvironmentVariables = None 101 | Encoding = None 102 | CancelAfter = None 103 | WindowStyle = None } 104 | ExecConfig = 105 | { Program = "" 106 | Arguments = None 107 | Input = None 108 | Output = None 109 | WorkingDirectory = None 110 | Verb = None 111 | UserName = None 112 | Credentials = None 113 | EnvironmentVariables = None 114 | Encoding = None 115 | CancelAfter = None 116 | WindowStyle = None } } 117 | 118 | type Output = 119 | { Id: int 120 | Text: string option 121 | ExitCode: int 122 | Error: string option } 123 | -------------------------------------------------------------------------------- /src/Fli/Dsl.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | open Domain 4 | open Helpers 5 | 6 | module Cli = 7 | 8 | let shell (shell: Shells) (config: Config) : ShellContext = 9 | { config = 10 | { config.ShellConfig with 11 | Shell = shell } } 12 | 13 | let command (command: string) (context: ShellContext) = 14 | { context with 15 | config.Command = Some command } 16 | 17 | let input (input: string) (context: ShellContext) = 18 | { context with 19 | config.Input = Some input } 20 | 21 | let output (output: Outputs) (context: ShellContext) = 22 | let outputsOption = 23 | match output with 24 | | File path -> path |> toOptionWithDefault output 25 | | _ -> Some output 26 | 27 | { context with 28 | config.Output = outputsOption } 29 | 30 | let workingDirectory (workingDirectory: string) (context: ShellContext) = 31 | { context with 32 | config.WorkingDirectory = Some workingDirectory } 33 | 34 | let windowStyle (windowStyle: WindowStyle) (context: ShellContext) = 35 | { context with 36 | config.WindowStyle = Some windowStyle } 37 | 38 | let environmentVariables (variables: (string * string) list) (context: ShellContext) = 39 | let vars = 40 | match context.config.EnvironmentVariables with 41 | | Some(vs) -> vs @ variables 42 | | None -> variables 43 | 44 | { context with 45 | config.EnvironmentVariables = Some vars } 46 | 47 | let encoding (encoding: System.Text.Encoding) (context: ShellContext) = 48 | { context with 49 | config.Encoding = Some encoding } 50 | 51 | let cancelAfter (cancelAfter: int) (context: ShellContext) = 52 | { context with 53 | config.CancelAfter = Some cancelAfter } 54 | 55 | module Program = 56 | 57 | let program (program: string) (config: Config) : ExecContext = 58 | { config = 59 | { config.ExecConfig with 60 | Program = program } } 61 | 62 | let arguments (arguments: Arguments) (context: ExecContext) = 63 | { context with 64 | config.Arguments = Some arguments } 65 | 66 | let input (input: string) (context: ExecContext) = 67 | { context with 68 | config.Input = Some input } 69 | 70 | let output (output: Outputs) (context: ExecContext) = 71 | let outputsOption = 72 | match output with 73 | | File path -> path |> toOptionWithDefault output 74 | | _ -> Some output 75 | 76 | { context with 77 | config.Output = outputsOption } 78 | 79 | let workingDirectory (workingDirectory: string) (context: ExecContext) = 80 | { context with 81 | config.WorkingDirectory = Some workingDirectory } 82 | 83 | let windowStyle (windowStyle: WindowStyle) (context: ExecContext) = 84 | { context with 85 | config.WindowStyle = Some windowStyle } 86 | 87 | let verb (verb: string) (context: ExecContext) = 88 | { context with config.Verb = Some verb } 89 | 90 | let userName (userName: string) (context: ExecContext) = 91 | { context with 92 | config.UserName = Some userName } 93 | 94 | let credentials (credentials: Credentials) (context: ExecContext) = 95 | { context with 96 | config.Credentials = Some credentials } 97 | 98 | let environmentVariables (variables: (string * string) list) (context: ExecContext) = 99 | let vars = 100 | match context.config.EnvironmentVariables with 101 | | Some(vs) -> vs @ variables 102 | | None -> variables 103 | 104 | { context with 105 | config.EnvironmentVariables = Some vars } 106 | 107 | let encoding (encoding: System.Text.Encoding) (context: ExecContext) = 108 | { context with 109 | config.Encoding = Some encoding } 110 | 111 | let cancelAfter (cancelAfter: int) (context: ExecContext) = 112 | { context with 113 | config.CancelAfter = Some cancelAfter } 114 | -------------------------------------------------------------------------------- /src/Fli/Extensions.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | module Extensions = 4 | open System.Diagnostics 5 | 6 | type ProcessStartInfo with 7 | 8 | member x.With() = x 9 | -------------------------------------------------------------------------------- /src/Fli/Fli.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | Library 6 | netstandard2.0;netstandard2.1;net8.0 7 | false 8 | Debug;Release 9 | 1.111.11.0 10 | 11 | 12 | true 13 | bin\$(Configuration)\$(TargetFramework)\Fli.xml 14 | 15 | 16 | Constantin Tews 17 | An F# library to run CLI commands and processes in F# (CE) style. 18 | Copyright © 2022-2025 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Fli/Helpers.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | #nowarn "9" 4 | 5 | module Helpers = 6 | 7 | open System.Runtime.InteropServices 8 | open System.Security 9 | 10 | let toSecureString (unsureString: string) = 11 | if isNull unsureString then 12 | raise <| System.ArgumentNullException("s") 13 | 14 | let gcHandle = GCHandle.Alloc(unsureString, GCHandleType.Pinned) 15 | 16 | try 17 | let secureString = 18 | new SecureString( 19 | NativeInterop.NativePtr.ofNativeInt (gcHandle.AddrOfPinnedObject()), 20 | unsureString.Length 21 | ) 22 | 23 | secureString.MakeReadOnly() 24 | secureString 25 | finally 26 | gcHandle.Free() 27 | 28 | let toOption = 29 | function 30 | | null 31 | | "" -> None 32 | | s -> Some s 33 | 34 | let toOptionWithDefault defaultValue value = 35 | match value with 36 | | null 37 | | "" -> None 38 | | _ -> Some defaultValue 39 | -------------------------------------------------------------------------------- /src/Fli/Output.fs: -------------------------------------------------------------------------------- 1 | namespace Fli 2 | 3 | module Output = 4 | 5 | /// Gets `Id` from `Output`. 6 | let toId (output: Output) = output.Id 7 | 8 | /// Prints `Id` from `Output`. 9 | let printId = toId >> printfn "%i" 10 | 11 | /// Gets `Text` from `Output`. 12 | let toText (output: Output) = output.Text |> Option.defaultValue "" 13 | 14 | /// Prints `Text` from `Output`. 15 | let printText = toText >> printfn "%s" 16 | 17 | /// Gets `ExitCode` from `Output`. 18 | let toExitCode (output: Output) = output.ExitCode 19 | 20 | /// Prints `ExitCode` from `Output`. 21 | let printExitCode = toExitCode >> printfn "%i" 22 | 23 | /// Gets `Error` from `Output`. 24 | let toError (output: Output) = output.Error |> Option.defaultValue "" 25 | 26 | /// Prints `Error` from `Output`. 27 | let printError = toError >> printfn "%s" 28 | 29 | /// Throws exception if given condition in `func` matches. 30 | let throw (func: Output -> bool) (output: Output) = 31 | if output |> func then 32 | failwith $"Execution failed with exit code {output.ExitCode} {output.Error}" 33 | 34 | output 35 | 36 | /// Throws exception if exit code is not 0. 37 | let throwIfErrored = throw (fun o -> o.ExitCode <> 0) 38 | -------------------------------------------------------------------------------- /src/Fli/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /src/Fli/paket.template: -------------------------------------------------------------------------------- 1 | type file 2 | id 3 | Fli 4 | owners 5 | Constantin Tews 6 | authors 7 | Constantin Tews 8 | version 9 | 1.111.11.0 10 | readme 11 | README.md 12 | releaseNotes 13 | - Improve ArgumentList in `Exec.Arguments`, this will split arguments more precise especially for python commands. (https://github.com/CaptnCodr/Fli/pull/76) 14 | - Improve documentations. 15 | (All release notes: https://github.com/CaptnCodr/Fli/blob/main/RELEASE_NOTES.md) 16 | projectUrl 17 | https://github.com/CaptnCodr/Fli 18 | iconUrl 19 | https://raw.githubusercontent.com/CaptnCodr/Fli/main/logo.png 20 | licenseExpression 21 | MIT 22 | licenseUrl 23 | https://licenses.nuget.org/MIT 24 | requireLicenseAcceptance 25 | false 26 | copyright 27 | Copyright 2022-2025 28 | tags 29 | fsharp cli shell process computation-expression 30 | description 31 | F# library to run CLI commands and processes in F# (CE) style. 32 | summary 33 | F# library to run CLI commands and processes in F# (CE) style. 34 | files 35 | /bin/Release/net8.0/Fli.* ==> lib/net8.0 36 | /bin/Release/netstandard2.0/Fli.* ==> lib/netstandard2.0 37 | /bin/Release/netstandard2.1/Fli.* ==> lib/netstandard2.1 38 | ../install.ps1 ==> tools 39 | ../../README.md ==> . 40 | ../../RELEASE_NOTES.md ==> . 41 | dependencies 42 | FSharp.Core >= LOCKEDVERSION -------------------------------------------------------------------------------- /src/install.ps1: -------------------------------------------------------------------------------- 1 | param($rootPath, $toolsPath, $package, $project) 2 | 3 | Add-BindingRedirect $project.Name --------------------------------------------------------------------------------