#!/usr/bin/env python '''fpaste - a cli frontend for the fpaste.org pastebin''' # # Copyright 2008, 2010 Fedora Unity Project (http://fedoraunity.org) # Author: Jason 'zcat' Farrell # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . VERSION = '0.3.6' USER_AGENT = 'fpaste/' + VERSION SET_DESCRIPTION_IF_EMPTY = 1 # stdin, clipboard, sysinfo import os, sys, urllib, urllib2, subprocess from optparse import OptionParser, OptionGroup, SUPPRESS_HELP def is_text(text, maxCheck = 100, pctPrintable = 0.75): '''returns true if maxCheck evenly distributed chars in text are >= pctPrintable% text chars''' # e.g.: /bin/* ranges between 19% and 42% printable from string import printable nchars = len(text) if nchars == 0: return False ncheck = min(nchars, maxCheck) inc = float(nchars)/ncheck i = 0.0 nprintable = 0 while i < nchars: if text[int(i)] in printable: nprintable += 1 i += inc pct = float(nprintable) / ncheck return (pct >= pctPrintable) def confirm(prompt = "OK?"): '''prompt user for yes/no input and return True or False''' prompt += " [y/N]: " try: ans = raw_input(prompt) except EOFError: # already read sys.stdin and hit EOF # rebind sys.stdin to user tty (unix-only) try: mytty = os.ttyname(sys.stdout.fileno()) sys.stdin = open(mytty) ans = raw_input() except: print >> sys.stderr, "could not rebind sys.stdin to %s after sys.stdin EOF" % mytty return False if ans.lower().startswith("y"): return True else: return False def paste(text, options): '''send text to fpaste.org and return the URL''' import re if not text: print >> sys.stderr, "No text to send." return False params = urllib.urlencode({'title': options.desc, 'author': options.nick, 'lexer': options.lang, 'content': text, 'expire_options': options.expires}) pasteSizeKiB = len(params)/1024.0 if pasteSizeKiB >= 512: # 512KiB appears to be the current hard limit (20110404); old limit was 16MiB print >> sys.stderr, "WARNING: your paste size (%.1fKiB) is very large and may be rejected by the server. A pastebin is NOT a file hosting service!" % (pasteSizeKiB) # verify that it's most likely *non-binary* data being sent. if not is_text(text): print >> sys.stderr, "WARNING: your paste looks a lot like binary data instead of text." if not confirm("Send binary data anyway?"): return False req = urllib2.Request(url='http://fpaste.org/', data=params, headers={'User-agent': USER_AGENT}) if options.proxy: if options.debug: print >> sys.stderr, "Using proxy: %s" % options.proxy req.set_proxy(options.proxy, 'http') print >> sys.stderr, "Uploading (%.1fKiB)..." % pasteSizeKiB try: f = urllib2.urlopen(req) except IOError, e: if hasattr(e, 'reason'): print >> sys.stderr, "Error Uploading: %s" % e.reason elif hasattr(e, 'code'): print >> sys.stderr, "Server Error: %d - %s" % (e.code, e.msg) if e.code == 500: print >> sys.stderr, "500 often means your paste was too large. You tried uploading %dKiB. A pastebin is NOT a file hosting service!" % (pasteSizeKiB) if options.debug: print f.read() return False url = f.geturl() if re.match('http://fpaste.org/?.+', url): return url elif urllib2.urlparse.urlsplit(url).path == '/static/limit/': # instead of returning a 500 server error, fpaste.org now returns "http://fedoraunity.org/static/limit/" if paste too large print >> sys.stderr, "Error: paste size (%.1fKiB) exceeded server limit. %s" % (pasteSizeKiB, url) return False else: print >> sys.stderr, "Invalid fpaste URL '%s' returned. This should not happen. Use --debug to see server output" % url if options.debug: print f.read() return False def sysinfo(show_stderr = False, show_successful_cmds = True, show_failed_cmds = True): '''returns commonly requested (and some fedora-specific) system info''' # 'ps' output below has been anonymized: -n for uid vs username, and -c for short processname # cmd name, command, command2 fallback, command3 fallback, ... cmdlist = [ ('OS Release', '''lsb_release -ds''', '''cat /etc/*-release | uniq''', 'cat /etc/issue', 'cat /etc/motd'), ('Kernel', '''uname -r ; cat /proc/cmdline'''), ('Desktop(s) Running', '''ps -eo comm= | egrep '(gnome-session|kdeinit|xfce.?-session|fluxbox|blackbox|hackedbox|ratpoison|enlightenment|icewm-session|od-session|wmaker|wmx|openbox-lxde|openbox-gnome-session|openbox-kde-session|mwm|e16|fvwm|xmonad|sugar-session)' '''), ('Desktop(s) Installed', '''ls -m /usr/share/xsessions/ | sed 's/\.desktop//g' '''), ('SELinux Status', '''sestatus''', '''/usr/sbin/sestatus''', '''getenforce''', '''grep -v '^#' /etc/sysconfig/selinux'''), ('SELinux Error Count', '''selinuxenabled && (grep avc: /var/log/messages; ausearch -m avc -ts today)2>/dev/null|egrep -o "comm=\\"[^ ]+"|sort|uniq -c|sort -rn'''), ('CPU Model', '''grep 'model name' /proc/cpuinfo | awk -F: '{print $2}' | uniq -c | sed -re 's/^ +//' ''', '''grep 'model name' /proc/cpuinfo'''), ('64-bit Support', '''grep -q ' lm ' /proc/cpuinfo && echo Yes || echo No'''), ('Hardware Virtualization Support', '''egrep -q '(vmx|svm)' /proc/cpuinfo && echo Yes || echo No'''), ('Load average', '''uptime'''), ('Memory usage', '''free -m''', 'free'), #('Top', '''top -n1 -b | head -15'''), ('Top 5 CPU hogs', '''ps axuScnh | awk '$2!=''' + str(os.getpid()) + '''' | sort -rnk3 | head -5'''), ('Top 5 Memory hogs', '''ps axuScnh | sort -rnk4 | head -5'''), ('Disk space usage', '''df -hT''', 'df -h', 'df'), ('Block devices', '''blkid''', '''/sbin/blkid'''), ('PCI devices', '''lspci''', '''/sbin/lspci'''), ('USB devices', '''lsusb''', '''/sbin/lsusb'''), ('DRM Information', '''grep drm /var/log/dmesg'''), ('Xorg modules', '''grep LoadModule /var/log/Xorg.0.log | cut -d \\" -f 2 | xargs'''), ('GL Support', '''glxinfo | egrep "OpenGL version|OpenGL renderer"'''), ('Xorg errors', '''grep '^\[.*(EE)' /var/log/Xorg.0.log'''), ('Kernel buffer tail', '''dmesg | tail'''), ('Last few reboots', '''last -x -n10 reboot runlevel'''), ('YUM Repositories', '''yum -C repolist''', '''ls -l /etc/yum.repos.d''', '''grep -v '^#' /etc/yum.conf'''), ('YUM Extras', '''yum -C list extras'''), ('Last 20 packages installed', '''rpm -qa --nodigest --nosignature --last | head -20''')] #('Installed packages', '''rpm -qa --nodigest --nosignature | sort''', '''dpkg -l''') ] si = [] print >> sys.stderr, "Gathering system info", for cmds in cmdlist: cmdname = cmds[0] cmd = "" for cmd in cmds[1:]: sys.stderr.write('.') # simple progress feedback p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = p.communicate() if p.returncode == 0 and out: break else: if show_stderr: print >> sys.stderr, "sysinfo Error: the cmd \"%s\" returned %d with stderr: %s" % (cmd, p.returncode, err) print >> sys.stderr, "Trying next fallback cmd..." if out: if show_successful_cmds: si.append( ('%s (%s)' % (cmdname, cmd), out) ) else: si.append( ('%s' % cmdname, out) ) else: if show_failed_cmds: si.append( ('%s (failed: "%s")' % (cmdname, '" AND "'.join(cmds[1:])), out) ) else: si.append( ('%s' % cmdname, out) ) # public SMOLT url try: sys.path.append('/usr/share/smolt/client') from smolt import get_profile_link, getPubUUID from smolt_config import get_config_attr smoonurl = get_config_attr("SMOON_URL", "http://smolts.org/") pubuuid = getPubUUID() puburl = get_profile_link(smoonurl, pubuuid)+os.linesep except: puburl = None si.insert(2, ('Smolt Profile URL', puburl) ) sys.stderr.write("\n") # return in readable indented format sistr = "=== fpaste %s System Information (fpaste --sysinfo) ===\n" % VERSION for cmdname, output in si: sistr += "* %s:\n" % cmdname if not output: sistr += " N/A\n\n" else: for line in output.split('\n'): sistr += " %s\n" % line return sistr def generate_man_page(): '''TODO: generate man page from usage''' pass def summarize_text(text): # use beginning/middle/end content snippets as a description summary. 120 char limit # "36chars ... 36chars ... 36chars" == 118 chars # TODO: nuking whitespace in huge text files might be expensive; optimize for b/m/e segments only sniplen = 36 seplen = len(" ... ") tsum = "" text = " ".join(text.split()) # nuke whitespace tlen = len(text) if tlen < sniplen+seplen: tsum += text if tlen >= sniplen+seplen: tsum += text[0:sniplen] + " ..." if tlen >= (sniplen*2)+seplen: tsum += " " + text[tlen/2-(sniplen/2):(tlen/2)+(sniplen/2)] + " ..." if tlen >= (sniplen*3)+(seplen*2): tsum += " " + text[-sniplen:] #print >> sys.stderr, str(len(tsum)) + ": " + tsum return tsum def main(): validExpiresOpts = [ '3600', '10800', '43200', '86400' ] validSyntaxOpts = [ 'abap', 'antlr', 'antlr-as', 'antlr-cpp', 'antlr-csharp', 'antlr-java', 'antlr-objc', 'antlr-perl', 'antlr-python', 'antlr-ruby', 'apacheconf', 'applescript', 'as', 'as3', 'aspx-cs', 'aspx-vb', 'basemake', 'bash', 'bat', 'bbcode', 'befunge', 'boo', 'brainfuck', 'c', 'c-objdump', 'cheetah', 'clojure', 'common-lisp', 'console', 'control', 'cpp', 'cpp-objdump', 'csharp', 'css', 'css+django', 'css+erb', 'css+genshitext', 'css+mako', 'css+myghty', 'css+php', 'css+smarty', 'cython', 'd', 'd-objdump', 'delphi', 'diff', 'django', 'dpatch', 'dylan', 'erb', 'erl', 'erlang', 'evoque', 'fortran', 'gas', 'genshi', 'genshitext', 'glsl', 'gnuplot', 'groff', 'haskell', 'html', 'html+cheetah', 'html+django', 'html+evoque', 'html+genshi', 'html+mako', 'html+myghty', 'html+php', 'html+smarty', 'ini', 'io', 'irc', 'java', 'js', 'js+cheetah', 'js+django', 'js+erb', 'js+genshitext', 'js+mako', 'js+myghty', 'js+php', 'js+smarty', 'jsp', 'lhs', 'lighty', 'llvm', 'logtalk', 'lua', 'make', 'mako', 'matlab', 'matlabsession', 'minid', 'modelica', 'moocode', 'mupad', 'mxml', 'myghty', 'mysql', 'nasm', 'newspeak', 'nginx', 'numpy', 'objdump', 'objective-c', 'ocaml', 'perl', 'php', 'pot', 'pov', 'prolog', 'py3tb', 'pycon', 'pytb', 'python', 'python3', 'ragel', 'ragel-c', 'ragel-cpp', 'ragel-d', 'ragel-em', 'ragel-java', 'ragel-objc', 'ragel-ruby', 'raw', 'rb', 'rbcon', 'rebol', 'redcode', 'rhtml', 'rst', 'scala', 'scheme', 'smalltalk', 'smarty', 'sourceslist', 'splus', 'sql', 'sqlite3', 'squidconf', 'tcl', 'tcsh', 'tex', 'text', 'trac-wiki', 'vala', 'vb.net', 'vim', 'xml', 'xml+cheetah', 'xml+django', 'xml+erb', 'xml+evoque', 'xml+mako', 'xml+myghty', 'xml+php', 'xml+smarty', 'xslt', 'yaml' ] validClipboardSelectionOpts = [ 'primary', 'secondary', 'clipboard' ] ext2lang_map = { 'sh':'bash', 'bash':'bash', 'bat':'bat', 'c':'c', 'h':'c', 'cpp':'cpp', 'css':'css', 'html':'html', 'htm':'html', 'ini':'ini', 'java':'java', 'js':'js', 'jsp':'jsp', 'pl':'perl', 'php':'php', 'php3':'php', 'py':'python', 'rb':'rb', 'rhtml':'rhtml', 'sql':'sql', 'sqlite':'sqlite3', 'tcl':'tcl', 'vim':'vim', 'xml':'xml' } usage = """\ Usage: %prog [OPTION]... [FILE]... send text file(s), stdin, or clipboard to the http://fpaste.org pastebin and return the URL. Examples: %prog file1.txt file2.txt dmesg | %prog (prog1; prog2; prog3) | fpaste %prog --sysinfo -d "my laptop" --confirm %prog -n codemonkey -d "problem with foo" -l python foo.py""" parser = OptionParser(usage=usage, version='%prog '+VERSION) parser.add_option('', '--debug', dest='debug', help=SUPPRESS_HELP, action="store_true", default=False) parser.add_option('', '--proxy', dest='proxy', help=SUPPRESS_HELP) # pastebin-specific options first fpasteOrg_group = OptionGroup(parser, "fpaste.org Options") fpasteOrg_group.add_option('-n', dest='nick', help='your nickname; default is "%default"', metavar='"NICKNAME"') fpasteOrg_group.add_option('-d', dest='desc', help='description of paste; default appends filename(s)', metavar='"DESCRIPTION"') fpasteOrg_group.add_option('-l', dest='lang', help='language of content for syntax highlighting; default is "%default"; use "list" to show all ' + str(len(validSyntaxOpts)) + ' supported langs', metavar='"LANGUAGE"') fpasteOrg_group.add_option('-x', dest='expires', help='time before paste is removed; default is %default seconds; valid options: ' + ', '.join(validExpiresOpts), metavar='EXPIRES') parser.add_option_group(fpasteOrg_group) # other options fpasteProg_group = OptionGroup(parser, "Input/Output Options") fpasteProg_group.add_option('-i', '--clipin', dest='clipin', help='read paste text from current X clipboard selection', action="store_true", default=False) fpasteProg_group.add_option('-o', '--clipout', dest='clipout', help='save returned paste URL to X clipboard', action="store_true", default=False) fpasteProg_group.add_option('', '--selection', dest='selection', help='specify which X clipboard to use. valid options: "primary" (default; middle-mouse-button paste), "secondary" (uncommon), or "clipboard" (ctrl-v paste)', metavar='CLIP') fpasteProg_group.add_option('', '--fullpath', dest='fullpath', help='use pathname VS basename for file description(s)', action="store_true", default=False) fpasteProg_group.add_option('', '--pasteself', dest='pasteself', help='paste this script itself', action="store_true", default=False) fpasteProg_group.add_option('', '--sysinfo', dest='sysinfo', help='paste system information', action="store_true", default=False) fpasteProg_group.add_option('', '--printonly', dest='printonly', help='print paste, but do not send', action="store_true", default=False) fpasteProg_group.add_option('', '--confirm', dest='confirm', help='print paste, and prompt for confirmation before sending', action="store_true", default=False) parser.add_option_group(fpasteProg_group) parser.set_defaults(desc='', nick='', lang='text', expires=max(validExpiresOpts), selection='primary') (options, args) = parser.parse_args() if options.lang.lower() == 'list': print 'Valid language syntax options:' for opt in validSyntaxOpts: print opt sys.exit(0) if options.clipin: if not os.access('/usr/bin/xsel', os.X_OK): # TODO: try falling back to xclip or dbus parser.error('OOPS - the clipboard options currently depend on "/usr/bin/xsel", which does not appear to be installed') if options.clipin and args: parser.error("Sending both clipboard contents AND files is not supported. Use -i OR filename(s)") for optk, optv, opts in [('language', options.lang, validSyntaxOpts), ('expires', options.expires, validExpiresOpts), ('clipboard selection', options.selection, validClipboardSelectionOpts)]: if optv not in opts: parser.error("'%s' is not a valid %s option.\n\tVALID OPTIONS: %s" % (optv, optk, ', '.join(opts))) fileargs = args if options.fullpath: fileargs = [os.path.abspath(x) for x in args] else: fileargs = [os.path.basename(x) for x in args] # remove potentially non-anonymous path info from file path descriptions #guess lang for some common file extensions, if all file exts similar, and lang not changed from default if options.lang == 'text': all_exts_similar = False for i in range(0, len(args)): all_exts_similar = True ext = os.path.splitext(args[i])[1].lstrip(os.extsep) if i > 0 and ext != ext_prev: all_exts_similar = False break ext_prev = ext if all_exts_similar and ext in ext2lang_map.keys(): options.lang = ext2lang_map[ext] # get input from mutually exclusive sources, though they *could* be combined text = "" if options.clipin: xselcmd = 'xsel -o --%s' % options.selection #text = os.popen(xselcmd).read() p = subprocess.Popen(xselcmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (text, err) = p.communicate() if p.returncode != 0: if options.debug: print >> sys.stderr, err parser.error("'xsel' failure. this usually means you're not running X") if not text: parser.error("%s clipboard is empty" % options.selection) if SET_DESCRIPTION_IF_EMPTY and not options.desc: #options.desc = '%s clipboard' % options.selection options.desc = summarize_text(text) elif options.pasteself: text = open(sys.argv[0]).read() options.desc = 'fpaste-' + VERSION options.lang = 'python' options.nick = 'Fedora Unity' elif options.sysinfo: text = sysinfo(options.debug) if SET_DESCRIPTION_IF_EMPTY and not options.desc: options.desc = 'fpaste --sysinfo' elif not args: # read from stdin if no file args supplied try: text += sys.stdin.read() except KeyboardInterrupt: print >> sys.stderr, "\nUSAGE REMINDER:\n fpaste waits for input when run without file arguments.\n Paste your text, then press on a new line to upload.\n Try `fpaste --help' for more information.\nExiting..." sys.exit(1) if SET_DESCRIPTION_IF_EMPTY and not options.desc: options.desc = summarize_text(text) else: if not options.desc: options.desc = '%s' % (' + '.join(fileargs)) else: options.desc = '%s: %s' % (options.desc, ' + '.join(fileargs)) for i, f in enumerate(args): if not os.access(f, os.R_OK): parser.error("file '%s' is not readable" % f) if (len(args) > 1): # separate multiple files with header text += '#' * 78 + '\n' text += '### file %d of %d: %s\n' % (i+1, len(args), fileargs[i]) text += '#' * 78 + '\n' text += open(f).read() if options.debug: print 'nick: "%s"' % options.nick print 'desc: "%s"' % options.desc print 'lang: "%s"' % options.lang print 'text (%d): "%s ..."' % (len(text), text[:80]) if options.printonly or options.confirm: try: if is_text(text): print text # when piped to less, sometimes fails with [Errno 32] Broken pipe else: print "DATA" except IOError: pass if options.printonly: # print only what would be sent, and exit sys.exit(0) elif options.confirm: # print what would be sent, and ask for permission if not confirm("OK to send?"): sys.exit(1) url = paste(text, options) if url: # try to save URL in clipboard, and warn but don't error if options.clipout: xselcmd = 'xsel -i --%s' % options.selection #os.popen(xselcmd, 'wb').write(url) p = subprocess.Popen(xselcmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) (out, err) = p.communicate(input=url) if p.returncode != 0: if options.debug: print >> sys.stderr, err #parser.error("'xsel' failure. this usually means you're not running X") print >> sys.stderr, "WARNING: URL not saved to clipboard" print url else: sys.exit(1) if options.pasteself: print >> sys.stderr, "install fpaste to local ~/bin dir by running: mkdir -p ~/bin; curl " + url + "raw/ -o ~/bin/fpaste && chmod +x ~/bin/fpaste" sys.exit(0) if __name__ == '__main__': try: if '--generate-man' in sys.argv: generate_man_page() else: main() except KeyboardInterrupt: print "\ninterrupted." sys.exit(1)