# Run a remote command from a template \*file\*

**URL:** <https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319>\
**Category:** Get Help\
**Tags:** yaml, playbook, jinja2\
**Created:** [September 16, 2026, 8:04pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319 "2026-09-16T20:04:49Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [September 16, 2026, 8:04pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/1 "2026-09-16T20:04:49Z")

</div>

I need to run a few lines of shell on a remote system, and I find YAML more than awkward to use for this and would prefer to put this into a `foo.sh.j2` file instead.

Now I could do the dance:

- stat to test if a remote directory exists
- create a remote temporary path if said directory does not exist
- run the template module with `foo.sh.j2` into the remote temporary path if said directory does not exist
- run `sh path/foo.sh` with the `command` module if said directory does not exist, creating said directory
- remove the remote temporary path if said directory did not exist

This is quite the overhead for something that ideally _does_ get executed only once.

What I am really looking for is something like:

```auto
ansible.builtin.shell:
  cmd_from_template: foo.sh.j2
  creates: /path/to/said/directory

```

The `foo.sh.j2` file needs to be evaluated locally, as is normal for templates; this is **not** a request to do that remotely.

---

<div class="post-metadata">

**Author:** ![jborean](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/jborean/32/1525_2.png) [@jborean](https://forum.ansible.com/u/jborean)\
**Post date:** [September 17, 2026, 12:15am UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/2 "2026-09-17T00:15:18Z")

</div>

You can use the template lookup to build a string from a template using the [template lookup](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/template_lookup.html):

```yaml
- name: run shell command from template
  ansible.builtin.shell:
    cmd: '{{ lookup("template", "foo.sh.j2") }}'
    creates: /path/to/said/directory

```

Please keep in mind this could be dangerous if the template is from an untrusted source, that may be a moot point if it’s stored in the same way as the playbook but if sourced elsewhere this could lead to an injection attack.

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [September 17, 2026, 2:04pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/3 "2026-09-17T14:04:51Z")

</div>

Ah, like that; yes, that should work, I’ll play with it around a bit before I check it as solution, thanks.

---

<div class="post-metadata">

**Author:** ![webfargo-dtoler](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/webfargo-dtoler/32/11321_2.png) [@webfargo-dtoler](https://forum.ansible.com/u/webfargo-dtoler)\
**Post date:** [October 4, 2026, 12:44pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/4 "2026-10-04T12:44:53Z")

</div>

The template module evaluates on the control node but using inventory context of the target node, generates the result and transfers it to the remote node for storage which also lets you specify permissions and file attributes. You can then call the “shell” or “command” module as necessary to execute your generated script. It’s two tasks in this fashion, but it depends on what level of detail you want to achieve.

Regardless of which method, you still need the additional “file” module task to delete the file on the target node.

I also assume you’re using the ssh connection type for all this. If you’re using something else, then this may not apply.

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [October 12, 2026, 1:20pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/5 "2026-10-12T13:20:22Z")

</div>

No, you can run a script from stdin or from command line, you do not need any file.

I was just missing the building blocks to template either. I’ve not yet come to it, but what jborean wrote may just be it.

---

<div class="post-metadata">

**Author:** ![utoddl](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/utoddl/32/249_2.png) [@utoddl](https://forum.ansible.com/u/utoddl)\
**Post date:** [October 12, 2026, 1:55pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/6 "2026-10-12T13:55:58Z")

</div>

I’m really curious about why you want to use a template rather than in-line shell text.  
You do you; it’s great you’ve got a solution that you like. I’ve done a lot (too much?) in-line shell, and maybe there’s something I’ve not considered. So I’m curious.

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [October 12, 2026, 2:18pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/7 "2026-10-12T14:18:20Z")

</div>

Because I want to write it as separate file, in my text editor, with syntax highlighting for shell, and avoiding escape hassles, instead of as one huge line for YAML with tricky escapes.

(I _loathe_ YAML.)

---

<div class="post-metadata">

**Author:** ![webfargo-dtoler](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/webfargo-dtoler/32/11321_2.png) [@webfargo-dtoler](https://forum.ansible.com/u/webfargo-dtoler)\
**Post date:** [October 12, 2026, 2:27pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/8 "2026-10-12T14:27:38Z")

</div>

I can’t speak to what @mirabilos is doing, but I use a J2 template when I’m running the CLI scripts on Check Point firewalls and management servers.

In the J2 template, I add pre-/post- type of boilerplate things. Such things include “is this running on a singe gateway or cluster member? if it’s a cluster member, is this the ACTIVE or STANDBY cluster member?”; sometimes I may want this script to run only on the ACTIVE gateway (for some of current-state info-fetching command which is only relevant on the ACTIVE gateway), and sometimes I want the script to run on both gateways (for some general configuration command). Having the J2 template do the “ugly” and highly-repetitive parts is nice, so that my playbook task (and the kickoff script I use) only sends the command itself. My kickoff script allows me to provide a runtime flag (-a) which passes an extra-vars variable to the playbook, and read by the J2 template, to choose “one gateway or all gateways of the cluster”. Plus, I the human don’t have to do any thinking beyond that one J2 template. 🙂

So you can extend the same mnemonic to any system that’s a member of group (K8S pods, load-balanced web heads, replicated databases, whatever).

If you ever have a problem or need a revision, you can edit the J2 template, as a changeable artifact, rather than edit the playbook, which should be a fixed and replicable structure. Think “separation of concerns”: let the variable data input be the driver, not the system itself. Much like an automobile: the driver is the variant; the vehicle is the fixed-frame device.

I’m also a very _ **very** _ lazy person, so I work hard now (once) to be lazy later. 😃

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [October 13, 2026, 7:20pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/9 "2026-10-13T19:20:55Z")

</div>

Thanks, this indeed worked. I wrote a `roles/schroot/templates/init.j2` with something along the lines of…

```auto
#!/bin/sh
 
# just something to test this procedure
 
{
        printf '%s' $#
        printf ' <%s>' "$0" "$@"
        echo
        #cat -- "$0"
} | logger -t schroot-init-j2-debug
 
# need entropy for some init in the chroot
echo .{{ lookup('community.general.random_string', base64=true, length=500) }} >>/dev/urandom
 
# all tests for validity are in confd.j2
echo would create chroot {{ thechroot.name }} with {{ thechroot.dist }} in {{ thechroot.path }}
echo with extra packages {{ (['apt', 'debian-archive-keyring'] + schroot_role_pkgs) | join(',') }}
echo {{ thechroot.name }} \>{{ thechroot.path }}/etc/debian_chroot

```

… and a task…

```auto
- name: "{{ thechroot.name }} : INSTALL | set up chroot"
  ansible.builtin.command:
    argv: ["/bin/sh", "-s"]
    chdir: /
    creates: "{{ thechroot.path }}"
    stdin: "{{ lookup('template', 'init.j2') }}"
  register: initsh
 
- name: "{{ thechroot.name }} : debug"
  ansible.builtin.debug:
    var: initsh

```

… and it works as expected (the actual setting up of the chroot will come later…)

---

<div class="post-metadata">

**Author:** ![webfargo-dtoler](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/webfargo-dtoler/32/11321_2.png) [@webfargo-dtoler](https://forum.ansible.com/u/webfargo-dtoler)\
**Post date:** [October 13, 2026, 7:44pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/10 "2026-10-13T19:44:36Z")

</div>

Nice work!

You may want to also consider explicitly using /bin/bash instead to ensure you get Bash-specific behavior (this assumes your target host has bash and a recent version). I know /bin/sh is a symlink to /bin/bash on many systems, but others actually link it to other shells ike /bin/dash, for example. Depending on where you are targeting this script, you may want to be more specific. Or, alternatively, specify whichever shell you’re expecting (ash, dash, zsh, …).

I’m also a fan of running the script under “set -Eeuo pipefail” for best safety and avoid unexpected weirdness. Warning: this radically changes the behavior of your script, tho! It’s just like running Perl scripts with “use strict;”.

Good luck with the rest of your script!

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [October 14, 2026, 1:54pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/11 "2026-10-14T13:54:30Z")

</div>

Ugh, no, why would I want GNU bash?

I write either for POSIX sh or for `/bin/mksh`, of course 😼

But thanks for explaining `set -euo pipefail` (there’s no `set -E`…) to the developer of a Unix shell 😺

I’m not a fan of `set -e` in _general_ because errorlevel ≠ 0 does not always mean error (e.g. in `grep(1)`). Enacting these flags for the whole of a script is good training wheels for novice shell programmers, agreed.

(This scriptlet likely won’t need anything not in POSIX sh, it’s just long enough to be annoying to write it inline, and, this way, I can comment and structure better. It probably will need cleanup `trap`s, though, or an atomic `mv(1)` at the end; we’ll see.)

---

<div class="post-metadata">

**Author:** ![webfargo-dtoler](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/webfargo-dtoler/32/11321_2.png) [@webfargo-dtoler](https://forum.ansible.com/u/webfargo-dtoler)\
**Post date:** [October 14, 2026, 2:24pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/12 "2026-10-14T14:24:19Z")

</div>

POSIX compliance is good; nice target for portability! For your intended script, agreed, all this is probably overkill. I was just getting myself tripped long ago when I was using /bin/sh and expecting bash behaviors in particular. Not everyone likes Bash, tho (it’s definitely quirky!).

You might already know, but just in case, for `set -e`, you get an exemption when it’s used in things like:

```bash
# set -e gets bypassed
if git diff --cached --quiet; then
  echo "uncommitted changes exist. barf."
  exit 1
fi

```

`set -e` was breaking my scripts at first, too. If you have such a scenario where `grep` is ok to return non-zero, and you’d rather check the output, then you can head-fake it like:

```bash
OUTPUT=$(grep foo bar || /bin/true) # allow nonzero for set -e
if [-z "${OUTPUT}"]; then
  echo "oops. null output."
  exit 1
fi

```

Anyhoo… I didn’t mean to hijack the direction; one thought started another. I’ll hush now. 🙂  
Good luck with the rest of your project!

---

<div class="post-metadata">

**Author:** ![mirabilos](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mirabilos/32/9721_2.png) [@mirabilos](https://forum.ansible.com/u/mirabilos)\
**Post date:** [October 14, 2026, 10:35pm UTC](https://forum.ansible.com/t/run-a-remote-command-from-a-template-file/46319/13 "2026-10-14T22:35:06Z")

</div>

As the developer of a shell, I _might_ just know 😉

It’s not helpful in that instance; for `grep(1)`, you normally need to differentiate between 0 (matches found), 1 (no mathches found) and else (error).

(btw, look into `grep -q`)

Also fun: if you have a function that ends with such a construct, e.g…

```auto
foo() {
        # …
        if false; then
                echo meow
        fi
}

```

… then the function can still return 1 (it does not in this particular example, but I’ve seen more complex cases in which it did), which can cause `set -e` to trigger at the _calling_ place, unless it _also_ uses `if`, `&&` or `||`; this gets even more fun with `set -o pipefail`.

But, we digress.
