plumber

Plumber – a modern approach to plumbing
git clone git://r-36.net/plumber
Log | Files | Refs | README | LICENSE

plumber (7960B)


      1 #!/usr/bin/env python3
      2 # coding=utf-8
      3 #
      4 # Copy me if you can.
      5 # by 20h
      6 #
      7 
      8 import sys
      9 import os
     10 import os.path
     11 import re
     12 import getopt
     13 from subprocess import Popen, PIPE
     14 import time
     15 import logging
     16 import logging.handlers
     17 
     18 # regexp    command (%s as file)    
     19 plumbrules = [ 
     20 	["^/.*", "fileopener '%s'"],
     21 	["^> .*", "run '%s'"],
     22 	["^file://.*", "mailcapopener '%s'"],
     23 	["^.*://.*wikipedia.*/.*\\.(jpeg|jpg|png|gif|xpm|JPG|JPEG|PNG|XPM|GIF)$", \
     24 			"webopener '%s'"],
     25 	["^.*://.*\\.(jpeg|jpg|png|gif|xpm|JPG|JPEG|PNG|XPM|GIF|svg|SVG|svgz|SVGZ|webp|WEBP)$", \
     26 			"imageopener '%s'"],
     27 	["^.*://.*\\.(pdf|PDF)$", "pdfopener '%s'"],
     28 	["^gopher(|s)://.*\\.(txt|TXT|patch|PATCH|diff|DIFF)$", "textgopheropener '%s'"],
     29 	["^.*://.*\\.(txt|TXT|patch|PATCH|diff|DIFF)$", "textwebopener '%s'"],
     30 	["^.*://.*\\.(mpg|MPG|mp3|MP3|mp4|MP4|FLAC|flac|ogg|OGG|m3u|M3U|m3u8|M3U8|flv|FLV|webm|WEBM|opus|OPUS|mov|MOV|mkv|MKV|ogv|OGV|wav|WAV|avi|AVI)$",\
     31 			"mediaopener '%s'"],
     32 	["^dvb://.*", "tvopener '%s'"],
     33 	["^geo:.*", "geoopener '%s'"],
     34 	["^gopher(|s)://.*", "gopheropener '%s'"],
     35 	["^http://sprunge.us/.*", "textwebopener '%s'"],
     36 	["^http://ix.io/.*", "textwebopener '%s'"],
     37 	["^http(|s)://(|www\\.)youtube.com/feeds/videos.xml\\?channel_id=.*", "feedopener '%s'"],
     38 	["^http(|s)://(|www\\.)yewtu.be/feed/channel/.*", "feedopener '%s'"],
     39 	["^http(|s)://(|www\\.)youtube.com/(watch|embed).*", "ytopener '%s'"],
     40 	["^http(|s)://(|www\\.)youtu.be/[\\w\\-]{10,}\\?t=\\d+", "ytopener '%s'"],
     41 	["^http(|s)://(|www\\.)yewtu.be/(watch|embed).*", "ytopener '%s'"],
     42 	["^http(|s)://.*", "webopener '%s'"],
     43 	["^mailto:.*", "mailcomposer '%s'"],
     44 	["^dance:.*", "danceopener '%s'"],
     45 	["^dict:.*", "dictopener '%s'"],
     46 	["^dhl:.*", "dhlopener '%s'"],
     47 	["^doi:.*", "doiopener '%s'"],
     48 	["^finger://.*", "fingeropener '%s'"],
     49 	["^ftp(|s)://.*", "ftpopener '%s'"],
     50 	["^sftp://.*", "ftpopener '%s'"],
     51 	["^ldap(|s)://.*", "ldapopener '%s'"],
     52 	["^moz://:*", "mozopener '%s'"],
     53 	["^mms://.*", "mediaopener '%s'"],
     54 	["^news://.*", "newsopener '%s'"],
     55 	["^newspost://.*", "newsopener '%s'"],
     56 	["^newsreply://.*", "newsopener '%s'"],
     57 	["^nntp://.*", "newsopener '%s'"],
     58 	["^snews://.*", "newsopener '%s'"],
     59 	["^paper:.*", "paperopener '%s'"],
     60 	["^pubmed:.*", "pubmedopener '%s'"],
     61 	["^rfc:.*", "rfcopener '%s'"],
     62 	["^rp:.*", "rpopener '%s'"],
     63 	["^rpo:.*/", "rpopener -o '%s'"],
     64 	["^rtmp://.*", "mediaopener '%s'"],
     65 	["^rtmfp://.*", "mediaopener '%s'"],
     66 	["^rtsp://.*", "mediaopener '%s'"],
     67 	["^searx:.*", "searxopener '%s'"],
     68 	["^udp://.*", "mediaopener '%s'"],
     69 	["^telnet(s|)(4|6|)://.*", "telnetopener '%s'"],
     70 	["^scm:.*", "scmopener '%s'"],
     71 	["^ssh://.*", "sshopener '%s'"],
     72 	["^tv://.*", "tvopener '%s'"],
     73 	["^yt://.*", "ytopener '%s'"],
     74         ["^ytdl://.*", "ytopener '%s'"],
     75 	["^youtube://.*", "ytopener '%s'"],
     76 	["^addr:.*", "addropener '%s'"],
     77 	["^blog://.*", "blogopener '%s'"],
     78 	["^portage:.*", "portageopener '%s'"],
     79 	# Just a try to imitate Ubuntu.
     80 	["^apt:.*", "portageopener '%s'"],
     81 	["^cso:.*", "csoopener '%s'"],
     82 	["^wais:.*", "waisopener '%s'"],
     83 	["^wikipedia:.*", "wikipediaopener '%s'"],
     84 	["^wikipedia-[a-zA-Z]*:.*", "wikipediaopener '%s'"],
     85 	["^wiki:.*", "wikiopener '%s'"],
     86 	["^w:.*", "wikiopener '%s'"],
     87 	["^@.*", False],
     88 	[".*@.*", "mailcomposer '%s'"],
     89 	[".*", ["fileopener '%s'", "webopener '%s'"]],
     90 ]
     91 
     92 menucmd = "dmenu -p \"URI to plumb> \" -i"
     93 
     94 def runcmd(cmd, arg=None):
     95 	fd = open("/dev/null")
     96 	if arg != None:
     97 		cmd = cmd % (arg)
     98 	# Run in background all the time.
     99 	p = Popen("%s &" % (cmd), shell=True, stdout=fd, stderr=fd)
    100 	p.wait()	
    101 	fd.close()
    102 
    103 def runmenu(menucmd, selectlines):
    104 	fd = open("/dev/null")
    105 	p = Popen(menucmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=fd)
    106 	output = p.communicate(input=("\n".join(selectlines)).encode("utf-8"))[0]
    107 	fd.close()
    108 	return output.decode().strip()
    109 
    110 def parsetext(text):
    111 	urire = re.compile("[a-z0-9A-Z]+:[a-z0-9A-Z/\\-\\.?=%+_]+")
    112 	return re.findall(urire, text)
    113 
    114 def trimstr(s):
    115 	framing = [
    116 		["<", ">"], ["\"", "\""], ["'", "'"],\
    117 		["(", ")"], ["[", "]"] \
    118 	]
    119 
    120 	matchstr = s
    121 
    122 	didtrim = 1
    123 	while didtrim == 1:
    124 		if len(matchstr) == 0:
    125 			break
    126 
    127 		didtrim = 0
    128 		for frame in framing:
    129 			if matchstr[0] == frame[0] and \
    130 					matchstr[-1] == frame[1]:
    131 				matchstr = matchstr[1:-1]
    132 				didtrim = 1
    133 				break
    134 			elif matchstr[0] == frame[0] and \
    135 					matchstr[-2] == frame[1]:
    136 				# When the delimiter is at the end of some
    137 				# sentence.
    138 				matchstr = matchstr[1:-2]
    139 				didtrim = 1
    140 				break
    141 
    142 	if len(matchstr) == 0:
    143 		return matchstr
    144 
    145 	# markdown links
    146 	if matchstr[0] == "[" and matchstr[-1] == ")":
    147 		if "](" in matchstr:
    148 			matchstr = matchstr[:-1].rsplit("](",1)[1]
    149 	if len(matchstr) == 0:
    150 		return matchstr
    151 
    152 	if matchstr[0] == "[" and matchstr[-1] == "]":
    153 		if "][" in matchstr:
    154 			matchstr = matchstr[1:].rsplit("][",1)[0]
    155 		else:
    156 			matchstr = matchstr[1:-1]
    157 
    158 	return matchstr
    159 
    160 def usage(app):
    161 	app = os.path.basename(app)
    162 	sys.stderr.write("usage: %s [-hdemty] string\n" % (app))
    163 	sys.stderr.write("-h Show this help.\n")
    164 	sys.stderr.write("-d Set debug loglevel.\n")
    165 	sys.stderr.write("-e Do not handle arbitrary results.\n")
    166 	sys.stderr.write("-m Use menu.\n")
    167 	sys.stderr.write("-t Parse text.\n")
    168 	sys.stderr.write("-y Dry run.\n")
    169 	sys.exit(1)
    170 
    171 def main(args):
    172 	global menucmd
    173 
    174 	try:
    175 		opts, largs = getopt.getopt(args[1:], "hdemty")
    176 	except getopt.GetoptError as err:
    177 		print(str(err))
    178 		usage(args[0])
    179 
    180 	logger = logging.getLogger("plumber")
    181 	formatter = logging.Formatter("%(name)s: %(message)s")
    182 	shandler = logging.handlers.SysLogHandler(\
    183 			facility=logging.handlers.SysLogHandler.LOG_DAEMON,\
    184 			address="/dev/log")
    185 	shandler.setFormatter(formatter)
    186 	logger.addHandler(shandler)
    187 
    188 	if os.getenv("PLUMBER_DEBUG"):
    189 		logger.setLevel(logging.DEBUG)
    190 	
    191 	dodebug = False
    192 	dodryrun = False
    193 	dotextparsing = False
    194 	dodefault = True
    195 	domenu = False
    196 	for o, a in opts:
    197 		if o == "-h":
    198 			usage(args[0])
    199 		elif o == "-d":
    200 			logger.setLevel(logging.DEBUG)
    201 		elif o == "-e":
    202 			dodefault = False
    203 		elif o == "-m":
    204 			domenu = True
    205 		elif o == "-t":
    206 			dotextparsing = True
    207 		elif o == "-y":
    208 			dodryrun = True
    209 		else:
    210 			assert False, "unhandled option"
    211 
    212 	if dotextparsing == True:
    213 		if len(largs) < 1:
    214 			itext = sys.stdin.read()
    215 		else:
    216 			itext = " ".join(largs)
    217 		largs = parsetext(itext)
    218 		logger.debug("text parsing returned: %s" % (largs))
    219 		# Do not handle arbitrary results.
    220 		dodefault = False
    221 	else:
    222 		if len(largs) < 1:
    223 			largs = sys.stdin.read().split("\n")
    224 
    225 	if domenu == True:
    226 		rval = runmenu(menucmd, largs)
    227 		if rval == "":
    228 			return 1
    229 		largs = [rval]
    230 		logger.debug("menu selection returned: %s" % (largs))
    231 
    232 	for arg in largs:
    233 		if len(arg) < 1:
    234 			continue
    235 		logger.debug("matchstr: '%s'" % (arg))
    236 
    237 		matchstr = trimstr(arg)
    238 		if len(matchstr) == 0:
    239 			logger.debug("Trimming reduced string too much.")
    240 			break
    241 
    242 		matchstr = matchstr.replace("'", "'\\''")
    243 		if matchstr[0:2] == "//":
    244 			matchstr = "http:%s" % (matchstr)
    245 		if matchstr != arg:
    246 			logger.debug("'%s' -> '%s'" % (arg, matchstr))
    247 
    248 		frule = None
    249 		for rule in plumbrules:
    250 			if re.search(rule[0], matchstr) != None:
    251 				frule = rule
    252 				break
    253 
    254 		if frule == False:
    255 			if logger.level == logging.DEBUG:
    256 				logger.debug("Plumb string is invalid.")
    257 			else:
    258 				logger.info("Plumb string is invalid.")
    259 			continue
    260 
    261 		if frule == None:
    262 			if logger.level == logging.DEBUG:
    263 				logger.debug("No match found.")
    264 			else:
    265 				logger.info("No match found.")
    266 			continue
    267 
    268 		if dodefault == False and frule == plumbrules[-1][0]:
    269 			logger.debug("found default match, won't continue")
    270 			continue
    271 
    272 		logger.debug("found match: '%s' -> '%s'" % (frule[0], frule[1]))
    273 
    274 		if frule[0] == ".*":
    275 			if os.path.exists(matchstr):
    276 				rcmd = frule[1][0]
    277 				matchstr = os.path.realpath(matchstr)
    278 			else:
    279 				rcmd = frule[1][1]
    280 		else:
    281 			rcmd = frule[1]
    282 
    283 		if dodebug:
    284 			logger.debug("running cmd: '%s'" % (rcmd % (matchstr)))
    285 
    286 		if dodryrun == False:
    287 			runcmd(rcmd, matchstr)
    288 
    289 	return 0
    290 
    291 if __name__ == "__main__":
    292 	sys.exit(main(sys.argv))
    293