nickstenning: @migurski Could you check this out and let me know if it correctly addresses the issue for you?

migurski: Thanks Nick! I checked again with Python 2.7.6 on Ubuntu 14.04. At 84a1f7d it generated a correct script, at d8be8f4 and it generated a bad one, and with your recent efd6292 and it generated this improved working script:

```
start on starting honcho-stuff
stop on stopping honcho-stuff
respawn

env VAR='foo bar'
env PORT=5000

exec su - migurski -m -s /bin/sh -c 'cd /home/migurski/honcho; exec python /home/migurski/honcho/stuff.py >> /var/log/honcho/stuff-1.log 2>&1'
```
migurski: I’ve updated https://github.com/nickstenning/honcho/pull/154, though with the file `honcho/test/integration/test_export.py` gone I’m not sure it will have an effect.
diff --git a/honcho/export/templates/upstart/process.conf b/honcho/export/templates/upstart/process.conf
index 8a05378..fe6b451 100644
--- a/honcho/export/templates/upstart/process.conf
+++ b/honcho/export/templates/upstart/process.conf
@@ -2,8 +2,7 @@ start on starting {{ group_name }}
 stop on stopping {{ group_name }}
 respawn
 
-exec su - {{ user }} -s {{ shell }} -c 'cd {{ app_root }};
-{%- for k, v in process.env.items() -%}
-  export {{ k }}={{ v | shellquote }};
-{%- endfor -%}
-exec {{ process.cmd }} >> {{ log }}/{{ process.name|dashrepl }}.log 2>&1'
+{% for k, v in process.env.items() -%}
+env {{ k }}={{ v | shellquote }}
+{% endfor %}
+exec su - {{ user }} -m -s {{ shell }} -c 'cd {{ app_root }}; exec {{ process.cmd }} >> {{ log }}/{{ process.name|dashrepl }}.log 2>&1'
diff --git a/honcho/manager.py b/honcho/manager.py
index 218f2b4..ff31a8e 100644
--- a/honcho/manager.py
+++ b/honcho/manager.py
@@ -53,7 +53,7 @@ class Manager(object):
 
         self._terminating = False
 
-    def add_process(self, name, cmd, quiet=False, env=None):
+    def add_process(self, name, cmd, quiet=False, env=None, cwd=None):
         """
         Add a process to this manager instance. The process will not be started
         until #loop() is called.
@@ -63,7 +63,8 @@ class Manager(object):
                                   name=name,
                                   quiet=quiet,
                                   colour=next(self._colours),
-                                  env=env)
+                                  env=env,
+                                  cwd=cwd)
         self._processes[name] = {}
         self._processes[name]['obj'] = proc
 
diff --git a/honcho/process.py b/honcho/process.py
index 669c8ef..c211af1 100644
--- a/honcho/process.py
+++ b/honcho/process.py
@@ -21,12 +21,14 @@ class Process(object):
                  name=None,
                  colour=None,
                  quiet=False,
-                 env=None):
+                 env=None,
+                 cwd=None):
         self.cmd = cmd
         self.colour = colour
         self.quiet = quiet
         self.name = name
         self.env = os.environ.copy() if env is None else env
+        self.cwd = cwd
 
         # This is a honcho.environ.Env object, to allow for stubbing of
         # external calls, not the operating system environment.
@@ -36,7 +38,7 @@ class Process(object):
 
     def run(self, events=None, ignore_signals=False):
         self._events = events
-        self._child = self._child_ctor(self.cmd, env=self.env)
+        self._child = self._child_ctor(self.cmd, env=self.env, cwd=self.cwd)
         self._send_message({'pid': self._child.pid}, type='start')
 
         # Don't pay attention to SIGINT/SIGTERM. The process itself is

Exported upstart configuration silently fails with quoted variables
At or near d8be8f4a8 (version 0.5.0), quoted variables in exported upstart scripts became invalid and fail silently. Previously, quoted `.env` variables with spaces generated upstart configurations with correctly-nested double and single quotes. This is an example generated by 84a1f7d (also version 0.5.0):

```
VAR="foo bar"
```

```
start on starting things-stuff
stop on stopping things-stuff
respawn

exec su - migurski -s /bin/sh -c 'cd /home/migurski/things; export PORT=5000; export VAR="foo bar"; python stuff.py >> /var/log/things/stuff-1.log 2>&1'
```

Starting at d8be8f4a8 and still in 0.6.6, the exported configuration from the configuration above began producing this invalid and failing upstart configuration, due to the single quotes:

```
start on starting things-stuff
stop on stopping things-stuff
respawn

exec su - migurski -s /bin/sh -c 'cd /home/migurski/things;export VAR='foo bar';export PORT=5000;python stuff.py >> /var/log/things/stuff-1.log 2>&1'
```

Here are my Procfile and python script for testing:

```
stuff: python stuff.py
```

```python
from sys import stderr
from os import environ
from time import sleep

while True:
	print >> stderr, repr(environ['VAR'])
	sleep(5)
```
diff --git a/tests/integration/test_export.py b/tests/integration/test_export.py
index bbb2397..09543b7 100644
--- a/tests/integration/test_export.py
+++ b/tests/integration/test_export.py
@@ -37,3 +37,37 @@ def test_export_upstart(testenv):
                      'trunk-web-1.conf'):
         expected = testenv.path('elephant', filename)
         assert os.path.exists(expected)
+
+
+@pytest.mark.parametrize('testenv', [{
+    'Procfile': "web: python web.py",
+    '.env': """
+NORMAL=ok
+SQ_SPACES='sqspace sqspace'
+DQ_SPACES="dqspace dqspace"
+SQ="it's got single quotes"
+DQ='it has "double" quotes'
+EXCL='an exclamation mark!'
+SQ_DOLLAR='costs $UNINTERPOLATED amount'
+DQ_DOLLAR="costs $UNINTERPOLATED amount"
+"""
+}], indirect=True)
+def test_export_upstart_environment(testenv):
+    ret, out, err = testenv.run_honcho([
+        'export',
+        'upstart',
+        testenv.path('test'),
+        '-a', 'envvars',
+    ])
+
+    assert ret == 0
+
+    lines = open(testenv.path('test', 'envvars-web-1.conf')).readlines()
+    assert 'env NORMAL=ok\n' in lines
+    assert "env SQ_SPACES='sqspace sqspace'\n" in lines
+    assert "env DQ_SPACES='dqspace dqspace'\n" in lines
+    assert "env SQ='it'\"'\"'s got single quotes'\n" in lines
+    assert "env DQ='it has \"double\" quotes'\n" in lines
+    assert "env EXCL='an exclamation mark!'\n" in lines
+    assert "env SQ_DOLLAR='costs $UNINTERPOLATED amount'\n" in lines
+    assert "env DQ_DOLLAR='costs $UNINTERPOLATED amount'\n" in lines
diff --git a/tests/test_manager.py b/tests/test_manager.py
index abee53c..ae028c6 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -59,12 +59,13 @@ class FakeEnv(object):
 
 class FakeProcess(object):
 
-    def __init__(self, cmd, name=None, colour=None, quiet=None, env=None):
+    def __init__(self, cmd, name=None, colour=None, quiet=None, env=None, cwd=None):
         self.cmd = cmd
         self.name = name
         self.colour = colour
         self.quiet = quiet
         self.env = env
+        self.cwd = cwd
 
         self._events = None
         self._options = {}
@@ -229,6 +230,10 @@ class TestManager(object):
         with pytest.raises(AssertionError):
             self.m.add_process('foo', 'another command')
 
+    def test_add_process_sets_cwd(self):
+        proc = self.m.add_process('foo', 'ruby server.rb', cwd='foo-dir')
+        assert proc.cwd == 'foo-dir'
+
     def test_loop_with_empty_manager_returns_immediately(self):
         self.m.loop()
 
diff --git a/tests/test_process.py b/tests/test_process.py
index 59e1538..9002212 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -184,3 +184,9 @@ class TestProcess(object):
         proc.run(self.q)
         msg = self.q.find_message({'returncode': 42})
         assert msg.type == 'stop'
+
+    def test_cwd_passed_along(self):
+        proc = Process('echo 123', cwd='fake-dir')
+        proc._child_ctor = FakePopen
+        proc.run(self.q)
+        assert proc._child.kwargs['cwd'] == 'fake-dir'

pip install -e .[export]
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
-e git+https://github.com/nickstenning/honcho.git@824775779ddf30606e7514b4639e81a2d6f25393#egg=honcho
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==2.7.3
MarkupSafe==3.0.2
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: honcho
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - jinja2==2.7.3
      - markupsafe==3.0.2
prefix: /opt/conda/envs/honcho

tests/test_manager.py::TestManager::test_add_process_sets_cwd tests/test_process.py::TestProcess::test_cwd_passed_along
tests/integration/test_export.py::test_export_supervisord[testenv0] tests/integration/test_export.py::test_export_upstart[testenv0] tests/integration/test_export.py::test_export_upstart_environment[testenv0]
tests/test_manager.py::TestManager::test_init_sets_default_printer_width tests/test_manager.py::TestManager::test_add_process_updates_printer_width tests/test_manager.py::TestManager::test_add_process_sets_name tests/test_manager.py::TestManager::test_add_process_sets_cmd tests/test_manager.py::TestManager::test_add_process_sets_colour tests/test_manager.py::TestManager::test_add_process_sets_unique_colours tests/test_manager.py::TestManager::test_add_process_sets_quiet tests/test_manager.py::TestManager::test_add_process_name_must_be_unique tests/test_manager.py::TestManager::test_loop_with_empty_manager_returns_immediately tests/test_manager.py::TestManager::test_loop_calls_process_run tests/test_manager.py::TestManager::test_printer_receives_messages_in_correct_order tests/test_manager.py::TestManager::test_printer_receives_lines_multi_process tests/test_manager.py::TestManager::test_returncode_set_by_first_exiting_process tests/test_manager.py::TestManager::test_printer_receives_lines_after_stop tests/test_process.py::TestProcess::test_ctor_cmd tests/test_process.py::TestProcess::test_ctor_name tests/test_process.py::TestProcess::test_ctor_colour tests/test_process.py::TestProcess::test_ctor_quiet tests/test_process.py::TestProcess::test_output_receives_start_with_pid tests/test_process.py::TestProcess::test_message_contains_name tests/test_process.py::TestProcess::test_message_contains_time tests/test_process.py::TestProcess::test_message_contains_colour tests/test_process.py::TestProcess::test_output_receives_lines tests/test_process.py::TestProcess::test_output_receives_lines_invalid_utf8 tests/test_process.py::TestProcess::test_output_does_not_receive_lines_when_quiet tests/test_process.py::TestProcess::test_output_receives_stop tests/test_process.py::TestProcess::test_output_receives_stop_with_returncode
honcho/process.py honcho/export/templates/upstart/process.conf honcho/manager.py
honcho/process.py honcho/export/templates/upstart/process.conf honcho/manager.py