diff --git a/README.md b/README.md
index 22eb799..43e6c79 100644
--- a/README.md
+++ b/README.md
@@ -23,21 +23,19 @@ The secret and otp values can be printed and exported to json or csv. The QR cod
## Program help: arguments and options
-
-usage: extract_otp_secret_keys.py [-h] [--verbose] [--quiet] [--saveqr] [--printqr] [--json JSON] [--csv CSV] infile
+usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--printqr] [--saveqr DIR] [--verbose] [--quiet] infile
positional arguments:
infile file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
options:
-h, --help show this help message and exit
- --verbose, -v verbose output
- --quiet, -q no stdout output
- --saveqr, -s save QR code(s) as images to the "qr" subfolder (requires qrcode module)
+ --json FILE, -j FILE export to json file
+ --csv FILE, -c FILE export to csv file
--printqr, -p print QR code(s) as text to the terminal (requires qrcode module)
- --json JSON, -j JSON export to json file
- --csv CSV, -c CSV export to csv file
-
+ --saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
+ --verbose, -v verbose output
+ --quiet, -q no stdout output
## Dependencies
diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py
index 8347879..f5d2766 100644
--- a/extract_otp_secret_keys.py
+++ b/extract_otp_secret_keys.py
@@ -48,7 +48,7 @@ import sys
import csv
import json
from urllib.parse import parse_qs, urlencode, urlparse, quote
-from os import path, mkdir
+from os import path, makedirs
from re import compile as rcompile
import protobuf_generated_python.google_auth_pb2
@@ -70,13 +70,13 @@ def main(sys_args):
def parse_args(sys_args):
arg_parser = argparse.ArgumentParser()
+ arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
+ arg_parser.add_argument('--json', '-j', help='export to json file', metavar=('FILE'))
+ arg_parser.add_argument('--csv', '-c', help='export to csv file', metavar=('FILE'))
+ arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
+ arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='count')
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
- arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the "qr" subfolder (requires qrcode module)', action='store_true')
- arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
- arg_parser.add_argument('--json', '-j', help='export to json file')
- arg_parser.add_argument('--csv', '-c', help='export to csv file')
- arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
args = arg_parser.parse_args(sys_args)
if args.verbose and args.quiet:
print("The arguments --verbose and --quite are mutual exclusive.")
@@ -176,7 +176,8 @@ def print_otp(otp):
def save_qr(otp, args, j):
- if not (path.exists('qr')): mkdir('qr')
+ dir = args.saveqr
+ if not (path.exists(dir)): makedirs(dir, exist_ok=True)
pattern = rcompile(r'[\W_]+')
file_otp_name = pattern.sub('', otp['name'])
file_otp_issuer = pattern.sub('', otp['issuer'])
diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py
index cf9d987..af95551 100644
--- a/test_extract_otp_secret_keys_pytest.py
+++ b/test_extract_otp_secret_keys_pytest.py
@@ -18,12 +18,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from utils import read_csv, read_json, remove_file, read_file_to_str
+from utils import read_csv, read_json, remove_file, remove_dir_with_files, read_file_to_str
+from os import path
+from pytest import raises
import extract_otp_secret_keys
-def test_extract_csv():
+def test_extract_csv(capsys):
# Arrange
cleanup()
@@ -36,11 +38,16 @@ def test_extract_csv():
assert actual_csv == expected_csv
+ captured = capsys.readouterr()
+
+ assert captured.out == ''
+ assert captured.err == ''
+
# Clean up
cleanup()
-def test_extract_json():
+def test_extract_json(capsys):
# Arrange
cleanup()
@@ -53,6 +60,11 @@ def test_extract_json():
assert actual_json == expected_json
+ captured = capsys.readouterr()
+
+ assert captured.out == ''
+ assert captured.err == ''
+
# Clean up
cleanup()
@@ -134,6 +146,28 @@ def test_extract_printqr(capsys):
assert captured.err == ''
+def test_extract_saveqr(capsys):
+ # Arrange
+ cleanup()
+
+ # Act
+ extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
+
+ # Assert
+ captured = capsys.readouterr()
+
+ assert captured.out == ''
+ assert captured.err == ''
+
+ assert path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')
+ assert path.isfile('testout/qr/2-piraspberrypi.png')
+ assert path.isfile('testout/qr/3-piraspberrypi.png')
+ assert path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')
+
+ # Clean up
+ cleanup()
+
+
def test_extract_verbose(capsys):
# Act
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
@@ -147,6 +181,36 @@ def test_extract_verbose(capsys):
assert captured.err == ''
+def test_extract_debug(capsys):
+ # Act
+ extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
+
+ # Assert
+ captured = capsys.readouterr()
+
+ expected_stdout = read_file_to_str('test/print_verbose_output.txt')
+
+ assert len(captured.out) > len(expected_stdout)
+ assert "DEBUG: " in captured.out
+ assert captured.err == ''
+
+
+def test_extract_help(capsys):
+ with raises(SystemExit) as pytest_wrapped_e:
+ # Act
+ extract_otp_secret_keys.main(['-h'])
+
+ # Assert
+ captured = capsys.readouterr()
+
+ assert len(captured.out) > 0
+ assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
+ assert captured.err == ''
+ assert pytest_wrapped_e.type == SystemExit
+ assert pytest_wrapped_e.value.code == 0
+
+
def cleanup():
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
+ remove_dir_with_files('testout/')
diff --git a/test_extract_otp_secret_keys_unittest.py b/test_extract_otp_secret_keys_unittest.py
index 869888e..91a6f51 100644
--- a/test_extract_otp_secret_keys_unittest.py
+++ b/test_extract_otp_secret_keys_unittest.py
@@ -21,7 +21,8 @@
import unittest
import io
from contextlib import redirect_stdout
-from utils import read_csv, read_json, remove_file, Capturing, read_file_to_str
+from utils import read_csv, read_json, remove_file, remove_dir_with_files, Capturing, read_file_to_str
+from os import path
import extract_otp_secret_keys
@@ -137,6 +138,14 @@ Type: OTP_TOTP
self.assertEqual(actual_output, expected_output)
+ def test_extract_saveqr(self):
+ extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
+
+ self.assertTrue(path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
+ self.assertTrue(path.isfile('testout/qr/2-piraspberrypi.png'))
+ self.assertTrue(path.isfile('testout/qr/3-piraspberrypi.png'))
+ self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
+
def test_extract_verbose(self):
out = io.StringIO()
with redirect_stdout(out):
@@ -147,6 +156,30 @@ Type: OTP_TOTP
self.assertEqual(actual_output, expected_output)
+ def test_extract_debug(self):
+ out = io.StringIO()
+ with redirect_stdout(out):
+ extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
+ actual_output = out.getvalue()
+
+ expected_stdout = read_file_to_str('test/print_verbose_output.txt')
+
+ self.assertGreater(len(actual_output), len(expected_stdout))
+ self.assertTrue("DEBUG: " in actual_output)
+
+ def test_extract_help(self):
+ out = io.StringIO()
+ with redirect_stdout(out):
+ try:
+ extract_otp_secret_keys.main(['-h'])
+ except SystemExit:
+ pass
+
+ actual_output = out.getvalue()
+
+ self.assertGreater(len(actual_output), 0)
+ self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
+
def setUp(self):
self.cleanup()
@@ -156,6 +189,7 @@ Type: OTP_TOTP
def cleanup(self):
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
+ remove_dir_with_files('testout/')
if __name__ == '__main__':
diff --git a/utils.py b/utils.py
index 3ff4ebc..26ca666 100644
--- a/utils.py
+++ b/utils.py
@@ -16,6 +16,7 @@
import csv
import json
import os
+import shutil
from io import StringIO
import sys
@@ -38,8 +39,12 @@ with Capturing() as output:
sys.stdout = self._stdout
-def remove_file(filename):
- if os.path.exists(filename): os.remove(filename)
+def remove_file(file):
+ if os.path.isfile(file): os.remove(file)
+
+
+def remove_dir_with_files(dir):
+ if os.path.exists(dir): shutil.rmtree(dir)
def read_csv(filename):