diff --git a/src/pyinfra/operations/files.py b/src/pyinfra/operations/files.py index afbfa9fcc..b33bc0707 100644 --- a/src/pyinfra/operations/files.py +++ b/src/pyinfra/operations/files.py @@ -1925,7 +1925,8 @@ def block( + line: regex before or after which the content should be added if it doesn't exist. + backup: whether to backup the file (see ``files.line``). Default False. + escape_regex_characters: whether to escape regex characters from the matching line - + try_prevent_shell_expansion: tries to prevent shell expanding by values like `$` + + try_prevent_shell_expansion: deprecated and ignored; ``content`` is always written + literally (no shell expansion) and is safely shell-quoted + marker: the base string used to mark the text. Default is ``# {mark} PYINFRA BLOCK`` + begin: the value for ``{mark}`` in the marker before the content. Default is ``BEGIN`` + end: the value for ``{mark}`` in the marker after the content. Default is ``END`` @@ -1945,8 +1946,9 @@ def block( Removal ignores ``content`` and ``line`` - Preventing shell expansion works by wrapping the content in '`' before passing to `awk`. - WARNING: This will break if the content contains raw single quotes. + ``content`` is written to the file verbatim. It is shell-quoted before being passed to + ``awk``, so shell metacharacters (``$(...)``, quotes, backticks) are kept literal and never + expanded on the remote host. **Examples:** @@ -1987,11 +1989,10 @@ def block( marker="", ) - # put complex alias into .zshrc + # put complex alias into .zshrc (written literally, no shell expansion) files.block( path="/home/user/.zshrc", content="eval $(thef -a)", - try_prevent_shell_expansion=True, marker="## {mark} ALIASES ##" ) """ @@ -2068,15 +2069,8 @@ def block( # convert string to list of lines content = content.split("\n") - the_block = "\n".join([mark_1, *content, mark_2]) - if try_prevent_shell_expansion: - the_block = f"'{the_block}'" - if any("'" in line for line in content): - logger.warning( - "content contains single quotes, shell expansion prevention may fail" - ) - else: - the_block = f'"{the_block}"' + block_with_markers = "\n".join([mark_1, *content, mark_2]) + block_content = "\n".join(content) if (current is None) or ((current == []) and (before == after)): # a) no file or b) file but no markers and we're adding at start or end. @@ -2090,7 +2084,7 @@ def block( original if not before else " - ", original if before else " - ", '> "$OUT"', - f"<<{here}\n{the_block[1:-1]}\n{here}\n", + f"<<'{here}'\n{block_with_markers}\n{here}\n", ")", real_out, ) @@ -2109,7 +2103,7 @@ def block( out_prep, prog, q_path, - the_block, + QuoteString(block_with_markers), '> "$OUT"', real_out, ) @@ -2125,11 +2119,7 @@ def block( out_prep, prog, q_path, - ( - '"' + "\n".join(content) + '"' - if not try_prevent_shell_expansion - else "'" + "\n".join(content) + "'" - ), + QuoteString(block_content), '> "$OUT"', real_out, ) diff --git a/tests/operations/files.block/add_existing_block_different_content.json b/tests/operations/files.block/add_existing_block_different_content.json index 87e26676f..a4e53991d 100644 --- a/tests/operations/files.block/add_existing_block_different_content.json +++ b/tests/operations/files.block/add_existing_block_different_content.json @@ -19,6 +19,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something \"should be this\" > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" + "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something 'should be this' > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_existing_block_different_content_and_backup.json b/tests/operations/files.block/add_existing_block_different_content_and_backup.json index f61c9e9a2..716fb459f 100644 --- a/tests/operations/files.block/add_existing_block_different_content_and_backup.json +++ b/tests/operations/files.block/add_existing_block_different_content_and_backup.json @@ -20,6 +20,6 @@ } }, "commands": [ - "cp /home/someone/something /home/someone/something.a-timestamp && OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something \"should be this\" > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" + "cp /home/someone/something /home/someone/something.a-timestamp && OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something 'should be this' > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_existing_block_different_content_multiple_lines.json b/tests/operations/files.block/add_existing_block_different_content_multiple_lines.json index 6bc1e7f63..ea6a71b2d 100644 --- a/tests/operations/files.block/add_existing_block_different_content_multiple_lines.json +++ b/tests/operations/files.block/add_existing_block_different_content_multiple_lines.json @@ -23,6 +23,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something \"should be this\nand this\nand even this\" > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" + "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something 'should be this\nand this\nand even this' > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_no_existing_block_and_no_line.json b/tests/operations/files.block/add_no_existing_block_and_no_line.json index 75b2c62f1..7549bb43c 100644 --- a/tests/operations/files.block/add_no_existing_block_and_no_line.json +++ b/tests/operations/files.block/add_no_existing_block_and_no_line.json @@ -17,6 +17,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && ( awk '{{print}}' - /dev/null > \"$OUT\" </dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && ( awk '{{print}}' - /dev/null > \"$OUT\" <<'PYINFRAHERE'\n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE\n ) && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_no_existing_block_line_provided.json b/tests/operations/files.block/add_no_existing_block_line_provided.json index 648fec0ab..a3b2f4ce3 100644 --- a/tests/operations/files.block/add_no_existing_block_line_provided.json +++ b/tests/operations/files.block/add_no_existing_block_line_provided.json @@ -17,6 +17,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this.*$/ { print x; f=1} END {if (f==0) print x } { print }' /home/someone/something \"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" + "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this.*$/ { print x; f=1} END {if (f==0) print x } { print }' /home/someone/something '# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK' > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_no_existing_block_line_provided_escape_regex.json b/tests/operations/files.block/add_no_existing_block_line_provided_escape_regex.json index 7c21c57dd..ee3235589 100644 --- a/tests/operations/files.block/add_no_existing_block_line_provided_escape_regex.json +++ b/tests/operations/files.block/add_no_existing_block_line_provided_escape_regex.json @@ -18,6 +18,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this \\*.*$/ { print x; f=1} END {if (f==0) print x } { print }' /home/someone/something \"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" + "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && MODE=\"$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this \\*.*$/ { print x; f=1} END {if (f==0) print x } { print }' /home/someone/something '# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK' > \"$OUT\" && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something && chmod \"$MODE\" /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/add_no_existing_file.json b/tests/operations/files.block/add_no_existing_file.json index e20948ff1..a990002a1 100644 --- a/tests/operations/files.block/add_no_existing_file.json +++ b/tests/operations/files.block/add_no_existing_file.json @@ -17,6 +17,6 @@ } }, "commands": [ - "OUT=\"$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)\" && OWNER=\"$(stat -c \"%u:%g\" /home/someone/something 2>/dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && ( awk '{{print}}' /dev/null - > \"$OUT\" </dev/null || stat -f \"%u:%g\" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))\" && ( awk '{{print}}' /dev/null - > \"$OUT\" <<'PYINFRAHERE'\n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE\n ) && mv \"$OUT\" /home/someone/something && chown \"$OWNER\" /home/someone/something /home/someone/something" ] } \ No newline at end of file diff --git a/tests/operations/files.block/line_match_block_with_shell_metachars.yaml b/tests/operations/files.block/line_match_block_with_shell_metachars.yaml new file mode 100644 index 000000000..ec072d030 --- /dev/null +++ b/tests/operations/files.block/line_match_block_with_shell_metachars.yaml @@ -0,0 +1,22 @@ +require_platform: +- Darwin +- Linux +args: +- /home/someone/something +kwargs: + content: |- + alias l="lsd -la" + export _ZO_ECHO='1' + eval "$(zoxide init --cmd cd bash)" + line: before this + before: true +facts: + files.Block: + begin=None, end=None, marker=None, path=/home/someone/something: [] +commands: +- |- + OUT="$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)" && MODE="$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)" && OWNER="$(stat -c "%u:%g" /home/someone/something 2>/dev/null || stat -f "%u:%g" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=""} f!=1 && /^.*before this.*$/ { print x; f=1} END {if (f==0) print x } { print }' /home/someone/something '# BEGIN PYINFRA BLOCK + alias l="lsd -la" + export _ZO_ECHO='"'"'1'"'"' + eval "$(zoxide init --cmd cd bash)" + # END PYINFRA BLOCK' > "$OUT" && mv "$OUT" /home/someone/something && chown "$OWNER" /home/someone/something && chmod "$MODE" /home/someone/something diff --git a/tests/operations/files.block/no_file_block_with_shell_metachars.yaml b/tests/operations/files.block/no_file_block_with_shell_metachars.yaml new file mode 100644 index 000000000..209caebf5 --- /dev/null +++ b/tests/operations/files.block/no_file_block_with_shell_metachars.yaml @@ -0,0 +1,25 @@ +require_platform: +- Darwin +- Linux +args: +- /home/someone/something +kwargs: + content: |- + alias l="lsd -la" + export _ZO_ECHO='1' + eval "$(zoxide init --cmd cd bash)" + before: false + after: false +facts: + files.Block: + begin=None, end=None, marker=None, path=/home/someone/something: null +commands: +- |- + OUT="$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)" && OWNER="$(stat -c "%u:%g" /home/someone/something 2>/dev/null || stat -f "%u:%g" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))" && ( awk '{{print}}' /dev/null - > "$OUT" <<'PYINFRAHERE' + # BEGIN PYINFRA BLOCK + alias l="lsd -la" + export _ZO_ECHO='1' + eval "$(zoxide init --cmd cd bash)" + # END PYINFRA BLOCK + PYINFRAHERE + ) && mv "$OUT" /home/someone/something && chown "$OWNER" /home/someone/something /home/someone/something diff --git a/tests/operations/files.block/update_block_with_shell_metachars.yaml b/tests/operations/files.block/update_block_with_shell_metachars.yaml new file mode 100644 index 000000000..6c4dc2064 --- /dev/null +++ b/tests/operations/files.block/update_block_with_shell_metachars.yaml @@ -0,0 +1,21 @@ +require_platform: +- Darwin +- Linux +args: +- /home/someone/something +kwargs: + content: |- + alias l="lsd -la" + export _ZO_ECHO='1' + eval "$(zoxide init --cmd cd bash)" + line: before this + before: true +facts: + files.Block: + begin=None, end=None, marker=None, path=/home/someone/something: + - previously was something else +commands: +- |- + OUT="$(TMPDIR=_tempdir_ mktemp -t pyinfra.XXXXXX)" && MODE="$(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something 2>/dev/null)" && OWNER="$(stat -c "%u:%g" /home/someone/something 2>/dev/null || stat -f "%u:%g" /home/someone/something 2>/dev/null || echo $(id -un):$(id -gn))" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=""}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something 'alias l="lsd -la" + export _ZO_ECHO='"'"'1'"'"' + eval "$(zoxide init --cmd cd bash)"' > "$OUT" && mv "$OUT" /home/someone/something && chown "$OWNER" /home/someone/something && chmod "$MODE" /home/someone/something