Ansible win_shell and win_command: Run Commands on Windows Complete Guide
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible win_shell and win_command modules. Run PowerShell scripts, CMD commands, and batch files on Windows.
win_shell and win_command run commands on Windows hosts. win_command executes a process directly (no shell). win_shell runs through PowerShell (supports pipes, redirects, variables). Here's when to use each, with practical examples.
win_command vs win_shell
| Feature | win_command | win_shell |
|---------|------------|-----------|
| Shell | None (direct process) | PowerShell |
| Pipes | ❌ | ✅ |
| Redirects | ❌ | ✅ |
| Variables | ❌ | ✅ $env:PATH |
| Wildcards | ❌ | ✅ |
| Security | Safer (no injection) | Flexible but riskier |
| Use when | Simple commands | PowerShell features needed |
# win_command — direct execution, no shell
- ansible.windows.win_command: ipconfig /all
# win_shell — through PowerShell
- ansible.windows.win_shell: Get-Process | Where-Object { $_.CPU -gt 100 }
See also: Ansible for Windows: Complete Guide to Managing Windows Hosts
win_command Examples
Basic Commands
- name: Check Windows version
ansible.windows.win_command: systeminfo
register: sysinfo
- name: Get IP configuration
ansible.windows.win_command: ipconfig /all
register: ipconfig
- name: Run executable with arguments
ansible.windows.win_command: C:\tools\setup.exe /silent /norestart
- name: Check service status
ansible.windows.win_command: sc query W3SVC
register: iis_status
Idempotent with creates/removes
- name: Extract archive (skip if already done)
ansible.windows.win_command: 7z x C:\temp\app.zip -oC:\app
args:
creates: C:\app\bin\app.exe
- name: Run installer (skip if installed)
ansible.windows.win_command: C:\temp\installer.msi /quiet
args:
creates: C:\Program Files\MyApp\app.exe
- name: Remove temp files (only if they exist)
ansible.windows.win_command: del /q C:\temp\*.tmp
args:
removes: C:\temp\*.tmp
Working Directory
- name: Run command in specific directory
ansible.windows.win_command: npm install
args:
chdir: C:\projects\myapp
win_shell Examples
PowerShell One-Liners
- name: Get disk space
ansible.windows.win_shell: |
Get-PSDrive -PSProvider FileSystem |
Select-Object Name, @{N='Free(GB)';E={[math]::Round($_.Free/1GB,2)}},
@{N='Used(GB)';E={[math]::Round($_.Used/1GB,2)}}
register: disk_space
- name: Find large files
ansible.windows.win_shell: |
Get-ChildItem -Path C:\ -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 100MB } |
Sort-Object Length -Descending |
Select-Object -First 10 FullName, @{N='Size(MB)';E={[math]::Round($_.Length/1MB,2)}}
register: large_files
- name: Get installed software
ansible.windows.win_shell: |
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |
Select-Object DisplayName, DisplayVersion, Publisher |
Sort-Object DisplayName |
ConvertTo-Json
register: installed_software
PowerShell Scripts
- name: Configure Windows features
ansible.windows.win_shell: |
$features = @('Web-Server', 'Web-Asp-Net45', 'Web-Mgmt-Console')
foreach ($feature in $features) {
$installed = Get-WindowsFeature -Name $feature
if (-not $installed.Installed) {
Install-WindowsFeature -Name $feature -IncludeManagementTools
Write-Output "Installed: $feature"
} else {
Write-Output "Already installed: $feature"
}
}
- name: Manage Windows Firewall
ansible.windows.win_shell: |
# Allow inbound HTTP and HTTPS
$rules = @(
@{Name='Allow HTTP'; Port=80},
@{Name='Allow HTTPS'; Port=443}
)
foreach ($rule in $rules) {
$existing = Get-NetFirewallRule -DisplayName $rule.Name -ErrorAction SilentlyContinue
if (-not $existing) {
New-NetFirewallRule -DisplayName $rule.Name `
-Direction Inbound -Protocol TCP `
-LocalPort $rule.Port -Action Allow
}
}
Using PowerShell Variables
- name: Create user with PowerShell
ansible.windows.win_shell: |
$password = ConvertTo-SecureString "{{ vault_user_password }}" -AsPlainText -Force
New-LocalUser -Name "{{ username }}" -Password $password -FullName "{{ full_name }}" -Description "Service account"
Add-LocalGroupMember -Group "Administrators" -Member "{{ username }}"
- name: Configure environment variables
ansible.windows.win_shell: |
[System.Environment]::SetEnvironmentVariable("APP_HOME", "C:\app", "Machine")
[System.Environment]::SetEnvironmentVariable("APP_ENV", "{{ environment }}", "Machine")
Error Handling
- name: Run with error handling
ansible.windows.win_shell: |
try {
$result = Invoke-WebRequest -Uri "https://api.example.com/health" -TimeoutSec 10
if ($result.StatusCode -eq 200) {
Write-Output "Service healthy"
exit 0
}
} catch {
Write-Error "Health check failed: $_"
exit 1
}
register: health_check
failed_when: health_check.rc != 0
- name: Capture both stdout and stderr
ansible.windows.win_shell: |
Write-Output "Standard output"
Write-Error "Error output"
exit 0
register: result
- name: Show outputs
ansible.builtin.debug:
msg:
stdout: "{{ result.stdout }}"
stderr: "{{ result.stderr }}"
rc: "{{ result.rc }}"
See also: AAP 2.6 Windows Automation: WinRM, PowerShell, and Active Directory Management
Running Scripts from Files
win_shell with Script Files
- name: Copy and run PowerShell script
block:
- name: Copy script
ansible.windows.win_copy:
src: scripts/configure-iis.ps1
dest: C:\temp\configure-iis.ps1
- name: Execute script
ansible.windows.win_shell: C:\temp\configure-iis.ps1 -SiteName "{{ site_name }}" -Port {{ port }}
args:
chdir: C:\temp
- name: Clean up script
ansible.windows.win_file:
path: C:\temp\configure-iis.ps1
state: absent
win_script (Alternative)
# Copies and runs a script in one step
- name: Run local script on remote Windows
ansible.builtin.script: scripts/setup.ps1
args:
executable: powershell.exe
Output Encoding
# Force UTF-8 output
- name: Get content with UTF-8
ansible.windows.win_shell: |
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Get-Content -Path C:\data\report.txt -Encoding UTF8
# Parse JSON output
- name: Get structured data
ansible.windows.win_shell: |
Get-Service | Where-Object { $_.Status -eq 'Running' } |
Select-Object Name, DisplayName, Status |
ConvertTo-Json
register: services
- name: Use parsed data
ansible.builtin.debug:
msg: "Running services: {{ (services.stdout | from_json) | length }}"
See also: Ansible for Windows Server Enterprise Management: Active Directory, IIS, and Group Policy
Real-World Examples
IIS Website Configuration
---
- name: Configure IIS
hosts: windows_web
tasks:
- name: Install IIS features
ansible.windows.win_shell: |
Install-WindowsFeature -Name Web-Server, Web-Asp-Net45, Web-Mgmt-Console -IncludeManagementTools
- name: Create website
ansible.windows.win_shell: |
Import-Module WebAdministration
$siteName = "{{ site_name }}"
$sitePath = "C:\inetpub\{{ site_name }}"
if (-not (Test-Path $sitePath)) {
New-Item -Path $sitePath -ItemType Directory
}
$site = Get-Website -Name $siteName -ErrorAction SilentlyContinue
if (-not $site) {
New-Website -Name $siteName -PhysicalPath $sitePath -Port {{ site_port }}
Write-Output "Created website: $siteName"
} else {
Write-Output "Website already exists: $siteName"
}
- name: Configure app pool
ansible.windows.win_shell: |
Import-Module WebAdministration
Set-ItemProperty "IIS:\AppPools\{{ site_name }}" -Name processModel.identityType -Value 3
Set-ItemProperty "IIS:\AppPools\{{ site_name }}" -Name recycling.periodicRestart.time -Value "02:00:00"
Windows Service Management
- name: Install and configure Windows service
ansible.windows.win_shell: |
$serviceName = "{{ service_name }}"
$servicePath = "{{ service_path }}"
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if (-not $service) {
New-Service -Name $serviceName `
-BinaryPathName $servicePath `
-DisplayName "{{ service_display_name }}" `
-StartupType Automatic `
-Description "{{ service_description }}"
Start-Service -Name $serviceName
Write-Output "Service created and started"
} else {
Write-Output "Service already exists"
}
Registry Management
- name: Configure registry settings
ansible.windows.win_shell: |
# Disable Windows Update auto-restart
$path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
if (-not (Test-Path $path)) {
New-Item -Path $path -Force
}
Set-ItemProperty -Path $path -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 -Type DWord
# Set timezone
Set-TimeZone -Id "{{ timezone }}"
# Configure power settings
powercfg /change standby-timeout-ac 0
powercfg /change monitor-timeout-ac 30
Prefer Dedicated Modules When Available
# ❌ Using win_shell when a module exists
- ansible.windows.win_shell: Install-WindowsFeature -Name Web-Server
# ✅ Use the dedicated module (idempotent, structured output)
- ansible.windows.win_feature:
name: Web-Server
state: present
# ❌ win_shell for service management
- ansible.windows.win_shell: Start-Service W3SVC
# ✅ Dedicated module
- ansible.windows.win_service:
name: W3SVC
state: started
# ❌ win_shell for file operations
- ansible.windows.win_shell: Copy-Item C:\source\* C:\dest\
# ✅ Dedicated module
- ansible.windows.win_copy:
src: C:\source\
dest: C:\dest\
remote_src: true
Use win_shell/win_command only when no dedicated module exists for the task.
FAQ
When should I use win_shell vs win_command?
Use win_command for simple executables that don't need shell features (pipes, variables, redirects). Use win_shell when you need PowerShell functionality. win_command is safer against injection since it doesn't interpret shell metacharacters.
How do I handle special characters in win_shell?
Ansible uses Jinja2 templating, which conflicts with PowerShell's $ and {{ }}. Use {% raw %}...{% endraw %} for literal PowerShell variables, or pass values through Ansible variables:
- ansible.windows.win_shell: |
$name = '{{ username }}' # Ansible variable
{% raw %}
$items = @(1,2,3) # PowerShell variable
foreach ($item in $items) { Write-Output $item }
{% endraw %}
Why does my win_shell task always show "changed"?
win_shell and win_command always report changed because Ansible can't know if the command modified anything. Use changed_when to control this:
- ansible.windows.win_shell: Get-Service W3SVC
register: result
changed_when: false # Read-only command
How do I run CMD (not PowerShell) commands?
- ansible.windows.win_shell: cmd.exe /c dir C:\Windows
# Or use win_command which doesn't use PowerShell
- ansible.windows.win_command: cmd.exe /c dir C:\Windows
Can I run elevated (admin) PowerShell?
Ansible runs as the connected user. Use become: true with become_method: runas for privilege escalation:
- ansible.windows.win_shell: Install-WindowsFeature Web-Server
become: true
become_method: runas
become_user: Administrator
Conclusion
Use win_command for simple process execution and win_shell when you need PowerShell features. Always prefer dedicated Ansible modules over shell commands when available — they're idempotent and return structured data. For complex PowerShell logic, use win_shell with proper error handling, changed_when, and creates/removes for idempotency.
Related Articles
• Ansible for Windows Automation • Ansible Connection Types • Ansible shell vs command vs raw • Ansible Error Handling GuideCategory: installation