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