|
| 1 | +// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved. |
| 2 | +// Use of this source code is governed by the license in the LICENSE file. |
| 3 | + |
| 4 | +// Package shell detects the user's default shell and configures it to run in |
| 5 | +// Devbox. |
| 6 | +package shell |
| 7 | + |
| 8 | +import ( |
| 9 | + "bytes" |
| 10 | + "errors" |
| 11 | + "fmt" |
| 12 | + "os" |
| 13 | + "path/filepath" |
| 14 | + "strings" |
| 15 | +) |
| 16 | + |
| 17 | +type name string |
| 18 | + |
| 19 | +const ( |
| 20 | + shUnknown name = "" |
| 21 | + shBash name = "bash" |
| 22 | + shZsh name = "zsh" |
| 23 | + shKsh name = "ksh" |
| 24 | + shPosix name = "posix" |
| 25 | +) |
| 26 | + |
| 27 | +// Shell configures a user's shell to run in Devbox. |
| 28 | +type Shell struct { |
| 29 | + name name |
| 30 | + path string |
| 31 | + initFile string |
| 32 | + devboxInitFile string |
| 33 | +} |
| 34 | + |
| 35 | +// Detect attempts to determine the user's default shell. |
| 36 | +func Detect() (*Shell, error) { |
| 37 | + path := os.Getenv("SHELL") |
| 38 | + if path == "" { |
| 39 | + return nil, errors.New("unable to detect the current shell") |
| 40 | + } |
| 41 | + |
| 42 | + sh := &Shell{path: filepath.Clean(path)} |
| 43 | + base := filepath.Base(path) |
| 44 | + // Login shell |
| 45 | + if base[0] == '-' { |
| 46 | + base = base[1:] |
| 47 | + } |
| 48 | + switch base { |
| 49 | + case "bash": |
| 50 | + sh.name = shBash |
| 51 | + sh.initFile = rcfilePath(".bashrc") |
| 52 | + case "zsh": |
| 53 | + sh.name = shZsh |
| 54 | + sh.initFile = rcfilePath(".zshrc") |
| 55 | + case "ksh": |
| 56 | + sh.name = shKsh |
| 57 | + sh.initFile = rcfilePath(".kshrc") |
| 58 | + case "dash", "ash", "sh": |
| 59 | + sh.name = shPosix |
| 60 | + sh.initFile = os.Getenv("ENV") |
| 61 | + |
| 62 | + // Just make up a name if there isn't already an init file set |
| 63 | + // so we have somewhere to put a new one. |
| 64 | + if sh.initFile == "" { |
| 65 | + sh.initFile = ".shinit" |
| 66 | + } |
| 67 | + default: |
| 68 | + sh.name = shUnknown |
| 69 | + } |
| 70 | + return sh, nil |
| 71 | +} |
| 72 | + |
| 73 | +// rcfilePath returns the absolute path for an rcfile, which is usually in the |
| 74 | +// user's home directory. It doesn't guarantee that the file exists. |
| 75 | +func rcfilePath(basename string) string { |
| 76 | + home, err := os.UserHomeDir() |
| 77 | + if err != nil { |
| 78 | + return "" |
| 79 | + } |
| 80 | + return filepath.Join(home, basename) |
| 81 | +} |
| 82 | + |
| 83 | +// SetInit configures the shell to run a script at startup. The script runs |
| 84 | +// after the user's usual init files. The script's environment will contain an |
| 85 | +// ORIGINAL_PATH environment variable, which will bet set to the PATH before |
| 86 | +// the user's init files have had a chance to modify it. |
| 87 | +func (s *Shell) SetInit(script string) error { |
| 88 | + script = strings.TrimSpace(script) |
| 89 | + if script == "" { |
| 90 | + return nil |
| 91 | + } |
| 92 | + |
| 93 | + initFile, _ := os.ReadFile(s.initFile) |
| 94 | + initFile = bytes.TrimSpace(initFile) |
| 95 | + if len(initFile) > 0 { |
| 96 | + initFile = append(initFile, '\n', '\n') |
| 97 | + } |
| 98 | + |
| 99 | + buf := bytes.NewBuffer(initFile) |
| 100 | + buf.WriteString(` |
| 101 | +
|
| 102 | +# Begin Devbox Shell Hook |
| 103 | +
|
| 104 | +`) |
| 105 | + buf.WriteString(script) |
| 106 | + buf.WriteString(` |
| 107 | +
|
| 108 | +# End Devbox Shell Hook |
| 109 | +`) |
| 110 | + |
| 111 | + // We need a temp dir (as opposed to a temp file) because zsh uses |
| 112 | + // ZDOTDIR to point to a new directory containing the .zshrc. |
| 113 | + tmp, err := os.MkdirTemp("", "devbox") |
| 114 | + if err != nil { |
| 115 | + return fmt.Errorf("create temp dir for shell init file: %v", err) |
| 116 | + } |
| 117 | + devboxInitFile := filepath.Join(tmp, filepath.Base(s.initFile)) |
| 118 | + if err := os.WriteFile(devboxInitFile, buf.Bytes(), 0600); err != nil { |
| 119 | + return fmt.Errorf("write to shell init file: %v", err) |
| 120 | + } |
| 121 | + s.devboxInitFile = devboxInitFile |
| 122 | + return nil |
| 123 | +} |
| 124 | + |
| 125 | +// ExecCommand is a command that replaces the current shell with s. |
| 126 | +func (s *Shell) ExecCommand() string { |
| 127 | + if s.devboxInitFile == "" { |
| 128 | + return "exec " + s.path |
| 129 | + } |
| 130 | + |
| 131 | + switch s.name { |
| 132 | + case shBash: |
| 133 | + return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" %s --rcfile "%s"`, |
| 134 | + os.Getenv("PATH"), s.path, s.devboxInitFile) |
| 135 | + case shZsh: |
| 136 | + return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" ZDOTDIR="%s" %s`, |
| 137 | + os.Getenv("PATH"), filepath.Dir(s.devboxInitFile), s.path) |
| 138 | + case shKsh, shPosix: |
| 139 | + return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" ENV="%s" %s `, |
| 140 | + os.Getenv("PATH"), s.devboxInitFile, s.path) |
| 141 | + default: |
| 142 | + return "exec " + s.path |
| 143 | + } |
| 144 | +} |
0 commit comments