citadel

My dotfiles, scripts and nix configs
git clone git://jb55.com/citadel
Log | Files | Refs | README | LICENSE

txtnish (38558B)


      1 #!/bin/sh
      2 
      3 VERSION="0.2"
      4 
      5 TAB=$(printf "\t")
      6 
      7 ###################
      8 ## Default config #
      9 ###################
     10 
     11 limit=20
     12 formatter="fold -s"
     13 use_pager=1
     14 use_color=1
     15 always_update=1
     16 sort_order=descending
     17 twtfile=~/twtxt.txt
     18 disclose_identity=0
     19 max_procs=50
     20 xargs_parallel=1
     21 editor=${EDITOR:-vi}
     22 pager=${PAGER:-less -R}
     23 color_nick=yellow
     24 color_time=blue
     25 color_hashtag=cyan
     26 color_mention=yellow
     27 gpg_bin=${gpg_bin:-gpg}
     28 sign_user=""
     29 sign_twtfile=0
     30 check_signatures=0
     31 ipfs_publish=0
     32 ipfs_wrap_with_dir=0
     33 ipfs_recursive=0
     34 ipfs_gateway=http://localhost:8080
     35 nick="${USER}"
     36 sync_followings=""
     37 awk=awk
     38 sed=sed
     39 # sync_followings="https://raw.githubusercontent.com/mdom/we-are-twtxt/master/we-are-twtxt.txt"
     40 timeout=0
     41 verbose=0
     42 force=0
     43 add_metadata=0
     44 
     45 http_proxy=""
     46 https_proxy=""
     47 
     48 ftp_user=""
     49 ftp_host=""
     50 
     51 sftp_over_scp=0
     52 scp_user=""
     53 scp_host=""
     54 
     55 http_backend_args=""
     56 
     57 theme="default"
     58 
     59 # this is the password for mailpipe. Mailpipe expects the first line of the
     60 # mail to be in the form "password $mail_password"
     61 mail_password=""
     62 
     63 # timestamps defaults
     64 last_timeline=0
     65 
     66 if [ -n "${NO_COLOR+1}" ];then
     67     use_color=0
     68 fi
     69 
     70 ######################
     71 ## Runtime variables #
     72 ######################
     73 
     74 program_name=${0##*/}
     75 config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/$program_name"
     76 config_file="$config_dir/config"
     77 follow_file="$config_dir/following"
     78 draft_file="$config_dir/draft"
     79 cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/$program_name"
     80 log_dir="$cache_dir/logs"
     81 
     82 #####################
     83 ## Helper Functions #
     84 #####################
     85 
     86 # Description: Check if command is in path
     87 # Synopsis: have_cmd COMMAND
     88 # Returns: success if command is found; fails otherwise
     89 
     90 have_cmd () {
     91 	command -v "$1" >/dev/null 2>&1
     92 }
     93 
     94 # Description: Print error message and exit
     95 # Synopsis: die MESSAGE RETURN_CODE
     96 # Returns: nothing
     97 
     98 die () {
     99 	printf "%s: %s\n" "$program_name" "$1" >&2
    100 	exit "${2:-1}"
    101 }
    102 
    103 warn () {
    104 	printf "%s: %s\n" "$program_name" "$1" >&2
    105 }
    106 
    107 info () {
    108 	[ "$verbose" -gt 0 ] && printf "%s: %s\n" "$program_name" "$1"
    109 }
    110 
    111 # Description: Create dir unless it exists
    112 # Synopsis: create_dir DIR
    113 # Returns: nothing
    114 
    115 create_dir () {
    116 	[ -d "$1" ] || mkdir -p "$1"
    117 }
    118 
    119 # Description: Source configuration file if it exists
    120 # Synopsis: read_config
    121 # Returns: nothing
    122 
    123 read_config () {
    124 	if [ -e "$config_file" ];then
    125 		# shellcheck source=/dev/null
    126 		. "$config_file"
    127 	fi
    128 
    129 	if [ "$disclose_identity" -eq 1 ] && [ -n "$twturl" ];then
    130 		user_agent="txtnish/$VERSION (+${twturl}; @$nick)"
    131 	else
    132 		user_agent="txtnish/$VERSION (+https://github.com/mdom/txtnish)"
    133 	fi
    134 
    135 	[ -n "$http_proxy"  ] && export http_proxy
    136 	[ -n "$https_proxy" ] && export https_proxy
    137 }
    138 
    139 # Description: Print arguments for curl on stdout for xargs
    140 # Synopsis: args_for_curl NICK URL COUNTER
    141 # Returns: Nothing
    142 
    143 args_for_curl () {
    144 	if [ "$timeout" -ne 0 ];then
    145 		printf "%s\n" "--max-time" "$timeout"
    146 	fi
    147 	printf "\"%s\"\n" \
    148 		--user-agent "$user_agent" \
    149 		--location \
    150 		--stderr "$log_dir/http.log.$1" \
    151 		--show-error \
    152 		--silent \
    153 		--compressed \
    154 		--output "$cache_dir/twtfiles/$1.txt" \
    155 		--time-cond "$cache_dir/twtfiles/$1.txt" \
    156 		--write-out '%{filename_effective}\t%{http_code}\t%{num_redirects}\t%{url_effective}\n' \
    157 		"$2"
    158 }
    159 
    160 rewrite_url () {
    161 	if [ -z "$_ipfs_checked" ];then
    162 		if ! curl -s "$ipfs_gateway" > /dev/null 2>&1 ;then
    163 			ipfs_gateway=https://ipfs.io
    164 		fi
    165 		_ipfs_checked=1
    166 	fi
    167 
    168 	case $url in
    169 		ipns://* )
    170 			url=$ipfs_gateway/ipns/${url#ipns://}
    171 		;;
    172 	esac
    173 }
    174 
    175 maybe_update () {
    176 	if [ "$always_update" -eq 1 ];then
    177 		update "$@"
    178 	fi
    179 }
    180 
    181 format_msg_html () {
    182 	$awk '
    183 		function escape (str) {
    184 			gsub(/&/,"\\&",str)
    185 			gsub(/</,"\\&lt;",str)
    186 			gsub(/>/,"\\&gt;",str)
    187 			return str
    188 		}
    189 
    190 		function linkify (str,   new_str, n, url, nick) {
    191 			while ( str ) {
    192 				if ( match(str,/@&lt;[^&]+&gt;/) ) {
    193 					new_str = new_str substr(str,1,RSTART-1)
    194 					n = split(substr(str,RSTART+5,RLENGTH-9),fields," ")
    195 					if ( n == 1 ) {
    196 						nick = ""
    197 						url = fields[1]
    198 					} else {
    199 						nick = fields[1]
    200 						url = fields[2]
    201 					}
    202 
    203 					new_str = new_str "<a href=\"" url "\">@" nick "</a>"
    204 					str = substr(str,RSTART+RLENGTH)
    205 				} else if ( match(str,/https?:\/\/[^ ]+/) ) {
    206 					new_str = new_str substr(str,1,RSTART-1)
    207 					url = substr(str,RSTART,RLENGTH)
    208 
    209 					# a trailing point or comma could be
    210 					# part of the url, but it is probably a
    211 					# punctuation mark.
    212 					if ( substr(url,length(url),1) ~ /[,.]/ ) {
    213 						url = substr(url,1,length(url)-1)
    214 						RSTART -= 1
    215 					}
    216 
    217 					new_str = new_str "<a href=\"" url "\">" url "</a>"
    218 					str = substr(str,RSTART+RLENGTH)
    219 				} else {
    220 					new_str = new_str str
    221 					str = ""
    222 				}
    223                         }
    224                         return new_str
    225 		}
    226 
    227 		BEGIN {
    228 			FS = "\t"
    229 			srand()
    230 			now = srand()
    231 			printf "<!doctype html>\n" \
    232 				"<html>" \
    233 					"<head>" \
    234 						"<meta charset=\"utf-8\">" \
    235 						"<style>" \
    236 							".nick { color: red }" \
    237 							"p { max-width: 60em; }" \
    238 						"</style>" \
    239 					"</head>"
    240 		}
    241 		{
    242 			nick=$1;url=$2;props=$3;ts=$4;msg=$5
    243 			seconds=now-ts
    244 			if ( seconds >= 172800 ) {
    245 				ts = int(seconds / 86400) " days ago"
    246 			} else if ( seconds >= 86400 ) {
    247 				ts = "1 day ago"
    248 			} else if ( seconds >= 7200 ) {
    249 				ts = int(seconds / 3600) " hours ago"
    250 			} else if ( seconds >= 3600 ) {
    251 				ts = "1 hour ago"
    252 			} else if ( seconds >= 120 ) {
    253 				ts = int(seconds / 60) " minutes ago"
    254 			} else if ( seconds >= 60 ) {
    255 				ts = "1 minute ago"
    256 			} else if ( seconds == 1 ) {
    257 				ts = "1 second ago"
    258 			} else {
    259 				ts = seconds " seconds ago"
    260 			}
    261 
    262 			nick = escape(nick)
    263 			msg = escape(msg)
    264 			msg = linkify(msg)
    265 
    266 			print "<p>* <span class=\"nick\">" nick "</span> (" ts ")<br/>" msg "</p>"
    267 		}
    268 
    269 		END {
    270 			print "</html>"
    271 		}
    272 	'
    273 }
    274 
    275 format_msg () {
    276 
    277 	export color_nick color_time color_hashtag color_mention use_color formatter
    278 	$awk -vtheme="$1" '
    279 		function colors_to_escape ( color,  fgc, bgc, n, c, attr ) {
    280 			n = split(color,c,/ /)
    281 			for (i=1; i<=n; i++ ) {
    282 				if ( match(c[i], /^on_/ ) ) {
    283 					bgc = bg colors[substr(c[i],4)]
    284 				} else if ( colors[c[i]] != "" ) {
    285 					fgc = fg colors[c[i]]
    286 				} else if ( attribs[c[i]] != "" ) {
    287 					if ( attr )
    288 						attr = attr ";" attribs[c[i]]
    289 					else
    290 						attr = ";" attribs[c[i]]
    291 				}
    292 			}
    293 			if ( bgc && fgc ) {
    294 				return csi fgc ";" bgc attr "m"
    295 			}
    296 			if ( fgc ) {
    297 				return csi fgc attr "m"
    298 			}
    299 		}
    300 
    301 		function colorize ( layer, color, text ) {
    302 			return csi layer colors[color] "m" text reset
    303 		}
    304 		BEGIN {
    305 			FS="\t"
    306 			ORS="\n\n"
    307 			csi = "\033["
    308 			reset = csi "0m"
    309 			fg = 3
    310 			bg = 4
    311 			colors["black"] = 0
    312 			colors["red"] = 1
    313 			colors["green"] = 2
    314 			colors["yellow"] = 3
    315 			colors["blue"] = 4
    316 			colors["magenta"] = 5
    317 			colors["cyan"] = 6
    318 			colors["white"] = 7
    319 
    320 			attribs["bold"] = 1
    321 			attribs["bright"] = 1
    322 			attribs["faint"] = 2
    323 			attribs["italic"] = 3
    324 			attribs["underline"] = 4
    325 			attribs["blink"] = 5
    326 			attribs["fastblink"] = 6
    327 			srand()
    328 			now=srand()
    329 
    330 			color_nick = colors_to_escape(ENVIRON["color_nick"])
    331 			color_time = colors_to_escape(ENVIRON["color_time"])
    332 			color_hashtag = colors_to_escape(ENVIRON["color_hashtag"])
    333 			color_mention = colors_to_escape(ENVIRON["color_mention"])
    334 		}
    335 
    336 	{
    337 		nick=$1;url=$2;props=$3;ts=$4;msg=$5
    338 		seconds=now-ts
    339 		if ( seconds >= 172800 ) {
    340 			ts = int(seconds / 86400) " days ago"
    341 		} else if ( seconds >= 86400 ) {
    342 			ts = "1 day ago"
    343 		} else if ( seconds >= 7200 ) {
    344 			ts = int(seconds / 3600) " hours ago"
    345 		} else if ( seconds >= 3600 ) {
    346 			ts = "1 hour ago"
    347 		} else if ( seconds >= 120 ) {
    348 			ts = int(seconds / 60) " minutes ago"
    349 		} else if ( seconds >= 60 ) {
    350 			ts = "1 minute ago"
    351 		} else if ( seconds == 1 ) {
    352 			ts = "1 second ago"
    353 		} else {
    354 			ts = seconds " seconds ago"
    355 		}
    356 
    357 		if ( ENVIRON["use_color"] == 1 ) {
    358 			n = split(props,prop_array,/,/)
    359 			props=""
    360 			for ( i in prop_array ) {
    361 				if ( prop_array[i] == "tls" || prop_array[i] == "gpg_trusted" ) {
    362 					prop_array[i] = colorize(fg,"green",prop_array[i])
    363 				}
    364 				if ( prop_array[i] == "notls" ) {
    365 					prop_array[i] = colorize(fg,"red",prop_array[i])
    366 				}
    367 			}
    368 
    369 			props = prop_array[1]
    370 			for (i = 2; i <= n; i++)
    371 				props = props "," prop_array[i]
    372 
    373 			nick = color_nick nick reset
    374 			ts = color_time ts reset
    375 			gsub(/#[[:alnum:]_-]+/, color_hashtag "&" reset, msg)
    376 			gsub(/@[[:alnum:]_-]+/, color_mention "&" reset, msg)
    377 		}
    378 		fmt = ENVIRON["formatter"]
    379 		if ( theme == "oneline" ) {
    380 			printf "* %s (%s %s)\n", nick, msg, ts
    381 		} else {
    382 			printf "* %s (%s)", nick, ts
    383 			if ( props )  printf " [%s]", props
    384 			printf "\n"
    385 			print msg | fmt
    386 			close(fmt)
    387 		}
    388 	}'
    389 }
    390 
    391 sort_tweets () {
    392 	case $sort_order in
    393 		ascending | descending  ) : ;;
    394 		* ) die "Sort order must be either ascending or descending." ;;
    395 	esac
    396 
    397 	$awk 'BEGIN { FS=OFS="\t" }{ split($4,a,/\./); print $0, a[0] ? a[0] :0, a[1] }' |\
    398 		sort -rn -k6,6 -k7,7 -t "$TAB" | \
    399 		limit_uniq_tweets
    400 }
    401 
    402 limit_uniq_tweets () {
    403 	$awk '
    404 		BEGIN {
    405 			FS=OFS="\t"
    406 			limit='"$limit"'
    407 			sort_order="'"$sort_order"'"
    408 		}
    409 		{
    410 			key = $5 $6 $7
    411 			NF-=2
    412 			if ( last != key ) {
    413 				lines_seen++
    414 				if ( sort_order == "ascending" ) {
    415 					lines[lines_seen] = $0
    416 				} else {
    417 					print
    418 				}
    419 				if ( limit > 0 && lines_seen >= limit )
    420 					exit
    421 			}
    422 			last = key
    423 		}
    424 		END {
    425 			if ( sort_order == "ascending" ) {
    426 				for ( i = lines_seen; i>0; i-- ) {
    427 					print lines[i]
    428 				}
    429 			}
    430 		}
    431 	'
    432 }
    433 
    434 maybe_pager () {
    435 	if [ "$use_pager" -eq 1 ];then
    436 		$pager
    437 	else
    438 		cat
    439 	fi
    440 }
    441 
    442 # Description: Prefixes every line in stdin with arguments
    443 # Synopsis: prefix column NICK URL PROPS
    444 
    445 prefix_columns () {
    446 	$awk 'BEGIN{OFS="\t"}{print "'"$1"'", "'"$2"'", "'"$3"'",$0}'
    447 }
    448 
    449 filter_tweets () {
    450 	$awk '
    451 		BEGIN {
    452 			FS=OFS="\t"
    453 			last_timeline = "'"$last_timeline"'"
    454 			srand()
    455 			now=srand()
    456 		}
    457 		function new () {
    458 			return $4 > last_timeline
    459 
    460 		}
    461 		{
    462 			nick=$1;url=$2;props=$3;ts=$4;msg=$5;
    463 			if ('"${filter_expr:-1}"') {
    464 				print
    465 			}
    466 		}
    467 	'
    468 }
    469 
    470 display_tweets () {
    471 	case $theme in
    472 		raw ) normalize_tweets | filter_tweets | sort_tweets ;;
    473 		html ) normalize_tweets | filter_tweets | sort_tweets | format_msg_html ;;
    474 		default ) normalize_tweets | filter_tweets | sort_tweets | collapse_mentions | format_msg| maybe_pager ;;
    475 		oneline ) normalize_tweets | filter_tweets | sort_tweets | collapse_mentions | format_msg "oneline" | maybe_pager ;;
    476 		* ) die "Unknown theme $theme." ;;
    477 	esac
    478 }
    479 
    480 normalize_tweets () {
    481 	$awk '
    482 		BEGIN{
    483 			FS=OFS="\t"
    484 			rfc3339 = "^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(([Zz])|([\\+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))$"
    485 			srand()
    486 			now = srand()
    487 		}
    488 		{
    489 			## handle timestamps without T
    490 			if ( substr($4,11,1) == " " ) {
    491 				$4 = substr($4,1,10) "T" substr($4,12)
    492 			}
    493 
    494 			## handle any ws as seperator
    495 			sub(/[[:space:]]+/, "\t", $4)
    496 			$0=$0
    497 
    498 			## remove escape sequences
    499 			## the trailing hyphen is important:
    500 			## https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=214783
    501 			gsub(/[^[:print:][:space:]-]/,"")
    502 
    503 			#normlize timestamp
    504 			sub(/Z$/,"+00:00",$4)
    505 
    506 			# remove leading spaces
    507 			sub(/^[[:space:]]*/,"",$4)
    508 
    509 			# add : to offset if missing
    510 			if ( match($4,/[+-][0-9]{4}$/) ) {
    511 				l = length($4)
    512 				$4 = substr($4,1,l-4) substr($4,l-3,2) ":" substr($4,l-1,2)
    513 			}
    514 
    515 			## add seconds if missing
    516 			if ( match( $4,/T[0-9]{2}:[0-9]{2}[+-]/ ) ) {
    517 				$4 = substr($4,1,RSTART + RLENGTH -2) ":00" substr($4,RSTART + RLENGTH -1 )
    518 			}
    519 
    520 			# ignore lines not matching spec
    521 			if ( NF != 5 || $4 !~ rfc3339 )
    522 				next
    523 
    524 			no_fields = split( $4, ta, /[Tt:+.-]/)
    525 			year = ta[1]; month  = ta[2]; day     = ta[3]
    526 			hour = ta[4]; minute = ta[5]; seconds = ta[6]
    527 
    528 			if ( no_fields == 9 ) {
    529 				offset = ta[8] * 3600 + ta[9] * 60
    530 				fracsecs=ta[7]
    531 			}
    532 			else {
    533 				offset = ta[7] * 3600 + ta[8] * 60
    534 				fracsecs=0
    535 			}
    536 			epoch_days = 719591
    537 
    538 			if ( month > 2 ) {
    539 				month++
    540 			} else {
    541 				year--
    542 				month+=13
    543 			}
    544 			tweet_days = (year*365)+int(year/4)-int(year/100)+int(year/400) + int(month*30.6)+ day
    545 
    546 			days_since_epoch = tweet_days - epoch_days
    547 
    548 			seconds_since_epoch = (days_since_epoch*86400)+(hour*3600)+(minute*60)+seconds
    549 
    550 			mod = substr( $4, length($4)-5, 1)
    551 			if ( mod == "+" ) {
    552 				seconds_since_epoch -= offset
    553 			} else {
    554 				seconds_since_epoch += offset
    555 			}
    556 
    557 			$4 = seconds_since_epoch "." fracsecs
    558 
    559 			if ( $4 > now )
    560 				next
    561 
    562 			print
    563 		}
    564 	'
    565 }
    566 
    567 gpg_verify () {
    568 	"$gpg_bin" --status-fd=1 --no-verbose --quiet --batch --verify "$1" 2>/dev/null
    569 }
    570 
    571 collapse_mentions () {
    572 	## TODO this *has* to be easier
    573 	export follow_file nick twturl
    574 	$awk '
    575 		function normalize_url (url,  host) {
    576 			sub(/^http:/,"https:",url)
    577 
    578 			if ( url ~ /^https:/ ) {
    579 				host = url
    580 				sub(/^https:\/\//,"", host)
    581 				sub(/\/.*/,"", host)
    582 				host = tolower(host)
    583 				url = "https://" host substr(url,9+length(host))
    584 			}
    585 			return url
    586 		}
    587 		BEGIN {
    588 			FS=" "
    589 			while ( (getline < ENVIRON["follow_file"] ) > 0 ) {
    590 				urls[normalize_url($2)] = $1
    591 			}
    592 			if ( ENVIRON["twturl"] && ENVIRON["nick"] )
    593 				urls[ENVIRON["twturl"]] = ENVIRON["nick"]
    594 			FS="\t"
    595 			OFS=FS
    596 		}
    597 		{
    598 			new_msg=""
    599 			while ( match($5,/@<[^>]+>/) ) {
    600 				new_msg = new_msg substr($5,1,RSTART-1)
    601 				n = split(substr($5,RSTART+2,RLENGTH-3),fields," ")
    602 				if ( n == 1 ) {
    603 					url = fields[1]
    604 				} else {
    605 					url = fields[2]
    606 				}
    607 
    608 				url = normalize_url(url)
    609 
    610 				if ( url in urls ) {
    611 					new_msg = new_msg "@" urls[url]
    612 				}
    613 				else {
    614 					new_msg = new_msg substr($5,RSTART,RLENGTH)
    615 				}
    616 				$5 = substr($5,RSTART+RLENGTH)
    617 			}
    618 			new_msg = new_msg $5
    619 			$5=new_msg
    620 			print
    621 		}
    622 	'
    623 }
    624 
    625 expand_mentions () {
    626 	export follow_file nick twturl
    627 	$awk '
    628 		BEGIN {
    629 			FS=" "
    630 			while ( (getline < ENVIRON["follow_file"] ) > 0 ) {
    631 				nicks[$1]=$2
    632 			}
    633 			if ( ENVIRON["twturl"] )
    634 				urls[ENVIRON["nick"]] = ENVIRON["twturl"]
    635 			FS="\t"
    636 			OFS=FS
    637 		}
    638 		{
    639 			expanded_line = ""
    640 			while ( match($2, /@[[:alnum:]_-]+/ )) {
    641 				expanded_line = expanded_line substr($2,1,RSTART-1)
    642 				nick = substr($2,RSTART+1,RLENGTH-1)
    643 				rest = substr($2,RSTART+RLENGTH)
    644 				prev = substr($2,RSTART-1,1);
    645 				expand = nick in nicks && (RSTART == 1 || match(prev, /[[:space:]]/))
    646 				if (expand) {
    647 					expanded_line = expanded_line "@<" nick " " nicks[nick] ">"
    648 				}
    649 				else {
    650 					expanded_line = expanded_line "@" nick
    651 				}
    652 				$2 = rest
    653 			}
    654 			expanded_line = expanded_line $2
    655 			$2 = expanded_line
    656 			print
    657 		}
    658 	'
    659 }
    660 
    661 draft_to_twtfile () {
    662 	[ -s "$draft_file" ] || return
    663 	pre_tweet_hook
    664 	_fracsecs=0
    665 	_timestamp_fmt=$(TZ=C date "+%Y-%m-%dT%H:%M:%S.%%06iZ")
    666 	while read -r msg;do
    667 		[ -n "$msg" ] || continue
    668 		case $msg in
    669 			/follow\ *|/unfollow\ * )
    670 				# shellcheck disable=2086
    671 				set -- ${msg#/}
    672 				if [ $# -eq 3 ]; then
    673 					"$@"
    674 					continue
    675 				fi
    676 				;;
    677 		esac
    678 		printf "$_timestamp_fmt\t%s\n" "$_fracsecs" "$msg"
    679 		_fracsecs=$((  _fracsecs + 1 ))
    680 	done < "$draft_file" | expand_mentions >> "$twtfile"
    681 	publish
    682 }
    683 
    684 cleanup () {
    685 	[ -e "$tempfile" ] && rm "$tempfile"
    686 	[ -d "$tempdir" ] && rm -r "$tempdir"
    687 }
    688 
    689 sync_twtfile () {
    690 	if [ -n "$twtfile" ] && [ -n "$twturl" ]; then
    691 		if ! curl -sS -o "$twtfile" "$twturl"; then
    692 			die "Can't sync twtfile. Aborting."
    693 		fi
    694 	else
    695 		die "Set twtfile and twturl to sync twtfile. Aborting.\n"
    696 	fi
    697 }
    698 
    699 pre_tweet_hook () {
    700 	:
    701 }
    702 
    703 post_tweet_hook () {
    704 	:
    705 }
    706 
    707 process_stat_log () {
    708 	tempfile="$follow_file.$$"
    709 
    710 	## Remove dos line endings before reading twtfiles
    711 	for file in "$cache_dir"/twtfiles/*.txt;do
    712 		sed 's/
$//' "$file" > "$file~" && mv "$file~" "$file"
    713 	done
    714 
    715 	$awk '
    716 
    717 	function get_location (file,   line, meta) {
    718 		while((getline line < file) > 0 ) {
    719 			if ( line ~ /^#[ \t]*[^ \t=]+[ \t]*=[ \t]*[^ \t]+/ ) {
    720 				sub(/^#[ \t]*/,"", line)
    721 				split(line,meta,/[ \t]*=[ \t]*/)
    722 				if ( meta[1] == "url" ) {
    723 					return meta[2]
    724 				}
    725 			}
    726 		}
    727 		return ""
    728 	}
    729 
    730 	function redirect ( nick, location ) {
    731 		following[nick] = location
    732 		printf "Following %s now at %s.\n", nick, following[nick] | stderr
    733 		changed = 1
    734 	}
    735 
    736 	BEGIN {
    737 		tempfile="'"$tempfile"'"
    738 		follow_file="'"$follow_file"'"
    739 		file      = 1
    740 		code      = 2
    741 		redirects = 3
    742 		url       = 4
    743 		while ( (getline < follow_file ) > 0 ) {
    744 			following[$1] = $2
    745 		}
    746 		stderr = "cat >&2"
    747 	}
    748 	{
    749 		match($file, /[^\/]+.txt$/)
    750 		nick = substr($file,RSTART,RLENGTH - 4 )
    751 		location = get_location($file)
    752 		if ( location && following[nick] != location ) {
    753 			redirect( nick, location )
    754 		} else if ( $code == 200 || $code == 304 ) {
    755 			if ( $redirects > 0 ) {
    756 				redirect( nick, $url )
    757 			}
    758 		} else if ( $code == 410 ) {
    759 			delete following[nick];
    760 			changed=1
    761 		} else if ( $code == 000 ) {
    762 			# curl error
    763 		} else {
    764 			printf "Fetching %s at %s returned %s.\n", nick, $url, $code | stderr
    765 		}
    766 	}
    767 	END {
    768 		if ( changed ) {
    769 			for ( nick in following ) {
    770 				print nick, following[nick] > tempfile
    771 			}
    772 			system("mv " tempfile " " follow_file)
    773 		}
    774 	}'
    775 }
    776 
    777 read_key () {
    778 	_key=
    779 	if [ -t 0 ];then
    780 		if [ -z "$_stty" ];then
    781 			_stty=$(stty -g)
    782 		fi
    783 		stty -echo -icanon min 1
    784 		_key=$(dd bs=1 count=1 2>/dev/null)
    785 		stty "$_stty"
    786 	fi
    787 }
    788 
    789 getline () {
    790 	_var=${2:-_line}
    791 	if [ -t 0 ];then
    792 		if [ -n "$BASH_VERSION" ];then
    793 			# shellcheck disable=SC2039
    794 			read -erp "$1: " "$_var"
    795 		else
    796 			printf "%s: " "$1"
    797 			IFS= read -r "$_var"
    798 		fi
    799 	fi
    800 }
    801 
    802 yesno () {
    803 	printf "%s [yN] " "$1"
    804 	read_key
    805 	case $_key in
    806 		y ) _rc=0 ;;
    807 		* ) _rc=1 ;;
    808 	esac
    809 	printf "\n"
    810 	return $_rc
    811 }
    812 
    813 parallel_curl () {
    814 	_no_args=$( args_for_curl | wc -l )
    815 
    816 	if [ "$xargs_parallel" -eq 0 ];then
    817 		unset xargs_parallel
    818 	fi
    819 
    820 	## POSIX xargs will even try to run an empty command
    821 	## therefor we wrap the call to curl in a shell that first checks
    822 	## if there any arguments
    823 
    824 	# shellcheck disable=SC2016
    825 	xargs -n "$_no_args" ${xargs_parallel+-P "$max_procs"} \
    826 		sh -c '[ $# -gt 0 ] && exec "$0" "$@"' curl $http_backend_args
    827 }
    828 
    829 set_timestamp () {
    830 	if [ -e "$cache_dir/timestamps/$1" ];then
    831 		get_timestamp "$@"
    832 	fi
    833 	$awk 'BEGIN{ srand(); print srand() > "'"$cache_dir/timestamps/$1"'"}'
    834 }
    835 
    836 get_timestamp () {
    837 	read -r "${2:-$1}" < "$cache_dir/timestamps/$1"
    838 }
    839 
    840 ################
    841 ## Subcommands #
    842 ################
    843 
    844 sync_followings () {
    845 
    846 	if [ -z "$sync_followings" ];then
    847 		die "You have to configure sync_followings in $config_file."
    848 	fi
    849 
    850 	curl --compressed -LSs "$sync_followings" > "$cache_dir/sync_followings.txt.new" || return
    851 
    852 	touch "$cache_dir/sync_followings.txt"
    853 	if cmp "$cache_dir/sync_followings.txt" "$cache_dir/sync_followings.txt.new" >/dev/null 2>&1; then
    854 		rm "$cache_dir/sync_followings.txt.new"
    855 		return
    856 	fi
    857 
    858 	tempdir="$cache_dir/tmp.$$"
    859 	mkdir -p "$tempdir" || return
    860 
    861 	sort "$follow_file"                       > "$tempdir/followings"
    862 	sort "$cache_dir/sync_followings.txt"     > "$tempdir/sync_followings.txt"
    863 	sort "$cache_dir/sync_followings.txt.new" > "$tempdir/sync_followings.txt.new"
    864 
    865 	comm -13 "$tempdir/sync_followings.txt" "$tempdir/sync_followings.txt.new" | \
    866 		comm -13 "$tempdir/followings" - | \
    867 		while read -r _nick _url; do
    868 			follow "$_nick" "$_url"
    869 		done
    870 	mv "$cache_dir/sync_followings.txt.new" "$cache_dir/sync_followings.txt"
    871 	rm -R "$tempdir"
    872 }
    873 
    874 update () {
    875 	if ! [ -s "$follow_file" ] ;then
    876 		die "You're not following anyone."
    877 	fi
    878 
    879 	if [ -e "$twtfile" ];then
    880 		ln -sf "$twtfile" "$cache_dir/twtfiles/$nick.txt"
    881 	fi
    882 
    883 	{
    884 		if [ $# -eq 0 ];then
    885 			cat "$follow_file"
    886 		else
    887 			printf "%s\n" "$@" | $awk '
    888 				BEGIN {
    889 					follow_file = "'"$follow_file"'"
    890 					while ((getline < follow_file) > 0 )
    891 						followings[$1] = $2
    892 				}
    893 				followings[$1] {
    894 					print $1 " " followings[$1]
    895 				}
    896 			'
    897 		fi
    898 	} |  while read -r nick url;do
    899 		rewrite_url
    900 		args_for_curl "$nick" "$url"
    901 	done | parallel_curl | process_stat_log
    902 
    903 	for logfile in "$log_dir"/http.log.*;do
    904 		[ -e "$logfile" ] || continue
    905 		_nick="${logfile##*.}"
    906 		$awk '{print "'"$_nick"'" ": " $0 }' "$logfile" >&2
    907 		rm "$logfile"
    908 	done
    909 }
    910 
    911 follow () {
    912 
    913 	if [ "$1" = "$nick" ]; then
    914 		die "You can't follow someone under your nick.";
    915 	fi
    916 
    917 	tempfile="$follow_file.$$"
    918 	$awk '
    919 		BEGIN {
    920 			nick = "'"$1"'"
    921 			url  = "'"$2"'"
    922 			force  = '"$force"'
    923 			stderr = "cat >&2"
    924 		}
    925 		$1 == nick || $2 == url {
    926 			if ( force ) next
    927 			printf "You are already following %s at %s.\n", $1, $2 | stderr
    928 			exit 1
    929 		}
    930 		1
    931 		END {
    932 			print nick, url
    933 		}
    934 	' "$follow_file" > "$tempfile" && mv "$tempfile" "$follow_file" && info "You're now following $1."
    935 }
    936 
    937 unfollow () {
    938 	tempfile="$follow_file.$$"
    939 
    940 	UNFOLLOW="$*" $awk '
    941 		BEGIN {
    942 			split(ENVIRON["UNFOLLOW"], unfollow, " ")
    943 			stderr = "cat >&2"
    944 		}
    945 		{
    946 			remove=0
    947 			for ( idx in unfollow ) {
    948 				if ( unfollow[idx] == $1 || unfollow[idx] == $2 ) {
    949 					remove=1
    950 					delete unfollow[idx]
    951 					break
    952 				}
    953 			}
    954 			if ( remove == 0 ) {
    955 				print
    956 			}
    957 		}
    958 		END {
    959 			for ( idx in unfollow )  {
    960 				printf "You are not following %s.\n", unfollow[idx] | stderr
    961 			}
    962 		}
    963 	' < "$follow_file" > "$tempfile" && mv "$tempfile" "$follow_file" && info "You're not following $* anymore."
    964 }
    965 
    966 following () {
    967     if [ -n "$1" ]; then
    968         maybe_update "$@"
    969         export cache_dir follow_file
    970         printf "%s\n" "$@" | $awk '
    971             BEGIN {
    972                 follow_file = ENVIRON["follow_file"]
    973                 while ( ( getline < follow_file ) > 0 )
    974                     followings[$1] = $2
    975                 close(follow_file)
    976             }
    977             {
    978                 if ( followings[$1] ) {
    979                     twtfile = ENVIRON["cache_dir"] "/twtfiles/" $1 ".txt"
    980                     while ( (getline < twtfile ) > 0 )
    981                         if ( /^#/ && $2 == "following" )
    982                             print $4 " " $5
    983                 }
    984             }
    985         ' | sort | uniq
    986 	elif [ -e "$follow_file" ];then
    987 		cat "$follow_file"
    988 	fi
    989 }
    990 
    991 url () {
    992 	for url; do
    993 		curl -s "$url" | $awk '
    994 			BEGIN {
    995 				url = "'"$url"'"
    996 			}
    997 			{
    998 				c = c $0
    999 			}
   1000 			END {
   1001 				if ( match(c,/<title>/) ) {
   1002 					start = RSTART+RLENGTH
   1003 					match(c,/<\/title>/)
   1004 					len = RSTART-start
   1005 					title = substr(c,start,len)
   1006 					gsub(/^[ \t]+|[ \t]+$/, "", title)
   1007 					sub(/\.$/,"", title)
   1008 					print title " ⌘ " url
   1009 				} else {
   1010 					print url
   1011 				}
   1012 			}
   1013 	'; done | tweet
   1014 }
   1015 
   1016 # shellcheck disable=SC2120
   1017 tweet () {
   1018 	: >"$draft_file"
   1019 
   1020 	if [ $# -eq 0 ];then
   1021 		if [ -t 0 ];then
   1022 			"$EDITOR" "$draft_file"
   1023 		else
   1024 			cat > "$draft_file"
   1025 		fi
   1026 	else
   1027 		printf "%s\n" "$@" > "$draft_file"
   1028 	fi
   1029 	draft_to_twtfile
   1030 }
   1031 
   1032 ui () {
   1033 	export FZF_DEFAULT_COMMAND="txtnish timeline -l 200 --theme=oneline 2>/dev/null"
   1034 	fzf \
   1035 		--exact \
   1036 		--ansi \
   1037 		--no-sort \
   1038 		--bind="ctrl-t:execute(txtnish tweet)+reload($FZF_DEFAULT_COMMAND),ctrl-u:reload($FZF_DEFAULT_COMMAND),enter:ignore" \
   1039 		--header 'Help: ctrl-u:Update ctrl-t:Tweet ctrl-q:Quit' \
   1040 		--preview="echo {} | $formatter" --preview-window=down:10% \
   1041 		--layout=reverse
   1042 }
   1043 
   1044 reply () {
   1045 	use_pager=0
   1046 	use_color=0
   1047 	cat <<-EOF > "$draft_file"
   1048 		
   1049 		# Please enter your tweets. Lines starting with '# ' and empty
   1050 		# lines will be ignored.
   1051 		# You can follow or unfollow feeds by starting a line with a /,
   1052 		# for example:
   1053 		# /follow foo https://example.com/foo.txt
   1054 		# Those lines will not be tweeted.
   1055 		
   1056 EOF
   1057 	timeline "$@" | $sed -e 's/^/# /' -e 's/^# $//' >> "$draft_file"
   1058 	$editor "$draft_file"
   1059 	tempfile="$draft_file.$$"
   1060 	$sed -e '/^# /d' -e '/^[[:space:]]*$/d' "$draft_file" > "$tempfile"
   1061 	mv "$tempfile" "$draft_file"
   1062 	draft_to_twtfile
   1063 }
   1064 
   1065 mailpipe () {
   1066 	if [ -z "$mail_password" ];then
   1067 		die "You have to set mail_password!"
   1068 	fi
   1069 	# shellcheck disable=SC2119
   1070 	$awk '
   1071 		NR==1,/^$/ {next }
   1072 		!pw_seen {
   1073 			if ( "password '"$mail_password"'" != $0 ) {
   1074 				exit
   1075 			}
   1076 			pw_seen=1;
   1077 			next
   1078 		}
   1079 		/^[[:space:]]*$/ || /^>/ { next }
   1080 		/-- /  { exit }
   1081 		1
   1082 	' | tweet
   1083 }
   1084 
   1085 mail () {
   1086 	have_cmd mail || die "mailx(1) not installed."
   1087 
   1088 	use_pager=0
   1089 	use_color=0
   1090 	filter_expr="new()"
   1091 	tempfile="$cache_dir/timeline.$$"
   1092 	timeline > "$tempfile"
   1093 	if [ -s "$tempfile" ]; then
   1094 		command mail -s twtxt "$@" < "$tempfile"
   1095 	fi
   1096 	rm -f "$tempfile"
   1097 }
   1098 
   1099 timeline () {
   1100 	set_timestamp last_timeline
   1101 
   1102 	case $1 in
   1103 		*://* )
   1104 			curl --compressed -Ss -L --user-agent "$user_agent" "$1" \
   1105 				| $awk 'BEGIN{OFS="\t"; url = "'"$1"'"}{ print url, url, "", $0 }' \
   1106 				| display_tweets
   1107 			exit 0
   1108 			;;
   1109 	esac
   1110 	maybe_update "$@";
   1111 
   1112 	have_gpg=0
   1113 	if have_cmd "$gpg_bin" ;then
   1114 		have_gpg=1
   1115 	fi
   1116 
   1117 	{
   1118 	if [ $# -eq 0 ];then
   1119 		following | while read -r _nick _url; do
   1120 			printf "%s %s\n" "$_nick" "$_url"
   1121 		done
   1122 		printf "%s %s\n" "$nick" "$twturl"
   1123 	else
   1124 		printf "%s\n" "$@" | $awk '
   1125 			BEGIN {
   1126 				follow_file = "'"$follow_file"'"
   1127 				while ( (getline < follow_file) > 0 )
   1128 					followings[$1] = $2
   1129 				close(follow_file)
   1130 			}
   1131 			{
   1132 				if ( followings[$1] )
   1133 					print $1, followings[$1]
   1134 			}
   1135 		'
   1136 	fi
   1137 	} | while read -r nick url;do
   1138 
   1139 		file="$cache_dir/twtfiles/$nick.txt"
   1140 
   1141 		[ -e "$file" ] || continue
   1142 
   1143 		prop=
   1144 
   1145 		if [ "$have_gpg" -eq 1 ] && [ "$check_signatures" -eq 1 ];then
   1146 			gpg_status="$(gpg_verify "$file")"
   1147 			case $gpg_status in
   1148 				*NODATA*   ) prop=gpg_unsigned ;;
   1149 				*NOPUBKEY* ) prop=gpg_signed ;;
   1150 				*VALIDSIG* ) prop=gpg_trusted ;;
   1151 				*          ) prop=gpg_unknown ;;
   1152 			esac
   1153 		fi
   1154 
   1155 		case $url in
   1156 			https://* ) prop="${prop:+$prop,}tls" ;;
   1157 			http://* ) prop="${prop:+$prop,}notls" ;;
   1158 			ipfs://* | ipns://* ) prop=ipfs ;;
   1159 		esac
   1160 
   1161 		prefix_columns "$nick" "$url" "$prop" < "$file"
   1162 	done | display_tweets
   1163 }
   1164 
   1165 update_metadata () {
   1166     [ "$add_metadata" -eq 0 ] && return
   1167 	tempfile="$twtfile.$$"
   1168 	{
   1169 		printf "# %s = %s\n" \
   1170             client "txtnish/$VERSION" \
   1171             nick "$nick"
   1172         if [ -n "$twturl" ]; then
   1173             printf "# %s = %s\n" twturl "$twturl"
   1174         fi
   1175 		if [ "$sign_twtfile" -eq 1 ];then
   1176 			gpgconf --list-options gpg \
   1177 			| awk -F: '$1 == "default-key" {print substr($10,2)}' \
   1178 			| xargs "$gpg_bin" --fingerprint \
   1179 			| awk '/Key fingerprint = / { print "# gpg_fingerprint = " substr($0,25) }'
   1180 		fi
   1181 		following | while read line; do printf "# following = %s\n" "$line" ;done
   1182 		grep -v -e '^#' "$twtfile"
   1183 	} > "$tempfile"
   1184 	mv "$tempfile" "$twtfile"
   1185 }
   1186 
   1187 publish () {
   1188 	update_metadata
   1189 	if [ "$ipfs_publish" -eq 1 ] && have_cmd ipfs;then
   1190 		_ipfs_path="$twtfile"
   1191 		if [ "$ipfs_wrap_with_dir" -eq 1 ];then
   1192 			_ipfs_args=-w
   1193 		fi
   1194 		if [ "$ipfs_recursive" -eq 1 ];then
   1195 			_ipfs_args="$_ipfs_args -r"
   1196 			_ipfs_path="${twtfile%/*}"
   1197 		fi
   1198 
   1199 		# shellcheck disable=SC2086
   1200 		if ipfs add -q $_ipfs_args "$_ipfs_path" > "$cache_dir/ipfs";then
   1201 			$awk 'END{ print $1 }' "$cache_dir/ipfs" | xargs ipfs name "publish"
   1202 		fi
   1203 	fi
   1204 	if [ "$sign_twtfile" -eq 1 ];then
   1205 		if [ -n "$sign_user" ]; then
   1206 			signopt="-u $sign_user"
   1207 			# for security, echo this
   1208 			printf "Signing as %s.\n" "$sign_user"
   1209 		else
   1210 			unset signopt
   1211 		fi
   1212 		if mkdir -p "$cache_dir/tmp.$$/";then
   1213 			tempdir="$cache_dir/tmp.$$/"
   1214 		else
   1215 			die "Can't create temporary dir $tempdir"
   1216 		fi
   1217 		if "$gpg_bin" --clearsign $signopt --output "$tempdir/${twtfile##*/}" "$twtfile";then
   1218 			twtfile="$tempdir/${twtfile##*/}"
   1219 		else
   1220 			die "Can't sign twtfile. Exiting";
   1221 		fi
   1222 	fi
   1223 	if [ -n "$scp_user" ] && [ -n "$scp_host" ];then
   1224 		if [ "$sftp_over_scp" -eq 1 ]; then
   1225 			sftp "$scp_user@$scp_host" <<-EOF
   1226 				put "$twtfile" "${scp_remote_name:-${twtfile##*/}}"
   1227 EOF
   1228 		else
   1229 			scp "$twtfile" "$scp_user@$scp_host:${scp_remote_name:-${twtfile##*/}}"
   1230 		fi
   1231 	fi
   1232 	if [ -n "$ftp_user" ] && [ -n "$ftp_host" ];then
   1233 		if curl -Ss -nT "$twtfile" "ftp://$ftp_user@$ftp_host/${ftp_remote_name:-${twtfile##*/}}";then
   1234 			info "Uploaded twtfile to ftp://$ftp_user@$ftp_host/${ftp_remote_name:-${twtfile##*/}}"
   1235 		fi
   1236 	fi
   1237 	post_tweet_hook
   1238 }
   1239 
   1240 quickstart () {
   1241 
   1242 	## Import settings from twtxt
   1243 
   1244 	if [ -e "${XDG_CONFIG_HOME:-$HOME/.config}/twtxt/config" ];then
   1245 		if ! [ -e "$follow_file" ] && yesno "Import followings from twtxt?" ;then
   1246 
   1247 			twtxt following | $awk '{ print $1, $3 }' > "$follow_file"
   1248 		fi
   1249 
   1250 		if ! [ -e "$config_file" ] && yesno "Import settings from twtxt?" ;then
   1251 			nick=$(twtxt config twtxt.nick)
   1252 			twturl=$(twtxt config twtxt.twturl)
   1253 			limit=$(twtxt config twtxt.limit_timeline)
   1254 			twtfile=$(twtxt config twtxt.twtfile)
   1255 			sort_order=$(twtxt config twtxt.sorting)
   1256 			disclose_identity=$(twtxt config twtxt.disclose_identity)
   1257 
   1258 			{
   1259 				[ -n "$nick" ]       && printf "nick=%s\n"       "$nick"
   1260 				[ -n "$twturl" ]     && printf "twturl=%s\n"     "$twturl"
   1261 				[ -n "$limit" ]      && printf "limit=%s\n"      "$limit"
   1262 				[ -n "$twtfile" ]    && printf "twtfile=%s\n"    "$twtfile"
   1263 				[ -n "$sort_order" ] && printf "sort_order=%s\n" "$sort_order"
   1264 				[ -n "$disclose_identity" ] && printf "disclose_identity=%s\n" "$disclose_identity"
   1265 
   1266 			} > "$config_file"
   1267 		fi
   1268 	fi
   1269 
   1270 	## Quickstart for new users
   1271 
   1272 	getline "Please enter your desired nick" nick
   1273 	getline "Please enter the desired location for your twtxt file" twtfile
   1274 	getline "Please enter the URL your twtxt file will be accessible from" twturl
   1275 	if yesno "Do you want to disclose your identity? Your nick and URL will be shared when making HTTP requests?";then
   1276 		disclose_identity=1
   1277 	fi
   1278 
   1279 	if yesno "Import urls to follow we-are-twtx?" ;then
   1280 		curl -Ss https://raw.githubusercontent.com/mdom/we-are-twtxt/master/we-are-twtxt.txt | \
   1281 			xargs -n2 "$program_name" "follow"
   1282 	fi
   1283 
   1284 	if yesno "Do you want to upload your twtfile with scp?";then
   1285 		getline "Please enter your scp username" scp_user
   1286 		getline "Pleaser enter scp host" scp_host
   1287 	fi
   1288 
   1289 	if yesno "Do you want to upload your twtfile with ftp?";then
   1290 		getline "Please enter your ftp username" ftp_user
   1291 		getline "Pleaser enter ftp host" ftp_host
   1292 	fi
   1293 
   1294 	if yesno "Write configuration to $config_file?";then
   1295 		if [ -e "$config_file" ];then
   1296 			mv "$config_file" "$config_file.bak"
   1297 			printf "Backup old config to %s.bak\n" "$config_file"
   1298 		fi
   1299 		cat <<-EOF > "$config_file"
   1300 			nick="$nick"
   1301 			twturl="$twturl"
   1302 			twtfile="$twtfile"
   1303 			disclose_identity="$disclose_identity"
   1304 EOF
   1305 		if [ -n "$scp_user" ] && [ -n "$scp_host" ];then
   1306 			cat <<-EOF >> "$config_file"
   1307 				scp_user="$scp_user"
   1308 				scp_host="$scp_host"
   1309 EOF
   1310 		fi
   1311 		if [ -n "$ftp_user" ] && [ -n "$ftp_host" ];then
   1312 			cat <<-EOF >> "$config_file"
   1313 				ftp_user="$ftp_user"
   1314 				ftp_host="$ftp_host"
   1315 EOF
   1316 		fi
   1317 		printf "Write new configuration to %s\n" "$config_file"
   1318 	fi
   1319 
   1320 
   1321 }
   1322 
   1323 ########################
   1324 # Command line parsing #
   1325 ########################
   1326 
   1327 check_if_valid_option () {
   1328 	case ",$options," in
   1329 		*,$1,* ) : ;;
   1330 		* ) usage "Invalid option $OPTION" ;;
   1331 	esac
   1332 }
   1333 
   1334 
   1335 set_optarg () {
   1336 	case $1 in
   1337 		-[!-]?* )
   1338 			[ -n "${1#??}" ] || usage "Option ${1%%${1#??}} requires an argument."
   1339 			OPTARG="${1#*??}"
   1340 			;;
   1341 		--?*=?* )
   1342 			[ -n "${1#*=}" ] || usage "Option ${1%%=*} requires an argument."
   1343 			OPTARG="${1#*=}"
   1344 			;;
   1345 		* )
   1346 			[ -n "$2" ] || usage "Option $1 requires an argument."
   1347 			OPTARG="$2"
   1348 			SHIFT=2
   1349 			;;
   1350 	esac
   1351 }
   1352 
   1353 set_optarg_bool () {
   1354 	case $OPTION in
   1355 		-[A-Z] ) OPTARG=0 ;;
   1356 		-[a-z] ) OPTARG=1 ;;
   1357 		--no-?* ) OPTARG=0 ;;
   1358 		--?* ) OPTARG=1 ;;
   1359 	esac
   1360 }
   1361 
   1362 check_arguments () {
   1363 	$awk '
   1364 		BEGIN {
   1365 			args_given = '"$1"'
   1366 			args_expected = split("'"$2"'",args)
   1367 			stderr = "cat >&2"
   1368 
   1369 			if ( args_given == args_expected ) exit 0
   1370 
   1371 			if ( args_given > args_expected )
   1372 				if ( args[ args_expected ] !~ /\.\.\.\]?$/ ) {
   1373 					print "Too many arguments." | stderr
   1374 					exit 1
   1375 				}
   1376 
   1377 			if ( args_given < args_expected )
   1378 				for( i = 1; i <= args_expected; i++ )
   1379 					if ( i > args_given && args[i] !~ /^\[.+\]$/ ) {
   1380 						printf "Required argument %s missing.\n", args[i] | stderr
   1381 						exit 1
   1382 					}
   1383 		}
   1384 	'
   1385 }
   1386 
   1387 call_mode () {
   1388 	while [ -n "$1" ]; do
   1389 		OPTION="$1"
   1390 		SHIFT=1
   1391 		case $1 in
   1392 			--theme | --theme=?* )
   1393 				check_if_valid_option theme
   1394 				set_optarg "$@"
   1395 				theme="$OPTARG"
   1396 				;;
   1397 			--timeout | --timeout=?* )
   1398 				check_if_valid_option timeout
   1399 				set_optarg "$@"
   1400 				timeout="$OPTARG"
   1401 				;;
   1402 			-l | --limit | --limit=?* | -l?* )
   1403 				check_if_valid_option limit
   1404 				set_optarg "$@"
   1405 				limit="$OPTARG"
   1406 				;;
   1407 			-N | --max-procs | --max-procs=?* | -N?* )
   1408 				check_if_valid_option max-procs
   1409 				set_optarg "$@"
   1410 				max_procs="$OPTARG"
   1411 				;;
   1412 			-S | --search | --search=?* | -S?* )
   1413 				check_if_valid_option search
   1414 				set_optarg "$2"
   1415 				filter_expr="$OPTARG"
   1416 				;;
   1417 			-h | --help )
   1418 				check_if_valid_option help
   1419 			       	usage
   1420 				;;
   1421 			-p | -P | --pager | --no-pager )
   1422 				check_if_valid_option pager
   1423 				set_optarg_bool
   1424 				use_pager="$OPTARG"
   1425 				;;
   1426 			-u | -U | --update | --no-update )
   1427 				check_if_valid_option update
   1428 				set_optarg_bool
   1429 				always_update="$OPTARG"
   1430 				;;
   1431 			-a | --ascending )
   1432 				check_if_valid_option ascending
   1433 				sort_order=ascending
   1434 				;;
   1435 			-d | --descending )
   1436 				check_if_valid_option descending
   1437 				sort_order=descending
   1438 				;;
   1439 			-R | --raw )
   1440 				check_if_valid_option raw
   1441 				theme=raw
   1442 				;;
   1443 			-v | --verbose )
   1444 				check_if_valid_option verbose
   1445 				verbose=1
   1446 				;;
   1447 			-f | --force )
   1448 				check_if_valid_option force
   1449 				force=1
   1450 				;;
   1451 			-- )
   1452 				shift
   1453 				break
   1454 				;;
   1455 			-[!-]?* )
   1456 				shift
   1457 				## dash 0.5.7 can't handle direct expansion
   1458 				_tmp=${OPTION#??}
   1459 				set -- "${OPTION%$_tmp}" "-$_tmp" "$@"
   1460 				SHIFT=0
   1461 				;;
   1462 			-* )
   1463 				usage "Invalid option $1."
   1464 				;;
   1465 			* )
   1466 				break
   1467 				;;
   1468 		esac
   1469 		shift "$SHIFT"
   1470 	done
   1471 
   1472 	if [ -n "$arguments" ]; then
   1473 		if ! check_arguments $# "$arguments"; then
   1474 			usage
   1475 			exit 1
   1476 		fi
   1477 	fi
   1478 
   1479 	case $mode in
   1480 		*-* ) mode="${mode%-*}_${mode#*-}"
   1481 	esac
   1482 
   1483 	$mode "$@"
   1484 }
   1485 
   1486 usage_main () {
   1487 	if [ -n "$1" ];then
   1488 		printf "%s\n" "$1" >&2;
   1489 		exec >&2
   1490 	fi
   1491 	cat <<-EOF
   1492 		usage: $program_name COMMAND [OPTIONS...]
   1493 
   1494 		  Command:
   1495 		    tweet           Append a new tweet to your twtxt file.
   1496 		    timeline        Retrieve your personal timeline.
   1497 		    follow          Add a new source to your followings.
   1498 		    unfollow        Remove an existing source from your followings.
   1499 		    following       Return the list of sources you're following.
   1500 		    reply           Reply to tweets.
   1501 		    publish         Publish your twtfile.
   1502 		    sync-followings Sync followings from remote file.
   1503 		    mail            Send new tweets per mail.
   1504 		    url             Share urls
   1505 		    ui              Start fzf based user-interface (experimental).
   1506 
   1507 		  Options:
   1508 		    -h, --help      Print a help message and exit.
   1509 		    -V, --version   Print version and exit.
   1510 
   1511 EOF
   1512 	if [ -n "$1" ];then
   1513 		exit 1
   1514 	else
   1515 		exit 0
   1516 	fi
   1517 }
   1518 
   1519 usage () {
   1520 	_err=$1
   1521 	if [ -n "$_err" ];then
   1522 		printf "%s\n" "$_err" >&2
   1523 		exec >&2
   1524 	fi
   1525 
   1526 	cat <<-EOF
   1527 		usage: $program_name $mode [OPTIONS...]${arguments:+ $arguments}
   1528 
   1529 		Synopsis:
   1530 		  $synopsis
   1531 
   1532 		Options:
   1533 EOF
   1534 
   1535 	options="$options,"
   1536 	while [ -n "$options" ] ;do
   1537 		c=${options%%,*}
   1538 		case $c in
   1539 			help )       printf "  -h, --help\n\tPrint a help message and exit.\n" ;;
   1540 			limit )      printf "  -l, --limit\n\t NUM Limit total numer of tweets shown.\n" ;;
   1541 			ascending )  printf "  -a, --ascending\n\tSort timeline in ascending order.\n" ;;
   1542 			descending ) printf "  -d, --descending\n\tSort timeline in descending order.\n" ;;
   1543 			pager )      printf "  -p, --pager\n\tUse pager to display content.\n" ;;
   1544 			no-pager )   printf "  -P, --no-pager\n\tDo not use pager to display content.\n" ;;
   1545 			update )     printf "  -u, --update\n\tUpdate sources.\n" ;;
   1546 			no-update )  printf "  -U, --no-update\n\tDo not update sources.\n" ;;
   1547 			raw )        printf "  -R, --raw\n\tPrint raw timeline.\n" ;;
   1548 			max-procs )  printf "  -N, --max-procs NUM\n\tUse NUM parallel download processes.\n" ;;
   1549 			search )     printf "  -S, --search EXP\n\tFilter tweets\n" ;;
   1550 			config )     printf "  -c, --config CFG\n\tSpecify a custom config file location.\n" ;;
   1551 			force )      printf "  -f, --force\n\tDisable safety checks and force action.\n" ;;
   1552 			theme )      printf "  --theme THEME\n\tUse theme to display timeline.\n" ;;
   1553 			timeout )    printf "  --timeout SECONDS\n\tMaximum time in seconds to fetch a feed.\n" ;;
   1554 		esac
   1555 		options=${options#$c,}
   1556 	done
   1557 
   1558 	## Always end usage with a empty line
   1559 	printf "\n"
   1560 
   1561 	if [ -n "$_err" ];then
   1562 		exit 1
   1563 	else
   1564 		exit 0
   1565 	fi
   1566 }
   1567 
   1568 check_curl () {
   1569 	if ! have_cmd curl;then
   1570 		die "curl has to be installed."
   1571 	fi
   1572 
   1573 	oIFS="$IFS"
   1574 	IFS=.
   1575 	set -- $(curl -V | $awk '{print $2;exit}')
   1576 	if [ "$1" -lt 7 ] || [ "$1" -eq 7 ] && [ "$2" -lt 26 ];then
   1577 		die "Need at least curl 7.26.0."
   1578 	fi
   1579 
   1580 	IFS="$oIFS"
   1581 }
   1582 
   1583 #########
   1584 ## Main #
   1585 #########
   1586 
   1587 main() {
   1588 
   1589 	trap cleanup EXIT
   1590 
   1591 	create_dir "$config_dir"
   1592 	create_dir "$cache_dir"
   1593 	create_dir "$cache_dir/twtfiles"
   1594 	create_dir "$cache_dir/timestamps"
   1595 	create_dir "$log_dir"
   1596 
   1597 	if ! [ -e "$follow_file" ];then
   1598 		:> "$follow_file"
   1599 	fi
   1600 
   1601 	check_curl
   1602 
   1603 	while [ -n "$1" ]; do
   1604 		SHIFT=1
   1605 		OPTION="$1"
   1606 		case $1 in
   1607 			-V | --version )
   1608 				printf "%s\n" "$VERSION"
   1609 				exit 0
   1610 				;;
   1611 			-h | --help )
   1612 				usage_main
   1613 				;;
   1614 			-c | --config | --config=?* | -c?*  )
   1615 				set_optarg "$@"
   1616 				config_file="$OPTARG"
   1617 				[ -e "$config_file" ] || die "Missing configuration file '$config_file'";
   1618 				;;
   1619 			-??* )
   1620 				shift
   1621 				## dash 0.5.7 can't handle direct expansion
   1622 				_tmp=${OPTION#??}
   1623 				set -- "${OPTION%$_tmp}" "-$_tmp" "$@"
   1624 				SHIFT=0
   1625 				;;
   1626 			-* )
   1627 				usage "Invalid option $1"
   1628 				;;
   1629 			* )
   1630 				break
   1631 				;;
   1632 		esac
   1633 		shift "$SHIFT"
   1634 	done
   1635 
   1636 	shift $(( OPTIND - 1))
   1637 
   1638 	read_config
   1639 
   1640 	mode=$1
   1641 
   1642 	if [ -z "$mode" ];then
   1643 		usage_main
   1644 		exit 1
   1645 	fi
   1646 
   1647 	shift
   1648 
   1649 	case $mode in
   1650 		update )
   1651 			synopsis="Fetching new twtfiles from all your sources."
   1652 			options="help,verbose,max-procs,timeout"
   1653 			;;
   1654 		follow )
   1655 			synopsis="Add a new source to your followings."
   1656 			arguments="NICK SOURCE"
   1657 			options="help,verbose,force"
   1658 			;;
   1659 		unfollow )
   1660 			synopsis="Remove an existing source from your followings."
   1661 			arguments="NICK..."
   1662 			options="help,verbose"
   1663 			;;
   1664 		following )
   1665 			synopsis="Return the list of sources you're following."
   1666 			options="help,verbose"
   1667 			;;
   1668 		timeline | view )
   1669 			synopsis="Display timeline."
   1670 			arguments="[NICK...]"
   1671 			options="help,limit,verbose,ascending,descending,update,no-update,max-procs,search,pager,no-pager,raw,theme,timeout"
   1672 			;;
   1673 		ui )
   1674 			synopsis="Start fzf based user-interface (experimental)."
   1675 			options="help,verbose"
   1676 			;;
   1677 		reply )
   1678 			synopsis="Reply to tweets."
   1679 			arguments="[NICK]"
   1680 			options="help,limit,verbose,ascending,descending,update,no-update,max-procs,search,timeout"
   1681 			;;
   1682 		publish )
   1683 			synopsis="Publish your twtfile."
   1684 			options="help,verbose"
   1685 			;;
   1686 		tweet )
   1687 			synopsis="Append a new tweet to your twtxt file."
   1688 			arguments="[TWEET...]"
   1689 			options="help,verbose"
   1690 			;;
   1691 		url )
   1692 			synopsis="Share urls with your followers."
   1693 			arguments="URL..."
   1694 			options="help,verbose"
   1695 			;;
   1696 		sync-followings )
   1697 			synopsis="Sync followings from a remote file."
   1698 			options="help,verbose"
   1699 			;;
   1700 		mail )
   1701 			synopsis="Mail new tweets."
   1702 			arguments="ADDRESS..."
   1703 			options="help,verbose,ascending,descending,update,no-update"
   1704 			;;
   1705 		mailpipe )
   1706 			synopsis="Read mails from stdin and tweet them."
   1707 			options="help,verbose"
   1708 			;;
   1709 		quickstart )
   1710 			synopsis="Import settings from twtxt."
   1711 			options="help,verbose"
   1712 			;;
   1713 		* ) printf "Unknown mode %s.\n" "$mode" >&2; usage_main; exit 1;;
   1714 
   1715 	esac
   1716 
   1717 	call_mode "$@"
   1718 
   1719 	exit 0
   1720 }
   1721 
   1722 main "$@"