From 47fed2c5d47a03fad7b91bfb890eed257e9c1b2d Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sat, 22 Mar 2014 12:01:52 -0600 Subject: Makefile: do not use recursion and organize --- contrib/importers/fpm2pass.pl | 79 ++++++++++++++++ contrib/importers/gorilla2pass.rb | 76 ++++++++++++++++ contrib/importers/kedpm2pass.py | 52 +++++++++++ contrib/importers/keepass2pass.py | 139 ++++++++++++++++++++++++++++ contrib/importers/keepassx2pass.py | 76 ++++++++++++++++ contrib/importers/lastpass2pass.rb | 131 ++++++++++++++++++++++++++ contrib/importers/pwsafe2pass.sh | 30 ++++++ contrib/importers/revelation2pass.py | 172 +++++++++++++++++++++++++++++++++++ 8 files changed, 755 insertions(+) create mode 100755 contrib/importers/fpm2pass.pl create mode 100755 contrib/importers/gorilla2pass.rb create mode 100755 contrib/importers/kedpm2pass.py create mode 100755 contrib/importers/keepass2pass.py create mode 100755 contrib/importers/keepassx2pass.py create mode 100755 contrib/importers/lastpass2pass.rb create mode 100755 contrib/importers/pwsafe2pass.sh create mode 100755 contrib/importers/revelation2pass.py (limited to 'contrib/importers') diff --git a/contrib/importers/fpm2pass.pl b/contrib/importers/fpm2pass.pl new file mode 100755 index 0000000..d1a0908 --- /dev/null +++ b/contrib/importers/fpm2pass.pl @@ -0,0 +1,79 @@ +#!/usr/bin/perl + +# Copyright (C) 2012 Jeffrey Ratcliffe . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. + +use warnings; +use strict; +use XML::Simple; +use Getopt::Long; +use Pod::Usage; + +my ($help, $man); +my @args = ('help' => \$help, + 'man' => \$man,); +GetOptions (@args) or pod2usage(2); +pod2usage(1) if ($help); +pod2usage(-exitstatus => 0, -verbose => 2) if $man; +pod2usage( + -msg => "Syntax error: must specify a file to read.", + -exitval => 2, + -verbose => 1 +) + if (@ARGV != 1); + +# Grab the XML to a perl structure +my $xs = XML::Simple->new(); +my $doc = $xs->XMLin(shift); + +for (@{$doc->{PasswordList}{PasswordItem}}) { + my $name; + if (ref($_->{category}) eq 'HASH') { + $name = escape($_->{title}); + } + else { + $name = escape($_->{category})."/".escape($_->{title}); + } + my $contents = ''; + $contents .= "$_->{password}\n" unless (ref($_->{password}) eq 'HASH'); + $contents .= "user $_->{user}\n" unless (ref($_->{user}) eq 'HASH'); + $contents .= "url $_->{url}\n" unless (ref($_->{url}) eq 'HASH'); + unless (ref($_->{notes}) eq 'HASH') { + $_->{notes} =~ s/\n/\n /g; + $contents .= "notes:\n $_->{notes}\n"; + } + my $cmd = "pass insert -f -m $name"; + my $pid = open(my $fh, "| $cmd") or die "Couldn't fork: $!\n"; + print $fh $contents; + close $fh; +} + +# escape inverted commas, spaces, ampersands and brackets +sub escape { + my ($s) = @_; + $s =~ s/\//-/g; + $s =~ s/(['\(\) &])/\\$1/g; + return $s; +} + +=head1 NAME + + fpm2pass.pl - imports an .xml exported by fpm2 into pass + +=head1 SYNOPSIS + +=head1 USAGE + + fpm2pass.pl [--help] [--man] + +The following options are available: + +=over + +=item --help + +=item --man + +=back + +=cut diff --git a/contrib/importers/gorilla2pass.rb b/contrib/importers/gorilla2pass.rb new file mode 100755 index 0000000..bf168a7 --- /dev/null +++ b/contrib/importers/gorilla2pass.rb @@ -0,0 +1,76 @@ +#!/usr/bin/env ruby + +# Copyright (C) 2013 David Sklar . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. + +entries = {} + +class HashCounter + + def initialize + @h = Hash.new {|h,k| h[k] = 2 } + end + + def get(k) + v = @h[k] + @h[k] = v + 1 + v + end +end + +hc = HashCounter.new + +$stdin.each do |line| + uuid, group, title, url, user, password, notes = line.strip.split(',') + next if uuid == "uuid" + + # check for missing group + # check for missing title + + prefix = "#{group}/#{title}".gsub(/[\s\'\"()!]/,'') + + + if user && user.length > 0 + entries["#{prefix}/user"] = user + end + if url && url.length > 0 + entries["#{prefix}/url"] = url + end + if password && password.length > 0 + entries["#{prefix}/password"] = password + end + if notes && notes.length > 0 + entries["#{prefix}/notes"] = notes.gsub('\n',"\n").strip + end +end + +entries.keys.each do |k| + if k =~ /^(.+?)-merged\d{4}-\d\d-\d\d\d\d:\d\d:\d\d(\/.+)$/ + other = $1 + $2 + if entries.has_key?(other) + if entries[k] == entries[other] + entries.delete(k) + else + i = hc.get(other) + entries["#{other}#{i}"] = entries[k] + entries.delete(k) + end + else + entries[other] = entries[k] + entries.delete(k) + end + end +end + +pass_top_level = "Gorilla" +entries.keys.each do |k| + print "#{k}...(#{entries[k]})..." + IO.popen("pass insert -e -f '#{pass_top_level}/#{k}' > /dev/null", 'w') do |io| + io.puts entries[k] + "\n" + end + if $? == 0 + puts " done!" + else + puts " error!" + end +end diff --git a/contrib/importers/kedpm2pass.py b/contrib/importers/kedpm2pass.py new file mode 100755 index 0000000..b79cc8b --- /dev/null +++ b/contrib/importers/kedpm2pass.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2012 Antoine Beaupré . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. +# +# To double-check your import worked: +# grep Path passwords | sed 's#^Path: ##;s/$/.gpg/' | sort > listpaths +# (cd ~/.password-store/ ; find -type f ) | sort | diff -u - listpaths + +import re +import fileinput + +import sys # for exit + +import subprocess + +def insert(d): + path = d['Path'] + del d['Path'] + print "inserting " + path + content = d['Password'] + "\n" + del d['Password'] + for k, v in d.iteritems(): + content += "%s: %s\n" % (k, v) + del d + cmd = ["pass", "insert", "--force", "--multiline", path] + process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate(content) + retcode = process.wait() + if retcode: + print 'command "%s" failed with exit code %d: %s' % (" ".join(cmd), retcode, stdout + stderr) + sys.exit(1); + +d = None +for line in fileinput.input(): + if line == "\n": + continue + match = re.match("(\w+): (.*)$", line) + if match: + if match.group(1) == 'Path': + if d is not None: + insert(d) + else: + d = {} + d[match.group(1)] = match.group(2) + #print "found field: %s => %s" % (match.group(1), match.group(2)) + else: + print "warning: no match found on line: *%s*" % line + +if d is not None: + insert(d) diff --git a/contrib/importers/keepass2pass.py b/contrib/importers/keepass2pass.py new file mode 100755 index 0000000..80a2ad9 --- /dev/null +++ b/contrib/importers/keepass2pass.py @@ -0,0 +1,139 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 Stefan Simroth . All Rights Reserved. +# Based on the script for KeepassX by Juhamatti Niemelä . +# This file is licensed under the GPLv2+. Please see COPYING for more information. +# +# Usage: +# ./keepass2pass.py -f export.xml +# By default, takes the name of the root element and puts all passwords in there, but you can disable this: +# ./keepass2pass.py -f export.xml -r "" +# Or you can use another root folder: +# ./keepass2pass.py -f export.xml -r foo +# +# Features: +# * This script can handle duplicates and will merge them. +# * Besides the password also the fields 'UserName', 'URL' and 'Notes' (comment) will be inserted. +# * You get a warning if an entry has no password, but it will still insert it. + +import getopt, sys +from subprocess import Popen, PIPE +from xml.etree import ElementTree + + +def pass_import_entry(path, data): + """ Import new password entry to password-store using pass insert command """ + proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE, stdout=PIPE) + proc.communicate(data.encode('utf8')) + proc.wait() + + +def get_value(elements, node_text): + for element in elements: + for child in element.findall('Key'): + if child.text == node_text: + return element.find('Value').text + return '' + +def path_for(element, path=''): + """ Generate path name from elements title and current path """ + if element.tag == 'Entry': + title = get_value(element.findall("String"), "Title") + elif element.tag == 'Group': + title = element.find('Name').text + else: title = '' + + if path == '': return title + else: return '/'.join([path, title]) + +def password_data(element, path=''): + """ Return password data and additional info if available from password entry element. """ + data = "" + password = get_value(element.findall('String'), 'Password') + if password is not None: data = password + "\n" + else: + print "[WARN] No password: %s" % path_for(element, path) + + for field in ['UserName', 'URL', 'Notes']: + value = get_value(element, field) + if value is not None and not len(value) == 0: + data = "%s%s: %s\n" % (data, field, value) + return data + +def import_entry(entries, element, path=''): + element_path = path_for(element, path) + if entries.has_key(element_path): + print "[INFO] Duplicate needs merging: %s" % element_path + existing_data = entries[element_path] + data = "%s---------\nPassword: %s" % (existing_data, password_data(element)) + else: + data = password_data(element, path) + + entries[element_path] = data + +def import_group(entries, element, path=''): + """ Import all entries and sub-groups from given group """ + npath = path_for(element, path) + for group in element.findall('Group'): + import_group(entries, group, npath) + for entry in element.findall('Entry'): + import_entry(entries, entry, npath) + +def import_passwords(xml_file, root_path=None): + """ Parse given Keepass2 XML file and import password groups from it """ + print "[>>>>] Importing passwords from file %s" % xml_file + print "[INFO] Root path: %s" % root_path + entries = dict() + with open(xml_file) as xml: + text = xml.read() + xml_tree = ElementTree.XML(text) + root = xml_tree.find('Root') + root_group = root.find('Group') + import_group(entries,root_group,'') + if root_path is None: root_path = root_group.find('Name').text + groups = root_group.findall('Group') + for group in groups: + import_group(entries, group, root_path) + password_count = 0 + for path, data in sorted(entries.iteritems()): + sys.stdout.write("[>>>>] Importing %s ... " % path.encode("utf-8")) + pass_import_entry(path, data) + sys.stdout.write("OK\n") + password_count += 1 + + print "[ OK ] Done. Imported %i passwords." % password_count + + +def usage(): + """ Print usage """ + print "Usage: %s -f XML_FILE" % (sys.argv[0]) + print "Optional:" + print " -r ROOT_PATH Different root path to use than the one in xml file, use \"\" for none" + + +def main(argv): + try: + opts, args = getopt.gnu_getopt(argv, "f:r:") + except getopt.GetoptError as err: + print str(err) + usage() + sys.exit(2) + + xml_file = None + root_path = None + + for opt, arg in opts: + if opt in "-f": + xml_file = arg + if opt in "-r": + root_path = arg + + if xml_file is not None: + import_passwords(xml_file, root_path) + else: + usage() + sys.exit(2) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/contrib/importers/keepassx2pass.py b/contrib/importers/keepassx2pass.py new file mode 100755 index 0000000..dc4b1e5 --- /dev/null +++ b/contrib/importers/keepassx2pass.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2012 Juhamatti Niemelä . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. + +import sys +import re + +from subprocess import Popen, PIPE +from xml.etree import ElementTree + +def space_to_camelcase(value): + output = "" + first_word_passed = False + for word in value.split(" "): + if not word: + output += "_" + continue + if first_word_passed: + output += word.capitalize() + else: + output += word.lower() + first_word_passed = True + return output + +def cleanTitle(title): + # make the title more command line friendly + title = re.sub("(\\|\||\(|\))", "-", title) + title = re.sub("-$", "", title) + title = re.sub("\@", "At", title) + title = re.sub("'", "", title) + return title + +def path_for(element, path=''): + """ Generate path name from elements title and current path """ + title = cleanTitle(space_to_camelcase(element.find('title').text)) + return '/'.join([path, title]) + +def password_data(element): + """ Return password data and additional info if available from + password entry element. """ + passwd = element.find('password').text + ret = passwd + "\n" if passwd else "\n" + for field in ['username', 'url', 'comment']: + fel = element.find(field) + if fel.text is not None: + ret = "%s%s: %s\n" % (ret, fel.tag, fel.text) + return ret + +def import_entry(element, path=''): + """ Import new password entry to password-store using pass insert + command """ + proc = Popen(['pass', 'insert', '--multiline', '--force', + path_for(element, path)], + stdin=PIPE, stdout=PIPE) + proc.communicate(password_data(element).encode('utf8')) + proc.wait() + +def import_group(element, path=''): + """ Import all entries and sub-groups from given group """ + npath = path_for(element, path) + for group in element.findall('group'): + import_group(group, npath) + for entry in element.findall('entry'): + import_entry(entry, npath) + + +def main(xml_file): + """ Parse given KeepassX XML file and import password groups from it """ + with open(xml_file) as xml: + for group in ElementTree.XML(xml.read()).findall('group'): + import_group(group) + +if __name__ == '__main__': + main(sys.argv[1]) diff --git a/contrib/importers/lastpass2pass.rb b/contrib/importers/lastpass2pass.rb new file mode 100755 index 0000000..41a2a29 --- /dev/null +++ b/contrib/importers/lastpass2pass.rb @@ -0,0 +1,131 @@ +#!/usr/bin/env ruby + +# Copyright (C) 2012 Alex Sayers . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. + +# LastPass Importer +# +# Reads CSV files exported from LastPass and imports them into pass. + +# Usage: +# +# Go to lastpass.com and sign in. Next click on your username in the top-right +# corner. In the drop-down meny that appears, click "Export". After filling in +# your details again, copy the text and save it somewhere on your disk. Make sure +# you copy the whole thing, and resist the temptation to "Save Page As" - the +# script doesn't like HTML. +# +# Fire up a terminal and run the script, passing the file you saved as an argument. +# It should look something like this: +# +#$ ./lastpass2pass.rb path/to/passwords_file.csv + +# Parse flags +require 'optparse' +optparse = OptionParser.new do |opts| + opts.banner = "Usage: #{$0} [options] filename" + + FORCE = false + opts.on("-f", "--force", "Overwrite existing records") { FORCE = true } + DEFAULT_GROUP = "" + opts.on("-d", "--default GROUP", "Place uncategorised records into GROUP") { |group| DEFAULT_GROUP = group } + opts.on("-h", "--help", "Display this screen") { puts opts; exit } + + opts.parse! +end + +# Check for a filename +if ARGV.empty? + puts optparse + exit 0 +end + +# Get filename of csv file +filename = ARGV.join(" ") +puts "Reading '#{filename}'..." + + +class Record + def initialize name, url, username, password, extra, grouping, fav + @name, @url, @username, @password, @extra, @grouping, @fav = name, url, username, password, extra, grouping, fav + end + + def name + s = "" + s << @grouping + "/" unless @grouping.empty? + s << @name + s.gsub(/ /, "_").gsub(/'/, "") + end + + def to_s + s = "" + s << "#{@password}\n---\n" + s << "#{@grouping} / " unless @grouping.empty? + s << "#{@name}\n" + s << "username: #{@username}\n" unless @username.empty? + s << "password: #{@password}\n" unless @password.empty? + s << "url: #{@url}\n" unless @url == "http://sn" + s << "#{@extra}\n" unless @extra.nil? + return s + end +end + +# Extract individual records +entries = [] +entry = "" +begin + file = File.open(filename) + file.each do |line| + if line =~ /^http/ + entries.push(entry) + entry = "" + end + entry += line + end + entries.push(entry) + entries.shift + puts "#{entries.length} records found!" +rescue + puts "Couldn't find #{filename}!" + exit 1 +end + +# Parse records and create Record objects +records = [] +entries.each do |e| + args = e.split(",") + url = args.shift + username = args.shift + password = args.shift + fav = args.pop + grouping = args.pop + grouping = DEFAULT_GROUP if grouping.empty? + name = args.pop + extra = args.join(",")[1...-1] + + records << Record.new(name, url, username, password, extra, grouping, fav) +end +puts "Records parsed: #{records.length}" + +successful = 0 +errors = [] +records.each do |r| + print "Creating record #{r.name}..." + IO.popen("pass insert -m#{"f" if FORCE} '#{r.name}' > /dev/null", 'w') do |io| + io.puts r + end + if $? == 0 + puts " done!" + successful += 1 + else + puts " error!" + errors << r + end +end +puts "#{successful} records successfully imported!" + +if errors + puts "There were #{errors.length} errors:" + errors.each { |e| print e.name + (e == errors.last ? ".\n" : ", ")} + puts "These probably occurred because an identically-named record already existed, or because there were multiple entries with the same name in the csv file." +end diff --git a/contrib/importers/pwsafe2pass.sh b/contrib/importers/pwsafe2pass.sh new file mode 100755 index 0000000..c29bb3f --- /dev/null +++ b/contrib/importers/pwsafe2pass.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright (C) 2013 Tom Hendrikx . All Rights Reserved. +# This file is licensed under the GPLv2+. Please see COPYING for more information. + +export=$1 + +IFS=" " # tab character +cat "$export" | while read uuid group name login passwd notes; do + test "$uuid" = "# passwordsafe version 2.0 database" && continue + test "$uuid" = "uuid" && continue + test "$name" = '""' && continue; + + group="$(echo $group | cut -d'"' -f2)" + login="$(echo $login | cut -d'"' -f2)" + passwd="$(echo $passwd | cut -d'"' -f2)" + name="$(echo $name | cut -d'"' -f2)" + + # cleanup + test "${name:0:4}" = "http" && name="$(echo $name | cut -d'/' -f3)" + test "${name:0:4}" = "www." && name="$(echo $name | cut -c 5-)" + + entry="" + test -n "$login" && entry="${entry}login: $login\n" + test -n "$passwd" && entry="${entry}pass: $passwd\n" + test -n "$group" && entry="${entry}group: $group\n" + + echo Adding entry for $name: + echo -e $entry | pass insert --multiline --force "$name" + test $? && echo "Added!" +done diff --git a/contrib/importers/revelation2pass.py b/contrib/importers/revelation2pass.py new file mode 100755 index 0000000..f04c1a8 --- /dev/null +++ b/contrib/importers/revelation2pass.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 Emanuele Aina . All Rights Reserved. +# Copyright (C) 2011 Toni Corvera. All Rights Reserved. +# This file is licensed under the BSD 2-clause license: +# http://www.opensource.org/licenses/BSD-2-Clause +# +# Import script for the Revelation password manager: +# http://revelation.olasagasti.info/ +# Heavily based on the Relevation command line tool: +# http://p.outlyer.net/relevation/ + +import os, sys, argparse, zlib, getpass, traceback +from subprocess import Popen, PIPE, STDOUT, CalledProcessError +from collections import OrderedDict +try: + from lxml import etree +except ImportError: + from xml.etree import ElementTree as etree + +USE_PYCRYPTO = True +try: + from Crypto.Cipher import AES +except ImportError: + USE_PYCRYPTO = False + try: + from crypto.cipher import rijndael, cbc + from crypto.cipher.base import noPadding + except ImportError: + sys.stderr.write('Either PyCrypto or cryptopy are required\n') + raise + +def path_for(element, path=None): + """ Generate path name from elements name and current path """ + name = element.find('name').text + name = name.replace('/', '-').replace('\\', '-') + path = path if path else '' + return os.path.join(path, name) + +def format_password_data(data): + """ Format the secret data that will be handed to Pass in multi-line mode: + $password + $fieldname: $fielddata + ... + $multi_line_notes_with_leading_spaces""" + password = data.pop('password', None) or '' + ret = password + '\n' + notes = data.pop('notes', None) + for label, text in data.iteritems(): + ret += label + ': ' + text + '\n' + if notes: + ret += ' ' + notes.replace('\n', '\n ').strip() + '\n' + return ret + +def password_data(element): + """ Return password data and additional info if available from + password entry element. """ + data = OrderedDict() + data['password'] = element.find('field[@id="generic-password"]').text + data['type'] = element.attrib['type'] + for field in element.findall('field'): + field_id = field.attrib['id'] + if field_id == 'generic-password': + continue + if field.text is not None: + data[field_id] = field.text + for tag in ('description', 'notes'): + field = element.find(tag) + if field is not None and field.text: + data[tag] = field.text + return format_password_data(data) + + +def import_entry(element, path=None, verbose=0): + """ Import new password entry to password-store using pass insert + command """ + cmd = ['pass', 'insert', '--multiline', '--force', path_for(element, path)] + if verbose: + print 'cmd:\n ' + ' '.join(cmd) + proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) + stdin = password_data(element).encode('utf8') + if verbose: + print 'input:\n ' + stdin.replace('\n', '\n ').strip() + stdout, _ = proc.communicate(stdin) + retcode = proc.poll() + if retcode: + raise CalledProcessError(retcode, cmd, output=stdout) + +def import_folder(element, path=None, verbose=0): + path = path_for(element, path) + import_subentries(element, path, verbose) + +def import_subentries(element, path=None, verbose=0): + """ Import all sub entries of the current folder element """ + for entry in element.findall('entry'): + if entry.attrib['type'] == 'folder': + import_folder(entry, path, verbose) + else: + import_entry(entry, path, verbose) + +def decrypt_gz(key, cipher_text): + ''' Decrypt cipher_text using key. + decrypt(str, str) -> cleartext (gzipped xml) + + This function will use the underlying, available, cipher module. + ''' + if USE_PYCRYPTO: + # Extract IV + c = AES.new(key) + iv = c.decrypt(cipher_text[12:28]) + # Decrypt data, CBC mode + c = AES.new(key, AES.MODE_CBC, iv) + ct = c.decrypt(cipher_text[28:]) + else: + # Extract IV + c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding()) + iv = c.decrypt(cipher_text[12:28]) + # Decrypt data, CBC mode + bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding()) + c = cbc.CBC(bc, padding=noPadding()) + ct = c.decrypt(cipher_text[28:], iv=iv) + return ct + +def main(datafile, verbose=False): + f = None + with open(datafile, "rb") as f: + # Encrypted data + data = f.read() + password = getpass.getpass() + # Pad password + password += (chr(0) * (32 - len(password))) + # Decrypt. Decrypted data is compressed + cleardata_gz = decrypt_gz(password, data) + # Length of data padding + padlen = ord(cleardata_gz[-1]) + # Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size) + xmldata = zlib.decompress(cleardata_gz[:-padlen], 15, 2**15) + root = etree.fromstring(xmldata) + import_subentries(root, verbose=verbose) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', '-v', action='count') + parser.add_argument('FILE', help="the file storing the Revelation passwords") + args = parser.parse_args() + + def err(s): + sys.stderr.write(s+'\n') + + try: + main(args.FILE, verbose=args.verbose) + except KeyboardInterrupt: + if args.verbose: + traceback.print_exc() + err(str(e)) + except zlib.error: + err('Failed to decompress decrypted data. Wrong password?') + sys.exit(os.EX_DATAERR) + except CalledProcessError as e: + if args.verbose: + traceback.print_exc() + print 'output:\n ' + e.output.replace('\n', '\n ').strip() + else: + err('CalledProcessError: ' + str(e)) + sys.exit(os.EX_IOERR) + except IOError as e: + if args.verbose: + traceback.print_exc() + else: + err('IOError: ' + str(e)) + sys.exit(os.EX_IOERR) -- cgit v1.2.3-59-g8ed1b