Scriptorium

This is archive of various scripts for bash i've made to automate something or as proof-of-concepts. Somewhen they gonna be documented, but probably not. The name was chosen because of its meaning – place where manuscripts were manually copied called «scriptorium», and as I decided just to insert scripts in page code, you have to copy it manually as well. Appearance and layout of this page slightly differs from rest of site, but I think it's justified decision for this contents.

TOC

catpak – primitive directory packer

#!/bin/bash ARCHIVE_INDEX="index.catpak.lst" ARCHIVE_CONTENT="index.catpak" function create_packageset { truncate -s 0 ./$ARCHIVE_INDEX truncate -s 0 ./$ARCHIVE_CONTENT find -type f ! -iname "$ARCHIVE_INDEX" ! -iname "$ARCHIVE_CONTENT" -exec sh -c 'stat -c "%s^%n^%a" "$1" >> "./$2"; cat "$1" >> "./$3"' _ {} "$ARCHIVE_INDEX" "$ARCHIVE_CONTENT" \; } function extract_file { CHUNK=$(awk -F '^' -v needle="$1" 'BEGIN { sum = 0 } $2 == needle { print sum " " $1; exit 1 } $2 != needle { sum += $1 }' "./$ARCHIVE_INDEX") if [[ "$?" != "1" ]] then exit 1 fi IFS=" " read -ra OFFLIM <<< "$CHUNK" dd if="./$ARCHIVE_CONTENT" of="/tmp/dumpdata" bs=1 skip="${OFFLIM[0]}" count="${OFFLIM[1]}" && \ cat /tmp/dumpdata rm /tmp/dumpdata } function extract_all { OFFSET=0 mkdir -p "$1" for LISTENTRY in $(cat "./$ARCHIVE_INDEX") do ITEMSIZE=$(cut -d^ -f1 <<< "$LISTENTRY") ITEMNAME="$1/$(cut -d^ -f2 <<< $LISTENTRY)" mkdir -p $(dirname $ITEMNAME) dd if="./$ARCHIVE_CONTENT" of="$ITEMNAME" bs=1 skip="$OFFSET" count="$ITEMSIZE" OFFSET=$(expr $OFFSET + $ITEMSIZE) done } if [[ "$1" == "create" ]] then create_packageset elif [[ "$1" == "extract" ]] then extract_file "./$2" elif [[ "$1" == "extractall" ]] then extract_all "/tmp/dumpie" elif [[ "$1" == "squash" ]] then create_packageset # tar czvf index.catpak.tgz "./$ARCHIVE_INDEX" "./$ARCHIVE_CONTENT" # rm "./$ARCHIVE_INDEX" "./$ARCHIVE_CONTENT" bzip2 "./$ARCHIVE_CONTENT" elif [[ "$1" == "unsquash" ]] then bzip2 -d -c "./$ARCHIVE_CONTENT.bz2" > "./$ARCHIVE_CONTENT" # tar xzvf index.catpak.tgz extract_file "./$2" # rm "./$ARCHIVE_INDEX" "./$ARCHIVE_CONTENT" rm "./$ARCHIVE_CONTENT" fi

custom certificate authority helper

#!/usr/bin/env bash if [ "$1" == "make" ] then if [ -f rootCA.key ] then echo "CA key and certificate exist" exit 1 fi # ca key openssl genrsa -out rootCA.key 2048 # ca cert openssl req -x509 -new -key rootCA.key -days 10000 -out rootCA.crt exit 0 fi if [ "$1" == "sign" ] && [ "$2" != "" ] then openssl genrsa -out $2.key 2048 openssl req -new -key $2.key -out $2.csr openssl x509 -req -in $2.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out $2.crt -days 5000 exit 0 fi echo "Usage: $0 make|sign <result basename>" exit 1

disk usage for top-level directories

#!/bin/sh # topdu -- du -h for top-level directoies du . | \ sed -rn '/^[0123456789]+[[:space:]]+\.\/*[^\/]*$/p' | \ sort -n | \ awk 'BEGIN { FS = "\t" } { dsize = $1 "K"; if($1 > lshift(1, 20)) { dsize = rshift($1, 20) "G" } else if($1 > lshift(1, 10)) { dsize = rshift($1, 10) "M" }; print dsize "\t" $2 }'

non-interactive gemini protocol client

More about gemini protocol

#!/bin/bash # view gemini capsules gemini_host=$(printf "$1" | sed 's#^gemini://\([^/]*\).*$#\1#gi') if [ "$2" != "" ] then gemini_path="$2" else gemini_path="$1" fi printf "$gemini_path\r\n" | openssl s_client -quiet -ign_eof -connect $gemini_host:1965 2> /dev/null 1> /dev/shm/gemini.tmp read gemini_status gemini_meta <<< $(head -n 1 /dev/shm/gemini.tmp | tr -d '\r\n') printf "Status: %s, Meta: %s\n" $gemini_status $gemini_meta clear case "$gemini_status" in 2*) if [ "$gemini_meta" == "text/gemini" ]; then tail -n +2 /dev/shm/gemini.tmp | sed 's/^\(=>.*$\)/\o033[1;36m\1\o033[0m/gi' else tail -n +2 /dev/shm/gemini.tmp fi ;; 3*) printf "Redirecting to $gemini_meta...\n" $0 "$gemini_meta" ;; 4*|5*) printf "Error: $gemini_meta!\n" ;; esac if [ -e "/dev/shm/gemini.tmp" ]; then rm /dev/shm/gemini.tmp; fi

extract links from html

Handles basic cases only

#!/bin/bash if [ "$1" == "" ] then find . -iname '*.htm*' -exec "$0" {} \; else printf "\n$1 dependencies:\n\n" cat "$1" | sed 's/[:space:]]+).*/\2/gi;p}' | tr -d '"'"'"'' fi

pat.sh – primitive version control

#!/bin/bash PATSH_REPO=".patches" PATSH_NAME="patsh" function check_if_unchanged { unset -v LATEST_DIFF for file in "${PATSH_REPO}/${1}"-*.diff; do [[ "$file" -nt "${LATEST_DIFF}" ]] && LATEST_DIFF="$file" done if [ "$LATEST_DIFF" != "" ]; then patch -i "${LATEST_DIFF}" -o - "${PATSH_REPO}/${1}" 2> /dev/null | cmp - "${1}" > /dev/null else cmp "${PATSH_REPO}/${1}" "${1}" > /dev/null fi } function showhelp { cat <<EOF ${PATSH_NAME} -- dead simple patch/version control system Usage: patsh [options] command [files...] Options: -h show this help -f enforce commit if no changes Commands: init initialize stash area add start tracking file changes save store changes done to file restore undo changes and rewind to last version diff show changes between working copy and last version list show stored versions EOF exit 0 } while [[ "$1" == -* ]]; do case "$1" in -f|--force) printf "${PATSH_NAME}: not implemented yet\n" ;; -h|--help) showhelp ;; esac shift done case "$1" in init) mkdir "${PATSH_REPO}" && \ printf "${PATSH_NAME}: stash initialized\n" ;; add|a) while [ -e "$2" ]; do if [ ! -e "${PATSH_REPO}/$2" ]; then cp "$2" "${PATSH_REPO}/" && \ printf "${PATSH_NAME}: tracked file: ${2}\n" else printf "${PATSH_NAME}: file already tracked\npat.sh: use ${0} save ${2} to commit changes\n" fi shift done ;; list|ls) while [ -e "$2" ]; do if [ ! -e "${PATSH_REPO}/$2" ]; then printf "${PATSH_NAME}: file not tracked\npat.sh: use ${0} add ${2} to track file\n" exit 0 fi printf "${PATSH_NAME}: ${2}: saved patches\n" for file in "${PATSH_REPO}/${2}"-*.diff ; do basename "${file}" done | sort shift done ;; save|ci) while [ -e "$2" ]; do if [ ! -e "${PATSH_REPO}/$2" ]; then printf "${PATSH_NAME}: file ${2} not tracked\n" elif check_if_unchanged "$2"; then printf "${PATSH_NAME}: file ${2} not changed\n" else LATEST_DIFF="${PATSH_REPO}/$2-$(date +%s).diff" diff -u "${PATSH_REPO}/$2" "$2" > "${LATEST_DIFF}" printf "${PATSH_NAME}: changes written to %s\n" "${LATEST_DIFF}" fi shift done ;; restore|co) while [ -e "$2" ]; do unset -v LATEST_DIFF if [ ! -e "${PATSH_REPO}/$2" ]; then printf "${PATSH_NAME}: file ${2} not tracked\n" exit 0 fi cp "${PATSH_REPO}/${2}" "${2}" for file in "${PATSH_REPO}/${2}"-*.diff; do [[ "$file" -nt "${LATEST_DIFF}" ]] && LATEST_DIFF="$file" done if [ "$LATEST_DIFF" != "" ]; then patch "${2}" < "${LATEST_DIFF}" > /dev/null printf "${PATSH_NAME}: reset to ${LATEST_DIFF}\n" else printf "${PATSH_NAME}: reset to original version\n" fi shift done ;; diff) while [ -e "$2" ]; do unset -v LATEST_DIFF printf "${PATSH_NAME}: ${2} working copy changes\n" for file in "${PATSH_REPO}/${2}"-*.diff; do [[ "$file" -nt "${LATEST_DIFF}" ]] && LATEST_DIFF="$file" done if [ "$LATEST_DIFF" != "" ]; then patch -i "${LATEST_DIFF}" -o - "${PATSH_REPO}/${2}" 2> /dev/null | diff --color=always - "${2}" else diff --color=always "${PATSH_REPO}/${2}" "${2}" fi shift done ;; *) if [ "$1" == "" ]; then showhelp; fi printf "${PATSH_NAME}: unknown operation: ${1}\n" ;; esac

omsk - interactive console gopher client in python

#!/usr/bin/python3 import enum, socket, traceback, subprocess class MenuType(str, enum.Enum): DOC = '0' MENU = '1' QUERY = '7' BIN = '9' HTML = 'h' INFO = 'i' SOUND = 's' IMAGE = 'I' GIF = 'g' ESC_WHITE = '\033[1;37m' ESC_CYAN = '\033[1;36m' ESC_RESET = '\033[0m' ESC_RED = '\033[1;31m' ESC_GREEN = '\033[1;32m' ESC_YELLO = '\033[1;33m' ESC_BLUE = '\033[1;34m' class Fetcher(object): def __init__(self): self.mode = MenuType.MENU self.host = 'gopher.floodgap.com' self.port = 70 self.path = '' self.history = [] def go(self, mode=None, host=None, port=None, path=None): self.history.append(( self.mode, self.host, self.port, self.path )) if mode is not None: self.mode = mode if host is not None: self.host = host if port is not None: self.port = int(port) if path is not None: self.path = path if mode == MenuType.QUERY: query = input('Search query: ') self.path += '\t{}'.format(query) def back(self): try: mode, host, port, path = self.history.pop() self.mode = mode self.host = host self.port = port self.path = path except: print('No entries in history') def fetch(self): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((self.host, self.port)) s.sendall('{}\r\n'.format(self.path).encode('utf-8')) resp = b'' while True: chunk = s.recv(1024) if not chunk: break resp += chunk return resp except: traceback_exc() print('Failed to fetch data') return None class Navigator(Fetcher): def __init__(self): super().__init__() self.links = [] def goto(self, num): if num <= 0 or num > len(self.links): print('\tWrong link number') else: self.go(*self.links[num - 1]) def display(self): if self.mode == MenuType.BIN: print('Enter filename or leave empty to ignore') filename = input('Save as: ') if filename == '': return elif self.mode == MenuType.HTML: if self.path.lower().startswith('url:'): subprocess.call(['/usr/bin/xdg-open', self.path[4:]]) else: subprocess.call(['/usr/bin/lynx', 'gopher://{}:{}/0{}'.format(self.host, self.port, self.path)]) self.back() return data = self.fetch() if self.mode == MenuType.DOC: print(data.decode('utf-8')) elif self.mode == MenuType.BIN: with open(filename, 'wb') as f: f.write(data) elif self.mode == MenuType.MENU: self.links = [] items = [item.split('\t') for item in data.decode('utf-8').split('\n')] for item in items: if len(item) < 4: continue itype = item[0][0] itext = item[0][1:] if itype == MenuType.INFO: print('\t{}'.format(itext)) elif itype == MenuType.DOC: self.links.append((itype, item[2], item[3], item[1])) print('{}[{}]\t{}{}{}'.format( ESC_WHITE, len(self.links), ESC_BLUE, itext, ESC_RESET )) elif itype == MenuType.MENU: self.links.append((itype, item[2], item[3], item[1])) print('{}[{}]\t{}{}{}'.format( ESC_WHITE, len(self.links), ESC_WHITE, itext, ESC_RESET )) elif itype == MenuType.BIN or itype == MenuType.SOUND or itype == MenuType.IMAGE or itype == MenuType.GIF: self.links.append((itype, item[2], item[3], item[1])) print('{}[{}]\t{}{}{}'.format( ESC_WHITE, len(self.links), ESC_GREEN, itext, ESC_RESET )) elif itype == MenuType.QUERY: self.links.append((itype, item[2], item[3], item[1])) print('{}[{}]\t{}{}{}'.format( ESC_WHITE, len(self.links), ESC_CYAN, itext, ESC_RESET )) elif itype == MenuType.HTML: self.links.append((itype, item[2], item[3], item[1])) print('{}[{}]\t{}{}{}'.format( ESC_WHITE, len(self.links), ESC_RED, itext, ESC_RESET )) else: print('\tUnknown type: {}'.format(itype)) else: print('Not yet implemented') def display_help(): print('''Omsk subway -- commandline gopher navigator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Commands: exit -- exit program help -- view this help back -- return to previous menu index -- return to server index @<host> -- request server's index <selector> -- request menu by selector on current server ''') if __name__ == '__main__': navi = Navigator() omit_display = False while True: try: if not omit_display: navi.display() else: omit_display = False except: traceback.print_exc() print('\tFailed to render') command = input('{}{} {}{}> {}'.format(ESC_WHITE, navi.host, ESC_CYAN, navi.path, ESC_RESET)) if command == '': continue elif command == 'help' or command == '?': display_help() omit_display = True elif command == 'exit': break elif command == 'back': navi.back() elif command == 'index': navi.go(path='') elif command[0] == '@': navi.go(host=command[1:], path='', mode=MenuType.MENU) elif command.isdigit(): navi.goto(int(command)) else: navi.go(path=command, mode=MenuType.MENU)

Code provided under CC-BY-SA license