Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

major changes to bash_startup.in #10

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 85 additions & 247 deletions source/misc/bash_startup.in
Original file line number Diff line number Diff line change
@@ -1,257 +1,95 @@
#!/bin/bash
# This is based on "preexec.bash" but is customized for iTerm2.
# This is inspired by "preexec.bash" but is customized for iTerm2. Sourced, so
# no shebang. This module requires 2 bash features you must not otherwise be
# using: the DEBUG trap and the PROMPT_COMMAND variable.

# Note: this module requires 2 bash features which you must not otherwise be
# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. iterm2_preexec_install
# will override these and if you override one or the other this _will_ break.
osc='\033]'; cso='\007' # Beginning, ending escape sequences.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use more than one command per line. I find it harder to read.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen \007 called "CSO" before. Where does the acronym come from?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use more than one command per line. I find it harder to read.

Fair enough.

I haven't seen \007 called "CSO" before. Where does the acronym come from?

I just wanted a short variable name and couldn't think of one. It's just OSC backwards. Was kinda thinking like case esac, if fi...

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BEL is the "official" name for \007, may as well use that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. right. Probably use esc instead of osc then too?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's actually an OSC (Operating System Command). ESC + ] = OSC, per ECMA 48 and various other standards. I use the xterm docs as the most convenient almanac for this stuff: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html


# This is known to support bash3, as well as *mostly* support bash2.05b. It
# has been tested with the default shells on MacOS X 10.4 "Tiger", Ubuntu 5.10
# "Breezy Badger", Ubuntu 6.06 "Dapper Drake", and Ubuntu 6.10 "Edgy Eft".
# tmux / screen are not supported; I think this is the theoretical tmux hack?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is the correct hack.

[ $TERM = screen ] && osc='\033Ptmux;\033\033]' && cso='\007\033\\'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, It was suggested to me that this would be a better test for TERM:

["${TERM:-}" != screen*]
This would seem to handle an empty $TERM better, but I'm not sure what benefit the * confers.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: That only works on bash and zsh, not pure sh. And it needs the double brackets (i.e. [[...]]).

A posix sh version would be:

#!/bin/sh

is_tmux_or_screen=f
if [ -n "$TMUX" ]; then
  is_tmux_or_screen=t
fi

case "$TERM" in
  screen*)
    is_tmux_or_screen=t
    ;;
esac

if [ "$is_tmux_or_screen" = f ]; then
  ...
fi

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnachman

This would seem to handle an empty $TERM better, but I'm not sure what benefit the * confers.
Well, I have seen variants like screen-256color, so I get that part. But does having an empty $TERM actually happen? I suppose you can unset it, but I'd think bad things would start to happen pretty quickly.

@docwhat
You're right in that 1) its not portable (and that it would need [[ ]]), and testing $TMUX is a good idea, probably more reliable. But there's a lot of extraneous code there.

Also, while its extremely common, if [ condition ]; then is redundant, and if then's are one of the slowest structures in shell scripts. There are rare occasions when you need it but generally, (like in case), all you need is something like:

[ -z "$TMUX" ] && case "$TERM" in
  screen*) is_tmux_or_screen=t ;; # or, in our use case here, do nothing
     ''|*) is_tmux_or_screen=f ;; # or, in our case, put our commands here
esac || is_tmux_or_screen=t

which is much faster, because we don't execute additional tests if they're not needed.


orig_ps1=$PS1; prev_ps1=$PS1 # orig is the user-set PS1; prev is our last PS1.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put an iterm2_ prefix on variables like this to avoid collisions with other scripts? Or does bash scope this sanely?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. You can scope them but local isn't portable, which is the direction I'm trying to take this in.

preexec_interactive_mode= # If set: "just executed prompt; waiting for input."

# Copy screen-run variables from the remote host, if they're available.
[ -z "$iterm2_hostname" ] && iterm2_hostname=$(uname -n) # hostname -f == slow
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, this is good! What are the odds that uname -n won't equal hostname -f?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On most systems it should be identical, but uname is never supposed to query an external DNS server, I believe (which is why hostname can be slow). Potentially there might be an issue if there's a proper DNS A record but not a matching PTR record, and the server sets another, default hostname. Not sure though.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, tried this on my linux machine and was disappointed with the result:

bryan:/home/gnachman% hostname -f
bryan.dreamhost.com
bryan:/home/gnachman% uname -n
bryan

Better keep it as hostname -f.


# Saved copy of your PS1. This is used to detect if the user changes PS1
# directly. prev_ps1 will hold the last value that this script set PS1 to
# (including various custom escape sequences). orig_ps1 always holds the last
# user-set value of PS1.
orig_ps1="$PS1"
prev_ps1="$PS1"
# Usage: iterm2_set_user_var key value
iterm2_set_user_var(){
var=$1; shift; val="$*"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val needs to be passed through base64 (see the original version, this was lost)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm I had that in there, must have dropped it.

printf $osc'1337;SetUserVar=%s=%s'$cso "$var" "$val"
unset var; unset val
}

# This variable describes whether we are currently in "interactive mode";
# i.e. whether this shell has just executed a prompt and is waiting for user
# input. It documents whether the current command invoked by the trace hook is
# run interactively by the user; it's set immediately after the prompt hook,
# and unset as soon as the trace hook is run.
preexec_interactive_mode=""
# Users can write their own version of this method. It should call
# iterm2_set_user_var but not produce any other output. # [ Huh? ]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More details on how to use this at http://iterm2.com/badges.html
The idea is the user's implementation of iterm2_print_user_vars consists of calls to iterm2_set_user_var but doesn't do anything else, especially writing to stdout/stderr.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. To me iterm2_print_user_vars sounded like the opposite of iterm2_get_user_vars, i.e., how you'd access these variables. I'm having trouble with \(var)... but I'll play with it some more, I'm probably not doing it right.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's set_user_vars, not get_user_vars. I agree print would be the opposite of get :)

You should be able to do
iterm2_print_user_vars() {
iterm2_set_user_var foo bar
}

and in your profile set the badge to \(user.foo) and see the value bar, assuming iterm2_set_user_var properly base64 encodes "bar".

iterm2_print_user_vars() { :; }

# tmux and screen are not supported; even using the tmux hack to get escape
# codes passed through, ncurses interferes and the cursor isn't in the right
# place at the time it's passed through.
if ( [ x"$TERM" != xscreen ] ); then
# Default do-nothing implementation of preexec.
function preexec () {
true
}
iterm2_print_version_number() {
printf $osc'1337;ShellIntegrationVersion=1'$cso
}

# Default do-nothing implementation of precmd.
function precmd () {
true
}
iterm2_print_state_data() {
printf $osc'1337;RemoteHost=%s@%s'$cso "$USER" "$iterm2_hostname"
printf $osc'1337;CurrentDir=%s'$cso "$PWD"
iterm2_print_user_vars
}

# This function is installed as the PROMPT_COMMAND; it is invoked before each
# interactive prompt display. It sets a variable to indicate that the prompt
# was just displayed, to allow the DEBUG trap, below, to know that the next
# command is likely interactive.
function iterm2_preexec_invoke_cmd () {
local s=$?
last_hist_ent="$(history 1)";
precmd;
# This is an iTerm2 addition to try to work around a problem in the
# original preexec.bash.
# When the PS1 has command substitutions, this gets invoked for each
# substitution and each command that's run within the substitution, which
# really adds up. It would be great if we could do something like this at
# the end of this script:
# PS1="$(iterm2_prompt_prefix)$PS1($iterm2_prompt_suffix)"
# and have iterm2_prompt_prefix set a global variable that tells precmd not to
# output anything and have iterm2_prompt_suffix reset that variable.
# Unfortunately, command substitutions run in subshells and can't
# communicate to the outside world.
# Instead, we have this workaround. We save the original value of PS1 in
# $orig_ps1. Then each time this function is run (it's called from
# PROMPT_COMMAND just before the prompt is shown) it will change PS1 to a
# string without any command substitutions by doing eval on orig_ps1. At
# this point preexec_interactive_mode is still the empty string, so preexec
# won't produce output for command substitutions.

if [[ "$PS1" != "$prev_ps1" ]]
then
export orig_ps1="$PS1"
fi

# Get the value of the prompt prefix, which will change $?
local iterm2_prompt_prefix_value="$(iterm2_prompt_prefix)"

# Reset $? to its saved value, which might be used in $orig_ps1.
sh -c "exit $s"

# Set PS1 to various escape sequences, the user's preferred prompt, and more escape sequences.
export PS1="\[$iterm2_prompt_prefix_value\]$orig_ps1\[$(iterm2_prompt_suffix)\]"

# Save the value we just set PS1 to so if the user changes PS1 we'll know and we can update orig_ps1.
export prev_ps1="$PS1"
sh -c "exit $s"

# This must be the last line in this function, or else
# iterm2_preexec_invoke_exec will do its thing at the wrong time.
preexec_interactive_mode="yes";
}

# This function is installed as the DEBUG trap. It is invoked before each
# interactive prompt display. Its purpose is to inspect the current
# environment to attempt to detect if the current command is being invoked
# interactively, and invoke 'preexec' if so.
function iterm2_preexec_invoke_exec () {
if [ ! -t 1 ]
then
# We're in a piped subshell (STDOUT is not a TTY) like
# (echo -n A; sleep 1; echo -n B) | wc -c
# ...which should return "2".
return
fi
if [[ -n "$COMP_LINE" ]]
then
# We're in the middle of a completer. This obviously can't be
# an interactively issued command.
return
fi
if [[ -z "$preexec_interactive_mode" ]]
then
# We're doing something related to displaying the prompt. Let the
# prompt set the title instead of me.
return
else
# If we're in a subshell, then the prompt won't be re-displayed to put
# us back into interactive mode, so let's not set the variable back.
# In other words, if you have a subshell like
# (sleep 1; sleep 2)
# You want to see the 'sleep 2' as a set_command_title as well.
if [[ 0 -eq "$BASH_SUBSHELL" ]]
then
preexec_interactive_mode=""
fi
fi
if [[ "iterm2_preexec_invoke_cmd" == "$BASH_COMMAND" ]]
then
# Sadly, there's no cleaner way to detect two prompts being displayed
# one after another. This makes it important that PROMPT_COMMAND
# remain set _exactly_ as below in iterm2_preexec_install. Let's switch back
# out of interactive mode and not trace any of the commands run in
# precmd.

# Given their buggy interaction between BASH_COMMAND and debug traps,
# versions of bash prior to 3.1 can't detect this at all.
preexec_interactive_mode=""
return
fi

# In more recent versions of bash, this could be set via the "BASH_COMMAND"
# variable, but using history here is better in some ways: for example, "ps
# auxf | less" will show up with both sides of the pipe if we use history,
# but only as "ps auxf" if not.
hist_ent="$(history 1)";
local prev_hist_ent="${last_hist_ent}";
last_hist_ent="${hist_ent}";
if [[ "${prev_hist_ent}" != "${hist_ent}" ]]; then
local this_command="$(echo "${hist_ent}" | sed -e "s/^[ ]*[0-9]*[ ]*//g")";
else
local this_command="";
fi;

# If none of the previous checks have earlied out of this function, then
# the command is in fact interactive and we should invoke the user's
# preexec hook with the running command as an argument.
preexec "$this_command";
}

# Execute this to set up preexec and precmd execution.
function iterm2_preexec_install () {

# *BOTH* of these options need to be set for the DEBUG trap to be invoked
# in ( ) subshells. This smells like a bug in bash to me. The null stackederr
# redirections are to quiet errors on bash2.05 (i.e. OSX's default shell)
# where the options can't be set, and it's impossible to inherit the trap
# into subshells.

set -o functrace > /dev/null 2>&1
shopt -s extdebug > /dev/null 2>&1

# Finally, install the actual traps.
if ( [ x"$PROMPT_COMMAND" = x ]); then
PROMPT_COMMAND="iterm2_preexec_invoke_cmd";
else
# If there's a trailing semicolon folowed by spaces, remove it (issue 3358).
PROMPT_COMMAND="$(echo -n $PROMPT_COMMAND | sed -e 's/; *$//'); iterm2_preexec_invoke_cmd";
fi
trap 'iterm2_preexec_invoke_exec' DEBUG;
}

# -- begin iTerm2 customization

function iterm2_begin_osc {
printf "\033]"
}

function iterm2_end_osc {
printf "\007"
}

# Runs after interactively edited command but before execution
function preexec() {
iterm2_begin_osc
printf "133;C"
iterm2_end_osc
}

function iterm2_print_state_data() {
iterm2_begin_osc
printf "1337;RemoteHost=%s@%s" "$USER" "$iterm2_hostname"
iterm2_end_osc

iterm2_begin_osc
printf "1337;CurrentDir=%s" "$PWD"
iterm2_end_osc

iterm2_print_user_vars
}

# Usage: iterm2_set_user_var key value
function iterm2_set_user_var() {
iterm2_begin_osc
printf "1337;SetUserVar=%s=%s" "$1" $(printf "%s" "$2" | base64)
iterm2_end_osc
}

# Users can write their own version of this method. It should call
# iterm2_set_user_var but not produce any other output.
function iterm2_print_user_vars() {
true
}

function iterm2_prompt_prefix() {
iterm2_begin_osc
printf "133;D;\$?"
iterm2_end_osc

iterm2_print_state_data

iterm2_begin_osc
printf "133;A"
iterm2_end_osc
}

function iterm2_prompt_suffix() {
iterm2_begin_osc
printf "133;B"
iterm2_end_osc
}

function iterm2_print_version_number() {
iterm2_begin_osc
printf "1337;ShellIntegrationVersion=1"
iterm2_end_osc
}


# If hostname -f is slow on your system, set iterm2_hostname before sourcing this script.
if [ -z "$iterm2_hostname" ]; then
iterm2_hostname=$(hostname -f)
fi
iterm2_preexec_install

# This is necessary so the first command line will have a hostname and current directory.
iterm2_prompt_prefix () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with functions above, this should be named iterm2_print_prompt_prefix

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right

printf $osc'133;D;'$?$cso
iterm2_print_state_data
iterm2_print_version_number
fi
printf $osc'133;A'$cso
}

iterm2_prompt_suffix() {
printf $osc'133;B'$cso
}

preexec() {
printf $osc'133;C'$cso
}

# This function is installed as bash PROMPT_COMMAND; invoked before each
# interactive prompt display. It sets preexec_interactive_mode for DEBUG trap.
iterm2_preexec_invoke_cmd() {
status=$?
last_command="$(echo $(fc -ln | tail -1))"; (exit $status) # Reset $? back.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than $(history 1)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its basically equivalent, but it's in POSIX. In shells like dash there is no history command. fc doesn't take a number for a relative position in the history (the history numbers are absolute), but without passing an argument, -l prints the last 16 commands, hence tail. -n tells it to not prefix the history numbers; echo to get rid of leading and trailing whitespace (easier/faster than sed).


PS1="\[$(iterm2_prompt_prefix)\]$orig_ps1\[$(iterm2_prompt_suffix)\]"
prev_ps1=$PS1; (exit $status) # Save PS1 in case user changes PS1; reset $?.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prev_ps1 is assigned to but never read. If the user changes PS1 their change will get lost since orig_ps1 won't ever be changed.


preexec_interactive_mode=yes # This must be the last line in this function.
}

# This function is the DEBUG trap. It inspects the current environment, and
# if it is ready to be invoked interactively, it 'preexec's the command line.
iterm2_preexec_invoke_exec() {
[ ! -t 1 ] && return # We're in a piped subshell (STDOUT is not a TTY).
[ -n "$COMP_LINE" ] && return # We're in the middle of a completion.
[ -z "$preexec_interactive_mode" ] && return # We're displaying prompt.
[ -"$BASH_SUBSHELL"- = -"0"- ] && preexec_interactive_mode=
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a comment

[ -"iterm2_preexec_invoke_cmd"- = -"$BASH_COMMAND"- ] &&
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment for this one didn't make much sense. There's a somewhat more lucid and modern version of it here, although it's still confusing to me (maybe you'll understand it?)
https://github.com/rcaloras/bash-preexec/blob/master/bash-preexec.sh#L155

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but its definitely confusing.

preexec_interactive_mode= && return

fc=$(echo $(fc -ln | tail -1)); prev_fc=$last_fc; last_fc=$fc
[ x"$prev_fc" != x"$fc" ] && this_command=$fc || this_command=
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit of logic is of dubious value. It comes from the original bash_preexec code (http://www.twistedmatrix.com/users/glyph/preexec.bash.txt) but the github.com/rcaloras/bash-preexec version does not perform this test. Take a look at what he's doing: he avoids calling preexec with a null string, which might be good to do?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I actually didn't particularly look at this one very hard, I was too busy just removing all the if ( [ condition ]); then stuff which tests the same condition three times (and forces a fork for good measure). 😉


preexec "$this_command"
}

iterm2_preexec_install() {
[ -z "$BASH" ] && set -o functrace >/dev/null 2>&1 &&
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $BASH is unset that means the shell is not bash, I presume, but then how did we get this far? What's the right thing to do?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precisely. Again, I'm trying to make some of this reusable for other shells. I think it would be ideal if there weren't a need for a separate file for other Almquist/Korn compatible shells.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I haven't seen a Korn shell in use since the late 90s but it would be cool to support other shells, esp. if it doesn't require maintaining more versions of this script. But please add a prominent comment to the top of the file indicating which shells any change should be tested with so we don't break anything in the future.

shopt -s extdebug >/dev/null 2>&1

PROMPT_COMMAND=iterm2_preexec_invoke_cmd
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to respect the existing PROMPT_COMMAND (the comment at the top about us owning it is wrong); in doing so, please preserve the behavior noted in the original about trailing semicolons and the reference to issue 3358.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look at this. As it was written before, in everything I traced, it never actually got to that line.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something may have broken. The test case is this:
If the user had a PROMPT_COMMAND like foo; or foo prior to invoking this script, then we should set PROMPT_COMMAND to foo; iterm2_preexec_invoke_cmd

trap 'iterm2_preexec_invoke_exec' DEBUG
}

[ $TERM != screen ] && # This needs to be disabled? Won't it just, not work?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works OK in tmux integration mode but fails badly in regular tmux mode. Marks get left all over the screen. So it should be off by default but easy to turn back on for users who only use tmux integration mode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnachman Got it. Give me a bit to make some of these changes and I'll push another commit to this same PR.

case $- in # Portable way to check if we're in an interactive login shell.
*i*) iterm2_preexec_install
iterm2_print_state_data
iterm2_print_version_number
;;
*) :
;;
esac