recall.sh 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #!/bin/bash
  2. set -euo pipefail
  3. if [ "$#" == "0" ]
  4. then
  5. echo recall
  6. exit 0
  7. fi
  8. . "${BASH_SOURCE[0]%/*}"/helpers/common.sh
  9. stderr_deps=/dev/null
  10. # NOTE: uncomment to see err output from `which` (if any)
  11. #stderr_deps='/proc/self/fd/2'
  12. check_deps 3>&2 2>"$stderr_deps" 1>/dev/null
  13. LOG_ROOT="$HOME"/.local/var/log/shell # source this
  14. EXIT_SUCCESS=false
  15. LIST_MODE=false
  16. CMD_STRING=false
  17. ORS='\n'
  18. usage () {
  19. cat <<EOF
  20. Recall prints the last captured output from the specified PROG and exits with
  21. the captured exit status.
  22. usage: "${0##*/} [ -l ] [ -n NUM ] PROG [ ARGS ... ]
  23. examples:
  24. - Print the output of last captured find command and exit with captured
  25. exit status
  26. ${0##*/} find
  27. - Print the output of last captured find command and exit with success
  28. ${0##*/} -z find
  29. - List paths to all captured commands
  30. ${0##*/} -l
  31. - List paths to all captured find commands
  32. ${0##*/} -l find
  33. - List at most 10 paths to all captured find commands
  34. ${0##*/} -l -n 10 find
  35. - List paths null delimited for pipe (in case of paths with new line char)
  36. ${0##*/} -l -0 find | xargs -r -0 ls -lahd
  37. - List last 30 command strings (bash escaped)
  38. ${0##*/} -lsn 30
  39. EOF
  40. }
  41. OPTSTRING=":zhln:s0"
  42. parse_opt () {
  43. case "${opt}" in
  44. h)
  45. usage
  46. exit 0
  47. ;;
  48. l)
  49. LIST_MODE=true
  50. ;;
  51. n)
  52. if [ "${OPTARG:-}" ] && [[ "$OPTARG" =~ [0-9]+ ]] && (( OPTARG > 0 ))
  53. then
  54. NUM="$OPTARG"
  55. else
  56. echo "-n paremeter must be an integer greater than zero" >&2
  57. echo >&2
  58. usage >&2
  59. exit 1
  60. fi
  61. ;;
  62. z)
  63. EXIT_SUCCESS=true
  64. ;;
  65. s)
  66. CMD_STRING=true
  67. ;;
  68. 0)
  69. ORS='\0'
  70. ;;
  71. \?)
  72. echo "Unknown switch: -$OPTARG" >&2
  73. echo >&2
  74. usage >&2
  75. exit 1
  76. ;;
  77. esac
  78. }
  79. validate () {
  80. # exit if vars are not valid
  81. # ie, set any dynamic defaults here
  82. if ! "$LIST_MODE" && [ "${NUM:=1}" ] && ! (( NUM == 1 ))
  83. then
  84. echo "-n != 1 can only be specified with -l" >&2
  85. echo >&2
  86. usage >&2
  87. exit 1
  88. fi
  89. if ! "$LIST_MODE" && [ "$#" == "0" ]
  90. then
  91. echo "PROG must be provided" >&2
  92. echo >&2
  93. usage >&2
  94. exit 1
  95. fi
  96. }
  97. while getopts "${OPTSTRING}" opt
  98. do
  99. parse_opt
  100. done
  101. shift $((OPTIND-1))
  102. validate "$@"
  103. fzf_rw0_inplace_preview () {
  104. # NOTE: to preview and select captured command directories
  105. header_len="$1"
  106. # shellcheck disable=SC2016
  107. fzf \
  108. --read0 --print0 \
  109. -m \
  110. --no-sort \
  111. -d / \
  112. --preview-window="~$header_len" \
  113. --preview='cat {}/info; head -n "$LINES" {}/dat' \
  114. && :
  115. }
  116. awk_strip_first_path_part_script () {
  117. cat <<'EOF'
  118. {
  119. for (i=2; i<=NF; i++)
  120. printf "%s%s", $(i), (i<NF ? OFS : ORS)
  121. }
  122. EOF
  123. }
  124. awk_rw0_strip_first_path_part () {
  125. # NOTE: ./a/b/c -> a/b/c (null delimited)
  126. awk \
  127. -v FS='/' -v RS='\0' \
  128. -v OFS='/' -v ORS='\0' \
  129. "$(awk_strip_first_path_part_script)" \
  130. && :
  131. }
  132. awk_r0_add_prefix () {
  133. # NOTE: some-string -> <prefix>some-string (null delimited)
  134. : "${2?awk_r0_add_prefix requires exactly two positional args}"
  135. : "${1:?awk_r0_add_prefix output record separator must be set and non-empty}"
  136. [ "${1:+DefinedNotEmpty}" == "DefinedNotEmpty" ]
  137. [ "${2+DefinedMaybeEmpty}" == "DefinedMaybeEmpty" ]
  138. awk \
  139. -v prefix="$2/" \
  140. -v RS='\0' \
  141. -v ORS="$1" \
  142. '{print prefix $0}' \
  143. && :
  144. }
  145. awk_r0_add_suffix () {
  146. # NOTE: some-string -> some-string<suffix> (null delimited)
  147. : "${2?awk_r0_add_suffix requires exactly two positional args}"
  148. : "${1:?awk_r0_add_suffix output record separator must be set and non-empty}"
  149. [ "${1:+DefinedNotEmpty}" == "DefinedNotEmpty" ]
  150. [ "${2+DefinedMaybeEmpty}" == "DefinedMaybeEmpty" ]
  151. awk \
  152. -v suffix="/$2" \
  153. -v RS='\0' \
  154. -v ORS="$1" \
  155. '{print $0 suffix}' \
  156. && :
  157. }
  158. sort_rw0 () {
  159. # NOTE: sort by program, or timestamp
  160. if [ "$1" = "prog" ]
  161. then
  162. sort -zrV
  163. elif [ "$1" = "ts" ]
  164. then
  165. awk \
  166. -v FS='/' -v RS='\0' \
  167. -v OFS='/' -v ORS='\0' \
  168. '{print $NF,$0}' \
  169. | sort -zrV \
  170. | awk_rw0_strip_first_path_part \
  171. :
  172. else
  173. echo "unsupported sort option: $1" >&2
  174. false
  175. fi
  176. }
  177. list_r0 (){
  178. ors="$1"
  179. if ! "$CMD_STRING"
  180. then
  181. awk_rw0_strip_first_path_part \
  182. | awk_r0_add_prefix "$ors" "$LOG_ROOT" \
  183. && :
  184. else
  185. awk_r0_add_suffix '\0' 'info' \
  186. | xargs -r0n1 head -n1 \
  187. | awk \
  188. -v RS='\n' \
  189. -v ORS="$ors" \
  190. '{print $0}' \
  191. && :
  192. fi
  193. }
  194. if "$LIST_MODE"
  195. then
  196. # TODO: make sort option configurable
  197. sort_opt="ts"
  198. find_name=()
  199. if [ "${1:-}" ]
  200. then
  201. slugs=( "$1" )
  202. if [ "${2:-}" ]
  203. then
  204. slugs+=( "$2" )
  205. fi
  206. query="$(bash -ac 'IFS=/; echo "$*"' "recall-bash" "${slugs[@]}")"
  207. find_name=( -wholename "**/$query/**" )
  208. fi
  209. (
  210. cd "$LOG_ROOT"
  211. find . \
  212. \( -type l -o -type d \) -wholename './????????_??????_?????????' -prune -o -type d "${find_name[@]}" \( \
  213. -links 2 \
  214. -o \( \
  215. -links 1 -exec bash -c '! [ "$(find "$1" -mindepth 1 -type d)" ]' find-bash {} \; \
  216. \) \
  217. \) \
  218. -print0 \
  219. | sort_rw0 "$sort_opt" \
  220. | head -zn "${NUM:--0}" \
  221. | fzf_rw0_inplace_preview "2" \
  222. | list_r0 "$ORS" \
  223. && :
  224. )
  225. else
  226. PROG="$1"
  227. SUBPROG="$(get_subprog "$@")"
  228. LOG_DIR="$LOG_ROOT"/"$PROG"/"$SUBPROG"
  229. LOG_DIR="$LOG_DIR"/"$(cd "$LOG_DIR" && find . -mindepth 1 -maxdepth 1 -print0 | sort -zV | tail -zn1 | xargs -r -0 echo)"
  230. OUT="$LOG_DIR"/stdout
  231. ERR="$LOG_DIR"/stderr
  232. # shellcheck disable=SC2034
  233. DAT="$LOG_DIR"/dat
  234. INF="$LOG_DIR"/info
  235. # shellcheck disable=SC1090
  236. . <(tail -n +2 "$INF") # first line is commandline
  237. "$EXIT_SUCCESS" && status=0
  238. #TODO: allow user to request stderr in case it is needed for
  239. # testing redirections
  240. # disabled by defualt as it won't be interleaved correctly
  241. # on the console
  242. false && cat "$ERR" >&2
  243. cat "$OUT"
  244. exit "$status"
  245. fi