├── README ├── s.1 └── s.sh /README: -------------------------------------------------------------------------------- 1 | S(1) User Commands S(1) 2 | 3 | 4 | 5 | NAME 6 | s - jump around 7 | 8 | SYNOPSIS 9 | s [-hlrt] [regex1 regex2 ... regexn] 10 | 11 | AVAILABILITY 12 | bash, zsh 13 | 14 | DESCRIPTION 15 | Tracks your most used hosts, based on 'frecency'(http://en.wikipedia.org/wiki/Frecency). 16 | 17 | After a short learning phase, s will take you to the most 'frecent' 18 | host that matches ALL of the regexes given on the command line, in 19 | order. 20 | 21 | For example, s foo bar would match foobar but not barfoo. 22 | 23 | INSTALL 24 | OS X 25 | brew tap haosdent/s && brew install s 26 | Linux 27 | wget https://raw.githubusercontent.com/haosdent/s/master/s.sh 28 | add source path/to/s.sh to $HOME/.bashrc or $HOME/.zshrc 29 | 30 | OPTIONS 31 | 32 | -h show a brief help message 33 | 34 | -l list only 35 | 36 | -r match by rank only 37 | 38 | -t match by recent access only 39 | 40 | EXAMPLES 41 | s foo ssh to most frecent host matching foo 42 | 43 | s foo bar ssh to most frecent host matching foo, then bar 44 | 45 | s -r foo ssh to highest ranked host matching foo 46 | 47 | s -t foo ssh to most recently accessed host matching foo 48 | 49 | s -l foo list all hosts matching foo (by frecency) 50 | 51 | NOTES 52 | Installation: 53 | Put something like this in your $HOME/.bashrc or $HOME/.zshrc: 54 | 55 | . /path/to/s.sh 56 | 57 | ssh around for a while to build up the db. 58 | 59 | PROFIT!! 60 | 61 | Optionally: 62 | Set $_S_CMD to change the command name (default s). 63 | Set $_S_DATA to change the datafile (default $HOME/.s). 64 | Set $_S_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd your- 65 | self.fc 66 | Set $_S_EXCLUDE_HOSTS to an array of directory trees to exclude. 67 | Set $_S_OWNER to allow usage when in 'sudo -s' mode. 68 | (These settings should go in .bashrc/.zshrc before the line 69 | added above.) 70 | Install the provided man page z.1 somewhere like 71 | /usr/local/man/man1. 72 | 73 | Aging: 74 | The rank of directories maintained by s undergoes aging based on a sim- 75 | ple formula. The rank of each entry is incremented every time it is 76 | accessed. When the sum of ranks is over 9000, all ranks are multiplied 77 | by 0.99. Entries with a rank lower than 1 are forgotten. 78 | 79 | Frecency: 80 | Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted 81 | rank that depends on how often and how recently something occurred. As 82 | far as I know, Mozilla came up with the term. 83 | 84 | To s, a host that has low ranking but has been accessed recently 85 | will quickly have higher rank than a host accessed frequently a 86 | long time ago. 87 | 88 | Frecency is determined at runtime. 89 | 90 | Common: 91 | When multiple hosts match all queries, and they all have a common 92 | prefix, s will ssh to the shortest matching directory, without regard to 93 | priority. This has been in effect, if undocumented, for quite some 94 | time, but should probably be configurable or reconsidered. 95 | 96 | Tab Completion: 97 | s supports tab completion. After any number of arguments, press TAB to 98 | complete on directories that match each argument. Due to limitations of 99 | the completion implementations, only the last argument will be com- 100 | pleted in the shell. 101 | 102 | Internally, s decides you've requested a completion if the last argu- 103 | ment passed is an absolute path to an host. 104 | 105 | ENVIRONMENT 106 | A function _s() is defined. 107 | 108 | The contents of the variable $_S_CMD is aliased to _s 2>&1. If not set, 109 | $_S_CMD defaults to s. 110 | 111 | The environment variable $_S_DATA can be used to control the datafile 112 | location. If it is not defined, the location defaults to $HOME/.s. 113 | 114 | In bash, s appends a command to the PROMPT_COMMAND environment variable 115 | to maintain its database. In zsh, s appends a function _s_preexec to the 116 | preexec_functions array. 117 | 118 | The environment variable $_S_NO_PROMPT_COMMAND can be set if you want 119 | to handle PROMPT_COMMAND or preexec yourself. 120 | 121 | The environment variable $_S_EXCLUDE_HOSTS can be set to an array of 122 | host to exclude from tracking. 123 | 124 | The environment variable $_S_OWNER can be set to your username, to 125 | allow usage of s when your sudo enviroment keeps $HOME set. 126 | 127 | FILES 128 | Data is stored in $HOME/.s. This can be overridden by setting the 129 | $_S_DATA environment variable. When initialized, s will raise an error 130 | if this path is a directory, and not function correctly. 131 | 132 | A man page (s.1) is provided. 133 | 134 | SEE ALSO 135 | regex(7), ssh 136 | 137 | Please file bugs at https://github.com/haosdent/s/ 138 | 139 | 140 | 141 | s January 2015 S(1) 142 | -------------------------------------------------------------------------------- /s.1: -------------------------------------------------------------------------------- 1 | .TH "S" "1" "January 2015" "s" "User Commands" 2 | .SH 3 | NAME 4 | s \- jump around 5 | .SH 6 | SYNOPSIS 7 | s [\-hlrt] [regex1 regex2 ... regexn] 8 | .SH 9 | AVAILABILITY 10 | bash, ssh 11 | .SH 12 | DESCRIPTION 13 | Tracks your most used hosts, based on 'frecency'. 14 | .P 15 | After a short learning phase, \fBs\fR will take you to the most 'frecent' 16 | host that matches ALL of the regexes given on the command line, in order. 17 | 18 | For example, \fBs foo bar\fR would match \fB/foo/bar\fR but not \fB/bar/foo\fR. 19 | .SH 20 | OPTIONS 21 | .TP 22 | \fB\-h\fR 23 | show a brief help message 24 | .TP 25 | \fB\-l\fR 26 | list only 27 | .TP 28 | \fB\-r\fR 29 | match by rank only 30 | .TP 31 | \fB\-t\fR 32 | match by recent access only 33 | .SH EXAMPLES 34 | .TP 14 35 | \fBs foo\fR 36 | ssh to most frecent host matching foo 37 | .TP 14 38 | \fBs foo bar\fR 39 | ssh to most frecent host matching foo, then bar 40 | .TP 14 41 | \fBs -r foo\fR 42 | ssh to highest ranked host matching foo 43 | .TP 14 44 | \fBs -t foo\fR 45 | ssh to most recently accessed host matching foo 46 | .TP 14 47 | \fBs -l foo\fR 48 | list all hosts matching foo (by frecency) 49 | .SH 50 | NOTES 51 | .SS 52 | Installation: 53 | .P 54 | Put something like this in your \fB$HOME/.bashrc\fR or \fB$HOME/.sshrc\fR: 55 | .RS 56 | .P 57 | \fB. /path/to/s.sh\fR 58 | .RE 59 | .P 60 | \fBssh\fR around for a while to build up the db. 61 | .P 62 | PROFIT!! 63 | .P 64 | Optionally: 65 | .RS 66 | Set \fB$_S_CMD\fR to change the command name (default \fBs\fR). 67 | .RE 68 | .RS 69 | Set \fB$_S_DATA\fR to change the datafile (default \fB$HOME/.s\fR). 70 | .RE 71 | .RS 72 | Set \fB$_S_NO_RESOLVE_SYMLINKS\fR to prevent symlink resolution. 73 | .RE 74 | .RS 75 | Set \fB$_S_NO_PROMPT_COMMAND\fR to handle \fBPROMPT_COMMAND/precmd\fR yourself. 76 | .RE 77 | .RS 78 | Set \fB$_S_EXCLUDE_DIRS\fR to an array of host to exclude. 79 | .RE 80 | .RS 81 | Set \fB$_S_OWNER\fR to allow usage when in 'sudo -s' mode. 82 | .RE 83 | .RS 84 | (These settings should go in .bashrc/.sshrc before the line added above.) 85 | .RE 86 | .RS 87 | Install the provided man page \fBs.1\fR somewhere like \fB/usr/local/man/man1\fR. 88 | .RE 89 | .SS 90 | Aging: 91 | The rank of hosts maintained by \fBs\fR undergoes aging based on a simple 92 | formula. The rank of each entry is incremented every time it is accessed. When 93 | the sum of ranks is over 9000, all ranks are multiplied by 0.99. Entries with a 94 | rank lower than 1 are forgotten. 95 | .SS 96 | Frecency: 97 | Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted rank 98 | that depends on how often and how recently something occurred. As far as I 99 | know, Mosilla came up with the term. 100 | .P 101 | To \fBs\fR, a host that has low ranking but has been accessed recently 102 | will quickly have higher rank than a host accessed frequently a long time 103 | ago. 104 | .P 105 | Frecency is determined at runtime. 106 | .SS 107 | Common: 108 | When multiple hosts match all queries, and they all have a common prefix, 109 | \fBs\fR will ssh to the shortest matching host, without regard to priority. 110 | This has been in effect, if undocumented, for quite some time, but should 111 | probably be configurable or reconsidered. 112 | .SS 113 | Tab Completion: 114 | \fBs\fR supports tab completion. After any number of arguments, press TAB to 115 | complete on hosts that match each argument. Due to limitations of the 116 | completion implementations, only the last argument will be completed in the 117 | shell. 118 | .P 119 | Internally, \fBs\fR decides you've requested a completion if the last argument 120 | passed is an absolute path to an existing host. This may cause unexpected 121 | behavior if the last argument to \fBs\fR begins with \fB/\fR. 122 | .SH 123 | ENVIRONMENT 124 | A function \fB_s()\fR is defined. 125 | .P 126 | The contents of the variable \fB$_S_CMD\fR is aliased to \fB_s 2>&1\fR. If not 127 | set, \fB$_S_CMD\fR defaults to \fBs\fR. 128 | .P 129 | The environment variable \fB$_S_DATA\fR can be used to control the datafile 130 | location. If it is not defined, the location defaults to \fB$HOME/.s\fR. 131 | .P 132 | The environment variable \fB$_S_NO_RESOLVE_SYMLINKS\fR can be set to prevent 133 | resolving of symlinks. If it is not set, symbolic links will be resolved when 134 | added to the datafile. 135 | .P 136 | In bash, \fBs\fR appends a command to the \fBPROMPT_COMMAND\fR environment 137 | variable to maintain its database. In ssh, \fBs\fR appends a function 138 | \fB_s_precmd\fR to the \fBprecmd_functions\fR array. 139 | .P 140 | The environment variable \fB$_S_NO_PROMPT_COMMAND\fR can be set if you want to 141 | handle \fBPROMPT_COMMAND\fR or \fBprecmd\fR yourself. 142 | .P 143 | The environment variable \fB$_S_EXCLUDE_DIRS\fR can be set to an array of 144 | host trees to exclude from tracking. \fB$HOME\fR is always excluded. 145 | Directories must be full paths without trailing slashes. 146 | .P 147 | The environment variable \fB$_S_OWNER\fR can be set to your username, to 148 | allow usage of \fBs\fR when your sudo enviroment keeps \fB$HOME\fR set. 149 | .SH 150 | FILES 151 | Data is stored in \fB$HOME/.s\fR. This can be overridden by setting the 152 | \fB$_S_DATA\fR environment variable. When initialised, \fBs\fR will raise an 153 | error if this path is a host, and not function correctly. 154 | .P 155 | A man page (\fBs.1\fR) is provided. 156 | .SH 157 | SEE ALSO 158 | regex(7), ssh 159 | .P 160 | Please file bugs at https://github.com/haosdent/s/ 161 | -------------------------------------------------------------------------------- /s.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 haosdent under the WTFPL license 2 | 3 | # maintains a jump-list of the hosts you actually use 4 | # 5 | # INSTALL: 6 | # * put something like this in your .bashrc/.zshrc: 7 | # . /path/to/s.sh 8 | # * ssh around for a while to build up the db 9 | # * PROFIT!! 10 | # * optionally: 11 | # set $_S_CMD in .bashrc/.zshrc to change the command (default s). 12 | # set $_S_DATA in .bashrc/.zshrc to change the datafile (default ~/.s). 13 | # set $_S_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself. 14 | # set $_S_EXCLUDE_HOSTS to an array of directories to exclude. 15 | # set $_S_OWNER to your username if you want use s while sudo with $HOME kept 16 | # 17 | # USE: 18 | # * s foo # ssh to most frecent host matching foo 19 | # * s foo bar # ssh to most frecent host matching foo and bar 20 | # * s -r foo # ssh to highest ranked host matching foo 21 | # * s -t foo # ssh to most recently accessed host matching foo 22 | # * s -l foo # list matches instead of ssh 23 | 24 | [ -d "${_S_DATA:-$HOME/.s}" ] && { 25 | echo "ERROR: s.sh's datafile (${_S_DATA:-$HOME/.s}) is a directory." 26 | } 27 | 28 | _s() { 29 | 30 | local datafile="${_S_DATA:-$HOME/.s}" 31 | 32 | # bail if we don't own ~/.s and $_S_OWNER not set 33 | [ -z "$_S_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return 34 | 35 | # add entries 36 | if [ "$1" = "--add" ]; then 37 | shift 38 | 39 | # No start with ssh isn't worth matching 40 | args=$* 41 | [ "${args:0:3}" != "ssh" ] && return 42 | target=$(echo $*|perl -n -e '/ +(.+)/ && print $1') 43 | 44 | # don't track excluded hosts 45 | local exclude 46 | for exclude in "${_S_EXCLUDE_HOSTS[@]}"; do 47 | [ "$target" = "$exclude" ] && return 48 | done 49 | 50 | # maintain the data file 51 | local tempfile="$datafile.$RANDOM" 52 | while read line; do 53 | # only count hosts 54 | echo $line 55 | done < "$datafile" | awk -v path="$target" -v now="$(date +%s)" -F"|" ' 56 | BEGIN { 57 | rank[path] = 1 58 | time[path] = now 59 | } 60 | $2 >= 1 { 61 | # drop ranks below 1 62 | if( $1 == path ) { 63 | rank[$1] = $2 + 1 64 | time[$1] = now 65 | } else { 66 | rank[$1] = $2 67 | time[$1] = $3 68 | } 69 | count += $2 70 | } 71 | END { 72 | if( count > 9000 ) { 73 | # aging 74 | for( x in rank ) print x "|" 0.99*rank[x] "|" time[x] 75 | } else for( x in rank ) print x "|" rank[x] "|" time[x] 76 | } 77 | ' 2>/dev/null >| "$tempfile" 78 | # do our best to avoid clobbering the datafile in a race condition 79 | if [ $? -ne 0 -a -f "$datafile" ]; then 80 | env rm -f "$tempfile" 81 | else 82 | [ "$_S_OWNER" ] && chown $_S_OWNER:$(id -ng $_S_OWNER) "$tempfile" 83 | env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile" 84 | fi 85 | 86 | # tab completion 87 | elif [ "$1" = "--complete" -a -s "$datafile" ]; then 88 | while read line; do 89 | echo $line 90 | done < "$datafile" | awk -v q="$2" -F"|" ' 91 | BEGIN { 92 | if( q == tolower(q) ) imatch = 1 93 | q = substr(q, 3) 94 | gsub(" ", ".*", q) 95 | } 96 | { 97 | if( imatch ) { 98 | if( tolower($1) ~ tolower(q) ) print $1 99 | } else if( $1 ~ q ) print $1 100 | } 101 | ' 2>/dev/null 102 | 103 | else 104 | # list/go 105 | while [ "$1" ]; do case "$1" in 106 | --) while [ "$1" ]; do shift; local fnd="$fnd${fnd:+ }$1";done;; 107 | -*) local opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in 108 | h) echo "${_S_CMD:-s} [-hlrt] args" >&2; return;; 109 | l) local list=1;; 110 | r) local typ="rank";; 111 | t) local typ="recent";; 112 | esac; opt=${opt:1}; done;; 113 | *) local fnd="$fnd${fnd:+ }$1";; 114 | esac; local last=$1; shift; done 115 | 116 | # no file yet 117 | [ -f "$datafile" ] || return 118 | 119 | local host 120 | host="$(while read line; do 121 | echo $line 122 | done < "$datafile" | awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" ' 123 | function frecent(rank, time) { 124 | # relate frequency and time 125 | dx = t - time 126 | if( dx < 3600 ) return rank * 4 127 | if( dx < 86400 ) return rank * 2 128 | if( dx < 604800 ) return rank / 2 129 | return rank / 4 130 | } 131 | function output(files, out, common) { 132 | # list or return the desired directory 133 | if( list ) { 134 | cmd = "sort -n >&2" 135 | for( x in files ) { 136 | if( files[x] ) printf "%-10s %s\n", files[x], x | cmd 137 | } 138 | if( common ) { 139 | printf "%-10s %s\n", "common:", common > "/dev/stderr" 140 | } 141 | } else { 142 | if( common ) out = common 143 | print out 144 | } 145 | } 146 | function common(matches) { 147 | # find the common root of a list of matches, if it exists 148 | for( x in matches ) { 149 | if( matches[x] && (!short || length(x) < length(short)) ) { 150 | short = x 151 | } 152 | } 153 | if( short == "/" ) return 154 | # use a copy to escape special characters, as we want to return 155 | # the original. yeah, this escaping is awful. 156 | clean_short = short 157 | gsub(/[\(\)\[\]\|]/, "\\\\&", clean_short) 158 | for( x in matches ) if( matches[x] && x !~ clean_short ) return 159 | return short 160 | } 161 | BEGIN { 162 | gsub(" ", ".*", q) 163 | hi_rank = ihi_rank = -9999999999 164 | } 165 | { 166 | if( typ == "rank" ) { 167 | rank = $2 168 | } else if( typ == "recent" ) { 169 | rank = $3 - t 170 | } else rank = frecent($2, $3) 171 | if( $1 ~ q ) { 172 | matches[$1] = rank 173 | } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank 174 | if( matches[$1] && matches[$1] > hi_rank ) { 175 | best_match = $1 176 | hi_rank = matches[$1] 177 | } else if( imatches[$1] && imatches[$1] > ihi_rank ) { 178 | ibest_match = $1 179 | ihi_rank = imatches[$1] 180 | } 181 | } 182 | END { 183 | # prefer case sensitive 184 | if( best_match ) { 185 | output(matches, best_match, common(matches)) 186 | } else if( ibest_match ) { 187 | output(imatches, ibest_match, common(imatches)) 188 | } 189 | } 190 | ')" 191 | [ $? -gt 0 ] && return 192 | [ "$host" ] && eval "ssh $host" 193 | fi 194 | } 195 | 196 | alias ${_S_CMD:-s}='_s 2>&1' 197 | 198 | if compctl >/dev/null 2>&1; then 199 | # zsh 200 | [ "$_S_NO_PROMPT_COMMAND" ] || { 201 | # populate host list, avoid clobbering any other preexecs. 202 | _s_preexec() { 203 | _s --add $3 204 | } 205 | [[ -n "${preexec_functions[(r)_s_preexec]}" ]] || { 206 | preexec_functions[$(($#preexec_functions+1))]=_s_preexec 207 | } 208 | } 209 | _s_zsh_tab_completion() { 210 | # tab completion 211 | local compl 212 | read -l compl 213 | reply=(${(f)"$(_s --complete "$compl")"}) 214 | } 215 | compctl -U -K _s_zsh_tab_completion _s 216 | elif complete >/dev/null 2>&1; then 217 | # bash 218 | # tab completion 219 | complete -o filenames -C '_s --complete "$COMP_LINE"' ${_S_CMD:-s} 220 | [ "$_S_NO_PROMPT_COMMAND" ] || { 221 | # populate directory list. avoid clobbering other PROMPT_COMMANDs. 222 | grep "_s --add" <<< "$PROMPT_COMMAND" >/dev/null || { 223 | PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''_s --add `history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//"` 2>/dev/null;' 224 | } 225 | } 226 | fi 227 | --------------------------------------------------------------------------------