Readme, fixes, changes

pull/184/head
Simon Let 3 years ago
parent 2b33598dde
commit 4f51e916c8
  1. 92
      README.md
  2. 4
      cmd/daemon/dump.go
  3. 38
      cmd/daemon/main.go
  4. 4
      cmd/daemon/record.go
  5. 4
      cmd/daemon/session-init.go
  6. 51
      installation.md
  7. 4
      internal/histfile/histfile.go
  8. 5
      internal/opt/opt.go
  9. 17
      internal/recio/read.go
  10. 26
      internal/searchapp/test.go
  11. 45
      roadmap.md
  12. 56
      scripts/install.sh
  13. 3
      scripts/resh-daemon-restart.sh
  14. 4
      scripts/resh-daemon-start.sh
  15. 31
      scripts/resh-daemon-stop.sh
  16. 2
      scripts/shellrc.sh
  17. 7
      scripts/test.sh
  18. 45
      troubleshooting.md

@ -19,68 +19,46 @@ Context-based replacement/enhancement for zsh and bash shell history
**Search your history by commands or arguments and get relevant results based on current directory, git repo, exit status, and device.**
## Installation
### Prerequisites
Standard stuff: `bash(4.3+)`, `curl`, `tar`, ...
MacOS: `coreutils` (`brew install coreutils`)
### Simplest installation
Run this command.
## Install with one command
```sh
curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | bash
curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh
```
### Simple installation
Run
You will need to have `curl` and `tar` installed.
```shell
git clone https://github.com/curusarn/resh.git
cd resh && scripts/rawinstall.sh
```
More options on [Installation page](./installation.md)
### Update
Check for updates and update
## Update
Once installed RESH can be updated using:
```sh
reshctl update
```
## Roadmap
[Overview of the features of the project](./roadmap.md)
## RESH SEARCH application
## Search your history
This is the most important part of this project.
TODO: redo this
RESH SEARCH app searches your history by commands. It uses device, directories, git remote, and exit status to show you relevant results first.
Draft:
See RESH in action - record a terminal video
All this context is not in the regular shell history. RESH records shell history with context to use it when searching.
Recording content:
Search your history by commands - Show searching some longer command
At first, the search application will look something like this. Some history with context and most of it without. As you can see, you can still search the history just fine.
Get results based on current context - Show getting project-specific commands
![resh search app](img/screen-resh-cli-v2-7-init.png)
Find any command - Show searching where the context brings the relevant command to the top
Eventually most of your history will have context and RESH SEARCH app will get more useful.
Start searching now - Show search in native shell histories
![resh search app](img/screen-resh-cli-v2-7.png)
Without a query, RESH SEARCH app shows you the latest history based on the current context (device, directory, git).
Press CTRL+R to search.
Say bye to weak standard history search.
![resh search app](img/screen-resh-cli-v2-7-no-query.png)
RESH SEARCH app replaces the standard reverse search - launch it using Ctrl+R.
Enable/disable the Ctrl+R keybinding:
TODO: how to enable disable keybindings
TODO: This doesn't seem like the right place for keybindings
### In-app key bindings
@ -92,36 +70,8 @@ TODO: how to enable disable keybindings
- Ctrl+G to abort and paste the current query onto the command line
- Ctrl+R to switch between RAW and NORMAL mode
### View the recorded history
FIXME: redo/update this section
Resh history is saved to: `~/.resh_history.json`
Each line is a versioned JSON that represents one executed command line.
This is how I view it `tail -f ~/.resh_history.json | jq` or `jq < ~/.resh_history.json`.
You can install `jq` using your favorite package manager or you can use other JSON parser to view the history.
![screenshot](img/screen.png)
*Recorded metadata will be reduced to only include useful information in the future.*
## Known issues
### Q: I use bash on macOS and resh doesn't work
**A:** Add line `[ -f ~/.bashrc ] && . ~/.bashrc` to your `~/.bash_profile`.
**Long Answer:** Under macOS bash shell only loads `~/.bash_profile` because every shell runs as login shell.
## Issues and ideas
Please do create issues if you encounter any problems or if you have suggestions: https://github.com/curusarn/resh/issues
## Uninstallation
## Issues & Ideas
You can uninstall this project at any time by running `rm -rf ~/.resh/`.
Find help on [Troubleshooting page](./troubleshooting.md)
You won't lose any recorded history by removing `~/.resh` directory because history is saved in `~/.resh_history.json`.
Still got an issue? Create an issue: https://github.com/curusarn/resh/issues

@ -2,7 +2,7 @@ package main
import (
"encoding/json"
"io/ioutil"
"io"
"net/http"
"github.com/curusarn/resh/internal/histfile"
@ -18,7 +18,7 @@ type dumpHandler struct {
func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sugar := h.sugar.With(zap.String("endpoint", "/dump"))
sugar.Debugw("Handling request, reading body ...")
jsn, err := ioutil.ReadAll(r.Body)
jsn, err := io.ReadAll(r.Body)
if err != nil {
sugar.Errorw("Error reading body", "error", err)
return

@ -2,7 +2,6 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@ -22,7 +21,37 @@ var version string
var commit string
var development string
const helpMsg = `ERROR: resh-daemon doesn't accept any arguments
WARNING:
You shouldn't typically need to start RESH daemon yourself.
Unless its already running, RESH daemon is started when a new terminal is opened.
RESH daemon will not start if it's already running even when you run it manually.
USAGE:
$ resh-daemon
Runs the daemon as foreground process. You can kill it with CTRL+C.
$ resh-daemon-start
Runs the daemon as background process detached from terminal.
LOGS & DEBUGGING:
Logs are located in:
${XDG_DATA_HOME}/resh/log.json (if XDG_DATA_HOME is set)
~/.local/share/resh/log.json (otherwise - more common)
A good way to see the logs as they are being produced is:
$ tail -f ~/.local/share/resh/log.json
MORE INFO:
https://github.com/curusarn/resh/
`
func main() {
if len(os.Args) > 1 {
fmt.Fprint(os.Stderr, helpMsg)
os.Exit(1)
}
config, errCfg := cfg.New()
logger, err := logger.New("daemon", config.LogLevel, development)
if err != nil {
@ -83,7 +112,7 @@ func main() {
)
}
}
err = ioutil.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0644)
err = os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0644)
if err != nil {
sugar.Fatalw("Could not create PID file",
"error", err,
@ -116,7 +145,7 @@ type daemon struct {
}
func (d *daemon) killDaemon(pidFile string) error {
dat, err := ioutil.ReadFile(pidFile)
dat, err := os.ReadFile(pidFile)
if err != nil {
d.sugar.Errorw("Reading PID file failed",
"PIDFile", pidFile,
@ -128,8 +157,7 @@ func (d *daemon) killDaemon(pidFile string) error {
return fmt.Errorf("could not parse PID file contents: %w", err)
}
d.sugar.Infow("Successfully parsed PID", "PID", pid)
cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid))
err = cmd.Run()
err = exec.Command("kill", "-SIGTERM", fmt.Sprintf("%d", pid)).Run()
if err != nil {
return fmt.Errorf("kill command finished with error: %w", err)
}

@ -2,7 +2,7 @@ package main
import (
"encoding/json"
"io/ioutil"
"io"
"net/http"
"github.com/curusarn/resh/internal/recordint"
@ -28,7 +28,7 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sugar := h.sugar.With(zap.String("endpoint", "/record"))
sugar.Debugw("Handling request, sending response, reading body ...")
w.Write([]byte("OK\n"))
jsn, err := ioutil.ReadAll(r.Body)
jsn, err := io.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {

@ -2,7 +2,7 @@ package main
import (
"encoding/json"
"io/ioutil"
"io"
"net/http"
"github.com/curusarn/resh/internal/recordint"
@ -19,7 +19,7 @@ func (h *sessionInitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sugar.Debugw("Handling request, sending response, reading body ...")
w.Write([]byte("OK\n"))
// TODO: should we somehow check for errors here?
jsn, err := ioutil.ReadAll(r.Body)
jsn, err := io.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {

@ -0,0 +1,51 @@
# Installation
## One command installation
Feel free to check the `rawinstall.sh` script before running it.
```sh
curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh
```
You will need to have `curl` and `tar` installed.
## Update
Once installed RESH can be updated using:
```sh
reshctl update
```
## Clone & Install
```sh
git clone https://github.com/curusarn/resh.git
cd resh
scripts/rawinstall.sh
```
## Build from source
:warning: Building from source is intended for development and troubleshooting.
```sh
git clone https://github.com/curusarn/resh.git
cd resh
make install
```
## Uninstallation
You can uninstall RESH by running: `rm -rf ~/.resh/`.
Restart your terminal after uninstall.
### Installed files
Binaries and shell files are in: `~/.resh/`
Recorded history, device files, and logs are in: `~/.local/share/resh/` (or `${XDG_DATA_HOME}/resh/`)
RESH config file is in: `~/.config/resh.toml`
Also check your `~/.zshrc` and `~/.bashrc`.
RESH adds a necessary line there to load itself on terminal startup.

@ -100,7 +100,7 @@ func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHi
)
history, err := h.rio.ReadAndFixFile(h.historyPath, 3)
if err != nil {
h.sugar.Panicf("Failed to read file: %w", err)
h.sugar.Fatalf("Failed to read history file: %v", err)
}
h.sugar.Infow("Resh history loaded from file",
"historyFile", h.historyPath,
@ -113,7 +113,7 @@ func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHi
h.sugar.Infow("Resh history loaded and processed",
"recordCount", len(reshCmdLines.List),
)
if useNativeHistories == false {
if !useNativeHistories {
h.bashCmdLines = reshCmdLines
h.zshCmdLines = histlist.Copy(reshCmdLines)
return

@ -1,6 +1,7 @@
package opt
import (
"fmt"
"os"
"github.com/curusarn/resh/internal/output"
@ -17,10 +18,10 @@ func HandleVersionOpts(out *output.Output, args []string, version, commit string
// and adding "more correct" variants would mean supporting more variants.
switch os.Args[1] {
case "-version":
out.Info(version)
fmt.Print(version)
os.Exit(0)
case "-revision":
out.Info(commit)
fmt.Print(commit)
os.Exit(0)
case "-requireVersion":
if len(os.Args) < 3 {

@ -22,18 +22,21 @@ func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]record.V1, error)
numErrs := len(decodeErrs)
if numErrs > maxErrors {
r.sugar.Errorw("Encountered too many decoding errors",
"corruptedRecords", numErrs,
"errorsCount", numErrs,
"individualErrors", "<Search 'Error while decoding line' to see individual errors>",
)
return nil, fmt.Errorf("encountered too many decoding errors")
return nil, fmt.Errorf("encountered too many decoding errors, last error: %w", decodeErrs[len(decodeErrs)-1])
}
if numErrs == 0 {
return recs, nil
}
// TODO: check the error messages
r.sugar.Warnw("Some history records could not be decoded - fixing resh history file by dropping them",
r.sugar.Warnw("Some history records could not be decoded - fixing RESH history file by dropping them",
"corruptedRecords", numErrs,
"lastError", decodeErrs[len(decodeErrs)-1],
"individualErrors", "<Search 'Error while decoding line' to see individual errors>",
)
fpathBak := fpath + ".bak"
r.sugar.Infow("Backing up current corrupted history file",
"historyFileBackup", fpathBak,
@ -93,13 +96,13 @@ func (r *RecIO) ReadFile(fpath string) ([]record.V1, []error, error) {
}
recs = append(recs, *rec)
}
r.sugar.Infow("Loaded resh history records",
"recordCount", len(recs),
)
if err != io.EOF {
r.sugar.Error("Error while reading file", zap.Error(err))
return recs, decodeErrs, err
}
r.sugar.Infow("Loaded resh history records",
"recordCount", len(recs),
)
return recs, decodeErrs, nil
}

@ -1,26 +0,0 @@
package searchapp
import (
"github.com/curusarn/resh/internal/histcli"
"github.com/curusarn/resh/internal/msg"
"github.com/curusarn/resh/internal/recio"
"go.uber.org/zap"
)
// LoadHistoryFromFile ...
func LoadHistoryFromFile(sugar *zap.SugaredLogger, historyPath string, numLines int) msg.CliResponse {
rio := recio.New(sugar)
recs, _, err := rio.ReadFile(historyPath)
if err != nil {
sugar.Panicf("failed to read history file: %w", err)
}
if numLines != 0 && numLines < len(recs) {
recs = recs[:numLines]
}
cliRecords := histcli.New(sugar)
for i := len(recs) - 1; i >= 0; i-- {
rec := recs[i]
cliRecords.AddRecord(&rec)
}
return msg.CliResponse{Records: cliRecords.List}
}

@ -1,45 +0,0 @@
# RESH Roadmap
| | Legend |
| --- | --- |
| :heavy_check_mark: | Implemented |
| :white_check_mark: | Implemented but I'm not happy with it |
| :x: | Not implemented |
*NOTE: Features can change in the future*
TODO: Update this
- :heavy_check_mark: Record shell history with metadata
- :heavy_check_mark: save it as JSON to `~/.resh_history.json`
- :white_check_mark: Provide an app to search the history
- :heavy_check_mark: launch with CTRL+R (enable it using `reshctl enable ctrl_r_binding_global`)
- :heavy_check_mark: search by keywords
- :heavy_check_mark: relevant results show up first based on context (host, directory, git, exit status)
- :heavy_check_mark: allow searching completely without context ("raw" mode)
- :heavy_check_mark: import and search history from before RESH was installed
- :white_check_mark: include a help with keybindings
- :x: allow listing details for individual commands
- :x: allow explicitly searching by metadata
- :heavy_check_mark: Provide a `reshctl` utility to control and interact with the project
- :heavy_check_mark: turn on/off resh key bindings
- :heavy_check_mark: zsh completion
- :heavy_check_mark: bash completion
- :x: Multi-device history
- :x: Synchronize recorded history between devices
- :x: Allow proxying history when ssh'ing into remote servers
- :x: Provide a stable API to make resh extensible
- :heavy_check_mark: Support zsh and bash
- :heavy_check_mark: Support Linux and macOS
- :white_check_mark: Require only essential prerequisite software
- :heavy_check_mark: Linux
- :white_check_mark: MacOS *(requires coreutils - `brew install coreutils`)*

@ -1,6 +1,4 @@
#!/usr/bin/env bash
# TODO: Swith to sh shebang?
#!/usr/bin/env sh
set -euo pipefail
@ -12,11 +10,11 @@ echo
echo "Checking your system ..."
printf '\e[31;1m' # red color on
reset() {
cleanup() {
printf '\e[0m' # reset
exit
}
trap reset EXIT INT TERM
trap cleanup EXIT INT TERM
# /usr/bin/zsh -> zsh
login_shell=$(echo "$SHELL" | rev | cut -d'/' -f1 | rev)
@ -35,9 +33,6 @@ fi
# TODO: Explicitly ask users if they want to enable RESH in shells
# Only offer shells with supported versions
# E.g. Enable RESH in: Zsh (your login shell), Bash, Both shells
# TODO: V3: We already partially have these checks in `reshctl doctor`
# figure out if we want to redo this in v3 or not
# the login shell logic is flawed
bash_version=$(bash -c 'echo ${BASH_VERSION}')
bash_version_major=$(bash -c 'echo ${BASH_VERSINFO[0]}')
@ -87,28 +82,13 @@ printf '\e[0m' # reset
# # shellcheck disable=2034
# read -r x
# Shutting down resh daemon ...
echo "Stopping RESH daemon ..."
pid_file="${XDG_DATA_HOME-~/.local/share}/resh/daemon.pid"
if [ ! -f "$pid_file" ]; then
# Use old pid file location
pid_file=~/.resh/resh.pid
fi
failed_to_kill() {
# Do not print error during first installation
if [ -n "${__RESH_VERSION-}" ]; then
echo "ERROR: Failed to kill the resh-daemon - maybe it wasn't running?"
fi
}
if [ -f "$pid_file" ]; then
pid=$(cat "$pid_file")
kill -SIGTERM "$pid" || failed_to_kill
rm "$pid_file"
if [ -z "${__RESH_VERSION-}" ]; then
# First installation
# Stop the daemon anyway just to be sure
# But don't output anything
./scripts/resh-daemon-stop.sh -q
else
killall -SIGTERM resh-daemon || failed_to_kill
./scripts/resh-daemon-stop.sh
fi
echo "Installing ..."
@ -143,6 +123,8 @@ cp -f submodules/bash-zsh-compat-widgets/bindfunc.sh ~/.resh/bindfunc.sh
cp -f scripts/shellrc.sh ~/.resh/shellrc
cp -f scripts/resh-daemon-start.sh ~/.resh/bin/resh-daemon-start
cp -f scripts/resh-daemon-stop.sh ~/.resh/bin/resh-daemon-stop
cp -f scripts/resh-daemon-restart.sh ~/.resh/bin/resh-daemon-restart
cp -f scripts/hooks.sh ~/.resh/
cp -f scripts/rawinstall.sh ~/.resh/
@ -161,10 +143,10 @@ if [ "$bash_ok" = 1 ]; then
fi
# Adding resh shellrc to .bashrc ...
grep -q '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\
echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH (REcycle SHell)' >> ~/.bashrc
echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.bashrc
# Adding bash-preexec to .bashrc ...
grep -q '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\
echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH (REcycle SHell)' >> ~/.bashrc
echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH' >> ~/.bashrc
fi
# Only add shell directives into zsh if it passed version checks
@ -172,11 +154,10 @@ if [ "$zsh_ok" = 1 ]; then
# Adding resh shellrc to .zshrc ...
if [ -f ~/.zshrc ]; then
grep -q '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\
echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH (REcycle SHell)' >> ~/.zshrc
echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.zshrc
fi
fi
echo "Starting RESH daemon ..."
~/.resh/bin/resh-daemon-start
printf '
@ -202,18 +183,23 @@ RESH HISTORY SEARCH
Searches your history by commands.
Device, directories, git remote, and exit status is used to display relevant results first.
At first, RESH SEARCH will use the standard shell history without context.
At first, RESH SEARCH will use bash/zsh history without context.
All history recorded from now on will have context which will be used by the RESH SEARCH.
CHECK FOR UPDATES
To check for (and install) updates use reshctl command:
To check for (and install) updates use:
$ reshctl update
'
# TODO: recorded history section would be better in github readme
printf "
RECORDED HISTORY
Your resh history will be recorded to '${XDG_DATA_HOME-~/.local/share}/resh/history.reshjson'
Look at it using e.g. following command (you might need to install jq)
$ cat ${XDG_DATA_HOME-~/.local/share}/resh/history.reshjson | sed 's/^v[^{]*{/{/' | jq .
LOGS
RESH logs to '${XDG_DATA_HOME-~/.local/share}/resh/log.json'
Logs are useful for troubleshooting issues.
"
printf '
ISSUES & FEEDBACK

@ -0,0 +1,3 @@
#!/usr/bin/env sh
resh-daemon-stop "$@"
resh-daemon-start "$@"

@ -1,4 +1,8 @@
#!/usr/bin/env sh
if [ "${1-}" != "-q" ]; then
echo "Starting RESH daemon ..."
printf "Logs are in: %s\n" "${XDG_DATA_HOME-~/.local/share}/resh/log.json"
fi
# Run daemon in background - don't block
# Redirect stdin, stdout, and stderr to /dev/null - detach all I/O
resh-daemon </dev/null >/dev/null 2>/dev/null &

@ -0,0 +1,31 @@
#!/usr/bin/env sh
failed_to_kill() {
[ "${1-}" != "-q" ] && echo "Failed to kill the RESH daemon - it probably isn't running"
}
xdg_pid() {
local path="${XDG_DATA_HOME-}"/resh/daemon.pid
[ -n "${XDG_DATA_HOME-}" ] && [ -f "$path" ] || return 1
cat "$path"
}
default_pid() {
local path=~/.local/share/resh/daemon.pid
[ -f "$path" ] || return 1
cat "$path"
}
legacy_pid() {
local path=~/.resh/resh.pid
[ -f "$path" ] || return 1
cat "$path"
}
pid=$(xdg_pid || default_pid || legacy_pid)
if [ -n "$pid" ]; then
[ "${1-}" != "-q" ] && printf "Stopping RESH daemon ... (PID: %s)\n" "$pid"
kill "$pid" || failed_to_kill
else
[ "${1-}" != "-q" ] && printf "Stopping RESH daemon ...\n"
killall -q resh-daemon || failed_to_kill
fi

@ -17,7 +17,7 @@ fi
# shellcheck disable=2155
export __RESH_VERSION=$(resh-collect -version)
resh-daemon-start
resh-daemon-start -q
[ "$(resh-config --key BindControlR)" = true ] && __resh_bind_control_R

@ -1,9 +1,6 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
# very simple tests to catch simple errors in scripts
# shellcheck disable=SC2016
[ "${BASH_SOURCE[0]}" != "scripts/test.sh" ] && echo 'Run this script using `make test`' && exit 1
for f in scripts/*.sh; do
echo "Running shellcheck on $f ..."
shellcheck "$f" --shell=bash --severity=error || exit 1
@ -14,7 +11,7 @@ for f in scripts/{shellrc,hooks}.sh; do
! zsh -n "$f" && echo "Zsh syntax check failed!" && exit 1
done
if [ "$1" == "--all" ]; then
if [ "$1" = "--all" ]; then
for sh in bash zsh; do
echo "Running functions in scripts/shellrc.sh using $sh ..."
! $sh -c ". scripts/shellrc.sh; __resh_preexec; __resh_precmd" && echo "Error while running functions!" && exit 1

@ -0,0 +1,45 @@
# Troubleshooting
## First help
Run RESH doctor to detect common issues:
```sh
reshctl doctor
```
## Restarting RESH daemon
Sometimes restarting RESH daemon can help:
```sh
resh-daemon-restart
```
Two more useful commands:
```sh
resh-daemon-start
resh-daemon-stop
```
:warning: You will get error messages in your shell when RESH daemon is not running.
## Logs
## Disabling RESH
If you have a persistent issue with RESH you can temporarily disable it.
Go to `~/.zshrc` and `~/.bashrc` and comment out following lines:
```sh
[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc
[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # bashrc only
```
The second line is bash-specific so you won't find it in `~/.zshrc`
### RESH in bash on macOS doesn't work
**A:** Add line `[ -f ~/.bashrc ] && . ~/.bashrc` to your `~/.bash_profile`.
**Long Answer:** Under macOS bash shell only loads `~/.bash_profile` because every shell runs as login shell.
Loading…
Cancel
Save