Repository: mitmproxy/mitmproxy

Issue:
commands taking a filename should expand home dir (tilde)
##### Steps to reproduce the problem:

1. Open a flow on the response tab.
2. Press "b".
3. When prompted for a filename, enter `~/example`.

This says "No such file or directory". Entering "/home/username/example" works.

##### System information

<!-- Paste the output of "mitmproxy --version" here. -->

Mitmproxy: 3.0.3
Python:    3.6.4
OpenSSL:   OpenSSL 1.1.0g  2 Nov 2017
Platform:  Linux-4.15.10-1-ARCH-x86_64-with-arch



Discussion:
Thanks for the issue.
### Tilda support for commands at this moment.

| Button | Command | Status |
| --- | --- | --- |
| `W` | console.command.set save_stream_file | :heavy_check_mark: |
| `b` | cut.save | :x: |
| `w` | save.file | :heavy_check_mark: |
| `e` | export.file | :x: |
| - | replay.client.file | :heavy_check_mark: |
| - | replay.server.file |:heavy_check_mark: |
| `L` | view.load | :heavy_check_mark: |
| `L` | options.load | :heavy_check_mark: |
| `S` | options.save | :heavy_check_mark: |
| `r` | console.grideditor.load | :heavy_check_mark: |
| `R` | console.grideditor.load_escaped | :heavy_check_mark: |
| `w` | console.grideditor.save | :x: |
| `shift+\` | script.run | :heavy_check_mark: |

I'll fix this.
IIRC at some point it was discussed to have a dedicated `Path` argument type that automatically handles that. Are we tracking that anywhere already?
@mhils I only see, that some commands have `path = os.path.expanduser(path)`
It looks like @cortesi started hacking on this: https://github.com/mitmproxy/mitmproxy/blob/443409e32bcc28a7f0475d7af42efff03473b72f/examples/addons/commands-paths.py#L17 - not sure if that auto-expands already?
> not sure if that auto-expands already?

It doesn’t; would be nice if it did.

Resolving patch:
diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py
--- a/mitmproxy/addons/cut.py
+++ b/mitmproxy/addons/cut.py
@@ -1,6 +1,8 @@
 import io
 import csv
 import typing
+import os.path
+
 from mitmproxy import command
 from mitmproxy import exceptions
 from mitmproxy import flow
@@ -87,7 +89,8 @@ def save(
         append = False
         if path.startswith("+"):
             append = True
-            path = mitmproxy.types.Path(path[1:])
+            epath = os.path.expanduser(path[1:])
+            path = mitmproxy.types.Path(epath)
         try:
             if len(cuts) == 1 and len(flows) == 1:
                 with open(path, "ab" if append else "wb") as fp:
diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py
--- a/mitmproxy/addons/view.py
+++ b/mitmproxy/addons/view.py
@@ -10,7 +10,6 @@
 """
 import collections
 import typing
-import os
 
 import blinker
 import sortedcontainers
@@ -359,9 +358,8 @@ def load_file(self, path: mitmproxy.types.Path) -> None:
         """
             Load flows into the view, without processing them with addons.
         """
-        spath = os.path.expanduser(path)
         try:
-            with open(spath, "rb") as f:
+            with open(path, "rb") as f:
                 for i in io.FlowReader(f).stream():
                     # Do this to get a new ID, so we can load the same file N times and
                     # get new flows each time. It would be more efficient to just have a
diff --git a/mitmproxy/types.py b/mitmproxy/types.py
--- a/mitmproxy/types.py
+++ b/mitmproxy/types.py
@@ -178,7 +178,7 @@ def completion(self, manager: _CommandBase, t: type, start: str) -> typing.Seque
         return ret
 
     def parse(self, manager: _CommandBase, t: type, s: str) -> str:
-        return s
+        return os.path.expanduser(s)
 
     def is_valid(self, manager: _CommandBase, typ: typing.Any, val: typing.Any) -> bool:
         return isinstance(val, str)


Test patch:
diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py
--- a/test/mitmproxy/test_types.py
+++ b/test/mitmproxy/test_types.py
@@ -2,6 +2,7 @@
 import os
 import typing
 import contextlib
+from unittest import mock
 
 from mitmproxy.test import tutils
 import mitmproxy.exceptions
@@ -69,7 +70,10 @@ def test_path():
         b = mitmproxy.types._PathType()
         assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/foo") == "/foo"
         assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar"
+        with mock.patch.dict("os.environ", {"HOME": "/home/test"}):
+            assert b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") == "/home/test/mitm"
         assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "foo") is True
+        assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "~/mitm") is True
         assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, 3) is False
 
         def normPathOpts(prefix, match):