Repository: mayuki/Chell Branch: main Commit: 79e25d9b187a Files: 57 Total size: 200.3 KB Directory structure: gitextract_m746u3r_/ ├── .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/ │ │ ├── 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 │ └── Chell.Run/ │ ├── Chell.Run.csproj │ └── Program.cs └── tests/ └── Chell.Tests/ ├── Chell.Tests.csproj ├── ChellEnvironmentTest.cs ├── CommandLineStringTest.cs ├── ProcessTaskTest.cs ├── Shell/ │ ├── BashShellExecutorTest.cs │ └── CmdShellExecutorTest.cs └── TemporaryAppBuilder.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # To learn more about .editorconfig see https://aka.ms/editorconfigdocs ############################### # Core EditorConfig Options # ############################### root = true # All files [*] indent_style = space # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true charset = utf-8 [*.md] charset = utf-8 ############################### # .NET Coding Conventions # ############################### [*.{cs,vb}] # Organize usings dotnet_sort_system_directives_first = true # this. preferences dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_event = false:silent # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent dotnet_style_readonly_field = true:suggestion # Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent ############################### # Naming Conventions # ############################### # Style Definitions dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = * dotnet_naming_symbols.constant_fields.required_modifiers = const ############################### # C# Coding Conventions # ############################### [*.cs] # var preferences csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent csharp_style_var_elsewhere = true:silent # Expression-bodied members csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_accessors = true:silent # Pattern matching preferences csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion # Null-checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion # Expression-level preferences csharp_prefer_braces = true:silent csharp_style_deconstructed_variable_declaration = true:suggestion csharp_prefer_simple_default_expression = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion ############################### # C# Formatting Rules # ############################### # New line preferences csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = flush_left # Space preferences csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto *.sh text eol=lf ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build-Development on: push: branches: - main - master pull_request: types: - opened - synchronize jobs: Build: runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] env: DOTNET_NOLOGO: true steps: - uses: actions/checkout@v1 - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' # Build - run: dotnet restore - run: dotnet build -c Release # Run Unit tests - run: dotnet test -c Release --no-build --logger trx --results-directory $GITHUB_WORKSPACE/artifacts # Packaging - name: dotnet pack run: dotnet pack -c Release --no-build -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --output $GITHUB_WORKSPACE/artifacts shell: bash # Upload & Publish - uses: actions/upload-artifact@master with: name: Packages path: artifacts ================================================ FILE: .github/workflows/release.yaml ================================================ name: Build-Release on: push: tags: - v* jobs: Release: if: "contains(github.ref, 'refs/tags')" runs-on: ubuntu-latest env: DOTNET_NOLOGO: true steps: - uses: actions/checkout@v1 - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' - name: "Set VersionSuffix for Preview" if: "contains(github.ref, 'refs/tags') && contains(github.ref, 'preview')" run: | echo "VERSION_SUFFIX=preview.`date '+%Y%m%d-%H%M%S'`+${GITHUB_SHA:0:6}" >> $GITHUB_ENV - name: "Get git tag" if: "contains(github.ref, 'refs/tags')" run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV # Build - run: dotnet restore - run: dotnet build -c Release # Packaging - name: dotnet pack run: dotnet pack -c Release --no-build --version-suffix "$VERSION_SUFFIX" -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg --output $GITHUB_WORKSPACE/artifacts # Upload & Publish - uses: actions/upload-artifact@master with: name: Packages path: artifacts - name: "Push to NuGet.org" run: | dotnet nuget push "$GITHUB_WORKSPACE/artifacts/*.nupkg" --skip-duplicate -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore .DS_Store # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- Backup*.rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # .NET Launch Profiles launchSettings.json ================================================ FILE: Chell.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30114.105 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell", "src\Chell\Chell.csproj", "{6BE659EC-A00D-4148-B19D-B5478DE001FA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell.Run", "src\Chell.Run\Chell.Run.csproj", "{893D8C70-3C13-47D6-987F-C099C6161D7E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chell.Tests", "tests\Chell.Tests\Chell.Tests.csproj", "{1FBAA8ED-438E-498C-AB1F-29429550DC21}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EBB92712-D878-48FD-8E31-E577AABDC0FB}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore .github\workflows\build.yaml = .github\workflows\build.yaml Directory.Build.props = Directory.Build.props README.md = README.md .github\workflows\release.yaml = .github\workflows\release.yaml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AF454F92-8B84-446B-B0E2-9BA8887B09CC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted.Basic.Windows", "samples\GettingStarted.Basic.Windows\GettingStarted.Basic.Windows.csproj", "{9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted.Basic.Unix", "samples\GettingStarted.Basic.Unix\GettingStarted.Basic.Unix.csproj", "{69DC1056-843E-4980-908A-5DB4ADA95460}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x64.ActiveCfg = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x64.Build.0 = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x86.ActiveCfg = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Debug|x86.Build.0 = Debug|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|Any CPU.Build.0 = Release|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x64.ActiveCfg = Release|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x64.Build.0 = Release|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x86.ActiveCfg = Release|Any CPU {6BE659EC-A00D-4148-B19D-B5478DE001FA}.Release|x86.Build.0 = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x64.ActiveCfg = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x64.Build.0 = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x86.ActiveCfg = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Debug|x86.Build.0 = Debug|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|Any CPU.Build.0 = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x64.ActiveCfg = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x64.Build.0 = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x86.ActiveCfg = Release|Any CPU {893D8C70-3C13-47D6-987F-C099C6161D7E}.Release|x86.Build.0 = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x64.ActiveCfg = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x64.Build.0 = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x86.ActiveCfg = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Debug|x86.Build.0 = Debug|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|Any CPU.Build.0 = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x64.ActiveCfg = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x64.Build.0 = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x86.ActiveCfg = Release|Any CPU {1FBAA8ED-438E-498C-AB1F-29429550DC21}.Release|x86.Build.0 = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x64.ActiveCfg = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x64.Build.0 = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x86.ActiveCfg = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Debug|x86.Build.0 = Debug|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|Any CPU.Build.0 = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x64.ActiveCfg = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x64.Build.0 = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x86.ActiveCfg = Release|Any CPU {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5}.Release|x86.Build.0 = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|Any CPU.Build.0 = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x64.ActiveCfg = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x64.Build.0 = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x86.ActiveCfg = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Debug|x86.Build.0 = Debug|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|Any CPU.ActiveCfg = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|Any CPU.Build.0 = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x64.ActiveCfg = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x64.Build.0 = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x86.ActiveCfg = Release|Any CPU {69DC1056-843E-4980-908A-5DB4ADA95460}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {9BE2CB2C-D6F2-429A-A00F-CF30CAEF28B5} = {AF454F92-8B84-446B-B0E2-9BA8887B09CC} {69DC1056-843E-4980-908A-5DB4ADA95460} = {AF454F92-8B84-446B-B0E2-9BA8887B09CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5B16EB0E-5342-4D93-9FA2-8327FF69A276} EndGlobalSection EndGlobal ================================================ FILE: Directory.Build.props ================================================  1.0.0 latest enable true $(NoWarn);1591;1587;1574 Write scripts with the power of .NET. Provides a shell script-like (bash, cmd, ...) experience to .NET application. Mayuki Sawatari Copyright © Mayuki Sawatari https://github.com/mayuki/Chell https://github.com/mayuki/Chell CommandLine Shell Process MIT ================================================ FILE: LICENSE ================================================ MIT License Copyright © Mayuki Sawatari Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Chell Write scripts with the power of C# and .NET. Chell is a library and execution tool for providing a shell script-like (bash, cmd, ...) experience to .NET applications. ```csharp var branch = await Run($"git branch --show-current"); await Run($"git archive {branch} -o {branch}.zip"); ``` .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). ## When should I use Chell? - **Write better shell scripts**: Write a complex script and use the power of .NET and C# - **Write multi-platform shell scripts**: As an alternative to scripts that work on multiple platforms - **Run a process quickly in your app**: As .NET library for easy handling of process launch and output - **All developers in the project are .NET developers**: 🙃 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. ## Chell at a glance 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`. ```csharp // Chell.Exports exposes a variety of functions and properties using Chell; using static Chell.Exports; ``` ```csharp // Move the current directory with Cd method Cd("/tmp"); // Dispose the return value of Cd method to return to the previous directory using (Cd("/usr/local/bin")) { // The current directory is "/usr/local/bin". } // The current directory is "/" again. ``` ```csharp // You can run the process by passing a string to Run method await Run($"ls -lFa"); ``` ```csharp // An interpolated string passed to Run method will be escaped and expanded if it is an array var newDirs = new [] { "foo", "bar", "my app", "your;app" }; await Run($"mkdir {newDirs}"); // $ mkdir foo bar "my app" "your;app" ``` ```csharp // Run method returns the result object of the command (ProcessOutput class) var result = await Run($"ls -lFa"); // You can read stdout & stderr line by line foreach (var line in result) { Echo(line); } // Allows to get stdout & stderr with implicit conversion to `string` string output = result; // You can also get stdout as bytes (ReadOnlyMemory) var binary = result.OutputBinary; ``` ```csharp // Provides convenient extension methods for parsing JSON. var images = await Run($"docker image ls --format {"{{json .}}"}").SuppressConsoleOutputs(); foreach (var image in images.AsJsonLines(new { Repository = "", ID = "", Tag = ""})) { Echo(image); } // $ docker image ls --format "{{json .}}" // { Repository = mcr.microsoft.com/dotnet/sdk, ID = b160c8f3dbd6, Tag = 5.0 } // { Repository = , ID = 3ee645b4a3bd, Tag = } ``` ```csharp // Standard input/output of process tasks can be connected by pipes await (Run($"ls -lFa") | Run($"grep dotnet")); // The difference with `await (Run($"ls -lFa | grep dotnet"));` is that the shell can pipe or not. // You can also specify a Stream as input or output // Write ffmpeg output to a Stream. await (Run($"ffmpeg ...") | destinationStream); // Write a Stream to ffmpeg process. await (srcStream | Run($"ffmpeg ...")); ``` 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(...)`. ```csharp using Chell; var result = await new Run($"ls -lF"); ``` Want to run it like a scripting language? Install [Chell.Run](#chellrun), and you can run it like a script. ```bash % dotnet tool install -g Chell.Run % chell -e "Echo(DateTime.Now)" 9/1/2021 0:00:00 PM % cat <<__EOF__ > MyScript.cs var dirs = new [] { "foo bar", "baz" }; await Run($"mkdir {dirs}"); await Run($"ls -l"); __EOF__ % chell MyScript.cs $ mkdir "foo bar" "baz" $ ls -l total 8 drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 baz/ drwxr-xr-x 2 mayuki mayuki 4096 Sep 1 00:00 'foo bar'/ ``` ## Features - Automatic shell character escaping and array expansion - Stream and Process Pipes - Provide utilities and shortcuts useful for scripting. - Simple shell script-like execution tools - Multi-platform (Windows, Linux, macOS) - LINQPad friendly ## Install ``` dotnet package add Chell ``` ### Requirements .NET Standard 2.1, .NET 5 or higher ## Chell.Exports 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`. ### Methods (Functions) #### `Run` Starts a process using the specified command-line and returns a `ProcessTask`. ```csharp await Run($"ls -lF"); // The followings are equivalent to calling Run method await (Run)$"ls -lF"; await new Run($"ls -lF"); ``` 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. If the exit code of the process returns non-zero, it will throw an exception. To suppress this exception, see `NoThrow`. An interpolated string passed to Run method will be escaped and expanded if it is an array. ```csharp var newDirs = new [] { "foo", "bar", "my app", "your;app" }; await Run($"mkdir {newDirs}"); // equivalent to `mkdir foo bar "my app" "your;app"` ``` You can also pass an execution options (`ProcessTaskOptions`) to Run method. ```csharp await Run($"ping -t localhost", new ProcessTaskOptions( workingDirectory: @"C:\Windows", timeout: TimeSpan.FromSeconds(1) )); ``` #### `Cd(string)` ```csharp Cd("/usr/local/bin"); // equivalent to `Environment.CurrentDirectory = "/usr/local/bin";` ``` Dispose the return value of `Cd` method to return to the previous directory. ```csharp Cd("/"); // The current directory is "/". using (Cd("/usr/local/bin")) { // The current directory is "/usr/local/bin". } // The current directory is "/" again. ``` #### `Mkdirp(string path)` Same as `mkdir -p`. Creates a new directory and any necessary sub-directories in the specified path. #### `Dump(T value)` Formats the object and write it to the console. ```csharp Dump(new { Foo = 123, Bar = "Baz" }); // => "{ Foo = 123, Bar = "Baz" }" ``` #### `Which(string name)`, `TryWhich(string name, out string path)` Returns a path of the specified command. ```csharp var dotnetPath = Which("dotnet"); await Run($"{dotnetPath} run"); ``` #### `Echo(object message = default)` `Echo` method is equivalent to Console.WriteLine. ```csharp Echo("Hello World!"); // equivalent to Console.WriteLine("Hello World!"); ``` #### `Sleep(int duration)`, `Sleep(TimeSpan duration)` Returns a Task that waits for the specified duration. ```csharp await Sleep(10); // Sleep for 10 seconds. ``` #### `Exit(int exitCode)` Terminates the application with an exit code. ```csharp Exit(1); ``` ### Properties #### `Env.Vars` Exposes the environment variables as `IDictionary`. ```csharp Env.Vars["PATH"] = Env.Vars["PATH"] + ":/path/to/"; ``` #### `Env.IsWindows` Returns whether the running operating system is Windows or not. If it returns `false`, the operating system is Linux or macOS. ```csharp if (Env.IsWindows) { /* Something to do for Windows */ } ``` #### `Env.Shell` Specify explicitly which shell to use, or set to not use a shell. ```csharp Env.Shell.UseBash(); Env.Shell.NoUseShell(); Env.Shell.UseCmd(); ``` #### `Env.Verbosity` Sets or gets the output level when executing a command/process. - `Verbosity.All`: Displays both the command line and the output of the command - `Verbosity.CommandLine`: Displays the command line - `Verbosity.Output`: Displays the output of the command - `Verbosity.Silent`: No display #### `Env.ProcessTimeout` Sets the timeout for running the process. The default value is `0` (disabled). ```csharp Env.ProcessTimeout = TimeSpan.FromSecond(1); // OperationCanceledException will be thrown after 1s. await Run($"ping -t localhost"); ``` #### `Arguments` Gets the arguments passed to the current application. ```csharp // $ myapp foo bar baz => new [] { "foo", "bar", "baz" }; foreach (var arg in Arguments) { /* ... */ } ``` #### `CurrentDirectory`, `ExecutableDirectory`, `ExecutableName`, `ExecutablePath` Gets the current directory and the application directory or name or path. ```csharp // C:\> cd C:\Users\Alice // C:\Users\Alice> Downloads\MyApp.exe Echo(CurrentDirectory); // C:\Users\Alice Echo(ExecutableDirectory); // C:\Users\Alice\Downloads Echo(ExecutableName); // MyApp.exe Echo(ExecutablePath); // C:\Users\Alice\Downloads\MyApp.exe ``` #### `HomeDirectory` Gets the path of the current user's home directory. ```csharp // Windows: C:/Users/ // Linux: /home/ // macOS: /Users/ Echo(HomeDirectory); ``` #### `StdIn`, `StdOut`, `StdErr` Provides the wrapper with methods useful for reading and writing to the standard input/output/error streams. ```csharp // Reads data from standard input. await StdIn.ReadToEndAsync(); // Writes data to standard output or error. StdOut.WriteLine("FooBar"); StdErr.WriteLine("Oops!"); ``` ## ProcessTask class Represents the execution task of the process started by `Run`. ### `Pipe` Connects the standard output of the process to another `ProcessTask` or `Stream`. ```csharp await (Run($"ls -lF") | Run($"grep .dll")); // The followings are equivalent to using '|'. var procTask1 = Run($"ls -lF"); var procTask2 = Run($"grep .dll"); procTask1.Pipe(procTask2); ``` A `Stream` can also be passed to Pipe. If the ProcessTask has connected to the `Stream`, it will not write to `ProcessOutput`. ```csharp var memStream = new MemoryStream(); await Run($"ls -lF").Pipe(memStream); ``` ### `ConnectStreamToStandardInput` Connects the Stream to the standard input of the process. The method can be called only once before the process starts. ```csharp await (myStream | Run($"grep .dll")); // The followings are equivalent to using '|'. var procTask = Run($"grep .dll"); procTask.ConnectStreamToStandardInput(myStream); ``` ### `NoThrow` Suppresses exception throwing when the exit code is non-zero. ```csharp await Run($"AppReturnsExitCodeNonZero").NoThrow(); ``` ### `SuppressConsoleOutputs` Suppresses the writing of command execution results to the standard output. ```csharp // equivalent to "Env.Verbosity = Verbosity.Silent" or pipe to null. await Run($"ls -lF").SuppressConsoleOutputs(); ``` ### `ExitCode` Returns a `Task` to get the exit code of the process. This is equivalent to waiting for a `ProcessTask` with `NoThrow`. ```csharp var proc = Run($"ls -lF"); if (await proc.ExitCode != 0) { ... } // equivalent to `(await Run($"ls -lF").NoThrow()).ExitCode` ``` ## ProcessOutput class Provides the results of the process execution. ### `Combined`, `CombinedBinary` Gets the combined standard output and error as a string or byte array. ### `Output`, `OutputBinary` Gets the standard output as a string or byte array. ### `Error`, `ErrorBinary` Gets the standard error as a string or byte array. ### `AsLines(bool trimEnd = false)`, `GetEnumerator()` Gets the combined standard output and error as a per-line `IEnumerable`. ```csharp // equivalent to `foreach (var line in procOutput.AsLines())` foreach (var line in procOutput) { ... } ``` ### `ToString()` The method equivalent to `Combined` property. ### `ExitCode` Gets the exit code of the process. ## Utilities and shortcuts Chell.Exports class also exposes a variety of useful utilities and shortcuts to libraries. ### `Prompt` Prompts the user for input and gets it. ```csharp var name = await Prompt("What's your name? "); ``` ### `Chalk`: Kokuban: Terminal string styling Provides a shortcut to [mayuki/Kokuban](https://github.com/mayuki/Kokuban). You can easily style the text on the terminal. ```csharp // "Error: " will be colored. Echo((Chalk.Red + "Error: ") + "Something went wrong."); ``` ### `Glob` Provides a shortcut to `Microsoft.Extensions.FileSystemGlobbing`. - `Glob(params string[] patterns)` - `Glob(string baseDir, string[] patterns)` ```csharp // Glob patterns starting with '!' will be treated as excludes. foreach (var path in Glob("**/*.cs", "!**/*.vb")) { ... } ``` ### JSON serialize/deserialize (System.Text.Json) Provides shortcuts to `System.Text.Json`. - `ToJson(T obj)` ```csharp var obj = new { Name = "Alice", Age = 18 }; var json = ToJson(obj); Echo(json); // {"Name":"Alice","Age":18} ``` - `FromJson(string json)` - `FromJson(string json, T shape)` ```csharp var json = "{ \"foo\": 123 }"; var obj = FromJson(json, new { Foo = 0 }); Dump(obj); // { Foo = 123 } ``` - `AsJson` - `AsJsonLines` ```csharp using Chell; var output = await Run($"docker image ls --format {"{{json .}}"}"); foreach (var image in output.AsJsonLines(new { Repository = "", ID = "", Tag = ""})) { // ... } ``` ```csharp using Chell; var output = await Run($"kubectl version --client -o json"); var obj = output.AsJson(new { clientVersion = new { major = "", minor = "", gitVersion = "" } }); Echo(obj); // { clientVersion = { major = 1, minor = 21, gitVersion = v1.21.2 } } ``` ### HTTP acccess (System.Net.Http) Provides shortcuts to `System.Net.Http.HttpClient`. - `FetchAsync` - `FetchByteArrayAsync` - `FetchStreamAsync` - `FetchStringAsync` ## Chell as a Library Chell can also be used as a utility library to run processes. 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. ```csharp using Chell; var results = await new Run($"ls -lF"); // ChellEnvironment.Current is equivalent to `Env` on `Chell.Exports`. Console.WriteLine(ChellEnvironment.Current.ExecutablePath); Console.WriteLine(ChellEnvironment.Current.ExecutableName); Console.WriteLine(ChellEnvironment.Current.Arguments); Console.WriteLine(ChellEnvironment.Current.Vars["PATH"]); ``` ## Chell.Run Chell.Run executes the input source code in an environment where Chell and some libraries are available. It does not perform any NuGet package resolution, so we recommend creating a typical C# project if you need to handle such complexities. ``` $ dotnet tool install -g Chell.Run ``` ```bash $ chell -e "Echo(123);" $ chell < Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: samples/GettingStarted.Basic.Unix/GettingStarted.Basic.Unix.csproj ================================================ Exe net5.0 false ================================================ FILE: samples/GettingStarted.Basic.Unix/Program.cs ================================================ using System; using System.Linq; using static Chell.Exports; // Starts a process. // The array will be expanded and the elements will be escaped var dirs = new[] { "/", "/usr", "/bin" }; var results = await Run($"ls -l {dirs}"); // Enumerates the results to retrieve the standard output line by line. foreach (var line in results) { Echo($"Result> {line}"); } Echo(); // Built-in Variables Echo((Chalk.Green + "ExecutableName: ") + string.Join(' ', ExecutableName)); Echo((Chalk.Green + "ExecutableDirectory: ") + string.Join(' ', ExecutableDirectory)); Echo((Chalk.Green + "Arguments: ") + string.Join(' ', Arguments)); Echo((Chalk.Green + "CurrentDirectory: ") + string.Join(' ', CurrentDirectory)); Echo(); // Environment Variables Echo((Chalk.Green + "Env.Vars[\"PATH\"]: ") + Env.Vars["PATH"]); Echo(); // Standard Input/Error as Stream + Utility methods. StdOut.WriteLine("Hello World!"); StdErr.WriteLine("Hello World! (Error)"); Echo(); // Get the data from network and pipe it to the process await (await FetchByteArrayAsync("http://www.example.com/") | Run("grep title")); // Temporarily change the current directory. using (Cd("/")) { await Run($"dir"); } Exit(1); ================================================ FILE: samples/GettingStarted.Basic.Windows/GettingStarted.Basic.Windows.csproj ================================================ Exe net5.0 false ================================================ FILE: samples/GettingStarted.Basic.Windows/Program.cs ================================================ using System; using System.Linq; using Chell; using static Chell.Exports; // Starts a process. // The array will be expanded and the elements will be escaped var dirs = new[] { @"C:\Windows\Microsoft.NET", @"C:\Program Files" }; var results = await Run($"dir {dirs}"); // // Enumerates the results to retrieve the standard output line by line. foreach (var line in results.Where(x => x.Contains("Windows"))) { Echo($"Result> {line}"); } Echo(); // Built-in Variables Echo((Chalk.Green + "ExecutableName: ") + string.Join(' ', ExecutableName)); Echo((Chalk.Green + "ExecutableDirectory: ") + string.Join(' ', ExecutableDirectory)); Echo((Chalk.Green + "Arguments: ") + string.Join(' ', Arguments)); Echo((Chalk.Green + "CurrentDirectory: ") + string.Join(' ', CurrentDirectory)); Echo(); // Environment Variables Echo((Chalk.Green + "Env.Vars[\"PATH\"]: ") + Env.Vars["PATH"]); Echo(); // Standard Input/Error as Stream + Utility methods. StdOut.WriteLine("Hello World!"); StdErr.WriteLine("Hello World! (Error)"); Echo(); // Get the data from network and pipe it to the process await (await FetchByteArrayAsync("http://www.example.com/") | Run("findstr title")); // Temporarily change the current directory. using (Cd("C:\\Users")) { await Run($"dir"); } Exit(1); ================================================ FILE: src/.editorconfig ================================================ ############################### # C# Nullability # ############################### [*.cs] # CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. dotnet_diagnostic.CS8618.severity = error # CS8604: Possible null reference argument. dotnet_diagnostic.CS8604.severity = error # CS8629: Nullable value type may be null. dotnet_diagnostic.CS8629.severity = error # CS8600: Converting null literal or possible null value to non-nullable type. dotnet_diagnostic.CS8600.severity = error # CS8603: Possible null reference return. dotnet_diagnostic.CS8603.severity = error # CS8610: Nullability of reference types in type of parameter doesn't match overridden member. dotnet_diagnostic.CS8610.severity = error # CS8625: Cannot convert null literal to non-nullable reference type. dotnet_diagnostic.CS8625.severity = error # CS8606: Possible null reference assignment to iteration variable dotnet_diagnostic.CS8606.severity = error # CS8602: Dereference of a possibly null reference. dotnet_diagnostic.CS8602.severity = error # CS8601: Possible null reference assignment. dotnet_diagnostic.CS8601.severity = error # CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member. dotnet_diagnostic.CS8614.severity = error ================================================ FILE: src/Chell/Chell.csproj ================================================  netstandard2.1;netcoreapp3.1;net5.0 latest enable true TextTemplatingFileGenerator ProcessTaskExtensions.Generated.cs TextTemplatingFileGenerator ChellWritableStream.Generated.cs True True ProcessTaskExtensions.tt True True ChellWritableStream.tt ================================================ FILE: src/Chell/ChellEnvironment.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Chell.Internal; using Chell.IO; using Chell.Shell; namespace Chell { [Flags] public enum ChellVerbosity { /// /// By default, no output is written to the console. /// Silent = 0, /// /// Writes a executing command line to the console. /// CommandLine = 1 << 0, /// /// Writes a command output to the console. /// ConsoleOutputs = 1 << 1, /// /// Writes all command lines and command outputs. /// Full = CommandLine | ConsoleOutputs, [EditorBrowsable(EditorBrowsableState.Never)] Debug = Full | 1 << 31, } public class ChellEnvironment { public static ChellEnvironment Current { get; set; } = new ChellEnvironment(); private string[] _arguments; private string? _executablePath; private string _executableName; private string _executableDirectory; public ChellEnvironment() { var args = Environment.GetCommandLineArgs(); var path = args[0]; _arguments = args.Skip(1).ToArray(); _executablePath = path; _executableName = Path.GetFileName(path); _executableDirectory = Path.GetDirectoryName(path)!; } /// /// Gets or sets the verbosity. /// public ChellVerbosity Verbosity { get; set; } = ChellVerbosity.Full; public ShellExecutorProvider Shell { get; } = new ShellExecutorProvider(); public IConsoleProvider Console { get; set; } = LINQPadHelper.RunningOnLINQPad ? new LINQPadConsoleProvider() : SystemConsoleProvider.Instance; /// /// Gets the identifier for the current application process. /// public int ProcessId => #if NET5_0_OR_GREATER Environment.ProcessId #else Process.GetCurrentProcess().Id #endif ; /// /// Gets whether the current application is running on Windows. /// public bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// Gets the command line arguments. like args of a entry point. /// public IReadOnlyList Arguments => _arguments; /// /// Gets the path of the executing application. like argv[0]. (e.g. C:\\Path\To\App.exe, /path/to/app) /// /// /// The path may be null when running a inline script. /// public string? ExecutablePath => _executablePath; /// /// Gets the name of the executing application. like argv[0]. (e.g. App.exe, app) /// public string ExecutableName => _executableName; /// /// Gets the directory of the executing application. like argv[0]. (e.g. C:\\Path\To, /path/to) /// public string ExecutableDirectory => _executableDirectory; /// /// Gets or sets the path of the current working directory. /// public string CurrentDirectory { get => Environment.CurrentDirectory; set => Environment.CurrentDirectory = value; } /// /// Gets the path of the current user's home directory. /// public string HomeDirectory => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); /// /// Gets the environment variables as representation. /// public IDictionary Vars { get; } = new EnvironmentVariables(); /// /// Gets the standard input stream. /// public ChellReadableStream StdIn => new ChellReadableStream(this.Console.OpenStandardInput(), this.Console.InputEncoding); /// /// Gets the standard output stream. /// public ChellWritableStream StdOut => new ChellWritableStream(this.Console.OpenStandardOutput(), this.Console.OutputEncoding); /// /// Gets the standard output stream. /// public ChellWritableStream StdErr => new ChellWritableStream(this.Console.OpenStandardError(), this.Console.OutputEncoding); /// /// Gets or sets the default timeout for the process. The value affects the current application. The default value is . /// /// /// If the value is or , the process will not be timed out. /// public TimeSpan ProcessTimeout { get; set; } = TimeSpan.Zero; [EditorBrowsable(EditorBrowsableState.Never)] public void SetCommandLineArgs(string? executablePath, string executableName, string executableDirectory, string[] args) { _arguments = args.ToArray(); _executableName = executableName; _executablePath = executablePath; _executableDirectory = executableDirectory; } } } ================================================ FILE: src/Chell/CommandLineString.cs ================================================ using System; using System.ComponentModel; using System.Diagnostics; namespace Chell { /// /// Workaround for string/FormattableString overload issues /// [EditorBrowsable(EditorBrowsableState.Never)] [DebuggerDisplay("CommandLineString: String={StringValue,nq}; FormattableString={FormattableStringValue,nq}")] public readonly struct CommandLineString { public string? StringValue { get; } public FormattableString? FormattableStringValue { get; } public CommandLineString(string value) { StringValue = value; FormattableStringValue = null; } public CommandLineString(FormattableString value) { StringValue = null; FormattableStringValue = value; } public static implicit operator CommandLineString(string value) { return new CommandLineString(value); } public static implicit operator CommandLineString(FormattableString value) { return new CommandLineString(value); } } } ================================================ FILE: src/Chell/Exports.cs ================================================ using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Chell.Internal; using Chell.IO; using Kokuban; using Kokuban.AnsiEscape; namespace Chell { public static class Exports { public static class Verbosity { /// /// By default, no output is written to the console. /// public const ChellVerbosity Silent = ChellVerbosity.Silent; /// /// Writes a executing command line to the console. /// public const ChellVerbosity CommandLine = ChellVerbosity.CommandLine; /// /// Writes a command output to the console. /// public const ChellVerbosity ConsoleOutputs = ChellVerbosity.ConsoleOutputs; /// /// Writes all command lines and command outputs. /// public const ChellVerbosity Full = ChellVerbosity.Full; } /// /// Gets the current environment. /// public static ChellEnvironment Env => ChellEnvironment.Current; /// /// Gets the standard input stream. /// public static ChellReadableStream StdIn => Env.StdIn; /// /// Gets the standard output stream. /// public static ChellWritableStream StdOut => Env.StdOut; /// /// Gets the standard error stream. /// public static ChellWritableStream StdErr => Env.StdErr; /// /// Gets the command line arguments. like args of a entry point. /// public static IReadOnlyList Arguments => Env.Arguments; /// /// Gets the path of the executing application. like argv[0]. (e.g. C:\\Path\To\App.exe, /path/to/app) /// /// /// The path may be null when running a inline script. /// public static string? ExecutablePath => Env.ExecutablePath; /// /// Gets the name of the executing application. like argv[0]. (e.g. App.exe, app) /// public static string ExecutableName => Env.ExecutableName; /// /// Gets the directory of the executing application. like argv[0]. (e.g. C:\\Path\To, /path/to) /// public static string ExecutableDirectory => Env.ExecutableDirectory; /// /// Gets or sets the path of the current working directory. /// public static string CurrentDirectory { get => Env.CurrentDirectory; set => Env.CurrentDirectory = value; } /// /// Gets the identifier for the current application process. /// public static int ProcessId => Env.ProcessId; /// /// Gets the Kokuban ANSI style builder to decorate texts. /// public static AnsiStyle Chalk => Kokuban.Chalk.Create(KokubanOptions.Default); /// /// Starts the process task with the specified command line. /// /// /// /// public static ProcessTask Run(FormattableString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(commandLine, options); /// /// Starts the process task with the specified command line. /// /// /// /// public static ProcessTask Run(CommandLineString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(commandLine, options); /// /// Starts the process task with the specified command line. /// /// The data to be passed to the standard input of the process. /// /// /// public static ProcessTask Run(Stream inputStream, FormattableString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(inputStream, commandLine, options); /// /// Starts the process task with the specified command line. /// /// The data to be passed to the standard input of the process. /// /// /// public static ProcessTask Run(Stream inputStream, CommandLineString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(inputStream, commandLine, options); /// /// Starts the process task with the specified command line. /// /// The data to be passed to the standard input of the process. /// /// /// public static ProcessTask Run(ReadOnlyMemory inputData, FormattableString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(inputData, commandLine, options); /// /// Starts the process task with the specified command line. /// /// The data to be passed to the standard input of the process. /// /// /// public static ProcessTask Run(ReadOnlyMemory inputData, CommandLineString commandLine, ProcessTaskOptions? options = default) => new ProcessTask(inputData, commandLine, options); /// /// Writes the message to the console. /// /// public static void Echo(object? message = default) => ChellEnvironment.Current.Console.Out.WriteLine(message); /// /// Writes the object details to the console. /// /// /// public static void Dump(T obj) => ObjectDumper.Dump(obj); /// /// Converts the object to a JSON. /// /// public static string ToJson(T obj, JsonSerializerOptions? options = default) => JsonSerializer.Serialize(obj, options); /// /// Converts the JSON to an object. /// /// public static T? FromJson(string json, T shape) => FromJson(json); /// /// Converts the JSON to an object. /// /// public static T? FromJson(string json) => Chell.Extensions.StringExtensions.AsJson(json); /// /// Changes the current directory to the specified path. /// /// /// Dispose the return value to return to the previous directory. /// /// public static IDisposable Cd(string path) => new ChangeDirectoryScope(path); private class ChangeDirectoryScope : IDisposable { private readonly string _previousCurrentDirectory; public ChangeDirectoryScope(string newCurrentDirectory) { _previousCurrentDirectory = Environment.CurrentDirectory; ChangeDirectory(newCurrentDirectory); } public void Dispose() { ChangeDirectory(_previousCurrentDirectory); } private void ChangeDirectory(string path) { CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"cd {path}"); Environment.CurrentDirectory = path; } } /// /// Sleeps for the specified time. /// /// /// public static Task Sleep(TimeSpan timeSpan) => Task.Delay(timeSpan); /// /// Sleeps for the specified time. /// /// /// public static Task Sleep(int seconds) => Task.Delay(TimeSpan.FromSeconds(seconds)); /// /// Get the task to ignore the exception and return . /// /// /// public static Task NoThrow(ProcessTask task) => task.NoThrow(); /// /// Terminates the current process with specified exit code. /// /// public static void Exit(int exitCode = 0) => Environment.Exit(exitCode); /// /// Creates a new directory and any necessary sub-directories in the specified path. /// /// public static void Mkdirp(string path) => Directory.CreateDirectory(path); /// /// Fetches the content of the specified URL using GET method. /// /// /// /// public static Task FetchAsync(string requestUri, CancellationToken cancellationToken = default) { CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchAsync)} {requestUri}"); var httpClient = new HttpClient(); return httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); } /// /// Fetches the content of the specified URL as string using GET method. /// /// /// /// public static async Task FetchStringAsync(string requestUri, CancellationToken cancellationToken = default) { CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchStringAsync)} {requestUri}"); var httpClient = new HttpClient(); var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); res.EnsureSuccessStatusCode(); #if NET5_0_OR_GREATER return await res.Content.ReadAsStringAsync(cancellationToken); #else return await res.Content.ReadAsStringAsync(); #endif } /// /// Fetches the content of the specified URL as byte[] using GET method. /// /// /// /// public static async Task FetchByteArrayAsync(string requestUri, CancellationToken cancellationToken = default) { CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchByteArrayAsync)} {requestUri}"); var httpClient = new HttpClient(); var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); res.EnsureSuccessStatusCode(); #if NET5_0_OR_GREATER return await res.Content.ReadAsByteArrayAsync(cancellationToken); #else return await res.Content.ReadAsByteArrayAsync(); #endif } /// /// Fetches the content of the specified URL as Stream using GET method. /// /// /// /// public static async Task FetchStreamAsync(string requestUri, CancellationToken cancellationToken = default) { CommandLineHelper.WriteCommandLineToConsole(ChellEnvironment.Current.Console, $"{nameof(FetchStreamAsync)} {requestUri}"); var httpClient = new HttpClient(); var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken); res.EnsureSuccessStatusCode(); #if NET5_0_OR_GREATER return await res.Content.ReadAsStreamAsync(cancellationToken); #else return await res.Content.ReadAsStreamAsync(); #endif } /// /// Gets the full path to a command, similar to the `which` command on Unix. /// /// /// public static string Which(string commandName) => Internal.Which.TryGetPath(commandName, out var matchedPath) ? matchedPath : throw new FileNotFoundException($"Command '{commandName}' is not found."); /// /// Gets the full path to a command, similar to the `which` command on Unix. /// /// /// /// public static bool TryWhich(string commandName, out string matchedPath) => Internal.Which.TryGetPath(commandName, out matchedPath); /// /// Enumerates paths under the current directory that match the specified glob pattern. /// /// /// A glob pattern accepts * and ** (e.g. **/*.cs). If the specify a pattern is started with '!', it will be treated as an excluded pattern. /// /// /// public static IEnumerable Glob(params string[] patterns) => Glob(Environment.CurrentDirectory, patterns); /// /// Enumerates paths under the specified directory that match the specified glob pattern. /// /// /// A glob pattern accepts * and ** (e.g. **/*.cs). If the specify a pattern is started with '!', it will be treated as an excluded pattern. /// /// /// /// public static IEnumerable Glob(string baseDir, string[] patterns) { var matcher = new Matcher(); foreach (var pattern in patterns) { if (pattern.StartsWith("!")) { matcher.AddExclude(pattern.Substring(1)); } else { matcher.AddInclude(pattern); } } var result = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(baseDir))); return result.Files .Select(x => Path.GetFullPath(Path.Combine(baseDir, x.Stem))); // NOTE: Microsoft.Extensions.FileSystemGlobbing 5.0.0 does not reflect the root directory in `Path`. } /// /// Displays the message and reads lines entered by the user from the console. /// /// /// public static async Task Prompt(string message) { Console.Write(message); return await Console.In.ReadLineAsync(); } } } ================================================ FILE: src/Chell/Extensions/ChellExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Chell.Internal; namespace Chell.Extensions { public static class ChellExtensions { /// /// Writes a object details to the output. /// /// /// /// public static T Dump(this T value) { return ObjectDumper.Dump(value); } /// /// Writes a object details to the output. /// /// /// /// public static async Task Dump(this Task task) { var result = await task.ConfigureAwait(false); ObjectDumper.Dump(result); return result; } } } ================================================ FILE: src/Chell/Extensions/ProcessOutputExtensions.cs ================================================ using System.Collections.Generic; using System.Text.Json; namespace Chell { public static class ProcessOutputExtensions { /// /// Enumerates lines by converting them to objects as JSON. /// /// /// Converts to the anonymous type specified in argument. /// public static IEnumerable AsJsonLines(this ProcessOutput processOutput, T shape, bool skipEmptyLine = true, JsonSerializerOptions? options = null) => Chell.Extensions.StringExtensions.AsJsonLines(processOutput.AsLines(), skipEmptyLine, options); /// /// Enumerates lines by converting them to objects as JSON. /// public static IEnumerable AsJsonLines(this ProcessOutput processOutput, bool skipEmptyLine = true, JsonSerializerOptions? options = null) => Chell.Extensions.StringExtensions.AsJsonLines(processOutput.AsLines(), skipEmptyLine, options); /// /// Converts the output string to an object as JSON. /// /// /// Converts to the anonymous type specified in argument. /// public static T? AsJson(this ProcessOutput processOutput, T shape, JsonSerializerOptions? options = null) => AsJson(processOutput, options); /// /// Converts the output string to an object as JSON. /// public static T? AsJson(this ProcessOutput processOutput, JsonSerializerOptions? options = null) => JsonSerializer.Deserialize(processOutput.ToString(), options); } } ================================================ FILE: src/Chell/Extensions/ProcessTaskExtensions.Generated.cs ================================================ /// using System.Threading.Tasks; namespace Chell { public static partial class ProcessTaskExtensions { public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2)> GetAwaiter(this (ProcessTask T1, ProcessTask T2) tasks) { static async Task<(ProcessOutput Result1, ProcessOutput Result2)> WhenAllAsync(ProcessTask t1, ProcessTask t2) { var results = await Task.WhenAll(t1, t2).ConfigureAwait(false); return (results[0], results[1]); } return WhenAllAsync(tasks.T1, tasks.T2).GetAwaiter(); } public static System.Runtime.CompilerServices.TaskAwaiter<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3)> GetAwaiter(this (ProcessTask T1, ProcessTask T2, ProcessTask T3) tasks) { static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3) { var results = await Task.WhenAll(t1, t2, t3).ConfigureAwait(false); return (results[0], results[1], results[2]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3).GetAwaiter(); } 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) { static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4) { var results = await Task.WhenAll(t1, t2, t3, t4).ConfigureAwait(false); return (results[0], results[1], results[2], results[3]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4).GetAwaiter(); } 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) { static async Task<(ProcessOutput Result1, ProcessOutput Result2, ProcessOutput Result3, ProcessOutput Result4, ProcessOutput Result5)> WhenAllAsync(ProcessTask t1, ProcessTask t2, ProcessTask t3, ProcessTask t4, ProcessTask t5) { var results = await Task.WhenAll(t1, t2, t3, t4, t5).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5).GetAwaiter(); } 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) { 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) { var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4], results[5]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6).GetAwaiter(); } 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) { 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) { var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4], results[5], results[6]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7).GetAwaiter(); } 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) { 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) { var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8).GetAwaiter(); } 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) { 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) { var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8, t9).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7], results[8]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8, tasks.T9).GetAwaiter(); } 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) { 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) { var results = await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10).ConfigureAwait(false); return (results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7], results[8], results[9]); } return WhenAllAsync(tasks.T1, tasks.T2, tasks.T3, tasks.T4, tasks.T5, tasks.T6, tasks.T7, tasks.T8, tasks.T9, tasks.T10).GetAwaiter(); } } } ================================================ FILE: src/Chell/Extensions/ProcessTaskExtensions.cs ================================================ using System.Threading.Tasks; namespace Chell { public static partial class ProcessTaskExtensions { } } ================================================ FILE: src/Chell/Extensions/ProcessTaskExtensions.tt ================================================ <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".Generated.cs" #> /// using System.Threading.Tasks; namespace Chell { public static partial class ProcessTaskExtensions { <# for (var i = 2; i <= 10; i++) { var inTuple = string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessTask T{x}")); var outTuple = string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessOutput Result{x}")); #> public static System.Runtime.CompilerServices.TaskAwaiter<(<#= outTuple #>)> GetAwaiter(this (<#= inTuple #>) tasks) { static async Task<(<#= outTuple #>)> WhenAllAsync(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"ProcessTask t{x}")) #>) { var results = await Task.WhenAll(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"t{x}")) #>).ConfigureAwait(false); return (<#= string.Join(", ", Enumerable.Range(0, i).Select(x => $"results[{x}]")) #>); } return WhenAllAsync(<#= string.Join(", ", Enumerable.Range(1, i).Select(x => $"tasks.T{x}")) #>).GetAwaiter(); } <# } #> } } ================================================ FILE: src/Chell/Extensions/StringExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace Chell.Extensions { public static class StringExtensions { /// /// Enumerates lines by converting them to objects as JSON. /// /// /// Converts to the anonymous type specified in argument. /// public static IEnumerable AsJsonLines(this IEnumerable lines, T shape, bool skipEmptyLine = true, JsonSerializerOptions? options = null) { return AsJsonLines(lines, skipEmptyLine, options); } /// /// Enumerates lines by converting them to objects as JSON. /// public static IEnumerable AsJsonLines(this IEnumerable lines, bool skipEmptyLine = true, JsonSerializerOptions? options = null) { return lines.Where(x => !skipEmptyLine || !string.IsNullOrWhiteSpace(x)).Select(x => JsonSerializer.Deserialize(x, options)); } /// /// Converts the output string to an object as JSON. /// /// /// Converts to the anonymous type specified in argument. /// public static T? AsJson(this string json, T shape, JsonSerializerOptions? options = null) => AsJson(json, options); /// /// Converts the output string to an object as JSON. /// public static T? AsJson(this string json, JsonSerializerOptions? options = null) => JsonSerializer.Deserialize(json.ToString(), options); } } ================================================ FILE: src/Chell/IO/ChellWrappedStream.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Chell.IO { public partial class ChellWritableStream : ChellWrappedStream { private readonly StreamWriter _writer; public ChellWritableStream(Stream baseStream, Encoding encoding) : base(baseStream) { _writer = new StreamWriter(baseStream, encoding); _writer.AutoFlush = true; } public void Write(byte[] value) => BaseStream.Write(value); public new void Write(ReadOnlySpan value) => BaseStream.Write(value); public ValueTask WriteAsync(byte[] value, CancellationToken cancellationToken = default) => BaseStream.WriteAsync(value, cancellationToken); public new ValueTask WriteAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) => BaseStream.WriteAsync(value, cancellationToken); protected override void Dispose(bool disposing) { _writer.Dispose(); base.Dispose(disposing); } public override async ValueTask DisposeAsync() { await _writer.DisposeAsync(); await base.DisposeAsync(); } } public partial class ChellReadableStream : ChellWrappedStream { private readonly StreamReader _reader; public ChellReadableStream(Stream baseStream, Encoding encoding) : base(baseStream) { _reader = new StreamReader(baseStream, encoding); } public async Task ReadAllBytesAsync(CancellationToken cancellationToken = default) { var bufferWriter = new ArrayBufferWriter(); while (true) { cancellationToken.ThrowIfCancellationRequested(); var readLen = await BaseStream.ReadAsync(bufferWriter.GetMemory(1024 * 32), cancellationToken); if (readLen == 0) { return bufferWriter.WrittenMemory.ToArray(); } bufferWriter.Advance(readLen); } } public byte[] ReadAllBytes() { var bufferWriter = new ArrayBufferWriter(); while (true) { var readLen = BaseStream.Read(bufferWriter.GetSpan(1024 * 32)); if (readLen == 0) { return bufferWriter.WrittenMemory.ToArray(); } bufferWriter.Advance(readLen); } } public async Task ReadToEndAsync() { return await _reader.ReadToEndAsync(); } public string ReadToEnd() { return _reader.ReadToEnd(); } public IEnumerable ReadAllLines() { while (!_reader.EndOfStream) { var line = _reader.ReadLine(); if (line is null) yield break; yield return line; } } public async IAsyncEnumerable ReadAllLinesAsync() { while (!_reader.EndOfStream) { var line = await _reader.ReadLineAsync(); if (line is null) yield break; yield return line; } } protected override void Dispose(bool disposing) { _reader.Dispose(); base.Dispose(disposing); } } public abstract class ChellWrappedStream : Stream { private readonly Stream _baseStream; protected Stream BaseStream => _baseStream; protected ChellWrappedStream(Stream baseStream) { _baseStream = baseStream; } #region Stream Implementation public override void Flush() { _baseStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) { return _baseStream.Read(buffer, offset, count); } public override int Read(Span buffer) { return _baseStream.Read(buffer); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _baseStream.ReadAsync(buffer, offset, count, cancellationToken); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) { return _baseStream.ReadAsync(buffer, cancellationToken); } public override long Seek(long offset, SeekOrigin origin) { return _baseStream.Seek(offset, origin); } public override void SetLength(long value) { _baseStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { _baseStream.Write(buffer, offset, count); } public override void Write(ReadOnlySpan buffer) { _baseStream.Write(buffer); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _baseStream.WriteAsync(buffer, offset, count, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = new CancellationToken()) { return _baseStream.WriteAsync(buffer, cancellationToken); } public override bool CanRead => _baseStream.CanRead; public override bool CanSeek => _baseStream.CanSeek; public override bool CanWrite => _baseStream.CanWrite; public override long Length => _baseStream.Length; public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } #endregion } } ================================================ FILE: src/Chell/IO/ChellWritableStream.Generated.cs ================================================ #nullable enable /// using System; using System.IO; using System.Threading.Tasks; namespace Chell.IO { public partial class ChellWritableStream { public void Write(string? value) => _writer.Write(value); public void WriteLine(string? value) => _writer.WriteLine(value); public Task WriteAsync(string? value) => _writer.WriteAsync(value); public Task WriteLineAsync(string? value) => _writer.WriteLineAsync(value); public void Write(char value) => _writer.Write(value); public void WriteLine(char value) => _writer.WriteLine(value); public Task WriteAsync(char value) => _writer.WriteAsync(value); public Task WriteLineAsync(char value) => _writer.WriteLineAsync(value); public void Write(char[]? value) => _writer.Write(value); public void WriteLine(char[]? value) => _writer.WriteLine(value); public Task WriteAsync(char[]? value) => _writer.WriteAsync(value); public Task WriteLineAsync(char[]? value) => _writer.WriteLineAsync(value); public void Write(object? value) => _writer.Write(value); public void WriteLine(object? value) => _writer.WriteLine(value); public void Write(double value) => _writer.Write(value); public void WriteLine(double value) => _writer.WriteLine(value); public void Write(float value) => _writer.Write(value); public void WriteLine(float value) => _writer.WriteLine(value); public void Write(long value) => _writer.Write(value); public void WriteLine(long value) => _writer.WriteLine(value); public void Write(int value) => _writer.Write(value); public void WriteLine(int value) => _writer.WriteLine(value); public void Write(ReadOnlySpan value) => _writer.Write(value); public void WriteLine(ReadOnlySpan value) => _writer.WriteLine(value); public Task WriteAsync(ReadOnlyMemory value) => _writer.WriteAsync(value); public Task WriteLineAsync(ReadOnlyMemory value) => _writer.WriteLineAsync(value); } } ================================================ FILE: src/Chell/IO/ChellWritableStream.tt ================================================ <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".Generated.cs" #> <# var types = new [] { (0, "string?"), (0, "char"), (0, "char[]?"), (1, "object?"), (1, "double"), (1, "float"), (1, "long"), (1, "int"), (1, "ReadOnlySpan"), (2, "ReadOnlyMemory"), }; #> #nullable enable /// using System; using System.IO; using System.Threading.Tasks; namespace Chell.IO { public partial class ChellWritableStream { <# foreach (var (target, type) in types) { #> <# if (target == 0 || target == 1) { #> public void Write(<#= type #> value) => _writer.Write(value); public void WriteLine(<#= type #> value) => _writer.WriteLine(value); <# } #> <# if (target == 0 || target == 2) { #> public Task WriteAsync(<#= type #> value) => _writer.WriteAsync(value); public Task WriteLineAsync(<#= type #> value) => _writer.WriteLineAsync(value); <# } #> <# } #> } } ================================================ FILE: src/Chell/IO/IConsoleProvider.cs ================================================ using System.Collections.Generic; using System.IO; using System.Text; namespace Chell.IO { public interface IConsoleProvider { Stream OpenStandardInput(); Stream OpenStandardOutput(); Stream OpenStandardError(); Encoding InputEncoding { get; } Encoding OutputEncoding { get; } bool IsInputRedirected { get; } bool IsOutputRedirected { get; } bool IsErrorRedirected { get; } TextWriter Out { get; } TextWriter Error { get; } } } ================================================ FILE: src/Chell/IO/LINQPadConsoleProvider.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Text; using System.Threading.Tasks; namespace Chell.IO { public class LINQPadConsoleProvider : IConsoleProvider { private readonly Pipe _pipe; public Encoding InputEncoding => Console.InputEncoding; public Encoding OutputEncoding => Console.OutputEncoding; public bool IsInputRedirected => Console.IsInputRedirected; public bool IsOutputRedirected => Console.IsOutputRedirected; public bool IsErrorRedirected => Console.IsErrorRedirected; public TextWriter Out { get; } public TextWriter Error { get; } public LINQPadConsoleProvider() { _pipe = new Pipe(); Out = new PipeTextWriter(_pipe.Writer, OutputEncoding); Error = new PipeTextWriter(_pipe.Writer, OutputEncoding); var reader = new StreamReader(_pipe.Reader.AsStream()); _ = Task.Run(async () => { Memory buffer = new char[1024]; while (true) { var read = await reader.ReadAsync(buffer, default); if (read != 0) { Console.Out.Write(buffer.Span.Slice(0, read)); } } }); } public Stream OpenStandardInput() => Console.OpenStandardInput(); public Stream OpenStandardOutput() => _pipe.Writer.AsStream(leaveOpen: true); public Stream OpenStandardError() => _pipe.Writer.AsStream(leaveOpen: true); private class PipeTextWriter : TextWriter { private readonly PipeWriter _writer; public override Encoding Encoding { get; } public PipeTextWriter(PipeWriter writer, Encoding encoding) { _writer = writer; Encoding = encoding; } public override void Write(char value) { Span buffer = stackalloc byte[4]; Span c = stackalloc char[1]; c[0] = value; var written = Encoding.GetBytes(c, buffer); _writer.Write(buffer.Slice(0, written)); _ = _writer.FlushAsync(); } } } } ================================================ FILE: src/Chell/IO/SystemConsoleProvider.cs ================================================ using System; using System.IO; using System.Text; namespace Chell.IO { /// /// Encapsulates console intrinsic members as object. /// public sealed class SystemConsoleProvider : IConsoleProvider { public static IConsoleProvider Instance { get; } = new SystemConsoleProvider(); private SystemConsoleProvider() {} public Stream OpenStandardInput() => Console.OpenStandardInput(); public Stream OpenStandardOutput() => Console.OpenStandardOutput(); public Stream OpenStandardError() => Console.OpenStandardError(); public Encoding InputEncoding => Console.InputEncoding; public Encoding OutputEncoding => Console.OutputEncoding; public bool IsInputRedirected => Console.IsInputRedirected; public bool IsOutputRedirected => Console.IsOutputRedirected; public bool IsErrorRedirected => Console.IsErrorRedirected; public TextWriter Out => Console.Out; public TextWriter Error => Console.Error; } } ================================================ FILE: src/Chell/Internal/CommandLineHelper.cs ================================================ using System; using System.Collections; using System.Linq; using Chell.IO; using Chell.Shell; using Kokuban; // ReSharper disable CoVariantArrayConversion namespace Chell.Internal { internal class CommandLineHelper { public static void WriteCommandLineToConsole(IConsoleProvider console, string commandLine, ChellVerbosity? verbosity = default) { verbosity ??= ChellEnvironment.Current.Verbosity; if (verbosity.Value.HasFlag(ChellVerbosity.CommandLine)) { var parts = commandLine.Split(' ', 2); if (LINQPadHelper.RunningOnLINQPad) { LINQPadHelper.WriteRawHtml( $"$ {EscapeHtml(parts[0])}{EscapeHtml((parts.Length > 1 ? " " + parts[1] : string.Empty))}"); } else { console.Out.WriteLine("$ " + (Chalk.BrightGreen + parts[0]) + (parts.Length > 1 ? " " + parts[1] : string.Empty)); } } static string EscapeHtml(string s) => s.Replace("&", "&").Replace("\"", """).Replace("<", ">"); } public static string Expand(FormattableString commandLine, IShellExecutor shellExecutor) { return string.Format(commandLine.Format.Trim(), commandLine.GetArguments().Select(x => { return x switch { ProcessOutput procOutput => shellExecutor.Escape(procOutput.Output.TrimEnd('\n')), string s => shellExecutor.Escape(s), IEnumerable enumerable => string.Join(" ", enumerable.OfType().Select(y => shellExecutor.Escape(y.ToString() ?? string.Empty))), null => string.Empty, _ => shellExecutor.Escape(x.ToString() ?? string.Empty), }; }).ToArray()); } public static (string Command, string Arguments) Parse(FormattableString commandLine) { var (command, argumentsFormat) = Parse(commandLine.Format.Trim()); return (command, string.Format(argumentsFormat, commandLine.GetArguments().Select(x => { if (x is IEnumerable enumerable) { return string.Join(" ", enumerable.OfType().Select(y => Escape(y.ToString() ?? string.Empty))); } return Escape(x?.ToString() ?? string.Empty); }).ToArray())); } public static (string Command, string Arguments) Parse(string commandLine) { if (commandLine.StartsWith("\"")) { var pos = commandLine.IndexOf('"', 1); if (pos == -1) { throw new InvalidOperationException("Invalid Command"); } return (Command: commandLine.Substring(1, pos), Arguments: commandLine.Substring(pos)); } else { var parts = commandLine.Split(' ', 2); return (Command: parts[0], Arguments: parts.Length == 1 ? string.Empty : parts[1]); } } public static string Escape(string v) => $"\"{v.Replace("`", "``").Replace("\"", "`\"")}\""; } } ================================================ FILE: src/Chell/Internal/EnvironmentVariables.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; namespace Chell.Internal { internal class EnvironmentVariables : IDictionary { public IEnumerator> GetEnumerator() { return Environment.GetEnvironmentVariables() .OfType() .Select(x => KeyValuePair.Create((string)x.Key, (string)(x.Value ?? string.Empty))) .GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public void Add(KeyValuePair item) { Environment.SetEnvironmentVariable(item.Key, item.Value); } public void Clear() { throw new NotSupportedException(); } public bool Contains(KeyValuePair item) { return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(item.Key)); } public void CopyTo(KeyValuePair[] array, int arrayIndex) { throw new NotSupportedException(); } public bool Remove(KeyValuePair item) { if (Contains(item)) { Environment.SetEnvironmentVariable(item.Key, null); return true; } return false; } public int Count => Environment.GetEnvironmentVariables().Count; public bool IsReadOnly => false; public void Add(string key, string value) { Environment.SetEnvironmentVariable(key, value); } public bool ContainsKey(string key) { return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(key)); } public bool Remove(string key) { if (ContainsKey(key)) { Environment.SetEnvironmentVariable(key, null); return true; } return false; } public bool TryGetValue(string key, out string value) { var tmpValue = Environment.GetEnvironmentVariable(key); if (string.IsNullOrWhiteSpace(tmpValue)) { value = string.Empty; return false; } value = tmpValue; return true; } public string this[string key] { get => TryGetValue(key, out var value) ? value : string.Empty; set => Add(key, value); } public ICollection Keys => Environment.GetEnvironmentVariables().OfType().Select(x => (string)x.Key).ToArray(); public ICollection Values => Environment.GetEnvironmentVariables().OfType().Select(x => (string)(x.Value ?? string.Empty)).ToArray(); } } ================================================ FILE: src/Chell/Internal/LINQPadHelper.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Pipelines; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Chell.Internal { internal class LINQPadHelper { private static bool? _runningOnLINQPad; internal static bool RunningOnLINQPad => _runningOnLINQPad ??= Type.GetType("LINQPad.Util, LINQPad.Runtime") != null; public static void WriteRawHtml(string html) { Debug.Assert(RunningOnLINQPad); var t = Type.GetType("LINQPad.Util, LINQPad.Runtime"); if (t != null) { var obj = t.InvokeMember("RawHtml", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new[] {html}); ObjectDumper.Dump(obj); } } } } ================================================ FILE: src/Chell/Internal/ObjectDumper.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Unicode; using System.Threading.Tasks; namespace Chell.Internal { internal static class ObjectDumper { public static T Dump(T obj) => DumpMethodCache.Method(obj); private static class DumpMethodCache { public static MethodInfo? LINQPadDumpMethod { get; } static DumpMethodCache() { var linqPadExtensionsType = Type.GetType("LINQPad.Extensions, LINQPad.Runtime"); if (linqPadExtensionsType != null) { LINQPadDumpMethod = linqPadExtensionsType.GetMethods() .FirstOrDefault(x => x.Name == "Dump" && x.GetParameters().Length == 1); } } } private static class DumpMethodCache { public static Func Method { get; } static DumpMethodCache() { if (DumpMethodCache.LINQPadDumpMethod != null) { var closedDumpMethod = DumpMethodCache.LINQPadDumpMethod.MakeGenericMethod(typeof(T)); Method = (Func)closedDumpMethod.CreateDelegate(typeof(Func)); } else { Method = x => { Console.WriteLine(JsonSerializer.Serialize(x, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), WriteIndented = true })); return x; }; } } } } } ================================================ FILE: src/Chell/Internal/OutputSink.cs ================================================ using System; using System.IO; using System.IO.Pipelines; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Chell.Internal { internal class OutputSink : IDisposable, IAsyncDisposable { private readonly Encoding _encoding; private readonly Pipe _outputPipe; private readonly Pipe _errorPipe; private readonly MemoryStream _outputBuffer; private readonly MemoryStream _errorBuffer; private readonly MemoryStream _combinedBuffer; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Task _readWriteTaskOutput; private readonly Task _readWriteTaskError; internal PipeWriter OutputWriter => _outputPipe.Writer; internal PipeWriter ErrorWriter => _errorPipe.Writer; public ReadOnlyMemory OutputBinary => new ReadOnlyMemory(_outputBuffer.GetBuffer(), 0, (int)_outputBuffer.Length); public ReadOnlyMemory ErrorBinary => new ReadOnlyMemory(_errorBuffer.GetBuffer(), 0, (int)_errorBuffer.Length); public ReadOnlyMemory CombinedBinary => new ReadOnlyMemory(_combinedBuffer.GetBuffer(), 0, (int)_combinedBuffer.Length); public string Output => (_outputBuffer is { Length: > 0} s) ? _encoding.GetString(OutputBinary.Span) : string.Empty; public string Error => (_errorBuffer is { Length: > 0 } s) ? _encoding.GetString(ErrorBinary.Span) : string.Empty; public string Combined => (_combinedBuffer is { Length: > 0 } s) ? _encoding.GetString(CombinedBinary.Span) : string.Empty; public OutputSink(Encoding encoding) { _encoding = encoding; _outputPipe = new Pipe(); _errorPipe = new Pipe(); _cancellationTokenSource = new CancellationTokenSource(); _outputBuffer = new MemoryStream(); _errorBuffer = new MemoryStream(); _combinedBuffer = new MemoryStream(); _readWriteTaskOutput = RunReadWriteLoopAsync(_outputPipe.Reader, _outputBuffer, _cancellationTokenSource.Token); _readWriteTaskError = RunReadWriteLoopAsync(_errorPipe.Reader, _errorBuffer, _cancellationTokenSource.Token); } public async Task CompleteAsync() { await _outputPipe.Writer.CompleteAsync().ConfigureAwait(false); await _errorPipe.Writer.CompleteAsync().ConfigureAwait(false); try { _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); await _readWriteTaskOutput.ConfigureAwait(false); await _readWriteTaskError.ConfigureAwait(false); } catch (OperationCanceledException) { } } private async Task RunReadWriteLoopAsync(PipeReader reader, Stream dest, CancellationToken cancellationToken) { while (true) { cancellationToken.ThrowIfCancellationRequested(); var result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); if (result.Buffer.IsSingleSegment) { dest.Write(result.Buffer.FirstSpan); _combinedBuffer.Write(result.Buffer.FirstSpan); } else { foreach (var segment in result.Buffer) { dest.Write(segment.Span); _combinedBuffer.Write(segment.Span); } } reader.AdvanceTo(result.Buffer.End); if (result.IsCanceled || result.IsCompleted) { return; } } } public void Dispose() { try { CompleteAsync().Wait(); } catch { } } public async ValueTask DisposeAsync() { try { await CompleteAsync().ConfigureAwait(false); } catch { } } } } ================================================ FILE: src/Chell/Internal/StandardInput.cs ================================================ using System; using System.Threading; namespace Chell.Internal { internal class StandardInput { private static readonly Lazy _pipe; internal static StreamPipe Pipe => _pipe.Value; static StandardInput() { _pipe = new Lazy(() => new StreamPipe(Console.OpenStandardInput()), LazyThreadSafetyMode.ExecutionAndPublication); } } } ================================================ FILE: src/Chell/Internal/StreamPipe.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Chell.Internal { internal class StreamPipe { private readonly Pipe _pipe; private readonly Stream _baseStream; private readonly Task _copyTask; private readonly Task _readerTask; private readonly CancellationTokenSource _cancellationTokenSourceCopyStreamToPipe; private readonly CancellationTokenSource _cancellationTokenSource; private readonly object _syncLock = new object(); private readonly List _destinations = new List(); private readonly TaskCompletionSource _destinationsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private bool _shutdown; public StreamPipe(Stream baseStream) { _pipe = new Pipe(new PipeOptions()); _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSourceCopyStreamToPipe = new CancellationTokenSource(); _copyTask = CopyStreamToPipeAsync(_cancellationTokenSourceCopyStreamToPipe.Token); _readerTask = RunReadLoopAsync(_cancellationTokenSource.Token); } private async Task CopyStreamToPipeAsync(CancellationToken cancellationToken) { try { await _baseStream.CopyToAsync(_pipe.Writer, cancellationToken).ConfigureAwait(false); await _pipe.Writer.CompleteAsync().ConfigureAwait(false); } catch (Exception ex) { await _pipe.Writer.CompleteAsync(ex).ConfigureAwait(false); } } public async Task CompleteAsync() { _shutdown = true; _cancellationTokenSource.CancelAfter(1000); _cancellationTokenSourceCopyStreamToPipe.CancelAfter(1000); try { await _copyTask.ConfigureAwait(false); await _readerTask.ConfigureAwait(false); } catch (OperationCanceledException) { } } public void Ready() { _destinationsReady.TrySetResult(true); } public StreamPipe Connect(Stream stream) { lock (_syncLock) { _destinations.Add(stream ?? throw new ArgumentNullException(nameof(stream))); } return this; } public StreamPipe Connect(PipeWriter writer) { lock (_syncLock) { _destinations.Add(writer ?? throw new ArgumentNullException(nameof(writer))); } return this; } public StreamPipe Disconnect(Stream stream) { lock (_syncLock) { _destinations.Remove(stream ?? throw new ArgumentNullException(nameof(stream))); } return this; } public StreamPipe Disconnect(PipeWriter writer) { lock (_syncLock) { _destinations.Remove(writer ?? throw new ArgumentNullException(nameof(writer))); } return this; } private async Task RunReadLoopAsync(CancellationToken cancellationToken) { var cancellationTokenTask = new TaskCompletionSource(); await using var cancellationTokenRegistration = cancellationToken.Register(() => cancellationTokenTask.TrySetCanceled(cancellationToken)); while (true) { cancellationToken.ThrowIfCancellationRequested(); // Wait for the destination to be available. object[] dests; lock (_syncLock) { dests = _destinations.ToArray(); } while (dests.Length == 0) { if (_shutdown) return; cancellationToken.ThrowIfCancellationRequested(); // Wait for signal to start sending to the destinations. // This will wait only once after the process starts. await Task.WhenAny(cancellationTokenTask.Task, _destinationsReady.Task).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); lock (_syncLock) { dests = _destinations.ToArray(); } if (dests.Length == 0) { await Task.Yield(); } } // Reads from the pipe reader. var result = await _pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); // Get the current destination again. // If there is no destination after reading, do not advance the pipe, and go to the top of the loop. lock (_syncLock) { var destsCurrent = _destinations.ToArray(); if (!dests.SequenceEqual(destsCurrent)) { dests = destsCurrent; if (dests.Length == 0) { _pipe.Reader.AdvanceTo(result.Buffer.Start); continue; } } } // Writes to the destinations. await Task.WhenAll(dests.Select(async x => { var writeTask = x switch { Stream stream => WriteAsync(stream, result, cancellationToken), PipeWriter writer => WriteAsync(writer, result, cancellationToken), _ => throw new NotSupportedException() }; // NOTE: The destination may be closed first. // When the destination is closed, the task throws an IOException (Broken pipe). try { await writeTask.ConfigureAwait(false); } catch (IOException) { } })).ConfigureAwait(false); _pipe.Reader.AdvanceTo(result.Buffer.End); if (result.IsCanceled || result.IsCompleted) { return; } } } private static async ValueTask WriteAsync(Stream stream, ReadResult result, CancellationToken cancellationToken) { if (!result.Buffer.IsEmpty) { if (result.Buffer.IsSingleSegment) { await stream.WriteAsync(result.Buffer.First, cancellationToken).ConfigureAwait(false); } else { foreach (var segment in result.Buffer) { await stream.WriteAsync(segment, cancellationToken).ConfigureAwait(false); } } await stream.FlushAsync(cancellationToken).ConfigureAwait(false); } if (result.IsCompleted || result.IsCanceled) { stream.Close(); } } private static async ValueTask WriteAsync(PipeWriter writer, ReadResult result, CancellationToken cancellationToken) { if (!result.Buffer.IsEmpty) { if (result.Buffer.IsSingleSegment) { await writer.WriteAsync(result.Buffer.First, cancellationToken).ConfigureAwait(false); } else { foreach (var segment in result.Buffer) { await writer.WriteAsync(segment, cancellationToken).ConfigureAwait(false); } } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } if (result.IsCompleted || result.IsCanceled) { await writer.CompleteAsync().ConfigureAwait(false); } } } } ================================================ FILE: src/Chell/Internal/Which.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace Chell.Internal { public static class Which { public static bool TryGetPath(string commandName, out string matchedPath) { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var paths = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(isWindows ? ';' : ':'); var pathExts = Array.Empty(); if (isWindows) { paths = paths.Prepend(Environment.CurrentDirectory).ToArray(); pathExts = (Environment.GetEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC").Split(';'); } foreach (var path in paths) { // /path/to/foo.ext foreach (var ext in pathExts) { var fullPath = Path.Combine(path, $"{commandName}{ext}"); if (File.Exists(fullPath)) { matchedPath = fullPath; return true; } } // /path/to/foo { var fullPath = Path.Combine(path, commandName); if (File.Exists(fullPath)) { matchedPath = fullPath; return true; } } } matchedPath = string.Empty; return false; } } } ================================================ FILE: src/Chell/ProcessOutput.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Text; using Chell.Internal; namespace Chell { /// /// Provides the outputs and results from the process. /// /// /// If the output is redirected or piped, it will not be captured. /// public class ProcessOutput : IEnumerable { /// /// Gets the standard outputs as . /// public string Output => Sink.Output; /// /// Gets the standard errors as . /// public string Error => Sink.Error; /// /// Gets the standard outputs and standard errors as . /// public string Combined => Sink.Combined; /// /// Gets the standard outputs as sequence. /// public ReadOnlyMemory OutputBinary => Sink.OutputBinary; /// /// Gets the standard errors as sequence. /// public ReadOnlyMemory ErrorBinary => Sink.ErrorBinary; /// /// Gets the standard outputs and standard errors as sequence. /// public ReadOnlyMemory CombinedBinary => Sink.CombinedBinary; /// /// Get the exit code when the process terminated. /// public int ExitCode { get; internal set; } internal OutputSink Sink { get; } public ProcessOutput(Encoding encoding) { Sink = new OutputSink(encoding); } public static implicit operator string(ProcessOutput processOutput) => processOutput.ToString(); /// /// Gets the standard outputs and standard errors as . /// public override string ToString() => Combined; public IEnumerator GetEnumerator() => AsLines().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Gets the standard outputs and standard errors as lines. /// public IEnumerable AsLines(bool trimEnd = false) => (trimEnd ? Combined.TrimEnd('\n', '\r') : Combined).Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); } } ================================================ FILE: src/Chell/ProcessTask.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Chell.Internal; namespace Chell { /// /// Represents the execution task of a process. /// public class ProcessTask { private static readonly TimeSpan ProcessStartScheduledDelay = TimeSpan.FromMilliseconds(250); private static int _idSequence = 0; private readonly int _id; private readonly DateTimeOffset _startedAt; private readonly Lazy> _taskLazy; private readonly ProcessOutput _output; private readonly ProcessTaskOptions _options; private readonly CancellationTokenSource _processCancellation; private readonly CancellationTokenRegistration _processCancellationRegistration; private readonly object _syncLock = new object(); private string? _processName; private Process? _process; private ExceptionDispatchInfo? _processException; private Stream? _stdInStream; private StreamPipe? _stdOutPipe; private StreamPipe? _stdErrorPipe; private bool _piped; private bool _hasStandardIn; private bool _suppressPipeToConsole; private bool _suppressExceptionExitCodeNonZero; /// /// Gets the process of the task. If the process is not started yet, the property returns null. /// public Process? Process => _process; /// /// Gets the command line string of the task. /// public string CommandLine { get; } /// /// Gets the command name of the task. /// /// /// This value is passed to . /// public string Command { get; } /// /// Gets the arguments of the task. /// /// /// The value is passed to . It may be escaped or quoted. /// public string Arguments { get; } /// /// Gets the previous task of the task if piped. /// public ProcessTask? PreviousTask { get; private set; } public ProcessTask(FormattableString commandLine, ProcessTaskOptions? options = default) : this(default(Stream?), commandLine, options) { } public ProcessTask(Stream? inputStream, FormattableString commandLine, ProcessTaskOptions? options = default) : this(inputStream, CommandLineHelper.Expand(commandLine, options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) { } public ProcessTask(ReadOnlyMemory inputData, FormattableString commandLine, ProcessTaskOptions? options = default) : this(new MemoryStream(inputData.ToArray()), CommandLineHelper.Expand(commandLine, options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor), options) { } // NOTE: The overload of `string commandLine` cannot be made public due to restrictions. public ProcessTask(CommandLineString commandLine, ProcessTaskOptions? options = default) : this(default(Stream?), commandLine, options) { } public ProcessTask(Stream? inputStream, CommandLineString commandLine, ProcessTaskOptions? options = default) : 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) { } public ProcessTask(ReadOnlyMemory inputData, CommandLineString commandLine, ProcessTaskOptions? options = default) : 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) { } private ProcessTask(Stream? inputStream, string commandLine, ProcessTaskOptions? options) : this(inputStream, commandLine, (options?.ShellExecutor ?? ChellEnvironment.Current.Shell.Executor).GetCommandAndArguments(commandLine), options) { } public ProcessTask(string command, string arguments, ProcessTaskOptions? options = default) : this(default(Stream?), command, arguments, options) { } public ProcessTask(Stream? inputStream, string command, string arguments, ProcessTaskOptions? options = default) : this(inputStream, $"{command} {arguments}", (command, arguments), options) { } public ProcessTask(ReadOnlyMemory inputData, string command, string arguments, ProcessTaskOptions? options = default) : this(new MemoryStream(inputData.ToArray()), $"{command} {arguments}", (command, arguments), options) { } private ProcessTask(Stream? inputStream, string commandLine, (string Command, string Arguments) commandAndArguments, ProcessTaskOptions? options = default) { _startedAt = DateTimeOffset.Now; _options = options ?? new ProcessTaskOptions(); _id = Interlocked.Increment(ref _idSequence); _processCancellation = new CancellationTokenSource(); _processCancellationRegistration = _processCancellation.Token.Register(() => { WriteDebugTrace($"ProcessTimedOut: Pid={_process?.Id}; StartedAt={_startedAt}; Elapsed={DateTimeOffset.Now - _startedAt}"); #if NET5_0_OR_GREATER || NETCOREAPP3_0 || NETCOREAPP3_1 _process?.Kill(true); #else // TODO: .NET Standard 2.0 or 2.1 does not support kill child processes. _process?.Kill(); #endif }); _output = new ProcessOutput(_options.ShellExecutor.Encoding); _taskLazy = new Lazy>(AsTaskCore, LazyThreadSafetyMode.ExecutionAndPublication); _suppressPipeToConsole = !_options.Verbosity.HasFlag(ChellVerbosity.ConsoleOutputs); CommandLine = commandLine ?? throw new ArgumentNullException(nameof(commandLine)); (Command, Arguments) = commandAndArguments; if (inputStream != null) { // Set the stdin stream and start a process immediately. ConnectStreamToStandardInput(inputStream); } else if (_options.RedirectStandardInput) { // Enable stdin redirection and start a process immediately. RedirectStandardInput(); } else { // Delay startup to allow time to configure the stdin stream. // If a Task is requested (e.g. `await`, `AsTask`, `Pipe` ...), it will be started immediately. _ = ScheduleStartProcessAsync(); } WriteDebugTrace($"Created: {commandLine}"); async Task ScheduleStartProcessAsync() { await Task.Delay(ProcessStartScheduledDelay).ConfigureAwait(false); EnsureProcess(); } } public static ProcessTask operator |(ProcessTask a, FormattableString b) => a.Pipe(new ProcessTask(b)); public static ProcessTask operator |(ProcessTask a, Stream b) => a.Pipe(b); public static ProcessTask operator |(ProcessTask a, ProcessTask b) => a.Pipe(b); public static ProcessTask operator |(Stream a, ProcessTask b) { b.ConnectStreamToStandardInput(a); return b; } public static ProcessTask operator |(ReadOnlyMemory a, ProcessTask b) { b.ConnectStreamToStandardInput(new MemoryStream(a.ToArray())); return b; } public static implicit operator Task(ProcessTask task) => task.AsTask(); public static implicit operator Task(ProcessTask task) => task.AsTask(); /// /// Gets the output of the process as Task. /// /// public async Task AsTask() { try { return await _taskLazy.Value.ConfigureAwait(false); } catch when (_suppressExceptionExitCodeNonZero) { return _output; } } public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { return AsTask().GetAwaiter(); } /// /// Gets the exit code of the process as Task. /// public Task ExitCode { get { if (_taskLazy.Value.IsCompleted) { return Task.FromResult(_output.ExitCode); } return _taskLazy.Value.ContinueWith(x => _output.ExitCode); } } /// /// Configures the task to ignore the exception when the process returns exit code with non-zero. /// /// public ProcessTask NoThrow() { _suppressExceptionExitCodeNonZero = true; return this; } public override string ToString() { return PreviousTask != null ? $"{PreviousTask} | {CommandLine}" : $"{CommandLine}"; } /// /// Enables standard output redirection of the process. /// /// /// 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 . /// public void RedirectStandardInput(bool immediateLaunchProcess = true) { WriteDebugTrace($"RedirectStandardInput: immediateLaunchProcess={immediateLaunchProcess}"); lock (_syncLock) { if (_process != null) { throw new InvalidOperationException("The process has already been started. Redirecting standard input must be done before the process has started."); } _hasStandardIn = true; } if (immediateLaunchProcess) { EnsureProcess(); } } /// /// Connects a stream to the standard input of the process. /// /// /// 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. /// /// public void ConnectStreamToStandardInput(Stream stream) { lock (_syncLock) { if (_stdInStream != null) throw new InvalidOperationException("The standard input has already connected to the process."); _stdInStream = stream ?? throw new ArgumentNullException(nameof(stream)); } RedirectStandardInput(); } /// /// Suppresses output to the console if the standard output of the process is not configured. /// /// public ProcessTask SuppressConsoleOutputs() { _suppressPipeToConsole = true; return this; } /// /// Pipes the standard output to the stream. /// /// /// public ProcessTask Pipe(Stream stream) { EnsureProcess(); if (_stdOutPipe != null && _stdErrorPipe != null) { _stdOutPipe.Connect(stream); } _piped = true; ReadyPipe(); return this; } /// /// Pipes the standard output to the another process. /// /// /// public ProcessTask Pipe(ProcessTask nextProcess) { // First, enable standard input redirection before starting a process for the next ProcessTask. nextProcess.RedirectStandardInput(immediateLaunchProcess: false); // Second, start a process. EnsureProcess(); // Third, start a process for the next ProcessTask. nextProcess.EnsureProcess(); if (_stdOutPipe != null && _stdErrorPipe != null) { if (nextProcess.Process != null) { WriteDebugTrace($"Pipe: {Process!.Id} -> {nextProcess.Process.Id}"); _stdOutPipe.Connect(nextProcess.Process.StandardInput.BaseStream ?? Stream.Null); } } nextProcess.PreviousTask = this; _piped = true; ReadyPipe(); return nextProcess; } private void EnsureProcess() { lock (_syncLock) { if (_process is null && _processException is null) { StartProcess(); } } } private void StartProcess() { Debug.Assert(_process is null); Debug.Assert(_processException is null); // Enable only when stdin is redirected or has input stream. // If RedirectStandardInput or CreateNoWindow is set to 'true', a process will not be interactive. var procStartInfo = new ProcessStartInfo { FileName = Command, Arguments = Arguments, UseShellExecute = false, CreateNoWindow = _options.Console.IsInputRedirected, RedirectStandardOutput = true, RedirectStandardInput = _options.Console.IsInputRedirected || _hasStandardIn, RedirectStandardError = true, WorkingDirectory = _options.WorkingDirectory ?? string.Empty, }; if (_options.Verbosity.HasFlag(ChellVerbosity.CommandLine)) { CommandLineHelper.WriteCommandLineToConsole(_options.Console, CommandLine, _options.Verbosity); } try { _processName = procStartInfo.FileName; _process = Process.Start(procStartInfo)!; WriteDebugTrace($"Process.Start: Pid={_process.Id}; HasStandardIn={_hasStandardIn}; StandardIn={_stdInStream}; IsInputRedirected={Console.IsInputRedirected}"); // Set a timeout if the option has non-zero or max-value. if (_options.Timeout != TimeSpan.MaxValue && _options.Timeout != TimeSpan.Zero) { _processCancellation.CancelAfter(_options.Timeout); } // Connect the stream to the standard input. if (_stdInStream != null) { _ = CopyCoreAsync(_stdInStream, _process.StandardInput.BaseStream); } _stdOutPipe = new StreamPipe(Process?.StandardOutput.BaseStream ?? Stream.Null); _stdErrorPipe = new StreamPipe(Process?.StandardError.BaseStream ?? Stream.Null); } catch (Exception e) { _processException = ExceptionDispatchInfo.Capture(e); } static async Task CopyCoreAsync(Stream src, Stream dest) { await UnbufferedCopyToAsync(src, dest).ConfigureAwait(false); dest.Close(); } } private void ReadyPipe() { WriteDebugTrace($"ReadyPipe: Pid={Process?.Id}; Piped={_piped}; _stdOutPipe={_stdOutPipe}; _stdErrorPipe={_stdErrorPipe}"); if (!_piped) { if (!_suppressPipeToConsole) { _stdOutPipe?.Connect(_options.Console.OpenStandardOutput()); _stdErrorPipe?.Connect(_options.Console.OpenStandardError()); } _stdOutPipe?.Connect(_output.Sink.OutputWriter); _stdErrorPipe?.Connect(_output.Sink.ErrorWriter); } _stdOutPipe?.Ready(); _stdErrorPipe?.Ready(); } private static async Task UnbufferedCopyToAsync(Stream src, Stream dest, CancellationToken cancellationToken = default) { var buffer = new byte[80 * 1024]; while (true) { var read = await src.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); if (read == 0) { return; } await dest.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); await dest.FlushAsync(cancellationToken).ConfigureAwait(false); } } private async Task ThrowIfParentTaskHasThrownProcessException(bool awaitForComplete) { if (PreviousTask != null) { // First, throw an exception for the parent task. await PreviousTask.ThrowIfParentTaskHasThrownProcessException(awaitForComplete).ConfigureAwait(false); // Second, Start the process. PreviousTask.EnsureProcess(); // Third, If the process is failed to start, the task will be Faulted state immediately. We should throw an exception of prev task here. var t = PreviousTask.AsTask(); if (t.IsFaulted) { await t.ConfigureAwait(false); } if (!t.IsCompleted && awaitForComplete) { await t.ConfigureAwait(false); } } } private async Task AsTaskCore() { await ThrowIfParentTaskHasThrownProcessException(awaitForComplete:false).ConfigureAwait(false); EnsureProcess(); if (Process is {} proc) { if (_stdOutPipe is null || _stdErrorPipe is null) throw new InvalidOperationException(); // If we have no stdin stream and Console's StandardInput is redirected, connects them automatically. var connectStdInToPipe = _options.EnableAutoWireStandardInput && !_hasStandardIn && _options.Console.IsInputRedirected /*Non-Interactive*/; if (connectStdInToPipe) { StandardInput.Pipe.Connect(proc.StandardInput.BaseStream); StandardInput.Pipe.Ready(); } ReadyPipe(); try { #if NET5_0_OR_GREATER await proc.WaitForExitAsync().ConfigureAwait(false); #else await Task.Run(() => proc.WaitForExit()).ConfigureAwait(false); #endif } finally { _processCancellationRegistration.Dispose(); } _output.ExitCode = proc.ExitCode; WriteDebugTrace($"ProcessExited: Pid={proc.Id}; ExitCode={proc.ExitCode}"); if (connectStdInToPipe) { StandardInput.Pipe.Disconnect(proc.StandardInput.BaseStream); } // Flush output streams/pipes WriteDebugTrace($"Pipe/Sink.CompleteAsync: Pid={proc.Id}"); await _stdOutPipe.CompleteAsync().ConfigureAwait(false); await _stdErrorPipe.CompleteAsync().ConfigureAwait(false); await _output.Sink.CompleteAsync().ConfigureAwait(false); WriteDebugTrace($"Pipe/Sink.CompleteAsync:Done: Pid={proc.Id}"); await ThrowIfParentTaskHasThrownProcessException(awaitForComplete: true).ConfigureAwait(false); if (_output.ExitCode != 0) { var ex = new ProcessTaskException(_processName ?? "Unknown", proc.Id, this, _output); if (_processCancellation.IsCancellationRequested) { throw new OperationCanceledException($"The process has reached the timeout. (Timeout: {_options.Timeout})", ex); } else { throw ex; } } } else { _output.ExitCode = 127; _processCancellationRegistration.Dispose(); if (_processException != null) { throw new ProcessTaskException(this, _output, _processException.SourceException); } else { throw new ProcessTaskException(this, _output); } } return _output; } [Conditional("DEBUG")] private void WriteDebugTrace(string s) { if (_options.Verbosity.HasFlag(ChellVerbosity.Debug)) { _options.Console.Out.WriteLine($"[DEBUG][{_id}] {s}"); } } } } ================================================ FILE: src/Chell/ProcessTaskException.cs ================================================ using System; namespace Chell { /// /// Represents an error that occurs during process execution. /// public class ProcessTaskException : Exception { public ProcessTask ProcessTask { get; } public ProcessOutput Output { get; } public ProcessTaskException(string processName, int processId, ProcessTask processTask, ProcessOutput output, Exception? innerException = default) : base($"Process '{processName}' ({processId}) has exited with exit code {output.ExitCode}. (Executed command: {processTask.Command} {processTask.Arguments})", innerException) { ProcessTask = processTask; Output = output; } public ProcessTaskException(ProcessTask processTask, ProcessOutput output, Exception? innerException = default) : base($"Failed to start the process. (Executed command: {processTask.Command} {processTask.Arguments})", innerException) { ProcessTask = processTask; Output = output; } } } ================================================ FILE: src/Chell/ProcessTaskOptions.cs ================================================ using System; using Chell.IO; using Chell.Shell; namespace Chell { public class ProcessTaskOptions { /// /// Gets or sets whether to enable automatic wiring of standard input to the process. The default value is true. /// public bool EnableAutoWireStandardInput { get; set; } /// /// Gets or sets to enable standard input redirection. The default value is false. /// public bool RedirectStandardInput { get; set; } /// /// Gets or sets the shell executor. The default value is ChellEnvironment.Current.Shell.Executor. /// public IShellExecutor ShellExecutor { get; set; } /// /// Gets or sets the console provider. The default value is ChellEnvironment.Current.Console. /// public IConsoleProvider Console { get; set; } /// /// Gets or sets the verbosity. The default value is ChellEnvironment.Current.Verbosity. /// public ChellVerbosity Verbosity { get; set; } /// /// Gets or sets the working directory for the process. /// public string? WorkingDirectory { get; set; } /// /// Gets or sets the duration to timeout the process. The default value is ChellEnvironment.Current.ProcessTimeout. /// /// /// If the value is or , the process will not be timed out. /// public TimeSpan Timeout { get; set; } public ProcessTaskOptions( bool? redirectStandardInput = default, bool? enableAutoWireStandardInput = default, ChellVerbosity? verbosity = default, IShellExecutor? shellExecutor = default, IConsoleProvider? console = default, string? workingDirectory = default, TimeSpan? timeout = default ) { RedirectStandardInput = redirectStandardInput ?? false; EnableAutoWireStandardInput = enableAutoWireStandardInput ?? true; ShellExecutor = shellExecutor ?? ChellEnvironment.Current.Shell.Executor; Console = console ?? ChellEnvironment.Current.Console; Verbosity = verbosity ?? ChellEnvironment.Current.Verbosity; WorkingDirectory = workingDirectory ?? workingDirectory; Timeout = timeout ?? ChellEnvironment.Current.ProcessTimeout; } private ProcessTaskOptions(ProcessTaskOptions orig) { RedirectStandardInput = orig.RedirectStandardInput; EnableAutoWireStandardInput = orig.EnableAutoWireStandardInput; ShellExecutor = orig.ShellExecutor; Console = orig.Console; Verbosity = orig.Verbosity; WorkingDirectory = orig.WorkingDirectory; Timeout = orig.Timeout; } public ProcessTaskOptions WithRedirectStandardInput(bool redirectStandardInput) => new ProcessTaskOptions(this) { RedirectStandardInput = redirectStandardInput }; public ProcessTaskOptions WithEnableAutoWireStandardInput(bool enableAutoWireStandardInput) => new ProcessTaskOptions(this) { EnableAutoWireStandardInput = enableAutoWireStandardInput }; public ProcessTaskOptions WithShellExecutor(IShellExecutor shellExecutor) => new ProcessTaskOptions(this) { ShellExecutor = shellExecutor }; public ProcessTaskOptions WithVerbosity(ChellVerbosity verbosity) => new ProcessTaskOptions(this) { Verbosity = verbosity }; public ProcessTaskOptions WithWorkingDirectory(string? workingDirectory) => new ProcessTaskOptions(this) { WorkingDirectory = workingDirectory }; public ProcessTaskOptions WithTimeout(TimeSpan timeout) => new ProcessTaskOptions(this) { Timeout = timeout }; } } ================================================ FILE: src/Chell/Run.cs ================================================ using System; using System.IO; using Chell.Shell; namespace Chell { /// /// Short cut for to launch a process from a or . /// public class Run : ProcessTask { public static implicit operator Run(FormattableString commandLine) => new Run(commandLine); public static implicit operator Run(CommandLineString commandLine) => new Run(commandLine); /// /// Launches a process from a . /// /// public Run(CommandLineString commandLine) : base(commandLine) { } /// /// Launches a process from a . /// /// /// The interpolated string will be escaped and the array will be expanded. /// public Run(FormattableString commandLine) : base(commandLine) { } /// /// Launches a process from a and connects the specified to the standard input. /// public Run(Stream inputStream, CommandLineString commandLine) : base(inputStream, commandLine) { } /// /// Launches a process from a and connects the specified to the standard input. /// /// /// The interpolated string will be escaped and the array will be expanded. /// /// /// public Run(Stream inputStream, FormattableString commandLine) : base(inputStream, commandLine) { } /// /// Launches a process from a and writes the specified binary data to the standard input. /// /// /// public Run(ReadOnlyMemory inputData, CommandLineString commandLine) : base(new MemoryStream(inputData.ToArray()), commandLine) { } /// /// Launches a process from a and writes the specified binary data to the standard input. /// /// /// The interpolated string will be escaped and the array will be expanded. /// /// /// public Run(ReadOnlyMemory inputData, FormattableString commandLine) : base(new MemoryStream(inputData.ToArray()), commandLine) { } } } ================================================ FILE: src/Chell/Shell/BashShellExecutor.cs ================================================ using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; using Chell.Internal; namespace Chell.Shell { public class BashShellExecutor : IShellExecutor { public static string? AutoDetectedPath { get; set; } public string? Path { get; set; } = AutoDetectedPath; public Encoding Encoding => Encoding.UTF8; public string Prefix { get; set; } public (string Command, string Arguments) GetCommandAndArguments(string commandLine) => (Path ?? throw new FileNotFoundException("Bash is not found in the PATH."), $"-c \"{Prefix}{commandLine}\""); // https://unix.stackexchange.com/questions/187651/how-to-echo-single-quote-when-using-single-quote-to-wrap-special-characters-in public string Escape(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (Regex.IsMatch(value, "^[a-zA-Z0-9_.-/]+$")) { return value; } return $"$'{value.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\"", "\\\"")}'"; } public BashShellExecutor(string? prefix = null) { Prefix = prefix ?? "set -euo pipefail;"; } static BashShellExecutor() { if (Which.TryGetPath("bash", out var bashPath)) { AutoDetectedPath = bashPath; } else { AutoDetectedPath = null; } } } } ================================================ FILE: src/Chell/Shell/CmdShellExecutor.cs ================================================ using System.Text; using System.Text.RegularExpressions; namespace Chell.Shell { public class CmdShellExecutor : IShellExecutor { public Encoding Encoding => Encoding.UTF8; public (string Command, string Arguments) GetCommandAndArguments(string commandLine) => ("cmd", $"/c \"{commandLine}\""); public string Escape(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (Regex.IsMatch(value, "^[a-zA-Z0-9_.-/\\\\]+$")) { return value; } value = Regex.Replace(value, "([<>|&^])", "^$1"); value = Regex.Replace(value, "(\\\\)?\"", x => x.Groups[1].Success ? "\\\\\\\"" : "\\\""); return $"\"{value}\""; } } } ================================================ FILE: src/Chell/Shell/IShellExecutor.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace Chell.Shell { public interface IShellExecutor { Encoding Encoding { get; } (string Command, string Arguments) GetCommandAndArguments(string commandLine); string Escape(string value); } } ================================================ FILE: src/Chell/Shell/NoUseShellExecutor.cs ================================================ using System.Text; using System.Text.RegularExpressions; using Chell.Internal; namespace Chell.Shell { public class NoUseShellExecutor : IShellExecutor { public Encoding Encoding => Encoding.UTF8; public (string Command, string Arguments) GetCommandAndArguments(string commandLine) { return CommandLineHelper.Parse(commandLine); } public string Escape(string value) => Regex.IsMatch(value, "^[a-zA-Z0-9_.-/]+$") ? value : $"\"{value.Replace("`", "``").Replace("\"", "`\"")}\""; } } ================================================ FILE: src/Chell/Shell/ShellExecutorProvider.cs ================================================ using System; using System.Runtime.InteropServices; using Chell.Shell; namespace Chell.Shell { public class ShellExecutorProvider { public IShellExecutor Executor { get; private set; } = GetPlatformPreferredExecutor(); public void SetExecutor(IShellExecutor shellExecutor) { Executor = shellExecutor ?? throw new ArgumentNullException(nameof(shellExecutor)); } internal static IShellExecutor GetPlatformPreferredExecutor() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new CmdShellExecutor() : BashShellExecutor.AutoDetectedPath != null ? new BashShellExecutor() : new NoUseShellExecutor(); } } namespace Chell { public static class ShellExecutorProviderExtensions { public static void NoUseShell(this ShellExecutorProvider provider) { provider.SetExecutor(new NoUseShellExecutor()); } public static void UseBash(this ShellExecutorProvider provider, string? prefix = null) { provider.SetExecutor(new BashShellExecutor(prefix)); } public static void UseCmd(this ShellExecutorProvider provider) { provider.SetExecutor(new CmdShellExecutor()); } public static void UseDefault(this ShellExecutorProvider provider) { provider.SetExecutor(ShellExecutorProvider.GetPlatformPreferredExecutor()); } } } ================================================ FILE: src/Chell.Run/Chell.Run.csproj ================================================  Exe netcoreapp3.1;net5.0 Tool to run C# code like a script. Provides a shell script-like (bash, cmd, ...) experience to .NET application. true chell ================================================ FILE: src/Chell.Run/Program.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using Cocona; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; namespace Chell.Run { partial class Program { static Task Main(string[] args) => CoconaLiteApp.RunAsync(args); public class RunCommandParameterSet : ICommandParameterSet { [Option("ref", new[] { 'r' }, Description = "Additional reference assembly")] [HasDefaultValue] public string[]? References { get; set; } = null; [Option("using", new[] { 'u' }, Description = "Additional `using` namespace")] [HasDefaultValue] public string[]? Usings { get; set; } = null; [Option('q')] [HasDefaultValue] public bool Silent { get; set; } = false; } [IgnoreUnknownOptions] [Command(Description = "Chell.Run: Run C# script instantly.")] public async Task RunAsync( RunCommandParameterSet runParams, [Option('e', Description = "A one-line program that can be run instantly.")] string? eval = default, [Argument(Description = "The path to a script file, or arguments to pass to the script")] string[]? filenameOrArgs = default ) { var fileName = filenameOrArgs is {Length: > 0} ? filenameOrArgs[0] : null; // -e ".." or --eval "..." if (!string.IsNullOrEmpty(eval)) { var args = Environment.GetCommandLineArgs(); var index = Array.FindIndex(args, x => x == "-e" || x == "--eval"); args = args.Skip(index + 2).ToArray(); await RunScriptAsync("", Environment.CurrentDirectory, eval, args, runParams); } // Read a script from stdin. else if (fileName == "-" || (string.IsNullOrWhiteSpace(fileName) && Console.IsInputRedirected)) { // Pass the strings as arguments after '-'. var args = Array.Empty(); if (fileName == "-") { args = Environment.GetCommandLineArgs(); var index = Array.IndexOf(args, "-"); args = args.Skip(index + 1).ToArray(); } using var reader = new StreamReader(Console.OpenStandardInput()); var code = await reader.ReadToEndAsync(); await RunScriptAsync("", Environment.CurrentDirectory, code, args, runParams); } else { if (string.IsNullOrWhiteSpace(fileName)) { throw new CommandExitedException("Error: Specify the path or pass the script from standard input.", -1); } if (!File.Exists(fileName)) { throw new CommandExitedException("Error: No such file or directory.", -1); } var ext = Path.GetExtension(fileName); if (ext == ".cs") { // Run .cs script file. var fullPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, fileName)); var args = filenameOrArgs?.Skip(1).ToArray() ?? Array.Empty(); await RunScriptAsync(fullPath, Path.GetDirectoryName(fullPath) ?? Environment.CurrentDirectory, await File.ReadAllTextAsync(fileName, Encoding.UTF8), args, runParams); } else { throw new CommandExitedException("Error: The specified file has unknown extension. Chell accepts a filename with `.cs` extension.", -1); } } } private async Task RunScriptAsync(string fileName, string executableDirectory, string content, string[] args, RunCommandParameterSet runParams) { _ = typeof(System.Text.Json.JsonSerializer).Assembly; _ = typeof(Chell.ChellEnvironment).Assembly; _ = typeof(Cocona.CoconaLiteApp).Assembly; _ = typeof(Sharprompt.Prompt).Assembly; _ = typeof(Mono.Options.Command).Assembly; var references = AppDomain.CurrentDomain.GetAssemblies() .Distinct() .GroupBy(x => x) .Select(x => x.Last()) .Select(x => MetadataReference.CreateFromFile(x.Location)); var usings = new[] { "System", "System.Collections", "System.Collections.Generic", "System.Diagnostics", "System.IO", "System.Text", "System.Text.RegularExpressions", "System.Linq", "System.Threading", "System.Threading.Tasks", "Chell", "Chell.Extensions", // using static "Chell.Exports" }.AsEnumerable(); var scriptOptions = ScriptOptions.Default .AddImports(usings.Concat(runParams.Usings ?? Array.Empty())) .AddReferences(references) .AddReferences(runParams.References ?? Array.Empty()); try { if (runParams.Silent) { ChellEnvironment.Current.Verbosity = ChellVerbosity.Silent; } ChellEnvironment.Current.SetCommandLineArgs(fileName, Path.GetFileName(fileName), executableDirectory, args); var script = await CSharpScript.RunAsync(content, scriptOptions); } catch (CompilationErrorException e) { Console.Error.WriteLine($"{fileName}{e.Message}"); } catch (ProcessTaskException e) { Console.Error.WriteLine(e.Message); } } } } ================================================ FILE: tests/Chell.Tests/Chell.Tests.csproj ================================================  net5.0 false runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: tests/Chell.Tests/ChellEnvironmentTest.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using FluentAssertions; using Xunit; namespace Chell.Tests { public class ChellEnvironmentTest { [Fact] public void HomeDirectory() { var env = new ChellEnvironment(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { env.HomeDirectory.Should().Be(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } else { env.HomeDirectory.Should().Be(Environment.GetEnvironmentVariable("HOME")); } } } } ================================================ FILE: tests/Chell.Tests/CommandLineStringTest.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FluentAssertions; using Xunit; namespace Chell.Tests { public class CommandLineStringTest { [Fact] public void StaticMethodPreferString() { StaticMethodOverloadedTest.A("Foo {0}").Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); } [Fact] public void StaticMethodPreferFormattableString() { StaticMethodOverloadedTest.A($"Foo {0}").Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); } private static class StaticMethodOverloadedTest { public static string A(CommandLineString s) => $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; public static string A(FormattableString s) => $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; } [Fact] public void InstanceMethodPreferString() { new InstanceMethodOverloadedTest().A("Foo {0}").Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); } [Fact] public void InstanceMethodPreferFormattableString() { new InstanceMethodOverloadedTest().A($"Foo {0}").Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); } private class InstanceMethodOverloadedTest { public string A(CommandLineString s) => $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; public string A(FormattableString s) => $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; } [Fact] public void ImplicitCastConstructorPreferString() { new ConstructorOverloadedTest("Foo {0}").Result.Should().Be($"{nameof(CommandLineString)}: StringValue=Foo {{0}}; FormattableStringValue="); } [Fact] public void ImplicitCastConstructorPreferFormattableString() { new ConstructorOverloadedTest($"Foo {0}").Result.Should().Be($"{nameof(FormattableString)}: Foo {{0}}; 1"); } private class ConstructorOverloadedTest { public string Result { get; } public ConstructorOverloadedTest(CommandLineString s) { Result = $"{nameof(CommandLineString)}: StringValue={s.StringValue}; FormattableStringValue={s.FormattableStringValue}"; } public ConstructorOverloadedTest(FormattableString s) { Result = $"{nameof(FormattableString)}: {s.Format}; {s.ArgumentCount}"; } } } } ================================================ FILE: tests/Chell.Tests/ProcessTaskTest.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Chell.IO; using Chell.Shell; using FluentAssertions; using Kokuban; using Xunit; namespace Chell.Tests { internal static class ProcessTaskTestFixtureExtensions { public static TemporaryAppBuilder.Compilation AddTo(this TemporaryAppBuilder.Compilation compilation, IList disposables) { disposables.Add(compilation); return compilation; } } public class ProcessTaskTestFixture : IDisposable { private readonly TemporaryAppSolutionBuilder _slnBuilder = new TemporaryAppSolutionBuilder(); public string EchoArg { get; } public string EchoOutAndErrorArgs { get; } public string HelloWorld { get; } public string ExitCodeNonZero { get; } public string ExitCodeNonZeroWaited { get; } public string WriteCommandLineArgs { get; } public string StandardInputPassThroughText { get; } public string StandardInputPassThroughBinary { get; } public string WriteSleepWriteExit { get; } public string ReadOnce { get; } public string ReadAllLines { get; } public string WriteCurrentDirectory { get; } public string Never { get; } public ProcessTaskTestFixture() { EchoArg = _slnBuilder.CreateProject(nameof(EchoArg), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.WriteLine(""["" + Environment.GetCommandLineArgs()[1] + ""]""); ")); EchoOutAndErrorArgs = _slnBuilder.CreateProject(nameof(EchoOutAndErrorArgs), builder => builder.WriteSourceFile("Program.cs", @" using System; using System.Threading.Tasks; Console.Out.WriteLine(""["" + Environment.GetCommandLineArgs()[1] + ""]""); await Task.Delay(100); Console.Error.WriteLine(""["" + Environment.GetCommandLineArgs()[2] + ""]""); await Task.Delay(100); Console.Out.WriteLine(""["" + Environment.GetCommandLineArgs()[3] + ""]""); await Task.Delay(100); Console.Error.WriteLine(""["" + Environment.GetCommandLineArgs()[4] + ""]""); ")); HelloWorld = _slnBuilder.CreateProject(nameof(HelloWorld), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.WriteLine(""Hello World!""); ")); ExitCodeNonZero = _slnBuilder.CreateProject(nameof(ExitCodeNonZero), builder => builder.WriteSourceFile("Program.cs", @" using System; Environment.ExitCode = 192; ")); ExitCodeNonZeroWaited = _slnBuilder.CreateProject(nameof(ExitCodeNonZeroWaited), builder => builder.WriteSourceFile("Program.cs", @" using System; using System.Threading.Tasks; await Task.Delay(100); Environment.ExitCode = 192; ")); WriteCommandLineArgs = _slnBuilder.CreateProject(nameof(WriteCommandLineArgs), builder => builder.WriteSourceFile("Program.cs", @" using System; foreach (var line in Environment.GetCommandLineArgs()) { Console.WriteLine(line); } ")); StandardInputPassThroughText = _slnBuilder.CreateProject(nameof(StandardInputPassThroughText), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.InputEncoding = System.Text.Encoding.UTF8; Console.OutputEncoding = System.Text.Encoding.UTF8; Console.Write(Console.In.ReadToEnd()); ")); StandardInputPassThroughBinary = _slnBuilder.CreateProject(nameof(StandardInputPassThroughBinary), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.OpenStandardInput().CopyTo(Console.OpenStandardOutput()); ")); WriteSleepWriteExit = _slnBuilder.CreateProject(nameof(WriteSleepWriteExit), builder => builder.WriteSourceFile("Program.cs", @" using System; using System.Threading; Console.WriteLine(""Hello""); Thread.Sleep(1000); Console.WriteLine(""Hello""); ")); ReadOnce = _slnBuilder.CreateProject(nameof(ReadOnce), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.WriteLine(Console.ReadLine()); ")); ReadAllLines = _slnBuilder.CreateProject(nameof(ReadAllLines), builder => builder.WriteSourceFile("Program.cs", @" using System; while (true) { var line = Console.ReadLine(); if (line == null) return; Console.WriteLine(line); } ")); WriteCurrentDirectory = _slnBuilder.CreateProject(nameof(WriteCurrentDirectory), builder => builder.WriteSourceFile("Program.cs", @" using System; Console.WriteLine(Environment.CurrentDirectory); ")); Never = _slnBuilder.CreateProject(nameof(Never), builder => builder.WriteSourceFile("Program.cs", @" using System; using System.Threading; Console.WriteLine(""Hello""); while (true) { Thread.Sleep(1000); } ")); _slnBuilder.Build(); } public void Dispose() { _slnBuilder.Dispose(); } } [Collection("ProcessTaskTest")] // NOTE: Test cases use `Console` and does not run in parallel. public class ProcessTaskTest : IClassFixture { private readonly ProcessTaskTestFixture _fixture; public ProcessTaskTest(ProcessTaskTestFixture fixture) { _fixture = fixture; } private async Task<(string StandardOut, string StandardError)> RunAsync(Func func) { var fakeConsole = new FakeConsoleProvider(); await func(fakeConsole); return (fakeConsole.GetStandardOutputAsString(), fakeConsole.GetStandardErrorAsString()); } [Fact] public async Task CommandNotFound_UseShell() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var dummyCommandName = Guid.NewGuid().ToString(); var procTask = new ProcessTask($"{dummyCommandName} --help"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { (await procTask.ExitCode).Should().Be(1); // A shell (cmd) will return 1. } else { (await procTask.ExitCode).Should().Be(127); // A shell (bash) will return 127. } } [Fact] public async Task CommandNotFound_NoUseShell() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var dummyCommandName = Guid.NewGuid().ToString(); var procTask = new ProcessTask($"{dummyCommandName} --help", new ProcessTaskOptions(shellExecutor: new NoUseShellExecutor())); (await procTask.ExitCode).Should().Be(127); // System.Diagnostics.Process will return 127. } [Fact] public async Task Execute() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var procTask = new ProcessTask($"{_fixture.HelloWorld}"); (await procTask.ExitCode).Should().Be(0); var result = await procTask; result.ExitCode.Should().Be(0); result.Combined.Should().Be("Hello World!" + Environment.NewLine); result.Output.Should().Be("Hello World!" + Environment.NewLine); result.Error.Should().BeEmpty(); } [Fact] public async Task ProcessOutputInArgumentShouldBeTrimmed() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var procTask1 = new ProcessTask($"{_fixture.HelloWorld}"); var result1 = await procTask1; var procTask2 = new ProcessTask($"{_fixture.EchoArg} {result1}"); var result2 = await procTask2; result1.Combined.Should().Be("Hello World!" + Environment.NewLine); result2.Combined.Should().Be("[Hello World!]" + Environment.NewLine); } [Fact] public async Task ExpandArguments() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var args = new[] { "Alice", "Karen", "Program Files", @"C:\Program Files (x86)\Microsoft Visual Studio" }; var procTask = new ProcessTask($"{_fixture.WriteCommandLineArgs} {args}"); var result = await procTask; // NOTE: We need skip the first line which is path of the command. result.AsLines(trimEnd: true).Skip(1).Should().BeEquivalentTo(args); } [Fact] public async Task ExpandArguments_Escape() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var args = new[] { "Alice", "Karen", "Program Files", @"C:\Program Files (x86)\Microsoft Visual Studio", "\"\\'|<>" }; var procTask = new ProcessTask($"{_fixture.WriteCommandLineArgs} {args}"); var result = await procTask; // NOTE: We need skip the first line which is path of the command. result.AsLines(trimEnd: true).Skip(1).Should().BeEquivalentTo(args); } [Fact] public async Task ExitCode() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var procTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); (await procTask.ExitCode).Should().Be(192); } [Fact] public async Task ExitCode_ThrowIfNonZero() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var procTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); await Assert.ThrowsAsync(async () => await procTask); } [Fact] public async Task ProcessOutput_StandardInputPassThroughText() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var memStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, コンニチハ!\nABCDEFG")); var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughText}"); var result = await procTask; result.Output.TrimEnd().Should().Be("Hello, コンニチハ!\nABCDEFG"); } [Fact] public async Task ProcessOutput_StandardInputOutputCombined() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var procTask = new ProcessTask($"{_fixture.EchoOutAndErrorArgs} Arg0 Arg1 Arg2 Arg3"); var result = await procTask; result.Output.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg0]", "[Arg2]")); result.Error.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg1]", "[Arg3]")); result.Combined.TrimEnd().Should().Be(string.Join(Environment.NewLine, "[Arg0]", "[Arg1]", "[Arg2]", "[Arg3]")); } [Fact] public async Task ProcessOutput_StandardInputPassThroughBinary() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var memStream = new MemoryStream(Encoding.Unicode.GetBytes("Hello, コンニチハ!\nABCDEFG")); var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughBinary}"); var result = await procTask; result.OutputBinary.ToArray().Should().BeEquivalentTo(memStream.ToArray()); } [Fact] public async Task Pipe_StandardInputPassThroughBinary() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var data = Encoding.Unicode.GetBytes("Hello, コンニチハ!\nABCDEFG"); var memStream = new MemoryStream(data); var procTask = new ProcessTask(memStream, $"{_fixture.StandardInputPassThroughBinary}"); var destStream = new MemoryStream(); var result = await procTask.Pipe(destStream); result.ExitCode.Should().Be(0); result.OutputBinary.Length.Should().Be(0); destStream.ToArray().Should().BeEquivalentTo(data); } [Fact] public async Task Pipe_CloseDestFirst() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var srcTask = new ProcessTask($"{_fixture.WriteSleepWriteExit}"); var destTask = new ProcessTask($"{_fixture.ReadOnce}"); await (srcTask | destTask); } [Fact] public async Task Pipe_CloseSrcFirst() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var srcTask = new ProcessTask($"{_fixture.HelloWorld}"); var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); await (srcTask | destTask); } [Fact] public async Task Pipe_ExitCode_NonZero() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); await Assert.ThrowsAsync(async () => await (srcTask | destTask)); } [Fact] public async Task Pipe_ExitCode_NonZero_ExitTailFirst() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZeroWaited}"); var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); await Assert.ThrowsAsync(async () => await (srcTask | destTask)); } [Fact] public async Task Pipe_ExitCode_NonZero_NoThrow() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var srcTask = new ProcessTask($"{_fixture.ExitCodeNonZero}"); var destTask = new ProcessTask($"{_fixture.ReadAllLines}"); await (srcTask.NoThrow() | destTask); } [Fact] public async Task WorkingDirectory() { using var fakeConsoleScope = new FakeConsoleProviderScope(); { var currentDirectory = Environment.CurrentDirectory; var output = await new ProcessTask($"{_fixture.WriteCurrentDirectory}"); output.ToString().Trim().Should().Be(currentDirectory); } { var currentDirectory = Environment.CurrentDirectory; var workingDirectory = Path.GetFullPath(Path.Combine(currentDirectory, "..")); var output = await new ProcessTask($"{_fixture.WriteCurrentDirectory}", new ProcessTaskOptions().WithWorkingDirectory(workingDirectory)); output.ToString().Trim().Should().Be(workingDirectory); } } [Fact] public async Task ProcessTimeout() { Func execute = async () => { using var fakeConsoleScope = new FakeConsoleProviderScope(); await Assert.ThrowsAsync(async () => { await new ProcessTask($"{_fixture.WriteSleepWriteExit}", new ProcessTaskOptions().WithTimeout(TimeSpan.FromMilliseconds(300))); }); }; await execute.Should().CompleteWithinAsync(TimeSpan.FromSeconds(2)); } [Fact] public async Task ProcessTimeout_Never() { Func execute = async () => { using var fakeConsoleScope = new FakeConsoleProviderScope(); await Assert.ThrowsAsync(async () => { await new ProcessTask($"{_fixture.Never}", new ProcessTaskOptions().WithTimeout(TimeSpan.FromMilliseconds(300))); }); }; await execute.Should().CompleteWithinAsync(TimeSpan.FromSeconds(2)); } [Fact] public async Task Verbosity_Silent() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var (stdOut, stdErr) = await RunAsync(async (console) => { await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.Silent)); }); stdOut.Should().BeEmpty(); } [Fact] public async Task Verbosity_CommandLine() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var (stdOut, stdErr) = await RunAsync(async (console) => { await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.CommandLine)); }); stdOut.Should().StartWith("$ "); stdOut.Should().NotContain("Hello World!"); } [Fact] public async Task Verbosity_ConsoleOutputs() { using var fakeConsoleScope = new FakeConsoleProviderScope(); var (stdOut, stdErr) = await RunAsync(async (console) => { await new ProcessTask($"{_fixture.HelloWorld}", new ProcessTaskOptions(console: console).WithVerbosity(ChellVerbosity.ConsoleOutputs)); }); stdOut.Should().Be("Hello World!" + Environment.NewLine); } private class FakeConsoleProviderScope : IDisposable { private readonly KokubanColorMode _origKokubanColorMode; private readonly IConsoleProvider _origConsoleProvider; private readonly FakeConsoleProvider _fakeConsoleProvider; public string StdOut => _fakeConsoleProvider.GetStandardOutputAsString(); public string StdErr => _fakeConsoleProvider.GetStandardErrorAsString(); public FakeConsoleProviderScope() { _origKokubanColorMode = KokubanOptions.Default.Mode; _origConsoleProvider = ChellEnvironment.Current.Console; _fakeConsoleProvider = new FakeConsoleProvider(); KokubanOptions.Default.Mode = KokubanColorMode.None; ChellEnvironment.Current.Console = _fakeConsoleProvider; } public void Dispose() { ChellEnvironment.Current.Console = _origConsoleProvider; _fakeConsoleProvider.Dispose(); KokubanOptions.Default.Mode = _origKokubanColorMode; } } } public class FakeConsoleProvider : IConsoleProvider, IDisposable { private readonly Pipe _outputPipe; private readonly Pipe _errorPipe; private readonly CancellationTokenSource _cts; private readonly MemoryStream _input = new MemoryStream(); private readonly MemoryStream _output = new MemoryStream(); private readonly MemoryStream _error = new MemoryStream(); public FakeConsoleProvider() { _cts = new CancellationTokenSource(); _outputPipe = new Pipe(new PipeOptions(readerScheduler:PipeScheduler.Inline, writerScheduler:PipeScheduler.Inline)); _outputPipe.Reader.CopyToAsync(_output, _cts.Token); _errorPipe = new Pipe(new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline)); _errorPipe.Reader.CopyToAsync(_error, _cts.Token); } public string GetStandardOutputAsString() => Encoding.UTF8.GetString(_output.ToArray()); public string GetStandardErrorAsString() => Encoding.UTF8.GetString(_error.ToArray()); public Stream OpenStandardInput() => _input; public Stream OpenStandardOutput() => _outputPipe.Writer.AsStream(leaveOpen: true); public Stream OpenStandardError() => _errorPipe.Writer.AsStream(leaveOpen: true); public Encoding InputEncoding => new UTF8Encoding(false); public Encoding OutputEncoding => new UTF8Encoding(false); public Encoding ErrorEncoding => new UTF8Encoding(false); public bool IsInputRedirected => false; public bool IsOutputRedirected => false; public bool IsErrorRedirected => false; public TextWriter Out => new StreamWriter(_outputPipe.Writer.AsStream(leaveOpen: true), OutputEncoding, leaveOpen: true) { AutoFlush = true }; public TextWriter Error => new StreamWriter(_errorPipe.Writer.AsStream(leaveOpen: true), ErrorEncoding, leaveOpen: true) { AutoFlush = true }; public void Dispose() { _cts.Cancel(); } } } ================================================ FILE: tests/Chell.Tests/Shell/BashShellExecutorTest.cs ================================================ using System; using Chell.Shell; using FluentAssertions; using Xunit; namespace Chell.Tests.Shell { public class BashShellExecutorTest { [Fact] public void Escape() { var executor = new BashShellExecutor(); executor.Escape(@"").Should().Be(@""); executor.Escape(@"foo").Should().Be(@"foo"); executor.Escape(@"123").Should().Be(@"123"); executor.Escape(@"f oo").Should().Be(@"$'f oo'"); executor.Escape(@"b'ar").Should().Be(@"$'b\'ar'"); executor.Escape(@"b""ar").Should().Be(@"$'b\""ar'"); executor.Escape(@"a\b").Should().Be(@"$'a\\b'"); } [Fact] public void GetCommandAndArguments() { { var executor = new BashShellExecutor(); executor.Path = "/bin/bash"; executor.Prefix = "set -euo pipefail;"; executor.GetCommandAndArguments("foo bar").Should().Be(("/bin/bash", "-c \"set -euo pipefail;foo bar\"")); } { var executor = new BashShellExecutor(); executor.Path = "/usr/local/bin/bash"; executor.Prefix = ""; executor.GetCommandAndArguments("foo bar").Should().Be(("/usr/local/bin/bash", "-c \"foo bar\"")); } } } } ================================================ FILE: tests/Chell.Tests/Shell/CmdShellExecutorTest.cs ================================================ using System; using Chell.Shell; using FluentAssertions; using Xunit; namespace Chell.Tests.Shell { public class CmdShellExecutorTest { [Fact] public void Escape() { var executor = new CmdShellExecutor(); executor.Escape(@"").Should().Be(@""); executor.Escape(@"foo").Should().Be(@"foo"); executor.Escape(@"123").Should().Be(@"123"); executor.Escape(@"Program Files").Should().Be(@"""Program Files"""); executor.Escape(@"b'ar").Should().Be(@"""b'ar"""); executor.Escape(@"b""ar").Should().Be(@"""b\""ar"""); executor.Escape(@"a\b").Should().Be(@"a\b"); executor.Escape(@"^").Should().Be(@"""^^"""); executor.Escape(@"<").Should().Be(@"""^<"""); executor.Escape(@">").Should().Be(@"""^>"""); executor.Escape(@"|").Should().Be(@"""^|"""); executor.Escape(@"&").Should().Be(@"""^&"""); executor.Escape(@"&&").Should().Be(@"""^&^&"""); // \ --> \ // " --> "\"" // \" --> "\\\"" executor.Escape(@"\").Should().Be(@"\"); executor.Escape(@"""").Should().Be(@"""\"""""); executor.Escape(@"\""").Should().Be(@"""\\\"""""); // "\" --> "\"\\\"" executor.Escape(@"""\""").Should().Be("\"\\\"\\\\\\\"\""); // "\"'|<>[] --> "\"\\\"'^|^<^>[]"" executor.Escape("\"\\\"'|<>[]").Should().Be("\"\\\"\\\\\\\"'^|^<^>[]\""); // "\'|<> --> "\"\'^|^<^>" executor.Escape("\"\\'|<>").Should().Be("\"\\\"\\'^|^<^>\""); // A B\C D --> "A B\C D" // A "B\C D --> "A \"B\C D" executor.Escape(@"A B\C D").Should().Be(@"""A B\C D"""); executor.Escape(@"A ""B\C D").Should().Be(@"""A \""B\C D"""); } [Fact] public void GetCommandAndArguments() { var executor = new CmdShellExecutor(); executor.GetCommandAndArguments("foo bar").Should().Be(("cmd", "/c \"foo bar\"")); } } } ================================================ FILE: tests/Chell.Tests/TemporaryAppBuilder.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Chell.Tests { public class TemporaryAppSolutionBuilder : IDisposable { private readonly List _apps; public string BaseDirectory { get; } public TemporaryAppSolutionBuilder() { _apps = new List(); BaseDirectory = Path.Combine(Path.GetTempPath(), $"Chell.Tests-{Guid.NewGuid()}"); } public string CreateProject(string projectName, Action configure) { var builder = TemporaryAppBuilder.Create(BaseDirectory, projectName); _apps.Add(builder); configure(builder); return Path.Combine(BaseDirectory, "out", projectName); } public TemporaryAppSolutionBuilder RunInSolutionDirectory(string fileName, string arguments) { var procStartInfo = new ProcessStartInfo() { FileName = fileName, Arguments = arguments, WorkingDirectory = BaseDirectory, RedirectStandardOutput = true, RedirectStandardError = true, }; var proc = Process.Start(procStartInfo)!; var standardOutput = proc.StandardOutput.ReadToEnd(); var standardError = proc.StandardError.ReadToEnd(); proc.WaitForExit(); if (proc.ExitCode != 0) { throw new InvalidOperationException(string.Join(Environment.NewLine, $"The process has been exited with code {proc.ExitCode}. (FileName={fileName}, Arguments={arguments}", "Output:", standardOutput, "Error:", standardError)); } return this; } public void Build() { RunInSolutionDirectory("dotnet", "new sln"); RunInSolutionDirectory("dotnet", $"sln add {string.Join(" ", _apps.Select(x => x.ProjectName + "/src"))}"); RunInSolutionDirectory("dotnet", "publish -o out"); } public void Dispose() { Directory.Delete(BaseDirectory, recursive:true); } } public class TemporaryAppBuilder : IDisposable { private bool _disposed; public string ProjectName { get; } public string BaseDirectory { get; } public string SourceDirectory { get; } public string OutputDirectory { get; } private TemporaryAppBuilder(string baseSlnDirectory, string projectName) { ProjectName = projectName; BaseDirectory = Path.Combine(baseSlnDirectory, projectName); SourceDirectory = Path.Combine(BaseDirectory, $"src"); OutputDirectory = Path.Combine(BaseDirectory, $"out"); } public static TemporaryAppBuilder Create(string baseSlnDirectory, string projectName) { var builder = new TemporaryAppBuilder(baseSlnDirectory, projectName); builder.Initialize(); return builder; } private void Initialize() { Directory.CreateDirectory(BaseDirectory); Directory.CreateDirectory(SourceDirectory); Directory.CreateDirectory(OutputDirectory); // Create .NET Console App project. //RunInSourceDirectory("dotnet", $"new console -f net5.0 -n {ProjectName} -o ."); // Explicitly use .NET 5 SDK. (AppHost is required for macOS with .NET 5 SDK) WriteSourceFile("global.json", @"{ ""sdk"": { ""version"": ""5.0.100"", ""rollForward"": ""latestFeature"" } }"); WriteSourceFile($"{ProjectName}.csproj", @" Exe net5.0 "); WriteSourceFile("Directory.Build.props", @" true "); } public TemporaryAppBuilder WriteSourceFile(string fileName, string content) { File.WriteAllText(Path.Combine(SourceDirectory, fileName), content, Encoding.UTF8); return this; } public string GetExecutablePath() => Path.Combine(OutputDirectory, ProjectName); public Compilation Build() { RunInSourceDirectory("dotnet", $"publish -o \"{OutputDirectory}\""); return new Compilation(this, Path.Combine(OutputDirectory, ProjectName)); } public class Compilation : IDisposable { private readonly TemporaryAppBuilder _builder; public string ExecutablePath { get; } public Compilation(TemporaryAppBuilder builder, string executablePath) { _builder = builder; ExecutablePath = executablePath; } public void Dispose() { _builder.Dispose(); } } public TemporaryAppBuilder RunInSourceDirectory(string fileName, string arguments) { var procStartInfo = new ProcessStartInfo() { FileName = fileName, Arguments = arguments, WorkingDirectory = SourceDirectory, RedirectStandardOutput = true, RedirectStandardError = true, }; var proc = Process.Start(procStartInfo)!; proc.WaitForExit(); var standardOutput = proc.StandardOutput.ReadToEnd(); var standardError = proc.StandardError.ReadToEnd(); if (proc.ExitCode != 0) { throw new InvalidOperationException(string.Join(Environment.NewLine, $"The process has been exited with code {proc.ExitCode}. (FileName={fileName}, Arguments={arguments}", "Output:", standardOutput, "Error:", standardError)); } return this; } public void Dispose() { if (_disposed) return; Directory.Delete(BaseDirectory, recursive:true); _disposed = true; } } }