Skip to content

Commit

Permalink
cmd/init: separate bash-preexec and savvy setup
Browse files Browse the repository at this point in the history
Savvy relies on bash-preexec.sh to setup pre_cmd and pre_exec hooks in
bash.

Users may rely on multiple other tools(atuin, starship, etc) that
install and setup bash-preexec.sh.

In such cases,  a user's bashrc may initialize bash-preexec before running: `eval $(savvy init
bash)`

In that case [bash-preexec.sh](https://github.com/getsavvyinc/savvy-cli/blob/8c6a834c5a140b83cc77c788e40738f510af7c12/cmd/setup/bash-preexec.sh#L52)
avoids duplicate inclusion and returns early.

As a result, none of Savvy's custom hooks are evaluated and user's get
an error messasge "not configured to use Savvy" when trying to
record/run runbooks.

This commit fixes the above issue by separating initializing
bash-preexec from adding savvy's shell hooks.

Fixes #118
  • Loading branch information
joshi4 committed Aug 5, 2024
1 parent 99d016f commit 137be72
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 126 deletions.
119 changes: 119 additions & 0 deletions cmd/setup/bash-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#### SAVVY CUSTOMIZATIONS ####

# Enable experimental subshell support
export __bp_enable_subshells="true"


SAVVY_INPUT_FILE=/tmp/savvy-socket

# Save the original PS1
orignal_ps1=$PS1

get_user_prompt() {
local user_prompt
# P expansion is only available in bash 4.4+
if [[ "${BASH_VERSINFO[0]}" -gt 4 ]] || (( BASH_VERSINFO[0] > 3 && BASH_VERSINFO[1] > 4)); then
user_prompt=$(printf '%s' "${PS1@P}")
else
user_prompt=""
fi
echo "${user_prompt}"
}

step_id=""
savvy_cmd_pre_exec() {
local expanded_command=""
local spaced_command=$(echo $1 | sed -e 's/\$(\([^)]*\))/$( \1 )/g' -e 's/`\(.*\)`/` \1 `/g')
local command_parts=( $spaced_command )
for part in "${command_parts[@]}"; do
if [[ "$part" =~ ^[a-zA-Z0-9_]+$ && $(type -t "$part") == "alias" ]]; then
expanded_command+=$(alias "$part" | sed -e "s/^[[:space:]]*alias $part='//" -e "s/^$part='//" -e "s/'$//")" "
else
expanded_command+="$part "
fi
done
local cmd="${expanded_command}"
local prompt=$(get_user_prompt)
step_id=""
if [[ "${SAVVY_CONTEXT}" == "record" ]] ; then
step_id=$(SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send --prompt="${prompt}" "$cmd")
fi
}

savvy_cmd_pre_cmd() {
local exit_code=$?

if [[ "${SAVVY_CONTEXT}" == "record" && "$PS1" != *'recording'* ]]; then
PS1+=$'\[\e[31m\]recording\[\e[0m\] \U1f60e '
fi

# if return code is not 0, send the return code to the server
if [[ "${SAVVY_CONTEXT}" == "record" && "${exit_code}" != "0" ]] ; then
SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send --step-id="${step_id}" --exit-code="${exit_code}"
fi
}

SAVVY_COMMANDS=()
SAVVY_RUN_CURR=""
SAVVY_NEXT_STEP=0

# Set up a function to run the next command in the runbook when the user presses C-n
savvy_runbook_runner() {
if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -le "${#SAVVY_COMMANDS[@]}" ]] ; then
next_step=$(savvy internal current)
READLINE_LINE="${next_step}"
READLINE_POINT=${#READLINE_LINE}
fi
}


savvy_run_pre_exec() {
# we want the command as it was typed in.
local cmd=$1
if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${#SAVVY_COMMANDS[@]}" ]] ; then
SAVVY_NEXT_STEP=$(savvy internal next --cmd="${cmd}")
fi
}

PROMPT_GREEN="\[$(tput setaf 2)\]"
PROMPT_BLUE="\[$(tput setaf 4)\]"
PROMPT_BOLD="\[$(tput bold)\]"
PROMPT_RED="\[$(tput setaf 1)\]"
PROMPT_RESET="\[$(tput sgr0)\]"

savvy_run_pre_cmd() {
# transorm 0 based index to 1 based index
local display_step=$((SAVVY_NEXT_STEP+1))
local size=${#SAVVY_COMMANDS[@]}

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${size}" && "${size}" -gt 0 ]] ; then
PS1="${orignal_ps1}\n${PROMPT_GREEN}[ctrl+n:get next step]${PROMPT_RESET}(running ${PROMPT_BOLD}${SAVVY_RUN_CURR} ${display_step}/${size}${PROMPT_RESET}) "
fi

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -ge "${size}" ]] ; then
# space at the end is important
PS1="${orignal_ps1}\n(${PROMPT_GREEN}done${PROMPT_RESET}"$' \U1f60e '"${PROMPT_BOLD}${SAVVY_RUN_CURR}${PROMPT_RESET})${PROMPT_GREEN}[exit/ctrl+d to exit]${PROMPT_RESET} "
fi

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${size}" ]] ; then
savvy internal set-param
fi
}


if [[ "${SAVVY_CONTEXT}" == "run" ]] ; then
mapfile -t SAVVY_COMMANDS < <(awk -F'COMMA' '{ for(i=1;i<=NF;i++) print $i }' <<< $SAVVY_RUNBOOK_COMMANDS)
SAVVY_RUN_CURR="${SAVVY_RUNBOOK_ALIAS}"

# Set up a keybinding to trigger the function
bind 'set keyseq-timeout 0'
bind -x '"\C-n":savvy_runbook_runner'

precmd_functions+=(savvy_run_pre_cmd)
preexec_functions+=(savvy_run_pre_exec)
fi;

preexec_functions+=(savvy_cmd_pre_exec)
# NOTE: If you change this function name, you must also change the corresponding check in shell/check_setup.go
# TODO: use templates to avoid the need to manually change shell checks
precmd_functions+=(savvy_cmd_pre_cmd)
121 changes: 1 addition & 120 deletions cmd/setup/bash-preexec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

# Make sure this is bash that's running and return otherwise.
# Use POSIX syntax for this line:

if [ -z "${BASH_VERSION-}" ]; then
return 1;
fi
Expand Down Expand Up @@ -380,123 +381,3 @@ __bp_install_after_session_init() {
if [[ -z "${__bp_delay_install:-}" ]]; then
__bp_install_after_session_init
fi;

#### SAVVY CUSTOMIZATIONS ####

# Enable experimental subshell support
export __bp_enable_subshells="true"


SAVVY_INPUT_FILE=/tmp/savvy-socket

# Save the original PS1
orignal_ps1=$PS1

get_user_prompt() {
local user_prompt
# P expansion is only available in bash 4.4+
if [[ "${BASH_VERSINFO[0]}" -gt 4 ]] || (( BASH_VERSINFO[0] > 3 && BASH_VERSINFO[1] > 4)); then
user_prompt=$(printf '%s' "${PS1@P}")
else
user_prompt=""
fi
echo "${user_prompt}"
}

step_id=""
savvy_cmd_pre_exec() {
local expanded_command=""
local spaced_command=$(echo $1 | sed -e 's/\$(\([^)]*\))/$( \1 )/g' -e 's/`\(.*\)`/` \1 `/g')
local command_parts=( $spaced_command )
for part in "${command_parts[@]}"; do
if [[ "$part" =~ ^[a-zA-Z0-9_]+$ && $(type -t "$part") == "alias" ]]; then
expanded_command+=$(alias "$part" | sed -e "s/^[[:space:]]*alias $part='//" -e "s/^$part='//" -e "s/'$//")" "
else
expanded_command+="$part "
fi
done
local cmd="${expanded_command}"
local prompt=$(get_user_prompt)
step_id=""
if [[ "${SAVVY_CONTEXT}" == "record" ]] ; then
step_id=$(SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send --prompt="${prompt}" "$cmd")
fi
}

savvy_cmd_pre_cmd() {
local exit_code=$?

if [[ "${SAVVY_CONTEXT}" == "record" && "$PS1" != *'recording'* ]]; then
PS1+=$'\[\e[31m\]recording\[\e[0m\] \U1f60e '
fi

# if return code is not 0, send the return code to the server
if [[ "${SAVVY_CONTEXT}" == "record" && "${exit_code}" != "0" ]] ; then
SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send --step-id="${step_id}" --exit-code="${exit_code}"
fi
}

SAVVY_COMMANDS=()
SAVVY_RUN_CURR=""
SAVVY_NEXT_STEP=0

# Set up a function to run the next command in the runbook when the user presses C-n
savvy_runbook_runner() {
if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -le "${#SAVVY_COMMANDS[@]}" ]] ; then
next_step=$(savvy internal current)
READLINE_LINE="${next_step}"
READLINE_POINT=${#READLINE_LINE}
fi
}


savvy_run_pre_exec() {
# we want the command as it was typed in.
local cmd=$1
if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${#SAVVY_COMMANDS[@]}" ]] ; then
SAVVY_NEXT_STEP=$(savvy internal next --cmd="${cmd}")
fi
}

PROMPT_GREEN="\[$(tput setaf 2)\]"
PROMPT_BLUE="\[$(tput setaf 4)\]"
PROMPT_BOLD="\[$(tput bold)\]"
PROMPT_RED="\[$(tput setaf 1)\]"
PROMPT_RESET="\[$(tput sgr0)\]"

savvy_run_pre_cmd() {
# transorm 0 based index to 1 based index
local display_step=$((SAVVY_NEXT_STEP+1))
local size=${#SAVVY_COMMANDS[@]}

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${size}" && "${size}" -gt 0 ]] ; then
PS1="${orignal_ps1}\n${PROMPT_GREEN}[ctrl+n:get next step]${PROMPT_RESET}(running ${PROMPT_BOLD}${SAVVY_RUN_CURR} ${display_step}/${size}${PROMPT_RESET}) "
fi

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -ge "${size}" ]] ; then
# space at the end is important
PS1="${orignal_ps1}\n(${PROMPT_GREEN}done${PROMPT_RESET}"$' \U1f60e '"${PROMPT_BOLD}${SAVVY_RUN_CURR}${PROMPT_RESET})${PROMPT_GREEN}[exit/ctrl+d to exit]${PROMPT_RESET} "
fi

if [[ "${SAVVY_CONTEXT}" == "run" && "${SAVVY_NEXT_STEP}" -lt "${size}" ]] ; then
savvy internal set-param
fi
}


if [[ "${SAVVY_CONTEXT}" == "run" ]] ; then
mapfile -t SAVVY_COMMANDS < <(awk -F'COMMA' '{ for(i=1;i<=NF;i++) print $i }' <<< $SAVVY_RUNBOOK_COMMANDS)
SAVVY_RUN_CURR="${SAVVY_RUNBOOK_ALIAS}"

# Set up a keybinding to trigger the function
bind 'set keyseq-timeout 0'
bind -x '"\C-n":savvy_runbook_runner'

precmd_functions+=(savvy_run_pre_cmd)
preexec_functions+=(savvy_run_pre_exec)
fi;

preexec_functions+=(savvy_cmd_pre_exec)
# NOTE: If you change this function name, you must also change the corresponding check in shell/check_setup.go
# TODO: use templates to avoid the need to manually change shell checks
precmd_functions+=(savvy_cmd_pre_cmd)
78 changes: 72 additions & 6 deletions cmd/setup/bash.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,53 @@ package setup
import (
"embed"
"fmt"
"os"
"text/template"

"github.com/spf13/cobra"
)

//go:embed bash-preexec.sh
var bashSetupScript embed.FS
//go:embed bash-preexec.sh bash-hooks.sh
var bashFiles embed.FS

const bashSetupScriptName = "bash-preexec.sh"
const bashSetupScript = `
# Savvy initialization for bash
# Function to source a file and check for errors
source_file() {
if [[ -n "$1" ]]; then
if ! source /dev/stdin <<< "$1"; then
echo "Error sourcing script" >&2
return 1
fi
else
echo "Empty script content" >&2
return 1
fi
}
# Embed bash-preexec.sh content
read -r -d '' BASH_PREEXEC_CONTENT << 'EOF'
{{.BashPreexecContent}}
EOF
# Embed bash-hooks.sh content
read -r -d '' BASH_HOOKS_CONTENT << 'EOF'
{{.BashHooksContent}}
EOF
# Source bash-preexec.sh
if ! source_file "$BASH_PREEXEC_CONTENT"; then
echo "Failed to source bash-preexec.sh" >&2
return 1
fi
# Source bash-hooks.sh
if ! source_file "$BASH_HOOKS_CONTENT"; then
echo "Failed to source bash-hooks.sh" >&2
return 1
fi
`

// initCmd represents the init command
var BashCmd = &cobra.Command{
Expand All @@ -21,11 +60,38 @@ var BashCmd = &cobra.Command{
}

func runCmd(cmd *cobra.Command, args []string) error {
content, err := bashSetupScript.ReadFile(bashSetupScriptName)

bashPreexecContent, err := bashFiles.ReadFile("bash-preexec.sh")
if err != nil {
return fmt.Errorf("failed to read bash-preexec.sh: %w", err)
}

// Read the content of bash-hooks.sh
bashHooksContent, err := bashFiles.ReadFile("bash-hooks.sh")
if err != nil {
return err
return fmt.Errorf("failed to read bash-hooks.sh: %w", err)
}

// Prepare the template data
data := struct {
BashPreexecContent string
BashHooksContent string
}{
BashPreexecContent: string(bashPreexecContent),
BashHooksContent: string(bashHooksContent),
}
fmt.Println(string(content))

// Parse the template
tmpl, err := template.New("bash_setup").Parse(bashSetupScript)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}

// Execute the template and write to stdout
if err := tmpl.Execute(os.Stdout, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

return nil
}

Expand Down

0 comments on commit 137be72

Please sign in to comment.