Repository: victorskl/yawsso (language: python)

Issue:
yawsso -e only exports Linux form of environment variables
In most cases yawsso works extremely well for both Linux and Windows. There is one exception: the command `yawsso -e` prefaces each variable with "export " while Windows uses "set ".  It would be great to have an option for Windows development as well. Maybe -ew as the option?

Thank you for writing such a great tool.


Discussion / hints:
Sound fair. I will add support for this over the weekend! Thanks for feedback. :)

Fix patch:
diff --git a/.gitignore b/.gitignore
index b02e1cf..bced5eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ py38/
 
 cdk.out/
 cdk.context.json
+
+h.bat
+h.sh
diff --git a/Makefile b/Makefile
index 2fa1d01..0391f82 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 install:
-	@pip install '.[dev,test]' .
+	@pip install ".[dev,test]" .
 	@pre-commit install
 
 check:
diff --git a/README.md b/README.md
index 6ddad41..2533b99 100644
--- a/README.md
+++ b/README.md
@@ -223,14 +223,17 @@ yawsso login --profile dev -e | yawsso decrypt
 `yawsso` can encrypt and decrypt some arbitrary string from `stdin` using [ROT13](https://en.wikipedia.org/wiki/ROT13) (_a simple letter substitution cipher_) as follows.
 
 ```
-yawsso encrypt <<< 'Hello this is a test'
+echo 'Hello this is a test' | yawsso encrypt
 Uryyb guvf vf n grfg
 
-yawsso decrypt <<< 'Uryyb guvf vf n grfg'
+echo 'Uryyb guvf vf n grfg' | yawsso decrypt
 Hello this is a test
 
 (or Pipe through some text corpus)
-cat test.txt | yawsso encrypt 
+cat test.txt | yawsso encrypt
+
+(or on Windows)
+type test.txt | yawsso encrypt
 ```
 
 This is the same as using trivial Unix `tr` command as follows.
@@ -259,6 +262,16 @@ make test
 python -m yawsso --trace version
 ```
 
+(Windows)
+
+```
+python -m venv venv
+.\venv\Scripts\activate
+pip install ".[dev,test]" .
+pytest
+python -m yawsso --trace version
+```
+
 - Create issue or pull request welcome
 
 ## License
diff --git a/h.py b/h.py
new file mode 100644
index 0000000..702f7db
--- /dev/null
+++ b/h.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+import ast
+
+with open('tests/test_cli.py') as f:
+    code = ast.parse(f.read())
+
+for node in ast.walk(code):
+    if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.Module)):
+        docstring = ast.get_docstring(node)
+        if docstring:
+            print(docstring)
+            # print(repr(docstring))
+
+# Usage:
+#   python h.py > h.bat
+#   h
diff --git a/setup.py b/setup.py
index 85824c0..4271bde 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 from setuptools import setup
 
-with open("README.md", "r") as f:
+with open("README.md", "r", encoding="utf-8") as f:
     long_description = f.read()
 
 setup(
@@ -51,6 +51,7 @@
             "nose2",
             "pre-commit",
             "detect-secrets",
+            "ggshield",
         ],
     },
     python_requires=">=3.6",
diff --git a/yawsso/__init__.py b/yawsso/__init__.py
index 24a6b0b..983f225 100644
--- a/yawsso/__init__.py
+++ b/yawsso/__init__.py
@@ -4,7 +4,7 @@
 from pathlib import Path
 
 # versioning follow PEP440
-__version__ = VERSION = '1.0.0rc1'  # pragma: no cover
+__version__ = VERSION = '1.0.0rc2'  # pragma: no cover
 
 PROGRAM = 'yawsso'  # pragma: no cover
 
diff --git a/yawsso/utils.py b/yawsso/utils.py
index 626e7f5..511d7e4 100644
--- a/yawsso/utils.py
+++ b/yawsso/utils.py
@@ -1,6 +1,7 @@
 import codecs
 import json
 import os
+import platform
 import shlex
 import subprocess
 from configparser import ConfigParser
@@ -97,11 +98,56 @@ def write_config(path, config):
 
 
 def encrypt(obj: str):
-    return codecs.encode(obj, encoding=Constant.ROT_13.value)
+    return codecs.encode(obj, encoding=str(Constant.ROT_13.value))
 
 
 def decrypt(obj):
-    return codecs.decode(obj, encoding=Constant.ROT_13.value)
+    return codecs.decode(obj, encoding=str(Constant.ROT_13.value))
+
+
+class Exporter(object):
+    """
+    https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
+    """
+
+    def __init__(self, credentials):
+        self.credentials = credentials
+        self.clipboard = ""
+
+    def _make_powershell(self):
+        self.clipboard = f"$Env:AWS_ACCESS_KEY_ID=\"{self.credentials['accessKeyId']}\"\n"
+        self.clipboard += f"$Env:AWS_SECRET_ACCESS_KEY=\"{self.credentials['secretAccessKey']}\"\n"
+        self.clipboard += f"$Env:AWS_SESSION_TOKEN=\"{self.credentials['sessionToken']}\""
+
+    def _make_cmd(self):
+        self.clipboard = f"set AWS_ACCESS_KEY_ID={self.credentials['accessKeyId']}\n"
+        self.clipboard += f"set AWS_SECRET_ACCESS_KEY={self.credentials['secretAccessKey']}\n"
+        self.clipboard += f"set AWS_SESSION_TOKEN={self.credentials['sessionToken']}"
+
+    def _make_nix(self):
+        self.clipboard = f"export AWS_ACCESS_KEY_ID={self.credentials['accessKeyId']}\n"
+        self.clipboard += f"export AWS_SECRET_ACCESS_KEY={self.credentials['secretAccessKey']}\n"
+        self.clipboard += f"export AWS_SESSION_TOKEN={self.credentials['sessionToken']}"
+
+    def _make_windows(self):
+        if "$P$G" in os.getenv("PROMPT", ""):
+            # cmd.exe
+            logger.debug(f"Detected Windows platform with cmd.exe")
+            self._make_cmd()
+        else:
+            # powershell
+            logger.debug(f"Detected Windows platform with PowerShell")
+            self._make_powershell()
+
+    def get_export_cmd(self):
+        if platform.system() == "Windows":
+            self._make_windows()
+        else:
+            # Unix
+            logger.debug(f"Detected Nix platform")
+            self._make_nix()
+
+        return self.clipboard
 
 
 def get_export_vars(profile_name, credentials):
@@ -113,9 +159,7 @@ def get_export_vars(profile_name, credentials):
     pyperclip_spec = importlib_util.find_spec("pyperclip")
     pyperclip_found = pyperclip_spec is not None
 
-    clipboard = f"export AWS_ACCESS_KEY_ID={credentials['accessKeyId']}\n"
-    clipboard += f"export AWS_SECRET_ACCESS_KEY={credentials['secretAccessKey']}\n"
-    clipboard += f"export AWS_SESSION_TOKEN={credentials['sessionToken']}"
+    clipboard = Exporter(credentials).get_export_cmd()
 
     if pyperclip_found:
         import pyperclip  # pragma: no cover


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 41e65dc..8655e70 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,5 +1,6 @@
 import json
 import logging
+import os
 import pathlib
 import tempfile
 import uuid
@@ -27,7 +28,7 @@
 class CLIUnitTests(TestCase):
 
     def setUp(self) -> None:
-        self.config = tempfile.NamedTemporaryFile()
+        self.config = tempfile.NamedTemporaryFile(delete=False)
         conf_ini = b"""
         [default]
         sso_start_url = https://petshop.awsapps.com/start
@@ -87,7 +88,7 @@ def setUp(self) -> None:
         self.config.seek(0)
         self.config.read()
 
-        self.credentials = tempfile.NamedTemporaryFile()
+        self.credentials = tempfile.NamedTemporaryFile(delete=False)
         cred_ini = b"""
         [default]
         region = ap-southeast-2
@@ -108,7 +109,7 @@ def setUp(self) -> None:
         self.credentials.read()
 
         self.sso_cache_dir = tempfile.TemporaryDirectory()
-        self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json')
+        self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json', delete=False)
         cache_json = {
             "startUrl": "https://petshop.awsapps.com/start",
             "region": "ap-southeast-2",
@@ -185,6 +186,16 @@ def tearDown(self) -> None:
         self.config.close()
         self.credentials.close()
         self.sso_cache_json.close()
+
+        if os.path.exists(self.config.name):
+            os.unlink(self.config.name)
+
+        if os.path.exists(self.credentials.name):
+            os.unlink(self.credentials.name)
+
+        if os.path.exists(self.sso_cache_json.name):
+            os.unlink(self.sso_cache_json.name)
+
         self.sso_cache_dir.cleanup()
         cli.core.aws_bin = "aws"
         unstub()
@@ -221,8 +232,9 @@ def test_not_sso_profile(self):
         with ArgvContext(program, '-p', 'dev', '-t'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [profile dev]
             region = ap-southeast-2
@@ -267,9 +279,10 @@ def test_credential_not_found(self):
         """
         python -m unittest tests.test_cli.CLIUnitTests.test_credential_not_found
         """
-        tmp_file = tempfile.NamedTemporaryFile()
+        tmp_file = tempfile.NamedTemporaryFile(delete=False)
         tmp_name = tmp_file.name
         tmp_file.close()
+        os.unlink(tmp_file.name)
         with ArgvContext(program, '-d', '-p', 'dev'):
             cli.core.aws_shared_credentials_file = tmp_name
             cli.main()
@@ -303,10 +316,11 @@ def test_sso_cache_expires(self):
         with ArgvContext(program, '-p', 'dev', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.sso_cache_json.close()
+            os.unlink(self.sso_cache_json.name)
             self.sso_cache_dir.cleanup()
             # start new test case
             self.sso_cache_dir = tempfile.TemporaryDirectory()
-            self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json')
+            self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json', delete=False)
             cache_json = {
                 "startUrl": "https://petshop.awsapps.com/start",
                 "region": "ap-southeast-2",
@@ -337,8 +351,9 @@ def test_default_profile(self):
         with ArgvContext(program, '--default-only', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             region = ap-southeast-2
@@ -358,8 +373,9 @@ def test_no_such_profile_section(self):
         with ArgvContext(program, '--default', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [profile default]
             region = ap-southeast-2
@@ -379,10 +395,11 @@ def test_sso_cache_not_json(self):
         with ArgvContext(program, '-p', 'dev', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.sso_cache_json.close()
+            os.unlink(self.sso_cache_json.name)
             self.sso_cache_dir.cleanup()
             # start new test case
             self.sso_cache_dir = tempfile.TemporaryDirectory()
-            self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.txt')
+            self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.txt', delete=False)
             self.sso_cache_json.seek(0)
             self.sso_cache_json.read()
             cli.core.aws_sso_cache_path = self.sso_cache_dir.name
@@ -396,8 +413,9 @@ def test_not_equal_sso_start_url(self):
         with ArgvContext(program, '-p', 'dev', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [profile dev]
             sso_start_url = https://vetclinic.awsapps.com/start
@@ -421,8 +439,9 @@ def test_not_equal_sso_region(self):
         with ArgvContext(program, '-p', 'dev', '-t'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [profile dev]
             sso_start_url = https://petshop.awsapps.com/start
@@ -451,10 +470,11 @@ def test_load_json_value_error(self):
         """
         # clean up as going to mutate this
         self.sso_cache_json.close()
+        os.unlink(self.sso_cache_json.name)
         self.sso_cache_dir.cleanup()
         # start new test case
         self.sso_cache_dir = tempfile.TemporaryDirectory()
-        self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json')
+        self.sso_cache_json = tempfile.NamedTemporaryFile(dir=self.sso_cache_dir.name, suffix='.json', delete=False)
         self.sso_cache_json.write('{}{}'.encode('utf-8'))
         self.sso_cache_json.seek(0)
         self.sso_cache_json.read()
@@ -486,8 +506,9 @@ def test_source_profile(self):
         with ArgvContext(program, '-t'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             sso_start_url = https://petshop.awsapps.com/start
@@ -521,8 +542,9 @@ def test_source_profile_region_mismatch(self):
         with ArgvContext(program, '-t', '-p', 'dev'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             sso_start_url = https://petshop.awsapps.com/start
@@ -554,7 +576,8 @@ def test_source_profile_eager_sync(self):
         """
         with ArgvContext(program, '-t', '-p', 'dev'):
             self.credentials.close()
-            self.credentials = tempfile.NamedTemporaryFile()
+            os.unlink(self.credentials.name)
+            self.credentials = tempfile.NamedTemporaryFile(delete=False)
             cred_ini = b"""
             [default]
             region = ap-southeast-2
@@ -569,8 +592,9 @@ def test_source_profile_eager_sync(self):
             cli.core.aws_shared_credentials_file = self.credentials.name
 
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             sso_start_url = https://petshop.awsapps.com/start
@@ -611,8 +635,9 @@ def test_source_profile_not_sso(self):
         with ArgvContext(program, '-t'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             region = ap-southeast-2
@@ -665,8 +690,9 @@ def test_print_export_vars_default_only_profile(self):
         with ArgvContext(program, '-e', '-t'), self.assertRaises(SystemExit) as x:
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             sso_start_url = https://petshop.awsapps.com/start
@@ -691,8 +717,9 @@ def test_print_export_vars_default_profile(self):
         with ArgvContext(program, '-e', '--default', '-t'):
             # clean up as going to mutate this
             self.config.close()
+            os.unlink(self.config.name)
             # now start new test case
-            self.config = tempfile.NamedTemporaryFile()
+            self.config = tempfile.NamedTemporaryFile(delete=False)
             conf_ini = b"""
             [default]
             sso_start_url = https://petshop.awsapps.com/start
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..683ca94
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,52 @@
+import os
+import platform
+from unittest import TestCase
+
+from mockito import when
+
+from tests.test_cli import logger
+from yawsso.utils import Exporter
+
+
+def mock_cred():
+    return {
+        "accessKeyId": "mock",
+        "secretAccessKey": "",      # pragma: allowlist secret
+        "sessionToken": "mock"
+    }
+
+
+class UtilsUnitTests(TestCase):
+    pass
+
+
+class ExporterUnitTests(TestCase):
+
+    def test_windows_powershell(self):
+        """
+        python -m unittest tests.test_utils.ExporterUnitTests.test_windows_powershell
+        """
+        when(platform).system().thenReturn("Windows")
+        when(os).getenv(...).thenReturn("")
+        clipboard = Exporter(credentials=mock_cred()).get_export_cmd()
+        logger.info(f"\n{clipboard}")
+        self.assertIn("$Env", clipboard)
+
+    def test_windows_cmd(self):
+        """
+        python -m unittest tests.test_utils.ExporterUnitTests.test_windows_cmd
+        """
+        when(platform).system().thenReturn("Windows")
+        when(os).getenv(...).thenReturn("$P$G")
+        clipboard = Exporter(credentials=mock_cred()).get_export_cmd()
+        logger.info(f"\n{clipboard}")
+        self.assertIn("set", clipboard)
+
+    def test_nix_cmd(self):
+        """
+        python -m unittest tests.test_utils.ExporterUnitTests.test_nix_cmd
+        """
+        when(platform).system().thenReturn("Nix")
+        clipboard = Exporter(credentials=mock_cred()).get_export_cmd()
+        logger.info(f"\n{clipboard}")
+        self.assertIn("export", clipboard)