Skip to content

Commit

Permalink
Attempt to add support for zsh
Browse files Browse the repository at this point in the history
  • Loading branch information
saheljalal committed Sep 6, 2019
1 parent e97c53c commit 4cdf991
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 46 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

nostromo is a CLI to manage aliases through simple commands to add and remove scoped aliases and substitutions.

Managing aliases can be tedious and difficult to set up. nostromo makes this process easy and reliable. The tool adds shortcuts to your `.bashrc` that call into the nostromo binary. It reads and manages all aliases within its manifest. This is used to find and execute the actual command as well as swap any substitutions to simplify calls.
Managing aliases can be tedious and difficult to set up. nostromo makes this process easy and reliable. The tool adds shortcuts to your `.bashrc` / `.zshrc` that call into the nostromo binary. It reads and manages all aliases within its manifest. This is used to find and execute the actual command as well as swap any substitutions to simplify calls.

nostromo can potentially help you build complex tools in a declarative way. Tools commonly allow you to run multi-level commands like `git rebase master branch` or `docker rmi b750fe78269d` which seem clear to use. Imagine if you could wrap your aliases / commands / workflow into custom commands that describe things you do often.

Expand All @@ -31,7 +31,7 @@ The possibilities are endless and up to your imagination with the ability to com

### Prerequisites
1. A working `go` installation with `GOPATH` and `PATH` set to run installed binaries
2. Works for MacOS and `bash` shell (other combinations untested)
2. Works for MacOS and `bash` / `zsh` shells (other combinations untested)

### Installation
```sh
Expand All @@ -58,13 +58,13 @@ Aliases to commands is one of the core functionalities provided by nostromo. Ins

**Notes**
- *Commands are executed in a child process, so aliases cannot modify the parent shell at this time.*
- *Commands won't take effect until you open a new shell or `source` your `.bashrc` again.*
- *Commands won't take effect until you open a new shell or `source` your `.bashrc` / `.zshrc` again.*

To add an alias (or command in nostromo parlance), simply run:
```sh
nostromo add cmd foo "echo doo"
```
Re-source your `.bashrc` and just like that you can run `foo` like any other alias.
Re-source your `.bashrc` / `.zshrc` and just like that you can run `foo` like any other alias.

#### Keypaths
nostromo uses the concept of keypaths to simplify building commands and accessing the command tree. A keypath is simply a `.` delimited string that represents the path to the command.
Expand Down Expand Up @@ -123,9 +123,10 @@ nostromo manifest show
```

### Bash Completion
nostromo provides bash completion scripts to allow tab completion. You can get this by adding this to your shell init file:
nostromo provides completion scripts to allow tab completion. You can get this by adding this to your shell init file:
```sh
eval "$(nostromo completion)"
eval "$(nostromo completion)" # for bash
eval "$(nostromo completion --zsh)" # for zsh
```

## Credits
Expand Down
75 changes: 60 additions & 15 deletions cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,79 @@ var writeCompletion bool
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generates bash completion scripts",
Short: "Generates shell completion scripts",
Long: `To load completion now, run
eval "$(nostromo completion)"
eval "$(nostromo completion)" # for bash
eval "$(nostromo completion --zsh)" # for zsh
To configure your bash shell to load completions for each session add to your shell init files
To configure your shell to load completions for each session add to your init files
# In ~/.bashrc or ~/.bash_profile
eval "$(nostromo completion)"`,
eval "$(nostromo completion)"
# In ~/.zshrc
eval "$(nostromo completion --zsh)"`,
Run: func(cmd *cobra.Command, args []string) {
if writeCompletion {
err := rootCmd.GenBashCompletionFile(pathutil.Abs("~/.nostromo/completion"))
if err != nil {
log.Error(err)
} else {
log.Highlight("bash completion script written to ~/.nostromo/completion")
}
writeCompletionFile()
} else {
err := rootCmd.GenBashCompletion(os.Stdout)
if err != nil {
log.Error(err)
}
printCompletion()
}
},
}

func init() {
rootCmd.AddCommand(completionCmd)

completionCmd.Flags().BoolVarP(&writeCompletion, "write", "w", false, "Write bash completion to file")
completionCmd.Flags().BoolVarP(&writeCompletion, "write", "w", false, "Write completions to file")
completionCmd.Flags().BoolVarP(&useZsh, "zsh", "z", false, "Generate completions for zsh")
}

func writeCompletionFile() {
if useZsh {
writeZshCompletionFile()
} else {
writeBashCompletionFile()
}
}

func writeBashCompletionFile() {
err := rootCmd.GenBashCompletionFile(pathutil.Abs("~/.nostromo/completion"))
if err != nil {
log.Error(err)
} else {
log.Highlight("bash completion script written to ~/.nostromo/completion")
}
}

func writeZshCompletionFile() {
err := rootCmd.GenZshCompletionFile(pathutil.Abs("~/.nostromo/zcompletion"))
if err != nil {
log.Error(err)
} else {
log.Highlight("zsh completion script written to ~/.nostromo/zcompletion")
}
}

func printCompletion() {
if useZsh {
printZshCompletion()
} else {
printBashCompletion()
}
}

func printBashCompletion() {
err := rootCmd.GenBashCompletion(os.Stdout)
if err != nil {
log.Error(err)
}
}

func printZshCompletion() {
err := rootCmd.GenZshCompletion(os.Stdout)
if err != nil {
log.Error(err)
}
}
6 changes: 5 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"github.com/spf13/cobra"
)

var useZsh bool

// runCmd represents the run command
var runCmd = &cobra.Command{
Use: "run [command] [args]",
Expand Down Expand Up @@ -38,10 +40,12 @@ commands that need to be run across the scope of the command.`,
Args: cobra.MinimumNArgs(1),
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
task.Run(args)
task.Run(args, useZsh)
},
}

func init() {
rootCmd.AddCommand(runCmd)

runCmd.Flags().BoolVarP(&useZsh, "zsh", "z", false, "Use zsh for shell exec")
}
46 changes: 33 additions & 13 deletions shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,26 @@ import (
"github.com/pokanop/nostromo/model"
)

// Shell type
type Shell int

// Supported shells
const (
Bash Shell = iota
Zsh
)

// Run a command on the shell
func Run(command string) error {
func Run(command string, shell Shell) error {
if len(command) == 0 {
return fmt.Errorf("cannot run empty command")
}

command = strings.TrimSuffix(command, "\n")

cmd := exec.Command("bash", "-ic", command)
name, args := buildExecArgs(shell, command)
cmd := exec.Command(name, args...)

cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
Expand All @@ -36,18 +47,20 @@ func Run(command string) error {
// with manifest's commands.
func Commit(manifest *model.Manifest) error {
initFiles := loadStartupFiles()
p := preferredStartupFile(initFiles)
if p == nil {
prefFiles := preferredStartupFiles(initFiles)
if len(prefFiles) == 0 {
return fmt.Errorf("could not find preferred init file")
}

// Forget previous aliases
p.reset()
for _, p := range prefFiles {
// Forget previous aliases
p.reset()

// Since nostromo works by aliasing only the top level commands,
// iterate the manifest's list and update.
for _, cmd := range manifest.Commands {
p.add(cmd.Alias)
// Since nostromo works by aliasing only the top level commands,
// iterate the manifest's list and update.
for _, cmd := range manifest.Commands {
p.add(cmd.Alias)
}
}

for _, f := range initFiles {
Expand All @@ -63,10 +76,17 @@ func Commit(manifest *model.Manifest) error {
// InitFileLines returns the shell initialization file lines
func InitFileLines() (string, error) {
initFiles := loadStartupFiles()
p := preferredStartupFile(initFiles)
if p == nil {
prefFiles := preferredStartupFiles(initFiles)
if len(prefFiles) == 0 {
return "", fmt.Errorf("could not find preferred init file")
}

return p.makeAliasBlock(), nil
return prefFiles[0].makeAliasBlock(), nil
}

func buildExecArgs(sh Shell, cmd string) (string, []string) {
if sh == Zsh {
return "zsh", []string{"-ic", cmd}
}
return "bash", []string{"-ic", cmd}
}
21 changes: 12 additions & 9 deletions shell/startupfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
)

var (
startupFileNames = []string{".profile", ".bash_profile", ".bashrc"}
startupFileNames = []string{".profile", ".bash_profile", ".bashrc", ".zshrc"}
)

type startupFile struct {
Expand Down Expand Up @@ -48,16 +48,14 @@ func loadStartupFiles() []*startupFile {
return files
}

func preferredStartupFile(files []*startupFile) *startupFile {
if len(files) == 0 {
return nil
}
func preferredStartupFiles(files []*startupFile) []*startupFile {
p := []*startupFile{}
for _, s := range files {
if s.preferred {
return s
p = append(p, s)
}
}
return nil
return p
}

func findStartupFile(name string) (string, os.FileMode, error) {
Expand Down Expand Up @@ -102,7 +100,7 @@ func newStartupFile(path, content string, mode os.FileMode) *startupFile {
mode: mode,
content: content,
aliases: []string{},
preferred: strings.Contains(path, ".bashrc"),
preferred: strings.Contains(path, ".bashrc") || strings.Contains(path, ".zshrc"),
}
}

Expand Down Expand Up @@ -181,9 +179,14 @@ func (s *startupFile) makeAliasBlock() string {
return ""
}

zsh := ""
if strings.Contains(s.path, "zsh") {
zsh = "--zsh"
}

aliases := []string{}
for _, a := range s.aliases {
alias := fmt.Sprintf("alias %s='nostromo run %s \"$*\"'", a, a)
alias := strings.TrimSpace(fmt.Sprintf("alias %s='nostromo run %s \"$*\"' %s", a, a, zsh))
aliases = append(aliases, alias)
}
return fmt.Sprintf("\n%s\n%s\n%s\n", beginBlockComment, strings.Join(aliases, "\n"), endBlockComment)
Expand Down
8 changes: 6 additions & 2 deletions task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func RemoveSubstitution(keyPath, alias string) {
}

// Run a command from the manifest
func Run(args []string) {
func Run(args []string, useZsh bool) {
cfg := checkConfig()
if cfg == nil {
return
Expand All @@ -212,7 +212,11 @@ func Run(args []string) {
log.Debug("executing:", cmd)
}

err = shell.Run(cmd)
sh := shell.Bash
if useZsh {
sh = shell.Zsh
}
err = shell.Run(cmd, sh)
if err != nil {
log.Error(err)
}
Expand Down

0 comments on commit 4cdf991

Please sign in to comment.