├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── .gitmodules
├── .vscode
├── launch.json
└── tasks.json
├── LICENSE
├── README.md
├── docs
├── docfx.json
├── index.md
├── prompts.md
└── toc.yml
├── dotnet-shell.sln
└── src
├── FirstRunWizard
├── FirstRunWizard.csproj
├── GlobalSuppressions.cs
├── WizardUI.cs
└── defaults
│ ├── aliases
│ └── core
├── Shell
├── API
│ ├── ExitException.cs
│ ├── HistoryAPI.cs
│ ├── HistoryItem.cs
│ └── Shell.cs
├── GlobalSuppressions.cs
├── Logic
│ ├── Compilation
│ │ ├── Commands
│ │ │ ├── BacktickCommand.cs
│ │ │ ├── CSharpRegion.cs
│ │ │ ├── CdCommand.cs
│ │ │ ├── ClsCommand.cs
│ │ │ ├── CommandRegion.cs
│ │ │ ├── ExitCommand.cs
│ │ │ ├── IShellBlockCommand.cs
│ │ │ ├── IShellCommand.cs
│ │ │ ├── LoadCommand.cs
│ │ │ ├── RefCommand.cs
│ │ │ ├── ResetCommand.cs
│ │ │ ├── SheBangCommand.cs
│ │ │ ├── ShellCommand.cs
│ │ │ └── ShellCommandUtilities.cs
│ │ ├── CommentWalker.cs
│ │ ├── Exceptions.cs
│ │ ├── Executer.cs
│ │ ├── InteractiveRunner.cs
│ │ └── SourceProcessor.cs
│ ├── Console
│ │ ├── ConsoleImproved.cs
│ │ ├── DotNetConsole.cs
│ │ ├── HideCursor.cs
│ │ └── IConsole.cs
│ ├── Execution
│ │ ├── OS.cs
│ │ └── ProcessEx.cs
│ ├── Settings.cs
│ └── Suggestions
│ │ ├── Autocompletion
│ │ ├── CdCompletion.cs
│ │ ├── ExecutableCompletions.cs
│ │ └── FileAndDirectoryCompletion.cs
│ │ ├── CSharpSuggestions.cs
│ │ ├── CmdSuggestions.cs
│ │ └── Suggestions.cs
├── Properties
│ └── launchSettings.json
├── UI
│ ├── ColorString.cs
│ ├── Enhanced
│ │ ├── HistoryBox.cs
│ │ └── ListView.cs
│ ├── ErrorDisplay.cs
│ └── Standard
│ │ └── HistorySearch.cs
└── dotnet-shell-lib.csproj
├── UnitTests
├── AssertingConsole.cs
├── CSharpSuggestionsTests.cs
├── CmdSuggestionsTests.cs
├── ColorStringTests.cs
├── ConsoleImprovedTests.cs
├── ExecutionTests.cs
├── GlobalSuppressions.cs
├── OSTests.cs
├── PreProcessorTests.cs
├── ShellTests.cs
├── SuggestionsTests.cs
├── TestFiles
│ ├── csScriptTest.cs
│ └── nshScriptTest.nsh
└── UnitTests.csproj
└── dotnet-shell
├── Program.cs
└── dotnet-shell.csproj
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 8.0.x
20 | - name: Restore dependencies
21 | run: dotnet restore
22 | - name: Build
23 | run: dotnet build --no-restore
24 | - name: Test
25 | run: dotnet test --no-build --verbosity normal
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]est[Rr]esult*
30 | .vs/
31 | #Nuget packages folder
32 | packages/
33 | /docs/_site/**
34 | *.nupkg
35 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "docs/templates/DiscordFX"]
2 | path = docs/templates/DiscordFX
3 | url = https://github.com/jbltx/DiscordFX
4 | branch = master
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "OS-COMMENT1": "Use IntelliSense to find out which attributes exist for C# debugging",
9 | "OS-COMMENT2": "Use hover for the description of the existing attributes",
10 | "OS-COMMENT3": "For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md",
11 | "name": ".NET Core Launch (console)",
12 | "type": "coreclr",
13 | "request": "launch",
14 | "preLaunchTask": "build",
15 | "OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
16 | "program": "${workspaceFolder}/src/CSXShell/bin/Debug/net5.0/CSXShell.dll",
17 | "args": [],
18 | "cwd": "${workspaceFolder}/src/CSXShell",
19 | "OS-COMMENT5": "For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console",
20 | "console": "integratedTerminal",
21 | "stopAtEntry": false
22 | },
23 | {
24 | "name": ".NET Core Attach",
25 | "type": "coreclr",
26 | "request": "attach",
27 | "processId": "${command:pickProcess}"
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "build",
8 | "command": "dotnet",
9 | "type": "shell",
10 | "args": [
11 | "build",
12 | // Ask dotnet build to generate full paths for file names.
13 | "/property:GenerateFullPaths=true",
14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel
15 | "/consoleloggerparameters:NoSummary"
16 | ],
17 | "group": "build",
18 | "presentation": {
19 | "reveal": "silent"
20 | },
21 | "problemMatcher": "$msCompile"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 CSharpShell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Welcome to dotnet-shell the C# script compatible shell
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | dotnet-shell is a replacement for your *Unix shell (bash,sh,dash etc) that brings C#/Dotnet to the command line in a familiar and Bash-like syntax. It combines the best of C# with the shell commands you already know. If you've used [dotnetscript](https://github.com/filipw/dotnet-script)
13 | or [nake](https://github.com/yevhen/Nake/blob/master/README.md) you will feel right at home. Best of all it is easy to take your existing Unix shell scripts and port them to dotnet-shell format.
14 |
15 |
16 |
17 |
18 |
19 | dotnet-shell acts as a meta shell that sits on top of your system shell (Bash/PowerShell etc). It replaces hard to remember loop/if syntax with C# and enables you to use the shell constructs that you know and can't unlearn! It works in both interactive and script modes allowing you to build variables and arguments in C# and use them easily in shell commands.
20 |
21 | It is fully featured, supporting:
22 | * Bash-style tab completion augmented with C# autocompletion
23 | * Advanced history searching with an improved UX (helped by tmux if desired)
24 | * Support for loading Nugets, DLLs and other scripts
25 | * Powerline, command aliasing
26 |
27 | ## Comparison to other projects / check these out too
28 |
29 | Since the dotnet runtime enabled REPL (Read Evaluate Print Loop) support there have been a few different projects that have evolved to use it, some of the best are:
30 | - [dotnet-script](https://github.com/filipw/dotnet-script) - A great scripting environment which we use internally to run commands. The UX however is not designed for everyday use and the command line environment lacks an easy way to run system commands.
31 | - [Nake](https://github.com/botanicus/nake) - Another great project focused on build scripts instead of interactive shell environments.
32 | - [Orbital Shell](https://github.com/OrbitalShell/Orbital-Shell) - A great project but focused on being cross platform, supporting the same commands on multiple platforms, as such 'OS' commands have been reimplemented. We took the view of keeping existing logic/syntax similar enough to aid porting.
33 |
34 | ### How to install
35 |
36 | First you need to [install the .NET6. runtime.](https://dotnet.microsoft.com/download/dotnet/6.0) this is usually easiest via your OS' package manager. Next run:
37 |
38 | dotnet --info
39 | If you see a lot of .NET version information that starts with 6.0 then you have a working copy of the .NET runtime. dotnet-shell is a dotnet tool. It is installed by:
40 |
41 | dotnet tool install -g dotnet-shell
42 |
43 | | OS | Status |
44 | |---------|--------------|
45 | | Linux | Stable |
46 | | Windows | In-Testing |
47 | | Mac | Should work *untested* |
48 | | BSD | [Unsupported](https://github.com/dotnet/runtime/issues/14537) |
49 |
50 | There is now initial support for Windows execution via PowerShell. Executing commands works as does variable capture.
51 |
52 | ## Syntax cheatsheet
53 |
54 | In general dotnet-shell uses the same syntax of [dotnetscript](https://github.com/filipw/dotnet-script). To make some operations easier this has been extended so that:
55 | * shell commands are created from any line that doesn't end with a ';' or part of existing C# syntax - just like in Bash
56 | * backtick characters allow you to execute a command and capture its stdout (rather than letting it go to the screen)
57 | * nake style variables \$...\$ allow you to take variables from C# and include these in your system commands
58 |
59 | **A key point to note is that in generally a line needs to end with a ';' to be interpreted as C# (unless it is part of loop, class etc)**
60 |
61 | | File extension | Usage|
62 | |---------|--------------|
63 | | CSX | File contains [dotnetscript](https://github.com/filipw/dotnet-script) syntax - no dotnet-shell extension can be used |
64 | | nsh | CSX script syntax with our extensions |
65 | | CS | Can be loaded and executed with #load |
66 | | DLL | Can be loaded with #r |
67 |
68 | The [ExampleScripts repo](https://github.com/dotnet-shell/ExampleScripts) is a good place to see what you can do.
69 |
70 | ```cs
71 | #!/usr/bin/env dotnet-shell
72 | #r "System.Xml.Linq" // reference assemblies
73 | #r "nuget: Newtonsoft.Json" // and nuget packages is fully supported
74 | #load "Other.csx" // You can load other script files
75 | #load ~/ExampleScripts/CmdSyntax.nsh // (both absolute and relative paths are fine)
76 |
77 | using System; // many namespaces are loaded by default
78 | using System.Collections.Generic;
79 | using System.Data;
80 | using System.Xml;
81 | using System.Xml.Linq;
82 | using Microsoft.CSharp;
83 |
84 | using static System.Console; // static members smake your scripts shorter
85 | WriteLine("Are you ready? Y/N:");
86 |
87 | // You can run a system command just like in Bash
88 | echo "Hello world"
89 |
90 | // Wrapping a command in ``(backticks) allows you to capture the output
91 | var x = `ps`; // but default this is a string
92 | // You can also create more complex objects
93 | DirectoryInfo dir = `/bin/echo /bin/`;
94 | FileInfo file = `/bin/echo /bin/ls`;
95 | int aNumber=`/bin/echo 500`;
96 |
97 | // You can combine these into something quite powerful
98 | List z=`dmesg`; z.Distinct().Count();
99 |
100 | var variable = "Lets say you have a variable";
101 | // This is how you pass it into a system command
102 | echo $variable$
103 |
104 | ```
105 |
106 | ### Useful tips and tricks
107 | Escaping input automatically - the following one liner will print escaped C#. Great for copy and pasting into your codebase.
108 |
109 | Console.ReadLine();
110 |
111 |
112 | ### Command line help
113 | ```
114 | -v, --verbose (Default: false) Set output to verbose messages.
115 |
116 | --earlyDebuggerAttach (Default: false) Enables early debugging for initialization related issues
117 |
118 | --showPreProcessorOutput (Default: false) Outputs preprocessed scripts and commands to StdOut prior to execution
119 |
120 | -x, --ux (Default: Enhanced) The user experience mode the shell starts in
121 |
122 | --profile The path to the personal initialization script file (core.nsh)
123 |
124 | -s, --subShell Path to the sub shell to invoke commands with
125 |
126 | -a, --subShellArgs Arguments to the provided to the SubShell, this MUST include the format specifier {0}
127 |
128 | -u, --using Additional 'using' statements to include
129 |
130 | --popupCmd (Default: tmux popup -KER '{0}' -x 60 -y 0 -w 60% -h 100%) Command to run to raise a system popup window, must include {0} format specifier for the dotnet-shell command
131 |
132 | --historyCmd (Default: dotnet {0} --history --apiport {1} --token {2}) dotnet-shell command line to execute when the history subprocess. Must include {0} format specifier for DLL location, {1} for port and {2} for token parameters
133 |
134 | --additionalHistory Path to additional OS specific history files
135 |
136 | --historyPath Path to CSX history file
137 |
138 | --nowizard (Default: false) Do not try and run the initial set up wizard
139 |
140 | --help Display this help screen.
141 |
142 | --version Display version information.
143 | ```
144 |
145 | ### How to build from source
146 |
147 | Visual Studio solutions and VS Code projects are published with this repo. Otherwise you can checkout the repo and run:
148 |
149 | dotnet build
150 | dotnet src/Shell/bin/Debug/net6.0/dotnet-shell.dll
151 |
152 | ## Author
153 | **i-am-shodan**
154 |
155 | * Twitter: [@therealshodan](https://twitter.com/therealshodan)
156 | * Github: [@i-am-shodan](https://github.com/i-am-shodan)
157 |
158 | ## Contributing
159 |
160 | Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/dotnet-shell/Shell/issues).
161 |
162 | ## License
163 |
164 | Copyright © 2021 [i-am-shodan](https://github.com/i-am-shodan).
165 | This project is [MIT](https://en.wikipedia.org/wiki/MIT_License) licensed.
166 |
167 | ***
168 | _This README was generated by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
169 |
--------------------------------------------------------------------------------
/docs/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "files": [ "**/*.csproj" ],
7 | "exclude": [ "**/unittests/**", "**/firstrunwizard/**" ],
8 | "src": "../src"
9 | }
10 | ],
11 | "dest": "obj/api"
12 | }
13 | ],
14 | "build": {
15 | "content": [
16 | {
17 | "files": [ "**/*.yml" ],
18 | "src": "obj/api",
19 | "dest": "api"
20 | },
21 | {
22 | "files": [ "*.md", "toc.yml" ]
23 | }
24 | ],
25 | "overwrite": "specs/*.md",
26 | "globalMetadata": {
27 | "_appTitle": "dotnet-shell",
28 | "_enableSearch": true
29 | },
30 | "markdownEngineName": "markdig",
31 | "dest": "_site",
32 | "template": ["default", "templates/DiscordFX/discordfx"]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | Welcome to dotnet-shell the C# script compatible shell
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | dotnet-shell is a replacement for your *Unix shell (bash,sh,dash etc) that brings C#/Dotnet to the command line in a familiar and Bash-like syntax. It combines the best of C# with the shell commands you already know. If you've used [dotnetscript](https://github.com/filipw/dotnet-script)
13 | or [nake](https://github.com/yevhen/Nake/blob/master/README.md) you will feel right at home. Best of all it is easy to take your existing Unix shell scripts and port them to dotnet-shell format.
14 |
15 |
16 |
17 |
18 |
19 | dotnet-shell acts as a meta shell that sits on top of your system shell (Bash/PowerShell etc). It replaces hard to remember loop/if syntax with C# and enables you to use the shell constructs that you know and can't unlearn! It works in both interactive and script modes allowing you to build variables and arguments in C# and use them easily in shell commands.
20 |
21 | It is fully featured, supporting:
22 | * Bash-style tab completion augmented with C# autocompletion
23 | * Advanced history searching with an improved UX (helped by tmux if desired)
24 | * Support for loading Nugets, DLLs and other scripts
25 | * Powerline, command aliasing
26 |
27 | ## Comparison to other projects / check these out too
28 |
29 | Since the dotnet runtime enabled REPL (Read Evaluate Print Loop) support there have been a few different projects that have evolved to use it, some of the best are:
30 | - [dotnet-script](https://github.com/filipw/dotnet-script) - A great scripting environment which we use internally to run commands. The UX however is not designed for everyday use and the command line environment lacks an easy way to run system commands.
31 | - [Nake](https://github.com/botanicus/nake) - Another great project focused on build scripts instead of interactive shell environments.
32 | - [Orbital Shell](https://github.com/OrbitalShell/Orbital-Shell) - A great project but focused on being cross platform, supporting the same commands on multiple platforms, as such 'OS' commands have been reimplemented. We took the view of keeping existing logic/syntax similar enough to aid porting.
33 |
34 | ### How to install
35 |
36 | First you need to [install the .NET6. runtime.](https://dotnet.microsoft.com/download/dotnet/6.0) this is usually easiest via your OS' package manager. Next run:
37 |
38 | dotnet --info
39 | If you see a lot of .NET version information that starts with 6.0 then you have a working copy of the .NET runtime. dotnet-shell is a dotnet tool. It is installed by:
40 |
41 | dotnet tool install -g dotnet-shell
42 |
43 | | OS | Status |
44 | |---------|--------------|
45 | | Linux | Stable |
46 | | Windows | In-Testing |
47 | | Mac | Should work *untested* |
48 | | BSD | [Unsupported](https://github.com/dotnet/runtime/issues/14537) |
49 |
50 | There is now initial support for Windows execution via PowerShell. Executing commands works as does variable capture.
51 |
52 | ## Syntax cheatsheet
53 |
54 | In general dotnet-shell uses the same syntax of [dotnetscript](https://github.com/filipw/dotnet-script). To make some operations easier this has been extended so that:
55 | * shell commands are created from any line that doesn't end with a ';' or part of existing C# syntax - just like in Bash
56 | * backtick characters allow you to execute a command and capture its stdout (rather than letting it go to the screen)
57 | * nake style variables \$...\$ allow you to take variables from C# and include these in your system commands
58 |
59 | **A key point to note is that in generally a line needs to end with a ';' to be interpreted as C# (unless it is part of loop, class etc)**
60 |
61 | | File extension | Usage|
62 | |---------|--------------|
63 | | CSX | File contains [dotnetscript](https://github.com/filipw/dotnet-script) syntax - no dotnet-shell extension can be used |
64 | | nsh | CSX script syntax with our extensions |
65 | | CS | Can be loaded and executed with #load |
66 | | DLL | Can be loaded with #r |
67 |
68 | The [ExampleScripts repo](https://github.com/dotnet-shell/ExampleScripts) is a good place to see what you can do.
69 |
70 | ```cs
71 | #!/usr/bin/env dotnet-shell
72 | #r "System.Xml.Linq" // reference assemblies
73 | #r "nuget: Newtonsoft.Json" // and nuget packages is fully supported
74 | #load "Other.csx" // You can load other script files
75 | #load ~/ExampleScripts/CmdSyntax.nsh // (both absolute and relative paths are fine)
76 |
77 | using System; // many namespaces are loaded by default
78 | using System.Collections.Generic;
79 | using System.Data;
80 | using System.Xml;
81 | using System.Xml.Linq;
82 | using Microsoft.CSharp;
83 |
84 | using static System.Console; // static members smake your scripts shorter
85 | WriteLine("Are you ready? Y/N:");
86 |
87 | // You can run a system command just like in Bash
88 | echo "Hello world"
89 |
90 | // Wrapping a command in ``(backticks) allows you to capture the output
91 | var x = `ps`; // but default this is a string
92 | // You can also create more complex objects
93 | DirectoryInfo dir = `/bin/echo /bin/`;
94 | FileInfo file = `/bin/echo /bin/ls`;
95 | int aNumber=`/bin/echo 500`;
96 |
97 | // You can combine these into something quite powerful
98 | List z=`dmesg`; z.Distinct().Count();
99 |
100 | var variable = "Lets say you have a variable";
101 | // This is how you pass it into a system command
102 | echo $variable$
103 |
104 | ```
105 |
106 | ### Useful tips and tricks
107 | Escaping input automatically - the following one liner will print escaped C#. Great for copy and pasting into your codebase.
108 |
109 | Console.ReadLine();
110 |
111 |
112 | ### Command line help
113 | ```
114 | -v, --verbose (Default: false) Set output to verbose messages.
115 |
116 | --earlyDebuggerAttach (Default: false) Enables early debugging for initialization related issues
117 |
118 | --showPreProcessorOutput (Default: false) Outputs preprocessed scripts and commands to StdOut prior to execution
119 |
120 | -x, --ux (Default: Enhanced) The user experience mode the shell starts in
121 |
122 | --profile The path to the personal initialization script file (core.nsh)
123 |
124 | -s, --subShell Path to the sub shell to invoke commands with
125 |
126 | -a, --subShellArgs Arguments to the provided to the SubShell, this MUST include the format specifier {0}
127 |
128 | -u, --using Additional 'using' statements to include
129 |
130 | --popupCmd (Default: tmux popup -KER '{0}' -x 60 -y 0 -w 60% -h 100%) Command to run to raise a system popup window, must include {0} format specifier for the dotnet-shell command
131 |
132 | --historyCmd (Default: dotnet {0} --history --apiport {1} --token {2}) dotnet-shell command line to execute when the history subprocess. Must include {0} format specifier for DLL location, {1} for port and {2} for token parameters
133 |
134 | --additionalHistory Path to additional OS specific history files
135 |
136 | --historyPath Path to CSX history file
137 |
138 | --nowizard (Default: false) Do not try and run the initial set up wizard
139 |
140 | --help Display this help screen.
141 |
142 | --version Display version information.
143 | ```
144 |
145 | ### How to build from source
146 |
147 | Visual Studio solutions and VS Code projects are published with this repo. Otherwise you can checkout the repo and run:
148 |
149 | dotnet build
150 | dotnet src/Shell/bin/Debug/net6.0/dotnet-shell.dll
151 |
152 | ## Author
153 | **i-am-shodan**
154 |
155 | * Twitter: [@therealshodan](https://twitter.com/therealshodan)
156 | * Github: [@i-am-shodan](https://github.com/i-am-shodan)
157 |
158 | ## Contributing
159 |
160 | Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/dotnet-shell/Shell/issues).
161 |
162 | ## License
163 |
164 | Copyright © 2021 [i-am-shodan](https://github.com/i-am-shodan).
165 | This project is [MIT](https://en.wikipedia.org/wiki/MIT_License) licensed.
166 |
167 | ***
168 | _This README was generated by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
169 |
--------------------------------------------------------------------------------
/docs/prompts.md:
--------------------------------------------------------------------------------
1 | # Customizing your prompt
2 | Dotnet-shell supports prompt customization through the [Shell.Prompt API function](/api/Dotnet.Shell.API.Shell.html#Dotnet_Shell_API_Shell_Prompt). To use this simply declare a function that will be called everytime a prompt is to be rendered and set this to Shell.Prompt. You can do this at any time but doing this in core.nsh will mean your prompt will always be displayed first.
3 |
4 | ## Powerline
5 |
6 | By default dotnet-shell ships with basinc support for Powerline - you can find this in core.nsh. A terse example is given here:
7 |
8 | ```
9 | Shell.Prompt = () =>
10 | {
11 | string powerLinePrompt=`powerline-render shell left --last-exit-code=$Shell.LastExitCode$`;
12 | return ColorString.FromRawANSI( powerLinePrompt );
13 | };
14 | ```
15 |
16 | ## Starship
17 |
18 | [Starship](https://starship.rs/) support can be added to dotnet-shell using the following syntax in you core.nsh file.
19 |
20 | ```
21 | Shell.Prompt = () =>
22 | var prompt = `starship prompt --status=$Shell.LastExitCode$ --jobs=$Shell.BackgroundProcesses.Count()$`;
23 | return ColorString.FromRawANSI(prompt);
24 | };
25 | ```
--------------------------------------------------------------------------------
/docs/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Home
2 | href: index.md
3 | - name: Custom prompts
4 | href: prompts.md
5 | - name: API Documentation
6 | href: obj/api/
7 |
--------------------------------------------------------------------------------
/dotnet-shell.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29905.134
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{657BCD33-DCB1-461E-B9A8-C721F7F21098}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "src\UnitTests\UnitTests.csproj", "{83E2C21F-D4E7-4696-AA10-D459812EDDB4}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1CF1FF3B-7E70-43B7-BFC2-05690943BBEF}"
11 | ProjectSection(SolutionItems) = preProject
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FirstRunWizard", "src\FirstRunWizard\FirstRunWizard.csproj", "{EC84DB46-B988-4EDC-834B-A7EF01F661B4}"
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-shell", "src\dotnet-shell\dotnet-shell.csproj", "{7B39EAE1-7C82-480C-95D0-1B202B78FCDF}"
18 | EndProject
19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-shell-lib", "src\Shell\dotnet-shell-lib.csproj", "{72CED60A-8302-4F4E-8437-B0963BB82EBB}"
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Release|Any CPU = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {83E2C21F-D4E7-4696-AA10-D459812EDDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {83E2C21F-D4E7-4696-AA10-D459812EDDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {83E2C21F-D4E7-4696-AA10-D459812EDDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {83E2C21F-D4E7-4696-AA10-D459812EDDB4}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {EC84DB46-B988-4EDC-834B-A7EF01F661B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {EC84DB46-B988-4EDC-834B-A7EF01F661B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {EC84DB46-B988-4EDC-834B-A7EF01F661B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {EC84DB46-B988-4EDC-834B-A7EF01F661B4}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {7B39EAE1-7C82-480C-95D0-1B202B78FCDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {7B39EAE1-7C82-480C-95D0-1B202B78FCDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {7B39EAE1-7C82-480C-95D0-1B202B78FCDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {7B39EAE1-7C82-480C-95D0-1B202B78FCDF}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {72CED60A-8302-4F4E-8437-B0963BB82EBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {72CED60A-8302-4F4E-8437-B0963BB82EBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {72CED60A-8302-4F4E-8437-B0963BB82EBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {72CED60A-8302-4F4E-8437-B0963BB82EBB}.Release|Any CPU.Build.0 = Release|Any CPU
43 | EndGlobalSection
44 | GlobalSection(SolutionProperties) = preSolution
45 | HideSolutionNode = FALSE
46 | EndGlobalSection
47 | GlobalSection(NestedProjects) = preSolution
48 | {83E2C21F-D4E7-4696-AA10-D459812EDDB4} = {657BCD33-DCB1-461E-B9A8-C721F7F21098}
49 | {EC84DB46-B988-4EDC-834B-A7EF01F661B4} = {657BCD33-DCB1-461E-B9A8-C721F7F21098}
50 | {7B39EAE1-7C82-480C-95D0-1B202B78FCDF} = {657BCD33-DCB1-461E-B9A8-C721F7F21098}
51 | {72CED60A-8302-4F4E-8437-B0963BB82EBB} = {657BCD33-DCB1-461E-B9A8-C721F7F21098}
52 | EndGlobalSection
53 | GlobalSection(ExtensibilityGlobals) = postSolution
54 | SolutionGuid = {57954085-D79B-443F-A676-4F57A0A5A1FF}
55 | EndGlobalSection
56 | EndGlobal
57 |
--------------------------------------------------------------------------------
/src/FirstRunWizard/FirstRunWizard.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/FirstRunWizard/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "I don't like the single line using syntax", Scope = "module")]
9 |
--------------------------------------------------------------------------------
/src/FirstRunWizard/WizardUI.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Reflection;
5 | using System.Runtime.InteropServices;
6 |
7 | namespace FirstRunWizard
8 | {
9 | public class WizardUI
10 | {
11 | private readonly string firstRunFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", ".firstrun");
12 |
13 | ///
14 | /// Runs the Wizard UI interactively.
15 | ///
16 | /// If False stop execution of the shell, user need to do work
17 | public static bool Run()
18 | {
19 | WizardUI wiz = new();
20 | return wiz.StartInteractive();
21 | }
22 |
23 | public bool StartInteractive()
24 | {
25 | bool ret = false;
26 |
27 | if (HasWizardRunBefore())
28 | {
29 | ret = true;
30 | return ret;
31 | }
32 |
33 | bool createFirstRunFile = true;
34 |
35 | Console.WriteLine("This looks like the first run of dotnet-shell, do you want to run the initial set up wizard (highly recommended)?");
36 | if (GetYesOrNo())
37 | {
38 | Console.WriteLine();
39 |
40 | if (PreRequisitesCheck())
41 | {
42 | Console.WriteLine("Do you want to auto-accept the recommended defaults (recommended)?");
43 | var autoAccept = GetYesOrNo();
44 | Console.WriteLine();
45 |
46 | var dirName = Path.GetDirectoryName(firstRunFilename);
47 | if (!Directory.Exists(dirName))
48 | {
49 | Directory.CreateDirectory(dirName);
50 | Directory.CreateDirectory(Path.Combine(dirName, "profiles"));
51 | }
52 | Console.WriteLine();
53 |
54 | Console.WriteLine("Create default core.nsh (autorun profile) in ~/.nsh/ (highly recommended)?");
55 | if (autoAccept || GetYesOrNo())
56 | {
57 | CreateDefaultProfile();
58 | }
59 | Console.WriteLine();
60 |
61 | Console.WriteLine("Create default aliases.nsh (Bash-like aliases) in ~/.nsh/ (recommended)?");
62 | if (autoAccept || GetYesOrNo())
63 | {
64 | CreateDefaultAliases();
65 | }
66 | Console.WriteLine();
67 |
68 | Console.WriteLine("Download useful scripts from GitHub (https://github.com/dotnet-shell/CoreScripts) and store in ~/.nsh/functions/ (highly recommended)?");
69 | if (autoAccept || GetYesOrNo())
70 | {
71 | DownloadCoreScripts();
72 | }
73 | else
74 | {
75 | CreateEmptyCoreScriptsDir();
76 | }
77 | Console.WriteLine();
78 |
79 | if (IsWindowsTerminal())
80 | {
81 | ShowWindowsTerminalConfig();
82 | }
83 | Console.WriteLine();
84 |
85 | Console.WriteLine("More configuration guides for Tmux, Vim and Windows Terminal can be found on online");
86 | ret = true;
87 | }
88 | else
89 | {
90 | createFirstRunFile = false;
91 | ret = false;
92 | }
93 | }
94 |
95 | if (createFirstRunFile)
96 | {
97 | Console.WriteLine("To run this wizard again remove " + firstRunFilename);
98 | CreateWizardAutorunFile();
99 | }
100 |
101 | return ret;
102 | }
103 |
104 | private bool HasWizardRunBefore()
105 | {
106 | return File.Exists(firstRunFilename);
107 | }
108 |
109 | private void CreateWizardAutorunFile()
110 | {
111 | using (var fs = File.Create(firstRunFilename))
112 | {
113 | fs.Close();
114 | }
115 | }
116 |
117 | private static void ShowWindowsTerminalConfig()
118 | {
119 | const string Data = @"If you use Windows Terminal you can easily access dotnet-shell by including a new environment in your
120 | Settings file such as in the following snippet:
121 | {
122 | ""guid"": ""{YOUR DISTRO GUID}"",
123 | ""hidden"": false,
124 | ""name"": ""OS NAME (dotnet-shell)"",
125 | ""bellStyle"": ""none"",
126 | ""commandline"": ""bash -c \""/usr/local/bin/tmux -2 new-session '~/.dotnet/tools/dotnet-shell'\""""
127 | }";
128 |
129 | Console.WriteLine(Data);
130 | Console.WriteLine("If you want to use tmux enhanced mode, use the following command line in your settings.json");
131 | Console.WriteLine(@"""commandline"": ""bash -c \""/usr/local/bin/tmux -2 new-session '~/.dotnet/tools/dotnet-shell --ux TmuxEnhanced'\""""");
132 | }
133 |
134 | private static void DownloadCoreScripts()
135 | {
136 | var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", "functions");
137 | RunAndGetStdOut("git", "clone https://github.com/dotnet-shell/CoreScripts \""+dir+"\"");
138 | }
139 |
140 | private static void CreateEmptyCoreScriptsDir()
141 | {
142 | var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", "functions");
143 | if (!Directory.Exists(dir))
144 | {
145 | Directory.CreateDirectory(dir);
146 | }
147 | }
148 |
149 | private static void CreateDefaultAliases()
150 | {
151 | var core = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", "aliases.nsh");
152 | var defaultAliases = GetEmbeddedResource("defaults/aliases", Assembly.GetExecutingAssembly());
153 |
154 | File.WriteAllText(core, defaultAliases);
155 | }
156 |
157 | private static void CreateDefaultProfile()
158 | {
159 | var core = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", "core.nsh");
160 | var defaultProfile = GetEmbeddedResource("defaults/core", Assembly.GetExecutingAssembly());
161 |
162 | File.WriteAllText(core, defaultProfile);
163 | }
164 |
165 | private static bool GetYesOrNo()
166 | {
167 | while (true)
168 | {
169 | Console.Write("Y|N) ");
170 | var key = Console.ReadLine().Trim().ToUpperInvariant();
171 | if (key == "Y" || key == "N")
172 | {
173 | return key == "Y";
174 | }
175 | }
176 | }
177 |
178 | public static bool PreRequisitesCheck()
179 | {
180 | if (string.IsNullOrWhiteSpace(RunAndGetStdOut("git", "--version")))
181 | {
182 | Console.WriteLine("Git could not be found!");
183 | Console.WriteLine("You can continue without Git but you won't be able to use the highly recommended useful scripts");
184 | Console.WriteLine("Do you want to stop and install Git?");
185 | if (!GetYesOrNo())
186 | {
187 | return false;
188 | }
189 | }
190 |
191 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
192 | {
193 | if (!IsSupportedTmuxVersionInstalled())
194 | {
195 | return false;
196 | }
197 | }
198 |
199 | return true;
200 | }
201 |
202 | public static bool IsSupportedTmuxVersionInstalled()
203 | {
204 | if (!DoesTmuxSupportPopups())
205 | {
206 | Console.WriteLine("The version of tmux installed is too old, or not available.");
207 | Console.WriteLine("In order to use the tmux enhanced version of dotnet-shell a version of tmux >= 3.2 must be installed in your path");
208 | Console.WriteLine("Please see https://github.com/tmux/tmux");
209 |
210 | Console.WriteLine("Do you want to stop and install tmux? (Most people don't have the latest version of tmux so its OK to say N here)");
211 | if (GetYesOrNo())
212 | {
213 | return false;
214 | }
215 | }
216 |
217 | return true;
218 | }
219 |
220 | private static bool DoesTmuxSupportPopups()
221 | {
222 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
223 | {
224 | var version = RunAndGetStdOut("tmux", "-V");
225 | return version != null && version.StartsWith("tmux 3.2");
226 | }
227 | return false;
228 | }
229 |
230 | public static bool IsWSL()
231 | {
232 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
233 | {
234 | foreach (var variable in Environment.GetEnvironmentVariables().Keys)
235 | {
236 | var name = variable.ToString();
237 |
238 | if (name == "WSL_DISTRO_NAME" || name == "WSL_INTEROP" || name == "WSLENV")
239 | {
240 | return true;
241 | }
242 | }
243 | }
244 | return false;
245 | }
246 |
247 | public static bool IsWindowsTerminal()
248 | {
249 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
250 | {
251 | foreach (var variable in Environment.GetEnvironmentVariables().Keys)
252 | {
253 | var name = variable.ToString();
254 |
255 | if (name == "WT_PROFILE_ID" || name == "WT_SESSION")
256 | {
257 | return true;
258 | }
259 | }
260 | }
261 | return false;
262 | }
263 |
264 | private static string RunAndGetStdOut(string program, string arguments)
265 | {
266 | try
267 | {
268 | var proc = new Process();
269 | proc.StartInfo.RedirectStandardOutput = true;
270 | proc.StartInfo.CreateNoWindow = true;
271 | proc.StartInfo.FileName = program;
272 | proc.StartInfo.Arguments = arguments;
273 | proc.StartInfo.UseShellExecute = false;
274 |
275 | proc.Start();
276 | proc.WaitForExit();
277 |
278 | return proc.StandardOutput.ReadToEnd();
279 | }
280 | catch
281 | {
282 | return string.Empty;
283 | }
284 | }
285 | public static string GetEmbeddedResource(string resourceName, Assembly assembly)
286 | {
287 | resourceName = assembly.GetName().Name + "." + resourceName.Replace(" ", "_")
288 | .Replace("\\", ".")
289 | .Replace("/", ".");
290 |
291 | using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
292 | {
293 | if (resourceStream == null)
294 | return null;
295 |
296 | using (StreamReader reader = new(resourceStream))
297 | {
298 | return reader.ReadToEnd();
299 | }
300 | }
301 | }
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/src/FirstRunWizard/defaults/aliases:
--------------------------------------------------------------------------------
1 | Shell.AddCSAlias("red", "Console.WriteLine(new ColorString(\"{0}\", Color.Red).TextWithFormattingCharacters);");
2 | Shell.AddCSAlias("green", "Console.WriteLine(new ColorString(\"{0}\", Color.Green).TextWithFormattingCharacters);");
3 | Shell.AddCSAlias("quit", "Environment.Exit(0);");
4 |
5 | // Begin Linux specific aliaases
6 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; }
7 |
8 | Shell.AddCmdAlias("ls", "ls --color=auto ");
9 | Shell.AddCmdAlias("dir", "dir --color=always ");
10 | Shell.AddCmdAlias("vdir", "vdir --color=always ");
11 | Shell.AddCmdAlias("grep", "grep --color=always ");
12 | Shell.AddCmdAlias("fgrep", "fgrep --color=always ");
13 | Shell.AddCmdAlias("egrep", "egrep --color=always ");
14 | Shell.AddCmdAlias("ll", "ls -alF ");
15 | Shell.AddCmdAlias("la", "ls -A ");
16 | Shell.AddCmdAlias("l", "ls -CF ");
17 | Shell.AddCmdAlias("vim", "vim -T xterm-256color ");
18 |
--------------------------------------------------------------------------------
/src/FirstRunWizard/defaults/core:
--------------------------------------------------------------------------------
1 | // ~/.csxx/core.nsh is executed by dotnet-shell for all interactive shells.
2 | // see https://github.com/dotnet-shell/ExampleScripts for more examples
3 |
4 | // Default includes
5 | using System.Runtime.InteropServices;
6 | using System.Drawing;
7 | using Dotnet.Shell.UI;
8 |
9 | // Custom prompt handling, this handler autodetects if powerline is availiable on Linux environments
10 | // Otherwise if falls back to an acceptable default
11 | bool? powerline = null;
12 | #region c#
13 | Shell.Prompt = () =>
14 | #endregion
15 | {
16 | if (powerline == null && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
17 | {
18 | var powerlineHelpOut=`which powerline`;
19 | powerline = !string.IsNullOrWhiteSpace(powerlineHelpOut);
20 | }
21 |
22 | if (powerline == true)
23 | {
24 | string powerLinePrompt=`powerline-render shell left --last-exit-code=$Shell.LastExitCode$`;
25 | return new ColorString("!", Color.Green, Color.LightBlue) + ColorString.FromRawANSI( powerLinePrompt );
26 | }
27 | else
28 | {
29 | return new ColorString("!" + Environment.UserName + "@" + Environment.MachineName, Color.Green) + new ColorString(" " + Shell.WorkingDirectory + ">", Color.Blue) + " ";
30 | }
31 | };
32 |
33 | // Add paths to Shell environment
34 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
35 | {
36 | Shell.Paths.Add("/bin");
37 | Shell.Paths.Add("/sbin");
38 | Shell.Paths.Add("/usr/bin");
39 | Shell.Paths.Add("/usr/sbin");
40 | Shell.Paths.Add("/usr/local/bin");
41 | Shell.Paths.Add("/usr/local/sbin");
42 |
43 | var userExes = Path.Combine(Shell.HomeDirectory, ".local", "bin");
44 | if (Directory.Exists(userExes))
45 | {
46 | Shell.Paths.Add(userExes);
47 | }
48 |
49 | var dotnetTools = Path.Combine(Shell.HomeDirectory, ".dotnet", "tools");
50 | if (Directory.Exists(dotnetTools))
51 | {
52 | Shell.Paths.Add(dotnetTools);
53 | }
54 | }
55 | else
56 | {
57 | Shell.Paths.Add("C:\\Windows");
58 | Shell.Paths.Add("C:\\Windows\\System32");
59 | }
60 |
61 | // Load user functions store in homedir
62 | var functionsDir = Path.Combine(Shell.HomeDirectory, Shell.DefaultScriptExtension, "functions");
63 | if (Directory.Exists(functionsDir))
64 | {
65 | var functions = new List();
66 | functions.AddRange(Directory.GetFiles(functionsDir, "*.csx*"));
67 | functions.AddRange(Directory.GetFiles(functionsDir, "*"+Shell.DefaultScriptExtension));
68 | foreach (var file in functions.OrderBy(q => q))
69 | {
70 | #load $file$
71 | }
72 | }
73 |
74 | // Load user command/script aliases
75 | var aliasesFile = Path.Combine(Shell.HomeDirectory, Shell.DefaultScriptExtension, "aliases"+Shell.DefaultScriptExtension);
76 | if (File.Exists(aliasesFile))
77 | {
78 | #load $aliasesFile$
79 | }
80 |
81 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
82 | {
83 | Console.WriteLine(new ColorString("This OS is currently in beta!", Color.Red).TextWithFormattingCharacters);
84 | }
85 |
86 | // Ensure we start in the users homedir
87 | cd ~
88 |
--------------------------------------------------------------------------------
/src/Shell/API/ExitException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dotnet.Shell.API
4 | {
5 | ///
6 | /// Throwing this exception will terminate the shell safely.
7 | ///
8 | ///
9 | public class ExitException : Exception
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Shell/API/HistoryAPI.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net;
4 | using System.Net.Sockets;
5 | using System.Threading.Tasks;
6 |
7 | namespace Dotnet.Shell.API
8 | {
9 | public class HistoryAPI
10 | {
11 | ///
12 | /// Searches the user history for a specific term.
13 | ///
14 | /// The term.
15 | /// The port the history server is listening on.
16 | /// The authentication token.
17 | public static async Task SearchResultAsync(string term, int port, string token)
18 | {
19 | using (var client = new TcpClient(IPAddress.Loopback.ToString(), port))
20 | using (var sw = new StreamWriter(client.GetStream()))
21 | {
22 | await sw.WriteLineAsync(token);
23 | await sw.WriteLineAsync(term);
24 | }
25 | }
26 |
27 | ///
28 | /// Listens for search requests asynchronous.
29 | ///
30 | /// Called when listening has started.
31 | /// Task
32 | /// Invalid token
33 | public static async Task ListenForSearchResultAsync(Action onStartedListening)
34 | {
35 | var result = string.Empty;
36 |
37 | var r = new Random();
38 | var token = Guid.NewGuid().ToString();
39 | TcpListener listener;
40 |
41 | int port;
42 | while (true)
43 | {
44 | try
45 | {
46 | var randomPortToTry = r.Next(1025, 65535);
47 | listener = new TcpListener(IPAddress.Loopback, randomPortToTry);
48 | listener.Start();
49 | port = randomPortToTry;
50 | break;
51 | }
52 | catch (SocketException)
53 | {
54 |
55 | }
56 | }
57 |
58 | onStartedListening?.Invoke(port, token);
59 |
60 | using (var client = await listener.AcceptTcpClientAsync())
61 | using (var sr = new StreamReader(client.GetStream()))
62 | {
63 | var clientToken = await sr.ReadLineAsync();
64 | if (clientToken != token)
65 | {
66 | throw new InvalidDataException("Invalid token");
67 | }
68 |
69 | result = await sr.ReadLineAsync();
70 | }
71 |
72 | listener.Stop();
73 | return result.Trim();
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Shell/API/HistoryItem.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 |
4 | namespace Dotnet.Shell.API
5 | {
6 | ///
7 | /// Representation of an item of shell history
8 | ///
9 | public class HistoryItem
10 | {
11 | ///
12 | /// Gets or sets the command line.
13 | ///
14 | ///
15 | /// The command line.
16 | ///
17 | public string CmdLine { get; set; }
18 |
19 | ///
20 | /// Gets or sets the time run.
21 | ///
22 | ///
23 | /// The time run.
24 | ///
25 | public DateTime TimeRun { get; set; }
26 |
27 | ///
28 | /// Gets or sets the legacy offset. In a file like .bash_history this is the line number
29 | ///
30 | ///
31 | /// The legacy offset.
32 | ///
33 | public int LegacyOffset { get; set; } = -1;
34 |
35 | ///
36 | /// Initializes a new instance of the class.
37 | ///
38 | /// The command line.
39 | /// The time run.
40 | /// The legacy offset.
41 | [JsonConstructor]
42 | public HistoryItem(string cmdLine, DateTime timeRun, int legacyOffset)
43 | {
44 | CmdLine = cmdLine;
45 | TimeRun = timeRun;
46 | LegacyOffset = legacyOffset;
47 | }
48 |
49 | ///
50 | /// Initializes a new instance of the class.
51 | ///
52 | /// The command.
53 | /// The time.
54 | public HistoryItem(string command, DateTime time)
55 | {
56 | CmdLine = command;
57 | TimeRun = time;
58 | }
59 |
60 | ///
61 | /// Initializes a new instance of the class.
62 | ///
63 | /// The command.
64 | /// The offset.
65 | public HistoryItem(string command, int offset)
66 | {
67 | CmdLine = command;
68 | LegacyOffset = offset;
69 | TimeRun = new DateTime(DateTime.MinValue.Ticks + (offset * 1000));
70 | }
71 |
72 | ///
73 | /// Converts the history item into a command line
74 | ///
75 | ///
76 | /// A that represents this instance.
77 | ///
78 | public override string ToString()
79 | {
80 | return CmdLine;
81 | }
82 |
83 | ///
84 | /// Serializes this instance to JSON
85 | ///
86 | ///
87 | public string Serialize()
88 | {
89 | return JsonConvert.SerializeObject(this, Formatting.None);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Shell/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "I don't like the single line using syntax", Scope = "module")]
9 | [assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "Needs this to shut up visual studios helpers", Scope = "module")]
10 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/BacktickCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.RegularExpressions;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Runtime.CompilerServices;
7 |
8 | [assembly: InternalsVisibleTo("UnitTests")]
9 |
10 | namespace Dotnet.Shell.Logic.Compilation.Commands
11 | {
12 | class BacktickCommand : IShellCommand
13 | {
14 | public const string MARKER = "#region SUBCOMMAND // ";
15 | public const string ENDMARKER = " #endregion";
16 |
17 | private readonly Regex UnescapedBackTicks = new(@"((?]+\d*\s+[a-zA-Z]+\d*\s*=\s*`.+`\s*", RegexOptions.Compiled);
20 |
21 | public string GetCodeFromMetaRepresentation(string line)
22 | {
23 | return line.Replace(MARKER, string.Empty).Replace(ENDMARKER, string.Empty);
24 | }
25 |
26 | public string GetMetaRepresentation(string line)
27 | {
28 | var variableType = "string";
29 | if (VariableAssignmentRegex.IsMatch(line))
30 | {
31 | var splitBySpace = line.Split(new char[] { ' ', '=' }, StringSplitOptions.RemoveEmptyEntries);
32 | if (splitBySpace.First() != "var")
33 | {
34 | variableType = splitBySpace.First();
35 | }
36 | }
37 |
38 | Queue> components = new();
39 | var positions = new Queue>(GetBacktickedCommands(line).OrderBy(x => x.Item1));
40 |
41 | var lastEndPos = 0;
42 | while (positions.Any())
43 | {
44 | var currentPosition = positions.Dequeue();
45 | var backTickedStr = line.Substring(currentPosition.Item1, currentPosition.Item2);
46 |
47 | if (lastEndPos != currentPosition.Item1)
48 | {
49 | var irrelevantStr = line.Substring(lastEndPos, currentPosition.Item1 - lastEndPos -1); // +1 for backtick
50 | components.Enqueue(new Tuple(irrelevantStr, false));
51 | }
52 | components.Enqueue(new Tuple( backTickedStr, true ));
53 |
54 | lastEndPos = currentPosition.Item1 + currentPosition.Item2 +1; // +1 for the backtick
55 | }
56 |
57 | if (lastEndPos != line.Length)
58 | {
59 | components.Enqueue(new Tuple(line.Remove(0, lastEndPos), false));
60 | }
61 |
62 | var result = new StringBuilder();
63 | result.Append(MARKER);
64 | while (components.Any())
65 | {
66 | var currentComponent = components.Dequeue();
67 | if (currentComponent.Item2)
68 | {
69 | result.Append(ShellCommand.BuildLine(currentComponent.Item1, false, null, variableType, Execution.Redirection.Out));
70 | }
71 | else
72 | {
73 | result.Append(currentComponent.Item1);
74 | }
75 | }
76 | result.Append(ENDMARKER);
77 |
78 | return result.ToString();
79 | }
80 |
81 | public string GetMetaRepresentationMarker()
82 | {
83 | return MARKER;
84 | }
85 |
86 | public bool IsValid(string line)
87 | {
88 | return line.Contains("`");
89 | }
90 |
91 | internal List> GetBacktickedCommands(string input)
92 | {
93 | var ret = new List>();
94 |
95 | // first find the set of strings, backticked commands won't be inside these strings
96 | // for easier maintenance we replace these with spaces so we can see what is going on
97 | foreach (Match match in UnescapedStrings.Matches(input))
98 | {
99 | var newString = new string(' ', match.Length);
100 | input = input.Remove(match.Index, match.Length).Insert(match.Index, newString);
101 | }
102 |
103 | foreach (Match match in UnescapedBackTicks.Matches(input))
104 | {
105 | foreach (Group group in match.Groups)
106 | {
107 | if (!group.Value.Contains("`"))
108 | {
109 | ret.Add(new Tuple(group.Index, group.Length));
110 | }
111 | }
112 | }
113 |
114 | return ret;
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/CSharpRegion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Dotnet.Shell.Logic.Compilation.Commands
6 | {
7 | class CSharpRegion : IShellBlockCommand
8 | {
9 | private const string MARKER = "#region c#";
10 | private const string ENDMARKER = "#endregion";
11 |
12 | public string GetCodeFromMetaRepresentation(string line)
13 | {
14 | throw new NotImplementedException();
15 | }
16 |
17 | public string GetMetaRepresentation(IList lines)
18 | {
19 | // all we need to do is throw away the first and last lines
20 | // GetCode() will never be called here as we never return a meta representation
21 | return string.Join(Environment.NewLine, lines.Skip(1).Take(lines.Count - 2));
22 | }
23 |
24 | public bool IsEnd(string line)
25 | {
26 | return line.StartsWith(ENDMARKER);
27 | }
28 |
29 | public bool IsStart(string line)
30 | {
31 | return line.StartsWith(MARKER);
32 | }
33 |
34 | public string GetMetaRepresentationMarker()
35 | {
36 | // this check will never actually work as our meta representation is the final
37 | // representation
38 | return MARKER;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/CdCommand.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | class CdCommand : IShellCommand
6 | {
7 | public const string CD = "#region CD // ";
8 | public const string ENDMARKER = " #endregion";
9 |
10 | public string GetCodeFromMetaRepresentation(string line)
11 | {
12 | line = line.Replace(CD, string.Empty).Replace(ENDMARKER, string.Empty).Trim();
13 | var escapedInput = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(line)).ToFullString();
14 | return "Shell.ChangeDir(" + escapedInput + ");";
15 | }
16 |
17 | public string GetMetaRepresentation(string line)
18 | {
19 | return CD+(line.Remove(0, 2)).TrimEnd()+ENDMARKER;
20 | }
21 |
22 | public string GetMetaRepresentationMarker()
23 | {
24 | return CD;
25 | }
26 |
27 | public bool IsValid(string line)
28 | {
29 | return line.StartsWith("cd ") || line == "cd";
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/ClsCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Dotnet.Shell.Logic.Compilation.Commands
2 | {
3 | class ClsCommand : IShellCommand
4 | {
5 | public const string CLS = "#region cls";
6 | public const string ENDMARKER = " #endregion";
7 |
8 | public string GetCodeFromMetaRepresentation(string line)
9 | {
10 | return "Console.Clear();";
11 | }
12 |
13 | public string GetMetaRepresentation(string line)
14 | {
15 | return CLS+ENDMARKER;
16 | }
17 |
18 | public string GetMetaRepresentationMarker()
19 | {
20 | return CLS;
21 | }
22 |
23 | public bool IsValid(string line)
24 | {
25 | return line.StartsWith("#cls");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/CommandRegion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Dotnet.Shell.Logic.Compilation.Commands
6 | {
7 | class CommandRegion : IShellBlockCommand
8 | {
9 | private const string MARKER = "#region cmd";
10 | private const string ENDMARKER = "#endregion";
11 |
12 | public string GetCodeFromMetaRepresentation(string line)
13 | {
14 | throw new NotImplementedException();
15 | }
16 |
17 | public string GetMetaRepresentation(IList lines)
18 | {
19 | var linesBetweenMarkers = string.Join(Environment.NewLine, lines.Skip(1).Take(lines.Count - 2));
20 |
21 | var script = string.Join(" && ", linesBetweenMarkers).Trim();
22 |
23 | return new ShellCommand().GetMetaRepresentation(script);
24 | }
25 |
26 | public bool IsEnd(string line)
27 | {
28 | return line.StartsWith(ENDMARKER);
29 | }
30 |
31 | public bool IsStart(string line)
32 | {
33 | return line.StartsWith(MARKER);
34 | }
35 |
36 | public string GetMetaRepresentationMarker()
37 | {
38 | // GetCodeFromMetaRepresentation won't be returned as we diver to ShellCommands meta
39 | return MARKER;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/ExitCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | class ExitCommand : IShellCommand
6 | {
7 | public const string Exit = "#region exit #endregion";
8 |
9 | public string GetCodeFromMetaRepresentation(string line)
10 | {
11 | return "throw new ExitException();";
12 | }
13 |
14 | public string GetMetaRepresentation(string line)
15 | {
16 | return Exit;
17 | }
18 |
19 | public string GetMetaRepresentationMarker()
20 | {
21 | return Exit;
22 | }
23 |
24 | public bool IsValid(string line)
25 | {
26 | return (line.StartsWith("#exit") || line == "exit");
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/IShellBlockCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | interface IShellBlockCommand
6 | {
7 | bool IsStart(string line);
8 |
9 | bool IsEnd(string line);
10 |
11 | string GetMetaRepresentation(IList lines);
12 |
13 | string GetCodeFromMetaRepresentation(string line);
14 |
15 | public string GetMetaRepresentationMarker();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/IShellCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Dotnet.Shell.Logic.Compilation.Commands
2 | {
3 | interface IShellCommand
4 | {
5 | bool IsValid(string line);
6 |
7 | string GetMetaRepresentation(string line);
8 |
9 | string GetCodeFromMetaRepresentation(string line);
10 |
11 | string GetMetaRepresentationMarker();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/LoadCommand.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | class LoadCommand : IShellCommand
6 | {
7 | const string LOAD = "#region load //";
8 | const string ENDMARKER = " #endregion";
9 |
10 | public string GetCodeFromMetaRepresentation(string line)
11 | {
12 | var argument = line.Replace(LOAD, string.Empty).Replace(ENDMARKER, string.Empty).Trim();
13 |
14 | if (argument.Contains("$")) // argument is a variable, we need to execute through a wrapper to unpack the variable
15 | {
16 | return "await Shell.LoadScriptFromFileAsync(Shell.ConvertPathToAbsolute(" + ShellCommandUtilities.VariableExpansion(argument) + "));";
17 | }
18 | else
19 | {
20 | var escapedInput = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(argument)).ToFullString();
21 | return "await Shell.LoadScriptFromFileAsync(Shell.ConvertPathToAbsolute(" + escapedInput + "));";
22 | }
23 | }
24 |
25 | public string GetMetaRepresentation(string line)
26 | {
27 | return LOAD + line.Remove(0, 6);
28 | }
29 |
30 | public string GetMetaRepresentationMarker()
31 | {
32 | return LOAD;
33 | }
34 |
35 | public bool IsValid(string line)
36 | {
37 | return line.StartsWith("#load ");
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/RefCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | class RefCommand : IShellCommand
6 | {
7 | const string REF = "#region r //";
8 |
9 | public string GetCodeFromMetaRepresentation(string line)
10 | {
11 | throw new NotImplementedException();
12 | }
13 |
14 | public string GetMetaRepresentation(string line)
15 | {
16 | // return ourselves, GetCodeFromMetaRepresentation won't be called
17 | return line;
18 | }
19 |
20 | public string GetMetaRepresentationMarker()
21 | {
22 | // #r is basically supported now
23 | return REF;
24 | }
25 |
26 | public bool IsValid(string line)
27 | {
28 | return line.StartsWith("#r ");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/ResetCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Dotnet.Shell.Logic.Compilation.Commands
2 | {
3 | class ResetCommand : IShellCommand
4 | {
5 | public const string RESET = "#region reset";
6 | public const string ENDMARKER = " #endregion";
7 |
8 | public string GetCodeFromMetaRepresentation(string line)
9 | {
10 | return "#reset";
11 | }
12 |
13 | public string GetMetaRepresentation(string line)
14 | {
15 | return RESET + ENDMARKER;
16 | }
17 |
18 | public string GetMetaRepresentationMarker()
19 | {
20 | return RESET;
21 | }
22 |
23 | public bool IsValid(string line)
24 | {
25 | return line.StartsWith("#reset");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/SheBangCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dotnet.Shell.Logic.Compilation.Commands
4 | {
5 | class SheBangCommand : IShellCommand
6 | {
7 | private const string MARKER = "#!";
8 |
9 | public string GetCodeFromMetaRepresentation(string line)
10 | {
11 | throw new NotImplementedException();
12 | }
13 |
14 | public string GetMetaRepresentation(string line)
15 | {
16 | // basically deletes the line
17 | return string.Empty;
18 | }
19 |
20 | public string GetMetaRepresentationMarker()
21 | {
22 | return MARKER;
23 | }
24 |
25 | public bool IsValid(string line)
26 | {
27 | return line.StartsWith(MARKER);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/ShellCommand.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Execution;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 |
6 | namespace Dotnet.Shell.Logic.Compilation.Commands
7 | {
8 | class ShellCommand : IShellCommand
9 | {
10 | private const string CMD = "#region CMD //";
11 | public const string ENDMARKER = " #endregion";
12 | private static readonly Regex Vars = new(@"[$].+[$]", RegexOptions.Compiled);
13 |
14 | internal static string BuildLine(string commandToRun, bool async, string variableName = null, string returnType = null, Redirection redirection = Redirection.None)
15 | {
16 | var returnHandling = returnType == null ? "" : string.Format(".ConvertStdOutToVariable<{0}>()", returnType);
17 | var method = async ? "await Shell.ExecuteAsync" : "Shell.Execute";
18 | // argument is a variable, we need to execute through a wrapper to unpack the variable
19 | var cmd = Vars.IsMatch(commandToRun) ?
20 | ShellCommandUtilities.VariableExpansion('"' + commandToRun + '"') :
21 | SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(commandToRun)).ToFullString();
22 | var variable = !string.IsNullOrWhiteSpace(variableName) ? "var " + variableName + "=" : string.Empty;
23 |
24 | var redir = string.Empty;
25 | if (redirection.HasFlag(Redirection.Out) && redirection.HasFlag(Redirection.Err))
26 | {
27 | redir = ", Redirection.Out | Redirection.Err";
28 | }
29 | else if (redirection.HasFlag(Redirection.Out))
30 | {
31 | redir = ", Redirection.Out";
32 | }
33 | else if (redirection.HasFlag(Redirection.Err))
34 | {
35 | redir = ", Redirection.Err";
36 | }
37 |
38 | return string.Format("{0}{1}({2}{3}){4}", variable, method, cmd, redir, returnHandling).Trim();
39 | }
40 |
41 | public string GetCodeFromMetaRepresentation(string line)
42 | {
43 | line = line.Replace(CMD, string.Empty).Replace(ENDMARKER, string.Empty);
44 | return "_= "+BuildLine(line, true)+";";
45 | }
46 |
47 | public string GetMetaRepresentation(string line)
48 | {
49 | return CMD+line+ENDMARKER;
50 | }
51 |
52 | public string GetMetaRepresentationMarker()
53 | {
54 | return CMD;
55 | }
56 |
57 | public bool IsValid(string line)
58 | {
59 | // matches what basically is a pretty weak regex for linux style commands
60 | // and does not start with a reserved word
61 | var regex = @"^\w+-*\w+([ ].+)*(? line.StartsWith(word));
65 | }
66 | return false;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Commands/ShellCommandUtilities.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 |
8 | namespace Dotnet.Shell.Logic.Compilation.Commands
9 | {
10 | internal class ShellCommandUtilities
11 | {
12 | internal static readonly string[] ReservedWords = new[] {
13 | "public ",
14 | "private ",
15 | "protected ",
16 | "internal ",
17 | "class ",
18 | "static ",
19 | "if ",
20 | "else",
21 | "while ",
22 | "for ",
23 | "foreach ",
24 | "abstract ",
25 | "base ",
26 | "delegate ",
27 | "lock ",
28 | "interface ",
29 | "namespace ",
30 | "switch ",
31 | "case ",
32 | "default:",
33 | "void ",
34 | "return ",
35 | };
36 |
37 | internal static string VariableExpansion(string shellCommand)
38 | {
39 | // What is this nightmare regex?
40 | // * Match anything between two $
41 | // * But NOT if the character before the $ is a \
42 | // * But NOT if there is a $ inside the two $
43 |
44 | /*
45 | * Matches
46 | hello my name is $bob$
47 | hello my name is $bob1$
48 | hello my name is $b$ob$ only ob
49 |
50 | Does not match
51 | hello my name is "$hello"
52 | hello my name is \$bob\$
53 | */
54 |
55 | //var rx = new Regex(@"[^\\]*([$][^$]+[^\\][$])");
56 | var rx = new Regex(@".*([^\\]*[$].+[^\\]*[$]).*");
57 | Match match = rx.Match(shellCommand);
58 |
59 | if (match == null || !match.Success || match.Groups.Count == 0)
60 | {
61 | return shellCommand;
62 | }
63 |
64 | // start with the smallest replacement and work up
65 | var matchGroup = match.Groups.Values.OrderBy(x => x.Length).First();
66 |
67 | var possibleVariableName = matchGroup.Value.Trim('$');
68 | var startPos = matchGroup.Index;
69 |
70 | // remove $varName$
71 | shellCommand = shellCommand.Remove(startPos, matchGroup.Value.Length);
72 |
73 | var strToInsert = "\"+" + possibleVariableName + "+\"";
74 |
75 | shellCommand = shellCommand.Insert(startPos, strToInsert);
76 |
77 | // check for more variables
78 | shellCommand = VariableExpansion(shellCommand);
79 |
80 | const string EmptyStrEnd = "+\"";
81 | const string EmptyStrStart = "\"+";
82 | const string DoubleEmptyStrEnd = "+\"\"";
83 | const string DoubleEmptyStrStart = "\"\"+";
84 | if (shellCommand.EndsWith(EmptyStrEnd))
85 | {
86 | shellCommand = shellCommand.Remove(shellCommand.Length - EmptyStrEnd.Length);
87 | }
88 | if (shellCommand.StartsWith(EmptyStrStart))
89 | {
90 | shellCommand = shellCommand.Remove(0, EmptyStrEnd.Length);
91 | }
92 |
93 | if (shellCommand.EndsWith(DoubleEmptyStrEnd))
94 | {
95 | shellCommand = shellCommand.Remove(shellCommand.Length - DoubleEmptyStrEnd.Length);
96 | }
97 | if (shellCommand.StartsWith(DoubleEmptyStrStart))
98 | {
99 | shellCommand = shellCommand.Remove(0, DoubleEmptyStrStart.Length);
100 | }
101 |
102 | return shellCommand;
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/CommentWalker.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using System.Collections.Generic;
4 |
5 | namespace Dotnet.Shell.Logic.Compilation
6 | {
7 | internal class CommentWalker : CSharpSyntaxWalker
8 | {
9 | public List Comments = new();
10 | public List SingleLineComments = new();
11 |
12 | public CommentWalker(SyntaxWalkerDepth depth = SyntaxWalkerDepth.Trivia) : base(depth)
13 | {
14 | }
15 |
16 | public override void VisitTrivia(SyntaxTrivia trivia)
17 | {
18 | if (trivia.IsKind(SyntaxKind.MultiLineCommentTrivia)
19 | || trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)
20 | || trivia.IsKind(SyntaxKind.XmlComment)
21 | || trivia.IsKind(SyntaxKind.XmlComment)
22 | || trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
23 | {
24 | if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
25 | {
26 | // we need to check to ensure that this isn't a URL masquerading as a comment.
27 | // In .nsh files a single line comment can be 'escaped' by a :
28 | var preText = trivia.SyntaxTree.GetText();
29 | var startOfPossibleComment = preText.ToString().Substring(0, trivia.SpanStart);
30 |
31 | if (trivia.Token.ValueText == ":" || startOfPossibleComment.EndsWith(":"))
32 | {
33 | return;
34 | }
35 |
36 | SingleLineComments.Add(trivia.ToFullString());
37 | }
38 |
39 | Comments.Add(trivia.GetLocation());
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/Exceptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Dotnet.Shell.Logic.Compilation
5 | {
6 | public class PreProcessorSyntaxException : Exception
7 | {
8 | public List RelatedLines { get; } = new List();
9 | public int Line { get; }
10 |
11 | public PreProcessorSyntaxException(string msg, int line) : base(msg)
12 | {
13 | Line = line;
14 | }
15 |
16 | public PreProcessorSyntaxException(string msg, int line, List relatedLines) : this(msg, line)
17 | {
18 | RelatedLines.AddRange(relatedLines);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/InteractiveRunner.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Immutable;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Dotnet.Script.Core;
7 | using Dotnet.Script.DependencyModel.Context;
8 | using Dotnet.Script.DependencyModel.Logging;
9 | using Dotnet.Script.DependencyModel.NuGet;
10 | using Dotnet.Shell.UI;
11 | using Microsoft.CodeAnalysis;
12 | using Microsoft.CodeAnalysis.CSharp;
13 | using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
14 | using Microsoft.CodeAnalysis.Scripting;
15 | using Microsoft.CodeAnalysis.Scripting.Hosting;
16 | using Microsoft.CodeAnalysis.Text;
17 | using Microsoft.Extensions.Logging;
18 | using Microsoft.Extensions.Logging.Console;
19 |
20 | namespace Dotnet.Shell.Logic.Compilation
21 | {
22 | ///
23 | /// This class integrates dotnet-shell with Dotnet Script.
24 | /// It is based on a simplified version of https://github.com/filipw/dotnet-script/blob/master/src/Dotnet.Script.Core/Interactive/InteractiveRunner.cs
25 | ///
26 | internal class InteractiveRunner
27 | {
28 | private ScriptState scriptState;
29 | private ScriptOptions scriptOptions;
30 |
31 | private readonly InteractiveScriptGlobals globals;
32 | private readonly string[] packageSources = Array.Empty();
33 |
34 | protected Logger logger;
35 | protected ScriptCompiler scriptCompiler;
36 | protected ScriptConsole console = ScriptConsole.Default;
37 | protected CSharpParseOptions parseOptions = new(LanguageVersion.Latest, kind: SourceCodeKind.Script);
38 | protected InteractiveCommandProvider interactiveCommandParser = new();
39 |
40 | public ImmutableDictionary ScriptVariables
41 | {
42 | get
43 | {
44 | var vars = new Dictionary();
45 | foreach (var variable in this.scriptState.Variables)
46 | {
47 | if (!vars.ContainsKey(variable.Name))
48 | {
49 | vars.Add(variable.Name, variable.Value);
50 | }
51 | }
52 | return vars.ToImmutableDictionary();
53 | }
54 | }
55 |
56 | public InteractiveRunner(ErrorDisplay errorDisplay)
57 | {
58 | var logFactory = CreateLogFactory("WARNING", errorDisplay);
59 | logger = logFactory.CreateLogger();
60 |
61 | globals = new InteractiveScriptGlobals(console.Out, CSharpObjectFormatter.Instance);
62 | scriptCompiler = new ScriptCompiler(logFactory, false);
63 | }
64 |
65 | public async Task ExecuteAsync(string input, string workingDirectory)
66 | {
67 | if (scriptState == null)
68 | {
69 | var sourceText = SourceText.From(input);
70 | var context = new ScriptContext(sourceText, workingDirectory, Enumerable.Empty(), scriptMode: ScriptMode.REPL, packageSources: packageSources);
71 | await RunFirstScriptAsync(context);
72 | }
73 | else
74 | {
75 | if (input.StartsWith("#r ") || input.StartsWith("#load "))
76 | {
77 | var lineRuntimeDependencies = scriptCompiler.RuntimeDependencyResolver.GetDependenciesForCode(workingDirectory, ScriptMode.REPL, packageSources, input).ToArray();
78 | var lineDependencies = lineRuntimeDependencies.SelectMany(rtd => rtd.Assemblies).Distinct();
79 |
80 | var scriptMap = lineRuntimeDependencies.ToDictionary(rdt => rdt.Name, rdt => rdt.Scripts);
81 | if (scriptMap.Count > 0)
82 | {
83 | scriptOptions =
84 | scriptOptions.WithSourceResolver(
85 | new NuGetSourceReferenceResolver(
86 | new SourceFileResolver(ImmutableArray.Empty, workingDirectory), scriptMap));
87 | }
88 | foreach (var runtimeDependency in lineDependencies)
89 | {
90 | logger.Debug("Adding reference to a runtime dependency => " + runtimeDependency);
91 | scriptOptions = scriptOptions.AddReferences(MetadataReference.CreateFromFile(runtimeDependency.Path));
92 | }
93 | }
94 | scriptState = await scriptState.ContinueWithAsync(input, scriptOptions);
95 | }
96 |
97 | return scriptState.ReturnValue;
98 | }
99 |
100 | private async Task RunFirstScriptAsync(ScriptContext scriptContext)
101 | {
102 | foreach (var arg in scriptContext.Args)
103 | {
104 | globals.Args.Add(arg);
105 | }
106 |
107 | var compilationContext = scriptCompiler.CreateCompilationContext(scriptContext);
108 | console.WriteDiagnostics(compilationContext.Warnings, compilationContext.Errors);
109 |
110 | if (compilationContext.Errors.Any())
111 | {
112 | throw new CompilationErrorException("Script compilation failed due to one or more errors.", compilationContext.Errors.ToImmutableArray());
113 | }
114 |
115 | scriptState = await compilationContext.Script.RunAsync(globals, ex => true).ConfigureAwait(false);
116 | scriptOptions = compilationContext.ScriptOptions;
117 | }
118 |
119 | private static LogFactory CreateLogFactory(string verbosity, ErrorDisplay errorDisplay)
120 | {
121 | var logLevel = (Microsoft.Extensions.Logging.LogLevel)LevelMapper.FromString(verbosity);
122 |
123 | var loggerFilterOptions = new LoggerFilterOptions() { MinLevel = logLevel };
124 |
125 | var consoleLoggerProvider = new ConsoleLoggerProvider(errorDisplay);
126 |
127 | var loggerFactory = new LoggerFactory(new[] { consoleLoggerProvider }, loggerFilterOptions);
128 |
129 | return type =>
130 | {
131 | var logger = loggerFactory.CreateLogger(type);
132 | return (level, message, exception) =>
133 | {
134 | logger.Log((Microsoft.Extensions.Logging.LogLevel)level, message, exception);
135 | };
136 | };
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/src/Shell/Logic/Compilation/SourceProcessor.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Compilation.Commands;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.Formatting;
5 | using Microsoft.CodeAnalysis.Options;
6 | using Microsoft.CodeAnalysis.Text;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Text.RegularExpressions;
12 | using System.Threading.Tasks;
13 |
14 | namespace Dotnet.Shell.Logic.Compilation
15 | {
16 |
17 | internal class SourceProcessor
18 | {
19 | private readonly CSharpParseOptions parseOptions = new(LanguageVersion.Latest, kind: SourceCodeKind.Script);
20 | private readonly List>> processingChain = new();
21 | private readonly List syntaxToProcess = new();
22 | private readonly List blockSyntaxToProcess = new();
23 | private readonly ShellCommand shellCommand;
24 | private readonly List postProcessingSyntax = new();
25 |
26 | public SourceProcessor()
27 | {
28 | syntaxToProcess.AddRange(new IShellCommand[] {
29 | new SheBangCommand(),
30 | new ExitCommand(),
31 | new ClsCommand(),
32 | new RefCommand(),
33 | new LoadCommand(),
34 | new CdCommand(),
35 | new BacktickCommand(),
36 | new ResetCommand()
37 | });
38 | blockSyntaxToProcess.AddRange(new IShellBlockCommand[]
39 | {
40 | new CSharpRegion(),
41 | new CommandRegion()
42 | });
43 | shellCommand = new ShellCommand();
44 | postProcessingSyntax.AddRange(syntaxToProcess);
45 | postProcessingSyntax.Add(shellCommand);
46 | processingChain.Add(ReplaceBuiltInCommandsWithMetaAsync);
47 | processingChain.Add(StripAllCommentsAsync);
48 | processingChain.Add(FormatCodeAsync);
49 | processingChain.Add(BuildFinalSourceAsync);
50 | }
51 |
52 | private async Task ReplaceBuiltInCommandsWithMetaAsync(string input)
53 | {
54 | var singleLineComments = GetComments(input).SingleLineComments;
55 |
56 | using (var sr = new StringReader(input))
57 | using (var sw = new StringWriter())
58 | {
59 | int lineNumber = 1;
60 | var line = await sr.ReadLineAsync();
61 | try
62 | {
63 | var currentBlockCommands = new Dictionary>();
64 |
65 | while (line != null)
66 | {
67 | var matchComments = singleLineComments
68 | .Where(x => line.EndsWith(x))
69 | .Distinct()
70 | .ToList();
71 |
72 | matchComments.ForEach(matchingComment =>
73 | {
74 | line = line.Remove(line.Length - matchingComment.Length);
75 | });
76 | var whitespaceRemovedLine = line.Trim();
77 |
78 | // add the line to anything handling blocks
79 | currentBlockCommands.Values.ToList().ForEach(x => x.Add(line));
80 |
81 | // if there is a block which has found its end marker then process the command and remove it
82 | var completedBlocksCommands = currentBlockCommands.Keys.Where(blockCommand => blockCommand.IsEnd(whitespaceRemovedLine)).ToList();
83 | var newBlocks = blockSyntaxToProcess.Where(blockCommandParser => blockCommandParser.IsStart(whitespaceRemovedLine));
84 |
85 | if (currentBlockCommands.Any() || completedBlocksCommands.Any())
86 | {
87 | // if we are processing any block command or have emitted results from a block command then don't process anything
88 | #pragma warning disable VSTHRD101 // Avoid unsupported async delegates (If this fails something terrible has happened anyway)
89 | completedBlocksCommands.ForEach(async x => await sw.WriteLineAsync(x.GetMetaRepresentation(currentBlockCommands[x])));
90 | #pragma warning restore VSTHRD101 // Avoid unsupported async delegates
91 | completedBlocksCommands.ForEach(x => currentBlockCommands.Remove(x));
92 | }
93 | else if (newBlocks.Any())
94 | {
95 | if (newBlocks.Count() > 1)
96 | {
97 | throw new PreProcessorSyntaxException("Error, multiple blocks match a single line", lineNumber);
98 | }
99 | else
100 | {
101 | currentBlockCommands.Add(newBlocks.First(), new List() { line });
102 | }
103 | }
104 | else if (string.IsNullOrWhiteSpace(line) || LooksLikeCSharp(whitespaceRemovedLine))
105 | {
106 | await sw.WriteLineAsync(line);
107 | }
108 | else
109 | {
110 | var builtinCommand = syntaxToProcess.FirstOrDefault(x => x.IsValid(whitespaceRemovedLine));
111 |
112 | if (builtinCommand != null)
113 | {
114 | await sw.WriteLineAsync(builtinCommand.GetMetaRepresentation(whitespaceRemovedLine));
115 | }
116 | else if (shellCommand != null && shellCommand.IsValid(whitespaceRemovedLine))
117 | {
118 | await sw.WriteLineAsync(shellCommand.GetMetaRepresentation(whitespaceRemovedLine));
119 | }
120 | else
121 | {
122 | await sw.WriteLineAsync(line);
123 | }
124 | }
125 |
126 | line = await sr.ReadLineAsync();
127 | lineNumber++;
128 | }
129 | }
130 | catch (Exception ex)
131 | {
132 | throw new PreProcessorSyntaxException(ex.Message, lineNumber);
133 | }
134 |
135 | return sw.ToString();
136 | }
137 | }
138 |
139 | private static bool LooksLikeCSharp(string line)
140 | {
141 | // todo remove any strings in the line
142 |
143 | var csharpEndChars = new char[] { ';', '{', '}', '(', ')', ',', ']' };
144 |
145 | // This regex matches assignment to a variable from a command in the form
146 | // var x = `ls`;
147 | // There are some obvious issues here:
148 | // * c# is more flexible with variable names
149 | // * only one variable assignment can be on a line
150 | var CmdToVariableNoSemiColonRegex = new Regex(@"^[a-zA-Z]+\d*\s+[a-zA-Z]+\d*\s*=\s*`.+`\s*$");
151 |
152 | if (CmdToVariableNoSemiColonRegex.IsMatch(line))
153 | {
154 | throw new Exception("Missing ; : "+line);
155 | }
156 | else if (line.Count(x => x == '`') > 1)
157 | {
158 | return false;
159 | }
160 | else if (csharpEndChars.Any(x => line.EndsWith(x)) || ShellCommandUtilities.ReservedWords.Any(word => line.StartsWith(word)))
161 | {
162 | return true;
163 | }
164 | else
165 | {
166 | return false;
167 | }
168 | }
169 |
170 | private CommentWalker GetComments(string text)
171 | {
172 | var tree = CSharpSyntaxTree.ParseText(text, parseOptions);
173 | var root = tree.GetCompilationUnitRoot();
174 |
175 | var walker = new CommentWalker();
176 | walker.Visit(root);
177 |
178 | return walker;
179 | }
180 |
181 | private Task StripAllCommentsAsync(string text)
182 | {
183 | return Task.Run(() =>
184 | {
185 | var walker = GetComments(text);
186 |
187 | var changes = walker.Comments.ConvertAll(comment => new TextChange(comment.SourceSpan, string.Empty));
188 |
189 | if (changes.Any())
190 | {
191 | var source = SourceText.From(text);
192 | return source.WithChanges(changes).ToString();
193 | }
194 | else
195 | {
196 | return text;
197 | }
198 | });
199 | }
200 |
201 | private Task FormatCodeAsync(string text)
202 | {
203 | return Task.Run(() =>
204 | {
205 | var source = SourceText.From(text);
206 | var tree = CSharpSyntaxTree.ParseText(source, parseOptions);
207 | var root = tree.GetCompilationUnitRoot();
208 |
209 | // now we reformat the source tree to make it consistent for the preprocessor to handle it
210 | var workspace = new AdhocWorkspace();
211 | OptionSet options = workspace.Options;
212 |
213 | SyntaxNode formattedNode = Formatter.Format(root, workspace, options);
214 |
215 | // emit some new source text but also replace `" with `
216 | using (var tw = new StringWriter())
217 | {
218 | formattedNode.WriteTo(tw);
219 | return tw.ToString();
220 | }
221 | });
222 | }
223 |
224 | private async Task BuildFinalSourceAsync(string input)
225 | {
226 | using (var sr = new StringReader(input))
227 | using (var sw = new StringWriter())
228 | {
229 | var line = await sr.ReadLineAsync();
230 | while (line != null)
231 | {
232 | line = line.TrimStart();
233 | var cmds = postProcessingSyntax.Where(x => line.StartsWith(x.GetMetaRepresentationMarker()));
234 |
235 | if (cmds.Any())
236 | {
237 | if (cmds.Count() != 1)
238 | {
239 | throw new PreProcessorSyntaxException("Duplicate format: "+string.Join(", ", cmds.Select(x => x.GetMetaRepresentationMarker())), 0);
240 | }
241 | await sw.WriteLineAsync(cmds.First().GetCodeFromMetaRepresentation(line));
242 | }
243 | else
244 | {
245 | await sw.WriteLineAsync(line);
246 | }
247 |
248 | line = await sr.ReadLineAsync();
249 | }
250 |
251 | return sw.ToString();
252 | }
253 | }
254 |
255 | public async Task ProcessAsync(string script)
256 | {
257 | foreach (var step in processingChain)
258 | {
259 | script = await step(script);
260 | }
261 | return script.Trim();
262 | }
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Console/DotNetConsole.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Execution;
2 | using System;
3 | using System.Drawing;
4 | using System.Runtime.InteropServices;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace Dotnet.Shell.Logic.Console
9 | {
10 | public class DotNetConsole : IConsole
11 | {
12 | public int CursorLeft { get => System.Console.CursorLeft; set { System.Console.CursorLeft = value; } }
13 | public int CursorTop { get => System.Console.CursorTop; set { System.Console.CursorTop = value; } }
14 |
15 | public ConsoleColor ForegroundColor { get => System.Console.ForegroundColor; set { System.Console.ForegroundColor = value; } }
16 | public ConsoleColor BackgroundColor { get => System.Console.BackgroundColor; set { System.Console.BackgroundColor = value; } }
17 |
18 | bool IConsole.CursorVisible { set { System.Console.CursorVisible = value; } }
19 | int IConsole.WindowWidth { get => System.Console.WindowWidth; }
20 | int IConsole.WindowHeight { get => System.Console.WindowHeight; }
21 |
22 | public bool KeyAvailiable { get => System.Console.KeyAvailable; }
23 |
24 | private Point savedCursorPos;
25 |
26 | public ConsoleKeyInfo ReadKey()
27 | {
28 | return System.Console.ReadKey();
29 | }
30 |
31 | public void Write(string text) => System.Console.Write(text);
32 |
33 | public void WriteLine(string message) => System.Console.WriteLine(message);
34 |
35 | public Task SaveAsync()
36 | {
37 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
38 | {
39 | var p = OS.Exec("tput smcup"); // todo
40 | return p.WaitForExitAsync();
41 | }
42 | else
43 | {
44 | System.Console.Clear();
45 | return Task.CompletedTask;
46 | }
47 | }
48 |
49 | public Task RestoreAsync()
50 | {
51 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
52 | {
53 | var p = OS.Exec("tput rmcup"); // todo
54 | return p.WaitForExitAsync();
55 | }
56 | else
57 | {
58 | System.Console.Clear();
59 | return Task.CompletedTask;
60 | }
61 | }
62 |
63 | public void ClearCurrentLine(int pos = -1)
64 | {
65 | if (pos == -1) // clear entire line, don't change position
66 | {
67 | // If n is 2, clear entire line. Cursor position does not change.
68 | System.Console.Write("\u001B[2K");
69 | }
70 | else // clear from position which will be set
71 | {
72 | //If n is 0 (or missing), clear from cursor to the end of the line.
73 | System.Console.Write("\u001B[0K");
74 | }
75 | }
76 |
77 | public void SaveCursorPosition()
78 | {
79 | savedCursorPos = new Point(System.Console.CursorLeft, System.Console.CursorTop);
80 | }
81 |
82 | public void RestoreCursorPosition(Action onRestore = null)
83 | {
84 | System.Console.CursorLeft = savedCursorPos.X;
85 | System.Console.CursorTop = savedCursorPos.Y;
86 | onRestore?.Invoke();
87 | }
88 |
89 | public void MoveCursorDown(int lines)
90 | {
91 | System.Console.Write("\u001B[" + lines + "B");
92 | }
93 |
94 | public void MoveCursorUp(int lines)
95 | {
96 | System.Console.Write("\u001B[" + lines + "A");
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Console/HideCursor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dotnet.Shell.Logic.Console
4 | {
5 | internal class HideCursor : IDisposable
6 | {
7 | private readonly IConsole c;
8 |
9 | public HideCursor(IConsole c)
10 | {
11 | this.c = c;
12 | c.CursorVisible = false;
13 | }
14 |
15 | public void Dispose()
16 | {
17 | c.CursorVisible = true;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Console/IConsole.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace Dotnet.Shell.Logic.Console
5 | {
6 | ///
7 | /// Defines the method that a console implementation is required to provide
8 | ///
9 | public interface IConsole
10 | {
11 | ///
12 | /// Gets or sets the cursor left position.
13 | ///
14 | ///
15 | /// The cursor left position.
16 | ///
17 | int CursorLeft { get; set; }
18 |
19 | ///
20 | /// Gets or sets the cursor top position.
21 | ///
22 | ///
23 | /// The cursor top position.
24 | ///
25 | int CursorTop { get; set; }
26 |
27 | ///
28 | /// Sets a value indicating whether [cursor visible].
29 | ///
30 | ///
31 | /// true if [cursor visible]; otherwise, false .
32 | ///
33 | bool CursorVisible { set; }
34 |
35 | ///
36 | /// Gets the width of the window.
37 | ///
38 | ///
39 | /// The width of the window.
40 | ///
41 | int WindowWidth { get; }
42 |
43 | ///
44 | /// Gets the height of the window.
45 | ///
46 | ///
47 | /// The height of the window.
48 | ///
49 | int WindowHeight { get; }
50 |
51 | ///
52 | /// Gets or sets the color of the foreground.
53 | ///
54 | ///
55 | /// The color of the foreground.
56 | ///
57 | ConsoleColor ForegroundColor { get; set; }
58 |
59 | ///
60 | /// Gets or sets the color of the background.
61 | ///
62 | ///
63 | /// The color of the background.
64 | ///
65 | ConsoleColor BackgroundColor { get; set; }
66 |
67 | ///
68 | /// Writes the specified text.
69 | ///
70 | /// The text.
71 | void Write(string text = default);
72 |
73 | ///
74 | /// Writes the specified text with a newline at the end
75 | ///
76 | /// The message.
77 | void WriteLine(string message = default);
78 |
79 | ///
80 | /// Saves the terminal screen state
81 | ///
82 | Task SaveAsync();
83 |
84 | ///
85 | /// Restores the terminal screen state
86 | ///
87 | Task RestoreAsync();
88 |
89 | ///
90 | /// Reads the next key press
91 | ///
92 | /// ConsoleKeyInfo
93 | ConsoleKeyInfo ReadKey();
94 |
95 | ///
96 | /// Gets a value indicating whether [key availiable].
97 | ///
98 | ///
99 | /// true if [key availiable]; otherwise, false .
100 | ///
101 | bool KeyAvailiable { get; }
102 |
103 | ///
104 | /// Clears the current line from given position.
105 | ///
106 | /// If set the current line will be cleared from this position, cursor will be moved here.
107 | void ClearCurrentLine(int pos = -1);
108 |
109 | ///
110 | /// Saves the cursor position.
111 | ///
112 | void SaveCursorPosition();
113 |
114 | ///
115 | /// Restores the cursor position.
116 | ///
117 | /// The on restore.
118 | void RestoreCursorPosition(Action onRestore = null);
119 |
120 | ///
121 | /// Moves the cursor down.
122 | ///
123 | /// The lines.
124 | void MoveCursorDown(int lines);
125 |
126 | ///
127 | /// Moves the cursor up.
128 | ///
129 | /// The lines.
130 | void MoveCursorUp(int lines);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Execution/OS.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.API;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Runtime.InteropServices;
7 | using System.Threading.Tasks;
8 | using System.Linq;
9 | using Newtonsoft.Json;
10 | using System.Collections.Concurrent;
11 | using System.Text;
12 |
13 | namespace Dotnet.Shell.Logic.Execution
14 | {
15 | ///
16 | /// Flags to determine which stream has been redirected
17 | ///
18 | [Flags]
19 | public enum Redirection
20 | {
21 | ///
22 | /// No stream redirection
23 | ///
24 | None = 0,
25 | ///
26 | /// StdOut redirection
27 | ///
28 | Out = 1,
29 | ///
30 | /// StdErr redirection
31 | ///
32 | Err = 2
33 | }
34 |
35 | ///
36 | /// This class implements OS actions such as Execution
37 | ///
38 | public class OS
39 | {
40 | ///
41 | /// Executes the specified cmdline.
42 | ///
43 | /// The cmdline.
44 | /// The shell object.
45 | /// The redirection object.
46 | /// Process
47 | public static ProcessEx Exec(string cmdline, object shellObj = null, Object redirectionObj = null)
48 | {
49 | Dotnet.Shell.API.Shell shell = shellObj as Dotnet.Shell.API.Shell;
50 | Redirection redirection = redirectionObj == null ? Redirection.None : (Redirection)redirectionObj;
51 |
52 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Settings.Default.SubShellArgumentsFormat.Contains("-Encoded"))
53 | {
54 | cmdline = Convert.ToBase64String(Encoding.Unicode.GetBytes(cmdline));
55 | }
56 |
57 | var proc = new Process();
58 | proc.StartInfo.RedirectStandardError = redirection.HasFlag(Redirection.Err);
59 | proc.StartInfo.RedirectStandardOutput = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || redirection.HasFlag(Redirection.Out);
60 | proc.StartInfo.RedirectStandardInput = false;
61 | proc.StartInfo.WorkingDirectory = shell != null ? shell.WorkingDirectory : Environment.CurrentDirectory;
62 | proc.StartInfo.CreateNoWindow = true;
63 | proc.StartInfo.FileName = Settings.Default.SubShell;
64 | proc.StartInfo.Arguments = string.Format(Settings.Default.SubShellArgumentsFormat, cmdline.Replace("\"", "\\\""));
65 | proc.StartInfo.UseShellExecute = false;
66 |
67 | if (shell != null)
68 | {
69 | // add any variables that have been created
70 | foreach (var kvp in shell.EnvironmentVariables())
71 | {
72 | if (!proc.StartInfo.EnvironmentVariables.ContainsKey(kvp.Key))
73 | {
74 | proc.StartInfo.EnvironmentVariables.Add(kvp.Key, kvp.Value);
75 | }
76 | else if (kvp.Key.ToLower() == "path")
77 | {
78 | proc.StartInfo.EnvironmentVariables.Remove(kvp.Key);
79 | proc.StartInfo.EnvironmentVariables.Add(kvp.Key, kvp.Value);
80 | }
81 | }
82 | }
83 |
84 | proc.Start();
85 | var procEx = new ProcessEx(proc);
86 |
87 | // Windows handles stdout redirection differently, to work around this we copy to console if
88 | // we have no redirection otherwise we copy to an internal stream
89 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !redirection.HasFlag(Redirection.Out))
90 | {
91 | proc.StandardOutput.BaseStream.CopyTo(System.Console.OpenStandardOutput());
92 | }
93 |
94 | return procEx;
95 | }
96 |
97 | ///
98 | /// Gets the OS command history.
99 | ///
100 | /// History
101 | public static async Task> GetOSHistoryAsync()
102 | {
103 | var history = new ConcurrentBag();
104 | var tasksToWaitOn = new List();
105 |
106 | foreach (var additionalHistoryFile in Settings.Default.AdditionalHistoryFiles)
107 | {
108 | tasksToWaitOn.Add(Task.Run(async () =>
109 | {
110 | if (File.Exists(additionalHistoryFile))
111 | {
112 | var lines = await File.ReadAllLinesAsync(additionalHistoryFile);
113 | for (int offset = 0; offset < lines.Length; offset++)
114 | {
115 | if (!string.IsNullOrWhiteSpace(lines[offset]))
116 | {
117 | history.Add(new HistoryItem(lines[offset], offset));
118 | }
119 | }
120 | }
121 | }));
122 | }
123 |
124 | tasksToWaitOn.Add(Task.Run(async () => {
125 | if (File.Exists(Settings.Default.HistoryFile))
126 | {
127 | var jsonLines = await File.ReadAllLinesAsync(Settings.Default.HistoryFile);
128 |
129 | Parallel.ForEach(jsonLines, item => {
130 | if (!string.IsNullOrWhiteSpace(item))
131 | {
132 | history.Add(JsonConvert.DeserializeObject(item));
133 | }
134 | });
135 | }
136 | }));
137 |
138 | await Task.WhenAll(tasksToWaitOn);
139 |
140 | return history.OrderBy(o => o.TimeRun).ToList();
141 | }
142 |
143 | ///
144 | /// Writes the history asynchronous to the configured history file
145 | ///
146 | /// The history.
147 | public static async Task WriteHistoryAsync(IEnumerable history)
148 | {
149 | string baseDir = Path.GetDirectoryName(Settings.Default.HistoryFile);
150 |
151 | if (!Directory.Exists(baseDir))
152 | {
153 | Directory.CreateDirectory(baseDir);
154 | }
155 |
156 | var json = history.ToList().ConvertAll(x => x.Serialize());
157 |
158 | await File.AppendAllLinesAsync(Settings.Default.HistoryFile, json);
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Settings.cs:
--------------------------------------------------------------------------------
1 | using CommandLine;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 |
8 | namespace Dotnet.Shell.Logic
9 | {
10 | ///
11 | /// The User Experience to use when console rendering
12 | ///
13 | public enum UserExperience
14 | {
15 | ///
16 | /// The classic mode - similar to Bash
17 | ///
18 | Classic,
19 | ///
20 | /// Enhanced mode with improved history
21 | ///
22 | Enhanced,
23 | ///
24 | /// The TMux enhanced version which uses Tmux popup functionality
25 | ///
26 | TmuxEnhanced
27 | }
28 |
29 | ///
30 | /// This defined all the global settings these can either be set on the command line or via a script
31 | ///
32 | public class Settings
33 | {
34 | ///
35 | /// The default settings used by dotnet-shell
36 | ///
37 | public static Settings Default = new();
38 |
39 | public void AddComplexDefaults()
40 | {
41 | if (!AdditionalHistoryFiles.Any())
42 | {
43 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
44 | {
45 | this.AdditionalHistoryFiles = new List() { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".bash_history") };
46 | }
47 | else
48 | {
49 | this.AdditionalHistoryFiles = new List() { Environment.ExpandEnvironmentVariables(@"%userprofile%\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt") };
50 | }
51 | }
52 | }
53 |
54 | ///
55 | /// Gets or sets a value indicating whether this is verbose.
56 | ///
57 | ///
58 | /// true if verbose; otherwise, false .
59 | ///
60 | [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.", Default = false)]
61 | public bool Verbose { get; set; }
62 |
63 | ///
64 | /// Gets or sets a value indicating whether [early debugger attach].
65 | ///
66 | ///
67 | /// true if [early debugger attach]; otherwise, false .
68 | ///
69 | [Option("earlyDebuggerAttach", Required = false, HelpText = "Enables early debugging for initialization related issues", Default = false)]
70 | public bool EarlyDebuggerAttach { get; set; }
71 |
72 | ///
73 | /// Gets or sets a value indicating whether [show pre processor output].
74 | ///
75 | ///
76 | /// true if [show pre processor output]; otherwise, false .
77 | ///
78 | [Option("showPreProcessorOutput", Required = false, HelpText = "Outputs preprocessed scripts and commands to StdOut prior to execution", Default = false)]
79 | public bool ShowPreProcessorOutput { get; set; }
80 |
81 | ///
82 | /// Gets or sets the token.
83 | ///
84 | ///
85 | /// The token.
86 | ///
87 | [Option("token", Required = false, HelpText = "Token shared between client and server instances", Hidden = true, SetName = "history")]
88 | public string Token { get; set; }
89 |
90 | ///
91 | /// Gets or sets the API port.
92 | ///
93 | ///
94 | /// The API port.
95 | ///
96 | [Option("apiport", Required = false, HelpText = "The port number of the API interface", Hidden = true, SetName = "history")]
97 | public int APIPort { get; set; }
98 |
99 | ///
100 | /// Gets or sets a value indicating whether [history mode].
101 | ///
102 | ///
103 | /// true if [history mode]; otherwise, false .
104 | ///
105 | [Option("history", Required = false, HelpText = "Starts the shell in history display mode", Hidden = true, Default = false, SetName = "history")]
106 | public bool HistoryMode { get; set; }
107 |
108 | ///
109 | /// Gets or sets the user experience mode.
110 | ///
111 | ///
112 | /// The ux.
113 | ///
114 | [Option('x', "ux", Required = false, HelpText = "The user experience mode the shell starts in", Default = UserExperience.Enhanced)]
115 | public UserExperience UX { get; set; }
116 |
117 | ///
118 | /// Gets the profile script path.
119 | ///
120 | ///
121 | /// The profile script path.
122 | ///
123 | [Option("profile", Required = false, HelpText = "The path to the personal initialization script file (core.nsh)")]
124 | public string ProfileScriptPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nsh", "core.nsh");
125 |
126 | ///
127 | /// Gets the sub shell.
128 | ///
129 | ///
130 | /// The sub shell.
131 | ///
132 | [Option('s', "subShell", Required = false, HelpText = "Path to the sub shell to invoke commands with")]
133 | public string SubShell { get; set; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "powershell.exe" : "/bin/bash";
134 |
135 | ///
136 | /// Gets the sub shell arguments format.
137 | ///
138 | ///
139 | /// The sub shell arguments format.
140 | ///
141 | [Option('a', "subShellArgs", Required = false, HelpText = "Arguments to the provided to the SubShell, this MUST include the format specifier {0}")]
142 | public string SubShellArgumentsFormat { get; set; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "-NoProfile -ExecutionPolicy unrestricted -Encoded {0}" : "-c \"{0}\"";
143 |
144 | ///
145 | /// Gets the additional usings.
146 | ///
147 | ///
148 | /// The additional usings.
149 | ///
150 | [Option('u', "using", Required = false, HelpText = "Additional 'using' statements to include")]
151 | public IEnumerable AdditionalUsings { get; set; } = new List();
152 |
153 | ///
154 | /// Gets the popup command.
155 | ///
156 | ///
157 | /// The popup command.
158 | ///
159 | [Option("popupCmd", Required = false, HelpText = "Command to run to raise a system popup window, must include {0} format specifier for the dotnet-shell command", Default = "tmux popup -KER '{0}' -x 60 -y 0 -w 60% -h 100%")]
160 | public string PopupCommand { get; set; }
161 |
162 | ///
163 | /// Gets the history popup command.
164 | ///
165 | ///
166 | /// The history popup command.
167 | ///
168 | [Option("historyCmd", Required = false, HelpText = "dotnet-shell command line to execute when the history subprocess. Must include {0} format specifier for DLL location, {1} for port and {2} for token parameters", Default = "dotnet {0} --history --apiport {1} --token {2}")]
169 | public string HistoryPopupCommand { get; set; }
170 |
171 | ///
172 | /// Adds one or more history file
173 | /// History files are interpreted as one command per line
174 | ///
175 | ///
176 | /// The history files.
177 | ///
178 | [Option("additionalHistory", Required = false, HelpText = "Path to additional OS specific history files")]
179 | public IEnumerable AdditionalHistoryFiles { get; set; } = new List();
180 |
181 | [Option("historyPath", Required = false, HelpText = "Path to CSX history file")]
182 | public string HistoryFile { get; set; } = Path.Combine(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Dotnet.Shell.API.Shell.DefaultScriptExtension), "history");
183 |
184 | [Option("nowizard", Required = false, HelpText = "Do not try and run the initial set up wizard", Default = false)]
185 | public bool DontRunWizard { get; set; }
186 |
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Suggestions/Autocompletion/CdCompletion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 |
7 | namespace Dotnet.Shell.Logic.Suggestions.Autocompletion
8 | {
9 | class CdCompletion
10 | {
11 | public static List GetCompletions(string sanitizedText, API.Shell shell, int cursorPos)
12 | {
13 | const string CD_MATCH = "cd";
14 | var cdSanitizedInput = sanitizedText.Remove(0, CD_MATCH.Length).TrimStart();
15 |
16 | bool isAbsolutePath = cdSanitizedInput.StartsWith(Path.DirectorySeparatorChar) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && cdSanitizedInput.Length >= 3 && cdSanitizedInput[1] == ':';
17 |
18 | if ((isAbsolutePath && Directory.Exists(cdSanitizedInput) && !cdSanitizedInput.EndsWith(Path.DirectorySeparatorChar)) ||
19 | (Directory.Exists(Path.Combine(shell.WorkingDirectory, cdSanitizedInput)) && !cdSanitizedInput.EndsWith(Path.DirectorySeparatorChar)))
20 | {
21 | // Directories must end with a slash
22 | return new List()
23 | {
24 | new Suggestion() { Index = cursorPos, CompletionText = Path.DirectorySeparatorChar.ToString(), FullText = cdSanitizedInput + Path.DirectorySeparatorChar }
25 | };
26 | }
27 |
28 | if (string.IsNullOrWhiteSpace(cdSanitizedInput) || (isAbsolutePath && Directory.Exists(cdSanitizedInput)))
29 | {
30 | return
31 | TryGetDirectories(isAbsolutePath ? cdSanitizedInput : shell.WorkingDirectory)
32 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = Path.GetFileName(x) + Path.DirectorySeparatorChar, FullText = Path.GetFileName(x) + Path.DirectorySeparatorChar })
33 | .ToList();
34 | }
35 | else if (isAbsolutePath) // absolute paths
36 | {
37 | return
38 | TryGetDirectories(Path.GetDirectoryName(cdSanitizedInput))
39 | .Where(x => x.StartsWith(cdSanitizedInput))
40 | .Select(x => x.Remove(0, cdSanitizedInput.Length))
41 | .Distinct()
42 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = x + Path.DirectorySeparatorChar, FullText = cdSanitizedInput + x + Path.DirectorySeparatorChar })
43 | .ToList();
44 | }
45 | else if (cdSanitizedInput.Contains(Path.DirectorySeparatorChar)) // sub directories based on CWD
46 | {
47 | var dirName = Path.GetFileName(cdSanitizedInput);
48 |
49 | return
50 | TryGetDirectories(Path.Combine(shell.WorkingDirectory, Path.GetDirectoryName(cdSanitizedInput)))
51 | .Select(x => Path.GetFileName(x))
52 | .Where(x => x.StartsWith(dirName))
53 | .Select(x => x.Remove(0, dirName.Length))
54 | .Distinct()
55 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = x + Path.DirectorySeparatorChar, FullText = cdSanitizedInput + x + Path.DirectorySeparatorChar })
56 | .ToList();
57 | }
58 | else // based from working dir
59 | {
60 | return TryGetDirectories(shell.WorkingDirectory)
61 | .Select(x => Path.GetFileName(x))
62 | .Where(x => x.StartsWith(cdSanitizedInput))
63 | .Select(x => x.Remove(0, cdSanitizedInput.Length))
64 | .Distinct()
65 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = x + Path.DirectorySeparatorChar, FullText = cdSanitizedInput + x + Path.DirectorySeparatorChar })
66 | .ToList();
67 | }
68 | }
69 |
70 | private static IEnumerable TryGetDirectories(string location)
71 | {
72 | try
73 | {
74 | return Directory.GetDirectories(location);
75 | }
76 | catch
77 | {
78 | return Array.Empty();
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Suggestions/Autocompletion/ExecutableCompletions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace Dotnet.Shell.Logic.Suggestions.Autocompletion
9 | {
10 | class ExecutableCompletions
11 | {
12 | public static List GetCompletions(string sanitizedText, API.Shell shell, int cursorPos)
13 | {
14 | var INVOKE_IN_DIR = "." + Path.DirectorySeparatorChar;
15 |
16 | var executableSanitizedInput = sanitizedText.Remove(0, INVOKE_IN_DIR.Length);
17 |
18 | var basePath = shell.WorkingDirectory;
19 | var startOfFilename = executableSanitizedInput;
20 |
21 | var lastDir = executableSanitizedInput.LastIndexOf(Path.DirectorySeparatorChar);
22 | if (lastDir != -1)
23 | {
24 | basePath = Path.Combine(basePath, executableSanitizedInput.Substring(0, lastDir));
25 | startOfFilename = executableSanitizedInput.Remove(0, lastDir + 1);
26 | }
27 |
28 | return GetFilesAndFolders(basePath)
29 | .Where(x => x.StartsWith(startOfFilename))
30 | // TODO we should check if the executable bit is set here
31 | .Distinct()
32 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = x.Remove(0, startOfFilename.Length), FullText = x })
33 | .ToList();
34 | }
35 |
36 | private static IEnumerable GetFilesAndFolders(string basePath)
37 | {
38 | List ret = new();
39 |
40 | try
41 | {
42 | ret.AddRange(Directory.GetFiles(basePath).Select(x => Path.GetFileName(x)));
43 | }
44 | catch
45 | {
46 |
47 | }
48 |
49 | try
50 | {
51 | ret.AddRange(Directory.GetDirectories(basePath).Select(x => Path.GetFileName(x) + Path.DirectorySeparatorChar));
52 | }
53 | catch
54 | {
55 |
56 | }
57 |
58 | return ret;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Suggestions/Autocompletion/FileAndDirectoryCompletion.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Runtime.CompilerServices;
5 | using System.Runtime.InteropServices;
6 | using System.Threading.Tasks;
7 |
8 | [assembly: InternalsVisibleTo("UnitTests")]
9 |
10 | namespace Dotnet.Shell.Logic.Suggestions.Autocompletion
11 | {
12 | class FileAndDirectoryCompletion
13 | {
14 | public static async Task> GetCompletionsAsync(string sanitizedText, API.Shell shell, int cursorPos, Task commandsInPath)
15 | {
16 | // if our cursor position is before a space then we are in command completion mode
17 | // otherwise we will complete with a filename
18 |
19 | // need to decide if we are going to look for a command, or a file
20 | var spacePos = sanitizedText.IndexOf(' ');
21 | bool suggestCommand = spacePos == -1 || spacePos >= cursorPos;
22 |
23 | if (suggestCommand)
24 | {
25 | #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
26 | var matchedEndings = (await commandsInPath).Where(x => x.StartsWith(sanitizedText)).Select(x => x.Remove(0, sanitizedText.Length)).Distinct().ToList();
27 | #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
28 |
29 | return matchedEndings
30 | .ConvertAll(x => new Suggestion() { CompletionText = x, Index = cursorPos, FullText = sanitizedText + x })
31 | .Union(GetMatchingFilesOrDirectories(sanitizedText, shell, cursorPos))
32 | .Distinct()
33 | .ToList();
34 | }
35 | else
36 | {
37 | return GetMatchingFilesOrDirectories(sanitizedText, shell, cursorPos);
38 | }
39 | }
40 |
41 | private static List GetMatchingFilesOrDirectories(string sanitizedText, API.Shell shell, int cursorPos)
42 | {
43 | // 'command arg1 arg2 /home/asdad/d'
44 |
45 | var fsStart = sanitizedText.LastIndexOf(' ', sanitizedText.Length - 1); // todo change to regex and match multiple chars?
46 | var startOfDirOrFile = sanitizedText.Remove(0, fsStart == -1 ? 0 : fsStart + 1); // +1 for the space
47 |
48 | var fullPath = ConvertToAbsolute(startOfDirOrFile, shell);
49 |
50 | var directoryName = Path.GetDirectoryName(fullPath);
51 | if (directoryName == null)
52 | {
53 | directoryName = Dotnet.Shell.API.Shell.BasePath;
54 | }
55 |
56 | var toMatch = Path.GetFileName(fullPath);
57 |
58 | // /ho
59 | // ./home
60 | // ../
61 | // bob/asdads
62 |
63 | // suggest a file or directory
64 | List items = new();
65 | try
66 | {
67 | items.AddRange(Directory.GetFiles(directoryName).Select(x => Path.GetFileName(x)));
68 | }
69 | catch
70 | {
71 |
72 | }
73 |
74 | try
75 | {
76 | items.AddRange(Directory.GetDirectories(directoryName).Select(x => Path.GetFileName(x) + Path.DirectorySeparatorChar));
77 | }
78 | catch
79 | {
80 |
81 | }
82 |
83 | return items
84 | .Where(x => string.IsNullOrWhiteSpace(toMatch) || x.StartsWith(toMatch))
85 | .Select(x => x.Remove(0, toMatch.Length))
86 | .Distinct()
87 | .Select(x => new Suggestion() { Index = cursorPos, CompletionText = x, FullText = toMatch + x }).ToList();
88 | }
89 |
90 | internal static string ConvertToAbsolute(string dir, API.Shell shell)
91 | {
92 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
93 | {
94 | dir = dir.Replace("~", shell.HomeDirectory);
95 |
96 | if (!dir.StartsWith(Path.DirectorySeparatorChar))
97 | {
98 | dir = Path.Combine(shell.WorkingDirectory, dir);
99 | }
100 |
101 | return Path.GetFullPath(dir);
102 | }
103 | else
104 | {
105 | return Path.GetFullPath(dir.Replace("~", shell.HomeDirectory), shell.WorkingDirectory);
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Suggestions/CSharpSuggestions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.CompilerServices;
6 | using System.Text.RegularExpressions;
7 | using System.Threading.Tasks;
8 | using Microsoft.CodeAnalysis;
9 | using Microsoft.CodeAnalysis.Completion;
10 | using Microsoft.CodeAnalysis.CSharp;
11 |
12 | [assembly: InternalsVisibleTo("UnitTests")]
13 |
14 | namespace Dotnet.Shell.Logic.Suggestions
15 | {
16 | internal class CSharpSuggestions
17 | {
18 | private readonly Task commandsInPath;
19 | private readonly IEnumerable assemblies;
20 | private readonly Project project;
21 | private readonly Regex PowershellOrBashCommandRegex = new(@"^[\w-]+$", RegexOptions.Compiled);
22 |
23 | internal CSharpSuggestions() : this(Task.FromResult(Array.Empty()))
24 | {
25 |
26 | }
27 |
28 | public CSharpSuggestions(Task commandsInPath, IEnumerable assemblies = null)
29 | {
30 | this.commandsInPath = commandsInPath;
31 | if (assemblies == null)
32 | {
33 | assemblies = GetAllLoadedAssemblies();
34 | }
35 | this.assemblies = assemblies;
36 |
37 | var usings = new List()
38 | {
39 | "System",
40 | "System.Collections",
41 | "System.Collections.Generic",
42 | "System.Linq",
43 | "System.Drawing",
44 | "System.IO",
45 | "Dotnet.Shell.UI"
46 | };
47 | usings.AddRange(Settings.Default.AdditionalUsings);
48 |
49 | var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
50 | .WithOverflowChecks(true).WithOptimizationLevel(OptimizationLevel.Release)
51 | .WithUsings(usings);
52 |
53 | var workspace = new AdhocWorkspace(); // dispose?
54 | string projName = "NewProject132";
55 | var projectId = ProjectId.CreateNewId();
56 | var projectInfo = ProjectInfo.Create(
57 | projectId,
58 | VersionStamp.Create(),
59 | projName,
60 | projName,
61 | LanguageNames.CSharp,
62 | isSubmission: true,
63 | compilationOptions: options,
64 | metadataReferences: this.assemblies,
65 | parseOptions: new CSharpParseOptions(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest));
66 | project = workspace.AddProject(projectInfo);
67 | }
68 |
69 | ///
70 | /// todo get this from executor
71 | ///
72 | ///
73 | private static List GetAllLoadedAssemblies()
74 | {
75 | var refs = AppDomain.CurrentDomain.GetAssemblies();
76 | var references = new List();
77 |
78 | foreach (var reference in refs.Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)))
79 | {
80 | var stream = new FileStream(reference.Location, FileMode.Open, FileAccess.Read);
81 | references.Add(MetadataReference.CreateFromStream(stream));
82 | }
83 | return references;
84 | }
85 |
86 | public async Task> GetSuggestionsAsync(string userText, int cursorPos)
87 | {
88 | if (cursorPos < 0 || cursorPos > userText.Length)
89 | {
90 | return null;
91 | }
92 |
93 | var sanitizedText = userText.Substring(0, cursorPos);
94 |
95 | // try and remove some cases where commands are incorrectly interpretted as C#
96 | // IP Addresses for commands like SSH are a good example here
97 | var possibleCommand = sanitizedText.Split(' ').FirstOrDefault();
98 | if (!string.IsNullOrWhiteSpace(possibleCommand) && PowershellOrBashCommandRegex.IsMatch(possibleCommand))
99 | {
100 | #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
101 | var cmds = await commandsInPath;
102 | #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
103 | if (cmds.Contains(possibleCommand))
104 | {
105 | return null;
106 | }
107 | }
108 |
109 | var id = DocumentId.CreateNewId(project.Id);
110 |
111 | var solution = project.Solution.AddDocument(id, project.Name, sanitizedText);
112 | var document = solution.GetDocument(id);
113 |
114 | return await GetCompletionResultsAsync(document, sanitizedText, sanitizedText.Length);
115 | }
116 |
117 | private static async Task> GetCompletionResultsAsync(Document document, string sanitizedText, int position)
118 | {
119 | var ret = new List();
120 |
121 | var completionService = CompletionService.GetService(document);
122 | var results = await completionService.GetCompletionsAsync(document, position);
123 |
124 | if (results == null)
125 | {
126 | return new List();
127 | }
128 |
129 | foreach (var i in results.ItemsList)
130 | {
131 | if (i.Properties.ContainsKey("SymbolKind") && i.Properties["SymbolKind"] == "9" &&
132 | i.Properties.ContainsKey("InsertionText"))
133 | {
134 | var text = i.Properties["InsertionText"];
135 |
136 | var fullText = sanitizedText.Substring(0, i.Span.Start) + text;
137 |
138 | if (fullText.StartsWith(sanitizedText))
139 | {
140 | ret.Add(new Suggestion() { Index = i.Span.Start, CompletionText = text, FullText = text });
141 | }
142 | }
143 | }
144 |
145 | return ret;
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Shell/Logic/Suggestions/CmdSuggestions.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Suggestions.Autocompletion;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.CompilerServices;
7 | using System.Runtime.InteropServices;
8 | using System.Threading.Tasks;
9 |
10 | [assembly: InternalsVisibleTo("UnitTests")]
11 |
12 | namespace Dotnet.Shell.Logic.Suggestions
13 | {
14 | internal class CmdSuggestions
15 | {
16 | private readonly Task commandsInPath;
17 | private readonly API.Shell shell;
18 |
19 | internal CmdSuggestions(API.Shell shell)
20 | {
21 | this.shell = shell;
22 | this.commandsInPath = Task.FromResult(Array.Empty());
23 | }
24 |
25 | public CmdSuggestions(API.Shell shell, Task commandsInPath)
26 | {
27 | this.shell = shell;
28 | this.commandsInPath = commandsInPath;
29 | }
30 |
31 | public async Task> GetSuggestionsAsync(string userText, int cursorPos)
32 | {
33 | if (cursorPos < 0 || cursorPos > userText.Length)
34 | {
35 | return new List();
36 | }
37 |
38 | var sanitizedText = userText.Substring(0, cursorPos);
39 |
40 | // first, remove anything that might be part of another command
41 | // look backward for the follow characters and forget everything before them
42 | // && ;
43 | sanitizedText = RemoveTextBeforeAndIncluding(sanitizedText, new string[] { "&&", ";" }).Replace("~", shell.HomeDirectory).Trim();
44 |
45 | // c -> cd [command]
46 | // ech -> echo [command]
47 | // cat b -> cat bob [file]
48 | // cat bob; ec[TAB] -> cat bob; echo [file]
49 |
50 | if (sanitizedText.StartsWith("cd"))
51 | {
52 | return CdCompletion.GetCompletions(sanitizedText, shell, cursorPos);
53 | }
54 | else if (sanitizedText.StartsWith("." + Path.DirectorySeparatorChar))
55 | {
56 | return ExecutableCompletions.GetCompletions(sanitizedText, shell, cursorPos);
57 | }
58 | else
59 | {
60 | #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
61 | return await FileAndDirectoryCompletion.GetCompletionsAsync(sanitizedText, shell, cursorPos, commandsInPath);
62 | #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
63 | }
64 |
65 | // user defined?
66 | }
67 |
68 | private static string RemoveTextBeforeAndIncluding(string userText, string[] markers)
69 | {
70 | var ret = userText;
71 |
72 | foreach (var marker in markers)
73 | {
74 | var index = ret.LastIndexOf(marker);
75 | if (index != -1)
76 | {
77 | ret = ret.Remove(0, index +1);
78 | }
79 | }
80 |
81 | return ret;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Shell/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/src/Shell/UI/ColorString.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.RegularExpressions;
4 |
5 | [assembly: InternalsVisibleTo("UnitTests")]
6 |
7 | namespace Dotnet.Shell.UI
8 | {
9 | ///
10 | /// Implements a color string replacemnt class using ANSI formatting escape codes
11 | ///
12 | public class ColorString
13 | {
14 | private const string Reset = "\u001b[0m";
15 |
16 | private const string RGBForegroundFormat = "\u001b[38;2;{0};{1};{2}m";
17 | private const string RGBBackgroundFormat = "\u001b[48;2;{0};{1};{2}m";
18 |
19 | private static readonly Regex RemoveEscapeCharsRegex = new(@"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]", RegexOptions.Compiled);
20 |
21 | ///
22 | /// Gets the string representation but without formatting characters
23 | ///
24 | public string Text { get; private set; }
25 |
26 | ///
27 | /// Gets the string representation but WITH formatting characters
28 | ///
29 | public string TextWithFormattingCharacters { get; private set; }
30 |
31 | ///
32 | /// Convert to a color string from an ANSI string with escape sequences
33 | ///
34 | ///
35 | ///
36 | public static ColorString FromRawANSI(string ansi)
37 | {
38 | return new ColorString(RemoveEscapeCharsRegex.Replace(ansi, string.Empty), ansi);
39 | }
40 |
41 | ///
42 | /// Initializes a new instance of the class.
43 | ///
44 | /// The s.
45 | /// The ANSI.
46 | internal ColorString(string s, string ansi = null)
47 | {
48 | this.Text = s;
49 | if (ansi != null)
50 | {
51 | this.TextWithFormattingCharacters = ansi;
52 | }
53 | else
54 | {
55 | this.TextWithFormattingCharacters = s;
56 | }
57 | }
58 |
59 | ///
60 | /// Initializes a new instance of the class.
61 | ///
62 | /// The s.
63 | /// The c.
64 | public ColorString(string s, Color c) : this(s)
65 | {
66 | // Modern terminals support RGB so we use that to get a full range of colors
67 | TextWithFormattingCharacters = string.Format(RGBForegroundFormat, c.R, c.G, c.B) + s + Reset;
68 | }
69 |
70 | ///
71 | /// Initializes a new instance of the class.
72 | ///
73 | /// The s.
74 | /// The c.
75 | /// The d.
76 | public ColorString(string s, Color c, Color d) : this(s, c)
77 | {
78 | // the base construct will already set the text and add reset so we only have to prepend the background colour
79 | TextWithFormattingCharacters = string.Format(RGBBackgroundFormat, d.R, d.G, d.B) + TextWithFormattingCharacters;
80 | }
81 |
82 | ///
83 | /// Gets the length.
84 | ///
85 | ///
86 | /// The length.
87 | ///
88 | public int Length => Text.Length;
89 |
90 | ///
91 | /// Performs an implicit conversion from to .
92 | ///
93 | /// The t.
94 | ///
95 | /// The result of the conversion.
96 | ///
97 | public static implicit operator ColorString(string t)
98 | {
99 | if (t == null)
100 | {
101 | return null;
102 | }
103 |
104 | return new ColorString(t);
105 | }
106 |
107 | ///
108 | /// Converts to string.
109 | ///
110 | ///
111 | /// A that represents this instance.
112 | ///
113 | public override string ToString()
114 | {
115 | return Text;
116 | }
117 |
118 | ///
119 | /// Determines whether the specified , is equal to this instance.
120 | ///
121 | /// The to compare with this instance.
122 | ///
123 | /// true if the specified is equal to this instance; otherwise, false .
124 | ///
125 | public override bool Equals(object obj)
126 | {
127 | return Text.Equals(obj);
128 | }
129 |
130 | ///
131 | /// Returns a hash code for this instance.
132 | ///
133 | ///
134 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
135 | ///
136 | public override int GetHashCode()
137 | {
138 | return Text.GetHashCode();
139 | }
140 |
141 | ///
142 | /// Gets the with the specified i.
143 | ///
144 | ///
145 | /// The .
146 | ///
147 | /// The i.
148 | ///
149 | public char this[int i]
150 | {
151 | get { return Text[i]; }
152 | }
153 |
154 | ///
155 | /// Implements the operator +.
156 | ///
157 | /// a.
158 | /// The b.
159 | ///
160 | /// The result of the operator.
161 | ///
162 | public static ColorString operator +(ColorString a, ColorString b)
163 | {
164 | var text = a.Text + b.Text;
165 | var newFormattedString = a.TextWithFormattingCharacters + b.TextWithFormattingCharacters;
166 |
167 | return new ColorString(text, newFormattedString);
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/Shell/UI/Enhanced/HistoryBox.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using ConsoleGUI;
6 | using ConsoleGUI.Common;
7 | using ConsoleGUI.Controls;
8 | using ConsoleGUI.Data;
9 | using ConsoleGUI.Input;
10 | using Dotnet.Shell.API;
11 | using Dotnet.Shell.Logic;
12 | using Dotnet.Shell.Logic.Console;
13 | using Dotnet.Shell.Logic.Execution;
14 |
15 | namespace Dotnet.Shell.UI.Enhanced
16 | {
17 | public class HistoryBox : IInputListener
18 | {
19 | private bool quit = false;
20 | private bool updateSearch = false;
21 | private bool clearOutput = false;
22 | private readonly IConsole console = null;
23 | private TextBox searchBox = null;
24 | private ListView listView = null;
25 |
26 | public static Func> OnSearchHistory(IConsole console)
27 | {
28 | var search = new HistoryBox(console);
29 | return search.OnSearchHistoryAltModeAsync;
30 | }
31 |
32 | public static Func> OnSearchHistoryTmux()
33 | {
34 | return OnSearchHistoryTmuxModeAsync;
35 | }
36 |
37 | private async Task OnSearchHistoryAltModeAsync(ConsoleImproved prompt, ConsoleKeyEx key)
38 | {
39 | await console.SaveAsync();
40 |
41 | var command = await this.RunInterfaceAsync(prompt.Shell.History);
42 |
43 | await console.RestoreAsync();
44 |
45 | if (!string.IsNullOrWhiteSpace(command))
46 | {
47 | prompt.DisplayPrompt(command, false);
48 | }
49 | else
50 | {
51 | prompt.ClearUserEntry();
52 | prompt.DisplayPrompt(prompt.UserEnteredText, false);
53 | }
54 |
55 | return false;
56 | }
57 |
58 | private static async Task OnSearchHistoryTmuxModeAsync(ConsoleImproved prompt, ConsoleKeyEx key)
59 | {
60 | ProcessEx tmuxPopup = null;
61 |
62 | var result = HistoryAPI.ListenForSearchResultAsync((port, token) => {
63 | var cssCommand = string.Format(Settings.Default.HistoryPopupCommand, API.Shell.AssemblyLocation, port, token);
64 | var tmuxCommand = string.Format(Settings.Default.PopupCommand, cssCommand);
65 |
66 | // start tmux prompt
67 | tmuxPopup = OS.Exec(tmuxCommand);
68 | });
69 |
70 | await tmuxPopup.WaitForExitAsync();
71 | tmuxPopup.Dispose();
72 |
73 | await Task.WhenAny(result, Task.Delay(1000));
74 |
75 | if (result.IsCompletedSuccessfully)
76 | {
77 | if (!string.IsNullOrWhiteSpace(await result))
78 | {
79 | prompt.DisplayPrompt(await result, false);
80 | }
81 | else
82 | {
83 | prompt.ClearUserEntry();
84 | prompt.DisplayPrompt(prompt.UserEnteredText, false);
85 | }
86 | }
87 | else
88 | {
89 | prompt.DisplayPrompt("Error", false);
90 | }
91 |
92 | return false;
93 | }
94 |
95 | public HistoryBox(IConsole console)
96 | {
97 | this.console = console;
98 | }
99 |
100 | private Control Build(List history)
101 | {
102 | searchBox = new TextBox();
103 | listView = new ListView
104 | {
105 | Items = history
106 | };
107 |
108 | return new Background()
109 | {
110 | Color = new Color(0, 135, 175),
111 | Content = new DockPanel()
112 | {
113 | FillingControl = listView,
114 | DockedControl = new Boundary()
115 | {
116 | MaxHeight = 2,
117 | Content = new Background()
118 | {
119 | Color = Color.Black,
120 | Content = new VerticalStackPanel()
121 | {
122 | Children = new IControl[]
123 | {
124 | new HorizontalSeparator(),
125 | new HorizontalStackPanel()
126 | {
127 | Children = new IControl[]
128 | {
129 | new TextBlock()
130 | {
131 | Text = "Search) ",
132 | },
133 | searchBox
134 | }
135 | }
136 | }
137 | }
138 | }
139 | },
140 | Placement = DockPanel.DockedControlPlacement.Bottom
141 | }
142 | };
143 | }
144 |
145 | public async Task RunInterfaceAsync(List history)
146 | {
147 | var historyToDisplay = history.Select(x => x.CmdLine).Distinct().ToList();
148 | var control = Build(historyToDisplay);
149 |
150 | ConsoleManager.Setup();
151 | ConsoleManager.Content = control;
152 |
153 | ConsoleManager.Resize(new ConsoleGUI.Space.Size(Console.WindowWidth, Console.WindowHeight));
154 | ConsoleManager.AdjustWindowSize();
155 |
156 | var inputListener = new IInputListener[]
157 | {
158 | this,
159 | listView,
160 | searchBox
161 | };
162 | quit = false;
163 |
164 | while (!quit)
165 | {
166 | ConsoleManager.AdjustBufferSize();
167 | ConsoleManager.ReadInput(inputListener);
168 |
169 | if (updateSearch)
170 | {
171 | updateSearch = false;
172 | var searchResults = historyToDisplay.Where(x => x.ToLowerInvariant().Contains(searchBox.Text.ToLowerInvariant())).ToList();
173 | listView.Items = searchResults;
174 | }
175 |
176 | await Task.Delay(50);
177 | }
178 |
179 | return clearOutput ? string.Empty: listView.SelectedItem;
180 | }
181 |
182 | public void OnInput(InputEvent inputEvent)
183 | {
184 | if (inputEvent.Key.Key == ConsoleKey.Escape)
185 | {
186 | clearOutput = true;
187 | quit = true;
188 | }
189 | if (inputEvent.Key.Key == ConsoleKey.Enter)
190 | {
191 | quit = true;
192 | }
193 | else if (inputEvent.Key.Key != ConsoleKey.UpArrow && inputEvent.Key.Key != ConsoleKey.DownArrow)
194 | {
195 | updateSearch = true;
196 | }
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/Shell/UI/Enhanced/ListView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using ConsoleGUI.Data;
5 | using ConsoleGUI.Input;
6 | using ConsoleGUI.UserDefined;
7 |
8 | namespace ConsoleGUI.Controls
9 | {
10 | internal class ListView : SimpleControl, IInputListener
11 | {
12 | private readonly Color _textColor = Color.White;
13 | public Color TextColor
14 | {
15 | get => _textColor;
16 | }
17 |
18 | private readonly Color _selectedTextColor = Color.Black;
19 | public Color SelectedTextColor
20 | {
21 | get => _selectedTextColor;
22 | }
23 |
24 | private int? _selectedIndex = null;
25 | public int? SelectedIndex
26 | {
27 | get => _selectedIndex;
28 | }
29 |
30 | public ConsoleKey ScrollUpKey => _vscrollp.ScrollUpKey;
31 | public ConsoleKey ScrollDownKey => _vscrollp.ScrollDownKey;
32 |
33 | private readonly VerticalScrollPanel _vscrollp;
34 | private readonly VerticalStackPanel _vstackp;
35 |
36 | public ListView()
37 | {
38 | Content = new VerticalScrollPanel()
39 | {
40 | Content = new VerticalStackPanel()
41 | };
42 | _vscrollp = Content as VerticalScrollPanel;
43 | _vstackp = _vscrollp.Content as VerticalStackPanel;
44 | }
45 |
46 | public string SelectedItem
47 | {
48 | get
49 | {
50 | if (_selectedIndex != null)
51 | {
52 | var selectedItem = _vstackp.Children.ElementAt(_selectedIndex.Value);
53 | var current = selectedItem as TextBlock;
54 | return current.Text;
55 | }
56 | else
57 | {
58 | return null;
59 | }
60 | }
61 | }
62 |
63 | public IEnumerable Items
64 | {
65 | set
66 | {
67 | if (value == null)
68 | {
69 | _vstackp.Children = new TextBlock[] { };
70 | _selectedIndex = null;
71 | }
72 | else
73 | {
74 | _selectedIndex = 0;
75 |
76 | var items = new List();
77 | for (int curIndex = 0; curIndex < value.Count(); curIndex++)
78 | {
79 | items.Add(new TextBlock()
80 | {
81 | Text = value.ElementAt(curIndex),
82 | Color = curIndex == _selectedIndex ? SelectedTextColor : TextColor
83 | });
84 | }
85 |
86 | _vstackp.Children = items;
87 | }
88 | }
89 | get
90 | {
91 | return _vstackp.Children.Select(x => x as TextBlock).Select(x => x.Text);
92 | }
93 | }
94 |
95 | void IInputListener.OnInput(InputEvent inputEvent)
96 | {
97 | if (inputEvent.Key.Key == ScrollUpKey)
98 | {
99 | if (_selectedIndex > 0)
100 | {
101 | UpdateColor(_selectedIndex, _selectedIndex - 1);
102 | _selectedIndex--;
103 | }
104 | }
105 | else if (inputEvent.Key.Key == ScrollDownKey)
106 | {
107 | if (_selectedIndex < _vstackp.Children.Count() - 1)
108 | {
109 | UpdateColor(_selectedIndex, _selectedIndex + 1);
110 | _selectedIndex++;
111 | }
112 | }
113 |
114 | // Page what is displayed based on the selected item
115 | var totalNumberOfItemsPossibleOnScreen = _vscrollp.Size.Height;
116 | if ((inputEvent.Key.Key == ScrollDownKey && _selectedIndex % totalNumberOfItemsPossibleOnScreen == 0) || (inputEvent.Key.Key == ScrollUpKey && _selectedIndex < _vscrollp.Top))
117 | {
118 | inputEvent.Handled = true;
119 | _vscrollp.Top = inputEvent.Key.Key == ScrollDownKey ?
120 | _vscrollp.Top + totalNumberOfItemsPossibleOnScreen :
121 | _vscrollp.Top - totalNumberOfItemsPossibleOnScreen;
122 |
123 | }
124 | }
125 |
126 | private void UpdateColor(int? oldRow, int? newRow)
127 | {
128 | if (oldRow != null)
129 | {
130 | var oldElement = _vstackp.Children.ElementAt(oldRow.Value);
131 | var textBlockOld = oldElement as TextBlock;
132 | textBlockOld.Color = TextColor;
133 | }
134 |
135 | if (newRow != null)
136 | {
137 | var newElement = _vstackp.Children.ElementAt(newRow.Value);
138 | var textBlockNew = newElement as TextBlock;
139 | textBlockNew.Color = SelectedTextColor;
140 | }
141 | }
142 | }
143 | }
--------------------------------------------------------------------------------
/src/Shell/UI/ErrorDisplay.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using Dotnet.Shell.Logic.Console;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Logging.Console;
6 | using Microsoft.Extensions.Options;
7 |
8 | namespace Dotnet.Shell.UI
9 | {
10 | ///
11 | /// This class handles displaying of errors to the screen
12 | ///
13 | ///
14 | public class ErrorDisplay : IOptionsMonitor
15 | {
16 | private readonly IConsole console;
17 | private readonly ConsoleLoggerOptions _consoleLoggerOptions;
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | /// The console.
23 | public ErrorDisplay(IConsole console)
24 | {
25 | this.console = console;
26 |
27 | _consoleLoggerOptions = new ConsoleLoggerOptions()
28 | {
29 | LogToStandardErrorThreshold = LogLevel.Trace
30 | };
31 | }
32 |
33 | ///
34 | /// Pretty prints the exception.
35 | ///
36 | /// The ex.
37 | /// The input.
38 | /// The original input.
39 | public void PrettyException(Exception ex, string input = default, string originalInput = default)
40 | {
41 | try
42 | {
43 | if (input != null && originalInput != null && ParseExceptionForErrorPosition(ex, out int line, out int charPos))
44 | {
45 | var badLine = input.Split(Environment.NewLine)[line];
46 | var beforeBadPos = badLine.Substring(0, charPos);
47 | var afterBadPos = badLine.Remove(0, charPos);
48 | var displayStr = beforeBadPos + new ColorString(afterBadPos, System.Drawing.Color.Red);
49 |
50 | Console.WriteLine(ex.Message);
51 | PrettyError(displayStr);
52 |
53 | try
54 | {
55 | var originalLine = originalInput.Split(Environment.NewLine)[line];
56 | console.WriteLine("Original input line: " + originalLine);
57 | }
58 | catch (Exception)
59 | {
60 | console.WriteLine("Original input line: " + input);
61 | }
62 | }
63 | else
64 | {
65 | PrettyError(ex.Message);
66 | }
67 | }
68 | catch (Exception)
69 | {
70 | console.WriteLine(new ColorString(ex.Message, System.Drawing.Color.Red).TextWithFormattingCharacters);
71 |
72 | if (!string.IsNullOrWhiteSpace(ex.StackTrace))
73 | {
74 | foreach (var line in ex.StackTrace.Split(Environment.NewLine))
75 | {
76 | var trimmedLine = line.TrimStart();
77 | if (!trimmedLine.StartsWith("at Microsoft.CodeAnalysis.Scripting.") &&
78 | !trimmedLine.StartsWith("at Dotnet.Shell.Logic.Compilation."))
79 | {
80 | console.WriteLine(new ColorString(line, System.Drawing.Color.Yellow).TextWithFormattingCharacters);
81 | }
82 | }
83 | }
84 |
85 | Debugger.Break();
86 | }
87 | }
88 |
89 | ///
90 | /// Prints the color string as an error
91 | ///
92 | /// The MSG.
93 | public void PrettyError(ColorString msg)
94 | {
95 | console.WriteLine(msg.TextWithFormattingCharacters);
96 | }
97 |
98 | ///
99 | /// Prints the string as an error
100 | ///
101 | /// The MSG.
102 | public void PrettyError(string msg)
103 | {
104 | console.WriteLine(new ColorString(msg, System.Drawing.Color.Red).TextWithFormattingCharacters);
105 | }
106 |
107 | private static bool ParseExceptionForErrorPosition(Exception ex, out int line, out int charPos)
108 | {
109 | line = -1;
110 | charPos = -1;
111 | try
112 | {
113 | if (ex.Message.StartsWith("(") && ex.Message.Contains(")"))
114 | {
115 | var lineAndChar = ex.Message[1..ex.Message.IndexOf(")")];
116 | var lineAndCharSplit = lineAndChar.Split(",");
117 | line = int.Parse(lineAndCharSplit[0]) - 1;
118 | charPos = int.Parse(lineAndCharSplit[1]);
119 |
120 | return true;
121 | }
122 | return false;
123 | }
124 | catch
125 | {
126 | return false;
127 | }
128 | }
129 |
130 | ///
131 | /// Returns the current instance with the .
132 | ///
133 | public ConsoleLoggerOptions CurrentValue => _consoleLoggerOptions;
134 |
135 | ///
136 | /// Returns a configured instance with the given name.
137 | ///
138 | ///
139 | ///
140 | public ConsoleLoggerOptions Get(string name) => _consoleLoggerOptions;
141 |
142 | ///
143 | /// Registers a listener to be called whenever a named changes.
144 | ///
145 | /// The action to be invoked when has changed.
146 | ///
147 | /// An which should be disposed to stop listening for changes.
148 | ///
149 | public IDisposable OnChange(Action listener)
150 | {
151 | return null;
152 | }
153 |
154 | ///
155 | /// Prints the string at information level
156 | ///
157 | /// The line.
158 | public static void PrettyInfo(string line)
159 | {
160 | Console.WriteLine(line);
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Shell/UI/Standard/HistorySearch.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Console;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Drawing;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace Dotnet.Shell.UI.Standard
10 | {
11 | internal struct SearchHistory
12 | {
13 | public string Term;
14 | public List SearchResults;
15 | public int SelectedItem;
16 | }
17 |
18 | public class HistorySearch
19 | {
20 | private readonly IConsole implementation;
21 | private readonly Dotnet.Shell.API.Shell shell;
22 | private ConsoleImproved ci = null;
23 |
24 | public static Func> OnSearchHistory(IConsole console, Dotnet.Shell.API.Shell shell)
25 | {
26 | var search = new HistorySearch(console, shell);
27 | return search.OnSearchHistoryAsync;
28 | }
29 |
30 | public HistorySearch(IConsole console, Dotnet.Shell.API.Shell shell)
31 | {
32 | this.implementation = console;
33 | this.shell = shell;
34 | }
35 |
36 | private async Task OnSearchHistoryAsync(ConsoleImproved prompt, ConsoleKeyEx key)
37 | {
38 | prompt.ClearUserEntry();
39 |
40 | int oldPos = implementation.CursorTop;
41 |
42 | implementation.CursorLeft = 0;
43 | implementation.CursorTop = implementation.WindowHeight - 2;
44 |
45 | // create fake shell object with a custom prompt
46 | var fakeShell = new Dotnet.Shell.API.Shell();
47 | fakeShell.History.AddRange(shell.History);
48 | fakeShell.Prompt = () =>
49 | {
50 | RenderSearchChanges();
51 |
52 | // Search) user search text
53 | // [1/3]: matched entry
54 | return "Search) ";
55 | };
56 |
57 | ci = new ConsoleImproved(implementation, fakeShell);
58 |
59 | ci.KeyOverrides.Where(x => x.Key.Key == ConsoleKey.UpArrow).ToList().ForEach(x => ci.KeyOverrides.Remove(x));
60 | ci.KeyOverrides.Where(x => x.Key.Key == ConsoleKey.DownArrow).ToList().ForEach(x => ci.KeyOverrides.Remove(x));
61 |
62 | ci.AddKeyOverride(ConsoleKeyEx.Any, OnSearchTextEnteredAsync);
63 | ci.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.UpArrow), OnChangeSearchEntryAsync);
64 | ci.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.DownArrow), OnChangeSearchEntryAsync);
65 | ci.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.Enter), OnSelectSearchEntryAsync);
66 |
67 | ci.DisplayPrompt();
68 | ci.ClearUserEntry();
69 |
70 | // When the prompt returns, instead of executing the command we just set that
71 | // as what to show on screen
72 | var command = await ci.GetCommandAsync();
73 |
74 | implementation.CursorTop = implementation.WindowHeight - 2;
75 | implementation.Write(new string(' ', implementation.WindowWidth));
76 | implementation.CursorTop = implementation.WindowHeight - 1;
77 | implementation.Write(new string(' ', implementation.WindowWidth));
78 |
79 | implementation.CursorTop = oldPos;
80 | implementation.Write(new string(' ', implementation.WindowWidth));
81 |
82 | prompt.ClearUserEntry();
83 | prompt.DisplayPrompt(command, false);
84 |
85 | return false;
86 | }
87 |
88 | private void RenderSearchChanges(SearchHistory? searchHistory = null)
89 | {
90 | var totalItems = !searchHistory.HasValue ? shell.History.Count : searchHistory.Value.SearchResults.Count;
91 | var currentItem = !searchHistory.HasValue ? 0 : searchHistory.Value.SelectedItem + 1;
92 | var match = !searchHistory.HasValue || searchHistory.Value.SearchResults.Count == 0 ? string.Empty : searchHistory.Value.SearchResults[searchHistory.Value.SelectedItem];
93 |
94 | var minimalPrompt = "[" + currentItem + "/" + totalItems + "]: ";
95 |
96 | var matchedEntryMaxLength = implementation.WindowWidth - minimalPrompt.Length;
97 | if (match.Length > matchedEntryMaxLength)
98 | {
99 | match = match.Substring(0, matchedEntryMaxLength);
100 | }
101 |
102 | ColorString highlightedLine = string.Empty;
103 |
104 | if (searchHistory.HasValue && !string.IsNullOrWhiteSpace(match))
105 | {
106 | var notMatchedString = new StringBuilder();
107 | var matchingPositions = FindAllIndexesOf(match.ToLowerInvariant(), searchHistory.Value.Term.ToLowerInvariant());
108 | for (int posInStr = 0; searchHistory != null && posInStr < match.Length; posInStr++)
109 | {
110 | if (matchingPositions.Contains(posInStr))
111 | {
112 | highlightedLine += notMatchedString.ToString() + new ColorString(searchHistory.Value.Term, Color.Green);
113 | notMatchedString.Clear();
114 | posInStr += searchHistory.Value.Term.Length - 1;
115 | }
116 | else
117 | {
118 | notMatchedString.Append(match[posInStr]);
119 | }
120 | }
121 |
122 | highlightedLine += notMatchedString.ToString();
123 | }
124 |
125 | // need to use the implementation functions as we are writing off the current line
126 | var pos = implementation.CursorLeft;
127 |
128 | implementation.CursorVisible = false;
129 | implementation.WriteLine();
130 | implementation.Write(new string(' ', implementation.WindowWidth - 1));
131 | implementation.CursorLeft = 0;
132 |
133 | implementation.Write(minimalPrompt);
134 |
135 | // our write function sets both implementation.CursorLeft and CursorPosition
136 | // we need to restore these to ensure we dont go out of sync
137 | var cpOld = ci.CursorPosition;
138 | ci.CursorPosition = minimalPrompt.Length;
139 | ci.Write(highlightedLine);
140 | ci.CursorPosition = cpOld;
141 |
142 | implementation.CursorTop--;
143 | implementation.CursorLeft = pos;
144 | implementation.CursorVisible = true;
145 | }
146 |
147 | private static int[] FindAllIndexesOf(string line, string term)
148 | {
149 | if (string.IsNullOrWhiteSpace(line) || string.IsNullOrWhiteSpace(term))
150 | {
151 | return Array.Empty();
152 | }
153 |
154 | var matches = new List();
155 |
156 | var index = 0;
157 | while (index != -1)
158 | {
159 | index = line.IndexOf(term, index);
160 | if (index != -1)
161 | {
162 | matches.Add(index);
163 | index += term.Length;
164 | }
165 | else
166 | {
167 | break;
168 | }
169 | }
170 | return matches.ToArray();
171 | }
172 |
173 | private Task OnSearchTextEnteredAsync(ConsoleImproved prompt, ConsoleKeyEx key)
174 | {
175 | return Task.Run(() =>
176 | {
177 | if (key.Key.Value == ConsoleKey.UpArrow || key.Key.Value == ConsoleKey.DownArrow)
178 | {
179 | return false;
180 | }
181 |
182 | var text = ci.UserEnteredText.ToString();
183 |
184 | var primaryResults = shell.History.Where(x => x.CmdLine.Contains(text));
185 | var secondaryResults = shell.History.Where(x => x.CmdLine.Contains(text.Trim()));
186 |
187 | var search = new SearchHistory()
188 | {
189 | SearchResults = primaryResults.Union(secondaryResults).Select(x => x.CmdLine).Distinct().ToList(),
190 | SelectedItem = 0,
191 | Term = text
192 | };
193 | RenderSearchChanges(search);
194 | ci.Tag = search;
195 |
196 | return false;
197 | });
198 | }
199 |
200 | private Task OnChangeSearchEntryAsync(ConsoleImproved prompt, ConsoleKeyEx key)
201 | {
202 | return Task.Run(() =>
203 | {
204 | if (ci.Tag is SearchHistory results)
205 | {
206 | if (key.Key == ConsoleKey.DownArrow)
207 | {
208 | if (results.SelectedItem < results.SearchResults.Count - 1)
209 | {
210 | results.SelectedItem++;
211 | }
212 | }
213 | else
214 | {
215 | if (results.SelectedItem > 0)
216 | {
217 | results.SelectedItem--;
218 | }
219 | }
220 |
221 | RenderSearchChanges(results);
222 | ci.Tag = results;
223 | }
224 |
225 | return false;
226 | });
227 | }
228 |
229 | private Task OnSelectSearchEntryAsync(ConsoleImproved prompt, ConsoleKeyEx key)
230 | {
231 | return Task.Run(() =>
232 | {
233 | if (ci.Tag is SearchHistory search)
234 | {
235 | if (search.SearchResults.Any())
236 | {
237 | ci.UserEnteredText = search.SearchResults[search.SelectedItem];
238 | }
239 | }
240 |
241 | return false;
242 | });
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/src/Shell/dotnet-shell-lib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | true
6 | 1.0.8.1
7 | dotnet-shell-lib is a library to build interactive shell environments like BASH. This library contains implementations to read and display multiline prompts as well as optionally invoking dotnet-shell syntax on these strings read from these.
8 | If you are looking for the dotnet-shell binary which implments all of the above check out dotnet-shell tool.
9 | LICENSE
10 | https://github.com/dotnet-shell
11 | https://github.com/dotnet-shell
12 |
13 |
14 |
15 | 5
16 | True
17 |
18 |
19 |
20 | 5
21 | True
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | True
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/UnitTests/AssertingConsole.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Console;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System;
4 | using System.Threading.Tasks;
5 |
6 | namespace UnitTests
7 | {
8 | class AssertingConsole : IConsole
9 | {
10 | public int CursorLeft { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
11 | public int CursorTop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
12 | public bool CursorVisible { set => throw new NotImplementedException(); }
13 |
14 | public int WindowWidth => throw new NotImplementedException();
15 |
16 | public int WindowHeight => throw new NotImplementedException();
17 |
18 | public ConsoleColor ForegroundColor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
19 | public ConsoleColor BackgroundColor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
20 |
21 | public bool KeyAvailiable => throw new NotImplementedException();
22 |
23 | public void ClearCurrentLine(int pos = -1)
24 | {
25 | throw new NotImplementedException();
26 | }
27 |
28 | public void MoveCursorDown(int lines)
29 | {
30 | throw new NotImplementedException();
31 | }
32 |
33 | public void MoveCursorUp(int lines)
34 | {
35 | throw new NotImplementedException();
36 | }
37 |
38 | public ConsoleKeyInfo ReadKey()
39 | {
40 | Assert.Fail();
41 | throw new Exception();
42 | }
43 |
44 | public Task RestoreAsync()
45 | {
46 | Assert.Fail();
47 | throw new Exception();
48 | }
49 |
50 | public void RestoreCursorPosition(Action onRestore = null)
51 | {
52 | throw new NotImplementedException();
53 | }
54 |
55 | public Task SaveAsync()
56 | {
57 | Assert.Fail();
58 | throw new Exception();
59 | }
60 |
61 | public void SaveCursorPosition()
62 | {
63 | throw new NotImplementedException();
64 | }
65 |
66 | public void Write(string text = null)
67 | {
68 | Assert.Fail();
69 | }
70 |
71 | public void WriteLine(string message = null)
72 | {
73 | Assert.Fail();
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/UnitTests/CSharpSuggestionsTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Suggestions;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace UnitTests
7 | {
8 | [TestClass]
9 | public class CSharpSuggestionsTests
10 | {
11 | [TestMethod]
12 | public async Task ConstructAsync()
13 | {
14 | CSharpSuggestions cSharpSuggestions = new();
15 | var results = await cSharpSuggestions.GetSuggestionsAsync(string.Empty, 0);
16 | Assert.AreEqual(0, results.Count());
17 | }
18 |
19 | [TestMethod]
20 | public async Task SimpleQueryAsync()
21 | {
22 | CSharpSuggestions cSharpSuggestions = new();
23 |
24 | var text = "Console.WriteLi";
25 |
26 | var results = await cSharpSuggestions.GetSuggestionsAsync(text, text.Length);
27 |
28 | Assert.AreEqual(1, results.Count());
29 | }
30 |
31 | [TestMethod]
32 | public async Task SimpleQueryWithMultipleResultsAsync()
33 | {
34 | CSharpSuggestions cSharpSuggestions = new();
35 |
36 | var text = "Console.";
37 |
38 | var results = await cSharpSuggestions.GetSuggestionsAsync(text, text.Length);
39 |
40 | Assert.AreEqual(22, results.Count());
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/UnitTests/CmdSuggestionsTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.API;
2 | using Dotnet.Shell.Logic.Suggestions;
3 | using Dotnet.Shell.Logic.Suggestions.Autocompletion;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using System;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Reflection;
9 | using System.Runtime.InteropServices;
10 | using System.Threading.Tasks;
11 |
12 | namespace UnitTests
13 | {
14 | [TestClass]
15 | public class CmdSuggestionsTests
16 | {
17 | private readonly string basePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestFiles");
18 |
19 | private static Task FakeCommandsAsync()
20 | {
21 | return Task.FromResult(
22 | new string[] {
23 | "atestcmd",
24 | "btestcmd",
25 | "testcmdc",
26 | "testcmdd"
27 | }
28 | );
29 | }
30 |
31 | [TestMethod]
32 | public void Construct()
33 | {
34 | using (var ms = new MemoryStream())
35 | {
36 | var fakeShell = new Shell();
37 | var s = new CmdSuggestions(fakeShell, FakeCommandsAsync());
38 | }
39 | }
40 |
41 | [TestMethod]
42 | public async Task SimpleQueryWithSingleResultAsync()
43 | {
44 | using (var ms = new MemoryStream())
45 | {
46 | var fakeShell = new Shell();
47 | fakeShell.Paths.Add(basePath);
48 |
49 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
50 | var result = await s.GetSuggestionsAsync("a", 1);
51 |
52 | Assert.AreEqual(1, result.Count());
53 | Assert.AreEqual("testcmd", result.ElementAt(0).CompletionText);
54 | Assert.AreEqual(1, result.ElementAt(0).Index);
55 |
56 | result = await s.GetSuggestionsAsync("b", 1);
57 |
58 | Assert.AreEqual(1, result.Count());
59 | Assert.AreEqual("testcmd", result.ElementAt(0).CompletionText);
60 | Assert.AreEqual(1, result.ElementAt(0).Index);
61 | }
62 | }
63 |
64 | [TestMethod]
65 | public async Task SimpleQueryWithoutResultAsync()
66 | {
67 | using (var ms = new MemoryStream())
68 | {
69 | var fakeShell = new Shell();
70 | fakeShell.Paths.Add(basePath);
71 |
72 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
73 | var result = await s.GetSuggestionsAsync("x", 1);
74 |
75 | Assert.AreEqual(0, result.Count());
76 | }
77 | }
78 |
79 | [TestMethod]
80 | public async Task QueryWithMultipleResultsAsync()
81 | {
82 | using (var ms = new MemoryStream())
83 | {
84 | var fakeShell = new Shell();
85 | fakeShell.Paths.Add(basePath);
86 |
87 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
88 | var results = await s.GetSuggestionsAsync("testcm", 6);
89 |
90 | Assert.AreEqual(2, results.Count());
91 |
92 | foreach (var result in results)
93 | {
94 | Assert.IsTrue(result.CompletionText == "dc" || result.CompletionText == "dd");
95 | Assert.AreEqual(6, result.Index);
96 | }
97 | }
98 | }
99 |
100 | [TestMethod]
101 | public async Task WithinLargerStringAsync()
102 | {
103 | using (var ms = new MemoryStream())
104 | {
105 | var fakeShell = new Shell();
106 | fakeShell.Paths.Add(basePath);
107 |
108 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
109 | var result = await s.GetSuggestionsAsync("echo a; b; echo c; echo a", 9);
110 |
111 | Assert.AreEqual(1, result.Count());
112 | Assert.AreEqual("testcmd", result.ElementAt(0).CompletionText);
113 | Assert.AreEqual(9, result.ElementAt(0).Index);
114 | }
115 | }
116 |
117 | [TestMethod]
118 | public async Task CompleteDirectoryAsync()
119 | {
120 | using (var ms = new MemoryStream())
121 | {
122 | var fakeShell = new Shell();
123 | fakeShell.ChangeDir( Path.GetFullPath(basePath + "/../") );
124 |
125 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
126 | var result = await s.GetSuggestionsAsync("cd T", 4);
127 |
128 | Assert.AreEqual(1, result.Count());
129 | Assert.AreEqual("estFiles"+Path.DirectorySeparatorChar, result.ElementAt(0).CompletionText);
130 | Assert.AreEqual(4, result.ElementAt(0).Index);
131 | }
132 | }
133 |
134 | [TestMethod]
135 | public async Task CompleteDirectory_NotFoundAsync()
136 | {
137 | using (var ms = new MemoryStream())
138 | {
139 | var fakeShell = new Shell();
140 | fakeShell.ChangeDir(Path.GetFullPath(basePath + "/../"));
141 |
142 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
143 | var result = await s.GetSuggestionsAsync("cd X", 4);
144 |
145 | Assert.AreEqual(0, result.Count());
146 | }
147 | }
148 |
149 | [TestMethod]
150 | public async Task CompleteDirectory_WithinLargerCommandAsync()
151 | {
152 | using (var ms = new MemoryStream())
153 | {
154 | var fakeShell = new Shell();
155 | fakeShell.ChangeDir(Path.GetFullPath(basePath + "/../"));
156 |
157 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
158 | var result = await s.GetSuggestionsAsync("echo A; cd T", 12);
159 | Assert.AreEqual(1, result.Count());
160 | Assert.AreEqual("estFiles"+ Path.DirectorySeparatorChar, result.ElementAt(0).CompletionText);
161 | Assert.AreEqual(12, result.ElementAt(0).Index);
162 |
163 | result = await s.GetSuggestionsAsync("echo A;cd T", 11);
164 | Assert.AreEqual(1, result.Count());
165 | Assert.AreEqual(1, result.Count());
166 | Assert.AreEqual("estFiles"+ Path.DirectorySeparatorChar, result.ElementAt(0).CompletionText);
167 | Assert.AreEqual(11, result.ElementAt(0).Index);
168 |
169 | result = await s.GetSuggestionsAsync("echo A && cd T", 14);
170 | Assert.AreEqual(1, result.Count());
171 | Assert.AreEqual(1, result.Count());
172 | Assert.AreEqual("estFiles"+ Path.DirectorySeparatorChar, result.ElementAt(0).CompletionText);
173 | Assert.AreEqual(14, result.ElementAt(0).Index);
174 |
175 | result = await s.GetSuggestionsAsync("cd T && cd A", 4);
176 | Assert.AreEqual(1, result.Count());
177 | Assert.AreEqual(1, result.Count());
178 | Assert.AreEqual("estFiles"+ Path.DirectorySeparatorChar, result.ElementAt(0).CompletionText);
179 | Assert.AreEqual(4, result.ElementAt(0).Index);
180 | }
181 | }
182 |
183 | [TestMethod]
184 | public async Task CompleteFileAsync()
185 | {
186 | using (var ms = new MemoryStream())
187 | {
188 | var fakeShell = new Shell();
189 | fakeShell.ChangeDir(Path.GetFullPath(basePath + "/../"));
190 |
191 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
192 | var result = await s.GetSuggestionsAsync("cat TestFi", 10);
193 |
194 | Assert.AreEqual(1, result.Count());
195 | }
196 | }
197 |
198 | [TestMethod]
199 | public void InternalFileResolution_ConvertToAbsolute()
200 | {
201 | using (var ms = new MemoryStream())
202 | {
203 | var fakeShell = new Shell
204 | {
205 | HomeDirectory = basePath,
206 | WorkingDirectory = basePath
207 | };
208 |
209 | Assert.AreEqual(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), FileAndDirectoryCompletion.ConvertToAbsolute(basePath + "/..", fakeShell));
210 | Assert.AreEqual(fakeShell.HomeDirectory + Path.DirectorySeparatorChar, FileAndDirectoryCompletion.ConvertToAbsolute("~/", fakeShell));
211 | Assert.AreEqual(fakeShell.WorkingDirectory + Path.DirectorySeparatorChar, FileAndDirectoryCompletion.ConvertToAbsolute("./", fakeShell));
212 | Assert.AreEqual(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "bob"), FileAndDirectoryCompletion.ConvertToAbsolute("~/../bob", fakeShell));
213 | }
214 | }
215 |
216 | [TestMethod]
217 | public async Task CompleteFilesInDirAsync()
218 | {
219 | using (var ms = new MemoryStream())
220 | {
221 | var fakeShell = new Shell
222 | {
223 | HomeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
224 | WorkingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
225 | };
226 |
227 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
228 | var result = await s.GetSuggestionsAsync("cat TestFiles/nsh", 17);
229 |
230 | Assert.AreEqual(1, result.Count());
231 | Assert.AreEqual("ScriptTest.nsh", result.ElementAt(0).CompletionText);
232 | Assert.AreEqual(17, result.ElementAt(0).Index);
233 | }
234 | }
235 |
236 | [TestMethod]
237 | public async Task CompleteExecutableInDirAsync()
238 | {
239 | using (var ms = new MemoryStream())
240 | {
241 | var fakeShell = new Shell
242 | {
243 | HomeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
244 | WorkingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
245 | };
246 |
247 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
248 | var result = await s.GetSuggestionsAsync("."+Path.DirectorySeparatorChar+"TestFiles"+Path.DirectorySeparatorChar+"nshScr", 18);
249 |
250 | Assert.AreEqual(1, result.Count());
251 | Assert.AreEqual("iptTest.nsh", result.ElementAt(0).CompletionText);
252 | Assert.AreEqual(18, result.ElementAt(0).Index);
253 | }
254 | }
255 |
256 | [TestMethod]
257 | public async Task CompleteExecutableInCurrentDirAsync()
258 | {
259 | using (var ms = new MemoryStream())
260 | {
261 | var fakeShell = new Shell
262 | {
263 | HomeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
264 | WorkingDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestFiles")
265 | };
266 |
267 | CmdSuggestions s = new(fakeShell, FakeCommandsAsync());
268 | var result = await s.GetSuggestionsAsync("." + Path.DirectorySeparatorChar + "nshScr", 8);
269 |
270 | Assert.AreEqual(1, result.Count());
271 | Assert.AreEqual("iptTest.nsh", result.ElementAt(0).CompletionText);
272 | Assert.AreEqual(8, result.ElementAt(0).Index);
273 | }
274 | }
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/src/UnitTests/ColorStringTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.UI;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System.Drawing;
4 | using System.Text;
5 |
6 | namespace UnitTests
7 | {
8 | [TestClass]
9 | public class ColorStringTests
10 | {
11 | [TestMethod]
12 | public void ConvertANSI()
13 | {
14 | byte[] ansiStrBytes = new byte[] {
15 | 0x1B, 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35,
16 | 0x3B, 0x32, 0x33, 0x31, 0x3B, 0x34, 0x38, 0x3B,
17 | 0x35, 0x3B, 0x33, 0x31, 0x3B, 0x31, 0x6D, 0xC2,
18 | 0xA0, 0x75, 0x73, 0x65, 0x72, 0xC2, 0xA0, 0x1B,
19 | 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35, 0x3B,
20 | 0x33, 0x31, 0x3B, 0x34, 0x38, 0x3B, 0x35, 0x3B,
21 | 0x32, 0x34, 0x30, 0x3B, 0x32, 0x32, 0x6D, 0xEE,
22 | 0x82, 0xB0, 0xC2, 0xA0, 0x1B, 0x5B, 0x30, 0x3B,
23 | 0x33, 0x38, 0x3B, 0x35, 0x3B, 0x32, 0x35, 0x30,
24 | 0x3B, 0x34, 0x38, 0x3B, 0x35, 0x3B, 0x32, 0x34,
25 | 0x30, 0x6D, 0xE2, 0x80, 0xA6, 0xC2, 0xA0, 0x1B,
26 | 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35, 0x3B,
27 | 0x32, 0x34, 0x35, 0x3B, 0x34, 0x38, 0x3B, 0x35,
28 | 0x3B, 0x32, 0x34, 0x30, 0x3B, 0x32, 0x32, 0x6D,
29 | 0xEE, 0x82, 0xB1, 0xC2, 0xA0, 0x1B, 0x5B, 0x30,
30 | 0x3B, 0x33, 0x38, 0x3B, 0x35, 0x3B, 0x32, 0x35,
31 | 0x30, 0x3B, 0x34, 0x38, 0x3B, 0x35, 0x3B, 0x32,
32 | 0x34, 0x30, 0x6D, 0x62, 0x69, 0x6E, 0xC2, 0xA0,
33 | 0x1B, 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35,
34 | 0x3B, 0x32, 0x34, 0x35, 0x3B, 0x34, 0x38, 0x3B,
35 | 0x35, 0x3B, 0x32, 0x34, 0x30, 0x3B, 0x32, 0x32,
36 | 0x6D, 0xEE, 0x82, 0xB1, 0xC2, 0xA0, 0x1B, 0x5B,
37 | 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35, 0x3B, 0x32,
38 | 0x35, 0x30, 0x3B, 0x34, 0x38, 0x3B, 0x35, 0x3B,
39 | 0x32, 0x34, 0x30, 0x6D, 0x44, 0x65, 0x62, 0x75,
40 | 0x67, 0xC2, 0xA0, 0x1B, 0x5B, 0x30, 0x3B, 0x33,
41 | 0x38, 0x3B, 0x35, 0x3B, 0x32, 0x34, 0x35, 0x3B,
42 | 0x34, 0x38, 0x3B, 0x35, 0x3B, 0x32, 0x34, 0x30,
43 | 0x3B, 0x32, 0x32, 0x6D, 0xEE, 0x82, 0xB1, 0xC2,
44 | 0xA0, 0x1B, 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B,
45 | 0x35, 0x3B, 0x32, 0x35, 0x32, 0x3B, 0x34, 0x38,
46 | 0x3B, 0x35, 0x3B, 0x32, 0x34, 0x30, 0x3B, 0x31,
47 | 0x6D, 0x6E, 0x65, 0x74, 0x63, 0x6F, 0x72, 0x65,
48 | 0x61, 0x70, 0x70, 0x33, 0x2E, 0x31, 0xC2, 0xA0,
49 | 0x1B, 0x5B, 0x30, 0x3B, 0x33, 0x38, 0x3B, 0x35,
50 | 0x3B, 0x32, 0x34, 0x30, 0x3B, 0x34, 0x39, 0x3B,
51 | 0x32, 0x32, 0x6D, 0xEE, 0x82, 0xB0, 0xC2, 0xA0,
52 | 0x1B, 0x5B, 0x30, 0x6D, 0x0A
53 | };
54 |
55 | var strData = Encoding.UTF8.GetString(ansiStrBytes);
56 |
57 | var cString = ColorString.FromRawANSI(strData);
58 |
59 | Assert.AreEqual(43, cString.Length);
60 | Assert.AreEqual(43, cString.Text.Length);
61 | Assert.AreNotEqual(43, cString.TextWithFormattingCharacters.Length);
62 | }
63 |
64 | [TestMethod]
65 | public void Construct()
66 | {
67 | ColorString a = new("hello", Color.Red);
68 | ColorString b = new("hello", Color.Green, Color.Blue);
69 |
70 | Assert.AreEqual(5, a.Length);
71 | Assert.AreEqual(5, b.Length);
72 | Assert.AreEqual(5, a.Text.Length);
73 | Assert.AreEqual(5, b.Text.Length);
74 | Assert.AreNotEqual("5", a.TextWithFormattingCharacters);
75 | Assert.AreNotEqual("5", b.TextWithFormattingCharacters);
76 |
77 | Assert.IsTrue(a.Equals("hello"));
78 | Assert.IsFalse(a.Equals("goodbye"));
79 | }
80 |
81 | [TestMethod]
82 | public void StringFunctions()
83 | {
84 | ColorString a = new("hello", Color.Red);
85 | ColorString b = new("goodbye", Color.Green, Color.Blue);
86 |
87 | var c = a + b;
88 |
89 | Assert.AreEqual("hellogoodbye", c.Text);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/UnitTests/ExecutionTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Compilation;
2 | using Dotnet.Shell.UI;
3 | using Microsoft.CodeAnalysis.Scripting;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using System;
6 | using System.IO;
7 | using System.Reflection;
8 | using System.Threading.Tasks;
9 |
10 | namespace UnitTests
11 | {
12 | [TestClass]
13 | public class ExecutionTests
14 | {
15 | [TestMethod]
16 | public async Task ConstructAsync()
17 | {
18 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
19 | _ = await Executer.GetDefaultExecuterAsync(errorDisplay);
20 | }
21 |
22 | [TestMethod]
23 | [ExpectedException(typeof(FileNotFoundException))]
24 | public async Task LoadAssemblyFromFile_DLL_InvalidAsync()
25 | {
26 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
27 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
28 | await exe.LoadAssemblyFromFileAsync("");
29 | await exe.ExecuteAsync(string.Empty);
30 | }
31 |
32 | [TestMethod]
33 | public async Task LoadAssemblyFromFile_DLL_MissingAsync()
34 | {
35 | var emptyFile = Path.GetTempFileName();
36 | try
37 | {
38 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
39 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
40 | await exe.LoadAssemblyFromFileAsync(emptyFile);
41 | await exe.ExecuteAsync(string.Empty);
42 | Assert.Fail();
43 | }
44 | catch (CompilationErrorException)
45 | {
46 | }
47 | finally
48 | {
49 | File.Delete(emptyFile);
50 | }
51 | }
52 |
53 | [TestMethod]
54 | public async Task LoadAssemblyFromFile_DLLAsync()
55 | {
56 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
57 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
58 | await exe.LoadAssemblyFromFileAsync(Assembly.GetExecutingAssembly().Location);
59 | }
60 |
61 | [TestMethod]
62 | public async Task Load_nshAsync()
63 | {
64 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
65 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
66 | await exe.ExecuteFileAsync(@".\TestFiles\nshScriptTest.nsh".Replace('\\', Path.DirectorySeparatorChar));
67 | }
68 |
69 | [TestMethod]
70 | public async Task Load_CSAsync()
71 | {
72 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
73 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
74 | await exe.ExecuteFileAsync(@".\TestFiles\csScriptTest.cs".Replace('\\', Path.DirectorySeparatorChar));
75 | }
76 |
77 | [TestMethod]
78 | public async Task AccessShellAPIAsync()
79 | {
80 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
81 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
82 | await exe.ExecuteAsync("Console.WriteLine( Shell.WorkingDirectory );");
83 | }
84 |
85 | [TestMethod]
86 | public async Task AccessColorStringAsync()
87 | {
88 | var errorDisplay = new ErrorDisplay(new AssertingConsole());
89 | var exe = await Executer.GetDefaultExecuterAsync(errorDisplay);
90 | await exe.ExecuteAsync("using System.Drawing; Console.WriteLine( new ColorString(\"Hello\", Color.Red) );");
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/UnitTests/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "I don't like this syntax", Scope = "module")]
9 |
--------------------------------------------------------------------------------
/src/UnitTests/OSTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.Logic.Execution;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System.Threading.Tasks;
4 |
5 | namespace UnitTests
6 | {
7 | [TestClass]
8 | public class OSTests
9 | {
10 | [TestMethod]
11 | public async Task GetOSHistoryAsync()
12 | {
13 | await OS.GetOSHistoryAsync();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/UnitTests/ShellTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.API;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using System.IO;
4 | using System.Reflection;
5 |
6 | namespace UnitTests
7 | {
8 | [TestClass]
9 | public class ShellTests
10 | {
11 | [TestMethod]
12 | public void Construct()
13 | {
14 | using (var ms = new MemoryStream())
15 | {
16 | var fakeShell = new Shell();
17 | }
18 | }
19 |
20 | [TestMethod]
21 | public void AddAliases()
22 | {
23 | using (var ms = new MemoryStream())
24 | {
25 | var fakeShell = new Shell();
26 | fakeShell.AddCSAlias("echo", "Console.WriteLine(\"{0}\");");
27 | fakeShell.AddCSAlias("red", "Console.WriteLine(new ColorString(\"{0}\", Color.Red).TextWithFormattingCharacters);");
28 | fakeShell.AddCSAlias("green", "Console.WriteLine(new ColorString(\"{0}\", Color.Green).TextWithFormattingCharacters);");
29 | fakeShell.AddCSAlias("quit", "Environment.Exit(0);");
30 |
31 | fakeShell.AddCmdAlias("ls", "ls --color=auto ");
32 | fakeShell.AddCmdAlias("dir", "dir --color=always ");
33 | fakeShell.AddCmdAlias("vdir", "vdir --color=always ");
34 | fakeShell.AddCmdAlias("grep", "grep --color=always ");
35 | fakeShell.AddCmdAlias("fgrep", "fgrep --color=alway s");
36 | fakeShell.AddCmdAlias("egrep", "egrep --color=always ");
37 | fakeShell.AddCmdAlias("ll", "ls -alF ");
38 | fakeShell.AddCmdAlias("la", "ls -A ");
39 | fakeShell.AddCmdAlias("l", "ls -CF ");
40 | }
41 | }
42 |
43 | [TestMethod]
44 | public void DuplicateAlias()
45 | {
46 | int errorCount = 0;
47 |
48 | using (var ms = new MemoryStream())
49 | {
50 | var fakeShell = new Shell
51 | {
52 | Error = (msg) => { Assert.IsTrue(!string.IsNullOrWhiteSpace(msg)); errorCount++; }
53 | };
54 | fakeShell.AddCSAlias("echo", "Console.WriteLine(\"{0}\");");
55 | fakeShell.AddCSAlias("echo", "sdfsdfsdf");
56 |
57 | fakeShell.AddCmdAlias("l", "ls -CF ");
58 | fakeShell.AddCmdAlias("l", "ls -CF ");
59 | }
60 |
61 | Assert.AreEqual(2, errorCount);
62 | }
63 |
64 | [TestMethod]
65 | public void ChangeDir()
66 | {
67 | var testDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
68 |
69 | using (var ms = new MemoryStream())
70 | {
71 | var fakeShell = new Shell();
72 | fakeShell.ChangeDir(Path.GetFullPath(testDir + Path.DirectorySeparatorChar + @"TestFiles"));
73 | Assert.IsTrue(fakeShell.WorkingDirectory.EndsWith("TestFiles"));
74 |
75 | fakeShell.ChangeDir("..");
76 | Assert.IsTrue(fakeShell.WorkingDirectory == testDir);
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/UnitTests/SuggestionsTests.cs:
--------------------------------------------------------------------------------
1 | using Dotnet.Shell.API;
2 | using Dotnet.Shell.Logic.Console;
3 | using Dotnet.Shell.Logic.Suggestions;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using System;
6 | using System.IO;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace UnitTests
11 | {
12 | [TestClass]
13 | public class SuggestionsTests
14 | {
15 | [TestMethod]
16 | public void Construction()
17 | {
18 | using (var ms = new MemoryStream())
19 | {
20 | var fakeShell = new Shell();
21 | Suggestions s = new(fakeShell);
22 | }
23 | }
24 |
25 | [TestMethod]
26 | public void Registration()
27 | {
28 | using (var ms = new MemoryStream())
29 | {
30 | var fakeShell = new Shell();
31 | Suggestions s = new(fakeShell);
32 |
33 | ConsoleImproved console = new(new MockConsole(), fakeShell);
34 | console.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.Tab), s.OnTabSuggestCmdAsync);
35 | }
36 | }
37 |
38 | [TestMethod]
39 | public async Task SuggestCSharpMultipleEntriesAsync()
40 | {
41 | using (var ms = new MemoryStream())
42 | {
43 | var fakeShell = new Shell();
44 | Suggestions s = new(fakeShell);
45 |
46 | var mockConsole = new MockConsole();
47 |
48 | ConsoleImproved console = new(mockConsole, fakeShell);
49 | console.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.Tab), s.OnTabSuggestCmdAsync);
50 |
51 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.C, ConsoleModifiers.Shift));
52 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.O));
53 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.N));
54 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.S));
55 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.O));
56 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.L));
57 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.E));
58 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.OemPeriod));
59 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.Tab));
60 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.Tab));
61 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.Enter));
62 |
63 | var command = await console.GetCommandAsync(CancellationToken.None);
64 |
65 | Assert.AreEqual("Console.", command);
66 |
67 | var stdOut = mockConsole.Output.ToString();
68 |
69 | Assert.IsTrue(stdOut.Contains("WriteLine"));
70 | Assert.IsTrue(stdOut.Contains("Beep"));
71 | }
72 | }
73 |
74 | [TestMethod]
75 | public async Task SuggestCSharpSingleEntryAsync()
76 | {
77 | using (var ms = new MemoryStream())
78 | {
79 | var fakeShell = new Shell();
80 | Suggestions s = new(fakeShell);
81 |
82 | var mockConsole = new MockConsole();
83 |
84 | ConsoleImproved console = new(mockConsole, fakeShell);
85 | console.AddKeyOverride(new ConsoleKeyEx(ConsoleKey.Tab), s.OnTabSuggestCmdAsync);
86 |
87 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.C, ConsoleModifiers.Shift));
88 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.O));
89 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.N));
90 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.S));
91 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.O));
92 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.L));
93 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.E));
94 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.OemPeriod));
95 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.B, ConsoleModifiers.Shift));
96 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.E));
97 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.E));
98 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.Tab));
99 | mockConsole.keys.Enqueue(new ConsoleKeyEx(ConsoleKey.Enter));
100 |
101 | var command = await console.GetCommandAsync(CancellationToken.None);
102 |
103 | Assert.AreEqual("Console.Beep", command);
104 |
105 | var stdOut = mockConsole.Output.ToString();
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/UnitTests/TestFiles/csScriptTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 |
4 | namespace Helper
5 | {
6 | public class Pretty
7 | {
8 | public static void Print(string str)
9 | {
10 | var strSplit = str.Split(Environment.NewLine);
11 | for (int x = 0; x < strSplit.Length; x++)
12 | {
13 | Console.WriteLine(
14 | new ColorString(
15 | strSplit[x],
16 | // Alternate between two colours
17 | x % 2 == 0 ? Color.Gray : Color.LightBlue).TextWithFormattingCharacters);
18 | }
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/UnitTests/TestFiles/nshScriptTest.nsh:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 |
3 | void PrettyPrint(string str)
4 | {
5 | var strSplit = str.Split(Environment.NewLine);
6 | for (int x = 0; x < strSplit.Length; x++)
7 | {
8 | Console.WriteLine(new ColorString(strSplit[x], x % 2 == 0 ? Color.Red : Color.Yellow).TextWithFormattingCharacters);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/UnitTests/UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | PreserveNewest
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | all
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | PreserveNewest
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/dotnet-shell/dotnet-shell.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | Dotnet.Shell
7 | dotnet-shell
8 | dotnet-shell
9 | true
10 | https://github.com/dotnet-shell
11 |
12 | https://github.com/dotnet-shell
13 | dotnet-shell is an interactive BASH-like shell based on CSharp script (CSX)
14 | LICENSE
15 | true
16 | dotnet-shell
17 | dotnet-shell
18 | dotnet-shell
19 | 1.0.8.1
20 | 1.0.8.1
21 |
22 |
23 |
24 |
25 | 5
26 | True
27 |
28 |
29 |
30 |
31 | 5
32 | True
33 |
34 |
35 |
36 |
37 | True
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------