├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── Chell.sln ├── Directory.Build.props ├── LICENSE ├── README.md ├── samples ├── GettingStarted.Basic.Unix │ ├── GettingStarted.Basic.Unix.csproj │ └── Program.cs └── GettingStarted.Basic.Windows │ ├── GettingStarted.Basic.Windows.csproj │ └── Program.cs ├── src ├── .editorconfig ├── Chell.Run │ ├── Chell.Run.csproj │ └── Program.cs └── Chell │ ├── Chell.csproj │ ├── ChellEnvironment.cs │ ├── CommandLineString.cs │ ├── Exports.cs │ ├── Extensions │ ├── ChellExtensions.cs │ ├── ProcessOutputExtensions.cs │ ├── ProcessTaskExtensions.Generated.cs │ ├── ProcessTaskExtensions.cs │ ├── ProcessTaskExtensions.tt │ └── StringExtensions.cs │ ├── IO │ ├── ChellWrappedStream.cs │ ├── ChellWritableStream.Generated.cs │ ├── ChellWritableStream.tt │ ├── IConsoleProvider.cs │ ├── LINQPadConsoleProvider.cs │ └── SystemConsoleProvider.cs │ ├── Internal │ ├── CommandLineHelper.cs │ ├── EnvironmentVariables.cs │ ├── LINQPadHelper.cs │ ├── ObjectDumper.cs │ ├── OutputSink.cs │ ├── StandardInput.cs │ ├── StreamPipe.cs │ └── Which.cs │ ├── ProcessOutput.cs │ ├── ProcessTask.cs │ ├── ProcessTaskException.cs │ ├── ProcessTaskOptions.cs │ ├── Run.cs │ └── Shell │ ├── BashShellExecutor.cs │ ├── CmdShellExecutor.cs │ ├── IShellExecutor.cs │ ├── NoUseShellExecutor.cs │ └── ShellExecutorProvider.cs └── tests └── Chell.Tests ├── Chell.Tests.csproj ├── ChellEnvironmentTest.cs ├── CommandLineStringTest.cs ├── ProcessTaskTest.cs ├── Shell ├── BashShellExecutorTest.cs └── CmdShellExecutorTest.cs └── TemporaryAppBuilder.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | 7 | # All files 8 | [*] 9 | indent_style = space 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8 16 | 17 | [*.md] 18 | charset = utf-8 19 | 20 | ############################### 21 | # .NET Coding Conventions # 22 | ############################### 23 | [*.{cs,vb}] 24 | # Organize usings 25 | dotnet_sort_system_directives_first = true 26 | # this. preferences 27 | dotnet_style_qualification_for_field = false:silent 28 | dotnet_style_qualification_for_property = false:silent 29 | dotnet_style_qualification_for_method = false:silent 30 | dotnet_style_qualification_for_event = false:silent 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 33 | dotnet_style_predefined_type_for_member_access = true:silent 34 | # Parentheses preferences 35 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 39 | # Modifier preferences 40 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 41 | dotnet_style_readonly_field = true:suggestion 42 | # Expression-level preferences 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_explicit_tuple_names = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_coalesce_expression = true:suggestion 48 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 49 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 50 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 51 | dotnet_style_prefer_auto_properties = true:silent 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | ############################### 55 | # Naming Conventions # 56 | ############################### 57 | # Style Definitions 58 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 59 | # Use PascalCase for constant fields 60 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 61 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 62 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 63 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 64 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 65 | dotnet_naming_symbols.constant_fields.required_modifiers = const 66 | ############################### 67 | # C# Coding Conventions # 68 | ############################### 69 | [*.cs] 70 | # var preferences 71 | csharp_style_var_for_built_in_types = true:silent 72 | csharp_style_var_when_type_is_apparent = true:silent 73 | csharp_style_var_elsewhere = true:silent 74 | # Expression-bodied members 75 | csharp_style_expression_bodied_methods = false:silent 76 | csharp_style_expression_bodied_constructors = false:silent 77 | csharp_style_expression_bodied_operators = false:silent 78 | csharp_style_expression_bodied_properties = true:silent 79 | csharp_style_expression_bodied_indexers = true:silent 80 | csharp_style_expression_bodied_accessors = true:silent 81 | # Pattern matching preferences 82 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 83 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 84 | # Null-checking preferences 85 | csharp_style_throw_expression = true:suggestion 86 | csharp_style_conditional_delegate_call = true:suggestion 87 | # Modifier preferences 88 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 89 | # Expression-level preferences 90 | csharp_prefer_braces = true:silent 91 | csharp_style_deconstructed_variable_declaration = true:suggestion 92 | csharp_prefer_simple_default_expression = true:suggestion 93 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 94 | csharp_style_inlined_variable_declaration = true:suggestion 95 | ############################### 96 | # C# Formatting Rules # 97 | ############################### 98 | # New line preferences 99 | csharp_new_line_before_open_brace = all 100 | csharp_new_line_before_else = true 101 | csharp_new_line_before_catch = true 102 | csharp_new_line_before_finally = true 103 | csharp_new_line_before_members_in_object_initializers = true 104 | csharp_new_line_before_members_in_anonymous_types = true 105 | csharp_new_line_between_query_expression_clauses = true 106 | # Indentation preferences 107 | csharp_indent_case_contents = true 108 | csharp_indent_switch_labels = true 109 | csharp_indent_labels = flush_left 110 | # Space preferences 111 | csharp_space_after_cast = false 112 | csharp_space_after_keywords_in_control_flow_statements = true 113 | csharp_space_between_method_call_parameter_list_parentheses = false 114 | csharp_space_between_method_declaration_parameter_list_parentheses = false 115 | csharp_space_between_parentheses = false 116 | csharp_space_before_colon_in_inheritance_clause = true 117 | csharp_space_after_colon_in_inheritance_clause = true 118 | csharp_space_around_binary_operators = before_and_after 119 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 120 | csharp_space_between_method_call_name_and_opening_parenthesis = false 121 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 122 | # Wrapping preferences 123 | csharp_preserve_single_line_statements = true 124 | csharp_preserve_single_line_blocks = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | *.sh text eol=lf 7 | 8 | ############################################################################### 9 | # Set default behavior for command prompt diff. 10 | # 11 | # This is need for earlier builds of msysgit that does not have it on by 12 | # default for csharp files. 13 | # Note: This is only used by command line 14 | ############################################################################### 15 | #*.cs diff=csharp 16 | 17 | ############################################################################### 18 | # Set the merge driver for project and solution files 19 | # 20 | # Merging from the command prompt will add diff markers to the files if there 21 | # are conflicts (Merging from VS is not affected by the settings below, in VS 22 | # the diff markers are never inserted). Diff markers may cause the following 23 | # file extensions to fail to load in VS. An alternative would be to treat 24 | # these files as binary and thus will always conflict and require user 25 | # intervention with every merge. To do so, just uncomment the entries below 26 | ############################################################################### 27 | #*.sln merge=binary 28 | #*.csproj merge=binary 29 | #*.vbproj merge=binary 30 | #*.vcxproj merge=binary 31 | #*.vcproj merge=binary 32 | #*.dbproj merge=binary 33 | #*.fsproj merge=binary 34 | #*.lsproj merge=binary 35 | #*.wixproj merge=binary 36 | #*.modelproj merge=binary 37 | #*.sqlproj merge=binary 38 | #*.wwaproj merge=binary 39 | 40 | ############################################################################### 41 | # behavior for image files 42 | # 43 | # image files are treated as binary by default. 44 | ############################################################################### 45 | #*.jpg binary 46 | #*.png binary 47 | #*.gif binary 48 | 49 | ############################################################################### 50 | # diff behavior for common document formats 51 | # 52 | # Convert binary document formats to text before diffing them. This feature 53 | # is only available from the command line. Turn it on by uncommenting the 54 | # entries below. 55 | ############################################################################### 56 | #*.doc diff=astextplain 57 | #*.DOC diff=astextplain 58 | #*.docx diff=astextplain 59 | #*.DOCX diff=astextplain 60 | #*.dot diff=astextplain 61 | #*.DOT diff=astextplain 62 | #*.pdf diff=astextplain 63 | #*.PDF diff=astextplain 64 | #*.rtf diff=astextplain 65 | #*.RTF diff=astextplain 66 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Development 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | types: 10 | - opened 11 | - synchronize 12 | 13 | jobs: 14 | Build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [windows-latest, ubuntu-latest, macos-latest] 19 | env: 20 | DOTNET_NOLOGO: true 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: '5.0.x' 26 | 27 | # Build 28 | - run: dotnet restore 29 | - run: dotnet build -c Release 30 | 31 | # Run Unit tests 32 | - run: dotnet test -c Release --no-build --logger trx --results-directory $GITHUB_WORKSPACE/artifacts 33 | 34 | # Packaging 35 | - name: dotnet pack 36 | run: dotnet pack -c Release --no-build -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --output $GITHUB_WORKSPACE/artifacts 37 | shell: bash 38 | 39 | # Upload & Publish 40 | - uses: actions/upload-artifact@master 41 | with: 42 | name: Packages 43 | path: artifacts -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | Release: 10 | if: "contains(github.ref, 'refs/tags')" 11 | runs-on: ubuntu-latest 12 | env: 13 | DOTNET_NOLOGO: true 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: '5.0.x' 19 | 20 | - name: "Set VersionSuffix for Preview" 21 | if: "contains(github.ref, 'refs/tags') && contains(github.ref, 'preview')" 22 | run: | 23 | echo "VERSION_SUFFIX=preview.`date '+%Y%m%d-%H%M%S'`+${GITHUB_SHA:0:6}" >> $GITHUB_ENV 24 | - name: "Get git tag" 25 | if: "contains(github.ref, 'refs/tags')" 26 | run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 27 | 28 | # Build 29 | - run: dotnet restore 30 | - run: dotnet build -c Release 31 | 32 | # Packaging 33 | - name: dotnet pack 34 | run: dotnet pack -c Release --no-build --version-suffix "$VERSION_SUFFIX" -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --output $GITHUB_WORKSPACE/artifacts 35 | 36 | # Upload & Publish 37 | - uses: actions/upload-artifact@master 38 | with: 39 | name: Packages 40 | path: artifacts 41 | 42 | - name: "Push to NuGet.org" 43 | run: | 44 | dotnet nuget push "$GITHUB_WORKSPACE/artifacts/*.nupkg" --skip-duplicate -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json -------------------------------------------------------------------------------- /.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 | .DS_Store 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | [Aa][Rr][Mm]/ 26 | [Aa][Rr][Mm]64/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | 32 | # Visual Studio 2015/2017 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # Visual Studio 2017 auto generated files 38 | Generated\ Files/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # Benchmark Results 54 | BenchmarkDotNet.Artifacts/ 55 | 56 | # .NET Core 57 | project.lock.json 58 | project.fragment.lock.json 59 | artifacts/ 60 | 61 | # StyleCop 62 | StyleCopReport.xml 63 | 64 | # Files built by Visual Studio 65 | *_i.c 66 | *_p.c 67 | *_h.h 68 | *.ilk 69 | *.meta 70 | *.obj 71 | *.iobj 72 | *.pch 73 | *.pdb 74 | *.ipdb 75 | *.pgc 76 | *.pgd 77 | *.rsp 78 | *.sbr 79 | *.tlb 80 | *.tli 81 | *.tlh 82 | *.tmp 83 | *.tmp_proj 84 | *_wpftmp.csproj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.svclog 91 | *.scc 92 | 93 | # Chutzpah Test files 94 | _Chutzpah* 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opendb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | *.VC.db 105 | *.VC.VC.opendb 106 | 107 | # Visual Studio profiler 108 | *.psess 109 | *.vsp 110 | *.vspx 111 | *.sap 112 | 113 | # Visual Studio Trace Files 114 | *.e2e 115 | 116 | # TFS 2012 Local Workspace 117 | $tf/ 118 | 119 | # Guidance Automation Toolkit 120 | *.gpState 121 | 122 | # ReSharper is a .NET coding add-in 123 | _ReSharper*/ 124 | *.[Rr]e[Ss]harper 125 | *.DotSettings.user 126 | 127 | # JustCode is a .NET coding add-in 128 | .JustCode 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 | *.snupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !?*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | *- Backup*.rdl 262 | 263 | # Microsoft Fakes 264 | FakesAssemblies/ 265 | 266 | # GhostDoc plugin setting file 267 | *.GhostDoc.xml 268 | 269 | # Node.js Tools for Visual Studio 270 | .ntvs_analysis.dat 271 | node_modules/ 272 | 273 | # Visual Studio 6 build log 274 | *.plg 275 | 276 | # Visual Studio 6 workspace options file 277 | *.opt 278 | 279 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 280 | *.vbw 281 | 282 | # Visual Studio LightSwitch build output 283 | **/*.HTMLClient/GeneratedArtifacts 284 | **/*.DesktopClient/GeneratedArtifacts 285 | **/*.DesktopClient/ModelManifest.xml 286 | **/*.Server/GeneratedArtifacts 287 | **/*.Server/ModelManifest.xml 288 | _Pvt_Extensions 289 | 290 | # Paket dependency manager 291 | .paket/paket.exe 292 | paket-files/ 293 | 294 | # FAKE - F# Make 295 | .fake/ 296 | 297 | # JetBrains Rider 298 | .idea/ 299 | *.sln.iml 300 | 301 | # CodeRush personal settings 302 | .cr/personal 303 | 304 | # Python Tools for Visual Studio (PTVS) 305 | __pycache__/ 306 | *.pyc 307 | 308 | # Cake - Uncomment if you are using it 309 | # tools/** 310 | # !tools/packages.config 311 | 312 | # Tabs Studio 313 | *.tss 314 | 315 | # Telerik's JustMock configuration file 316 | *.jmconfig 317 | 318 | # BizTalk build output 319 | *.btp.cs 320 | *.btm.cs 321 | *.odx.cs 322 | *.xsd.cs 323 | 324 | # OpenCover UI analysis results 325 | OpenCover/ 326 | 327 | # Azure Stream Analytics local run output 328 | ASALocalRun/ 329 | 330 | # MSBuild Binary and Structured Log 331 | *.binlog 332 | 333 | # NVidia Nsight GPU debugger configuration file 334 | *.nvuser 335 | 336 | # MFractors (Xamarin productivity tool) working folder 337 | .mfractor/ 338 | 339 | # Local History for Visual Studio 340 | .localhistory/ 341 | 342 | # BeatPulse healthcheck temp database 343 | healthchecksdb 344 | 345 | # .NET Launch Profiles 346 | launchSettings.json -------------------------------------------------------------------------------- /Chell.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell", "src\Chell\Chell.csproj", "{6BE659EC-A00D-4148-B19D-B5478DE001FA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell.Run", "src\Chell.Run\Chell.Run.csproj", "{893D8C70-3C13-47D6-987F-C099C6161D7E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell.Tests", "tests\Chell.Tests\Chell.Tests.csproj", "{1FBAA8ED-438E-498C-AB1F-29429550DC21}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EBB92712-D878-48FD-8E31-E577AABDC0FB}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | .github\workflows\build.yaml = .github\workflows\build.yaml 16 | Directory.Build.props = Directory.Build.props 17 | README.md = README.md 18 | .github\workflows\release.yaml = .github\workflows\release.yaml 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AF454F92-8B84-446B-B0E2-9BA8887B09CC}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted.Basic.Windows", "samples\GettingStarted.Basic.Windows\GettingStarted.Basic.Windows.csproj", "{9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted.Basic.Unix", "samples\GettingStarted.Basic.Unix\GettingStarted.Basic.Unix.csproj", "{69DC1056-843E-4980-908A-5DB4ADA95460}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Debug|x64 = Debug|x64 31 | Debug|x86 = Debug|x86 32 | Release|Any CPU = Release|Any CPU 33 | Release|x64 = Release|x64 34 | Release|x86 = Release|x86 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x64.Build.0 = Debug|Any CPU 41 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x86.Build.0 = Debug|Any CPU 43 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x64.ActiveCfg = Release|Any CPU 46 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x64.Build.0 = Release|Any CPU 47 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x86.ActiveCfg = Release|Any CPU 48 | {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x86.Build.0 = Release|Any CPU 49 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x64.Build.0 = Debug|Any CPU 53 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x86.Build.0 = Debug|Any CPU 55 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x64.ActiveCfg = Release|Any CPU 58 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x64.Build.0 = Release|Any CPU 59 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x86.ActiveCfg = Release|Any CPU 60 | {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x86.Build.0 = Release|Any CPU 61 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x64.ActiveCfg = Debug|Any CPU 64 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x64.Build.0 = Debug|Any CPU 65 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x86.ActiveCfg = Debug|Any CPU 66 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x86.Build.0 = Debug|Any CPU 67 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x64.ActiveCfg = Release|Any CPU 70 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x64.Build.0 = Release|Any CPU 71 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x86.ActiveCfg = Release|Any CPU 72 | {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x86.Build.0 = Release|Any CPU 73 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x64.ActiveCfg = Debug|Any CPU 76 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x64.Build.0 = Debug|Any CPU 77 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x86.ActiveCfg = Debug|Any CPU 78 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x86.Build.0 = Debug|Any CPU 79 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x64.ActiveCfg = Release|Any CPU 82 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x64.Build.0 = Release|Any CPU 83 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x86.ActiveCfg = Release|Any CPU 84 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x86.Build.0 = Release|Any CPU 85 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x64.ActiveCfg = Debug|Any CPU 88 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x64.Build.0 = Debug|Any CPU 89 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x86.ActiveCfg = Debug|Any CPU 90 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x86.Build.0 = Debug|Any CPU 91 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|Any CPU.ActiveCfg = Release|Any CPU 92 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|Any CPU.Build.0 = Release|Any CPU 93 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x64.ActiveCfg = Release|Any CPU 94 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x64.Build.0 = Release|Any CPU 95 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x86.ActiveCfg = Release|Any CPU 96 | {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x86.Build.0 = Release|Any CPU 97 | EndGlobalSection 98 | GlobalSection(SolutionProperties) = preSolution 99 | HideSolutionNode = FALSE 100 | EndGlobalSection 101 | GlobalSection(NestedProjects) = preSolution 102 | {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5} = {AF454F92-8B84-446B-B0E2-9BA8887B09CC} 103 | {69DC1056-843E-4980-908A-5DB4ADA95460} = {AF454F92-8B84-446B-B0E2-9BA8887B09CC} 104 | EndGlobalSection 105 | GlobalSection(ExtensibilityGlobals) = postSolution 106 | SolutionGuid = {5B16EB0E-5342-4D93-9FA2-8327FF69A276} 107 | EndGlobalSection 108 | EndGlobal 109 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 1.0.0 5 | latest 6 | enable 7 | true 8 | $(NoWarn);1591;1587;1574 9 | 10 | 11 | Write scripts with the power of .NET. Provides a shell script-like (bash, cmd, ...) experience to .NET application. 12 | Mayuki Sawatari 13 | Copyright © Mayuki Sawatari 14 | https://github.com/mayuki/Chell 15 | https://github.com/mayuki/Chell 16 | 17 | CommandLine Shell Process 18 | MIT 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © Mayuki Sawatari 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chell 2 | Write scripts with the power of C# and .NET. 3 | 4 | Chell is a library and execution tool for providing a shell script-like (bash, cmd, ...) experience to .NET applications. 5 | 6 | ```csharp 7 | var branch = await Run($"git branch --show-current"); 8 | await Run($"git archive {branch} -o {branch}.zip"); 9 | ``` 10 | 11 | .NET applications are great for complex tasks, but executing processes can be boring. Chell brings the experience closer to shell scripting. This library is heavily influenced by [google/zx](https://github.com/google/zx). 12 | 13 | ## When should I use Chell? 14 | - **Write better shell scripts**: Write a complex script and use the power of .NET and C# 15 | - **Write multi-platform shell scripts**: As an alternative to scripts that work on multiple platforms 16 | - **Run a process quickly in your app**: As .NET library for easy handling of process launch and output 17 | - **All developers in the project are .NET developers**: 🙃 18 | 19 | Of course, if the shell script is already working fine and you don't have any problems, then there is no need to use Chell. 20 | 21 | ## Chell at a glance 22 | Using Chell makes the code feel more like a script by taking advantage of C# 9's top-level statements and C# 6's `using static`. 23 | 24 | ```csharp 25 | // Chell.Exports exposes a variety of functions and properties 26 | using Chell; 27 | using static Chell.Exports; 28 | ``` 29 | ```csharp 30 | // Move the current directory with Cd method 31 | Cd("/tmp"); 32 | 33 | // Dispose the return value of Cd method to return to the previous directory 34 | using (Cd("/usr/local/bin")) 35 | { 36 | // The current directory is "/usr/local/bin". 37 | } 38 | // The current directory is "/" again. 39 | ``` 40 | ```csharp 41 | // You can run the process by passing a string to Run method 42 | await Run($"ls -lFa"); 43 | ``` 44 | ```csharp 45 | // An interpolated string passed to Run method will be escaped and expanded if it is an array 46 | var newDirs = new [] { "foo", "bar", "my app", "your;app" }; 47 | await Run($"mkdir {newDirs}"); // $ mkdir foo bar "my app" "your;app" 48 | ``` 49 | ```csharp 50 | // Run method returns the result object of the command (ProcessOutput class) 51 | var result = await Run($"ls -lFa"); 52 | // You can read stdout & stderr line by line 53 | foreach (var line in result) 54 | { 55 | Echo(line); 56 | } 57 | 58 | // Allows to get stdout & stderr with implicit conversion to `string` 59 | string output = result; 60 | // You can also get stdout as bytes (ReadOnlyMemory) 61 | var binary = result.OutputBinary; 62 | ``` 63 | ```csharp 64 | // Provides convenient extension methods for parsing JSON. 65 | var images = await Run($"docker image ls --format {"{{json .}}"}").SuppressConsoleOutputs(); 66 | foreach (var image in images.AsJsonLines(new { Repository = "", ID = "", Tag = ""})) 67 | { 68 | Echo(image); 69 | } 70 | // $ docker image ls --format "{{json .}}" 71 | // { Repository = mcr.microsoft.com/dotnet/sdk, ID = b160c8f3dbd6, Tag = 5.0 } 72 | // { Repository = , ID = 3ee645b4a3bd, Tag = } 73 | ``` 74 | ```csharp 75 | // Standard input/output of process tasks can be connected by pipes 76 | await (Run($"ls -lFa") | Run($"grep dotnet")); 77 | // The difference with `await (Run($"ls -lFa | grep dotnet"));` is that the shell can pipe or not. 78 | 79 | // You can also specify a Stream as input or output 80 | // Write ffmpeg output to a Stream. 81 | await (Run($"ffmpeg ...") | destinationStream); 82 | // Write a Stream to ffmpeg process. 83 | await (srcStream | Run($"ffmpeg ...")); 84 | ``` 85 | 86 | Just want to make it easy for your app to handle processes? If you don't use `Chell.Exports`, you won't get any unnecessary methods or properties, and you'll get the same functions by `new Run(...)`. 87 | 88 | ```csharp 89 | using Chell; 90 | var result = await new Run($"ls -lF"); 91 | ``` 92 | 93 | Want to run it like a scripting language? Install [Chell.Run](#chellrun), and you can run it like a script. 94 | 95 | ```bash 96 | % dotnet tool install -g Chell.Run 97 | 98 | % chell -e "Echo(DateTime.Now)" 99 | 9/1/2021 0:00:00 PM 100 | 101 | % cat <<__EOF__ > MyScript.cs 102 | var dirs = new [] { "foo bar", "baz" }; 103 | await Run($"mkdir {dirs}"); 104 | await Run($"ls -l"); 105 | __EOF__ 106 | 107 | % chell MyScript.cs 108 | $ mkdir "foo bar" "baz" 109 | $ ls -l 110 | total 8 111 | drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 baz/ 112 | drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 'foo bar'/ 113 | ``` 114 | 115 | 116 | ## Features 117 | - Automatic shell character escaping and array expansion 118 | - Stream and Process Pipes 119 | - Provide utilities and shortcuts useful for scripting. 120 | - Simple shell script-like execution tools 121 | - Multi-platform (Windows, Linux, macOS) 122 | - LINQPad friendly 123 | 124 | ## Install 125 | ``` 126 | dotnet package add Chell 127 | ``` 128 | ### Requirements 129 | .NET Standard 2.1, .NET 5 or higher 130 | 131 | ## Chell.Exports 132 | Chell.Exports class exposes a variety of utilities and shortcuts to make writing feel like shell scripting. It is recommended to include this class in your scripts with `static using`. 133 | 134 | ### Methods (Functions) 135 | #### `Run` 136 | Starts a process using the specified command-line and returns a `ProcessTask`. 137 | 138 | ```csharp 139 | await Run($"ls -lF"); 140 | 141 | // The followings are equivalent to calling Run method 142 | await (Run)$"ls -lF"; 143 | await new Run($"ls -lF"); 144 | ``` 145 | 146 | The process will be launched asynchronously and can wait for completion by `await`. And you can `await` to get a `ProcessOutput` object with its output. 147 | 148 | If the exit code of the process returns non-zero, it will throw an exception. To suppress this exception, see `NoThrow`. 149 | 150 | An interpolated string passed to Run method will be escaped and expanded if it is an array. 151 | 152 | ```csharp 153 | var newDirs = new [] { "foo", "bar", "my app", "your;app" }; 154 | await Run($"mkdir {newDirs}"); // equivalent to `mkdir foo bar "my app" "your;app"` 155 | ``` 156 | 157 | You can also pass an execution options (`ProcessTaskOptions`) to Run method. 158 | 159 | ```csharp 160 | await Run($"ping -t localhost", new ProcessTaskOptions( 161 | workingDirectory: @"C:\Windows", 162 | timeout: TimeSpan.FromSeconds(1) 163 | )); 164 | ``` 165 | 166 | #### `Cd(string)` 167 | ```csharp 168 | Cd("/usr/local/bin"); // equivalent to `Environment.CurrentDirectory = "/usr/local/bin";` 169 | ``` 170 | 171 | Dispose the return value of `Cd` method to return to the previous directory. 172 | 173 | ```csharp 174 | Cd("/"); // The current directory is "/". 175 | using (Cd("/usr/local/bin")) 176 | { 177 | // The current directory is "/usr/local/bin". 178 | } 179 | // The current directory is "/" again. 180 | ``` 181 | 182 | #### `Mkdirp(string path)` 183 | Same as `mkdir -p`. Creates a new directory and any necessary sub-directories in the specified path. 184 | 185 | #### `Dump(T value)` 186 | Formats the object and write it to the console. 187 | 188 | ```csharp 189 | Dump(new { Foo = 123, Bar = "Baz" }); // => "{ Foo = 123, Bar = "Baz" }" 190 | ``` 191 | 192 | #### `Which(string name)`, `TryWhich(string name, out string path)` 193 | Returns a path of the specified command. 194 | 195 | ```csharp 196 | var dotnetPath = Which("dotnet"); 197 | await Run($"{dotnetPath} run"); 198 | ``` 199 | 200 | #### `Echo(object message = default)` 201 | `Echo` method is equivalent to Console.WriteLine. 202 | 203 | ```csharp 204 | Echo("Hello World!"); // equivalent to Console.WriteLine("Hello World!"); 205 | ``` 206 | 207 | #### `Sleep(int duration)`, `Sleep(TimeSpan duration)` 208 | Returns a Task that waits for the specified duration. 209 | 210 | ```csharp 211 | await Sleep(10); // Sleep for 10 seconds. 212 | ``` 213 | 214 | #### `Exit(int exitCode)` 215 | Terminates the application with an exit code. 216 | 217 | ```csharp 218 | Exit(1); 219 | ``` 220 | 221 | ### Properties 222 | 223 | #### `Env.Vars` 224 | 225 | Exposes the environment variables as `IDictionary`. 226 | 227 | ```csharp 228 | Env.Vars["PATH"] = Env.Vars["PATH"] + ":/path/to/"; 229 | ``` 230 | 231 | #### `Env.IsWindows` 232 | Returns whether the running operating system is Windows or not. If it returns `false`, the operating system is Linux or macOS. 233 | 234 | ```csharp 235 | if (Env.IsWindows) { /* Something to do for Windows */ } 236 | ``` 237 | 238 | #### `Env.Shell` 239 | Specify explicitly which shell to use, or set to not use a shell. 240 | 241 | ```csharp 242 | Env.Shell.UseBash(); 243 | Env.Shell.NoUseShell(); 244 | Env.Shell.UseCmd(); 245 | ``` 246 | 247 | #### `Env.Verbosity` 248 | Sets or gets the output level when executing a command/process. 249 | 250 | - `Verbosity.All`: Displays both the command line and the output of the command 251 | - `Verbosity.CommandLine`: Displays the command line 252 | - `Verbosity.Output`: Displays the output of the command 253 | - `Verbosity.Silent`: No display 254 | 255 | #### `Env.ProcessTimeout` 256 | 257 | Sets the timeout for running the process. The default value is `0` (disabled). 258 | 259 | ```csharp 260 | Env.ProcessTimeout = TimeSpan.FromSecond(1); 261 | 262 | // OperationCanceledException will be thrown after 1s. 263 | await Run($"ping -t localhost"); 264 | ``` 265 | 266 | #### `Arguments` 267 | Gets the arguments passed to the current application. 268 | 269 | ```csharp 270 | // $ myapp foo bar baz => new [] { "foo", "bar", "baz" }; 271 | foreach (var arg in Arguments) { /* ... */ } 272 | ``` 273 | #### `CurrentDirectory`, `ExecutableDirectory`, `ExecutableName`, `ExecutablePath` 274 | Gets the current directory and the application directory or name or path. 275 | 276 | ```csharp 277 | // C:\> cd C:\Users\Alice 278 | // C:\Users\Alice> Downloads\MyApp.exe 279 | 280 | Echo(CurrentDirectory); // C:\Users\Alice 281 | Echo(ExecutableDirectory); // C:\Users\Alice\Downloads 282 | Echo(ExecutableName); // MyApp.exe 283 | Echo(ExecutablePath); // C:\Users\Alice\Downloads\MyApp.exe 284 | ``` 285 | 286 | #### `HomeDirectory` 287 | Gets the path of the current user's home directory. 288 | 289 | ```csharp 290 | // Windows: C:/Users/ 291 | // Linux: /home/ 292 | // macOS: /Users/ 293 | Echo(HomeDirectory); 294 | ``` 295 | 296 | #### `StdIn`, `StdOut`, `StdErr` 297 | Provides the wrapper with methods useful for reading and writing to the standard input/output/error streams. 298 | 299 | ```csharp 300 | // Reads data from standard input. 301 | await StdIn.ReadToEndAsync(); 302 | 303 | // Writes data to standard output or error. 304 | StdOut.WriteLine("FooBar"); 305 | StdErr.WriteLine("Oops!"); 306 | ``` 307 | 308 | ## ProcessTask class 309 | Represents the execution task of the process started by `Run`. 310 | 311 | 312 | ### `Pipe` 313 | Connects the standard output of the process to another `ProcessTask` or `Stream`. 314 | 315 | ```csharp 316 | await (Run($"ls -lF") | Run($"grep .dll")); 317 | 318 | // The followings are equivalent to using '|'. 319 | var procTask1 = Run($"ls -lF"); 320 | var procTask2 = Run($"grep .dll"); 321 | procTask1.Pipe(procTask2); 322 | ``` 323 | 324 | A `Stream` can also be passed to Pipe. If the ProcessTask has connected to the `Stream`, it will not write to `ProcessOutput`. 325 | 326 | ```csharp 327 | var memStream = new MemoryStream(); 328 | await Run($"ls -lF").Pipe(memStream); 329 | ``` 330 | 331 | ### `ConnectStreamToStandardInput` 332 | Connects the Stream to the standard input of the process. The method can be called only once before the process starts. 333 | 334 | ```csharp 335 | await (myStream | Run($"grep .dll")); 336 | 337 | // The followings are equivalent to using '|'. 338 | var procTask = Run($"grep .dll"); 339 | procTask.ConnectStreamToStandardInput(myStream); 340 | ``` 341 | 342 | ### `NoThrow` 343 | Suppresses exception throwing when the exit code is non-zero. 344 | 345 | ```csharp 346 | await Run($"AppReturnsExitCodeNonZero").NoThrow(); 347 | ``` 348 | 349 | ### `SuppressConsoleOutputs` 350 | Suppresses the writing of command execution results to the standard output. 351 | 352 | ```csharp 353 | // equivalent to "Env.Verbosity = Verbosity.Silent" or pipe to null. 354 | await Run($"ls -lF").SuppressConsoleOutputs(); 355 | 356 | ``` 357 | ### `ExitCode` 358 | Returns a `Task` to get the exit code of the process. This is equivalent to waiting for a `ProcessTask` with `NoThrow`. 359 | 360 | ```csharp 361 | var proc = Run($"ls -lF"); 362 | if (await proc.ExitCode != 0) 363 | { 364 | ... 365 | } 366 | 367 | // equivalent to `(await Run($"ls -lF").NoThrow()).ExitCode` 368 | ``` 369 | 370 | ## ProcessOutput class 371 | Provides the results of the process execution. 372 | 373 | ### `Combined`, `CombinedBinary` 374 | Gets the combined standard output and error as a string or byte array. 375 | 376 | ### `Output`, `OutputBinary` 377 | Gets the standard output as a string or byte array. 378 | 379 | ### `Error`, `ErrorBinary` 380 | Gets the standard error as a string or byte array. 381 | 382 | ### `AsLines(bool trimEnd = false)`, `GetEnumerator()` 383 | Gets the combined standard output and error as a per-line `IEnumerable`. 384 | 385 | ```csharp 386 | // equivalent to `foreach (var line in procOutput.AsLines())` 387 | foreach (var line in procOutput) { ... } 388 | ``` 389 | 390 | ### `ToString()` 391 | The method equivalent to `Combined` property. 392 | 393 | ### `ExitCode` 394 | Gets the exit code of the process. 395 | 396 | ## Utilities and shortcuts 397 | Chell.Exports class also exposes a variety of useful utilities and shortcuts to libraries. 398 | 399 | ### `Prompt` 400 | Prompts the user for input and gets it. 401 | 402 | ```csharp 403 | var name = await Prompt("What's your name? "); 404 | ``` 405 | 406 | ### `Chalk`: Kokuban: Terminal string styling 407 | Provides a shortcut to [mayuki/Kokuban](https://github.com/mayuki/Kokuban). You can easily style the text on the terminal. 408 | 409 | ```csharp 410 | // "Error: " will be colored. 411 | Echo((Chalk.Red + "Error: ") + "Something went wrong."); 412 | ``` 413 | 414 | ### `Glob` 415 | Provides a shortcut to `Microsoft.Extensions.FileSystemGlobbing`. 416 | 417 | - `Glob(params string[] patterns)` 418 | - `Glob(string baseDir, string[] patterns)` 419 | 420 | ```csharp 421 | // Glob patterns starting with '!' will be treated as excludes. 422 | foreach (var path in Glob("**/*.cs", "!**/*.vb")) 423 | { 424 | ... 425 | } 426 | ``` 427 | 428 | ### JSON serialize/deserialize (System.Text.Json) 429 | Provides shortcuts to `System.Text.Json`. 430 | 431 | - `ToJson(T obj)` 432 | ```csharp 433 | var obj = new { Name = "Alice", Age = 18 }; 434 | var json = ToJson(obj); 435 | Echo(json); // {"Name":"Alice","Age":18} 436 | ``` 437 | 438 | - `FromJson(string json)` 439 | - `FromJson(string json, T shape)` 440 | ```csharp 441 | var json = "{ \"foo\": 123 }"; 442 | var obj = FromJson(json, new { Foo = 0 }); 443 | Dump(obj); // { Foo = 123 } 444 | ``` 445 | 446 | - `AsJson` 447 | - `AsJsonLines` 448 | ```csharp 449 | using Chell; 450 | var output = await Run($"docker image ls --format {"{{json .}}"}"); 451 | foreach (var image in output.AsJsonLines(new { Repository = "", ID = "", Tag = ""})) 452 | { 453 | // ... 454 | } 455 | ``` 456 | ```csharp 457 | using Chell; 458 | var output = await Run($"kubectl version --client -o json"); 459 | var obj = output.AsJson(new { clientVersion = new { major = "", minor = "", gitVersion = "" } }); 460 | Echo(obj); // { clientVersion = { major = 1, minor = 21, gitVersion = v1.21.2 } } 461 | ``` 462 | 463 | ### HTTP acccess (System.Net.Http) 464 | Provides shortcuts to `System.Net.Http.HttpClient`. 465 | 466 | - `FetchAsync` 467 | - `FetchByteArrayAsync` 468 | - `FetchStreamAsync` 469 | - `FetchStringAsync` 470 | 471 | ## Chell as a Library 472 | Chell can also be used as a utility library to run processes. 473 | 474 | If you don't use `Chell.Exports`, you won't get any unnecessary methods or properties, and you can use `Run` and `ChellEnvironment`, `Exports` class. 475 | 476 | ```csharp 477 | using Chell; 478 | 479 | var results = await new Run($"ls -lF"); 480 | 481 | // ChellEnvironment.Current is equivalent to `Env` on `Chell.Exports`. 482 | Console.WriteLine(ChellEnvironment.Current.ExecutablePath); 483 | Console.WriteLine(ChellEnvironment.Current.ExecutableName); 484 | Console.WriteLine(ChellEnvironment.Current.Arguments); 485 | Console.WriteLine(ChellEnvironment.Current.Vars["PATH"]); 486 | ``` 487 | 488 | ## Chell.Run 489 | Chell.Run executes the input source code in an environment where Chell and some libraries are available. 490 | 491 | It does not perform any NuGet package resolution, so we recommend creating a typical C# project if you need to handle such complexities. 492 | 493 | ``` 494 | $ dotnet tool install -g Chell.Run 495 | ``` 496 | ```bash 497 | $ chell -e "Echo(123);" 498 | $ chell < 540 | 541 | Permission is hereby granted, free of charge, to any person obtaining a copy 542 | of this software and associated documentation files (the "Software"), to deal 543 | in the Software without restriction, including without limitation the rights 544 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 545 | copies of the Software, and to permit persons to whom the Software is 546 | furnished to do so, subject to the following conditions: 547 | 548 | The above copyright notice and this permission notice shall be included in all 549 | copies or substantial portions of the Software. 550 | 551 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 552 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 553 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 554 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 555 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 556 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 557 | SOFTWARE. 558 | ``` 559 | -------------------------------------------------------------------------------- /samples/GettingStarted.Basic.Unix/GettingStarted.Basic.Unix.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/GettingStarted.Basic.Unix/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using static Chell.Exports; 4 | 5 | // Starts a process. 6 | // The array will be expanded and the elements will be escaped 7 | var dirs = new[] { "/", "/usr", "/bin" }; 8 | var results = await Run($"ls -l {dirs}"); 9 | 10 | // Enumerates the results to retrieve the standard output line by line. 11 | foreach (var line in results) 12 | { 13 | Echo($"Result> {line}"); 14 | } 15 | Echo(); 16 | 17 | // Built-in Variables 18 | Echo((Chalk.Green + "ExecutableName: ") + string.Join(' ', ExecutableName)); 19 | Echo((Chalk.Green + "ExecutableDirectory: ") + string.Join(' ', ExecutableDirectory)); 20 | Echo((Chalk.Green + "Arguments: ") + string.Join(' ', Arguments)); 21 | Echo((Chalk.Green + "CurrentDirectory: ") + string.Join(' ', CurrentDirectory)); 22 | Echo(); 23 | 24 | // Environment Variables 25 | Echo((Chalk.Green + "Env.Vars[\"PATH\"]: ") + Env.Vars["PATH"]); 26 | Echo(); 27 | 28 | // Standard Input/Error as Stream + Utility methods. 29 | StdOut.WriteLine("Hello World!"); 30 | StdErr.WriteLine("Hello World! (Error)"); 31 | Echo(); 32 | 33 | // Get the data from network and pipe it to the process 34 | await (await FetchByteArrayAsync("http://www.example.com/") | Run("grep title")); 35 | 36 | 37 | // Temporarily change the current directory. 38 | using (Cd("/")) 39 | { 40 | await Run($"dir"); 41 | } 42 | 43 | Exit(1); 44 | -------------------------------------------------------------------------------- /samples/GettingStarted.Basic.Windows/GettingStarted.Basic.Windows.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/GettingStarted.Basic.Windows/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Chell; 4 | using static Chell.Exports; 5 | 6 | // Starts a process. 7 | // The array will be expanded and the elements will be escaped 8 | var dirs = new[] { @"C:\Windows\Microsoft.NET", @"C:\Program Files" }; 9 | var results = await Run($"dir {dirs}"); // 10 | 11 | // Enumerates the results to retrieve the standard output line by line. 12 | foreach (var line in results.Where(x => x.Contains("Windows"))) 13 | { 14 | Echo($"Result> {line}"); 15 | } 16 | Echo(); 17 | 18 | // Built-in Variables 19 | Echo((Chalk.Green + "ExecutableName: ") + string.Join(' ', ExecutableName)); 20 | Echo((Chalk.Green + "ExecutableDirectory: ") + string.Join(' ', ExecutableDirectory)); 21 | Echo((Chalk.Green + "Arguments: ") + string.Join(' ', Arguments)); 22 | Echo((Chalk.Green + "CurrentDirectory: ") + string.Join(' ', CurrentDirectory)); 23 | Echo(); 24 | 25 | // Environment Variables 26 | Echo((Chalk.Green + "Env.Vars[\"PATH\"]: ") + Env.Vars["PATH"]); 27 | Echo(); 28 | 29 | // Standard Input/Error as Stream + Utility methods. 30 | StdOut.WriteLine("Hello World!"); 31 | StdErr.WriteLine("Hello World! (Error)"); 32 | Echo(); 33 | 34 | // Get the data from network and pipe it to the process 35 | await (await FetchByteArrayAsync("http://www.example.com/") | Run("findstr title")); 36 | 37 | // Temporarily change the current directory. 38 | using (Cd("C:\\Users")) 39 | { 40 | await Run($"dir"); 41 | } 42 | 43 | Exit(1); 44 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # C# Nullability # 3 | ############################### 4 | [*.cs] 5 | # CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. 6 | dotnet_diagnostic.CS8618.severity = error 7 | # CS8604: Possible null reference argument. 8 | dotnet_diagnostic.CS8604.severity = error 9 | # CS8629: Nullable value type may be null. 10 | dotnet_diagnostic.CS8629.severity = error 11 | # CS8600: Converting null literal or possible null value to non-nullable type. 12 | dotnet_diagnostic.CS8600.severity = error 13 | # CS8603: Possible null reference return. 14 | dotnet_diagnostic.CS8603.severity = error 15 | # CS8610: Nullability of reference types in type of parameter doesn't match overridden member. 16 | dotnet_diagnostic.CS8610.severity = error 17 | # CS8625: Cannot convert null literal to non-nullable reference type. 18 | dotnet_diagnostic.CS8625.severity = error 19 | # CS8606: Possible null reference assignment to iteration variable 20 | dotnet_diagnostic.CS8606.severity = error 21 | # CS8602: Dereference of a possibly null reference. 22 | dotnet_diagnostic.CS8602.severity = error 23 | # CS8601: Possible null reference assignment. 24 | dotnet_diagnostic.CS8601.severity = error 25 | # CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member. 26 | dotnet_diagnostic.CS8614.severity = error -------------------------------------------------------------------------------- /src/Chell.Run/Chell.Run.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1;net5.0 6 | 7 | Tool to run C# code like a script. Provides a shell script-like (bash, cmd, ...) experience to .NET application. 8 | true 9 | chell 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Chell.Run/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Cocona; 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.CodeAnalysis.CSharp.Scripting; 12 | using Microsoft.CodeAnalysis.Scripting; 13 | 14 | namespace Chell.Run 15 | { 16 | partial class Program 17 | { 18 | static Task Main(string[] args) 19 | => CoconaLiteApp.RunAsync(args); 20 | 21 | public class RunCommandParameterSet : ICommandParameterSet 22 | { 23 | [Option("ref", new[] { 'r' }, Description = "Additional reference assembly")] 24 | [HasDefaultValue] 25 | public string[]? References { get; set; } = null; 26 | 27 | [Option("using", new[] { 'u' }, Description = "Additional `using` namespace")] 28 | [HasDefaultValue] 29 | public string[]? Usings { get; set; } = null; 30 | 31 | [Option('q')] 32 | [HasDefaultValue] 33 | public bool Silent { get; set; } = false; 34 | } 35 | 36 | [IgnoreUnknownOptions] 37 | [Command(Description = "Chell.Run: Run C# script instantly.")] 38 | public async Task RunAsync( 39 | RunCommandParameterSet runParams, 40 | [Option('e', Description = "A one-line program that can be run instantly.")] string? eval = default, 41 | [Argument(Description = "The path to a script file, or arguments to pass to the script")] string[]? filenameOrArgs = default 42 | ) 43 | { 44 | var fileName = filenameOrArgs is {Length: > 0} ? filenameOrArgs[0] : null; 45 | 46 | // -e ".." or --eval "..." 47 | if (!string.IsNullOrEmpty(eval)) 48 | { 49 | var args = Environment.GetCommandLineArgs(); 50 | var index = Array.FindIndex(args, x => x == "-e" || x == "--eval"); 51 | args = args.Skip(index + 2).ToArray(); 52 | await RunScriptAsync("", Environment.CurrentDirectory, eval, args, runParams); 53 | } 54 | // Read a script from stdin. 55 | else if (fileName == "-" || (string.IsNullOrWhiteSpace(fileName) && Console.IsInputRedirected)) 56 | { 57 | // Pass the strings as arguments after '-'. 58 | var args = Array.Empty(); 59 | if (fileName == "-") 60 | { 61 | args = Environment.GetCommandLineArgs(); 62 | var index = Array.IndexOf(args, "-"); 63 | args = args.Skip(index + 1).ToArray(); 64 | } 65 | 66 | using var reader = new StreamReader(Console.OpenStandardInput()); 67 | var code = await reader.ReadToEndAsync(); 68 | await RunScriptAsync("", Environment.CurrentDirectory, code, args, runParams); 69 | } 70 | else 71 | { 72 | if (string.IsNullOrWhiteSpace(fileName)) 73 | { 74 | throw new CommandExitedException("Error: Specify the path or pass the script from standard input.", -1); 75 | } 76 | if (!File.Exists(fileName)) 77 | { 78 | throw new CommandExitedException("Error: No such file or directory.", -1); 79 | } 80 | 81 | var ext = Path.GetExtension(fileName); 82 | if (ext == ".cs") 83 | { 84 | // Run .cs script file. 85 | var fullPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, fileName)); 86 | 87 | var args = filenameOrArgs?.Skip(1).ToArray() ?? Array.Empty(); 88 | await RunScriptAsync(fullPath, Path.GetDirectoryName(fullPath) ?? Environment.CurrentDirectory, await File.ReadAllTextAsync(fileName, Encoding.UTF8), args, runParams); 89 | } 90 | else 91 | { 92 | throw new CommandExitedException("Error: The specified file has unknown extension. Chell accepts a filename with `.cs` extension.", -1); 93 | } 94 | } 95 | } 96 | 97 | private async Task RunScriptAsync(string fileName, string executableDirectory, string content, string[] args, RunCommandParameterSet runParams) 98 | { 99 | _ = typeof(System.Text.Json.JsonSerializer).Assembly; 100 | _ = typeof(Chell.ChellEnvironment).Assembly; 101 | _ = typeof(Cocona.CoconaLiteApp).Assembly; 102 | _ = typeof(Sharprompt.Prompt).Assembly; 103 | _ = typeof(Mono.Options.Command).Assembly; 104 | 105 | var references = AppDomain.CurrentDomain.GetAssemblies() 106 | .Distinct() 107 | .GroupBy(x => x) 108 | .Select(x => x.Last()) 109 | .Select(x => MetadataReference.CreateFromFile(x.Location)); 110 | var usings = new[] 111 | { 112 | "System", 113 | "System.Collections", 114 | "System.Collections.Generic", 115 | "System.Diagnostics", 116 | "System.IO", 117 | "System.Text", 118 | "System.Text.RegularExpressions", 119 | "System.Linq", 120 | "System.Threading", 121 | "System.Threading.Tasks", 122 | "Chell", 123 | "Chell.Extensions", 124 | 125 | // using static 126 | "Chell.Exports" 127 | }.AsEnumerable(); 128 | 129 | var scriptOptions = ScriptOptions.Default 130 | .AddImports(usings.Concat(runParams.Usings ?? Array.Empty())) 131 | .AddReferences(references) 132 | .AddReferences(runParams.References ?? Array.Empty()); 133 | 134 | try 135 | { 136 | if (runParams.Silent) 137 | { 138 | ChellEnvironment.Current.Verbosity = ChellVerbosity.Silent; 139 | } 140 | 141 | ChellEnvironment.Current.SetCommandLineArgs(fileName, Path.GetFileName(fileName), executableDirectory, args); 142 | var script = await CSharpScript.RunAsync(content, scriptOptions); 143 | } 144 | catch (CompilationErrorException e) 145 | { 146 | Console.Error.WriteLine($"{fileName}{e.Message}"); 147 | } 148 | catch (ProcessTaskException e) 149 | { 150 | Console.Error.WriteLine(e.Message); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Chell/Chell.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;netcoreapp3.1;net5.0 5 | latest 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | TextTemplatingFileGenerator 23 | ProcessTaskExtensions.Generated.cs 24 | 25 | 26 | TextTemplatingFileGenerator 27 | ChellWritableStream.Generated.cs 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | True 38 | True 39 | ProcessTaskExtensions.tt 40 | 41 | 42 | True 43 | True 44 | ChellWritableStream.tt 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Chell/ChellEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.IO.Pipes; 7 | using System.Linq; 8 | using System.Runtime.InteropServices; 9 | using System.Threading; 10 | using Chell.Internal; 11 | using Chell.IO; 12 | using Chell.Shell; 13 | 14 | namespace Chell 15 | { 16 | [Flags] 17 | public enum ChellVerbosity 18 | { 19 | /// 20 | /// By default, no output is written to the console. 21 | /// 22 | Silent = 0, 23 | 24 | /// 25 | /// Writes a executing command line to the console. 26 | /// 27 | CommandLine = 1 << 0, 28 | 29 | /// 30 | /// Writes a command output to the console. 31 | /// 32 | ConsoleOutputs = 1 << 1, 33 | 34 | /// 35 | /// Writes all command lines and command outputs. 36 | /// 37 | Full = CommandLine | ConsoleOutputs, 38 | 39 | [EditorBrowsable(EditorBrowsableState.Never)] 40 | Debug = Full | 1 << 31, 41 | } 42 | 43 | public class ChellEnvironment 44 | { 45 | public static ChellEnvironment Current { get; set; } = new ChellEnvironment(); 46 | 47 | private string[] _arguments; 48 | private string? _executablePath; 49 | private string _executableName; 50 | private string _executableDirectory; 51 | 52 | public ChellEnvironment() 53 | { 54 | var args = Environment.GetCommandLineArgs(); 55 | var path = args[0]; 56 | _arguments = args.Skip(1).ToArray(); 57 | _executablePath = path; 58 | _executableName = Path.GetFileName(path); 59 | _executableDirectory = Path.GetDirectoryName(path)!; 60 | } 61 | 62 | /// 63 | /// Gets or sets the verbosity. 64 | /// 65 | public ChellVerbosity Verbosity { get; set; } = ChellVerbosity.Full; 66 | 67 | public ShellExecutorProvider Shell { get; } = new ShellExecutorProvider(); 68 | public IConsoleProvider Console { get; set; } = 69 | LINQPadHelper.RunningOnLINQPad 70 | ? new LINQPadConsoleProvider() 71 | : SystemConsoleProvider.Instance; 72 | 73 | /// 74 | /// Gets the identifier for the current application process. 75 | /// 76 | public int ProcessId => 77 | #if NET5_0_OR_GREATER 78 | Environment.ProcessId 79 | #else 80 | Process.GetCurrentProcess().Id 81 | #endif 82 | ; 83 | 84 | /// 85 | /// Gets whether the current application is running on Windows. 86 | /// 87 | public bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 88 | 89 | /// 90 | /// Gets the command line arguments. like args of a entry point. 91 | /// 92 | public IReadOnlyList Arguments => _arguments; 93 | 94 | /// 95 | /// Gets the path of the executing application. like argv[0]. (e.g. C:\\Path\To\App.exe, /path/to/app) 96 | /// 97 | /// 98 | /// The path may be null when running a inline script. 99 | /// 100 | public string? ExecutablePath => _executablePath; 101 | 102 | /// 103 | /// Gets the name of the executing application. like argv[0]. (e.g. App.exe, app) 104 | /// 105 | public string ExecutableName => _executableName; 106 | 107 | /// 108 | /// Gets the directory of the executing application. like argv[0]. (e.g. C:\\Path\To, /path/to) 109 | /// 110 | public string ExecutableDirectory => _executableDirectory; 111 | 112 | /// 113 | /// Gets or sets the path of the current working directory. 114 | /// 115 | public string CurrentDirectory 116 | { 117 | get => Environment.CurrentDirectory; 118 | set => Environment.CurrentDirectory = value; 119 | } 120 | 121 | /// 122 | /// Gets the path of the current user's home directory. 123 | /// 124 | public string HomeDirectory => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); 125 | 126 | /// 127 | /// Gets the environment variables as representation. 128 | /// 129 | public IDictionary Vars { get; } = new EnvironmentVariables(); 130 | 131 | /// 132 | /// Gets the standard input stream. 133 | /// 134 | public ChellReadableStream StdIn => new ChellReadableStream(this.Console.OpenStandardInput(), this.Console.InputEncoding); 135 | 136 | /// 137 | /// Gets the standard output stream. 138 | /// 139 | public ChellWritableStream StdOut => new ChellWritableStream(this.Console.OpenStandardOutput(), this.Console.OutputEncoding); 140 | 141 | /// 142 | /// Gets the standard output stream. 143 | /// 144 | public ChellWritableStream StdErr => new ChellWritableStream(this.Console.OpenStandardError(), this.Console.OutputEncoding); 145 | 146 | /// 147 | /// Gets or sets the default timeout for the process. The value affects the current application. The default value is . 148 | /// 149 | /// 150 | /// If the value is or , the process will not be timed out. 151 | /// 152 | public TimeSpan ProcessTimeout { get; set; } = TimeSpan.Zero; 153 | 154 | [EditorBrowsable(EditorBrowsableState.Never)] 155 | public void SetCommandLineArgs(string? executablePath, string executableName, string executableDirectory, string[] args) 156 | { 157 | _arguments = args.ToArray(); 158 | _executableName = executableName; 159 | _executablePath = executablePath; 160 | _executableDirectory = executableDirectory; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Chell/CommandLineString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | 5 | namespace Chell 6 | { 7 | /// 8 | /// Workaround for string/FormattableString overload issues 9 | /// 10 | [EditorBrowsable(EditorBrowsableState.Never)] 11 | [DebuggerDisplay("CommandLineString: String={StringValue,nq}; FormattableString={FormattableStringValue,nq}")] 12 | public readonly struct CommandLineString 13 | { 14 | public string? StringValue { get; } 15 | public FormattableString? FormattableStringValue { get; } 16 | 17 | public CommandLineString(string value) 18 | { 19 | StringValue = value; 20 | FormattableStringValue = null; 21 | } 22 | 23 | public CommandLineString(FormattableString value) 24 | { 25 | StringValue = null; 26 | FormattableStringValue = value; 27 | } 28 | 29 | public static implicit operator CommandLineString(string value) 30 | { 31 | return new CommandLineString(value); 32 | } 33 | 34 | public static implicit operator CommandLineString(FormattableString value) 35 | { 36 | return new CommandLineString(value); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Chell/Exports.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.FileSystemGlobbing; 2 | using Microsoft.Extensions.FileSystemGlobbing.Abstractions; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Text.Json; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Chell.Internal; 12 | using Chell.IO; 13 | using Kokuban; 14 | using Kokuban.AnsiEscape; 15 | 16 | namespace Chell 17 | { 18 | public static class Exports 19 | { 20 | public static class Verbosity 21 | { 22 | /// 23 | /// By default, no output is written to the console. 24 | /// 25 | public const ChellVerbosity Silent = ChellVerbosity.Silent; 26 | 27 | /// 28 | /// Writes a executing command line to the console. 29 | /// 30 | public const ChellVerbosity CommandLine = ChellVerbosity.CommandLine; 31 | 32 | /// 33 | /// Writes a command output to the console. 34 | /// 35 | public const ChellVerbosity ConsoleOutputs = ChellVerbosity.ConsoleOutputs; 36 | 37 | /// 38 | /// Writes all command lines and command outputs. 39 | /// 40 | public const ChellVerbosity Full = ChellVerbosity.Full; 41 | } 42 | 43 | /// 44 | /// Gets the current environment. 45 | /// 46 | public static ChellEnvironment Env => ChellEnvironment.Current; 47 | 48 | /// 49 | /// Gets the standard input stream. 50 | /// 51 | public static ChellReadableStream StdIn => Env.StdIn; 52 | 53 | /// 54 | /// Gets the standard output stream. 55 | /// 56 | public static ChellWritableStream StdOut => Env.StdOut; 57 | 58 | /// 59 | /// Gets the standard error stream. 60 | /// 61 | public static ChellWritableStream StdErr => Env.StdErr; 62 | 63 | /// 64 | /// Gets the command line arguments. like args of a entry point. 65 | /// 66 | public static IReadOnlyList Arguments => Env.Arguments; 67 | 68 | /// 69 | /// Gets the path of the executing application. like argv[0]. (e.g. C:\\Path\To\App.exe, /path/to/app) 70 | /// 71 | /// 72 | /// The path may be null when running a inline script. 73 | /// 74 | public static string? ExecutablePath => Env.ExecutablePath; 75 | 76 | /// 77 | /// Gets the name of the executing application. like argv[0]. (e.g. App.exe, app) 78 | /// 79 | public static string ExecutableName => Env.ExecutableName; 80 | 81 | /// 82 | /// Gets the directory of the executing application. like argv[0]. (e.g. C:\\Path\To, /path/to) 83 | /// 84 | public static string ExecutableDirectory => Env.ExecutableDirectory; 85 | 86 | /// 87 | /// Gets or sets the path of the current working directory. 88 | /// 89 | public static string CurrentDirectory 90 | { 91 | get => Env.CurrentDirectory; 92 | set => Env.CurrentDirectory = value; 93 | } 94 | 95 | /// 96 | /// Gets the identifier for the current application process. 97 | /// 98 | public static int ProcessId => Env.ProcessId; 99 | 100 | /// 101 | /// Gets the Kokuban ANSI style builder to decorate texts. 102 | /// 103 | public static AnsiStyle Chalk => Kokuban.Chalk.Create(KokubanOptions.Default); 104 | 105 | /// 106 | /// Starts the process task with the specified command line. 107 | /// 108 | /// 109 | /// 110 | /// 111 | public static ProcessTask Run(FormattableString commandLine, ProcessTaskOptions? options = default) 112 | => new ProcessTask(commandLine, options); 113 | 114 | /// 115 | /// Starts the process task with the specified command line. 116 | /// 117 | /// 118 | /// 119 | /// 120 | public static ProcessTask Run(CommandLineString commandLine, ProcessTaskOptions? options = default) 121 | => new ProcessTask(commandLine, options); 122 | 123 | /// 124 | /// Starts the process task with the specified command line. 125 | /// 126 | /// The data to be passed to the standard input of the process. 127 | /// 128 | /// 129 | /// 130 | public static ProcessTask Run(Stream inputStream, FormattableString commandLine, ProcessTaskOptions? options = default) 131 | => new ProcessTask(inputStream, commandLine, options); 132 | 133 | /// 134 | /// Starts the process task with the specified command line. 135 | /// 136 | /// The data to be passed to the standard input of the process. 137 | /// 138 | /// 139 | /// 140 | public static ProcessTask Run(Stream inputStream, CommandLineString commandLine, ProcessTaskOptions? options = default) 141 | => new ProcessTask(inputStream, commandLine, options); 142 | 143 | /// 144 | /// Starts the process task with the specified command line. 145 | /// 146 | /// The data to be passed to the standard input of the process. 147 | /// 148 | /// 149 | /// 150 | public static ProcessTask Run(ReadOnlyMemory inputData, FormattableString commandLine, ProcessTaskOptions? options = default) 151 | => new ProcessTask(inputData, commandLine, options); 152 | 153 | /// 154 | /// Starts the process task with the specified command line. 155 | /// 156 | /// The data to be passed to the standard input of the process. 157 | /// 158 | /// 159 | /// 160 | public static ProcessTask Run(ReadOnlyMemory inputData, CommandLineString commandLine, ProcessTaskOptions? options = default) 161 | => new ProcessTask(inputData, commandLine, options); 162 | 163 | /// 164 | /// Writes the message to the console. 165 | /// 166 | /// 167 | public static void Echo(object? message = default) 168 | => ChellEnvironment.Current.Console.Out.WriteLine(message); 169 | 170 | /// 171 | /// Writes the object details to the console. 172 | /// 173 | /// 174 | /// 175 | public static void Dump(T obj) 176 | => ObjectDumper.Dump(obj); 177 | 178 | /// 179 | /// Converts the object to a JSON. 180 | /// 181 | /// 182 | public static string ToJson(T obj, JsonSerializerOptions? options = default) 183 | => JsonSerializer.Serialize(obj, options); 184 | 185 | /// 186 | /// Converts the JSON to an object. 187 | /// 188 | /// 189 | public static T? FromJson(string json, T shape) 190 | => FromJson(json); 191 | 192 | /// 193 | /// Converts the JSON to an object. 194 | /// 195 | /// 196 | public static T? FromJson(string json) 197 | => Chell.Extensions.StringExtensions.AsJson(json); 198 | 199 | /// 200 | /// Changes the current directory to the specified path. 201 | /// 202 | /// 203 | /// Dispose the return value to return to the previous directory. 204 | /// 205 | /// 206 | public static IDisposable Cd(string path) 207 | => new ChangeDirectoryScope(path); 208 | 209 | private class ChangeDirectoryScope : IDisposable 210 | { 211 | private readonly string _previousCurrentDirectory; 212 | 213 | public ChangeDirectoryScope(string newCurrentDirectory) 214 | { 215 | _previousCurrentDirectory = Environment.CurrentDirectory; 216 | ChangeDirectory(newCurrentDirectory); 217 | } 218 | 219 | public void Dispose() 220 | { 221 | ChangeDirectory(_previousCurrentDirectory); 222 | } 223 | 224 | private void ChangeDirectory(string path) 225 | { 226 | CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"cd {path}"); 227 | Environment.CurrentDirectory = path; 228 | } 229 | } 230 | 231 | /// 232 | /// Sleeps for the specified time. 233 | /// 234 | /// 235 | /// 236 | public static Task Sleep(TimeSpan timeSpan) 237 | => Task.Delay(timeSpan); 238 | 239 | /// 240 | /// Sleeps for the specified time. 241 | /// 242 | /// 243 | /// 244 | public static Task Sleep(int seconds) 245 | => Task.Delay(TimeSpan.FromSeconds(seconds)); 246 | 247 | /// 248 | /// Get the task to ignore the exception and return . 249 | /// 250 | /// 251 | /// 252 | public static Task NoThrow(ProcessTask task) 253 | => task.NoThrow(); 254 | 255 | /// 256 | /// Terminates the current process with specified exit code. 257 | /// 258 | /// 259 | public static void Exit(int exitCode = 0) 260 | => Environment.Exit(exitCode); 261 | 262 | /// 263 | /// Creates a new directory and any necessary sub-directories in the specified path. 264 | /// 265 | /// 266 | public static void Mkdirp(string path) 267 | => Directory.CreateDirectory(path); 268 | 269 | /// 270 | /// Fetches the content of the specified URL using GET method. 271 | /// 272 | /// 273 | /// 274 | /// 275 | public static Task FetchAsync(string requestUri, CancellationToken cancellationToken = default) 276 | { 277 | CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchAsync)} {requestUri}"); 278 | var httpClient = new HttpClient(); 279 | return httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); 280 | } 281 | 282 | /// 283 | /// Fetches the content of the specified URL as string using GET method. 284 | /// 285 | /// 286 | /// 287 | /// 288 | public static async Task FetchStringAsync(string requestUri, CancellationToken cancellationToken = default) 289 | { 290 | CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchStringAsync)} {requestUri}"); 291 | var httpClient = new HttpClient(); 292 | var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); 293 | res.EnsureSuccessStatusCode(); 294 | 295 | #if NET5_0_OR_GREATER 296 | return await res.Content.ReadAsStringAsync(cancellationToken); 297 | #else 298 | return await res.Content.ReadAsStringAsync(); 299 | #endif 300 | } 301 | 302 | /// 303 | /// Fetches the content of the specified URL as byte[] using GET method. 304 | /// 305 | /// 306 | /// 307 | /// 308 | public static async Task FetchByteArrayAsync(string requestUri, CancellationToken cancellationToken = default) 309 | { 310 | CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchByteArrayAsync)} {requestUri}"); 311 | var httpClient = new HttpClient(); 312 | var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); 313 | res.EnsureSuccessStatusCode(); 314 | 315 | #if NET5_0_OR_GREATER 316 | return await res.Content.ReadAsByteArrayAsync(cancellationToken); 317 | #else 318 | return await res.Content.ReadAsByteArrayAsync(); 319 | #endif 320 | } 321 | 322 | /// 323 | /// Fetches the content of the specified URL as Stream using GET method. 324 | /// 325 | /// 326 | /// 327 | /// 328 | public static async Task FetchStreamAsync(string requestUri, CancellationToken cancellationToken = default) 329 | { 330 | CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchStreamAsync)} {requestUri}"); 331 | var httpClient = new HttpClient(); 332 | var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); 333 | res.EnsureSuccessStatusCode(); 334 | 335 | #if NET5_0_OR_GREATER 336 | return await res.Content.ReadAsStreamAsync(cancellationToken); 337 | #else 338 | return await res.Content.ReadAsStreamAsync(); 339 | #endif 340 | } 341 | 342 | /// 343 | /// Gets the full path to a command, similar to the `which` command on Unix. 344 | /// 345 | /// 346 | /// 347 | public static string Which(string commandName) 348 | => Internal.Which.TryGetPath(commandName, out var matchedPath) 349 | ? matchedPath 350 | : throw new FileNotFoundException($"Command '{commandName}' is not found."); 351 | 352 | /// 353 | /// Gets the full path to a command, similar to the `which` command on Unix. 354 | /// 355 | /// 356 | /// 357 | /// 358 | public static bool TryWhich(string commandName, out string matchedPath) 359 | => Internal.Which.TryGetPath(commandName, out matchedPath); 360 | 361 | /// 362 | /// Enumerates paths under the current directory that match the specified glob pattern. 363 | /// 364 | /// 365 | /// A glob pattern accepts * and ** (e.g. **/*.cs). If the specify a pattern is started with '!', it will be treated as an excluded pattern. 366 | /// 367 | /// 368 | /// 369 | public static IEnumerable Glob(params string[] patterns) 370 | => Glob(Environment.CurrentDirectory, patterns); 371 | 372 | /// 373 | /// Enumerates paths under the specified directory that match the specified glob pattern. 374 | /// 375 | /// 376 | /// A glob pattern accepts * and ** (e.g. **/*.cs). If the specify a pattern is started with '!', it will be treated as an excluded pattern. 377 | /// 378 | /// 379 | /// 380 | /// 381 | public static IEnumerable Glob(string baseDir, string[] patterns) 382 | { 383 | var matcher = new Matcher(); 384 | 385 | foreach (var pattern in patterns) 386 | { 387 | if (pattern.StartsWith("!")) 388 | { 389 | matcher.AddExclude(pattern.Substring(1)); 390 | } 391 | else 392 | { 393 | matcher.AddInclude(pattern); 394 | } 395 | } 396 | 397 | var result = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(baseDir))); 398 | return result.Files 399 | .Select(x => Path.GetFullPath(Path.Combine(baseDir, x.Stem))); // NOTE: Microsoft.Extensions.FileSystemGlobbing 5.0.0 does not reflect the root directory in `Path`. 400 | } 401 | 402 | /// 403 | /// Displays the message and reads lines entered by the user from the console. 404 | /// 405 | /// 406 | /// 407 | public static async Task Prompt(string message) 408 | { 409 | Console.Write(message); 410 | return await Console.In.ReadLineAsync(); 411 | } 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/Chell/Extensions/ChellExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Chell.Internal; 8 | 9 | namespace Chell.Extensions 10 | { 11 | public static class ChellExtensions 12 | { 13 | /// 14 | /// Writes a object details to the output. 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static T Dump(this T value) 20 | { 21 | return ObjectDumper.Dump(value); 22 | } 23 | 24 | /// 25 | /// Writes a object details to the output. 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static async Task Dump(this Task task) 31 | { 32 | var result = await task.ConfigureAwait(false); 33 | ObjectDumper.Dump(result); 34 | return result; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Chell/Extensions/ProcessOutputExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | 4 | namespace Chell 5 | { 6 | public static class ProcessOutputExtensions 7 | { 8 | /// 9 | /// Enumerates lines by converting them to objects as JSON. 10 | /// 11 | /// 12 | /// Converts to the anonymous type specified in argument. 13 | /// 14 | public static IEnumerable AsJsonLines(this ProcessOutput processOutput, T shape, bool skipEmptyLine = true, JsonSerializerOptions? options = null) 15 | => Chell.Extensions.StringExtensions.AsJsonLines(processOutput.AsLines(), skipEmptyLine, options); 16 | 17 | /// 18 | /// Enumerates lines by converting them to objects as JSON. 19 | /// 20 | public static IEnumerable AsJsonLines(this ProcessOutput processOutput, bool skipEmptyLine = true, JsonSerializerOptions? options = null) 21 | => Chell.Extensions.StringExtensions.AsJsonLines(processOutput.AsLines(), skipEmptyLine, options); 22 | 23 | /// 24 | /// Converts the output string to an object as JSON. 25 | /// 26 | /// 27 | /// Converts to the anonymous type specified in argument. 28 | /// 29 | public static T? AsJson(this ProcessOutput processOutput, T shape, JsonSerializerOptions? options = null) 30 | => AsJson(processOutput, options); 31 | 32 | /// 33 | /// Converts the output string to an object as JSON. 34 | /// 35 | public static T? AsJson(this ProcessOutput processOutput, JsonSerializerOptions? options = null) 36 | => JsonSerializer.Deserialize(processOutput.ToString(), options); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Chell/Extensions/ProcessTaskExtensions.Generated.cs: -------------------------------------------------------------------------------- 1 | /// 2 | using System.Threading.Tasks; 3 | 4 | namespace Chell 5 | { 6 | public static partial class ProcessTaskExtensions 7 | { 8 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2)> GetAwaiter(this (ProcessTask T1, ProcessTask T2) tasks) 9 | { 10 | static async Task<(ProcessOutput Result1, ProcessOutput Result2)> WhenAllAsync(ProcessTask t1, ProcessTask t2) 11 | { 12 | var results = await Task.WhenAll(t1, t2).ConfigureAwait(false); 13 | return (results[0], results[1]); 14 | } 15 | return WhenAllAsync(tasks.T1, tasks.T2).GetAwaiter(); 16 | } 17 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3) tasks) 18 | { 19 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3) 20 | { 21 | var results = await Task.WhenAll(t1, t2, t3).ConfigureAwait(false); 22 | return (results[0], results[1], results[2]); 23 | } 24 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3).GetAwaiter(); 25 | } 26 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4) tasks) 27 | { 28 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4) 29 | { 30 | var results = await Task.WhenAll(t1, t2, t3, t4).ConfigureAwait(false); 31 | return (results[0], results[1], results[2], results[3]); 32 | } 33 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4).GetAwaiter(); 34 | } 35 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5) tasks) 36 | { 37 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5) 38 | { 39 | var results = await Task.WhenAll(t1, t2, t3, t4, t5).ConfigureAwait(false); 40 | return (results[0], results[1], results[2], results[3], results[4]); 41 | } 42 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5).GetAwaiter(); 43 | } 44 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5, ProcessTask T6) tasks) 45 | { 46 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5, ProcessTask t6) 47 | { 48 | var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6).ConfigureAwait(false); 49 | return (results[0], results[1], results[2], results[3], results[4], results[5]); 50 | } 51 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6).GetAwaiter(); 52 | } 53 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5, ProcessTask T6, ProcessTask T7) tasks) 54 | { 55 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5, ProcessTask t6, ProcessTask t7) 56 | { 57 | var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7).ConfigureAwait(false); 58 | return (results[0], results[1], results[2], results[3], results[4], results[5], results[6]); 59 | } 60 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7).GetAwaiter(); 61 | } 62 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5, ProcessTask T6, ProcessTask T7, ProcessTask T8) tasks) 63 | { 64 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5, ProcessTask t6, ProcessTask t7, ProcessTask t8) 65 | { 66 | var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8).ConfigureAwait(false); 67 | return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7]); 68 | } 69 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8).GetAwaiter(); 70 | } 71 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8, ProcessOutput Result9)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5, ProcessTask T6, ProcessTask T7, ProcessTask T8, ProcessTask T9) tasks) 72 | { 73 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8, ProcessOutput Result9)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5, ProcessTask t6, ProcessTask t7, ProcessTask t8, ProcessTask t9) 74 | { 75 | var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8, t9).ConfigureAwait(false); 76 | return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7], results[8]); 77 | } 78 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8, tasks.T9).GetAwaiter(); 79 | } 80 | public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8, ProcessOutput Result9, ProcessOutput Result10)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3, ProcessTask T4, ProcessTask T5, ProcessTask T6, ProcessTask T7, ProcessTask T8, ProcessTask T9, ProcessTask T10) tasks) 81 | { 82 | static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5, ProcessOutput Result6, ProcessOutput Result7, ProcessOutput Result8, ProcessOutput Result9, ProcessOutput Result10)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5, ProcessTask t6, ProcessTask t7, ProcessTask t8, ProcessTask t9, ProcessTask t10) 83 | { 84 | var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10).ConfigureAwait(false); 85 | return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7], results[8], results[9]); 86 | } 87 | return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8, tasks.T9, tasks.T10).GetAwaiter(); 88 | } 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /src/Chell/Extensions/ProcessTaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Chell 4 | { 5 | public static partial class ProcessTaskExtensions 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Chell/Extensions/ProcessTaskExtensions.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".Generated.cs" #> 7 | /// 8 | using System.Threading.Tasks; 9 | 10 | namespace Chell 11 | { 12 | public static partial class ProcessTaskExtensions 13 | { 14 | <# for (var i = 2; i <= 10; i++) { 15 | var inTuple = string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessTask T{x}")); 16 | var outTuple = string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessOutput Result{x}")); 17 | #> 18 | public static System.Runtime.CompilerServices.TaskAwaiter<(<#= outTuple #>)> GetAwaiter(this (<#= inTuple #>) tasks) 19 | { 20 | static async Task<(<#= outTuple #>)> WhenAllAsync(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessTask t{x}")) #>) 21 | { 22 | var results = await Task.WhenAll(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"t{x}")) #>).ConfigureAwait(false); 23 | return (<#= string.Join(", ", Enumerable.Range(0, i).Select(x => $"results[{x}]")) #>); 24 | } 25 | return WhenAllAsync(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"tasks.T{x}")) #>).GetAwaiter(); 26 | } 27 | <# } #> 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/Chell/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace Chell.Extensions 9 | { 10 | public static class StringExtensions 11 | { 12 | /// 13 | /// Enumerates lines by converting them to objects as JSON. 14 | /// 15 | /// 16 | /// Converts to the anonymous type specified in argument. 17 | /// 18 | public static IEnumerable AsJsonLines(this IEnumerable lines, T shape, bool skipEmptyLine = true, JsonSerializerOptions? options = null) 19 | { 20 | return AsJsonLines(lines, skipEmptyLine, options); 21 | } 22 | 23 | /// 24 | /// Enumerates lines by converting them to objects as JSON. 25 | /// 26 | public static IEnumerable AsJsonLines(this IEnumerable lines, bool skipEmptyLine = true, JsonSerializerOptions? options = null) 27 | { 28 | return lines.Where(x => !skipEmptyLine || !string.IsNullOrWhiteSpace(x)).Select(x => JsonSerializer.Deserialize(x, options)); 29 | } 30 | 31 | /// 32 | /// Converts the output string to an object as JSON. 33 | /// 34 | /// 35 | /// Converts to the anonymous type specified in argument. 36 | /// 37 | public static T? AsJson(this string json, T shape, JsonSerializerOptions? options = null) 38 | => AsJson(json, options); 39 | 40 | /// 41 | /// Converts the output string to an object as JSON. 42 | /// 43 | public static T? AsJson(this string json, JsonSerializerOptions? options = null) 44 | => JsonSerializer.Deserialize(json.ToString(), options); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Chell/IO/ChellWrappedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Chell.IO 11 | { 12 | public partial class ChellWritableStream : ChellWrappedStream 13 | { 14 | private readonly StreamWriter _writer; 15 | 16 | public ChellWritableStream(Stream baseStream, Encoding encoding) 17 | : base(baseStream) 18 | { 19 | _writer = new StreamWriter(baseStream, encoding); 20 | _writer.AutoFlush = true; 21 | } 22 | 23 | public void Write(byte[] value) => BaseStream.Write(value); 24 | public new void Write(ReadOnlySpan value) => BaseStream.Write(value); 25 | public ValueTask WriteAsync(byte[] value, CancellationToken cancellationToken = default) => BaseStream.WriteAsync(value, cancellationToken); 26 | public new ValueTask WriteAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) => BaseStream.WriteAsync(value, cancellationToken); 27 | 28 | protected override void Dispose(bool disposing) 29 | { 30 | _writer.Dispose(); 31 | base.Dispose(disposing); 32 | } 33 | 34 | public override async ValueTask DisposeAsync() 35 | { 36 | await _writer.DisposeAsync(); 37 | await base.DisposeAsync(); 38 | } 39 | } 40 | 41 | public partial class ChellReadableStream : ChellWrappedStream 42 | { 43 | private readonly StreamReader _reader; 44 | 45 | public ChellReadableStream(Stream baseStream, Encoding encoding) 46 | : base(baseStream) 47 | { 48 | _reader = new StreamReader(baseStream, encoding); 49 | } 50 | 51 | public async Task ReadAllBytesAsync(CancellationToken cancellationToken = default) 52 | { 53 | var bufferWriter = new ArrayBufferWriter(); 54 | while (true) 55 | { 56 | cancellationToken.ThrowIfCancellationRequested(); 57 | var readLen = await BaseStream.ReadAsync(bufferWriter.GetMemory(1024 * 32), cancellationToken); 58 | if (readLen == 0) 59 | { 60 | return bufferWriter.WrittenMemory.ToArray(); 61 | } 62 | bufferWriter.Advance(readLen); 63 | } 64 | } 65 | 66 | public byte[] ReadAllBytes() 67 | { 68 | var bufferWriter = new ArrayBufferWriter(); 69 | while (true) 70 | { 71 | var readLen = BaseStream.Read(bufferWriter.GetSpan(1024 * 32)); 72 | if (readLen == 0) 73 | { 74 | return bufferWriter.WrittenMemory.ToArray(); 75 | } 76 | bufferWriter.Advance(readLen); 77 | } 78 | } 79 | 80 | public async Task ReadToEndAsync() 81 | { 82 | return await _reader.ReadToEndAsync(); 83 | } 84 | 85 | public string ReadToEnd() 86 | { 87 | return _reader.ReadToEnd(); 88 | } 89 | 90 | public IEnumerable ReadAllLines() 91 | { 92 | while (!_reader.EndOfStream) 93 | { 94 | var line = _reader.ReadLine(); 95 | if (line is null) yield break; 96 | 97 | yield return line; 98 | } 99 | } 100 | 101 | public async IAsyncEnumerable ReadAllLinesAsync() 102 | { 103 | while (!_reader.EndOfStream) 104 | { 105 | var line = await _reader.ReadLineAsync(); 106 | if (line is null) yield break; 107 | 108 | yield return line; 109 | } 110 | } 111 | 112 | protected override void Dispose(bool disposing) 113 | { 114 | _reader.Dispose(); 115 | base.Dispose(disposing); 116 | } 117 | } 118 | 119 | public abstract class ChellWrappedStream : Stream 120 | { 121 | private readonly Stream _baseStream; 122 | 123 | protected Stream BaseStream => _baseStream; 124 | 125 | protected ChellWrappedStream(Stream baseStream) 126 | { 127 | _baseStream = baseStream; 128 | } 129 | 130 | #region Stream Implementation 131 | public override void Flush() 132 | { 133 | _baseStream.Flush(); 134 | } 135 | 136 | public override int Read(byte[] buffer, int offset, int count) 137 | { 138 | return _baseStream.Read(buffer, offset, count); 139 | } 140 | 141 | public override int Read(Span buffer) 142 | { 143 | return _baseStream.Read(buffer); 144 | } 145 | 146 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 147 | { 148 | return _baseStream.ReadAsync(buffer, offset, count, cancellationToken); 149 | } 150 | 151 | public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) 152 | { 153 | return _baseStream.ReadAsync(buffer, cancellationToken); 154 | } 155 | 156 | public override long Seek(long offset, SeekOrigin origin) 157 | { 158 | return _baseStream.Seek(offset, origin); 159 | } 160 | 161 | public override void SetLength(long value) 162 | { 163 | _baseStream.SetLength(value); 164 | } 165 | 166 | public override void Write(byte[] buffer, int offset, int count) 167 | { 168 | _baseStream.Write(buffer, offset, count); 169 | } 170 | 171 | public override void Write(ReadOnlySpan buffer) 172 | { 173 | _baseStream.Write(buffer); 174 | } 175 | 176 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 177 | { 178 | return _baseStream.WriteAsync(buffer, offset, count, cancellationToken); 179 | } 180 | 181 | public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = new CancellationToken()) 182 | { 183 | return _baseStream.WriteAsync(buffer, cancellationToken); 184 | } 185 | 186 | public override bool CanRead => _baseStream.CanRead; 187 | 188 | public override bool CanSeek => _baseStream.CanSeek; 189 | 190 | public override bool CanWrite => _baseStream.CanWrite; 191 | 192 | public override long Length => _baseStream.Length; 193 | 194 | public override long Position 195 | { 196 | get => _baseStream.Position; 197 | set => _baseStream.Position = value; 198 | } 199 | #endregion 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Chell/IO/ChellWritableStream.Generated.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | /// 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chell.IO 8 | { 9 | public partial class ChellWritableStream 10 | { 11 | public void Write(string? value) => _writer.Write(value); 12 | public void WriteLine(string? value) => _writer.WriteLine(value); 13 | public Task WriteAsync(string? value) => _writer.WriteAsync(value); 14 | public Task WriteLineAsync(string? value) => _writer.WriteLineAsync(value); 15 | public void Write(char value) => _writer.Write(value); 16 | public void WriteLine(char value) => _writer.WriteLine(value); 17 | public Task WriteAsync(char value) => _writer.WriteAsync(value); 18 | public Task WriteLineAsync(char value) => _writer.WriteLineAsync(value); 19 | public void Write(char[]? value) => _writer.Write(value); 20 | public void WriteLine(char[]? value) => _writer.WriteLine(value); 21 | public Task WriteAsync(char[]? value) => _writer.WriteAsync(value); 22 | public Task WriteLineAsync(char[]? value) => _writer.WriteLineAsync(value); 23 | public void Write(object? value) => _writer.Write(value); 24 | public void WriteLine(object? value) => _writer.WriteLine(value); 25 | public void Write(double value) => _writer.Write(value); 26 | public void WriteLine(double value) => _writer.WriteLine(value); 27 | public void Write(float value) => _writer.Write(value); 28 | public void WriteLine(float value) => _writer.WriteLine(value); 29 | public void Write(long value) => _writer.Write(value); 30 | public void WriteLine(long value) => _writer.WriteLine(value); 31 | public void Write(int value) => _writer.Write(value); 32 | public void WriteLine(int value) => _writer.WriteLine(value); 33 | public void Write(ReadOnlySpan value) => _writer.Write(value); 34 | public void WriteLine(ReadOnlySpan value) => _writer.WriteLine(value); 35 | public Task WriteAsync(ReadOnlyMemory value) => _writer.WriteAsync(value); 36 | public Task WriteLineAsync(ReadOnlyMemory value) => _writer.WriteLineAsync(value); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Chell/IO/ChellWritableStream.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".Generated.cs" #> 7 | <# 8 | var types = new [] 9 | { 10 | (0, "string?"), 11 | (0, "char"), 12 | (0, "char[]?"), 13 | (1, "object?"), 14 | (1, "double"), 15 | (1, "float"), 16 | (1, "long"), 17 | (1, "int"), 18 | (1, "ReadOnlySpan"), 19 | (2, "ReadOnlyMemory"), 20 | }; 21 | #> 22 | #nullable enable 23 | /// 24 | using System; 25 | using System.IO; 26 | using System.Threading.Tasks; 27 | 28 | namespace Chell.IO 29 | { 30 | public partial class ChellWritableStream 31 | { 32 | <# foreach (var (target, type) in types) { #> 33 | <# if (target == 0 || target == 1) { #> 34 | public void Write(<#= type #> value) => _writer.Write(value); 35 | public void WriteLine(<#= type #> value) => _writer.WriteLine(value); 36 | <# } #> 37 | <# if (target == 0 || target == 2) { #> 38 | public Task WriteAsync(<#= type #> value) => _writer.WriteAsync(value); 39 | public Task WriteLineAsync(<#= type #> value) => _writer.WriteLineAsync(value); 40 | <# } #> 41 | <# } #> 42 | } 43 | } -------------------------------------------------------------------------------- /src/Chell/IO/IConsoleProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Chell.IO 6 | { 7 | public interface IConsoleProvider 8 | { 9 | Stream OpenStandardInput(); 10 | Stream OpenStandardOutput(); 11 | Stream OpenStandardError(); 12 | Encoding InputEncoding { get; } 13 | Encoding OutputEncoding { get; } 14 | bool IsInputRedirected { get; } 15 | bool IsOutputRedirected { get; } 16 | bool IsErrorRedirected { get; } 17 | 18 | TextWriter Out { get; } 19 | TextWriter Error { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Chell/IO/LINQPadConsoleProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Pipelines; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Chell.IO 10 | { 11 | public class LINQPadConsoleProvider : IConsoleProvider 12 | { 13 | private readonly Pipe _pipe; 14 | 15 | public Encoding InputEncoding => Console.InputEncoding; 16 | public Encoding OutputEncoding => Console.OutputEncoding; 17 | public bool IsInputRedirected => Console.IsInputRedirected; 18 | public bool IsOutputRedirected => Console.IsOutputRedirected; 19 | public bool IsErrorRedirected => Console.IsErrorRedirected; 20 | 21 | public TextWriter Out { get; } 22 | public TextWriter Error { get; } 23 | 24 | public LINQPadConsoleProvider() 25 | { 26 | _pipe = new Pipe(); 27 | Out = new PipeTextWriter(_pipe.Writer, OutputEncoding); 28 | Error = new PipeTextWriter(_pipe.Writer, OutputEncoding); 29 | 30 | var reader = new StreamReader(_pipe.Reader.AsStream()); 31 | _ = Task.Run(async () => 32 | { 33 | Memory buffer = new char[1024]; 34 | while (true) 35 | { 36 | var read = await reader.ReadAsync(buffer, default); 37 | if (read != 0) 38 | { 39 | Console.Out.Write(buffer.Span.Slice(0, read)); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | public Stream OpenStandardInput() 46 | => Console.OpenStandardInput(); 47 | 48 | public Stream OpenStandardOutput() 49 | => _pipe.Writer.AsStream(leaveOpen: true); 50 | 51 | public Stream OpenStandardError() 52 | => _pipe.Writer.AsStream(leaveOpen: true); 53 | 54 | private class PipeTextWriter : TextWriter 55 | { 56 | private readonly PipeWriter _writer; 57 | public override Encoding Encoding { get; } 58 | 59 | public PipeTextWriter(PipeWriter writer, Encoding encoding) 60 | { 61 | _writer = writer; 62 | Encoding = encoding; 63 | } 64 | 65 | public override void Write(char value) 66 | { 67 | Span buffer = stackalloc byte[4]; 68 | Span c = stackalloc char[1]; 69 | c[0] = value; 70 | 71 | var written = Encoding.GetBytes(c, buffer); 72 | _writer.Write(buffer.Slice(0, written)); 73 | _ = _writer.FlushAsync(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Chell/IO/SystemConsoleProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Chell.IO 6 | { 7 | /// 8 | /// Encapsulates console intrinsic members as object. 9 | /// 10 | public sealed class SystemConsoleProvider : IConsoleProvider 11 | { 12 | public static IConsoleProvider Instance { get; } = new SystemConsoleProvider(); 13 | 14 | private SystemConsoleProvider() 15 | {} 16 | 17 | public Stream OpenStandardInput() 18 | => Console.OpenStandardInput(); 19 | 20 | public Stream OpenStandardOutput() 21 | => Console.OpenStandardOutput(); 22 | 23 | public Stream OpenStandardError() 24 | => Console.OpenStandardError(); 25 | 26 | public Encoding InputEncoding => Console.InputEncoding; 27 | public Encoding OutputEncoding => Console.OutputEncoding; 28 | 29 | public bool IsInputRedirected => Console.IsInputRedirected; 30 | public bool IsOutputRedirected => Console.IsOutputRedirected; 31 | public bool IsErrorRedirected => Console.IsErrorRedirected; 32 | 33 | public TextWriter Out => Console.Out; 34 | public TextWriter Error => Console.Error; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Chell/Internal/CommandLineHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using Chell.IO; 5 | using Chell.Shell; 6 | using Kokuban; 7 | 8 | // ReSharper disable CoVariantArrayConversion 9 | 10 | namespace Chell.Internal 11 | { 12 | internal class CommandLineHelper 13 | { 14 | public static void WriteCommandLineToConsole(IConsoleProvider console, string commandLine, ChellVerbosity? verbosity = default) 15 | { 16 | verbosity ??= ChellEnvironment.Current.Verbosity; 17 | if (verbosity.Value.HasFlag(ChellVerbosity.CommandLine)) 18 | { 19 | var parts = commandLine.Split(' ', 2); 20 | if (LINQPadHelper.RunningOnLINQPad) 21 | { 22 | LINQPadHelper.WriteRawHtml( 23 | $"$ {EscapeHtml(parts[0])}{EscapeHtml((parts.Length > 1 ? " " + parts[1] : string.Empty))}"); 24 | } 25 | else 26 | { 27 | console.Out.WriteLine("$ " + (Chalk.BrightGreen + parts[0]) + (parts.Length > 1 ? " " + parts[1] : string.Empty)); 28 | } 29 | } 30 | 31 | static string EscapeHtml(string s) 32 | => s.Replace("&", "&").Replace("\"", """).Replace("<", ">"); 33 | } 34 | 35 | public static string Expand(FormattableString commandLine, IShellExecutor shellExecutor) 36 | { 37 | return string.Format(commandLine.Format.Trim(), commandLine.GetArguments().Select(x => 38 | { 39 | return x switch 40 | { 41 | ProcessOutput procOutput => shellExecutor.Escape(procOutput.Output.TrimEnd('\n')), 42 | string s => shellExecutor.Escape(s), 43 | IEnumerable enumerable => string.Join(" ", enumerable.OfType().Select(y => shellExecutor.Escape(y.ToString() ?? string.Empty))), 44 | null => string.Empty, 45 | _ => shellExecutor.Escape(x.ToString() ?? string.Empty), 46 | }; 47 | }).ToArray()); 48 | } 49 | 50 | public static (string Command, string Arguments) Parse(FormattableString commandLine) 51 | { 52 | var (command, argumentsFormat) = Parse(commandLine.Format.Trim()); 53 | return (command, string.Format(argumentsFormat, commandLine.GetArguments().Select(x => 54 | { 55 | if (x is IEnumerable enumerable) 56 | { 57 | return string.Join(" ", enumerable.OfType().Select(y => Escape(y.ToString() ?? string.Empty))); 58 | } 59 | return Escape(x?.ToString() ?? string.Empty); 60 | }).ToArray())); 61 | } 62 | 63 | public static (string Command, string Arguments) Parse(string commandLine) 64 | { 65 | if (commandLine.StartsWith("\"")) 66 | { 67 | var pos = commandLine.IndexOf('"', 1); 68 | if (pos == -1) 69 | { 70 | throw new InvalidOperationException("Invalid Command"); 71 | } 72 | return (Command: commandLine.Substring(1, pos), Arguments: commandLine.Substring(pos)); 73 | } 74 | else 75 | { 76 | var parts = commandLine.Split(' ', 2); 77 | return (Command: parts[0], Arguments: parts.Length == 1 ? string.Empty : parts[1]); 78 | } 79 | } 80 | 81 | public static string Escape(string v) 82 | => $"\"{v.Replace("`", "``").Replace("\"", "`\"")}\""; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Chell/Internal/EnvironmentVariables.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Chell.Internal 7 | { 8 | internal class EnvironmentVariables : IDictionary 9 | { 10 | public IEnumerator> GetEnumerator() 11 | { 12 | return Environment.GetEnvironmentVariables() 13 | .OfType() 14 | .Select(x => KeyValuePair.Create((string)x.Key, (string)(x.Value ?? string.Empty))) 15 | .GetEnumerator(); 16 | } 17 | 18 | IEnumerator IEnumerable.GetEnumerator() 19 | { 20 | return GetEnumerator(); 21 | } 22 | 23 | public void Add(KeyValuePair item) 24 | { 25 | Environment.SetEnvironmentVariable(item.Key, item.Value); 26 | } 27 | 28 | public void Clear() 29 | { 30 | throw new NotSupportedException(); 31 | } 32 | 33 | public bool Contains(KeyValuePair item) 34 | { 35 | return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(item.Key)); 36 | } 37 | 38 | public void CopyTo(KeyValuePair[] array, int arrayIndex) 39 | { 40 | throw new NotSupportedException(); 41 | } 42 | 43 | public bool Remove(KeyValuePair item) 44 | { 45 | if (Contains(item)) 46 | { 47 | Environment.SetEnvironmentVariable(item.Key, null); 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | public int Count => Environment.GetEnvironmentVariables().Count; 55 | public bool IsReadOnly => false; 56 | 57 | public void Add(string key, string value) 58 | { 59 | Environment.SetEnvironmentVariable(key, value); 60 | } 61 | 62 | public bool ContainsKey(string key) 63 | { 64 | return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(key)); 65 | } 66 | 67 | public bool Remove(string key) 68 | { 69 | if (ContainsKey(key)) 70 | { 71 | Environment.SetEnvironmentVariable(key, null); 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | 78 | public bool TryGetValue(string key, out string value) 79 | { 80 | var tmpValue = Environment.GetEnvironmentVariable(key); 81 | if (string.IsNullOrWhiteSpace(tmpValue)) 82 | { 83 | value = string.Empty; 84 | return false; 85 | } 86 | 87 | value = tmpValue; 88 | return true; 89 | } 90 | 91 | public string this[string key] 92 | { 93 | get => TryGetValue(key, out var value) ? value : string.Empty; 94 | set => Add(key, value); 95 | } 96 | 97 | public ICollection Keys 98 | => Environment.GetEnvironmentVariables().OfType().Select(x => (string)x.Key).ToArray(); 99 | public ICollection Values 100 | => Environment.GetEnvironmentVariables().OfType().Select(x => (string)(x.Value ?? string.Empty)).ToArray(); 101 | } 102 | } -------------------------------------------------------------------------------- /src/Chell/Internal/LINQPadHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.IO.Pipelines; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Chell.Internal 12 | { 13 | internal class LINQPadHelper 14 | { 15 | private static bool? _runningOnLINQPad; 16 | internal static bool RunningOnLINQPad => _runningOnLINQPad ??= Type.GetType("LINQPad.Util, LINQPad.Runtime") != null; 17 | 18 | public static void WriteRawHtml(string html) 19 | { 20 | Debug.Assert(RunningOnLINQPad); 21 | 22 | var t = Type.GetType("LINQPad.Util, LINQPad.Runtime"); 23 | if (t != null) 24 | { 25 | var obj = t.InvokeMember("RawHtml", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new[] {html}); 26 | ObjectDumper.Dump(obj); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Chell/Internal/ObjectDumper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Text.Encodings.Web; 7 | using System.Text.Json; 8 | using System.Text.Unicode; 9 | using System.Threading.Tasks; 10 | 11 | namespace Chell.Internal 12 | { 13 | internal static class ObjectDumper 14 | { 15 | public static T Dump(T obj) 16 | => DumpMethodCache.Method(obj); 17 | 18 | private static class DumpMethodCache 19 | { 20 | public static MethodInfo? LINQPadDumpMethod { get; } 21 | 22 | static DumpMethodCache() 23 | { 24 | var linqPadExtensionsType = Type.GetType("LINQPad.Extensions, LINQPad.Runtime"); 25 | if (linqPadExtensionsType != null) 26 | { 27 | LINQPadDumpMethod = linqPadExtensionsType.GetMethods() 28 | .FirstOrDefault(x => x.Name == "Dump" && x.GetParameters().Length == 1); 29 | } 30 | } 31 | } 32 | 33 | private static class DumpMethodCache 34 | { 35 | public static Func Method { get; } 36 | 37 | static DumpMethodCache() 38 | { 39 | if (DumpMethodCache.LINQPadDumpMethod != null) 40 | { 41 | var closedDumpMethod = DumpMethodCache.LINQPadDumpMethod.MakeGenericMethod(typeof(T)); 42 | Method = (Func)closedDumpMethod.CreateDelegate(typeof(Func)); 43 | } 44 | else 45 | { 46 | Method = x => 47 | { 48 | Console.WriteLine(JsonSerializer.Serialize(x, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), WriteIndented = true })); 49 | return x; 50 | }; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Chell/Internal/OutputSink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipelines; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Chell.Internal 9 | { 10 | internal class OutputSink : IDisposable, IAsyncDisposable 11 | { 12 | private readonly Encoding _encoding; 13 | private readonly Pipe _outputPipe; 14 | private readonly Pipe _errorPipe; 15 | private readonly MemoryStream _outputBuffer; 16 | private readonly MemoryStream _errorBuffer; 17 | private readonly MemoryStream _combinedBuffer; 18 | private readonly CancellationTokenSource _cancellationTokenSource; 19 | private readonly Task _readWriteTaskOutput; 20 | private readonly Task _readWriteTaskError; 21 | 22 | internal PipeWriter OutputWriter => _outputPipe.Writer; 23 | internal PipeWriter ErrorWriter => _errorPipe.Writer; 24 | 25 | public ReadOnlyMemory OutputBinary => new ReadOnlyMemory(_outputBuffer.GetBuffer(), 0, (int)_outputBuffer.Length); 26 | public ReadOnlyMemory ErrorBinary => new ReadOnlyMemory(_errorBuffer.GetBuffer(), 0, (int)_errorBuffer.Length); 27 | public ReadOnlyMemory CombinedBinary => new ReadOnlyMemory(_combinedBuffer.GetBuffer(), 0, (int)_combinedBuffer.Length); 28 | public string Output => (_outputBuffer is { Length: > 0} s) ? _encoding.GetString(OutputBinary.Span) : string.Empty; 29 | public string Error => (_errorBuffer is { Length: > 0 } s) ? _encoding.GetString(ErrorBinary.Span) : string.Empty; 30 | public string Combined => (_combinedBuffer is { Length: > 0 } s) ? _encoding.GetString(CombinedBinary.Span) : string.Empty; 31 | 32 | public OutputSink(Encoding encoding) 33 | { 34 | _encoding = encoding; 35 | _outputPipe = new Pipe(); 36 | _errorPipe = new Pipe(); 37 | _cancellationTokenSource = new CancellationTokenSource(); 38 | 39 | _outputBuffer = new MemoryStream(); 40 | _errorBuffer = new MemoryStream(); 41 | _combinedBuffer = new MemoryStream(); 42 | 43 | _readWriteTaskOutput = RunReadWriteLoopAsync(_outputPipe.Reader, _outputBuffer, _cancellationTokenSource.Token); 44 | _readWriteTaskError = RunReadWriteLoopAsync(_errorPipe.Reader, _errorBuffer, _cancellationTokenSource.Token); 45 | } 46 | 47 | public async Task CompleteAsync() 48 | { 49 | await _outputPipe.Writer.CompleteAsync().ConfigureAwait(false); 50 | await _errorPipe.Writer.CompleteAsync().ConfigureAwait(false); 51 | 52 | try 53 | { 54 | _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); 55 | await _readWriteTaskOutput.ConfigureAwait(false); 56 | await _readWriteTaskError.ConfigureAwait(false); 57 | } 58 | catch (OperationCanceledException) 59 | { 60 | } 61 | } 62 | 63 | private async Task RunReadWriteLoopAsync(PipeReader reader, Stream dest, CancellationToken cancellationToken) 64 | { 65 | while (true) 66 | { 67 | cancellationToken.ThrowIfCancellationRequested(); 68 | 69 | var result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); 70 | 71 | if (result.Buffer.IsSingleSegment) 72 | { 73 | dest.Write(result.Buffer.FirstSpan); 74 | _combinedBuffer.Write(result.Buffer.FirstSpan); 75 | } 76 | else 77 | { 78 | foreach (var segment in result.Buffer) 79 | { 80 | dest.Write(segment.Span); 81 | _combinedBuffer.Write(segment.Span); 82 | } 83 | } 84 | 85 | reader.AdvanceTo(result.Buffer.End); 86 | 87 | if (result.IsCanceled || result.IsCompleted) 88 | { 89 | return; 90 | } 91 | } 92 | } 93 | 94 | public void Dispose() 95 | { 96 | try 97 | { 98 | CompleteAsync().Wait(); 99 | } 100 | catch { } 101 | } 102 | 103 | public async ValueTask DisposeAsync() 104 | { 105 | try 106 | { 107 | await CompleteAsync().ConfigureAwait(false); 108 | } 109 | catch { } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/Chell/Internal/StandardInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Chell.Internal 5 | { 6 | internal class StandardInput 7 | { 8 | private static readonly Lazy _pipe; 9 | internal static StreamPipe Pipe => _pipe.Value; 10 | 11 | static StandardInput() 12 | { 13 | _pipe = new Lazy(() => new StreamPipe(Console.OpenStandardInput()), LazyThreadSafetyMode.ExecutionAndPublication); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Chell/Internal/StreamPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Pipelines; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Chell.Internal 10 | { 11 | internal class StreamPipe 12 | { 13 | private readonly Pipe _pipe; 14 | private readonly Stream _baseStream; 15 | private readonly Task _copyTask; 16 | private readonly Task _readerTask; 17 | private readonly CancellationTokenSource _cancellationTokenSourceCopyStreamToPipe; 18 | private readonly CancellationTokenSource _cancellationTokenSource; 19 | private readonly object _syncLock = new object(); 20 | private readonly List _destinations = new List(); 21 | private readonly TaskCompletionSource _destinationsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 22 | private bool _shutdown; 23 | 24 | public StreamPipe(Stream baseStream) 25 | { 26 | _pipe = new Pipe(new PipeOptions()); 27 | _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); 28 | _cancellationTokenSource = new CancellationTokenSource(); 29 | _cancellationTokenSourceCopyStreamToPipe = new CancellationTokenSource(); 30 | 31 | _copyTask = CopyStreamToPipeAsync(_cancellationTokenSourceCopyStreamToPipe.Token); 32 | _readerTask = RunReadLoopAsync(_cancellationTokenSource.Token); 33 | } 34 | 35 | private async Task CopyStreamToPipeAsync(CancellationToken cancellationToken) 36 | { 37 | try 38 | { 39 | await _baseStream.CopyToAsync(_pipe.Writer, cancellationToken).ConfigureAwait(false); 40 | await _pipe.Writer.CompleteAsync().ConfigureAwait(false); 41 | } 42 | catch (Exception ex) 43 | { 44 | await _pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); 45 | } 46 | } 47 | 48 | public async Task CompleteAsync() 49 | { 50 | _shutdown = true; 51 | _cancellationTokenSource.CancelAfter(1000); 52 | _cancellationTokenSourceCopyStreamToPipe.CancelAfter(1000); 53 | 54 | try 55 | { 56 | await _copyTask.ConfigureAwait(false); 57 | await _readerTask.ConfigureAwait(false); 58 | } 59 | catch (OperationCanceledException) 60 | { 61 | } 62 | } 63 | 64 | public void Ready() 65 | { 66 | _destinationsReady.TrySetResult(true); 67 | } 68 | 69 | public StreamPipe Connect(Stream stream) 70 | { 71 | lock (_syncLock) 72 | { 73 | _destinations.Add(stream ?? throw new ArgumentNullException(nameof(stream))); 74 | } 75 | return this; 76 | } 77 | public StreamPipe Connect(PipeWriter writer) 78 | { 79 | lock (_syncLock) 80 | { 81 | _destinations.Add(writer ?? throw new ArgumentNullException(nameof(writer))); 82 | } 83 | return this; 84 | } 85 | 86 | public StreamPipe Disconnect(Stream stream) 87 | { 88 | lock (_syncLock) 89 | { 90 | _destinations.Remove(stream ?? throw new ArgumentNullException(nameof(stream))); 91 | } 92 | return this; 93 | } 94 | 95 | public StreamPipe Disconnect(PipeWriter writer) 96 | { 97 | lock (_syncLock) 98 | { 99 | _destinations.Remove(writer ?? throw new ArgumentNullException(nameof(writer))); 100 | } 101 | return this; 102 | } 103 | 104 | private async Task RunReadLoopAsync(CancellationToken cancellationToken) 105 | { 106 | var cancellationTokenTask = new TaskCompletionSource(); 107 | await using var cancellationTokenRegistration = cancellationToken.Register(() => cancellationTokenTask.TrySetCanceled(cancellationToken)); 108 | 109 | while (true) 110 | { 111 | cancellationToken.ThrowIfCancellationRequested(); 112 | 113 | // Wait for the destination to be available. 114 | object[] dests; 115 | lock (_syncLock) 116 | { 117 | dests = _destinations.ToArray(); 118 | } 119 | while (dests.Length == 0) 120 | { 121 | if (_shutdown) return; 122 | cancellationToken.ThrowIfCancellationRequested(); 123 | 124 | // Wait for signal to start sending to the destinations. 125 | // This will wait only once after the process starts. 126 | await Task.WhenAny(cancellationTokenTask.Task, _destinationsReady.Task).ConfigureAwait(false); 127 | 128 | cancellationToken.ThrowIfCancellationRequested(); 129 | 130 | lock (_syncLock) 131 | { 132 | dests = _destinations.ToArray(); 133 | } 134 | 135 | if (dests.Length == 0) 136 | { 137 | await Task.Yield(); 138 | } 139 | } 140 | 141 | // Reads from the pipe reader. 142 | var result = await _pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 143 | 144 | // Get the current destination again. 145 | // If there is no destination after reading, do not advance the pipe, and go to the top of the loop. 146 | lock (_syncLock) 147 | { 148 | var destsCurrent = _destinations.ToArray(); 149 | if (!dests.SequenceEqual(destsCurrent)) 150 | { 151 | dests = destsCurrent; 152 | if (dests.Length == 0) 153 | { 154 | _pipe.Reader.AdvanceTo(result.Buffer.Start); 155 | continue; 156 | } 157 | } 158 | } 159 | 160 | // Writes to the destinations. 161 | await Task.WhenAll(dests.Select(async x => 162 | { 163 | var writeTask = x switch 164 | { 165 | Stream stream => WriteAsync(stream, result, cancellationToken), 166 | PipeWriter writer => WriteAsync(writer, result, cancellationToken), 167 | _ => throw new NotSupportedException() 168 | }; 169 | 170 | // NOTE: The destination may be closed first. 171 | // When the destination is closed, the task throws an IOException (Broken pipe). 172 | try 173 | { 174 | await writeTask.ConfigureAwait(false); 175 | } 176 | catch (IOException) 177 | { 178 | } 179 | })).ConfigureAwait(false); 180 | 181 | _pipe.Reader.AdvanceTo(result.Buffer.End); 182 | 183 | if (result.IsCanceled || result.IsCompleted) 184 | { 185 | return; 186 | } 187 | } 188 | } 189 | 190 | private static async ValueTask WriteAsync(Stream stream, ReadResult result, CancellationToken cancellationToken) 191 | { 192 | if (!result.Buffer.IsEmpty) 193 | { 194 | if (result.Buffer.IsSingleSegment) 195 | { 196 | await stream.WriteAsync(result.Buffer.First, cancellationToken).ConfigureAwait(false); 197 | } 198 | else 199 | { 200 | foreach (var segment in result.Buffer) 201 | { 202 | await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); 203 | } 204 | } 205 | await stream.FlushAsync(cancellationToken).ConfigureAwait(false); 206 | 207 | } 208 | 209 | if (result.IsCompleted || result.IsCanceled) 210 | { 211 | stream.Close(); 212 | } 213 | } 214 | 215 | private static async ValueTask WriteAsync(PipeWriter writer, ReadResult result, CancellationToken cancellationToken) 216 | { 217 | if (!result.Buffer.IsEmpty) 218 | { 219 | if (result.Buffer.IsSingleSegment) 220 | { 221 | await writer.WriteAsync(result.Buffer.First, cancellationToken).ConfigureAwait(false); 222 | } 223 | else 224 | { 225 | foreach (var segment in result.Buffer) 226 | { 227 | await writer.WriteAsync(segment, cancellationToken).ConfigureAwait(false); 228 | } 229 | } 230 | await writer.FlushAsync(cancellationToken).ConfigureAwait(false); 231 | 232 | } 233 | 234 | if (result.IsCompleted || result.IsCanceled) 235 | { 236 | await writer.CompleteAsync().ConfigureAwait(false); 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Chell/Internal/Which.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Chell.Internal 10 | { 11 | public static class Which 12 | { 13 | public static bool TryGetPath(string commandName, out string matchedPath) 14 | { 15 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 16 | var paths = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(isWindows ? ';' : ':'); 17 | var pathExts = Array.Empty(); 18 | 19 | if (isWindows) 20 | { 21 | paths = paths.Prepend(Environment.CurrentDirectory).ToArray(); 22 | pathExts = (Environment.GetEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC").Split(';'); 23 | } 24 | 25 | foreach (var path in paths) 26 | { 27 | // /path/to/foo.ext 28 | foreach (var ext in pathExts) 29 | { 30 | var fullPath = Path.Combine(path, $"{commandName}{ext}"); 31 | if (File.Exists(fullPath)) 32 | { 33 | matchedPath = fullPath; 34 | return true; 35 | } 36 | } 37 | 38 | // /path/to/foo 39 | { 40 | var fullPath = Path.Combine(path, commandName); 41 | if (File.Exists(fullPath)) 42 | { 43 | matchedPath = fullPath; 44 | return true; 45 | } 46 | } 47 | } 48 | 49 | matchedPath = string.Empty; 50 | return false; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Chell/ProcessOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Text; 6 | using Chell.Internal; 7 | 8 | namespace Chell 9 | { 10 | /// 11 | /// Provides the outputs and results from the process. 12 | /// 13 | /// 14 | /// If the output is redirected or piped, it will not be captured. 15 | /// 16 | public class ProcessOutput : IEnumerable 17 | { 18 | /// 19 | /// Gets the standard outputs as . 20 | /// 21 | public string Output => Sink.Output; 22 | 23 | /// 24 | /// Gets the standard errors as . 25 | /// 26 | public string Error => Sink.Error; 27 | 28 | /// 29 | /// Gets the standard outputs and standard errors as . 30 | /// 31 | public string Combined => Sink.Combined; 32 | 33 | /// 34 | /// Gets the standard outputs as sequence. 35 | /// 36 | public ReadOnlyMemory OutputBinary => Sink.OutputBinary; 37 | 38 | /// 39 | /// Gets the standard errors as sequence. 40 | /// 41 | public ReadOnlyMemory ErrorBinary => Sink.ErrorBinary; 42 | 43 | /// 44 | /// Gets the standard outputs and standard errors as sequence. 45 | /// 46 | public ReadOnlyMemory CombinedBinary => Sink.CombinedBinary; 47 | 48 | /// 49 | /// Get the exit code when the process terminated. 50 | /// 51 | public int ExitCode { get; internal set; } 52 | 53 | internal OutputSink Sink { get; } 54 | 55 | public ProcessOutput(Encoding encoding) 56 | { 57 | Sink = new OutputSink(encoding); 58 | } 59 | 60 | public static implicit operator string(ProcessOutput processOutput) 61 | => processOutput.ToString(); 62 | 63 | /// 64 | /// Gets the standard outputs and standard errors as . 65 | /// 66 | public override string ToString() => Combined; 67 | 68 | public IEnumerator GetEnumerator() 69 | => AsLines().GetEnumerator(); 70 | 71 | IEnumerator IEnumerable.GetEnumerator() 72 | => GetEnumerator(); 73 | 74 | /// 75 | /// Gets the standard outputs and standard errors as lines. 76 | /// 77 | public IEnumerable AsLines(bool trimEnd = false) 78 | => (trimEnd ? Combined.TrimEnd('\n', '\r') : Combined).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Chell/ProcessTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.ExceptionServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Chell.Internal; 8 | 9 | namespace Chell 10 | { 11 | /// 12 | /// Represents the execution task of a process. 13 | /// 14 | public class ProcessTask 15 | { 16 | private static readonly TimeSpan ProcessStartScheduledDelay = TimeSpan.FromMilliseconds(250); 17 | private static int _idSequence = 0; 18 | 19 | private readonly int _id; 20 | private readonly DateTimeOffset _startedAt; 21 | private readonly Lazy> _taskLazy; 22 | private readonly ProcessOutput _output; 23 | private readonly ProcessTaskOptions _options; 24 | private readonly CancellationTokenSource _processCancellation; 25 | private readonly CancellationTokenRegistration _processCancellationRegistration; 26 | 27 | private readonly object _syncLock = new object(); 28 | private string? _processName; 29 | private Process? _process; 30 | private ExceptionDispatchInfo? _processException; 31 | 32 | private Stream? _stdInStream; 33 | private StreamPipe? _stdOutPipe; 34 | private StreamPipe? _stdErrorPipe; 35 | private bool _piped; 36 | private bool _hasStandardIn; 37 | private bool _suppressPipeToConsole; 38 | private bool _suppressExceptionExitCodeNonZero; 39 | 40 | /// 41 | /// Gets the process of the task. If the process is not started yet, the property returns null. 42 | /// 43 | public Process? Process => _process; 44 | 45 | /// 46 | /// Gets the command line string of the task. 47 | /// 48 | public string CommandLine { get; } 49 | 50 | /// 51 | /// Gets the command name of the task. 52 | /// 53 | /// 54 | /// This value is passed to . 55 | /// 56 | public string Command { get; } 57 | 58 | /// 59 | /// Gets the arguments of the task. 60 | /// 61 | /// 62 | /// The value is passed to . It may be escaped or quoted. 63 | /// 64 | public string Arguments { get; } 65 | 66 | /// 67 | /// Gets the previous task of the task if piped. 68 | /// 69 | public ProcessTask? PreviousTask { get; private set; } 70 | 71 | public ProcessTask(FormattableString commandLine, ProcessTaskOptions? options = default) 72 | : this(default(Stream?), commandLine, options) 73 | { } 74 | public ProcessTask(Stream? inputStream, FormattableString commandLine, ProcessTaskOptions? options = default) 75 | : this(inputStream, CommandLineHelper.Expand(commandLine, options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) 76 | { } 77 | public ProcessTask(ReadOnlyMemory inputData, FormattableString commandLine, ProcessTaskOptions? options = default) 78 | : this(new MemoryStream(inputData.ToArray()), CommandLineHelper.Expand(commandLine, options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) 79 | { } 80 | 81 | // NOTE: The overload of `string commandLine` cannot be made public due to restrictions. 82 | public ProcessTask(CommandLineString commandLine, ProcessTaskOptions? options = default) 83 | : this(default(Stream?), commandLine, options) 84 | { } 85 | public ProcessTask(Stream? inputStream, CommandLineString commandLine, ProcessTaskOptions? options = default) 86 | : this(inputStream, commandLine.StringValue ?? CommandLineHelper.Expand(commandLine.FormattableStringValue ?? throw new InvalidOperationException("The command line string cannot be null."), options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) 87 | { } 88 | public ProcessTask(ReadOnlyMemory inputData, CommandLineString commandLine, ProcessTaskOptions? options = default) 89 | : this(new MemoryStream(inputData.ToArray()), commandLine.StringValue ?? CommandLineHelper.Expand(commandLine.FormattableStringValue ?? throw new InvalidOperationException("The command line string cannot be null."), options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) 90 | { } 91 | private ProcessTask(Stream? inputStream, string commandLine, ProcessTaskOptions? options) 92 | : this(inputStream, commandLine, (options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor).GetCommandAndArguments(commandLine), options) 93 | { } 94 | 95 | public ProcessTask(string command, string arguments, ProcessTaskOptions? options = default) 96 | : this(default(Stream?), command, arguments, options) 97 | { } 98 | public ProcessTask(Stream? inputStream, string command, string arguments, ProcessTaskOptions? options = default) 99 | : this(inputStream, $"{command} {arguments}", (command, arguments), options) 100 | { } 101 | public ProcessTask(ReadOnlyMemory inputData, string command, string arguments, ProcessTaskOptions? options = default) 102 | : this(new MemoryStream(inputData.ToArray()), $"{command} {arguments}", (command, arguments), options) 103 | { } 104 | 105 | private ProcessTask(Stream? inputStream, string commandLine, (string Command, string Arguments) commandAndArguments, ProcessTaskOptions? options = default) 106 | { 107 | _startedAt = DateTimeOffset.Now; 108 | _options = options ?? new ProcessTaskOptions(); 109 | _id = Interlocked.Increment(ref _idSequence); 110 | _processCancellation = new CancellationTokenSource(); 111 | _processCancellationRegistration = _processCancellation.Token.Register(() => 112 | { 113 | WriteDebugTrace($"ProcessTimedOut: Pid={_process?.Id}; StartedAt={_startedAt}; Elapsed={DateTimeOffset.Now - _startedAt}"); 114 | #if NET5_0_OR_GREATER || NETCOREAPP3_0 || NETCOREAPP3_1 115 | _process?.Kill(true); 116 | #else 117 | // TODO: .NET Standard 2.0 or 2.1 does not support kill child processes. 118 | _process?.Kill(); 119 | #endif 120 | }); 121 | 122 | _output = new ProcessOutput(_options.ShellExecutor.Encoding); 123 | _taskLazy = new Lazy>(AsTaskCore, LazyThreadSafetyMode.ExecutionAndPublication); 124 | _suppressPipeToConsole = !_options.Verbosity.HasFlag(ChellVerbosity.ConsoleOutputs); 125 | 126 | CommandLine = commandLine ?? throw new ArgumentNullException(nameof(commandLine)); 127 | (Command, Arguments) = commandAndArguments; 128 | 129 | if (inputStream != null) 130 | { 131 | // Set the stdin stream and start a process immediately. 132 | ConnectStreamToStandardInput(inputStream); 133 | } 134 | else if (_options.RedirectStandardInput) 135 | { 136 | // Enable stdin redirection and start a process immediately. 137 | RedirectStandardInput(); 138 | } 139 | else 140 | { 141 | // Delay startup to allow time to configure the stdin stream. 142 | // If a Task is requested (e.g. `await`, `AsTask`, `Pipe` ...), it will be started immediately. 143 | _ = ScheduleStartProcessAsync(); 144 | } 145 | 146 | WriteDebugTrace($"Created: {commandLine}"); 147 | 148 | async Task ScheduleStartProcessAsync() 149 | { 150 | await Task.Delay(ProcessStartScheduledDelay).ConfigureAwait(false); 151 | EnsureProcess(); 152 | } 153 | } 154 | 155 | public static ProcessTask operator |(ProcessTask a, FormattableString b) 156 | => a.Pipe(new ProcessTask(b)); 157 | public static ProcessTask operator |(ProcessTask a, Stream b) 158 | => a.Pipe(b); 159 | public static ProcessTask operator |(ProcessTask a, ProcessTask b) 160 | => a.Pipe(b); 161 | 162 | public static ProcessTask operator |(Stream a, ProcessTask b) 163 | { 164 | b.ConnectStreamToStandardInput(a); 165 | return b; 166 | } 167 | public static ProcessTask operator |(ReadOnlyMemory a, ProcessTask b) 168 | { 169 | b.ConnectStreamToStandardInput(new MemoryStream(a.ToArray())); 170 | return b; 171 | } 172 | 173 | public static implicit operator Task(ProcessTask task) 174 | => task.AsTask(); 175 | public static implicit operator Task(ProcessTask task) 176 | => task.AsTask(); 177 | 178 | /// 179 | /// Gets the output of the process as Task. 180 | /// 181 | /// 182 | public async Task AsTask() 183 | { 184 | try 185 | { 186 | return await _taskLazy.Value.ConfigureAwait(false); 187 | } 188 | catch when (_suppressExceptionExitCodeNonZero) 189 | { 190 | return _output; 191 | } 192 | } 193 | 194 | public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() 195 | { 196 | return AsTask().GetAwaiter(); 197 | } 198 | 199 | /// 200 | /// Gets the exit code of the process as Task. 201 | /// 202 | public Task ExitCode 203 | { 204 | get 205 | { 206 | if (_taskLazy.Value.IsCompleted) 207 | { 208 | return Task.FromResult(_output.ExitCode); 209 | } 210 | 211 | return _taskLazy.Value.ContinueWith(x => _output.ExitCode); 212 | } 213 | } 214 | 215 | /// 216 | /// Configures the task to ignore the exception when the process returns exit code with non-zero. 217 | /// 218 | /// 219 | public ProcessTask NoThrow() 220 | { 221 | _suppressExceptionExitCodeNonZero = true; 222 | return this; 223 | } 224 | 225 | public override string ToString() 226 | { 227 | return PreviousTask != null ? $"{PreviousTask} | {CommandLine}" : $"{CommandLine}"; 228 | } 229 | 230 | /// 231 | /// Enables standard output redirection of the process. 232 | /// 233 | /// 234 | /// Redirecting standard input must be done before the process has started. You can also use a that is guaranteed to enable redirection while creating a . 235 | /// 236 | public void RedirectStandardInput(bool immediateLaunchProcess = true) 237 | { 238 | WriteDebugTrace($"RedirectStandardInput: immediateLaunchProcess={immediateLaunchProcess}"); 239 | lock (_syncLock) 240 | { 241 | if (_process != null) 242 | { 243 | throw new InvalidOperationException("The process has already been started. Redirecting standard input must be done before the process has started."); 244 | } 245 | 246 | _hasStandardIn = true; 247 | } 248 | 249 | if (immediateLaunchProcess) 250 | { 251 | EnsureProcess(); 252 | } 253 | } 254 | 255 | /// 256 | /// Connects a stream to the standard input of the process. 257 | /// 258 | /// 259 | /// Connecting standard input must be done before the process has started. You can also use a constructor argument that is guaranteed to receive a stream. 260 | /// 261 | /// 262 | public void ConnectStreamToStandardInput(Stream stream) 263 | { 264 | lock (_syncLock) 265 | { 266 | if (_stdInStream != null) throw new InvalidOperationException("The standard input has already connected to the process."); 267 | _stdInStream = stream ?? throw new ArgumentNullException(nameof(stream)); 268 | } 269 | 270 | RedirectStandardInput(); 271 | } 272 | 273 | /// 274 | /// Suppresses output to the console if the standard output of the process is not configured. 275 | /// 276 | /// 277 | public ProcessTask SuppressConsoleOutputs() 278 | { 279 | _suppressPipeToConsole = true; 280 | return this; 281 | } 282 | 283 | /// 284 | /// Pipes the standard output to the stream. 285 | /// 286 | /// 287 | /// 288 | public ProcessTask Pipe(Stream stream) 289 | { 290 | EnsureProcess(); 291 | 292 | if (_stdOutPipe != null && _stdErrorPipe != null) 293 | { 294 | _stdOutPipe.Connect(stream); 295 | } 296 | 297 | _piped = true; 298 | 299 | ReadyPipe(); 300 | return this; 301 | } 302 | 303 | /// 304 | /// Pipes the standard output to the another process. 305 | /// 306 | /// 307 | /// 308 | public ProcessTask Pipe(ProcessTask nextProcess) 309 | { 310 | // First, enable standard input redirection before starting a process for the next ProcessTask. 311 | nextProcess.RedirectStandardInput(immediateLaunchProcess: false); 312 | 313 | // Second, start a process. 314 | EnsureProcess(); 315 | 316 | // Third, start a process for the next ProcessTask. 317 | nextProcess.EnsureProcess(); 318 | 319 | if (_stdOutPipe != null && _stdErrorPipe != null) 320 | { 321 | if (nextProcess.Process != null) 322 | { 323 | WriteDebugTrace($"Pipe: {Process!.Id} -> {nextProcess.Process.Id}"); 324 | _stdOutPipe.Connect(nextProcess.Process.StandardInput.BaseStream ?? Stream.Null); 325 | } 326 | } 327 | 328 | nextProcess.PreviousTask = this; 329 | _piped = true; 330 | 331 | ReadyPipe(); 332 | return nextProcess; 333 | } 334 | 335 | private void EnsureProcess() 336 | { 337 | lock (_syncLock) 338 | { 339 | if (_process is null && _processException is null) 340 | { 341 | StartProcess(); 342 | } 343 | } 344 | } 345 | 346 | private void StartProcess() 347 | { 348 | Debug.Assert(_process is null); 349 | Debug.Assert(_processException is null); 350 | 351 | // Enable only when stdin is redirected or has input stream. 352 | // If RedirectStandardInput or CreateNoWindow is set to 'true', a process will not be interactive. 353 | var procStartInfo = new ProcessStartInfo 354 | { 355 | FileName = Command, 356 | Arguments = Arguments, 357 | UseShellExecute = false, 358 | CreateNoWindow = _options.Console.IsInputRedirected, 359 | RedirectStandardOutput = true, 360 | RedirectStandardInput = _options.Console.IsInputRedirected || _hasStandardIn, 361 | RedirectStandardError = true, 362 | WorkingDirectory = _options.WorkingDirectory ?? string.Empty, 363 | }; 364 | 365 | if (_options.Verbosity.HasFlag(ChellVerbosity.CommandLine)) 366 | { 367 | CommandLineHelper.WriteCommandLineToConsole(_options.Console, CommandLine, _options.Verbosity); 368 | } 369 | 370 | try 371 | { 372 | _processName = procStartInfo.FileName; 373 | _process = Process.Start(procStartInfo)!; 374 | WriteDebugTrace($"Process.Start: Pid={_process.Id}; HasStandardIn={_hasStandardIn}; StandardIn={_stdInStream}; IsInputRedirected={Console.IsInputRedirected}"); 375 | 376 | // Set a timeout if the option has non-zero or max-value. 377 | if (_options.Timeout != TimeSpan.MaxValue && _options.Timeout != TimeSpan.Zero) 378 | { 379 | _processCancellation.CancelAfter(_options.Timeout); 380 | } 381 | 382 | // Connect the stream to the standard input. 383 | if (_stdInStream != null) 384 | { 385 | _ = CopyCoreAsync(_stdInStream, _process.StandardInput.BaseStream); 386 | } 387 | 388 | _stdOutPipe = new StreamPipe(Process?.StandardOutput.BaseStream ?? Stream.Null); 389 | _stdErrorPipe = new StreamPipe(Process?.StandardError.BaseStream ?? Stream.Null); 390 | } 391 | catch (Exception e) 392 | { 393 | _processException = ExceptionDispatchInfo.Capture(e); 394 | } 395 | 396 | static async Task CopyCoreAsync(Stream src, Stream dest) 397 | { 398 | await UnbufferedCopyToAsync(src, dest).ConfigureAwait(false); 399 | dest.Close(); 400 | } 401 | } 402 | 403 | private void ReadyPipe() 404 | { 405 | WriteDebugTrace($"ReadyPipe: Pid={Process?.Id}; Piped={_piped}; _stdOutPipe={_stdOutPipe}; _stdErrorPipe={_stdErrorPipe}"); 406 | 407 | if (!_piped) 408 | { 409 | if (!_suppressPipeToConsole) 410 | { 411 | _stdOutPipe?.Connect(_options.Console.OpenStandardOutput()); 412 | _stdErrorPipe?.Connect(_options.Console.OpenStandardError()); 413 | } 414 | _stdOutPipe?.Connect(_output.Sink.OutputWriter); 415 | _stdErrorPipe?.Connect(_output.Sink.ErrorWriter); 416 | } 417 | 418 | _stdOutPipe?.Ready(); 419 | _stdErrorPipe?.Ready(); 420 | } 421 | 422 | private static async Task UnbufferedCopyToAsync(Stream src, Stream dest, CancellationToken cancellationToken = default) 423 | { 424 | var buffer = new byte[80 * 1024]; 425 | while (true) 426 | { 427 | var read = await src.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); 428 | if (read == 0) 429 | { 430 | return; 431 | } 432 | 433 | await dest.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); 434 | await dest.FlushAsync(cancellationToken).ConfigureAwait(false); 435 | } 436 | } 437 | 438 | private async Task ThrowIfParentTaskHasThrownProcessException(bool awaitForComplete) 439 | { 440 | if (PreviousTask != null) 441 | { 442 | // First, throw an exception for the parent task. 443 | await PreviousTask.ThrowIfParentTaskHasThrownProcessException(awaitForComplete).ConfigureAwait(false); 444 | 445 | // Second, Start the process. 446 | PreviousTask.EnsureProcess(); 447 | 448 | // Third, If the process is failed to start, the task will be Faulted state immediately. We should throw an exception of prev task here. 449 | var t = PreviousTask.AsTask(); 450 | if (t.IsFaulted) 451 | { 452 | await t.ConfigureAwait(false); 453 | } 454 | 455 | if (!t.IsCompleted && awaitForComplete) 456 | { 457 | await t.ConfigureAwait(false); 458 | } 459 | } 460 | } 461 | 462 | private async Task AsTaskCore() 463 | { 464 | await ThrowIfParentTaskHasThrownProcessException(awaitForComplete:false).ConfigureAwait(false); 465 | 466 | EnsureProcess(); 467 | 468 | if (Process is {} proc) 469 | { 470 | if (_stdOutPipe is null || _stdErrorPipe is null) throw new InvalidOperationException(); 471 | 472 | // If we have no stdin stream and Console's StandardInput is redirected, connects them automatically. 473 | var connectStdInToPipe = _options.EnableAutoWireStandardInput && !_hasStandardIn && _options.Console.IsInputRedirected /*Non-Interactive*/; 474 | if (connectStdInToPipe) 475 | { 476 | StandardInput.Pipe.Connect(proc.StandardInput.BaseStream); 477 | StandardInput.Pipe.Ready(); 478 | } 479 | 480 | ReadyPipe(); 481 | 482 | 483 | try 484 | { 485 | #if NET5_0_OR_GREATER 486 | await proc.WaitForExitAsync().ConfigureAwait(false); 487 | #else 488 | await Task.Run(() => proc.WaitForExit()).ConfigureAwait(false); 489 | #endif 490 | } 491 | finally 492 | { 493 | _processCancellationRegistration.Dispose(); 494 | } 495 | 496 | _output.ExitCode = proc.ExitCode; 497 | 498 | WriteDebugTrace($"ProcessExited: Pid={proc.Id}; ExitCode={proc.ExitCode}"); 499 | 500 | if (connectStdInToPipe) 501 | { 502 | StandardInput.Pipe.Disconnect(proc.StandardInput.BaseStream); 503 | } 504 | 505 | // Flush output streams/pipes 506 | WriteDebugTrace($"Pipe/Sink.CompleteAsync: Pid={proc.Id}"); 507 | await _stdOutPipe.CompleteAsync().ConfigureAwait(false); 508 | await _stdErrorPipe.CompleteAsync().ConfigureAwait(false); 509 | await _output.Sink.CompleteAsync().ConfigureAwait(false); 510 | WriteDebugTrace($"Pipe/Sink.CompleteAsync:Done: Pid={proc.Id}"); 511 | 512 | await ThrowIfParentTaskHasThrownProcessException(awaitForComplete: true).ConfigureAwait(false); 513 | 514 | if (_output.ExitCode != 0) 515 | { 516 | var ex = new ProcessTaskException(_processName ?? "Unknown", proc.Id, this, _output); 517 | if (_processCancellation.IsCancellationRequested) 518 | { 519 | throw new OperationCanceledException($"The process has reached the timeout. (Timeout: {_options.Timeout})", ex); 520 | } 521 | else 522 | { 523 | throw ex; 524 | } 525 | } 526 | } 527 | else 528 | { 529 | _output.ExitCode = 127; 530 | _processCancellationRegistration.Dispose(); 531 | if (_processException != null) 532 | { 533 | throw new ProcessTaskException(this, _output, _processException.SourceException); 534 | } 535 | else 536 | { 537 | throw new ProcessTaskException(this, _output); 538 | } 539 | } 540 | return _output; 541 | } 542 | 543 | [Conditional("DEBUG")] 544 | private void WriteDebugTrace(string s) 545 | { 546 | if (_options.Verbosity.HasFlag(ChellVerbosity.Debug)) 547 | { 548 | _options.Console.Out.WriteLine($"[DEBUG][{_id}] {s}"); 549 | } 550 | } 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/Chell/ProcessTaskException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Chell 4 | { 5 | /// 6 | /// Represents an error that occurs during process execution. 7 | /// 8 | public class ProcessTaskException : Exception 9 | { 10 | public ProcessTask ProcessTask { get; } 11 | public ProcessOutput Output { get; } 12 | 13 | public ProcessTaskException(string processName, int processId, ProcessTask processTask, ProcessOutput output, Exception? innerException = default) 14 | : base($"Process '{processName}' ({processId}) has exited with exit code {output.ExitCode}. (Executed command: {processTask.Command} {processTask.Arguments})", innerException) 15 | { 16 | ProcessTask = processTask; 17 | Output = output; 18 | } 19 | 20 | public ProcessTaskException(ProcessTask processTask, ProcessOutput output, Exception? innerException = default) 21 | : base($"Failed to start the process. (Executed command: {processTask.Command} {processTask.Arguments})", innerException) 22 | { 23 | ProcessTask = processTask; 24 | Output = output; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Chell/ProcessTaskOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Chell.IO; 3 | using Chell.Shell; 4 | 5 | namespace Chell 6 | { 7 | public class ProcessTaskOptions 8 | { 9 | /// 10 | /// Gets or sets whether to enable automatic wiring of standard input to the process. The default value is true. 11 | /// 12 | public bool EnableAutoWireStandardInput { get; set; } 13 | 14 | /// 15 | /// Gets or sets to enable standard input redirection. The default value is false. 16 | /// 17 | public bool RedirectStandardInput { get; set; } 18 | 19 | /// 20 | /// Gets or sets the shell executor. The default value is ChellEnvironment.Current.Shell.Executor. 21 | /// 22 | public IShellExecutor ShellExecutor { get; set; } 23 | 24 | /// 25 | /// Gets or sets the console provider. The default value is ChellEnvironment.Current.Console. 26 | /// 27 | public IConsoleProvider Console { get; set; } 28 | 29 | /// 30 | /// Gets or sets the verbosity. The default value is ChellEnvironment.Current.Verbosity. 31 | /// 32 | public ChellVerbosity Verbosity { get; set; } 33 | 34 | /// 35 | /// Gets or sets the working directory for the process. 36 | /// 37 | public string? WorkingDirectory { get; set; } 38 | 39 | /// 40 | /// Gets or sets the duration to timeout the process. The default value is ChellEnvironment.Current.ProcessTimeout. 41 | /// 42 | /// 43 | /// If the value is or , the process will not be timed out. 44 | /// 45 | public TimeSpan Timeout { get; set; } 46 | 47 | public ProcessTaskOptions( 48 | bool? redirectStandardInput = default, 49 | bool? enableAutoWireStandardInput = default, 50 | ChellVerbosity? verbosity = default, 51 | IShellExecutor? shellExecutor = default, 52 | IConsoleProvider? console = default, 53 | string? workingDirectory = default, 54 | TimeSpan? timeout = default 55 | ) 56 | { 57 | RedirectStandardInput = redirectStandardInput ?? false; 58 | EnableAutoWireStandardInput = enableAutoWireStandardInput ?? true; 59 | ShellExecutor = shellExecutor ?? ChellEnvironment.Current.Shell.Executor; 60 | Console = console ?? ChellEnvironment.Current.Console; 61 | Verbosity = verbosity ?? ChellEnvironment.Current.Verbosity; 62 | WorkingDirectory = workingDirectory ?? workingDirectory; 63 | Timeout = timeout ?? ChellEnvironment.Current.ProcessTimeout; 64 | } 65 | 66 | private ProcessTaskOptions(ProcessTaskOptions orig) 67 | { 68 | RedirectStandardInput = orig.RedirectStandardInput; 69 | EnableAutoWireStandardInput = orig.EnableAutoWireStandardInput; 70 | ShellExecutor = orig.ShellExecutor; 71 | Console = orig.Console; 72 | Verbosity = orig.Verbosity; 73 | WorkingDirectory = orig.WorkingDirectory; 74 | Timeout = orig.Timeout; 75 | } 76 | 77 | public ProcessTaskOptions WithRedirectStandardInput(bool redirectStandardInput) 78 | => new ProcessTaskOptions(this) { RedirectStandardInput = redirectStandardInput }; 79 | public ProcessTaskOptions WithEnableAutoWireStandardInput(bool enableAutoWireStandardInput) 80 | => new ProcessTaskOptions(this) { EnableAutoWireStandardInput = enableAutoWireStandardInput }; 81 | public ProcessTaskOptions WithShellExecutor(IShellExecutor shellExecutor) 82 | => new ProcessTaskOptions(this) { ShellExecutor = shellExecutor }; 83 | public ProcessTaskOptions WithVerbosity(ChellVerbosity verbosity) 84 | => new ProcessTaskOptions(this) { Verbosity = verbosity }; 85 | public ProcessTaskOptions WithWorkingDirectory(string? workingDirectory) 86 | => new ProcessTaskOptions(this) { WorkingDirectory = workingDirectory }; 87 | public ProcessTaskOptions WithTimeout(TimeSpan timeout) 88 | => new ProcessTaskOptions(this) { Timeout = timeout }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Chell/Run.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Chell.Shell; 4 | 5 | namespace Chell 6 | { 7 | /// 8 | /// Short cut for to launch a process from a or . 9 | /// 10 | public class Run : ProcessTask 11 | { 12 | public static implicit operator Run(FormattableString commandLine) 13 | => new Run(commandLine); 14 | public static implicit operator Run(CommandLineString commandLine) 15 | => new Run(commandLine); 16 | 17 | /// 18 | /// Launches a process from a . 19 | /// 20 | /// 21 | public Run(CommandLineString commandLine) : base(commandLine) { } 22 | 23 | /// 24 | /// Launches a process from a . 25 | /// 26 | /// 27 | /// The interpolated string will be escaped and the array will be expanded. 28 | /// 29 | public Run(FormattableString commandLine) : base(commandLine) { } 30 | 31 | /// 32 | /// Launches a process from a and connects the specified to the standard input. 33 | /// 34 | public Run(Stream inputStream, CommandLineString commandLine) : base(inputStream, commandLine) { } 35 | 36 | /// 37 | /// Launches a process from a and connects the specified to the standard input. 38 | /// 39 | /// 40 | /// The interpolated string will be escaped and the array will be expanded. 41 | /// 42 | /// 43 | /// 44 | public Run(Stream inputStream, FormattableString commandLine) : base(inputStream, commandLine) { } 45 | 46 | /// 47 | /// Launches a process from a and writes the specified binary data to the standard input. 48 | /// 49 | /// 50 | /// 51 | public Run(ReadOnlyMemory inputData, CommandLineString commandLine) : base(new MemoryStream(inputData.ToArray()), commandLine) { } 52 | 53 | /// 54 | /// Launches a process from a and writes the specified binary data to the standard input. 55 | /// 56 | /// 57 | /// The interpolated string will be escaped and the array will be expanded. 58 | /// 59 | /// 60 | /// 61 | public Run(ReadOnlyMemory inputData, FormattableString commandLine) : base(new MemoryStream(inputData.ToArray()), commandLine) { } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Chell/Shell/BashShellExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using Chell.Internal; 6 | 7 | namespace Chell.Shell 8 | { 9 | public class BashShellExecutor : IShellExecutor 10 | { 11 | public static string? AutoDetectedPath { get; set; } 12 | 13 | public string? Path { get; set; } = AutoDetectedPath; 14 | 15 | public Encoding Encoding => Encoding.UTF8; 16 | 17 | public string Prefix { get; set; } 18 | 19 | public (string Command, string Arguments) GetCommandAndArguments(string commandLine) 20 | => (Path ?? throw new FileNotFoundException("Bash is not found in the PATH."), $"-c \"{Prefix}{commandLine}\""); 21 | 22 | // https://unix.stackexchange.com/questions/187651/how-to-echo-single-quote-when-using-single-quote-to-wrap-special-characters-in 23 | public string Escape(string value) 24 | { 25 | if (string.IsNullOrEmpty(value)) 26 | { 27 | return string.Empty; 28 | } 29 | if (Regex.IsMatch(value, "^[a-zA-Z0-9_.-/]+$")) 30 | { 31 | return value; 32 | } 33 | return $"$'{value.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\"", "\\\"")}'"; 34 | } 35 | 36 | public BashShellExecutor(string? prefix = null) 37 | { 38 | Prefix = prefix ?? "set -euo pipefail;"; 39 | } 40 | 41 | static BashShellExecutor() 42 | { 43 | if (Which.TryGetPath("bash", out var bashPath)) 44 | { 45 | AutoDetectedPath = bashPath; 46 | } 47 | else 48 | { 49 | AutoDetectedPath = null; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Chell/Shell/CmdShellExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Chell.Shell 5 | { 6 | public class CmdShellExecutor : IShellExecutor 7 | { 8 | public Encoding Encoding => Encoding.UTF8; 9 | 10 | public (string Command, string Arguments) GetCommandAndArguments(string commandLine) 11 | => ("cmd", $"/c \"{commandLine}\""); 12 | 13 | public string Escape(string value) 14 | { 15 | if (string.IsNullOrEmpty(value)) 16 | { 17 | return string.Empty; 18 | } 19 | if (Regex.IsMatch(value, "^[a-zA-Z0-9_.-/\\\\]+$")) 20 | { 21 | return value; 22 | } 23 | 24 | value = Regex.Replace(value, "([<>|&^])", "^$1"); 25 | value = Regex.Replace(value, "(\\\\)?\"", x => x.Groups[1].Success ? "\\\\\\\"" : "\\\""); 26 | 27 | return $"\"{value}\""; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Chell/Shell/IShellExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chell.Shell 8 | { 9 | public interface IShellExecutor 10 | { 11 | Encoding Encoding { get; } 12 | (string Command, string Arguments) GetCommandAndArguments(string commandLine); 13 | string Escape(string value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Chell/Shell/NoUseShellExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | using Chell.Internal; 4 | 5 | namespace Chell.Shell 6 | { 7 | public class NoUseShellExecutor : IShellExecutor 8 | { 9 | public Encoding Encoding => Encoding.UTF8; 10 | 11 | public (string Command, string Arguments) GetCommandAndArguments(string commandLine) 12 | { 13 | return CommandLineHelper.Parse(commandLine); 14 | } 15 | 16 | public string Escape(string value) 17 | => Regex.IsMatch(value, "^[a-zA-Z0-9_.-/]+$") 18 | ? value 19 | : $"\"{value.Replace("`", "``").Replace("\"", "`\"")}\""; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Chell/Shell/ShellExecutorProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Chell.Shell; 4 | 5 | namespace Chell.Shell 6 | { 7 | public class ShellExecutorProvider 8 | { 9 | public IShellExecutor Executor { get; private set; } = GetPlatformPreferredExecutor(); 10 | 11 | public void SetExecutor(IShellExecutor shellExecutor) 12 | { 13 | Executor = shellExecutor ?? throw new ArgumentNullException(nameof(shellExecutor)); 14 | } 15 | 16 | internal static IShellExecutor GetPlatformPreferredExecutor() 17 | => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 18 | ? new CmdShellExecutor() 19 | : BashShellExecutor.AutoDetectedPath != null 20 | ? new BashShellExecutor() 21 | : new NoUseShellExecutor(); 22 | } 23 | } 24 | 25 | namespace Chell 26 | { 27 | public static class ShellExecutorProviderExtensions 28 | { 29 | public static void NoUseShell(this ShellExecutorProvider provider) 30 | { 31 | provider.SetExecutor(new NoUseShellExecutor()); 32 | } 33 | public static void UseBash(this ShellExecutorProvider provider, string? prefix = null) 34 | { 35 | provider.SetExecutor(new BashShellExecutor(prefix)); 36 | } 37 | public static void UseCmd(this ShellExecutorProvider provider) 38 | { 39 | provider.SetExecutor(new CmdShellExecutor()); 40 | } 41 | public static void UseDefault(this ShellExecutorProvider provider) 42 | { 43 | provider.SetExecutor(ShellExecutorProvider.GetPlatformPreferredExecutor()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Chell.Tests/Chell.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/Chell.Tests/ChellEnvironmentTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace Chell.Tests 11 | { 12 | public class ChellEnvironmentTest 13 | { 14 | [Fact] 15 | public void HomeDirectory() 16 | { 17 | var env = new ChellEnvironment(); 18 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 19 | { 20 | env.HomeDirectory.Should().Be(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); 21 | } 22 | else 23 | { 24 | env.HomeDirectory.Should().Be(Environment.GetEnvironmentVariable("HOME")); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Chell.Tests/CommandLineStringTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Xunit; 8 | 9 | namespace Chell.Tests 10 | { 11 | public class CommandLineStringTest 12 | { 13 | [Fact] 14 | public void StaticMethodPreferString() 15 | { 16 | StaticMethodOverloadedTest.A("Foo {0}").Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); 17 | } 18 | [Fact] 19 | public void StaticMethodPreferFormattableString() 20 | { 21 | StaticMethodOverloadedTest.A($"Foo {0}").Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); 22 | } 23 | 24 | private static class StaticMethodOverloadedTest 25 | { 26 | public static string A(CommandLineString s) => $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; 27 | public static string A(FormattableString s) => $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; 28 | } 29 | 30 | [Fact] 31 | public void InstanceMethodPreferString() 32 | { 33 | new InstanceMethodOverloadedTest().A("Foo {0}").Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); 34 | } 35 | [Fact] 36 | public void InstanceMethodPreferFormattableString() 37 | { 38 | new InstanceMethodOverloadedTest().A($"Foo {0}").Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); 39 | } 40 | 41 | private class InstanceMethodOverloadedTest 42 | { 43 | public string A(CommandLineString s) => $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; 44 | public string A(FormattableString s) => $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; 45 | } 46 | 47 | [Fact] 48 | public void ImplicitCastConstructorPreferString() 49 | { 50 | new ConstructorOverloadedTest("Foo {0}").Result.Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); 51 | } 52 | [Fact] 53 | public void ImplicitCastConstructorPreferFormattableString() 54 | { 55 | new ConstructorOverloadedTest($"Foo {0}").Result.Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); 56 | } 57 | 58 | private class ConstructorOverloadedTest 59 | { 60 | public string Result { get; } 61 | 62 | public ConstructorOverloadedTest(CommandLineString s) 63 | { 64 | Result = $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; 65 | } 66 | 67 | public ConstructorOverloadedTest(FormattableString s) 68 | { 69 | Result = $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Chell.Tests/ProcessTaskTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Pipelines; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Chell.IO; 11 | using Chell.Shell; 12 | using FluentAssertions; 13 | using Kokuban; 14 | using Xunit; 15 | 16 | namespace Chell.Tests 17 | { 18 | internal static class ProcessTaskTestFixtureExtensions 19 | { 20 | public static TemporaryAppBuilder.Compilation AddTo(this TemporaryAppBuilder.Compilation compilation, 21 | IList disposables) 22 | { 23 | disposables.Add(compilation); 24 | return compilation; 25 | } 26 | } 27 | 28 | public class ProcessTaskTestFixture : IDisposable 29 | { 30 | private readonly TemporaryAppSolutionBuilder _slnBuilder = new TemporaryAppSolutionBuilder(); 31 | public string EchoArg { get; } 32 | public string EchoOutAndErrorArgs { get; } 33 | public string HelloWorld { get; } 34 | public string ExitCodeNonZero { get; } 35 | public string ExitCodeNonZeroWaited { get; } 36 | public string WriteCommandLineArgs { get; } 37 | public string StandardInputPassThroughText { get; } 38 | public string StandardInputPassThroughBinary { get; } 39 | public string WriteSleepWriteExit { get; } 40 | public string ReadOnce { get; } 41 | public string ReadAllLines { get; } 42 | public string WriteCurrentDirectory { get; } 43 | public string Never { get; } 44 | 45 | public ProcessTaskTestFixture() 46 | { 47 | EchoArg = _slnBuilder.CreateProject(nameof(EchoArg), builder => 48 | builder.WriteSourceFile("Program.cs", @" 49 | using System; 50 | Console.WriteLine(""["" + Environment.GetCommandLineArgs()[1] + ""]""); 51 | ")); 52 | EchoOutAndErrorArgs = _slnBuilder.CreateProject(nameof(EchoOutAndErrorArgs), builder => 53 | builder.WriteSourceFile("Program.cs", @" 54 | using System; 55 | using System.Threading.Tasks; 56 | Console.Out.WriteLine(""["" + Environment.GetCommandLineArgs()[1] + ""]""); 57 | await Task.Delay(100); 58 | Console.Error.WriteLine(""["" + Environment.GetCommandLineArgs()[2] + ""]""); 59 | await Task.Delay(100); 60 | Console.Out.WriteLine(""["" + Environment.GetCommandLineArgs()[3] + ""]""); 61 | await Task.Delay(100); 62 | Console.Error.WriteLine(""["" + Environment.GetCommandLineArgs()[4] + ""]""); 63 | ")); 64 | HelloWorld = _slnBuilder.CreateProject(nameof(HelloWorld), builder => 65 | builder.WriteSourceFile("Program.cs", @" 66 | using System; 67 | Console.WriteLine(""Hello World!""); 68 | ")); 69 | ExitCodeNonZero = _slnBuilder.CreateProject(nameof(ExitCodeNonZero), builder => 70 | builder.WriteSourceFile("Program.cs", @" 71 | using System; 72 | Environment.ExitCode = 192; 73 | ")); 74 | ExitCodeNonZeroWaited = _slnBuilder.CreateProject(nameof(ExitCodeNonZeroWaited), builder => 75 | builder.WriteSourceFile("Program.cs", @" 76 | using System; 77 | using System.Threading.Tasks; 78 | await Task.Delay(100); 79 | Environment.ExitCode = 192; 80 | ")); 81 | WriteCommandLineArgs = _slnBuilder.CreateProject(nameof(WriteCommandLineArgs), builder => 82 | builder.WriteSourceFile("Program.cs", @" 83 | using System; 84 | foreach (var line in Environment.GetCommandLineArgs()) 85 | { 86 | Console.WriteLine(line); 87 | } 88 | ")); 89 | StandardInputPassThroughText = _slnBuilder.CreateProject(nameof(StandardInputPassThroughText), builder => 90 | builder.WriteSourceFile("Program.cs", @" 91 | using System; 92 | Console.InputEncoding = System.Text.Encoding.UTF8; 93 | Console.OutputEncoding = System.Text.Encoding.UTF8; 94 | Console.Write(Console.In.ReadToEnd()); 95 | ")); 96 | StandardInputPassThroughBinary = _slnBuilder.CreateProject(nameof(StandardInputPassThroughBinary), builder => 97 | builder.WriteSourceFile("Program.cs", @" 98 | using System; 99 | Console.OpenStandardInput().CopyTo(Console.OpenStandardOutput()); 100 | ")); 101 | WriteSleepWriteExit = _slnBuilder.CreateProject(nameof(WriteSleepWriteExit), builder => 102 | builder.WriteSourceFile("Program.cs", @" 103 | using System; 104 | using System.Threading; 105 | Console.WriteLine(""Hello""); 106 | Thread.Sleep(1000); 107 | Console.WriteLine(""Hello""); 108 | ")); 109 | ReadOnce = _slnBuilder.CreateProject(nameof(ReadOnce), builder => 110 | builder.WriteSourceFile("Program.cs", @" 111 | using System; 112 | Console.WriteLine(Console.ReadLine()); 113 | ")); 114 | ReadAllLines = _slnBuilder.CreateProject(nameof(ReadAllLines), builder => 115 | builder.WriteSourceFile("Program.cs", @" 116 | using System; 117 | while (true) 118 | { 119 | var line = Console.ReadLine(); 120 | if (line == null) return; 121 | Console.WriteLine(line); 122 | } 123 | ")); 124 | WriteCurrentDirectory = _slnBuilder.CreateProject(nameof(WriteCurrentDirectory), builder => 125 | builder.WriteSourceFile("Program.cs", @" 126 | using System; 127 | Console.WriteLine(Environment.CurrentDirectory); 128 | ")); 129 | Never = _slnBuilder.CreateProject(nameof(Never), builder => 130 | builder.WriteSourceFile("Program.cs", @" 131 | using System; 132 | using System.Threading; 133 | Console.WriteLine(""Hello""); 134 | while (true) { Thread.Sleep(1000); } 135 | ")); 136 | _slnBuilder.Build(); 137 | } 138 | 139 | public void Dispose() 140 | { 141 | _slnBuilder.Dispose(); 142 | } 143 | } 144 | 145 | [Collection("ProcessTaskTest")] // NOTE: Test cases use `Console` and does not run in parallel. 146 | public class ProcessTaskTest : IClassFixture 147 | { 148 | private readonly ProcessTaskTestFixture _fixture; 149 | 150 | public ProcessTaskTest(ProcessTaskTestFixture fixture) 151 | { 152 | _fixture = fixture; 153 | } 154 | 155 | private async Task<(string StandardOut, string StandardError)> RunAsync(Func func) 156 | { 157 | var fakeConsole = new FakeConsoleProvider(); 158 | 159 | await func(fakeConsole); 160 | 161 | return (fakeConsole.GetStandardOutputAsString(), fakeConsole.GetStandardErrorAsString()); 162 | } 163 | 164 | [Fact] 165 | public async Task CommandNotFound_UseShell() 166 | { 167 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 168 | var dummyCommandName = Guid.NewGuid().ToString(); 169 | var procTask = new ProcessTask($"{dummyCommandName} --help"); 170 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 171 | { 172 | (await procTask.ExitCode).Should().Be(1); // A shell (cmd) will return 1. 173 | } 174 | else 175 | { 176 | (await procTask.ExitCode).Should().Be(127); // A shell (bash) will return 127. 177 | } 178 | } 179 | 180 | [Fact] 181 | public async Task CommandNotFound_NoUseShell() 182 | { 183 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 184 | var dummyCommandName = Guid.NewGuid().ToString(); 185 | var procTask = new ProcessTask($"{dummyCommandName} --help", new ProcessTaskOptions(shellExecutor: new NoUseShellExecutor())); 186 | (await procTask.ExitCode).Should().Be(127); // System.Diagnostics.Process will return 127. 187 | } 188 | 189 | [Fact] 190 | public async Task Execute() 191 | { 192 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 193 | var procTask = new ProcessTask($"{_fixture.HelloWorld}"); 194 | (await procTask.ExitCode).Should().Be(0); 195 | 196 | var result = await procTask; 197 | result.ExitCode.Should().Be(0); 198 | result.Combined.Should().Be("Hello World!" + Environment.NewLine); 199 | result.Output.Should().Be("Hello World!" + Environment.NewLine); 200 | result.Error.Should().BeEmpty(); 201 | } 202 | 203 | [Fact] 204 | public async Task ProcessOutputInArgumentShouldBeTrimmed() 205 | { 206 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 207 | var procTask1 = new ProcessTask($"{_fixture.HelloWorld}"); 208 | var result1 = await procTask1; 209 | var procTask2 = new ProcessTask($"{_fixture.EchoArg} {result1}"); 210 | var result2 = await procTask2; 211 | 212 | result1.Combined.Should().Be("Hello World!" + Environment.NewLine); 213 | result2.Combined.Should().Be("[Hello World!]" + Environment.NewLine); 214 | } 215 | 216 | [Fact] 217 | public async Task ExpandArguments() 218 | { 219 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 220 | var args = new[] { "Alice", "Karen", "Program Files", @"C:\Program Files (x86)\Microsoft Visual Studio" }; 221 | var procTask = new ProcessTask($"{_fixture.WriteCommandLineArgs} {args}"); 222 | var result = await procTask; 223 | 224 | // NOTE: We need skip the first line which is path of the command. 225 | result.AsLines(trimEnd: true).Skip(1).Should().BeEquivalentTo(args); 226 | } 227 | 228 | [Fact] 229 | public async Task ExpandArguments_Escape() 230 | { 231 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 232 | var args = new[] { "Alice", "Karen", "Program Files", @"C:\Program Files (x86)\Microsoft Visual Studio", "\"\\'|<>" }; 233 | var procTask = new ProcessTask($"{_fixture.WriteCommandLineArgs} {args}"); 234 | var result = await procTask; 235 | 236 | // NOTE: We need skip the first line which is path of the command. 237 | result.AsLines(trimEnd: true).Skip(1).Should().BeEquivalentTo(args); 238 | } 239 | 240 | [Fact] 241 | public async Task ExitCode() 242 | { 243 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 244 | var procTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); 245 | (await procTask.ExitCode).Should().Be(192); 246 | } 247 | 248 | [Fact] 249 | public async Task ExitCode_ThrowIfNonZero() 250 | { 251 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 252 | var procTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); 253 | await Assert.ThrowsAsync(async () => await procTask); 254 | } 255 | 256 | [Fact] 257 | public async Task ProcessOutput_StandardInputPassThroughText() 258 | { 259 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 260 | var memStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, コンニチハ!\nABCDEFG")); 261 | var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughText}"); 262 | var result = await procTask; 263 | 264 | result.Output.TrimEnd().Should().Be("Hello, コンニチハ!\nABCDEFG"); 265 | } 266 | 267 | [Fact] 268 | public async Task ProcessOutput_StandardInputOutputCombined() 269 | { 270 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 271 | var procTask = new ProcessTask($"{_fixture.EchoOutAndErrorArgs} Arg0 Arg1 Arg2 Arg3"); 272 | var result = await procTask; 273 | 274 | result.Output.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg0]", "[Arg2]")); 275 | result.Error.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg1]", "[Arg3]")); 276 | result.Combined.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg0]", "[Arg1]", "[Arg2]", "[Arg3]")); 277 | } 278 | 279 | 280 | [Fact] 281 | public async Task ProcessOutput_StandardInputPassThroughBinary() 282 | { 283 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 284 | var memStream = new MemoryStream(Encoding.Unicode.GetBytes("Hello, コンニチハ!\nABCDEFG")); 285 | var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughBinary}"); 286 | var result = await procTask; 287 | 288 | result.OutputBinary.ToArray().Should().BeEquivalentTo(memStream.ToArray()); 289 | } 290 | 291 | [Fact] 292 | public async Task Pipe_StandardInputPassThroughBinary() 293 | { 294 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 295 | var data = Encoding.Unicode.GetBytes("Hello, コンニチハ!\nABCDEFG"); 296 | var memStream = new MemoryStream(data); 297 | var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughBinary}"); 298 | var destStream = new MemoryStream(); 299 | var result = await procTask.Pipe(destStream); 300 | 301 | result.ExitCode.Should().Be(0); 302 | result.OutputBinary.Length.Should().Be(0); 303 | destStream.ToArray().Should().BeEquivalentTo(data); 304 | } 305 | 306 | [Fact] 307 | public async Task Pipe_CloseDestFirst() 308 | { 309 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 310 | var srcTask = new ProcessTask($"{_fixture.WriteSleepWriteExit}"); 311 | var destTask = new ProcessTask($"{_fixture.ReadOnce}"); 312 | 313 | await (srcTask | destTask); 314 | } 315 | 316 | [Fact] 317 | public async Task Pipe_CloseSrcFirst() 318 | { 319 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 320 | var srcTask = new ProcessTask($"{_fixture.HelloWorld}"); 321 | var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); 322 | 323 | await (srcTask | destTask); 324 | } 325 | 326 | [Fact] 327 | public async Task Pipe_ExitCode_NonZero() 328 | { 329 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 330 | var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); 331 | var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); 332 | 333 | await Assert.ThrowsAsync(async () => await (srcTask | destTask)); 334 | } 335 | 336 | [Fact] 337 | public async Task Pipe_ExitCode_NonZero_ExitTailFirst() 338 | { 339 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 340 | var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZeroWaited}"); 341 | var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); 342 | 343 | await Assert.ThrowsAsync(async () => await (srcTask | destTask)); 344 | } 345 | 346 | [Fact] 347 | public async Task Pipe_ExitCode_NonZero_NoThrow() 348 | { 349 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 350 | var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); 351 | var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); 352 | 353 | await (srcTask.NoThrow() | destTask); 354 | } 355 | 356 | [Fact] 357 | public async Task WorkingDirectory() 358 | { 359 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 360 | { 361 | var currentDirectory = Environment.CurrentDirectory; 362 | var output = await new ProcessTask($"{_fixture.WriteCurrentDirectory}"); 363 | output.ToString().Trim().Should().Be(currentDirectory); 364 | } 365 | { 366 | var currentDirectory = Environment.CurrentDirectory; 367 | var workingDirectory = Path.GetFullPath(Path.Combine(currentDirectory, "..")); 368 | var output = await new ProcessTask($"{_fixture.WriteCurrentDirectory}", new ProcessTaskOptions().WithWorkingDirectory(workingDirectory)); 369 | output.ToString().Trim().Should().Be(workingDirectory); 370 | } 371 | } 372 | 373 | [Fact] 374 | public async Task ProcessTimeout() 375 | { 376 | Func execute = async () => 377 | { 378 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 379 | await Assert.ThrowsAsync(async () => 380 | { 381 | await new ProcessTask($"{_fixture.WriteSleepWriteExit}", 382 | new ProcessTaskOptions().WithTimeout(TimeSpan.FromMilliseconds(300))); 383 | }); 384 | }; 385 | await execute.Should().CompleteWithinAsync(TimeSpan.FromSeconds(2)); 386 | } 387 | 388 | [Fact] 389 | public async Task ProcessTimeout_Never() 390 | { 391 | Func execute = async () => 392 | { 393 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 394 | await Assert.ThrowsAsync(async () => 395 | { 396 | await new ProcessTask($"{_fixture.Never}", 397 | new ProcessTaskOptions().WithTimeout(TimeSpan.FromMilliseconds(300))); 398 | }); 399 | }; 400 | await execute.Should().CompleteWithinAsync(TimeSpan.FromSeconds(2)); 401 | } 402 | 403 | [Fact] 404 | public async Task Verbosity_Silent() 405 | { 406 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 407 | var (stdOut, stdErr) = await RunAsync(async (console) => 408 | { 409 | await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.Silent)); 410 | }); 411 | 412 | stdOut.Should().BeEmpty(); 413 | } 414 | 415 | [Fact] 416 | public async Task Verbosity_CommandLine() 417 | { 418 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 419 | var (stdOut, stdErr) = await RunAsync(async (console) => 420 | { 421 | await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.CommandLine)); 422 | }); 423 | 424 | stdOut.Should().StartWith("$ "); 425 | stdOut.Should().NotContain("Hello World!"); 426 | } 427 | 428 | [Fact] 429 | public async Task Verbosity_ConsoleOutputs() 430 | { 431 | using var fakeConsoleScope = new FakeConsoleProviderScope(); 432 | var (stdOut, stdErr) = await RunAsync(async (console) => 433 | { 434 | await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.ConsoleOutputs)); 435 | }); 436 | 437 | stdOut.Should().Be("Hello World!" + Environment.NewLine); 438 | } 439 | 440 | private class FakeConsoleProviderScope : IDisposable 441 | { 442 | private readonly KokubanColorMode _origKokubanColorMode; 443 | private readonly IConsoleProvider _origConsoleProvider; 444 | private readonly FakeConsoleProvider _fakeConsoleProvider; 445 | 446 | public string StdOut => _fakeConsoleProvider.GetStandardOutputAsString(); 447 | public string StdErr => _fakeConsoleProvider.GetStandardErrorAsString(); 448 | 449 | public FakeConsoleProviderScope() 450 | { 451 | _origKokubanColorMode = KokubanOptions.Default.Mode; 452 | _origConsoleProvider = ChellEnvironment.Current.Console; 453 | _fakeConsoleProvider = new FakeConsoleProvider(); 454 | 455 | KokubanOptions.Default.Mode = KokubanColorMode.None; 456 | ChellEnvironment.Current.Console = _fakeConsoleProvider; 457 | } 458 | 459 | public void Dispose() 460 | { 461 | ChellEnvironment.Current.Console = _origConsoleProvider; 462 | _fakeConsoleProvider.Dispose(); 463 | KokubanOptions.Default.Mode = _origKokubanColorMode; 464 | } 465 | } 466 | } 467 | 468 | public class FakeConsoleProvider : IConsoleProvider, IDisposable 469 | { 470 | private readonly Pipe _outputPipe; 471 | private readonly Pipe _errorPipe; 472 | private readonly CancellationTokenSource _cts; 473 | 474 | private readonly MemoryStream _input = new MemoryStream(); 475 | private readonly MemoryStream _output = new MemoryStream(); 476 | private readonly MemoryStream _error = new MemoryStream(); 477 | 478 | public FakeConsoleProvider() 479 | { 480 | _cts = new CancellationTokenSource(); 481 | _outputPipe = new Pipe(new PipeOptions(readerScheduler:PipeScheduler.Inline, writerScheduler:PipeScheduler.Inline)); 482 | _outputPipe.Reader.CopyToAsync(_output, _cts.Token); 483 | _errorPipe = new Pipe(new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline)); 484 | _errorPipe.Reader.CopyToAsync(_error, _cts.Token); 485 | } 486 | 487 | public string GetStandardOutputAsString() => Encoding.UTF8.GetString(_output.ToArray()); 488 | public string GetStandardErrorAsString() => Encoding.UTF8.GetString(_error.ToArray()); 489 | 490 | public Stream OpenStandardInput() 491 | => _input; 492 | 493 | public Stream OpenStandardOutput() 494 | => _outputPipe.Writer.AsStream(leaveOpen: true); 495 | 496 | public Stream OpenStandardError() 497 | => _errorPipe.Writer.AsStream(leaveOpen: true); 498 | 499 | public Encoding InputEncoding => new UTF8Encoding(false); 500 | public Encoding OutputEncoding => new UTF8Encoding(false); 501 | public Encoding ErrorEncoding => new UTF8Encoding(false); 502 | public bool IsInputRedirected => false; 503 | public bool IsOutputRedirected => false; 504 | public bool IsErrorRedirected => false; 505 | public TextWriter Out => new StreamWriter(_outputPipe.Writer.AsStream(leaveOpen: true), OutputEncoding, leaveOpen: true) { AutoFlush = true }; 506 | public TextWriter Error => new StreamWriter(_errorPipe.Writer.AsStream(leaveOpen: true), ErrorEncoding, leaveOpen: true) { AutoFlush = true }; 507 | public void Dispose() 508 | { 509 | _cts.Cancel(); 510 | } 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /tests/Chell.Tests/Shell/BashShellExecutorTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Chell.Shell; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Chell.Tests.Shell 7 | { 8 | public class BashShellExecutorTest 9 | { 10 | [Fact] 11 | public void Escape() 12 | { 13 | var executor = new BashShellExecutor(); 14 | executor.Escape(@"").Should().Be(@""); 15 | executor.Escape(@"foo").Should().Be(@"foo"); 16 | executor.Escape(@"123").Should().Be(@"123"); 17 | executor.Escape(@"f oo").Should().Be(@"$'f oo'"); 18 | executor.Escape(@"b'ar").Should().Be(@"$'b\'ar'"); 19 | executor.Escape(@"b""ar").Should().Be(@"$'b\""ar'"); 20 | executor.Escape(@"a\b").Should().Be(@"$'a\\b'"); 21 | } 22 | 23 | [Fact] 24 | public void GetCommandAndArguments() 25 | { 26 | { 27 | var executor = new BashShellExecutor(); 28 | executor.Path = "/bin/bash"; 29 | executor.Prefix = "set -euo pipefail;"; 30 | executor.GetCommandAndArguments("foo bar").Should().Be(("/bin/bash", "-c \"set -euo pipefail;foo bar\"")); 31 | } 32 | { 33 | var executor = new BashShellExecutor(); 34 | executor.Path = "/usr/local/bin/bash"; 35 | executor.Prefix = ""; 36 | executor.GetCommandAndArguments("foo bar").Should().Be(("/usr/local/bin/bash", "-c \"foo bar\"")); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Chell.Tests/Shell/CmdShellExecutorTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Chell.Shell; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Chell.Tests.Shell 7 | { 8 | public class CmdShellExecutorTest 9 | { 10 | [Fact] 11 | public void Escape() 12 | { 13 | var executor = new CmdShellExecutor(); 14 | executor.Escape(@"").Should().Be(@""); 15 | executor.Escape(@"foo").Should().Be(@"foo"); 16 | executor.Escape(@"123").Should().Be(@"123"); 17 | executor.Escape(@"Program Files").Should().Be(@"""Program Files"""); 18 | executor.Escape(@"b'ar").Should().Be(@"""b'ar"""); 19 | executor.Escape(@"b""ar").Should().Be(@"""b\""ar"""); 20 | executor.Escape(@"a\b").Should().Be(@"a\b"); 21 | executor.Escape(@"^").Should().Be(@"""^^"""); 22 | executor.Escape(@"<").Should().Be(@"""^<"""); 23 | executor.Escape(@">").Should().Be(@"""^>"""); 24 | executor.Escape(@"|").Should().Be(@"""^|"""); 25 | executor.Escape(@"&").Should().Be(@"""^&"""); 26 | executor.Escape(@"&&").Should().Be(@"""^&^&"""); 27 | 28 | // \ --> \ 29 | // " --> "\"" 30 | // \" --> "\\\"" 31 | executor.Escape(@"\").Should().Be(@"\"); 32 | executor.Escape(@"""").Should().Be(@"""\"""""); 33 | executor.Escape(@"\""").Should().Be(@"""\\\"""""); 34 | 35 | // "\" --> "\"\\\"" 36 | executor.Escape(@"""\""").Should().Be("\"\\\"\\\\\\\"\""); 37 | 38 | // "\"'|<>[] --> "\"\\\"'^|^<^>[]"" 39 | executor.Escape("\"\\\"'|<>[]").Should().Be("\"\\\"\\\\\\\"'^|^<^>[]\""); 40 | 41 | // "\'|<> --> "\"\'^|^<^>" 42 | executor.Escape("\"\\'|<>").Should().Be("\"\\\"\\'^|^<^>\""); 43 | 44 | // A B\C D --> "A B\C D" 45 | // A "B\C D --> "A \"B\C D" 46 | executor.Escape(@"A B\C D").Should().Be(@"""A B\C D"""); 47 | executor.Escape(@"A ""B\C D").Should().Be(@"""A \""B\C D"""); 48 | } 49 | 50 | [Fact] 51 | public void GetCommandAndArguments() 52 | { 53 | var executor = new CmdShellExecutor(); 54 | executor.GetCommandAndArguments("foo bar").Should().Be(("cmd", "/c \"foo bar\"")); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Chell.Tests/TemporaryAppBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Chell.Tests 10 | { 11 | public class TemporaryAppSolutionBuilder : IDisposable 12 | { 13 | private readonly List _apps; 14 | public string BaseDirectory { get; } 15 | 16 | public TemporaryAppSolutionBuilder() 17 | { 18 | _apps = new List(); 19 | BaseDirectory = Path.Combine(Path.GetTempPath(), $"Chell.Tests-{Guid.NewGuid()}"); 20 | } 21 | 22 | public string CreateProject(string projectName, Action configure) 23 | { 24 | var builder = TemporaryAppBuilder.Create(BaseDirectory, projectName); 25 | _apps.Add(builder); 26 | configure(builder); 27 | return Path.Combine(BaseDirectory, "out", projectName); 28 | } 29 | 30 | public TemporaryAppSolutionBuilder RunInSolutionDirectory(string fileName, string arguments) 31 | { 32 | var procStartInfo = new ProcessStartInfo() 33 | { 34 | FileName = fileName, 35 | Arguments = arguments, 36 | WorkingDirectory = BaseDirectory, 37 | RedirectStandardOutput = true, 38 | RedirectStandardError = true, 39 | }; 40 | 41 | var proc = Process.Start(procStartInfo)!; 42 | var standardOutput = proc.StandardOutput.ReadToEnd(); 43 | var standardError = proc.StandardError.ReadToEnd(); 44 | proc.WaitForExit(); 45 | 46 | if (proc.ExitCode != 0) 47 | { 48 | throw new InvalidOperationException(string.Join(Environment.NewLine, $"The process has been exited with code {proc.ExitCode}. (FileName={fileName}, Arguments={arguments}", "Output:", standardOutput, "Error:", standardError)); 49 | } 50 | 51 | return this; 52 | } 53 | 54 | public void Build() 55 | { 56 | RunInSolutionDirectory("dotnet", "new sln"); 57 | RunInSolutionDirectory("dotnet", $"sln add {string.Join(" ", _apps.Select(x => x.ProjectName + "/src"))}"); 58 | RunInSolutionDirectory("dotnet", "publish -o out"); 59 | } 60 | 61 | public void Dispose() 62 | { 63 | Directory.Delete(BaseDirectory, recursive:true); 64 | } 65 | } 66 | public class TemporaryAppBuilder : IDisposable 67 | { 68 | private bool _disposed; 69 | 70 | public string ProjectName { get; } 71 | 72 | public string BaseDirectory { get; } 73 | public string SourceDirectory { get; } 74 | public string OutputDirectory { get; } 75 | 76 | private TemporaryAppBuilder(string baseSlnDirectory, string projectName) 77 | { 78 | ProjectName = projectName; 79 | BaseDirectory = Path.Combine(baseSlnDirectory, projectName); 80 | SourceDirectory = Path.Combine(BaseDirectory, $"src"); 81 | OutputDirectory = Path.Combine(BaseDirectory, $"out"); 82 | } 83 | 84 | public static TemporaryAppBuilder Create(string baseSlnDirectory, string projectName) 85 | { 86 | var builder = new TemporaryAppBuilder(baseSlnDirectory, projectName); 87 | builder.Initialize(); 88 | return builder; 89 | } 90 | 91 | private void Initialize() 92 | { 93 | Directory.CreateDirectory(BaseDirectory); 94 | Directory.CreateDirectory(SourceDirectory); 95 | Directory.CreateDirectory(OutputDirectory); 96 | 97 | // Create .NET Console App project. 98 | //RunInSourceDirectory("dotnet", $"new console -f net5.0 -n {ProjectName} -o ."); 99 | // Explicitly use .NET 5 SDK. (AppHost is required for macOS with .NET 5 SDK) 100 | WriteSourceFile("global.json", 101 | @"{ 102 | ""sdk"": { 103 | ""version"": ""5.0.100"", 104 | ""rollForward"": ""latestFeature"" 105 | } 106 | }"); 107 | WriteSourceFile($"{ProjectName}.csproj", 108 | @" 109 | 110 | Exe 111 | net5.0 112 | 113 | "); 114 | WriteSourceFile("Directory.Build.props", 115 | @" 116 | 117 | true 118 | 119 | "); 120 | } 121 | 122 | public TemporaryAppBuilder WriteSourceFile(string fileName, string content) 123 | { 124 | File.WriteAllText(Path.Combine(SourceDirectory, fileName), content, Encoding.UTF8); 125 | return this; 126 | } 127 | 128 | public string GetExecutablePath() 129 | => Path.Combine(OutputDirectory, ProjectName); 130 | 131 | public Compilation Build() 132 | { 133 | RunInSourceDirectory("dotnet", $"publish -o \"{OutputDirectory}\""); 134 | 135 | return new Compilation(this, Path.Combine(OutputDirectory, ProjectName)); 136 | } 137 | 138 | public class Compilation : IDisposable 139 | { 140 | private readonly TemporaryAppBuilder _builder; 141 | public string ExecutablePath { get; } 142 | 143 | public Compilation(TemporaryAppBuilder builder, string executablePath) 144 | { 145 | _builder = builder; 146 | ExecutablePath = executablePath; 147 | } 148 | 149 | public void Dispose() 150 | { 151 | _builder.Dispose(); 152 | } 153 | } 154 | 155 | public TemporaryAppBuilder RunInSourceDirectory(string fileName, string arguments) 156 | { 157 | var procStartInfo = new ProcessStartInfo() 158 | { 159 | FileName = fileName, 160 | Arguments = arguments, 161 | WorkingDirectory = SourceDirectory, 162 | RedirectStandardOutput = true, 163 | RedirectStandardError = true, 164 | }; 165 | 166 | var proc = Process.Start(procStartInfo)!; 167 | proc.WaitForExit(); 168 | var standardOutput = proc.StandardOutput.ReadToEnd(); 169 | var standardError = proc.StandardError.ReadToEnd(); 170 | 171 | 172 | if (proc.ExitCode != 0) 173 | { 174 | throw new InvalidOperationException(string.Join(Environment.NewLine, $"The process has been exited with code {proc.ExitCode}. (FileName={fileName}, Arguments={arguments}", "Output:", standardOutput, "Error:", standardError)); 175 | } 176 | 177 | return this; 178 | } 179 | 180 | public void Dispose() 181 | { 182 | if (_disposed) return; 183 | 184 | Directory.Delete(BaseDirectory, recursive:true); 185 | 186 | _disposed = true; 187 | } 188 | } 189 | } 190 | --------------------------------------------------------------------------------