plumber

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

plumber (7468B)


      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)$", \
     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)$",\
     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/(watch|embed).*", "ytopener '%s'"],
     38 	["^http(|s)://(|www\.)youtu.be/[\w\-]{10,}\?t=\d+", "ytopener '%s'"],
     39 	["^http(|s)://(|www\.)yewtu.be/(watch|embed).*", "ytopener '%s'"],
     40 	["^http(|s)://.*", "webopener '%s'"],
     41 	["^mailto:.*", "mailcomposer '%s'"],
     42 	["^dance:.*", "danceopener '%s'"],
     43 	["^dict:.*", "dictopener '%s'"],
     44 	["^dhl:.*", "dhlopener '%s'"],
     45 	["^doi:.*", "doiopener '%s'"],
     46 	["^finger://.*", "fingeropener '%s'"],
     47 	["^ftp(|s)://.*", "ftpopener '%s'"],
     48 	["^sftp://.*", "ftpopener '%s'"],
     49 	["^ldap(|s)://.*", "ldapopener '%s'"],
     50 	["^moz://:*", "mozopener '%s'"],
     51 	["^mms://.*", "mediaopener '%s'"],
     52 	["^paper:.*", "paperopener '%s'"],
     53 	["^pubmed:.*", "pubmedopener '%s'"],
     54 	["^rfc:.*", "rfcopener '%s'"],
     55 	["^rp:.*", "rpopener '%s'"],
     56 	["^rpo:.*/", "rpopener -o '%s'"],
     57 	["^rtmp://.*", "mediaopener '%s'"],
     58 	["^rtmfp://.*", "mediaopener '%s'"],
     59 	["^rtsp://.*", "mediaopener '%s'"],
     60 	["^searx:.*", "searxopener '%s'"],
     61 	["^udp://.*", "mediaopener '%s'"],
     62 	["^telnet(s|)(4|6|)://.*", "telnetopener '%s'"],
     63 	["^scm:.*", "scmopener '%s'"],
     64 	["^ssh://.*", "sshopener '%s'"],
     65 	["^tv://.*", "tvopener '%s'"],
     66 	["^yt://.*", "ytopener '%s'"],
     67 	["^youtube://.*", "ytopener '%s'"],
     68 	["^addr:.*", "addropener '%s'"],
     69 	["^blog://.*", "blogopener '%s'"],
     70 	["^portage:.*", "portageopener '%s'"],
     71 	# Just a try to imitate Ubuntu.
     72 	["^apt:.*", "portageopener '%s'"],
     73 	["^wikipedia:.*", "wikipediaopener '%s'"],
     74 	["^wikipedia-[a-zA-Z]*:.*", "wikipediaopener '%s'"],
     75 	["^wiki:.*", "wikiopener '%s'"],
     76 	["^w:.*", "wikiopener '%s'"],
     77 	["^@.*", False],
     78 	[".*@.*", "mailcomposer '%s'"],
     79 	[".*", ["fileopener '%s'", "webopener '%s'"]],
     80 ]
     81 
     82 menucmd = "dmenu -p \"URI to plumb> \" -i"
     83 
     84 def runcmd(cmd, arg=None):
     85 	fd = open("/dev/null")
     86 	if arg != None:
     87 		cmd = cmd % (arg)
     88 	# Run in background all the time.
     89 	p = Popen("%s &" % (cmd), shell=True, stdout=fd, stderr=fd)
     90 	p.wait()	
     91 	fd.close()
     92 
     93 def runmenu(menucmd, selectlines):
     94 	fd = open("/dev/null")
     95 	p = Popen(menucmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=fd)
     96 	output = p.communicate(input=("\n".join(selectlines)).encode("utf-8"))[0]
     97 	fd.close()
     98 	return output.decode().strip()
     99 
    100 def parsetext(text):
    101 	urire = re.compile("[a-z0-9A-Z]+:[a-z0-9A-Z/\-\.?=%+_]+")
    102 	return re.findall(urire, text)
    103 
    104 def trimstr(s):
    105 	framing = [
    106 		["<", ">"], ["\"", "\""], ["'", "'"],\
    107 		["(", ")"], ["[", "]"] \
    108 	]
    109 
    110 	matchstr = s
    111 
    112 	didtrim = 1
    113 	while didtrim == 1:
    114 		if len(matchstr) == 0:
    115 			break
    116 
    117 		didtrim = 0
    118 		for frame in framing:
    119 			if matchstr[0] == frame[0] and \
    120 					matchstr[-1] == frame[1]:
    121 				matchstr = matchstr[1:-1]
    122 				didtrim = 1
    123 				break
    124 			elif matchstr[0] == frame[0] and \
    125 					matchstr[-2] == frame[1]:
    126 				# When the delimiter is at the end of some
    127 				# sentence.
    128 				matchstr = matchstr[1:-2]
    129 				didtrim = 1
    130 				break
    131 
    132 	if len(matchstr) == 0:
    133 		return matchstr
    134 
    135 	# markdown links
    136 	if matchstr[0] == "[" and matchstr[-1] == ")":
    137 		if "](" in matchstr:
    138 			matchstr = matchstr[:-1].rsplit("](",1)[1]
    139 	if len(matchstr) == 0:
    140 		return matchstr
    141 
    142 	if matchstr[0] == "[" and matchstr[-1] == "]":
    143 		if "][" in matchstr:
    144 			matchstr = matchstr[1:].rsplit("][",1)[0]
    145 		else:
    146 			matchstr = matchstr[1:-1]
    147 
    148 	return matchstr
    149 
    150 def usage(app):
    151 	app = os.path.basename(app)
    152 	sys.stderr.write("usage: %s [-hdemty] string\n" % (app))
    153 	sys.stderr.write("-h Show this help.\n")
    154 	sys.stderr.write("-d Set debug loglevel.\n")
    155 	sys.stderr.write("-e Do not handle arbitrary results.\n")
    156 	sys.stderr.write("-m Use menu.\n")
    157 	sys.stderr.write("-t Parse text.\n")
    158 	sys.stderr.write("-y Dry run.\n")
    159 	sys.exit(1)
    160 
    161 def main(args):
    162 	global menucmd
    163 
    164 	try:
    165 		opts, largs = getopt.getopt(args[1:], "hdemty")
    166 	except getopt.GetoptError as err:
    167 		print(str(err))
    168 		usage(args[0])
    169 
    170 	logger = logging.getLogger("plumber")
    171 	formatter = logging.Formatter("%(name)s: %(message)s")
    172 	shandler = logging.handlers.SysLogHandler(\
    173 			facility=logging.handlers.SysLogHandler.LOG_DAEMON,\
    174 			address="/dev/log")
    175 	shandler.setFormatter(formatter)
    176 	logger.addHandler(shandler)
    177 
    178 	if os.getenv("PLUMBER_DEBUG"):
    179 		logger.setLevel(logging.DEBUG)
    180 	
    181 	dodebug = False
    182 	dodryrun = False
    183 	dotextparsing = False
    184 	dodefault = True
    185 	domenu = False
    186 	for o, a in opts:
    187 		if o == "-h":
    188 			usage(args[0])
    189 		elif o == "-d":
    190 			logger.setLevel(logging.DEBUG)
    191 		elif o == "-e":
    192 			dodefault = False
    193 		elif o == "-m":
    194 			domenu = True
    195 		elif o == "-t":
    196 			dotextparsing = True
    197 		elif o == "-y":
    198 			dodryrun = True
    199 		else:
    200 			assert False, "unhandled option"
    201 
    202 	if dotextparsing == True:
    203 		if len(largs) < 1:
    204 			itext = sys.stdin.read()
    205 		else:
    206 			itext = " ".join(largs)
    207 		largs = parsetext(itext)
    208 		logger.debug("text parsing returned: %s" % (largs))
    209 		# Do not handle arbitrary results.
    210 		dodefault = False
    211 	else:
    212 		if len(largs) < 1:
    213 			largs = sys.stdin.read().split("\n")
    214 
    215 	if domenu == True:
    216 		rval = runmenu(menucmd, largs)
    217 		if rval == "":
    218 			return 1
    219 		largs = [rval]
    220 		logger.debug("menu selection returned: %s" % (largs))
    221 
    222 	for arg in largs:
    223 		if len(arg) < 1:
    224 			continue
    225 		logger.debug("matchstr: '%s'" % (arg))
    226 
    227 		matchstr = trimstr(arg)
    228 		if len(matchstr) == 0:
    229 			logger.debug("Trimming reduced string too much.")
    230 			break
    231 
    232 		matchstr = matchstr.replace("'", "'\\''")
    233 		if matchstr[0:2] == "//":
    234 			matchstr = "http:%s" % (matchstr)
    235 		if matchstr != arg:
    236 			logger.debug("'%s' -> '%s'" % (arg, matchstr))
    237 
    238 		frule = None
    239 		for rule in plumbrules:
    240 			if re.search(rule[0], matchstr) != None:
    241 				frule = rule
    242 				break
    243 
    244 		if frule == False:
    245 			if logger.level == logging.DEBUG:
    246 				logger.debug("Plumb string is invalid.")
    247 			else:
    248 				logger.info("Plumb string is invalid.")
    249 			continue
    250 
    251 		if frule == None:
    252 			if logger.level == logging.DEBUG:
    253 				logger.debug("No match found.")
    254 			else:
    255 				logger.info("No match found.")
    256 			continue
    257 
    258 		if dodefault == False and frule == plumbrules[-1][0]:
    259 			logger.debug("found default match, won't continue")
    260 			continue
    261 
    262 		logger.debug("found match: '%s' -> '%s'" % (frule[0], frule[1]))
    263 
    264 		if frule[0] == ".*":
    265 			if os.path.exists(matchstr):
    266 				rcmd = frule[1][0]
    267 				matchstr = os.path.realpath(matchstr)
    268 			else:
    269 				rcmd = frule[1][1]
    270 		else:
    271 			rcmd = frule[1]
    272 
    273 		if dodebug:
    274 			logger.debug("running cmd: '%s'" % (rcmd % (matchstr)))
    275 
    276 		if dodryrun == False:
    277 			runcmd(rcmd, matchstr)
    278 
    279 	return 0
    280 
    281 if __name__ == "__main__":
    282 	sys.exit(main(sys.argv))
    283