Repository: kubeflow/pipelines

Issue:
Escape sequence bug in func_to_container_op
### What steps did you take:

I tried to run a pipeline that contains newline code which is one of the escape sequences in `func_to_container_op` as follows.

```python
import kfp
from kfp import dsl
from kfp.components import func_to_container_op

@func_to_container_op
def test():
    print("hello\nworld")

@dsl.pipeline()
def pipeline():
    test()

if __name__ == '__main__':
    kfp.compiler.Compiler().compile(pipeline, __file__ + '.yaml')
```

This is the compiled yaml.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: pipeline-
  annotations: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0, pipelines.kubeflow.org/pipeline_compilation_time: '2020-12-25T00:18:32.938960',
    pipelines.kubeflow.org/pipeline_spec: '{"name": "Pipeline"}'}
  labels: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0}
spec:
  entrypoint: pipeline
  templates:
  - name: pipeline
    dag:
      tasks:
      - {name: test, template: test}
  - name: test
    container:
      args: []
      command:
      - sh
      - -ec
      - |
        program_path=$(mktemp)
        echo -n "$0" > "$program_path"
        python3 -u "$program_path" "$@"
      - |
        def test():
            print("hello\nworld")

        import argparse
        _parser = argparse.ArgumentParser(prog='Test', description='')
        _parsed_args = vars(_parser.parse_args())

        _outputs = test(**_parsed_args)
      image: python:3.7
    metadata:
      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
          {"args": [], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
          > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def test():\n    print(\"hello\\nworld\")\n\nimport
          argparse\n_parser = argparse.ArgumentParser(prog=''Test'', description='''')\n_parsed_args
          = vars(_parser.parse_args())\n\n_outputs = test(**_parsed_args)\n"], "image":
          "python:3.7"}}, "name": "Test"}', pipelines.kubeflow.org/component_ref: '{}'}
  arguments:
    parameters: []
  serviceAccountName: pipeline-runner

```

### What happened:

Syntax error have been displayed.

```
  File "/tmp/tmp.ncPxm38FZc", line 2
    print("hello
               ^
SyntaxError: EOL while scanning string literal
```

### What did you expect to happen:

Escape sequences are being interpreted when outputting python to a file. I want the newline code to be output to the file as is.

### Environment:

AI Platform Pipeline in GCP
cluster version: 1.0.4
KFP version: 1.2.0

### Anything else you would like to add:

https://github.com/kubeflow/pipelines/commit/7a66414cf72ba5c746cac7fc6c8ecad67fc5e885
This bug is caused by the above commit. By using echo in /bin/sh, the escape sequence is interpreted and output to a file. When I changed sh in yaml to bash as a test, this syntax error did not occur and the program worked fine.

fix yaml
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: pipeline-
  annotations: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0, pipelines.kubeflow.org/pipeline_compilation_time: '2020-12-24T23:54:02.062104',
    pipelines.kubeflow.org/pipeline_spec: '{"name": "Pipeline"}'}
  labels: {pipelines.kubeflow.org/kfp_sdk_version: 1.2.0}
spec:
  entrypoint: pipeline
  templates:
  - name: pipeline
    dag:
      tasks:
      - {name: test, template: test}
  - name: test
    container:
      args: []
      command:
      - bash
      - -ec
      - |
        program_path=$(mktemp)
        echo -n "$0" > "$program_path"
        python3 -u "$program_path" "$@"
      - |
        def test():
            print("hello\nworld")

        import argparse
        _parser = argparse.ArgumentParser(prog='Test', description='')
        _parsed_args = vars(_parser.parse_args())

        _outputs = test(**_parsed_args)
      image: python:3.7
    metadata:
      annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
          {"args": [], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
          > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def test():\n    print(\"hello\\nworld\")\n\nimport
          argparse\n_parser = argparse.ArgumentParser(prog=''Test'', description='''')\n_parsed_args
          = vars(_parser.parse_args())\n\n_outputs = test(**_parsed_args)\n"], "image":
          "python:3.7"}}, "name": "Test"}', pipelines.kubeflow.org/component_ref: '{}'}
  arguments:
    parameters: []
  serviceAccountName: pipeline-runner
```

output
```
hello
world

```

/kind bug
<!-- Please include labels by uncommenting them to help us better triage issues, choose from the following -->
<!--
// /area frontend
// /area backend
// /area sdk
// /area testing
// /area engprod
-->



Discussion:
I've run into this issue as well with escaping quoted characters. I feel like this ought to be a higher priority bug because this creates an unexpected difference in serialization of strings in lightweight components versus regular components. 

For us this is a blocking issue for adopting 1.2/1.3 since it would require a lot of refactoring in our string manipulation components to work around this.

@Ark-kun  do you think that the above workaround using bash would be an appropriate fix or is there a particular reason for using sh? 
/assign @chensun @numerology 
I think this is a P0, I tried `Data passing sample pipeline`, it's also failing with `EOL while scanning string literal` like these issues.
/assign @Ark-kun 
> I think this is a P0, I tried `Data passing sample pipeline`, it's also failing with `EOL while scanning string literal` like these issues.

Oh. I think I know what's happening then. It's `echo` interpreting the escapes. I'll fix this ASAP.

Resolving patch:
diff --git a/sdk/python/kfp/components/_python_op.py b/sdk/python/kfp/components/_python_op.py
--- a/sdk/python/kfp/components/_python_op.py
+++ b/sdk/python/kfp/components/_python_op.py
@@ -675,7 +675,7 @@ def get_serializer_and_register_definitions(type_name) -> str:
                 # This is needed for Python to show stack traces and for `inspect.getsource` to work (used by PyTorch JIT and this module for example).
                 textwrap.dedent('''\
                     program_path=$(mktemp)
-                    echo -n "$0" > "$program_path"
+                    printf "%s" "$0" > "$program_path"
                     python3 -u "$program_path" "$@"
                 '''),
                 full_source,
diff --git a/sdk/python/kfp/containers/_component_builder.py b/sdk/python/kfp/containers/_component_builder.py
--- a/sdk/python/kfp/containers/_component_builder.py
+++ b/sdk/python/kfp/containers/_component_builder.py
@@ -217,7 +217,7 @@ def build_python_component(component_func, target_image, base_image=None, depend
   component_spec = _func_to_component_spec(component_func, base_image=base_image)
   command_line_args = component_spec.implementation.container.command
 
-  program_launcher_index = command_line_args.index('program_path=$(mktemp)\necho -n "$0" > "$program_path"\npython3 -u "$program_path" "$@"\n')
+  program_launcher_index = command_line_args.index('program_path=$(mktemp)\nprintf "%s" "$0" > "$program_path"\npython3 -u "$program_path" "$@"\n')
   assert program_launcher_index in [2, 3]
 
   program_code_index = program_launcher_index + 1


Test patch:
diff --git a/sdk/python/kfp/components_tests/test_python_op.py b/sdk/python/kfp/components_tests/test_python_op.py
--- a/sdk/python/kfp/components_tests/test_python_op.py
+++ b/sdk/python/kfp/components_tests/test_python_op.py
@@ -945,6 +945,13 @@ def some_func():
         component_spec = task_factory.component_spec
         self.assertEqual(component_spec.metadata.annotations, annotations)
 
+    def test_code_with_escapes(self):
+        def my_func():
+            "Hello \n world"
+
+        task_factory = comp.create_component_from_func(my_func)
+        self.helper_test_component_using_local_call(task_factory, arguments={}, expected_output_values={})
+
     def test_end_to_end_python_component_pipeline(self):
         #Defining the Python function
         def add(a: float, b: float) -> float:
diff --git a/sdk/python/tests/compiler/testdata/parallelfor_item_argument_resolving.yaml b/sdk/python/tests/compiler/testdata/parallelfor_item_argument_resolving.yaml
--- a/sdk/python/tests/compiler/testdata/parallelfor_item_argument_resolving.yaml
+++ b/sdk/python/tests/compiler/testdata/parallelfor_item_argument_resolving.yaml
@@ -20,7 +20,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -51,7 +51,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -82,7 +82,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -113,7 +113,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -144,7 +144,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -175,7 +175,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -206,7 +206,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def consume(param1):
@@ -387,7 +387,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def produce_list_of_dicts():
@@ -452,7 +452,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def produce_list_of_ints():
@@ -517,7 +517,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def produce_list_of_strings():
@@ -582,7 +582,7 @@ spec:
           - "-ec"
           - |
               program_path=$(mktemp)
-              echo -n "$0" > "$program_path"
+              printf "%s" "$0" > "$program_path"
               python3 -u "$program_path" "$@"
           - |
               def produce_str():
diff --git a/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml
--- a/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml
+++ b/sdk/python/tests/compiler/testdata/parallelfor_pipeline_param_in_items_resolving.yaml
@@ -18,7 +18,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def consume(param1):
@@ -37,7 +37,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
-          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          "program_path=$(mktemp)\nprintf \"%s\" \"$0\" > \"$program_path\"\npython3 -u
           \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
           argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
           dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
@@ -53,7 +53,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def consume(param1):
@@ -72,7 +72,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
-          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          "program_path=$(mktemp)\nprintf \"%s\" \"$0\" > \"$program_path\"\npython3 -u
           \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
           argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
           dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
@@ -88,7 +88,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def consume(param1):
@@ -107,7 +107,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
-          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          "program_path=$(mktemp)\nprintf \"%s\" \"$0\" > \"$program_path\"\npython3 -u
           \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
           argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
           dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
@@ -123,7 +123,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def consume(param1):
@@ -142,7 +142,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
-          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          "program_path=$(mktemp)\nprintf \"%s\" \"$0\" > \"$program_path\"\npython3 -u
           \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
           argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
           dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
@@ -158,7 +158,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def consume(param1):
@@ -177,7 +177,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--param1", {"inputValue": "param1"}], "command": ["sh", "-ec",
-          "program_path=$(mktemp)\necho -n \"$0\" > \"$program_path\"\npython3 -u
+          "program_path=$(mktemp)\nprintf \"%s\" \"$0\" > \"$program_path\"\npython3 -u
           \"$program_path\" \"$@\"\n", "def consume(param1):\n    print(param1)\n\nimport
           argparse\n_parser = argparse.ArgumentParser(prog=''Consume'', description='''')\n_parser.add_argument(\"--param1\",
           dest=\"param1\", type=str, required=True, default=argparse.SUPPRESS)\n_parsed_args
@@ -280,7 +280,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def produce_message(fname1):
@@ -328,7 +328,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--fname1", {"inputValue": "fname1"}, "----output-paths", {"outputPath":
-          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
+          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\nprintf \"%s\" \"$0\"
           > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def produce_message(fname1):\n    return
           \"My name is %s\" % fname1\n\ndef _serialize_str(str_value: str) -> str:\n    if
           not isinstance(str_value, str):\n        raise TypeError(''Value \"{}\"
@@ -353,7 +353,7 @@ spec:
       - -ec
       - |
         program_path=$(mktemp)
-        echo -n "$0" > "$program_path"
+        printf "%s" "$0" > "$program_path"
         python3 -u "$program_path" "$@"
       - |
         def produce_message(fname1):
@@ -401,7 +401,7 @@ spec:
     metadata:
       annotations: {pipelines.kubeflow.org/component_spec: '{"implementation": {"container":
           {"args": ["--fname1", {"inputValue": "fname1"}, "----output-paths", {"outputPath":
-          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\necho -n \"$0\"
+          "Output"}], "command": ["sh", "-ec", "program_path=$(mktemp)\nprintf \"%s\" \"$0\"
           > \"$program_path\"\npython3 -u \"$program_path\" \"$@\"\n", "def produce_message(fname1):\n    return
           \"My name is %s\" % fname1\n\ndef _serialize_str(str_value: str) -> str:\n    if
           not isinstance(str_value, str):\n        raise TypeError(''Value \"{}\"