diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6da72d2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +# top-most EditorConfig file +root = true + +# Windows-style newlines with a newline ending every file +[*] +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +# 4 space indentation +[*.{cs, cshtml, razor}] +indent_style = space +indent_size = 4 +# Default severity for analyzer diagnostics with category 'Style' +dotnet_analyzer_diagnostic.category-Style.severity = none +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none + +# 2 space indentation +[*.csproj] +indent_style = space +indent_size = 2 +charset = utf-8-bom + +# 2 space indentation +[*.{xaml, wxs, config, yml}] +indent_style = space +indent_size = 2 + +# Matches the exact files package.json +[{packages.config}] +indent_style = space +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/ask-a-question.md b/.github/ISSUE_TEMPLATE/ask-a-question.md new file mode 100644 index 0000000..4dc78e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask-a-question.md @@ -0,0 +1,10 @@ +--- +name: Ask a question +about: If you're not sure about something feel free to ask +title: '' +labels: question +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1f7515f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: Oh dear - tell me what didn't work +title: '' +labels: bug +assignees: '' + +--- + +**Description of the problem** +What happened and why do you think it's a bug? + +**TextrudeInteractive project** +If you are able to reproduce the issue in TextrudeInteractive then the best way to help us get to the bottom of the issue is to post a project file with the bug-report. Failing that, as much detail as possible about the models and templates you used would be helpful. + +**Desktop (please complete the following information):** + - OS: [if you think it is OS specific] + - Version [I'll assume you're using the latest release unless you say otherwise] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6c5e143 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Got an idea? Tell me about it +title: '' +labels: enhancement +assignees: '' + +--- + +**Tell me what you think would be a good addition! I can't promise to add it but you never know...** diff --git a/.github/ISSUE_TEMPLATE/positive-feedback.md b/.github/ISSUE_TEMPLATE/positive-feedback.md new file mode 100644 index 0000000..4b2c7c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/positive-feedback.md @@ -0,0 +1,10 @@ +--- +name: Send a smile +about: Tell me what you like - it keeps me motivated! +title: I like Textrude because.... +labels: smile +assignees: '' + +--- + +**Don't be shy - tell me why you like Textrude!** diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03ec545 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "nuget" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..d42a9fc --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,25 @@ +name: CD + +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + +jobs: + package: + env: + DisableGitVersionTask: true + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Package + run: dotnet run --project build/Build.csproj -- --target Package + - uses: actions/upload-artifact@v2 + with: + name: jumpfs.zip + path: publish/jumpfs.zip diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..db3967b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + pull_request: + paths-ignore: + - "docs/**" + - "*.md" + +jobs: + test: + env: + DisableGitVersionTask: true + strategy: + matrix: + include: + - os: windows-latest + build_config: Debug + - os: ubuntu-latest + build_config: Linux-Debug + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Test + run: dotnet run --project build/Build.csproj -- --target Test --configuration ${{ matrix.build_config }} --clean + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-on-${{ matrix.os }} + path-to-lcov: TestResults/lcov.info + parallel: true + coveralls-finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f46d519 --- /dev/null +++ b/.gitignore @@ -0,0 +1,369 @@ +/act +/tools +/TestReports + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +.vscode/tasks.json +.vscode/launch.json + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5146860 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at neil.macmullen@outlook.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc79afc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 NeilMacMullen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..11ec279 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Overview +gomark is a simple cross-platform exe and collection of scripts that allow you quickly bookmark locations in your file system, jump between them, or reference them from script commands. + +For example: + +mark appdata "C:\users\neilm\AppData\Roaming" +go appdata +ls (d appdata) +x appdata +--- show cross dos/ps/linux/explorer operation + +It's invaluable if you spend a lot of time in a command shell. + +The core is the gomark.exe executable. + +Wrapper scripts are provided for... +- Powershell +- DOS +- BASH + +Basic Usage + +## mark name [path] +Create or overwrite a bookmark at the specified path. If the path is omitted, the current working directory is used. + +## go name +Go to the path bookmarked by the specified name. + +## lst match +List all bookmarks where either the name or path contain the provided match string + +## x name +(Windows) opens a file-explorer window at the bookmark. + +##gol name [parent] +Finds the "best" match for a bookmark. For example if you have bookmarked .... + +##edit name +Opens Visual Studio Code + + +#Data +The Bookmarks file is stored as JSON at ..... + + +#Installation + +##PowerShell +Download the gomark executable and ensure it is in your path. +Download the gomark-functions.psm1 module. (Edit aliases to taste) +Run + + import-module gomark-functions.psm1 + +##DOS + +##WSL + +sudo apt-get install dotnet-dev-1.0.1 + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/BasicApplicationTests.cs b/Tests/BasicApplicationTests.cs new file mode 100644 index 0000000..1da4984 --- /dev/null +++ b/Tests/BasicApplicationTests.cs @@ -0,0 +1,66 @@ +using System.IO; +using FluentAssertions; +using jumpfs; +using jumpfs.Bookmarking; +using jumpfs.Commands; +using jumpfs.EnvironmentAccess; +using jumpfs.Extensions; +using NUnit.Framework; +using Tests.SupportClasses; + +namespace Tests +{ + [TestFixture] + public class BasicApplicationTests + { + [SetUp] + public void Setup() + { + _stdout = new StringWriter(); + _stderr = new StringWriter(); + env = new MockEnvironment(ShellType.PowerShell, new MockFileSystem()); + if (ShellGuesser.IsUnixy()) + { + env.SetCwd(@"/usr"); + } + else + { + env.SetCwd(@"D:\"); + } + + _repo = new BookmarkRepository(env); + _context = new ApplicationContext(_repo, _stdout, _stderr); + } + + private ApplicationContext _context; + private BookmarkRepository _repo; + private StringWriter _stderr; + + private StringWriter _stdout; + private MockEnvironment env; + + private void Execute(string cmd) + { + JumpFs.ExecuteWithContext(cmd.Tokenise(), _context); + } + + private string GetStdOut() => _stdout.ToString(); + private void CheckOutput(string expected) => GetStdOut().Trim().Should().EndWith(expected); + + [Test] + public void SimpleBookmarking() + { + Execute("mark -name here -path apath -literal"); + Execute("find -name here"); + CheckOutput(@"apath"); + } + + [Test] + public void FormattedBookmarking() + { + Execute("mark -name here -path apath -line 15 -column 10 -literal"); + Execute("find -name here -format %p:%l:%c"); + CheckOutput(@"apath:15:10"); + } + } +} \ No newline at end of file diff --git a/Tests/BookMarkTests.cs b/Tests/BookMarkTests.cs new file mode 100644 index 0000000..2bf7f01 --- /dev/null +++ b/Tests/BookMarkTests.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using jumpfs; +using jumpfs.Bookmarking; +using NUnit.Framework; +using Tests.SupportClasses; + +namespace Tests +{ + [TestFixture] + public class BookMarkTests + { + [SetUp] + public void Setup() + { + _environment = new MockEnvironment(ShellType.PowerShell, + new MockFileSystem()); + _repo = new BookmarkRepository(_environment); + } + + private BookmarkRepository _repo; + private MockEnvironment _environment; + + [Test] + public void BookMarkCanBeWritten() + { + _repo.Mark("a", "a path"); + var b = _repo.Find("a"); + b.Name.Should().Be("a"); + } + + [Test] + public void BookMarkCanBeChanged() + { + _repo.Mark("home", @"path\1"); + _repo.Mark("home", @"path\2"); + + var b = _repo.Find("home"); + b.Name.Should().Be("home"); + b.Path.Should().Be(@"path\2"); + } + } +} \ No newline at end of file diff --git a/Tests/CommandParserTests.cs b/Tests/CommandParserTests.cs new file mode 100644 index 0000000..967b4e9 --- /dev/null +++ b/Tests/CommandParserTests.cs @@ -0,0 +1,161 @@ +using System; +using FluentAssertions; +using jumpfs.CommandLineParsing; +using jumpfs.Commands; +using jumpfs.Extensions; +using NUnit.Framework; + +namespace Tests +{ + [TestFixture] + public class CommandParserTests + { + private static void Noop(ParseResults p, ApplicationContext context) + { + } + + [Test] + public void ParsingCommandSucceeds() + { + var command = new CommandDescriptor(Noop, "test"); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test".Tokenise()); + results.IsSuccess.Should().BeTrue("because we recognised the command"); + results.CommandDescriptor.Name.Should().Be("test"); + } + + [Test] + public void ParsingUnrecognisedCommandFails() + { + var parser = new CommandLineParser(); + var results = parser.Parse(Array.Empty()); + results.IsSuccess.Should().BeFalse("because there are no commands to parse"); + } + + [Test] + public void ParsingRecognisesArguments() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments(ArgumentDescriptor.Create("foo") + ); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test -foo xyz".Tokenise()); + results.IsSuccess.Should().BeTrue("because we recognised the command"); + results.CommandDescriptor.Name.Should().Be("test"); + results.ValueOf("foo").Should().Be("xyz"); + } + + [Test] + public void MissingCommandProvidesListInErrorOutput() + { + var command = new CommandDescriptor(Noop, "testcommand") + .WithArguments(ArgumentDescriptor.Create("foo") + ); + + var parser = new CommandLineParser(command); + var results = parser.Parse(Array.Empty()); + results.IsSuccess.Should().BeFalse(); + results.Message.Should().Contain("testcommand"); + } + + [Test] + public void UnrecognisedArgumentFails() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments(ArgumentDescriptor.Create("foo")); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test -fo xyz".Tokenise()); + results.IsSuccess.Should().BeFalse(); + } + + [Test] + public void MissingValueFails() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments(ArgumentDescriptor.Create("foo")); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test -foo ".Tokenise()); + results.IsSuccess.Should().BeFalse(); + } + + [Test] + public void OptionalValueReturnsEmptyString() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments( + ArgumentDescriptor.Create("foo") + .AllowEmpty()); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test -foo ".Tokenise()); + results.IsSuccess.Should().BeTrue(); + results.ValueOf("foo").Should().Be(string.Empty); + } + + [Test] + public void OptionalIntReturns0() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments( + ArgumentDescriptor.Create("foo")); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test".Tokenise()); + results.IsSuccess.Should().BeTrue(); + results.ValueOf("foo").Should().Be(0); + } + + + [Test] + public void IntParsesCorrectly() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments( + ArgumentDescriptor.Create("foo")); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test -foo 123".Tokenise()); + results.IsSuccess.Should().BeTrue(); + results.ValueOf("foo").Should().Be(123); + } + + [Test] + public void SwitchWorksAsExpected() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments( + ArgumentDescriptor + .CreateSwitch("foo") + ); + + var parser = new CommandLineParser(command); + + var results = parser.Parse("test".Tokenise()); + results.IsSuccess.Should().BeTrue(); + results.ValueOf("foo").Should().Be(false); + + var results2 = parser.Parse("test -foo".Tokenise()); + results2.IsSuccess.Should().BeTrue(); + results2.ValueOf("foo").Should().Be(true); + } + + + [Test] + public void MissingParameterFails() + { + var command = new CommandDescriptor(Noop, "test") + .WithArguments( + ArgumentDescriptor.Create("foo") + .Mandatory() + ); + + var parser = new CommandLineParser(command); + var results = parser.Parse("test".Tokenise()); + results.IsSuccess.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/Tests/CrossShellTests.cs b/Tests/CrossShellTests.cs new file mode 100644 index 0000000..1fe80af --- /dev/null +++ b/Tests/CrossShellTests.cs @@ -0,0 +1,100 @@ +using System.IO; +using FluentAssertions; +using jumpfs; +using jumpfs.Bookmarking; +using jumpfs.Commands; +using jumpfs.EnvironmentAccess; +using jumpfs.Extensions; +using NUnit.Framework; +using Tests.SupportClasses; + +namespace Tests +{ + [TestFixture] + public class CrossShellTests + { + [SetUp] + public void Setup() + { + _stdout = new StringWriter(); + _stderr = new StringWriter(); + _fs = new MockFileSystem(); + var winMock = new MockEnvironment(ShellType.PowerShell, _fs); + winMock.SetCwd(@"C:\"); + var winRepo = new BookmarkRepository(winMock); + _win = new ApplicationContext(winRepo, _stdout, _stderr); + var wslMock = new MockEnvironment(ShellType.Wsl, _fs); + wslMock.SetCwd("/usr"); + wslMock.SetEnvironmentVariable( + EnvVariables.WslEnvVar, + winRepo.Folder + ); + wslMock.SetEnvironmentVariable( + EnvVariables.WslRootVar, + @"\\wsl$\Ubuntu\" + ); + var wslRepo = new BookmarkRepository(wslMock); + + _wsl = new ApplicationContext(wslRepo, _stdout, _stderr); + } + + private StringWriter _stdout; + private StringWriter _stderr; + + private ApplicationContext _win; + private MockFileSystem _fs; + private ApplicationContext _wsl; + + + private void Execute(ApplicationContext context, string cmd) + { + JumpFs.ExecuteWithContext(cmd.Tokenise(), context); + } + + private string GetStdOut() => _stdout.ToString(); + private void CheckOutput(string expected) => GetStdOut().Trim().Should().Be(expected); + + [Test] + public void SimpleBookmarking() + { + Execute(_win, @"mark -name here -path C:/thepath -literal"); + Execute(_wsl, "find -name here"); + CheckOutput("/mnt/c/thepath"); + } + + [Test] + public void Win2WslNameTranslation() + { + Execute(_win, "mark -name here -path D:/a/b -literal"); + Execute(_wsl, "find -name here"); + CheckOutput("/mnt/d/a/b"); + } + + [Test] + public void WslToWinNameTranslation() + { + Execute(_wsl, "mark -name here -path /mnt/d/a/b -literal"); + Execute(_win, "find -name here"); + CheckOutput(@"d:\a\b"); + } + + [Test] + public void WslToWinNameTranslation2() + { + Execute(_wsl, @"mark -name here -path \\wsl$\Ubuntu\etc\x -literal"); + Execute(_win, "find -name here"); + CheckOutput(@"\\wsl$\Ubuntu\etc\x"); + } + + [Test] + public void TrueUnixWslToWinNameTranslation() + { + if (ShellGuesser.IsUnixy()) + { + Execute(_wsl, "mark -name here -path /etc/x"); + Execute(_win, "find -name here"); + CheckOutput(@"\\wsl$\Ubuntu\etc\x"); + } + } + } +} \ No newline at end of file diff --git a/Tests/FullPathTests.cs b/Tests/FullPathTests.cs new file mode 100644 index 0000000..8b105c0 --- /dev/null +++ b/Tests/FullPathTests.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using jumpfs; +using jumpfs.Commands; +using jumpfs.EnvironmentAccess; +using NUnit.Framework; +using Tests.SupportClasses; + +namespace Tests +{ + [TestFixture] + public class FullPathTests + { + [Test] + public void FromWindows() + { + //These tests only run on windows + if (ShellGuesser.IsUnixy()) + return; + var env = new MockEnvironment(ShellType.PowerShell, new MockFileSystem()); + env.SetCwd(@"C:\a\b"); + var p = new FullPathCalculator(env); + p.ToAbsolute(@"d:\x").Should().Be(@"d:\x"); + p.ToAbsolute(@"..\x").Should().Be(@"C:\a\x"); + } + + [Test] + public void FromWslVirtualDrive() + { + //These tests only run on *nix + if (!ShellGuesser.IsUnixy()) + return; + var env = new MockEnvironment(ShellType.Wsl, new MockFileSystem()); + env.SetCwd(@"/mnt/d/a"); + var p = new FullPathCalculator(env); + p.ToAbsolute(@"../x").Should().Be(@"/mnt/d/x"); + p.ToAbsolute(@"/x").Should().Be(@"/x"); + p.ToAbsolute(@"D:\x").Should().Be(@"D:\x"); + p.ToAbsolute(@"\\wsl$Ubuntu\var").Should().Be(@"\\wsl$Ubuntu\var"); + } + } +} \ No newline at end of file diff --git a/Tests/PathConverterTests.cs b/Tests/PathConverterTests.cs new file mode 100644 index 0000000..aff9c51 --- /dev/null +++ b/Tests/PathConverterTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using jumpfs; +using jumpfs.Bookmarking; +using NUnit.Framework; + +namespace Tests +{ + [TestFixture] + public class PathConverterTests + { + [SetUp] + public void Setup() + { + } + + private readonly string Unc = @"\\wsl$Ubuntu\"; + + [Test] + public void RootedPath() + { + var p = new PathConverter(string.Empty); + var src = @"C:\a\b\c"; + p.ToShell(ShellType.Wsl, src).Should().Be("/mnt/c/a/b/c"); + p.ToShell(ShellType.PowerShell, src).Should().Be(src); + } + + [Test] + public void InternalWslPath() + { + var p = new PathConverter(Unc); + var src = @"/var/etc"; + p.ToShell(ShellType.Wsl, src).Should().Be("/var/etc"); + } + + [Test] + public void InternalPSPath() + { + var p = new PathConverter(Unc); + var src = @"/var/etc"; + p.ToShell(ShellType.PowerShell, src).Should().Be($@"{Unc}var\etc"); + } + + [Test] + public void UNCPath() + { + var p = new PathConverter(Unc); + var src = @$"{Unc}var\etc"; + p.ToShell(ShellType.Wsl, src).Should().Be("/var/etc"); + p.ToShell(ShellType.PowerShell, src).Should().Be(src); + } + } +} \ No newline at end of file diff --git a/Tests/PathOperationTests.cs b/Tests/PathOperationTests.cs new file mode 100644 index 0000000..e69de29 diff --git a/Tests/ShellGuesserTests.cs b/Tests/ShellGuesserTests.cs new file mode 100644 index 0000000..3456d57 --- /dev/null +++ b/Tests/ShellGuesserTests.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using jumpfs; +using jumpfs.Bookmarking; +using jumpfs.EnvironmentAccess; +using NUnit.Framework; +using Tests.SupportClasses; + +namespace Tests +{ + [TestFixture] + public class ShellGuesserTests + { + [Test] + public void ShouldUseVarIfPresent() + { + var initialEnv = new MockEnvironment(ShellType.PowerShell, new MockFileSystem()); + initialEnv.SetEnvironmentVariable(EnvVariables.ShellOveride, "cmd"); + ShellGuesser + .GuessShell(initialEnv) + .Should() + .Be(ShellType.Cmd); + } + + [Test] + public void ShouldGuessIfVarNotPresent() + { + var initialEnv = new MockEnvironment(ShellType.PowerShell, new MockFileSystem()); + ShellGuesser + .GuessShell(initialEnv) + .Should() + .NotBe(ShellType.Cmd); + } + } +} \ No newline at end of file diff --git a/Tests/SupportClasses/MockEnvironment.cs b/Tests/SupportClasses/MockEnvironment.cs new file mode 100644 index 0000000..ae7cb6e --- /dev/null +++ b/Tests/SupportClasses/MockEnvironment.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using jumpfs; +using jumpfs.EnvironmentAccess; +using Environment = System.Environment; + +namespace Tests.SupportClasses +{ + public class MockFileSystem + { + private readonly Dictionary _files = new(); + private readonly HashSet _folders = new(); + public bool DirectoryExists(string path) => _folders.Contains(path); + public bool FileExists(string path) => _files.ContainsKey(path); + + public string ReadAllText(string path) + { + if (!FileExists(path)) + throw new IOException("File not found"); + return _files[path]; + } + + public void WriteAllText(string path, string text) + { + var folder = Path.GetDirectoryName(path); + _folders.Add(folder); + + _files[path] = text; + } + } + + //Mocks up the environment so we can test with a fake bookmark file + public class MockEnvironment : IEnvironment + { + private readonly Dictionary _env = new(); + private readonly MockFileSystem _fileSystem; + private string _cwd = string.Empty; + + + public MockEnvironment(ShellType shellType, MockFileSystem fs) + { + ShellType = shellType; + _fileSystem = fs; + } + + public ShellType ShellType { get; } + public bool DirectoryExists(string path) => _fileSystem.DirectoryExists(path); + + public bool FileExists(string path) => _fileSystem.FileExists(path); + + public string ReadAllText(string path) => _fileSystem.ReadAllText(path); + + public void WriteAllText(string location, string text) + { + _fileSystem.WriteAllText(location, text); + } + + + public string GetEnvironmentVariable(string name) => _env.TryGetValue(name, out var v) ? v : string.Empty; + + public string GetFolderPath(Environment.SpecialFolder folderName) => + Environment.GetFolderPath(folderName); + + public string Cwd() => _cwd; + + + public string SetEnvironmentVariable(string name, string val) => _env[name] = val; + + public void SetCwd(string s) + { + _cwd = s; + } + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 0000000..693d90a --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + diff --git a/bash/jumpfs-setup.sh b/bash/jumpfs-setup.sh new file mode 100644 index 0000000..35cc6a1 --- /dev/null +++ b/bash/jumpfs-setup.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Either ensure that jumpfs is on your path or modify this + +JumpFsExe="/mnt/d/work/jumpfs/publish/jumpfs" +# JumpFs needs to know where to look for the bookmarks file +loc="$(wslvar LOCALAPPDATA)" +JUMPFS_FOLDER="$(wslpath $loc)" +export JUMPFS_FOLDER + +# JumpFs needs to know the UNC path for the root of this WSL installation +# to allow access from windows +JUMPFS_WSL_ROOT="$(wslpath -w /)" +export JUMPFS_WSL_ROOT + +#functions feel free to change the names of these to suit.... +go() { + d=`$JumpFsExe find -name $1` + cd $d +} + +#functions feel free to change the names of these to suit.... +codego() { + d=`$JumpFsExe find -name $1 -format %p:%l:%c` + `code --goto "$d"` +} + +mark() { + if [[ -z "$2" ]] + then + d=`pwd` + echo "current dir $d" + `$JumpFsExe mark -name $1 -path $d` + else + echo "calling with $1/$2" + `$JumpFsExe mark -name $1 -path $2` + fi + +} + +lst() { + matches=`$JumpFsExe list -name $1` + echo "$matches" +} + + +jpenv() { + jumpfsenv=`$JumpFsExe env` + echo "$jumpfsenv" +} + diff --git a/jumpfs.sln b/jumpfs.sln new file mode 100644 index 0000000..42c3346 --- /dev/null +++ b/jumpfs.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30223.230 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "jumpfs", "jumpfs\jumpfs.csproj", "{E9E6F99B-8455-40F9-BD52-61987D8D8A36}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerShell", "PowerShell", "{06C2B334-C4DB-4EE4-9E41-6B96BDD5EC64}" + ProjectSection(SolutionItems) = preProject + powershell\jumpfs-functions.psm1 = powershell\jumpfs-functions.psm1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7E47A2B1-0B76-4D5E-A646-C38BC1405E07}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{1D00CA0A-2EA0-4592-BD3A-789640C821AC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bash", "bash", "{71679E9C-4289-45AB-83E2-45891796DB9C}" + ProjectSection(SolutionItems) = preProject + bash\jumpfs-setup.sh = bash\jumpfs-setup.sh + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmd", "cmd", "{61E0D795-C342-4852-A92B-09D4452A36B5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E9E6F99B-8455-40F9-BD52-61987D8D8A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9E6F99B-8455-40F9-BD52-61987D8D8A36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9E6F99B-8455-40F9-BD52-61987D8D8A36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9E6F99B-8455-40F9-BD52-61987D8D8A36}.Release|Any CPU.Build.0 = Release|Any CPU + {1D00CA0A-2EA0-4592-BD3A-789640C821AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D00CA0A-2EA0-4592-BD3A-789640C821AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D00CA0A-2EA0-4592-BD3A-789640C821AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D00CA0A-2EA0-4592-BD3A-789640C821AC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CADCDD24-864F-420B-9E27-DD613740F461} + EndGlobalSection +EndGlobal diff --git a/jumpfs.v3.ncrunchsolution b/jumpfs.v3.ncrunchsolution new file mode 100644 index 0000000..471729f --- /dev/null +++ b/jumpfs.v3.ncrunchsolution @@ -0,0 +1,10 @@ + + + False + + + Legacy + %LOCALAPPDATA%\NCrunchCache + True + + \ No newline at end of file diff --git a/jumpfs/Bookmarking/Bookmark.cs b/jumpfs/Bookmarking/Bookmark.cs new file mode 100644 index 0000000..c126040 --- /dev/null +++ b/jumpfs/Bookmarking/Bookmark.cs @@ -0,0 +1,13 @@ +namespace jumpfs.Bookmarking +{ + /// + /// A bookmark entry + /// + public class Bookmark + { + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public int Line { get; set; } + public int Column { get; set; } + } +} \ No newline at end of file diff --git a/jumpfs/Bookmarking/BookmarkRepository.cs b/jumpfs/Bookmarking/BookmarkRepository.cs new file mode 100644 index 0000000..2676ab2 --- /dev/null +++ b/jumpfs/Bookmarking/BookmarkRepository.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using jumpfs.EnvironmentAccess; + +namespace jumpfs.Bookmarking +{ + /// + /// A repository for bookmarks + /// + /// + /// The repository makes certain assumptions about the persistence mechanism + /// + public class BookmarkRepository + { + public readonly string BookmarkFile; + public readonly IEnvironment Environment; + + public BookmarkRepository(IEnvironment environment) + { + Environment = environment; + BookmarkFile = Path.Combine(Folder, "jumpfs", "bookmarks.json"); + } + + public string Folder => + (Environment.ShellType == ShellType.Wsl) + ? Environment.GetEnvironmentVariable(EnvVariables.WslEnvVar).Trim() + : Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData); + + public Bookmark[] Load() + { + var text = !Environment.FileExists(BookmarkFile) ? "[]" : Environment.ReadAllText(BookmarkFile); + return JsonSerializer.Deserialize(text); + } + + public Bookmark[] List(string match) + { + var all = Load(); + var matches = all.Where(m => m.Name.Contains(match) || m.Path.Contains(match)).ToArray(); + return matches; + } + + + public void Save(Bookmark[] bookmarks) + { + var text = JsonSerializer.Serialize(bookmarks, new JsonSerializerOptions {WriteIndented = true}); + Environment.WriteAllText(BookmarkFile, text); + } + + public void Mark(string name, string path) => Mark(name, path, 0, 0); + + public void Mark(string name, string path, int line, int column) + { + var marks = Load(); + var existing = marks.SingleOrDefault(m => m.Name == name); + if (existing == null) + { + existing = new Bookmark {Path = path}; + marks = marks.Append(existing).ToArray(); + } + + existing.Path = path; + existing.Name = name; + existing.Line = line; + existing.Column = column; + Save(marks); + } + + public Bookmark Find(string name) + { + var marks = Load(); + var existing = marks.SingleOrDefault(m => m.Name == name) + ?? new Bookmark(); + return existing; + } + } + + public class BookmarkSet + { + public JumpFsConfiguration Configuration { get; set; } = JumpFsConfiguration.Empty; + public Bookmark[] Bookmarks { get; set; } = Array.Empty(); + } + + public class JumpFsConfiguration + { + public static readonly JumpFsConfiguration Empty = new(); + } +} \ No newline at end of file diff --git a/jumpfs/Bookmarking/EnvVariables.cs b/jumpfs/Bookmarking/EnvVariables.cs new file mode 100644 index 0000000..6f54374 --- /dev/null +++ b/jumpfs/Bookmarking/EnvVariables.cs @@ -0,0 +1,9 @@ +namespace jumpfs.Bookmarking +{ + public static class EnvVariables + { + public const string WslEnvVar = "JUMPFS_FOLDER"; + public const string WslRootVar = "JUMPFS_WSL_ROOT"; + public const string ShellOveride = "JUMPFS_SHELL"; + } +} \ No newline at end of file diff --git a/jumpfs/Bookmarking/PathConverter.cs b/jumpfs/Bookmarking/PathConverter.cs new file mode 100644 index 0000000..eeb32e2 --- /dev/null +++ b/jumpfs/Bookmarking/PathConverter.cs @@ -0,0 +1,62 @@ +using System.Text.RegularExpressions; +using jumpfs.Extensions; + +namespace jumpfs.Bookmarking +{ + /// + /// Converts paths between Windows and Linux/WSL + /// + /// + /// Requires some extra logic to cope with WSL + /// + public class PathConverter + { + private readonly string _unc; + + public PathConverter(string unc) => _unc = unc; + + public string ToWsl(string path) + { + if (_unc.Length > 0 && path.StartsWith(_unc)) + return path.Substring(_unc.Length - 1).UnixSlash(); + + var m = Regex.Match(path, @"^(\w):(.*)"); + + if (m.Success) + { + var root = m.Groups[1].Value; + path = $"/mnt/{root.ToLowerInvariant()}" + m.Groups[2].Value; + } + + return path.UnixSlash(); + } + + public string ToUnc(string path) + { + if (path.StartsWith("/")) + { + var m = Regex.Match(path, @"^/mnt/(\w)/(.*)"); + if (m.Success) + { + var drv = m.Groups[1].Value; + var subPath = m.Groups[2].Value.WinSlash(); + return $@"{drv}:\{subPath}"; + } + + return $"{_unc}{path.Substring(1).WinSlash()}"; + } + + return path; + } + + public string ToShell(ShellType environment, string path) + { + switch (environment) + { + case ShellType.Wsl: + return ToWsl(path); + default: return ToUnc(path); + } + } + } +} \ No newline at end of file diff --git a/jumpfs/Bookmarking/PathOperations.cs b/jumpfs/Bookmarking/PathOperations.cs new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/jumpfs/Bookmarking/PathOperations.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jumpfs/CommandLineParsing/ArgumentDescriptor.cs b/jumpfs/CommandLineParsing/ArgumentDescriptor.cs new file mode 100644 index 0000000..5fb5c72 --- /dev/null +++ b/jumpfs/CommandLineParsing/ArgumentDescriptor.cs @@ -0,0 +1,95 @@ +using System; + +namespace jumpfs.CommandLineParsing +{ + [Flags] + public enum ArgumentFlags + { + None, + AllowNoValue = (1 << 1), + Mandatory = (1 << 2) + } + + /// + /// Describes an argument that can be passed to a particular command + /// + public readonly struct ArgumentDescriptor + { + public readonly string Name; + public readonly Type Type; + public readonly string HelpText; + + private ArgumentDescriptor(string name, Type type, string helpText, ArgumentFlags flags) + { + Name = name; + Type = type; + HelpText = helpText; + Flags = flags; + } + + public readonly ArgumentFlags Flags; + public bool IsMandatory => Flags.HasFlag(ArgumentFlags.Mandatory); + public bool CanBeEmpty => Flags.HasFlag(ArgumentFlags.AllowNoValue); + + /// + /// Creates a string argument that doesn't necessarily need to be supplied + /// + public static ArgumentDescriptor Create(string name) => + new(name, typeof(T), string.Empty, ArgumentFlags.None); + + public ArgumentDescriptor WithHelpText(string helpText) => + new(Name, Type, helpText, Flags); + + + public ArgumentDescriptor WithFlags(ArgumentFlags flags) => + new(Name, Type, HelpText, flags); + + public ArgumentDescriptor Mandatory() => + WithFlags(Flags | ArgumentFlags.Mandatory); + + public ArgumentDescriptor AllowEmpty() => WithFlags(Flags | ArgumentFlags.AllowNoValue); + + public object DefaultValue() + { + if (Type == typeof(string)) return string.Empty; + if (Type == typeof(int)) return 0; + if (Type == typeof(bool)) return false; + throw new NotImplementedException($"Unable to provide default value for type {Type.Name}"); + } + + public object DefaultValueWhenFlagPresent() => Type == typeof(bool) ? true : DefaultValue(); + + public static ArgumentDescriptor CreateSwitch(string name) => Create(name).AllowEmpty(); + + public bool TryConvert(string valStr, out object o) + { + if (Type == typeof(string)) + { + o = valStr; + return true; + } + + ; + if (Type == typeof(int)) + { + if (int.TryParse(valStr, out var i)) + { + o = i; + return true; + } + } + + if (Type == typeof(bool)) + { + if (bool.TryParse(valStr, out var b)) + { + o = b; + return true; + } + } + + o = null; + return false; + } + } +} \ No newline at end of file diff --git a/jumpfs/CommandLineParsing/ArgumentType.cs b/jumpfs/CommandLineParsing/ArgumentType.cs new file mode 100644 index 0000000..0665b9c --- /dev/null +++ b/jumpfs/CommandLineParsing/ArgumentType.cs @@ -0,0 +1,3 @@ +namespace jumpfs.CommandLineParsing +{ +} \ No newline at end of file diff --git a/jumpfs/CommandLineParsing/CommandDescriptor.cs b/jumpfs/CommandLineParsing/CommandDescriptor.cs new file mode 100644 index 0000000..cd622fb --- /dev/null +++ b/jumpfs/CommandLineParsing/CommandDescriptor.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using jumpfs.Commands; +using jumpfs.Extensions; + +namespace jumpfs.CommandLineParsing +{ + /// + /// A command that can be supported by the command line parser + /// + public readonly struct CommandDescriptor + { + /// + /// The action to be taken when this command is run + /// + public readonly Action Action; + + /// + /// Name of the command (string used to invoke it) + /// + public readonly string Name; + + /// + /// List of Argument Descriptors for this command + /// + public readonly ArgumentDescriptor[] Arguments; + + public CommandDescriptor(Action action, string name, + ArgumentDescriptor[] arguments, + string helpText) + { + Action = action; + //ensure the name is always lower-cased + Name = name.ToLowerInvariant(); + Arguments = arguments.ToArray(); + HelpText = helpText; + } + + public readonly string HelpText; + + public CommandDescriptor(Action action, string name) : this(action, name, + Array.Empty(), string.Empty) + { + } + + public CommandDescriptor WithArguments(params ArgumentDescriptor[] args) => + new(Action, Name, args, HelpText); + + public CommandDescriptor WithHelpText(string helpText) => + new(Action, Name, Arguments, helpText); + + public static readonly CommandDescriptor Empty = new((_, _) => { }, string.Empty); + + public bool TryArgument(string name, out ArgumentDescriptor arg) + { + return Arguments.TryGetSingle(a => a.Name == name, out arg); + } + } +} \ No newline at end of file diff --git a/jumpfs/CommandLineParsing/CommandLineParser.cs b/jumpfs/CommandLineParsing/CommandLineParser.cs new file mode 100644 index 0000000..21cc348 --- /dev/null +++ b/jumpfs/CommandLineParsing/CommandLineParser.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using jumpfs.Extensions; + +namespace jumpfs.CommandLineParsing +{ + /// + /// Parses an array of strings passed on the command line + /// + /// + /// No action is taken by the parser - it's up to the caller to use + /// the returned ParseResult + /// + public class CommandLineParser + { + private const string CommandPrefix = "-"; + private readonly CommandDescriptor[] _commands; + + public CommandLineParser(params CommandDescriptor[] args) => _commands = args.ToArray(); + + public string ConstructHelp() + { + return + @"Missing/unrecognised command. +Use of of the following: +" + string.Join(Environment.NewLine, _commands.Select(c => $" {c.Name} - {c.HelpText}")) + + @" +"; + } + + public ParseResults Parse(string[] suppliedArguments) + { + if (!suppliedArguments.Any()) + return ParseResults.Error(CommandDescriptor.Empty, ConstructHelp()); + + if (!_commands.TryGetSingle(c => c.Name == suppliedArguments[0], out var requestedCommand)) + return ParseResults.Error(CommandDescriptor.Empty, ConstructHelp()); + + bool IsValue(int i) => i < suppliedArguments.Length && !suppliedArguments[i].StartsWith(CommandPrefix); + + var assignedArguments = new Dictionary(); + + + for (var i = 1; i < suppliedArguments.Length; i++) + { + var token = suppliedArguments[i]; + if (!token.StartsWith(CommandPrefix)) continue; + var p = token.Substring(CommandPrefix.Length); + if (!requestedCommand.TryArgument(p, out var arg)) + { + return ParseResults.Error(requestedCommand, + ConstructHelpForCommand(requestedCommand, + $"unrecognised argument '{p}'" + )); + } + + //move on to the next token + i++; + + if (IsValue(i)) + { + if (!arg.TryConvert(suppliedArguments[i], out var v)) + return ParseResults.Error(requestedCommand, $"Parameter '{arg.Name}' invalid value"); + assignedArguments[arg.Name] = v; + } + else + { + if (arg.CanBeEmpty) + assignedArguments[arg.Name] = arg.DefaultValueWhenFlagPresent(); + else + return ParseResults.Error(requestedCommand, $"Parameter '{arg.Name}' missing value"); + } + } + + var missingParameters = requestedCommand + .Arguments + .Where(p => p.IsMandatory) + .Where(req => !assignedArguments.ContainsKey(req.Name)) + .ToArray(); + if (missingParameters.Any()) + return ParseResults.Error(requestedCommand, ConstructHelpForCommand(requestedCommand, + $"missing arguments: {string.Join(" ", missingParameters.Select(p => p.Name))}" + )); + + return ParseResults.Success(requestedCommand, assignedArguments); + } + + public static string ConstructHelpForCommand(CommandDescriptor cmd, string additional) + { + var sb = new StringBuilder(); + if (additional.Length != 0) + sb.AppendLine(additional); + foreach (var a in cmd.Arguments) + { + sb.AppendLine($" {CommandPrefix}{a.Name} {a.Type} {a.HelpText}"); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/jumpfs/CommandLineParsing/ParseResults.cs b/jumpfs/CommandLineParsing/ParseResults.cs new file mode 100644 index 0000000..bdc5831 --- /dev/null +++ b/jumpfs/CommandLineParsing/ParseResults.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using jumpfs.Commands; + +namespace jumpfs.CommandLineParsing +{ + public class ParseResults + { + private Dictionary _argumentValues = new(); + + public CommandDescriptor CommandDescriptor { get; private set; } + + public string Message { get; private set; } = string.Empty; + public bool IsSuccess { get; private set; } + + public static ParseResults Error(CommandDescriptor commandDescriptor, string error) => + new() + { + Message = error, + CommandDescriptor = commandDescriptor + }; + + public static ParseResults Success(CommandDescriptor commandDescriptor, Dictionary vals) => + new() + { + CommandDescriptor = commandDescriptor, + IsSuccess = true, + _argumentValues = vals, + }; + + public T ValueOf(string argName) + { + if (!CommandDescriptor.TryArgument(argName, out var arg)) + throw new ArgumentException($"attempt to retrieve invalid argument {argName}"); + + return _argumentValues.TryGetValue(arg.Name, out var v) + ? (T) v + : (T) arg.DefaultValue(); + } + + public void Execute(ApplicationContext context) + { + CommandDescriptor.Action(this, context); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/ApplicationContext.cs b/jumpfs/Commands/ApplicationContext.cs new file mode 100644 index 0000000..75a9bd5 --- /dev/null +++ b/jumpfs/Commands/ApplicationContext.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; +using jumpfs.Bookmarking; + +namespace jumpfs.Commands +{ + public class ApplicationContext + { + private readonly PathConverter _pathConverter; + public readonly string[] Args = Array.Empty(); + public readonly TextWriter ErrorStream; + public readonly TextWriter OutputStream; + public readonly BookmarkRepository Repo; + + public ApplicationContext( + BookmarkRepository repo, + TextWriter outputStream, + TextWriter errorStream) + { + Repo = repo; + + OutputStream = outputStream; + ErrorStream = errorStream; + _pathConverter = new PathConverter(repo.Environment.GetEnvironmentVariable(EnvVariables.WslRootVar)); + } + + public void WriteLine(string str) => OutputStream.WriteLine(str); + public string ToNative(string path) => _pathConverter.ToShell(Repo.Environment.ShellType, path); + + public string ToAbsolutePath(string path) + { + var pm = new FullPathCalculator(Repo.Environment); + var abs = pm.ToAbsolute(path); + return _pathConverter.ToShell(ShellType.PowerShell, abs); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdDebug.cs b/jumpfs/Commands/CmdDebug.cs new file mode 100644 index 0000000..4a2da67 --- /dev/null +++ b/jumpfs/Commands/CmdDebug.cs @@ -0,0 +1,39 @@ +using System.IO; +using jumpfs.Bookmarking; +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdDebug + { + public static readonly CommandDescriptor Descriptor = new CommandDescriptor(Run, "debug") + .WithArguments( + ArgumentDescriptor.Create(Names.Path) + .WithHelpText("file or folder name") + ) + .WithHelpText("shows information about the supplied path"); + + private static void Run(ParseResults results, ApplicationContext context) + { + var path = results.ValueOf(Names.Path); + + var env = context.Repo.Environment; + context.WriteLine($"cwd '{env.Cwd()}"); + var pm = new FullPathCalculator(env); + var fullPath = pm.ToAbsolute(path); + context.WriteLine($"path '{path}' -> '{fullPath}'"); + + var isFile = File.Exists(fullPath); + var isDirectory = Directory.Exists(fullPath); + context.WriteLine($"isFile:{isFile} isDirectory:{isDirectory}"); + if (env.ShellType == ShellType.Wsl && !fullPath.StartsWith("/mnt/")) + { + var root = env.GetEnvironmentVariable(EnvVariables.WslRootVar); + context.WriteLine($"WSL root {root}"); + //can't use path combine + var cp = root + fullPath; + context.WriteLine($"New path {cp}"); + } + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdEnv.cs b/jumpfs/Commands/CmdEnv.cs new file mode 100644 index 0000000..a842980 --- /dev/null +++ b/jumpfs/Commands/CmdEnv.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdEnv + { + public static readonly CommandDescriptor Descriptor = + new CommandDescriptor(Run, "env") + .WithHelpText("shows information about the application and environment it is running in"); + + private static void Run(ParseResults results, ApplicationContext context) + { + context.WriteLine($"System: {RuntimeEnvironment.GetSystemVersion()}"); + context.WriteLine($"Architecture: {RuntimeInformation.OSArchitecture}"); + context.WriteLine($"OS: {RuntimeInformation.OSDescription}"); + context.WriteLine($"Shell: {context.Repo.Environment.ShellType}"); + context.WriteLine($"Bookmark file: {context.Repo.BookmarkFile}"); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdFind.cs b/jumpfs/Commands/CmdFind.cs new file mode 100644 index 0000000..e58861b --- /dev/null +++ b/jumpfs/Commands/CmdFind.cs @@ -0,0 +1,45 @@ +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdFind + { + public static readonly CommandDescriptor Descriptor = new CommandDescriptor(Run, "find") + .WithArguments( + ArgumentDescriptor.Create(Names.Name) + .Mandatory() + .WithHelpText("name of the bookmark"), + ArgumentDescriptor.Create(Names.Format) + .WithHelpText(@"custom output format: + %p - full path + %l - line number + %c - column number + +specifiers can be combined and separated . For example: + +--format %p:%l:%c + +") + ) + .WithHelpText("locates a bookmark with the specified name and outputs the associated path"); + + public static void Run(ParseResults results, ApplicationContext context) + { + var name = results.ValueOf(Names.Name); + var format = results.ValueOf(Names.Format); + var mark = context.Repo.Find(name); + var path = context.ToNative(mark.Path); + if (format.Length == 0) + context.WriteLine(path); + else + { + format = format + .Replace("%p", mark.Path) + .Replace("%l", mark.Line.ToString()) + .Replace("%c", mark.Column.ToString()) + ; + context.WriteLine(format); + } + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdList.cs b/jumpfs/Commands/CmdList.cs new file mode 100644 index 0000000..de95de4 --- /dev/null +++ b/jumpfs/Commands/CmdList.cs @@ -0,0 +1,26 @@ +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdList + { + public static readonly CommandDescriptor Descriptor + = new CommandDescriptor(Run, "list") + .WithArguments( + ArgumentDescriptor.Create(Names.Name) + .WithHelpText("restrict the output to items where the name or path contains the supplied name") + .AllowEmpty() + ) + .WithHelpText("lists all or a subset of stored bookmarks"); + + private static void Run(ParseResults results, ApplicationContext context) + { + var name = results.ValueOf(Names.Name); + var marks = context.Repo.List(name); + foreach (var mark in marks) + { + context.WriteLine($"{mark.Name} --> {context.ToNative(mark.Path)}"); + } + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdMark.cs b/jumpfs/Commands/CmdMark.cs new file mode 100644 index 0000000..201cae7 --- /dev/null +++ b/jumpfs/Commands/CmdMark.cs @@ -0,0 +1,35 @@ +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdMark + { + public static readonly CommandDescriptor Descriptor = new CommandDescriptor(Run, "mark") + .WithArguments( + ArgumentDescriptor.Create(Names.Name) + .WithHelpText("name of the bookmark"), + ArgumentDescriptor.Create(Names.Path) + .WithHelpText("file or folder name"), + ArgumentDescriptor.Create(Names.Line) + .WithHelpText("line number"), + ArgumentDescriptor.Create(Names.Column) + .WithHelpText("column number"), + ArgumentDescriptor.CreateSwitch(Names.Literal) + .WithHelpText("use the path as provided rather than trying to turn it into an absolute path") + ) + .WithHelpText("adds a bookmark. By default this is considered to be a folder"); + + private static void Run(ParseResults results, ApplicationContext context) + { + var name = results.ValueOf(Names.Name); + var path = results.ValueOf(Names.Path); + var line = results.ValueOf(Names.Line); + var column = results.ValueOf(Names.Column); + + if (!results.ValueOf(Names.Literal)) + path = context.ToAbsolutePath(path); + + context.Repo.Mark(name, path, line, column); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/CmdShowArgs.cs b/jumpfs/Commands/CmdShowArgs.cs new file mode 100644 index 0000000..8bee64b --- /dev/null +++ b/jumpfs/Commands/CmdShowArgs.cs @@ -0,0 +1,20 @@ +using jumpfs.CommandLineParsing; + +namespace jumpfs.Commands +{ + public class CmdShowArgs + { + public static readonly CommandDescriptor Descriptor + = new CommandDescriptor(Run, "args") + .WithHelpText( + @"writes out the arguments supplied to the program +This can be useful when debugging interpolation issues."); + + private static void Run(ParseResults results, ApplicationContext context) + { + context.WriteLine("Received arguments..."); + foreach (var a in context.Args) + context.WriteLine($" '{a}'"); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/FullPathCalculator.cs b/jumpfs/Commands/FullPathCalculator.cs new file mode 100644 index 0000000..47a410d --- /dev/null +++ b/jumpfs/Commands/FullPathCalculator.cs @@ -0,0 +1,51 @@ +using System.IO; +using jumpfs.EnvironmentAccess; + +namespace jumpfs.Commands +{ + /// + /// Allows conversion between absolute and relative paths + /// + public class FullPathCalculator + { + public FullPathCalculator(IEnvironment env) => Env = env; + public IEnvironment Env { get; set; } + + /// + /// Returns an absolute (rooted) path + /// + /// + /// If a relative path is passed in we assume it is relative to the root + /// + public string ToAbsolute(string path) + { + if (path.Length == 0) return path; + var root = Env.Cwd(); + //Path.GetFullPath does different things under unix and windows + //so we need to do some run-time adjustment! + if (ShellGuesser.IsUnixy()) + { + //under wls we have want to be able to cope with the relative path + //being either.... + // a relative path ../x + // an absolute path /var + // a wsl path such as c:\users\aaa + // a wsl path to internal drive such \\wsl$\Ubuntu\var + + + if (path.StartsWith("/")) + return path; + + + if (path.StartsWith(@"\\")) + return path; + if (path.Length > 1 && path[1] == ':') + return path; + return Path.GetFullPath(path, root); + } + + //there's less to worry about on windows! + return Path.GetFullPath(path, root); + } + } +} \ No newline at end of file diff --git a/jumpfs/Commands/JumpFs.cs b/jumpfs/Commands/JumpFs.cs new file mode 100644 index 0000000..ae228da --- /dev/null +++ b/jumpfs/Commands/JumpFs.cs @@ -0,0 +1,49 @@ +using System; +using jumpfs.Bookmarking; +using jumpfs.CommandLineParsing; +using Environment = jumpfs.EnvironmentAccess.Environment; + +namespace jumpfs.Commands +{ + public class JumpFs + { + public static CommandLineParser CreateParser() + { + var parser = new CommandLineParser( + CmdShowArgs.Descriptor, + CmdEnv.Descriptor + , + CmdMark.Descriptor + , + CmdFind.Descriptor, + CmdList.Descriptor, + CmdDebug.Descriptor + ); + return parser; + } + + public static ApplicationContext CliContext() + { + var outputStream = Console.Out; + var errorStream = Console.Error; + + var repo = new BookmarkRepository(new Environment()); + return new ApplicationContext(repo, outputStream, errorStream); + } + + + public static void ExecuteWithContext(string[] args, ApplicationContext context) + { + var parser = CreateParser(); + var results = parser.Parse(args); + if (results.IsSuccess) + results.Execute(context); + else + { + if (results.CommandDescriptor.Name.Length != 0) + context.ErrorStream.WriteLine($"Command: {results.CommandDescriptor.Name}"); + context.ErrorStream.WriteLine($"Error: {results.Message}"); + } + } + } +} \ No newline at end of file diff --git a/jumpfs/EnvironmentAccess/Environment.cs b/jumpfs/EnvironmentAccess/Environment.cs new file mode 100644 index 0000000..ccdb417 --- /dev/null +++ b/jumpfs/EnvironmentAccess/Environment.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using jumpfs.Bookmarking; + +namespace jumpfs.EnvironmentAccess +{ + public class Environment : IEnvironment + { + public Environment() => ShellType = ShellGuesser.GuessShell(this); + + public ShellType ShellType { get; init; } + public bool DirectoryExists(string path) => Directory.Exists(path); + + public bool FileExists(string path) => File.Exists(path); + public string ReadAllText(string path) => File.ReadAllText(path); + + public void WriteAllText(string location, string text) + { + File.WriteAllText(location, text); + } + + public string GetEnvironmentVariable(string name) => + System.Environment.GetEnvironmentVariable(name) ?? string.Empty; + + public string GetFolderPath(System.Environment.SpecialFolder folderName) => + System.Environment.GetFolderPath(folderName); + + public string Cwd() => Directory.GetCurrentDirectory(); + + private ShellType GuessShell() + { + //if the user has not specified the shell, try to guess it from environmental information + var forcedEnv = GetEnvironmentVariable(EnvVariables.ShellOveride); + return Enum.TryParse(typeof(ShellType), forcedEnv, true, out var shell) + ? (ShellType) shell + : RuntimeInformation.OSDescription.Contains("Linux") + ? ShellType.Wsl + : ShellType.PowerShell; + } + } +} \ No newline at end of file diff --git a/jumpfs/EnvironmentAccess/IEnvironment.cs b/jumpfs/EnvironmentAccess/IEnvironment.cs new file mode 100644 index 0000000..4947beb --- /dev/null +++ b/jumpfs/EnvironmentAccess/IEnvironment.cs @@ -0,0 +1,17 @@ +namespace jumpfs.EnvironmentAccess +{ + /// + /// Provides a way of abstracting the file system and environment which is handy for testing + /// + public interface IEnvironment + { + ShellType ShellType { get; } + bool DirectoryExists(string path); + bool FileExists(string path); + string ReadAllText(string path); + void WriteAllText(string location, string text); + public string GetEnvironmentVariable(string name); + string GetFolderPath(System.Environment.SpecialFolder folderName); + public string Cwd(); + } +} \ No newline at end of file diff --git a/jumpfs/EnvironmentAccess/ShellGuesser.cs b/jumpfs/EnvironmentAccess/ShellGuesser.cs new file mode 100644 index 0000000..020ee94 --- /dev/null +++ b/jumpfs/EnvironmentAccess/ShellGuesser.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; +using jumpfs.Bookmarking; + +namespace jumpfs.EnvironmentAccess +{ + public static class ShellGuesser + { + public static ShellType GuessShell(IEnvironment env) + { + //if the user has not specified the shell, try to guess it from environmental information + var forcedEnv = env.GetEnvironmentVariable(EnvVariables.ShellOveride); + return Enum.TryParse(typeof(ShellType), forcedEnv, true, out var shell) + ? (ShellType) shell + : RuntimeInformation.OSDescription.Contains("Linux") + ? ShellType.Wsl + : ShellType.PowerShell; + } + + public static bool IsUnixy() => + ( + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) + ); + } +} \ No newline at end of file diff --git a/jumpfs/Extensions/EnumerableExtensions.cs b/jumpfs/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..c4e834e --- /dev/null +++ b/jumpfs/Extensions/EnumerableExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace jumpfs.Extensions +{ + public static class EnumerableExtensions + { + /// + /// Attempts to get the one and only item in a set + /// + public static bool TryGetSingle(this IEnumerable items, Func selector, out T selected) + { + selected = default; + var matches = items.Where(selector).ToArray(); + if (matches.Length != 1) + return false; + selected = matches.First(); + return true; + } + } +} \ No newline at end of file diff --git a/jumpfs/Extensions/StringExtensions.cs b/jumpfs/Extensions/StringExtensions.cs new file mode 100644 index 0000000..6f0e931 --- /dev/null +++ b/jumpfs/Extensions/StringExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace jumpfs.Extensions +{ + public static class StringExtensions + { + //split a string by spaces - useful for testing + public static string[] Tokenise(this string str) => str.Split(' ', + StringSplitOptions.TrimEntries | + StringSplitOptions.RemoveEmptyEntries + ); + + public static string WinSlash(this string str) => str.Replace("/", @"\"); + public static string UnixSlash(this string str) => str.Replace(@"\", "/"); + } +} \ No newline at end of file diff --git a/jumpfs/Names.cs b/jumpfs/Names.cs new file mode 100644 index 0000000..1fae3dc --- /dev/null +++ b/jumpfs/Names.cs @@ -0,0 +1,14 @@ +namespace jumpfs +{ + public static class Names + { + public const string Name = "name"; + public const string Path = "path"; + public const string Container = "container"; + public const string Help = "help"; + public const string Line = "line"; + public const string Column = "column"; + public const string Format = "format"; + public const string Literal = "literal"; + } +} \ No newline at end of file diff --git a/jumpfs/Program.cs b/jumpfs/Program.cs new file mode 100644 index 0000000..a459db7 --- /dev/null +++ b/jumpfs/Program.cs @@ -0,0 +1,13 @@ +using jumpfs.Commands; + +namespace jumpfs +{ + internal class Program + { + private static void Main(string[] args) + { + var context = JumpFs.CliContext(); + JumpFs.ExecuteWithContext(args, context); + } + } +} \ No newline at end of file diff --git a/jumpfs/Properties/PublishProfiles/linux.pubxml b/jumpfs/Properties/PublishProfiles/linux.pubxml new file mode 100644 index 0000000..d4dbb9d --- /dev/null +++ b/jumpfs/Properties/PublishProfiles/linux.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + ..\publish + FileSystem + net5.0 + linux-x64 + false + True + + \ No newline at end of file diff --git a/jumpfs/Properties/PublishProfiles/windows.pubxml b/jumpfs/Properties/PublishProfiles/windows.pubxml new file mode 100644 index 0000000..fdbce83 --- /dev/null +++ b/jumpfs/Properties/PublishProfiles/windows.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + ..\publish + FileSystem + net5.0 + win-x64 + false + True + True + + \ No newline at end of file diff --git a/jumpfs/Properties/launchSettings.json b/jumpfs/Properties/launchSettings.json new file mode 100644 index 0000000..2e09faf --- /dev/null +++ b/jumpfs/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "gomark": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/jumpfs/ShellType.cs b/jumpfs/ShellType.cs new file mode 100644 index 0000000..c5eb239 --- /dev/null +++ b/jumpfs/ShellType.cs @@ -0,0 +1,15 @@ +namespace jumpfs +{ + /// + /// These are the environments we currently know how to run under + /// + public enum ShellType + { + PowerShell, + Cmd, + Wsl, + + //currently Linux is treated the same as WSL + Linux + } +} \ No newline at end of file diff --git a/jumpfs/jumpfs.csproj b/jumpfs/jumpfs.csproj new file mode 100644 index 0000000..00d409c --- /dev/null +++ b/jumpfs/jumpfs.csproj @@ -0,0 +1,14 @@ + + + + Exe + net5.0 + + + + + + + + + diff --git a/jumpfs/jumpfs.v3.ncrunchproject b/jumpfs/jumpfs.v3.ncrunchproject new file mode 100644 index 0000000..b4b755c --- /dev/null +++ b/jumpfs/jumpfs.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + NetCoreNetStandardLocalSystem + + + \ No newline at end of file diff --git a/powershell/jumpfs-functions.psm1 b/powershell/jumpfs-functions.psm1 new file mode 100644 index 0000000..e5e048c --- /dev/null +++ b/powershell/jumpfs-functions.psm1 @@ -0,0 +1,40 @@ + + +function default_to_current_location($r) +{ +if ($r -eq $null) +{ + $r = (Get-Location).Path +} +$r +} + +function go($p) { + $path = (jumpfs.exe find -name $p) ; + set-location $path; + write-host (Get-Location).Path +} + +function mark($p,$r) { + + jumpfs.exe mark -name $p -path (default_to_current_location $r) +} + + +function lst($p) { jumpfs.exe list -name $p } + +function gol($p,$r) { jumpfs.exe findrelative -path $p -container (default_to_current_location $r) } + +function edit($p) { + $path = (jumpfs.exe find -name $p) ; + code --new-window -g $path +} + + +function x($p) { + $path = (jumpfs.exe find -name $p) ; + explorer $path +} + + +Export-ModuleMember -Function * \ No newline at end of file