Repository: PrefectHQ/prefect

Issue:
Add a `chdir` kwarg to `ShellTask`
Maybe named something more explicit, but I believe a common pattern with `ShellTask`s will be:
```
cd $DIR && do_something
```
so being able to write
```python
bash = ShellTask(chdir='/usr/tmp')
```
would be helpful.


Discussion:
+1 but I think "cd=" might be a more universally understood name for the keyword

Resolving patch:
diff --git a/src/prefect/tasks/shell.py b/src/prefect/tasks/shell.py
--- a/src/prefect/tasks/shell.py
+++ b/src/prefect/tasks/shell.py
@@ -10,19 +10,38 @@ class ShellTask(prefect.Task):
 
     Args:
         - shell (string, optional): shell to run the command with; defaults to "bash"
+        - cd (string, optional): string specifying the absolute path to a
+            directory to run the `command` from
+        - command (string, optional): shell command to be executed; can also be
+            provided post-initialization by calling this task instance
         - **kwargs: additional keyword arguments to pass to the Task constructor
+
+    Example:
+        ```python
+        from prefect import Flow
+        from prefect.tasks.shell import ShellTask
+
+        task = ShellTask(cd='/usr/bin')
+        with Flow() as f:
+            res = task(command='ls')
+
+        out = f.run(return_tasks=[res])
+        ```
     """
 
-    def __init__(self, shell="bash", **kwargs):
+    def __init__(self, shell="bash", cd=None, command=None, **kwargs):
         self.shell = shell
+        self.cd = cd
+        self.command = command
         super().__init__(**kwargs)
 
-    def run(self, command, env=None):
+    def run(self, command=None, env=None):
         """
         Run the shell command.
 
         Args:
-            - command (string): shell command to be executed
+            - command (string): shell command to be executed; can also be
+                provided at task initialization
             - env (dict, optional): dictionary of environment variables to use for
                 the subprocess; if provided, will override all other environment variables present
                 on the system
@@ -35,6 +54,13 @@ def run(self, command, env=None):
             - prefect.engine.signals.FAIL: if command has an exit code other
                 than 0
         """
+        command = command or self.command
+        if command is None:
+            raise TypeError("run() missing required argument: 'command'")
+
+        if self.cd is not None:
+            command = "cd {} && ".format(self.cd) + command
+
         current_env = env or os.environ.copy()
         try:
             out = subprocess.check_output(


Test patch:
diff --git a/tests/tasks/test_shell.py b/tests/tasks/test_shell.py
--- a/tests/tasks/test_shell.py
+++ b/tests/tasks/test_shell.py
@@ -1,10 +1,12 @@
 import os
 import pytest
 import subprocess
+import tempfile
 
 from prefect import Flow
 from prefect.engine import signals
 from prefect.tasks.shell import ShellTask
+from prefect.utilities.tests import raise_on_exception
 
 
 def test_shell_initializes_and_runs_basic_cmd():
@@ -15,6 +17,23 @@ def test_shell_initializes_and_runs_basic_cmd():
     assert out.result[task].result == b"hello world"
 
 
+def test_shell_initializes_with_basic_cmd():
+    with Flow() as f:
+        task = ShellTask(command="echo -n 'hello world'")()
+    out = f.run(return_tasks=[task])
+    assert out.is_successful()
+    assert out.result[task].result == b"hello world"
+
+
+def test_shell_raises_if_no_command_provided():
+    with Flow() as f:
+        task = ShellTask()()
+
+    with pytest.raises(TypeError):
+        with raise_on_exception():
+            out = f.run(return_tasks=[task])
+
+
 @pytest.mark.skipif(subprocess.call(["which", "zsh"]), reason="zsh not installed.")
 def test_shell_runs_other_shells():
     with Flow() as f:
@@ -79,3 +98,37 @@ def test_shell_task_raises_fail_if_cmd_fails():
     out = f.run(return_tasks=[task])
     assert out.is_failed()
     assert "Command failed with exit code" in str(out.result[task].message)
+
+
+def test_shell_task_accepts_and_uses_cd_kwarg_at_init():
+    with tempfile.TemporaryDirectory() as tempdir:
+        with open(tempdir + "/testfile.txt", "w") as f:
+            f.write("this is a test")
+
+        with Flow() as f:
+            task = ShellTask(cd=tempdir)(command="ls")
+        out = f.run(return_tasks=[task])
+
+    assert out.is_successful()
+    assert out.result[task].result == b"testfile.txt\n"
+
+
+def test_shell_task_cd_kwarg_handles_multiline_commands():
+    cmd = """
+    for file in $(ls)
+    do
+        cat $file
+    done
+    """
+
+    with tempfile.TemporaryDirectory() as tempdir:
+        with open(tempdir + "/testfile.txt", "w") as f:
+            f.write("this is a test")
+
+        with Flow() as f:
+            task = ShellTask(cd=tempdir)(command=cmd)
+
+        out = f.run(return_tasks=[task])
+
+    assert out.is_successful()
+    assert out.result[task].result == b"this is a test"