chore: bring readline in repo for easier maintenance

windows-fixes
TorchedSammy 2022-03-13 13:48:49 -04:00
parent b05cb30ed7
commit 5a2e3e4700
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
60 changed files with 9178 additions and 1 deletions

2
go.mod
View File

@ -16,6 +16,6 @@ require (
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e
replace github.com/maxlandon/readline => github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93
replace github.com/maxlandon/readline => ./readline
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10

122
readline/CHANGES.md 100644
View File

@ -0,0 +1,122 @@
## Changes
### 4.1.0
---------
Many new features and improvements in this version:
- New keybindings (working on Emacs, and in `Vim Insert Mode`):
* `CtrlW` to cut the previous word at the cursor
* `CtrlA` to go back to the beginning of the line
* `CtrlY` to paste the laste copy/paste buffer (see Registers)
* `CtrlU` to cut the whole line.
- More precise Vim iterations:
* Iterations can now be applied to some Vim actions (`y4w`, `d3b`)
- Implemented Vim registers:
* Yank/paste operations of any sort can occur and be assigned to registers.
* The default `""` register
* 10 numbered registers, to which bufffers are automatically added
* 26 lettered registers (lowercase), to which you can append with `"D` (D being the uppercase of the `"d` register)
* Triggered in Insert Mode with `Alt"` (buggy sometimes: goes back to Normal mode selecting a register, will have to fix this)
- Unified iterations and registers:
* To copy to the `d` register the next 4 words: `"d y4w`
* To append to this `d` register the cuttend end of line: `"D d$"`
* In this example, the `d` register buffer is also the buffer in the default register `""`
* You could either:
- Paste 3 times this buffer while in Normal mode: `3p`
- Paste the buffer once in Insert mode: `CtrlY`
- History completions:
* The binding for the alternative history changed to `AltR` (the normal remains `CtrlR`)
* By defaul the history filters only against the search pattern.
* If there are matches for this patten, the first occurence is insert (virtually)
* This is refreshed as the pattern changes
* `CtrlG` to exit the comps, while leaving the current candidate
* `CtrlC` to exit and delete the current candidate
- Completions:
* When a candidate is inserted virtually, `CtrlC` to abort both completions and the candidate
* Implemented global printing size: If the overall number of completions is biffer, will roll over them.
**Notes:**
* The `rl.Readline()` function dispatch has some big cases, maybe a bit of refactoring would be nice
* The way the buffer storing bytes from key strokes sometimes gives weird results (like `Alt"` for showing Vim registers)
* Some defer/cancel calls related to DelayedTabContext that should have been merged from lmorg/readline are still missing.
### 4.0.0-beta
---------
This version is the merge of [maxlandon/readline](https://github.com/maxlandon/readline)
and [lmorg/readline](https://github.com/lmorg/readline). Therefore it both integrates parts
from both libraries, but also adds a few features, with some API breaking changes (ex: completions),
thus the new 4.0.0 version. Remains a beta because maxlandon/readline code has not been thoroughly
test neither in nor of itself, and no more against `lmorg/murex`, it's main consumer until now.
#### Code
- Enhance delete/copy buffer in Vim mode
- DelayedTabContext now works with completion groups
#### Packages
- Added a `completers` package, with a default tab/hint/syntax completer working with
the [go-flags](https://github.com/jessevdk/go-flags) library.
- The `examples` package has been enhanced with a more complete -base- application code. See the wiki
#### Documentation
- Merged relevant parts of both READMEs
- Use documentation from maxlandon/readline
#### New features / bindings
- CtrlL now clears the screen and reprints the prompt
- Added evilsocket's tui colors/effects, for ease of use & integration with shell. Has not yet replaced the current `seqColor` variables everywhere though
#### Changes I'm not sure of
- is the function leftMost() in cursor.go useful ?
- is the function getCursorPos() in cursor.go useful ?
### 3.0.0
---------
- Added test (input line, prompt, correct refresh, etc)
- Added multiline support
- Added `DelayedTabContext` and `DelayedSyntaxWorker`
### 2.1.0
---------
Error returns from `readline` have been created as error a variable, which is
more idiomatic to Go than the err constants that existed previously. Currently
both are still available to use however I will be deprecating the the constants
in a latter release.
**Deprecated constants:**
```go
const (
// ErrCtrlC is returned when ctrl+c is pressed
ErrCtrlC = "Ctrl+C"
// ErrEOF is returned when ctrl+d is pressed
ErrEOF = "EOF"
)
```
**New error variables:**
```go
var (
// CtrlC is returned when ctrl+c is pressed
CtrlC = errors.New("Ctrl+C")
// EOF is returned when ctrl+d is pressed
// (this is actually the same value as io.EOF)
EOF = errors.New("EOF")
)
```
## Version Information
`readline`'s version numbers are based on Semantic Versioning. More details can
be found in the [README.md](README.md#version-information).

201
readline/LICENSE 100644
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

165
readline/README.md 100644
View File

@ -0,0 +1,165 @@
# Readline - Console library in Go
![Demo](../assets/readline-demo.gif)
*This demo GIF has been made with a Sliver project client.*
## Introduction
**This project is actually the merging of an original project (github.com/lmorg/readline) and one of its
forks (github.com/maxlandon/readline): both introductions are thus here given, in chronological order.**
#### lmorg
This project began a few years prior to this git commit history as an API for
_[murex](https://github.com/lmorg/murex)_, an alternative UNIX shell, because
I wasn't satisfied with the state of existing Go packages for readline (at that
time they were either bugger and/or poorly maintained, or lacked features I
desired). The state of things for readline in Go may have changed since then
however own package had also matured and grown to include many more features
that has arisen during the development of _murex_. So it seemed only fair to
give back to the community considering it was other peoples readline libraries
that allowed me rapidly prototype _murex_ during it's early stages of
development.
#### maxlandon
This project started out of the wish to make an enhanced console for a security tool (Sliver, see below).
There are already several readline libraries available in Go ([github.com/chzyer/readline](https://github.com/chzyer/readline),
and [github.com/lmorg/readline](https://github.com/lmorg/readline)), but being stricter readline implementations, their completion
## Features Summary
This project is not an integrated REPL/command-line application, which means it does not automatically understand nor executes any commands.
However, having been developed in a project using the CLI [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags) library,
it also includes some default utilities (completers) that are made to work with this library, which I humbly but highly recommend.
Please see the [Wiki](https://github.com/maxlandon/readline/wiki) (or the `examples/` directory) for information on how to use these utilities.
A summarized list of features supported by this library is the following:
### Input & Editing
- Vim / Emacs input and editing modes.
- Optional, live-refresh Vim status.
- Vim modes (Insert, Normal, Replace, Delete) with visual prompt Vim status indicator
- line editing using `$EDITOR` (`vi` in the example - enabled by pressing `[ESC]` followed by `[v]`)
- Vim registers (one default, 10 numbered, and 26 lettered)
- Vim iterations
- Most default keybindings you might find in Emacs-like readline. Some are still missing though
### Completion engine
- 3 types of completion categories (`Grid`, `List` and `Map`)
- Stackable, combinable completions (completion groups of any type & size can be proposed simultaneously).
- Controlable completion group sizes (if size is greater than completions, the completions will roll automatically)
- Virtual insertion of the current candidate, like in Z-shell.
- In `List` completion groups, ability to have alternative candidates (used for displaying `--long` and `-s` (short) options, with descriptions)
- Completions working anywhere in the input line (your cursor can be anywhere)
- Completions are searchable with *Ctrl-F*, like in lmorg's library.
### Prompt system & Colors
- 1-line and 2-line prompts, both being customizable.
- Functions for refreshing the prompt, with optional behavior settings.
- Optional colors (can be disabled).
### Hints & Syntax highlighting
- A hint line can be printed below the input line, with any type of information. See utilities for a default one.
- The Hint system is now refreshed depending on the cursor position as well, like completions.
- A syntax highlighting system. A default one is also available.
### Command history
- Ability to have 2 different history sources (I used this for clients connected to a server, used by a single user).
- History is searchable like completions.
- Default history is an in-memory list.
- Quick history navigation with *Up*/*Down* arrow keys in Emacs mode, and *j*/*k* keys in Vim mode.
### Utilities
- Default Tab completer, Hint formatter and Syntax highlighter provided, using [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags)
command parser to build themselves. These are in the `completers/` directory. Please look at the [Wiki page](https://github.com/maxlandon/readline/wiki)
for how to use them. Also feel free to use them as an inspiration source to make your owns.
- Colors mercilessly copied from [github.com/evilsocket/islazy/](https://github.com/evilsocket/islazy) `tui/` package.
- Also in the `completers` directory, completion functions for environment variables (using Go's std lib for getting them), and dir/file path completions.
## Installation & Usage
As usual with Go, installation:
```
go get github.com/maxlandon/readline
```
Please see either the `examples` directory, or the Wiki for detailed instructions on how to use this library.
## Documentation
The complete documentation for this library can be found in the repo's [Wiki](https://github.com/maxlandon/readline/wiki). Below is the Table of Contents:
**Getting started**
* [ Embedding readline in a project ](https://github.com/maxlandon/readline/wiki/Embedding-Readline-In-A-Project)
* [ Input Modes ](https://github.com/maxlandon/readline/wiki/Input-Modes)
**Prompt system**
* [ Setting the Prompts](https://github.com/maxlandon/readline/wiki/Prompt-Setup)
* [ Prompt Refresh ](https://github.com/maxlandon/readline/wiki/Prompt-Refresh)
**Completion Engine**
* [ Completion Groups ](https://github.com/maxlandon/readline/wiki/Completion-Groups)
* [ Completion Search ](https://github.com/maxlandon/readline/wiki/Completion-Search)
**Hint Formatter & Syntax Highlighter**
* [ Live Refresh Demonstration ](https://github.com/maxlandon/readline/wiki/Live-Refresh-Demonstration)
**Command History**
* [ Main & Alternative Sources ](https://github.com/maxlandon/readline/wiki/Main-&-Alternative-Sources)
* [ Navigation & Search ](https://github.com/maxlandon/readline/wiki/Navigation-&-Search)
#### Command & Completion utilities
* [ Interfacing with the go-flags library](https://github.com/maxlandon/readline/wiki/Interfacing-With-Go-Flags)
* [ Declaring go-flags commands](https://github.com/maxlandon/readline/wiki/Declaring-Commands)
* [ Colors/Effects Usage ](https://github.com/maxlandon/readline/wiki/Colors-&-Effects-Usage)
## Project Status & Support
Being alone working on this project and having only one lifetime (anyone able to solve this please call me), I can engage myself over the following:
- Support for any issue opened.
- Answering any questions related.
- Being available for any blame you'd like to make for my humble but passioned work. I don't mind, I need to go up.
## Version Information
* The version string will be based on Semantic Versioning. ie version numbers
will be formatted `(major).(minor).(patch)` - for example `2.0.1`
* `major` releases _will_ have breaking changes. Be sure to read CHANGES.md for
upgrade instructions
* `minor` releases will contain new APIs or introduce new user facing features
which may affect useability from an end user perspective. However `minor`
releases will not break backwards compatibility at the source code level and
nor will it break existing expected user-facing behavior. These changes will
be documented in CHANGES.md too
* `patch` releases will be bug fixes and such like. Where the code has changed
but both API endpoints and user experience will remain the same (except where
expected user experience was broken due to a bug, then that would be bumped
to either a `minor` or `major` depending on the significance of the bug and
the significance of the change to the user experience)
* Any updates to documentation, comments within code or the example code will
not result in a version bump because they will not affect the output of the
go compiler. However if this concerns you then I recommend pinning your
project to the git commit hash rather than a `patch` release
## License Information
The `readline` library is distributed under the Apache License (Version 2.0, January 2004) (http://www.apache.org/licenses/).
All the example code and documentation in `/examples`, `/completers` is public domain.
## Warmest Thanks
- The [Sliver](https://github.com/BishopFox/sliver) implant framework project, which I used as a basis to make, test and refine this library. as well as all the GIFs and documentation pictures !
- [evilsocket](https://github.com/evilsocket) for his TUI library !

128
readline/codes.go 100644
View File

@ -0,0 +1,128 @@
package readline
// Character codes
const (
charCtrlA = iota + 1
charCtrlB
charCtrlC
charEOF
charCtrlE
charCtrlF
charCtrlG
charBackspace // ISO 646
charTab
charCtrlJ
charCtrlK
charCtrlL
charCtrlM
charCtrlN
charCtrlO
charCtrlP
charCtrlQ
charCtrlR
charCtrlS
charCtrlT
charCtrlU
charCtrlV
charCtrlW
charCtrlX
charCtrlY
charCtrlZ
charEscape
charCtrlSlash // ^\
charCtrlCloseSquare // ^]
charCtrlHat // ^^
charCtrlUnderscore // ^_
charBackspace2 = 127 // ASCII 1963
)
// Escape sequences
var (
seqUp = string([]byte{27, 91, 65})
seqDown = string([]byte{27, 91, 66})
seqForwards = string([]byte{27, 91, 67})
seqBackwards = string([]byte{27, 91, 68})
seqHome = string([]byte{27, 91, 72})
seqHomeSc = string([]byte{27, 91, 49, 126})
seqEnd = string([]byte{27, 91, 70})
seqEndSc = string([]byte{27, 91, 52, 126})
seqDelete = string([]byte{27, 91, 51, 126})
seqShiftTab = string([]byte{27, 91, 90})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
seqAltR = string([]byte{27, 114}) // Used for alternative history
)
const (
seqPosSave = "\x1b[s"
seqPosRestore = "\x1b[u"
seqClearLineAfer = "\x1b[0k"
seqClearLineBefore = "\x1b[1k"
seqClearLine = "\x1b[2k"
seqClearScreenBelow = "\x1b[0J"
seqClearScreen = "\x1b[2J" // Clears screen fully
seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left
seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R"
seqCtrlLeftArrow = "\x1b[1;5D"
seqCtrlRightArrow = "\x1b[1;5C"
// seqAltQuote = "\x1b\"" // trigger registers list
)
// Text effects
const (
seqReset = "\x1b[0m"
seqBold = "\x1b[1m"
seqUnderscore = "\x1b[4m"
seqBlink = "\x1b[5m"
)
// Text colours
const (
seqFgBlack = "\x1b[30m"
seqFgRed = "\x1b[31m"
seqFgGreen = "\x1b[32m"
seqFgYellow = "\x1b[33m"
seqFgBlue = "\x1b[34m"
seqFgMagenta = "\x1b[35m"
seqFgCyan = "\x1b[36m"
seqFgWhite = "\x1b[37m"
seqFgBlackBright = "\x1b[1;30m"
seqFgRedBright = "\x1b[1;31m"
seqFgGreenBright = "\x1b[1;32m"
seqFgYellowBright = "\x1b[1;33m"
seqFgBlueBright = "\x1b[1;34m"
seqFgMagentaBright = "\x1b[1;35m"
seqFgCyanBright = "\x1b[1;36m"
seqFgWhiteBright = "\x1b[1;37m"
)
// Background colours
const (
seqBgBlack = "\x1b[40m"
seqBgRed = "\x1b[41m"
seqBgGreen = "\x1b[42m"
seqBgYellow = "\x1b[43m"
seqBgBlue = "\x1b[44m"
seqBgMagenta = "\x1b[45m"
seqBgCyan = "\x1b[46m"
seqBgWhite = "\x1b[47m"
seqBgBlackBright = "\x1b[1;40m"
seqBgRedBright = "\x1b[1;41m"
seqBgGreenBright = "\x1b[1;42m"
seqBgYellowBright = "\x1b[1;43m"
seqBgBlueBright = "\x1b[1;44m"
seqBgMagentaBright = "\x1b[1;45m"
seqBgCyanBright = "\x1b[1;46m"
seqBgWhiteBright = "\x1b[1;47m"
)
// Xterm 256 colors
const (
seqCtermFg255 = "\033[48;5;255m"
)

View File

@ -0,0 +1,144 @@
package readline
import (
"fmt"
"strconv"
"strings"
)
// initGrid - Grid display details. Called each time we want to be sure to have
// a working completion group either immediately, or later on. Generally defered.
func (g *CompletionGroup) initGrid(rl *Instance) {
// Compute size of each completion item box
tcMaxLength := 1
for i := range g.Suggestions {
if len(g.Suggestions[i]) > tcMaxLength {
tcMaxLength = len([]rune(g.Suggestions[i]))
}
}
g.tcPosX = 0
g.tcPosY = 1
g.tcOffset = 0
// Max number of columns
g.tcMaxX = GetTermWidth() / (tcMaxLength + 2)
if g.tcMaxX < 1 {
g.tcMaxX = 1 // avoid a divide by zero error
}
// Maximum number of lines
maxY := len(g.Suggestions) / g.tcMaxX
rest := len(g.Suggestions) % g.tcMaxX
if rest != 0 {
// if rest != 0 && maxY != 1 {
maxY++
}
if maxY > g.MaxLength {
g.tcMaxY = g.MaxLength
} else {
g.tcMaxY = maxY
}
}
// moveTabGridHighlight - Moves the highlighting for currently selected completion item (grid display)
func (g *CompletionGroup) moveTabGridHighlight(rl *Instance, x, y int) (done bool, next bool) {
g.tcPosX += x
g.tcPosY += y
// Columns
if g.tcPosX < 1 {
if g.tcPosY == 1 && rl.tabCompletionReverse {
g.tcPosX = 1
g.tcPosY = 0
} else {
// This is when multiple ligns, not yet on first one.
g.tcPosX = g.tcMaxX
g.tcPosY--
}
}
if g.tcPosY > g.tcMaxY {
g.tcPosY = 1
return true, true
}
// If we must move to next line in same group
if g.tcPosX > g.tcMaxX {
g.tcPosX = 1
g.tcPosY++
}
// Real max number of suggestions.
max := g.tcMaxX * g.tcMaxY
if max > len(g.Suggestions) {
max = len(g.Suggestions)
}
// We arrived at the end of suggestions. This condition can never be triggered
// while going in the reverse order, only forward, so no further checks in it.
if (g.tcMaxX*(g.tcPosY-1))+g.tcPosX > max {
return true, true
}
// In case we are reverse cycling and currently selecting the first item,
// we adjust the coordinates to point to the last item and return
// We set g.tcPosY because the printer needs to get the a candidate nonetheless.
if rl.tabCompletionReverse && g.tcPosX == 1 && g.tcPosY == 0 {
g.tcPosY = 1
return true, false
}
// By default, come back to this group for next item.
return false, false
}
// writeGrid - A grid completion string
func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
// If group title, print it and adjust offset.
if g.Name != "" {
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
rl.tcUsedY++
}
cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 2)
x := 0
y := 1
for i := range g.Suggestions {
x++
if x > g.tcMaxX {
x = 1
y++
if y > g.tcMaxY {
y--
break
} else {
comp += "\r\n"
}
}
if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) {
comp += seqCtermFg255 + seqFgBlackBright
}
comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset)
}
// Always add a newline to the group if the end if not punctuated with one
if !strings.HasSuffix(comp, "\n") {
comp += "\n"
}
// Add the equivalent of this group's size to final screen clearing.
// This is either the max allowed print size for this group, or its actual size if inferior.
if g.MaxLength < y {
rl.tcUsedY += g.MaxLength
} else {
rl.tcUsedY += y
}
return
}

View File

@ -0,0 +1,286 @@
package readline
// CompletionGroup - A group/category of items offered to completion, with its own
// name, descriptions and completion display format/type.
// The output, if there are multiple groups available for a given completion input,
// will look like ZSH's completion system.
type CompletionGroup struct {
Name string // If not nil, printed on top of the group's completions
Description string
// Candidates & related
Suggestions []string
Aliases map[string]string // A candidate has an alternative name (ex: --long, -l option flags)
Descriptions map[string]string // Items descriptions
DisplayType TabDisplayType // Map, list or normal
MaxLength int // Each group can be limited in the number of comps offered
// When this is true, the completion is inserted really (not virtually) without
// the trailing slash, if any. This is used when we want to complete paths.
TrimSlash bool
// PathSeparator - If you intend to write path completions, you can specify the path separator to use, depending on which OS you want completion for. By default, this will be set to the GOOS of the binary. This is also used internally for many things.
PathSeparator rune
// When this is true, we don't add a space after entering the candidate.
// Can be used for multi-stage completions, like URLS (scheme:// + host)
NoSpace bool
// For each group, we can define the min and max tab item length
MinTabItemLength int
MaxTabItemLength int
// Values used by the shell
tcPosX int
tcPosY int
tcMaxX int
tcMaxY int
tcOffset int
tcMaxLength int // Used when display is map/list, for determining message width
tcMaxLengthAlt int // Same as tcMaxLength but for SuggestionsAlt.
// true if we want to cycle through suggestions because they overflow MaxLength
allowCycle bool
// This is to say we are currently cycling through this group, for highlighting choice
isCurrent bool
}
// init - The completion group computes and sets all its values, and is then ready to work.
func (g *CompletionGroup) init(rl *Instance) {
// Details common to all displays
g.checkCycle(rl) // Based on the number of groups given to the shell, allows cycling or not
g.checkMaxLength(rl)
// Details specific to tab display modes
switch g.DisplayType {
case TabDisplayGrid:
g.initGrid(rl)
case TabDisplayMap:
g.initMap(rl)
case TabDisplayList:
g.initList(rl)
}
}
// updateTabFind - When searching through all completion groups (whether it be command history or not),
// we ask each of them to filter its own items and return the results to the shell for aggregating them.
// The rx parameter is passed, as the shell already checked that the search pattern is valid.
func (g *CompletionGroup) updateTabFind(rl *Instance) {
suggs := make([]string, 0)
// We perform filter right here, so we create a new completion group, and populate it with our results.
for i := range g.Suggestions {
if rl.regexSearch.MatchString(g.Suggestions[i]) {
suggs = append(suggs, g.Suggestions[i])
} else if g.DisplayType == TabDisplayList && rl.regexSearch.MatchString(g.Descriptions[g.Suggestions[i]]) {
// this is a list so lets also check the descriptions
suggs = append(suggs, g.Suggestions[i])
}
}
// We overwrite the group's items, (will be refreshed as soon as something is typed in the search)
g.Suggestions = suggs
// Finally, the group computes its new printing settings
g.init(rl)
// If we are in history completion, we directly pass to the first candidate
if rl.modeAutoFind && rl.searchMode == HistoryFind && len(g.Suggestions) > 0 {
g.tcPosY = 1
}
}
// checkCycle - Based on the number of groups given to the shell, allows cycling or not
func (g *CompletionGroup) checkCycle(rl *Instance) {
if len(rl.tcGroups) == 1 {
g.allowCycle = true
}
if len(rl.tcGroups) >= 10 {
g.allowCycle = false
}
}
// checkMaxLength - Based on the number of groups given to the shell, check/set MaxLength defaults
func (g *CompletionGroup) checkMaxLength(rl *Instance) {
// This means the user forgot to set it
if g.MaxLength == 0 {
if len(rl.tcGroups) < 5 {
g.MaxLength = 20
}
if len(rl.tcGroups) >= 5 {
g.MaxLength = 20
}
// Lists that have a alternative completions are not allowed to have
// MaxLength set, because rolling does not work yet.
if g.DisplayType == TabDisplayList {
g.MaxLength = 1000 // Should be enough not to trigger anything related.
}
}
}
// checkNilItems - For each completion group we avoid nil maps and possibly other items
func checkNilItems(groups []*CompletionGroup) (checked []*CompletionGroup) {
for _, grp := range groups {
if grp.Descriptions == nil || len(grp.Descriptions) == 0 {
grp.Descriptions = make(map[string]string)
}
if grp.Aliases == nil || len(grp.Aliases) == 0 {
grp.Aliases = make(map[string]string)
}
checked = append(checked, grp)
}
return
}
// writeCompletion - This function produces a formatted string containing all appropriate items
// and according to display settings. This string is then appended to the main completion string.
func (g *CompletionGroup) writeCompletion(rl *Instance) (comp string) {
// Avoids empty groups in suggestions
if len(g.Suggestions) == 0 {
return
}
// Depending on display type we produce the approriate string
switch g.DisplayType {
case TabDisplayGrid:
comp += g.writeGrid(rl)
case TabDisplayMap:
comp += g.writeMap(rl)
case TabDisplayList:
comp += g.writeList(rl)
}
return
}
// getCurrentCell - The completion groups computes the current cell value,
// depending on its display type and its different parameters
func (g *CompletionGroup) getCurrentCell(rl *Instance) string {
switch g.DisplayType {
case TabDisplayGrid:
// x & y coodinates + safety check
cell := (g.tcMaxX * (g.tcPosY - 1)) + g.tcOffset + g.tcPosX - 1
if cell < 0 {
cell = 0
}
if cell < len(g.Suggestions) {
return g.Suggestions[cell]
}
return ""
case TabDisplayMap:
// x & y coodinates + safety check
cell := g.tcOffset + g.tcPosY - 1
if cell < 0 {
cell = 0
}
sugg := g.Suggestions[cell]
return sugg
case TabDisplayList:
// x & y coodinates + safety check
cell := g.tcOffset + g.tcPosY - 1
if cell < 0 {
cell = 0
}
sugg := g.Suggestions[cell]
// If we are in the alt suggestions column, check key and return
if g.tcPosX == 1 {
if alt, ok := g.Aliases[sugg]; ok {
return alt
}
return sugg
}
return sugg
}
// We should never get here
return ""
}
func (g *CompletionGroup) goFirstCell() {
switch g.DisplayType {
case TabDisplayGrid:
g.tcPosX = 1
g.tcPosY = 1
case TabDisplayList:
g.tcPosX = 0
g.tcPosY = 1
g.tcOffset = 0
case TabDisplayMap:
g.tcPosX = 0
g.tcPosY = 1
g.tcOffset = 0
}
}
func (g *CompletionGroup) goLastCell() {
switch g.DisplayType {
case TabDisplayGrid:
g.tcPosY = g.tcMaxY
restX := len(g.Suggestions) % g.tcMaxX
if restX != 0 {
g.tcPosX = restX
} else {
g.tcPosX = g.tcMaxX
}
// We need to adjust the X position depending
// on the interpretation of the remainder with
// respect to the group's MaxLength.
restY := len(g.Suggestions) % g.tcMaxY
maxY := len(g.Suggestions) / g.tcMaxX
if restY == 0 && maxY > g.MaxLength {
g.tcPosX = g.tcMaxX
}
if restY != 0 && maxY > g.MaxLength-1 {
g.tcPosX = g.tcMaxX
}
case TabDisplayList:
// By default, the last item is at maxY
g.tcPosY = g.tcMaxY
// If the max length is smaller than the number
// of suggestions, we need to adjust the offset.
if len(g.Suggestions) > g.MaxLength {
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
// We do not take into account the alternative suggestions
g.tcPosX = 0
case TabDisplayMap:
// By default, the last item is at maxY
g.tcPosY = g.tcMaxY
// If the max length is smaller than the number
// of suggestions, we need to adjust the offset.
if len(g.Suggestions) > g.MaxLength {
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
// We do not take into account the alternative suggestions
g.tcPosX = 0
}
}

View File

@ -0,0 +1,259 @@
package readline
import (
"fmt"
"strconv"
"strings"
)
// initList - List display details. Because of the way alternative completions
// are handled, MaxLength cannot be set when there are alternative completions.
func (g *CompletionGroup) initList(rl *Instance) {
// We may only ever have two different
// columns: (suggestions, and alternatives)
g.tcMaxX = 2
// We make the list anyway, especially if we need to use it later
if g.Descriptions == nil {
g.Descriptions = make(map[string]string)
}
if g.Aliases == nil {
g.Aliases = make(map[string]string)
}
// Compute size of each completion item box. Group independent
g.tcMaxLength = rl.getListPad()
// Same for suggestions alt
g.tcMaxLengthAlt = 0
for i := range g.Suggestions {
if len(g.Suggestions[i]) > g.tcMaxLength {
g.tcMaxLength = len([]rune(g.Suggestions[i]))
}
}
// Max values depend on if we have alternative suggestions
if len(g.Aliases) == 0 {
g.tcMaxX = 1
} else {
g.tcMaxX = 2
}
if len(g.Suggestions) > g.MaxLength {
g.tcMaxY = g.MaxLength
} else {
g.tcMaxY = len(g.Suggestions)
}
g.tcPosX = 0
g.tcPosY = 0
g.tcOffset = 0
}
// moveTabListHighlight - Moves the highlighting for currently selected completion item (list display)
// We don't care about the x, because only can have 2 columns of selectable choices (--long and -s)
func (g *CompletionGroup) moveTabListHighlight(rl *Instance, x, y int) (done bool, next bool) {
// We dont' pass to x, because not managed by callers
g.tcPosY += x
g.tcPosY += y
// Lines
if g.tcPosY < 1 {
if rl.tabCompletionReverse {
if g.tcOffset > 0 {
g.tcPosY = 1
g.tcOffset--
} else {
return true, false
}
}
}
if g.tcPosY > g.tcMaxY {
g.tcPosY--
g.tcOffset++
}
// Once we get to the end of choices: check which column we were selecting.
if g.tcOffset+g.tcPosY > len(g.Suggestions) {
// If we have alternative options and that we are not yet
// completing them, start on top of their column
if g.tcPosX == 0 && len(g.Aliases) > 0 {
g.tcPosX++
g.tcPosY = 1
g.tcOffset = 0
return false, false
}
// Else no alternatives, return for next group.
// Reset all values, in case we pass on them again.
g.tcPosX = 0 // First column
g.tcPosY = 1 // first row
g.tcOffset = 0
return true, true
}
// Here we must check, in x == 1, that the current choice
// is not empty. Handle for both reverse and forward movements.
sugg := g.Suggestions[g.tcPosY-1]
_, ok := g.Aliases[sugg]
if !ok && g.tcPosX == 1 {
if rl.tabCompletionReverse {
for i := len(g.Suggestions[:g.tcPosY-1]); i > 0; i-- {
su := g.Suggestions[i]
if _, ok := g.Aliases[su]; ok {
g.tcPosY -= (len(g.Suggestions[:g.tcPosY-1])) - i
return false, false
}
}
g.tcPosX = 0
g.tcPosY = g.tcMaxY
} else {
for i, su := range g.Suggestions[g.tcPosY-1:] {
if _, ok := g.Aliases[su]; ok {
g.tcPosY += i
return false, false
}
}
}
}
// Setup offset if needs to be.
// TODO: should be rewrited to conditionally process rolling menus with alternatives
if g.tcOffset+g.tcPosY < 1 && len(g.Suggestions) > 0 {
g.tcPosY = g.tcMaxY
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
if g.tcOffset < 0 {
g.tcOffset = 0
}
// MIGHT BE NEEDED IF PROBLEMS WIHT ROLLING COMPLETIONS
// ------------------------------------------------------------------------------
// Once we get to the end of choices: check which column we were selecting.
// We use +1 because we may have a single suggestion, and we just want "a ratio"
// if g.tcOffset+g.tcPosY > len(g.Suggestions) {
//
// // If we have alternative options and that we are not yet
// // completing them, start on top of their column
// if g.tcPosX == 1 && len(g.SuggestionsAlt) > 0 {
// g.tcPosX++
// g.tcPosY = 1
// g.tcOffset = 0
// return false
// }
//
// // Else no alternatives, return for next group.
// g.tcPosY = 1
// return true
// }
return false, false
}
// writeList - A list completion string
func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
// Print group title and adjust offset if there is one.
if g.Name != "" {
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
rl.tcUsedY++
}
termWidth := GetTermWidth()
if termWidth < 20 {
// terminal too small. Probably better we do nothing instead of crash
// We are more conservative than lmorg, and push it to 20 instead of 10
return
}
// Suggestion cells dimensions
maxLength := g.tcMaxLength
if maxLength > termWidth-9 {
maxLength = termWidth - 9
}
cellWidth := strconv.Itoa(maxLength)
// Alternative suggestion cells dimensions
maxLengthAlt := g.tcMaxLengthAlt + 2
if maxLengthAlt > termWidth-9 {
maxLengthAlt = termWidth - 9
}
cellWidthAlt := strconv.Itoa(maxLengthAlt)
// Descriptions cells dimensions
maxDescWidth := termWidth - maxLength - maxLengthAlt - 4
// function highlights the cell depending on current selector place.
highlight := func(y int, x int) string {
if y == g.tcPosY && x == g.tcPosX && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright
}
return ""
}
// For each line in completions
y := 0
for i := g.tcOffset; i < len(g.Suggestions); i++ {
y++ // Consider next item
if y > g.tcMaxY {
break
}
// Main suggestion
item := g.Suggestions[i]
if len(item) > maxLength {
item = item[:maxLength-3] + "..."
}
sugg := fmt.Sprintf("\r%s%-"+cellWidth+"s", highlight(y, 0), item)
// Alt suggestion
alt, ok := g.Aliases[item]
if ok {
alt = fmt.Sprintf(" %s%"+cellWidthAlt+"s", highlight(y, 1), alt)
} else {
// Else, make an empty cell
alt = strings.Repeat(" ", maxLengthAlt+1) // + 2 to keep account of spaces
}
// Description
description := g.Descriptions[g.Suggestions[i]]
if len(description) > maxDescWidth {
description = description[:maxDescWidth-3] + "..." + RESET + "\n"
} else {
description += "\n"
}
// Total completion line
comp += sugg + seqReset + alt + " " + seqReset + description
}
// Add the equivalent of this group's size to final screen clearing
// Can be set and used only if no alterative completions have been given.
if len(g.Aliases) == 0 {
if len(g.Suggestions) > g.MaxLength {
rl.tcUsedY += g.MaxLength
} else {
rl.tcUsedY += len(g.Suggestions)
}
} else {
rl.tcUsedY += len(g.Suggestions)
}
return
}
func (rl *Instance) getListPad() (pad int) {
for _, group := range rl.tcGroups {
if group.DisplayType == TabDisplayList {
for i := range group.Suggestions {
if len(group.Suggestions[i]) > pad {
pad = len([]rune(group.Suggestions[i]))
}
}
}
}
return
}

View File

@ -0,0 +1,140 @@
package readline
import (
"fmt"
"strconv"
)
// initMap - Map display details. Called each time we want to be sure to have
// a working completion group either immediately, or later on. Generally defered.
func (g *CompletionGroup) initMap(rl *Instance) {
// We make the map anyway, especially if we need to use it later
if g.Descriptions == nil {
g.Descriptions = make(map[string]string)
}
// Compute size of each completion item box. Group independent
g.tcMaxLength = 1
for i := range g.Suggestions {
if len(g.Descriptions[g.Suggestions[i]]) > g.tcMaxLength {
g.tcMaxLength = len(g.Descriptions[g.Suggestions[i]])
}
}
g.tcPosX = 0
g.tcPosY = 0
g.tcOffset = 0
// Number of lines allowed to be printed for group
if len(g.Suggestions) > g.MaxLength {
g.tcMaxY = g.MaxLength
} else {
g.tcMaxY = len(g.Suggestions)
}
}
// moveTabMapHighlight - Moves the highlighting for currently selected completion item (map display)
func (g *CompletionGroup) moveTabMapHighlight(rl *Instance, x, y int) (done bool, next bool) {
g.tcPosY += x
g.tcPosY += y
// Lines
if g.tcPosY < 1 {
if rl.tabCompletionReverse {
if g.tcOffset > 0 {
g.tcPosY = 1
g.tcOffset--
} else {
return true, false
}
}
}
if g.tcPosY > g.tcMaxY {
g.tcPosY--
g.tcOffset++
}
if g.tcOffset+g.tcPosY < 1 && len(g.Suggestions) > 0 {
g.tcPosY = g.tcMaxY
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
if g.tcOffset < 0 {
g.tcOffset = 0
}
if g.tcOffset+g.tcPosY > len(g.Suggestions) {
g.tcOffset--
return true, true
}
return false, false
}
// writeMap - A map or list completion string
func (g *CompletionGroup) writeMap(rl *Instance) (comp string) {
if g.Name != "" {
// Print group title (changes with line returns depending on type)
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
rl.tcUsedY++
}
termWidth := GetTermWidth()
if termWidth < 20 {
// terminal too small. Probably better we do nothing instead of crash
// We are more conservative than lmorg, and push it to 20 instead of 10
return
}
// Set all necessary dimensions
maxLength := g.tcMaxLength
if maxLength > termWidth-9 {
maxLength = termWidth - 9
}
maxDescWidth := termWidth - maxLength - 4
cellWidth := strconv.Itoa(maxLength)
itemWidth := strconv.Itoa(maxDescWidth)
y := 0
// Highlighting function
highlight := func(y int) string {
if y == g.tcPosY && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright
}
return ""
}
// String formating
var item, description string
for i := g.tcOffset; i < len(g.Suggestions); i++ {
y++ // Consider new item
if y > g.tcMaxY {
break
}
item = g.Suggestions[i]
if len(item) > maxDescWidth {
item = item[:maxDescWidth-3] + "..."
}
description = g.Descriptions[g.Suggestions[i]]
if len(description) > maxLength {
description = description[:maxLength-3] + "..."
}
comp += fmt.Sprintf("\r%-"+cellWidth+"s %s %-"+itemWidth+"s %s\n",
description, highlight(y), item, seqReset)
}
// Add the equivalent of this group's size to final screen clearing
if len(g.Suggestions) > g.MaxLength {
rl.tcUsedY += g.MaxLength
} else {
rl.tcUsedY += len(g.Suggestions)
}
return
}

View File

@ -0,0 +1,23 @@
package completers
import (
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// CompleteCommandArguments - Completes all values for arguments to a command.
// Arguments here are different from command options (--option).
// Many categories, from multiple sources in multiple contexts
func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
// the prefix is the last word, by default
prefix = lastWord
// SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------
// found := argumentByName(cmd, arg)
// var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
return
}

View File

@ -0,0 +1,124 @@
package completers
import (
"os"
"strings"
"github.com/maxlandon/readline"
)
// completeEnvironmentVariables - Returns all environment variables as suggestions
func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) {
// Check if last input is made of several different variables
allVars := strings.Split(lastWord, "/")
lastVar := allVars[len(allVars)-1]
var evaluated = map[string]string{}
grp := &readline.CompletionGroup{
Name: "console OS environment",
MaxLength: 5, // Should be plenty enough
DisplayType: readline.TabDisplayGrid,
TrimSlash: true, // Some variables can be paths
}
for k, v := range clientEnv {
if strings.HasPrefix("$"+k, lastVar) {
grp.Suggestions = append(grp.Suggestions, "$"+k+"/")
evaluated[k] = v
}
}
completions = append(completions, grp)
return lastVar, completions
}
// clientEnv - Contains all OS environment variables, client-side.
// This is used for things like downloading/uploading files from localhost, etc.,
// therefore we need completion and parsing stuff, sometimes.
var clientEnv = map[string]string{}
// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values.
func ParseEnvironmentVariables(args []string) (processed []string, err error) {
for _, arg := range args {
// Anywhere a $ is assigned means there is an env variable
if strings.Contains(arg, "$") || strings.Contains(arg, "~") {
//Split in case env is embedded in path
envArgs := strings.Split(arg, "/")
// If its not a path
if len(envArgs) == 1 {
processed = append(processed, handleCuratedVar(arg))
}
// If len of the env var split is > 1, its a path
if len(envArgs) > 1 {
processed = append(processed, handleEmbeddedVar(arg))
}
} else if arg != "" && arg != " " {
// Else, if arg is not an environment variable, return it as is
processed = append(processed, arg)
}
}
return
}
// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached
func handleCuratedVar(arg string) (value string) {
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
envVar := strings.TrimPrefix(arg, "$")
val, ok := clientEnv[envVar]
if !ok {
return envVar
}
return val
}
if arg != "" && arg == "~" {
return clientEnv["HOME"]
}
return arg
}
// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination
func handleEmbeddedVar(arg string) (value string) {
envArgs := strings.Split(arg, "/")
var path []string
for _, arg := range envArgs {
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
envVar := strings.TrimPrefix(arg, "$")
val, ok := clientEnv[envVar]
if !ok {
// Err will be caught when command is ran anyway, or completion will stop...
path = append(path, arg)
}
path = append(path, val)
} else if arg != "" && arg == "~" {
path = append(path, clientEnv["HOME"])
} else if arg != " " && arg != "" {
path = append(path, arg)
}
}
return strings.Join(path, "/")
}
// loadClientEnv - Loads all user environment variables
func loadClientEnv() error {
env := os.Environ()
for _, kv := range env {
key := strings.Split(kv, "=")[0]
value := strings.Split(kv, "=")[1]
clientEnv[key] = value
}
return nil
}

View File

@ -0,0 +1,180 @@
package completers
import (
"strings"
"github.com/jessevdk/go-flags"