/
bashrc
643 lines (554 loc) · 20.9 KB
/
bashrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
#!/usr/bin/env bash
# The user profile and bashrc contain circular references to one another
# because different forms of accessing the system lead to different loading
# behaviors (an "*" indicates only part of the file is executed):
#
# ssh $LOGNAME@$HOSTNAME: ~/.profile -> ~/.bashrc
# ssh $LOGNAME@$HOSTNAME "command...": ~/.bashrc* -> ~/.profile
# X11 Session: ~/.profile
# └─> GUI Terminal Emulator: ~/.bashrc
#
# Since the profile modifies the PATH environment variable, it is always
# loaded, but the entirety of the bashrc is only loaded when Bash is running as
# an interactive shell.
test "$PROFILE_INCLUDE_GUARD" || source "$HOME/.profile" 2>&-
# The rest of this file should only be loaded for interactive sessions.
test "$PS1" || return 0
# If the current Bash interpreter is not the first entry in a $PATH, re-execute
# Bash to pivot into the user's preferred Bash shell.
if ! test "$BASH" -ef "$(command -v bash || echo "$BASH")"; then
options="${-:+-$-}"
set -m 2>&- # Without this, Bash may send itself SIGTTIN.
exec bash $options
fi
# This regex is used to determine whether a username are included in the shell
# prompt and title; if the current username does not match this pattern, the
# username and hostname are inserted in the text.
declare -r EPONYMS='^e(ric)?pruitt$'
# If the "TERM" environment variable matches this regular expression, tmux will
# not be launched automatically.
declare -r NO_AUTO_TMUX='^(tmux|screen|linux|vt[0-9]+|dumb)([+-].+)?$'
# If the "TERM" environment variable matches this regular expression, the
# terminal title is set using the "\033]2;...\033\\" sequence when a command is
# executed.
declare -r XTERM_TITLE_SUPPORT='^(tmux|xterm|screen|st)([+-].+)?$'
# Result of the last expression that was successfully evaluated by the
# "-calculate" function.
declare LAST_RESULT="0"
# This tracks the history number of the command the last time the
# "-prompt-command" was executed. This is used to help determine whether a
# non-zero exit status should be displayed before the next prompt.
declare -i PROMPT_HISTORY_NUMBER=0
# Ensure TERM is always defined.
: "${TERM:=}"
# Define various command aliases.
#
function -define-aliases()
{
alias awk='-paginate awk --'
alias aws='-paginate aws --'
alias back='cd -'
alias cat='-paginate cat --bold-escapes'
alias cmark='-paginate cmark --'
alias colgrep='-paginate colgrep --color=always'
alias cp='cp -a -v'
alias demo='env PS1=$"$\040" playground'
alias detach='bg && disown'
alias df='-paginate df -- -h'
alias diff='-paginate diff --color=always -u'
alias dmesg='sudo dmesg'
alias dpkg-query='-paginate dpkg-query --'
alias du='-paginate du -- -h'
alias egrep='grep -E'
alias esed='sed -E'
alias fgrep='grep -F'
alias find='-paginate find -- -xdev -regextype egrep'
alias gawk='-paginate gawk --'
alias gpg='gpg --pinentry-mode=loopback'
alias grep='-paginate grep --color=always --exclude-dir=.git \
--exclude-dir=__pycache__ --exclude="*.py[co]" \
--exclude-dir=lost+found'
alias head='head -n "$(((LINES - 5) / 2 + (LINES < 6)))"'
alias help='-paginate help --'
alias history='-paginate history --'
alias info='info --vi-keys'
alias jq='-paginate jq -C --indent 4'
alias ldd='-paginate ldd --'
alias ls='-paginate ls "-C -w $COLUMNS --color=always" -b -h \
-I lost+found -I __pycache__ --almost-all-if-not-home'
alias lsblk='-paginate lsblk --'
alias make='gmake'
alias meme='sed -r "s/([a-z])([^a-z]*)([a-z])/\\l\\1\\2\\u\\3/gI" --'
alias mtr='mtr -t'
alias nohist='HISTFILE=/dev/null && exit'
alias open='xdg-open'
alias otr='env HISTFILE=/dev/null bash'
alias paragrep='-paginate paragrep -T -n'
alias pcre2grep='-paginate pcre2grep --color=always \
--exclude-dir="(^__pycache__|^lost\\+found|\\.(git|py[co]))$"'
alias pgrep='-paginate epgrep --enhance'
alias plgrep='grep -P'
alias ps='-paginate ps --'
alias pstree='-paginate pstree -- -a -p -s -U'
alias readelf='-paginate readelf --'
alias reset='tput reset'
alias rm='rm -v'
alias rmdir='rmdir -v'
alias rot13='tr "A-Za-z" "N-ZA-Mn-za-m"'
alias scp='scp -p -r'
alias screen='env SHLVL_OFFSET= screen'
alias sed='-paginate sed --'
alias shred='shred -n 0 -v -u -z'
alias sort='-paginate sort --'
alias strace='strace -tttT'
alias strings='-paginate strings --'
alias sudo='env TERM="${TERM/#tmux*/screen}" sudo'
alias tac='-paginate tac --'
alias tail='tail -n "$(((LINES - 5) / 2 + (LINES < 6)))"'
alias tmux='env SHLVL_OFFSET= tmux'
alias tr='-paginate tr --'
alias tree='-paginate tree -C -a -I ".git|__pycache__|lost+found"'
alias uniq='-paginate uniq --'
alias units='units -1 -v'
alias vi='vim'
alias vlc='vlc --verbose -1'
alias watch='env TERM="${TERM/#tmux*/vt100}" watch'
alias whois='-paginate whois --'
alias wmctrl='-paginate env -- DISPLAY="${DISPLAY:-:0.0}" wmctrl'
alias xargs='-paginate xargs -- --verbose'
alias xmllint='-paginate xmllint --format'
alias xxd='-paginate xxd --'
alias ...="cd ../.."
alias ....="cd ../../.."
alias .....="cd ../../../.."
# This alias is used to allow the user to execute a command without adding
# it to the shell history by prefixing it with "silent".
alias silent=''
# BASH_ALIASES is used because the "alias" command will not accept "=" or
# "-" as an identifier.
BASH_ALIASES[=]='-calculate #'
BASH_ALIASES[-]='cd -'
# Disable hyphenated word-breaks when showing manuals.
man --no-hyphenation --help &>/dev/null && alias man='man --no-hyphenation'
case "$(uname; ps -V 2>/dev/null)" in
Darwin*|FreeBSD*|OpenBSD*)
alias pgrep='-paginate epgrep --enhance \
--ps-options="-o user,pid,ppid,start,command"'
alias ps="-paginate ps -- -o user,pid,ppid,start,command"
;;
*procps*)
alias ps='-paginate ps -- --sort=uid,ppid,pid'
;;
esac
if free -h &>/dev/null; then
alias free="free -h"
fi
# Disable aliases for commands that are not present on this system, and
# compact multi-line aliases into a single line.
local alias_key
local alias_value
local argv
local i
for alias_key in "${!BASH_ALIASES[@]}"; do
alias_value="${BASH_ALIASES["$alias_key"]}"
# Always enable aliases for compound commands since figuring out what
# programs are invoked would be complicated.
case "$alias_value" in
*"&&"*|*"||"*)
continue
;;
esac
test -n "$alias_value" || continue
argv=($alias_value)
# Ignore env(1), the paginate function and any associated options.
i=0
while [[ "${argv[i]}" = @(env|[+-]*|[A-Z_]*([A-Z0-9_])=*) ]]; do
test "${argv[i]}" = "-calculate" && break
let i++
done
if ! hash -- "${argv[i]}" 2>&-; then
# Disable aliases for unrecognized commands
test -z "$alias_value" || unalias "$alias_key"
elif [[ "$alias_value" = *$'\n'* ]]; then
# Compact multi-line aliases
BASH_ALIASES["$alias_key"]="${alias_value//\\$'\n'+(\ )/}"
fi
done
DECLARE_BASH_ALIASES="$(declare -p BASH_ALIASES)"
DECLARE_BASH_ALIASES="${DECLARE_BASH_ALIASES#declare -A }"
export DECLARE_BASH_ALIASES
}
# Handler used to intercept arithmetic expressions so they can be entered
# without having to add a space after the "=".
#
function command_not_found_handle()
{
if [[ "$1" != =* ]]; then
printf "%s: command not found\n" "$1" >&2
return 127
fi
eval "= ${1#=} ${@:2}"
}
# Handler used to intercept arithmetic expressions so they can be entered
# without having to add a space after the "=".
#
function no_such_file_handle()
{
if [[ "$1" != =* ]]; then
printf "%s: No such file or directory\n" "$1" >&2
return 127
fi
eval "= ${1#=} ${@:2}"
}
# Executed when the shell exits.
#
function -exit-trap()
{
rm -f "${TMUX_ENV_TIMESTAMP_FILE:-}"
test -z "${TMUX:-}" || metamux shell-exit-hook "$PPID" 2>&-
}
# Calculate the result of an expression. The result of the last successfully
# evaluated expression is made available as the variable "x". The expression
# must appear in the command history to be evaluated.
#
function -calculate()
{
local result
# The use of eval and echo makes it possible to use command substitution in
# arithmetic expressions when using the "=" alias:
#
# $ = $(cat * | wc -l) / $(echo * | wc -w)
# 88887
#
result="$(
expression="$(HISTTIMEFORMAT= \history 1)"
pc "x = $LAST_RESULT" "$(eval "echo \" ${expression#*=}\"")"
)"
test -n "$result" && LAST_RESULT="${result##* }" && echo "$result"
}
# Paginate arbitrary commands when stdout is a TTY. If stderr is attached to a
# TTY, data written to it will also be sent to the pager. Just because stdout
# and stderr are both TTYs does not necessarily mean it is the same terminal,
# but in practice, this is rarely a problem.
#
# Arguments:
# - $@: Any arguments that start with "-" or "+" before the command name are
# treated as Less options.
# - $1: Name or path of the command to execute.
# - $2: White-space separated list of options to pass to the command when
# stdout is a TTY. If there are no TTY-dependent options, this should be
# "--".
# - $@: Arguments to pass to command.
#
function -paginate()
{
local errfd=1
local -a lessopts=("-XFR")
while [[ "${1:-}" = [-+]* ]]; do
lessopts+=("$1")
shift
done
local command="$1"
local tty_specific_args="$2"
shift 2
if [[ -t 1 ]]; then
test "$tty_specific_args" != "--" || tty_specific_args=""
test -t 2 || errfd=2
"$command" $tty_specific_args "$@" 2>&"$errfd" | less "${lessopts[@]}"
return "${PIPESTATUS[0]/141/0}" # Ignore SIGPIPE failures.
fi
"$command" "$@"
}
# Execute a command inside a folder. This command will expand aliases before
# execution and is run in a subshell so "cd" will not change the working
# directory of the parent shell.
#
# Arguments:
# - $1: Directory
# - $@: Command and command arguments
#
# Variables:
# - FROM_CDPATH: the value of this variable is prepended to the existing value
# (if any) of "CDPATH" before the "cd" command is executed.
#
function from()
{
if [[ -z "${1:-}" ]] || [[ "$#" -lt 2 ]]; then
echo "Usage: ${0##*/} FOLDER COMMAND [ARGUMENT...]"
return 1
fi
(
CDPATH="${FROM_CDPATH:-}:${CDPATH:-}" cd -- "$1" > /dev/null
shift
# The eval command is used here because "$@" will not expand aliases.
eval "$(printf " %q" "$@")"
)
}
# Launch a background command detached from the current terminal session.
#
# Arguments:
# - $1: Command
# - $@: Command arguments
#
function spawn()
{
local command="${1:-}"
if [[ -z "$command" ]]; then
echo "usage: spawn COMMAND [ARGUMENT...]"
return 1
elif ! hash -- "$command" 2>&-; then
echo "spawn: $command: command not found" >&2
return 127
fi
# The eval command is used here because "$@" will not expand aliases.
eval "$(printf " %q" "$@") < /dev/null &> /dev/null & disown"
}
# Run a command in a verbose manner; before the command is executed, the
# command to be run is written to standard error, and after the command runs,
# its exist status is also written to standard error in green if the command
# succeeded or red otherwise.
#
# Arguments:
# - $1: Command
# - $@: Command arguments
#
function run()
{
local script
local -i exit_status=0
if [[ "$#" -eq 0 ]]; then
echo "Usage: run COMMAND [ARGUMENT...]"
return 1
fi
printf -v script " %q" "$@"
{
tput bold
tput setaf 3
printf "Running:%s\n" "$script"
tput sgr0
} >&2
(eval "$script") || exit_status="$?"
{
tput bold
tput setaf "$((exit_status ? 1 : 2))"
echo "(\$?=$exit_status)"
tput sgr0
} >&2
return "$exit_status"
}
# Reload environment variables from tmux that are specified in its
# "update-environment" configuration option. The variables TMUX and
# TMUX_ENV_FILE must be set to non-empty strings or this function is a no-op.
# TMUX_ENV_FILE should be the path of a file that contains the output of `tmux
# show-environment -s`. This function always finishes with a return code of 0
# even if there were errors.
#
function -reload-tmux-env()
{
test "${TMUX:-}" -a "${TMUX_ENV_FILE:-}" || return 0
# Use an empty file to keep track of when the tmux environment file was
# last read so it is only sourced when there have been changes to the file.
if [[ -z "${TMUX_ENV_TIMESTAMP_FILE:-}" ]]; then
# The use of "declare" with a process substitution is deliberate; the
# substitution failing should be a non-fatal error.
declare -g TMUX_ENV_TIMESTAMP_FILE="$(
mktemp -t .$$-tmux-env-last-read-timestamp-XXXXXX 2>/dev/null
)"
fi
if [[ "$TMUX_ENV_TIMESTAMP_FILE" -ot "$TMUX_ENV_FILE" ]]; then
> "$TMUX_ENV_TIMESTAMP_FILE" && source "$TMUX_ENV_FILE" || return 0
fi
}
# Update the Bash prompt and display the exit status of the previously executed
# command if it was non-zero. The prompt has the following indicators:
#
# - Show nesting depth of interactive shells when greater than 1.
# - When accessing the host over SSH, include username and hostname.
# - If there are background jobs, show the quantity in brackets.
# - Terminate the prompt with "#" when running as root and "$" otherwise.
#
# Variables:
# - EPONYMS: If the username matches the regular expression defined in this
# variable, it will be suppressed by logic defined in the "-setup" function.
#
# Any variables that do not match the regular expression `/^[A-Z_][A-Z0-9_]*$/`
# are automatically unset.
#
function -prompt-command()
{
local exit_status="$?"
local -i history_number=-1
local signal=""
# XXX: Not entirely sure why this is necessary as I only observed the bug
# once; the first word of "history_number" was "6868*" instead of just
# 6868. To prevent this from happening again, strip off non-numeric
# suffixes.
if [[ "$(HISTTIMEFORMAT= \history 1)" =~ ([0-9]+) ]]; then
history_number="${BASH_REMATCH[0]}"
fi
# This function originally used the variable "debug_hook_ran" to determine
# if the user executed a command or if the input was blank, but that meant
# that non-zero exit statuses of subshells would not be shown. To work
# around this, the history index of the command being executed is checked
# in conjunction with "debug_hook_ran".
if [[ "$history_number" -ne "$PROMPT_HISTORY_NUMBER" ]]; then
PROMPT_HISTORY_NUMBER="$history_number"
# If the history number did not change and the debug hook was not executed,
# then there was probably no input from the user or it was cleared with
# Ctrl+C.
elif ! [[ "${debug_hook_ran:-}" ]]; then
exit_status=""
fi
local depth="$((SHLVL - SHLVL_OFFSET + 1))"
local jobs=""
jobs % &> /dev/null && jobs="x"
test "$depth" -gt 1 || depth=""
if [[ "$exit_status" -ne 0 ]]; then
if ((exit_status <= (128 + RTSIG_MAX) && exit_status > 128)); then
signal="$(\kill -l "$((exit_status - 128))" 2>/dev/null || :)"
fi
echo -e "($exit_status${signal:+: SIG$signal})\a"
fi
# Only set PS1 if a pre-existing value was not inherited.
if [[ "${PS1@a}" != *x* ]]; then
PS1="${depth:+$depth: }${SSH_TTY:+\\u@\\h:}\\W${jobs:+ [\\j]}\\$ "
fi
# SECONDS is reset in -debug-hook, and this logic ensures the reload
# function is only called during prompt generation if more than one second
# has passed since the last reload call.
test "$SECONDS" -lt 1 || -reload-tmux-env
unset $(compgen -v -X '[A-Z_]*([A-Z0-9_])')
}
# Hook executed before every Bash command that is not run inside a subshell or
# function. When using a supported terminal emulator, the title will be set to
# the last executed command.
#
# Arguments:
# - $1: Last executed command.
#
function -debug-hook()
{
local alias_key
local expansion
local guess
local command="$1"
local env=""
local envregex="^([A-Z_][A-Z0-9_]*=(\"[^\"]+\"|'[^']+'|[^ ]+)? )+"
local search_again="x"
local shortest_guess="$command"
test "$command" != "$PROMPT_COMMAND" || return 0
if [[ -w /dev/tty ]] && [[ "$TERM" =~ $XTERM_TITLE_SUPPORT ]]; then
# None of my aliases start with environment variables, so they are
# temporarily stripped before looking for substitutions.
if [[ "$command" =~ $envregex ]]; then
env="${BASH_REMATCH[0]}"
command="${command#$env}"
fi
# Iterate over all aliases and figure out which ones were likely used
# to create the command. The looping handles recursive aliases.
while [[ "${search_again:-}" ]]; do
unset search_again
for alias_key in "${!BASH_ALIASES[@]}"; do
expansion="${BASH_ALIASES["$alias_key"]}"
guess="${command/#"$expansion"/$alias_key}"
test "${#guess}" -lt "${#shortest_guess}" || continue
shortest_guess="$guess"
search_again="x"
done
command="$shortest_guess"
done
test -z "$env" || command="$env$command"
printf > /dev/tty \
'\e]2;%s\e\' "${SSH_TTY:+${LOGNAME:-$USER}@$HOSTNAME: }$command"
fi
# To minimize the performance impact of calling -reload-tmux-env, only try
# to run -reload-tmux-env once per script i.e. only one time after the user
# presses enter instead of whenever any command in the script runs.
# Additionally, the special SECONDS variable is reset and used by
# -prompt-command so the reload function is only called if more than one
# second has passed since -debug-hook called the reload function.
test ! "${debug_hook_ran:-}" && -reload-tmux-env && SECONDS=0
debug_hook_ran="x"
}
# Bootstrap function to configure various settings and launch tmux when it
# appears that Bash is not already running inside of another multiplexer.
#
function -setup()
{
unset -f -- -setup
set -u
complete -r
complete -a unalias
complete -c which vw
complete -d cd rmdir
complete -v export unset
shopt -s autocd
shopt -s cdspell
shopt -s dirspell
shopt -s execfail
shopt -s extglob
shopt -s globstar
shopt -s histappend
shopt -s patsub_replacement
shopt -u hostcomplete
trap -- -exit-trap EXIT
local orphaned_session
# If the current directory is not readable, "cd" to the home directory.
test -r . || cd
eval -- "${DECLARE_BASH_ALIASES:--define-aliases}"
if [[ -z "${TMUX:-}" ]]; then
if [[ "$TERM" = tmux* ]]; then
# If a terminfo definition for tmux is not available, use screen's.
tput -S 2>&- < /dev/null || export TERM="screen"
elif [[ "$TERM" ]] && ! [[ "$TERM" =~ $NO_AUTO_TMUX ]]; then
orphaned_session="$(
tmux list-sessions -F \
'#{session_id} #{window_activity} #{window_active_clients}' \
| awk '
$3 == 0 && $2 > latest_activity {
session_id = $1
latest_activity = $2
}
END {
print session_id
}
'
)"
if [[ "$orphaned_session" ]]; then
exec tmux attach-session -t "$orphaned_session"
else
exec tmux new
fi
fi
fi
if [[ -z "${SSH_TTY:-}" ]] && ! [[ "${LOGNAME:-$USER}" =~ $EPONYMS ]]; then
SSH_TTY="/dev/null"
fi
if [[ "$TERM" =~ $XTERM_TITLE_SUPPORT ]]; then
export REPORT_REMAINING_SLEEP_TIME=x
fi
# macOS tends to use filesystems configured to be case insensitive.
test "$UNAME" = "Darwin" && bind "set completion-ignore-case on"
HISTFILESIZE=""
HISTIGNORE="history?( -[acdnrw]*):@(fg|help|history)?( ):@(silent|fg) *"
HISTSIZE="2147483647"
HISTTIMEFORMAT="%Y-%m-%dT%H:%M:%S%z "
TIMEFORMAT="[%Rs (%P%% CPU; User: %U, Sys: %S)]"
PROMPT_COMMAND="-prompt-command"
test "${HISTFILE:-}" = "/dev/null" || HISTFILE="$HOME/.xbash_history"
# Disable sending SIGQUIT with keyboard shortcuts since I only ever use
# this by accident.
stty quit undef
# Disable flow control for terminals that appear to be virtual devices to
# make ^Q and ^S usable.
if ! [[ "$(tty)" = */ttyUSB* ]]; then
stty -ixon -ixoff
fi
# Secondary bashrc for machine-specific settings.
source "$HOME/.local.bashrc" 2>&-
export COLUMNS
export LINES
test "${SHLVL_OFFSET:-}" || export SHLVL_OFFSET="$SHLVL"
test "$(trap -p DEBUG)" || trap '\-debug-hook "$BASH_COMMAND"' DEBUG
}
-setup