How to Write Automation Scripts in Python, Bash, and PowerShell
Choose Python, Bash, or PowerShell for your automation task, then build a script with safe inputs, useful logs, and clear failure handling.
Choose the scripting environment that fits the machine and task: Python is a practical default for structured data and richer program logic; Bash fits command composition on Unix-like systems; PowerShell fits Windows and Microsoft administration workflows. These are task-based heuristics, not performance rankings. Whichever you choose, make inputs explicit, quote or pass arguments safely, log what the script does, and decide how failures should behave before scheduling it.
1. Choose the environment from the task
Before writing code, list the operating system, files and services the automation touches, external commands it needs, expected inputs, and the action to take when a step fails. Also check what runtimes and modules the eventual execution environment provides.
| Environment | Good fit | Check before choosing |
|---|---|---|
| Python | Structured data, validation, branching logic, and filesystem work using standard-library tools. | Python runtime availability and required packages on the machine or scheduler. |
| Bash | Composing Unix-like command-line tools and handling shell-oriented workflows. | That the target has Bash and compatible utilities; shells and utility versions can differ. |
| PowerShell | Windows workflows, Microsoft administration, and tasks using cmdlets or native commands. | PowerShell version, modules, execution context, and native-command argument and error behavior. |
PowerShell is both a command-line shell and scripting language; it can run PowerShell commands such as cmdlets as well as native operating-system commands. Its parsing, output streams, and native-process error behavior differ from Bash, so do not assume a pasted Bash command behaves the same way. [Microsoft’s shell documentation](https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/running-commands?view=powershell-7.6) describes these distinctions.
2. Start with one small, repeatable task
The equivalent examples below create a dated backup copy of a supplied file. They avoid overwriting an existing backup and report errors. Run them with a file path as the first argument. The Python example uses a UTC timestamp; Bash and PowerShell use their local system clock. Adapt the timestamp policy if backups must follow a particular timezone or naming convention.
Python
#!/usr/bin/env python3
"""Copy one file to a timestamped backup without overwriting an existing copy."""
import logging
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
def main() -> int:
if len(sys.argv) != 2:
logging.error("Usage: python backup.py PATH")
return 2
source = Path(sys.argv[1]).expanduser()
if not source.is_file():
logging.error("Not a regular file: %s", source)
return 1
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
destination = source.with_name(f"{source.name}.{stamp}.bak")
if destination.exists():
logging.error("Destination already exists: %s", destination)
return 1
try:
shutil.copy2(source, destination)
except OSError:
logging.exception("Could not copy %s", source)
return 1
logging.info("Created %s", destination)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Save as backup.py and run python backup.py "/path/to/my file.txt" (use python3 where that is the installed command). Python’s standard library includes path and file operations such as pathlib, shutil, glob, and os.walk; use these for filesystem work instead of invoking a shell unnecessarily.
Bash
#!/usr/bin/env bash
set -u
set -o pipefail
if (( $# != 1 )); then
printf 'Usage: %s PATH\n' "$0" >&2
exit 2
fi
source=$1
if [[ ! -f "$source" ]]; then
printf 'Not a regular file: %s\n' "$source" >&2
exit 1
fi
stamp=$(date '+%Y%m%dT%H%M%S') || {
printf 'Could not get the current time\n' >&2
exit 1
}
destination="${source}.${stamp}.bak"
if [[ -e "$destination" ]]; then
printf 'Destination already exists: %s\n' "$destination" >&2
exit 1
fi
if cp -p -- "$source" "$destination"; then
printf 'Created %s\n' "$destination"
else
status=$?
printf 'Copy failed with status %s: %s\n' "$status" "$source" >&2
exit "$status"
fi
Save as backup.sh, then run bash backup.sh "/path/to/my file.txt". The -- tells common Unix cp implementations to treat following values as operands, even if a filename begins with a hyphen; check portability if targeting minimal or non-GNU systems. Quoting "$source" preserves spaces and prevents shell metacharacters in the filename from being interpreted as syntax.
This deliberately does not use set -e as a substitute for error handling. Bash’s manual documents contexts where errexit does not exit on a nonzero status. pipefail makes a pipeline return a failure if a component fails, but this example has no pipeline. The script checks the copy operation directly and returns its status.
PowerShell
param(
[Parameter(Mandatory = $true, Position = 0)]
[string] $Path
)
$ErrorActionPreference = 'Stop'
try {
$source = Get-Item -LiteralPath $Path
if ($source.PSIsContainer) {
throw "Not a regular file: $Path"
}
$stamp = Get-Date -Format 'yyyyMMddTHHmmss'
$destination = "$($source.FullName).$stamp.bak"
if (Test-Path -LiteralPath $destination) {
throw "Destination already exists: $destination"
}
Copy-Item -LiteralPath $source.FullName -Destination $destination -ErrorAction Stop
Write-Output "Created $destination"
exit 0
}
catch {
[Console]::Error.WriteLine("Backup failed: {0}", $_.Exception.Message)
exit 1
}
Save as backup.ps1 and run it from PowerShell with ./backup.ps1 -Path 'C:\Data\my file.txt' or a suitable Unix-style path on a platform where PowerShell is installed. -LiteralPath prevents wildcard characters in a path from being treated as patterns. PowerShell’s native command behavior and error handling depend in part on version; review the shell documentation for the version you deploy.
3. Turn the example into a dependable automation
- Define inputs. Specify required and optional arguments, defaults, allowed values, and whether paths are relative to a known directory. Validate before changing files or services.
- Make reruns safe. Decide whether to skip, replace, or version existing outputs. Use temporary files and an atomic rename where partial writes would be harmful. Consider concurrent runs and use a lock or unique output names if needed.
- Make side effects visible. Log the target and result, but avoid printing passwords, tokens, or sensitive file contents. Send diagnostics to stderr or an appropriate logging stream and keep machine-readable output separate if another program consumes it.
- Handle expected failures explicitly. Identify which operations can fail, what should be retried, and when to stop. Use bounded retries with a delay for transient service failures; do not retry validation errors or non-idempotent actions blindly.
- Return useful status. Use zero for success and nonzero for failure, and document meanings if callers need to distinguish failure types. Preserve the failing command’s status when wrapping it.
- Test with safe inputs. Try a valid input, a missing input, a path containing spaces, a permission failure, and a repeated run. Use a disposable directory or test environment before automating destructive operations.
- Keep secrets out of source code. Read them from an appropriate secret store or protected environment and limit access to the account that runs the job.
4. Pass external command arguments safely
When Python needs an external process, pass an argument list to subprocess.run and check its result. Python generally recommends a sequence because it can handle required quoting and escaping. shell=True is an explicit request for shell parsing and has security implications; use it only when shell syntax is genuinely required and inputs are carefully controlled.
import subprocess
subprocess.run(["git", "-C", "/srv/project", "status", "--short"], check=True, text=True)
In Bash, quote expansions and use arrays when building a command from variable arguments:
args=(--format=json "$input_path")
my_tool "${args[@]}"
In PowerShell, use native commands with explicit arguments and inspect the process exit status when it matters. PowerShell also has its own output streams, beyond the standard output and error streams familiar from Bash. Use Start-Process when you need process control such as a different working directory, credentials, or redirected streams; Microsoft’s guidance recommends it for that kind of control. Do not assume shell quoting rules transfer between PowerShell, Bash, and Windows native programs.
5. Schedule scripts with the right execution context
Scheduling is separate from script logic. A scheduled run can use a different account, permissions, environment variables, working directory, network access, and runtime than an interactive terminal. Configure each deliberately. Use absolute paths when practical, set the working directory explicitly, make logs accessible to the scheduler’s account, and verify that required modules and commands are installed in that context.
Runtime support belongs to the service that executes the script, not to a language recommendation in the abstract. For example, Microsoft’s Azure Automation documentation lists supported runbook runtimes for that service and says it follows the PowerShell and Python support lifecycles. Check its current runtime matrix, or the equivalent documentation for your scheduler, before deployment. Those Azure-specific versions do not describe every computer or scheduler.
6. Performance, reliability, and cost
- Performance: Choose based on task fit and measure the actual workload if speed matters. The cited language references do not establish a general head-to-head performance winner. Avoid starting a subprocess for work the language’s standard library can do directly.
- Reliability: The main risks are usually incomplete failure handling, incorrect assumptions about paths or permissions, unsafe retries, and differences between interactive and scheduled environments. Validate outputs and make operations safe to rerun where possible.
- Cost: A local script has no language license fee, but execution services, compute, storage, network calls, and maintenance can have costs. Estimate from the actual scheduler and services used; there is no universal cost figure.
7. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| “Command not found” or executable missing | The runtime or external tool is absent, or the scheduled environment has a different PATH. | Check the executable under the same account and context as the job; configure an explicit path if appropriate. |
| A path with spaces or special characters fails | An argument was split or parsed as syntax. | Quote variable expansions in Bash; use argument sequences with Python subprocess; use PowerShell’s literal-path parameters where available. |
| Script works interactively but fails when scheduled | Different account, working directory, environment, permissions, network, or runtime. | Log the effective directory and non-sensitive environment details; use absolute paths and configure the scheduler’s execution identity. |
| Bash continues after a failed operation | set -e has documented exceptions, or the failing status was masked by later commands. |
Check critical commands explicitly and preserve their exit status. Use pipefail when pipeline components must affect the pipeline result. |
Python raises CalledProcessError |
check=True correctly reported a nonzero child-process exit. |
Inspect the command, its arguments, and captured diagnostics; handle the exception only if the failure is expected. |
| PowerShell reports an unexpected native-command result | Argument parsing or native exit handling differs by PowerShell version. | Check the installed version and process exit code, and follow Microsoft’s guidance for that version. |
| Second run reports destination exists | The example intentionally avoids overwriting a backup. | Choose a retention/versioning policy or remove the old file only after verifying the intended destination. |
| Permission denied | The execution account cannot read the source or write to the destination. | Grant the minimum required permissions to the scheduler’s account and verify access in that context. |
8. Capture website screenshots from an automation script
If the task is to capture website screenshots, you can use a browser automation library and manage browser installation, page readiness, and output handling yourself. For an HTTP API alternative, ScreenshotNeo takes a URL in one GET request and returns an image or PDF. See the ScreenshotNeo API documentation for parameters.
Python browser automation outline
A browser-based approach needs a browser automation package and a compatible browser installation. The following Playwright example opens a page and saves a full-page PNG; install and configure Playwright and its browser for your platform before running it.
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
async def main():
url = "https://example.com"
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
response = await page.goto(url, wait_until="networkidle", timeout=60000)
if response is None or not response.ok:
raise RuntimeError(f"Page load failed: {response.status if response else 'no response'}")
await page.screenshot(path="page.png", full_page=True)
await browser.close()
asyncio.run(main())
Choose a readiness condition that matches the site. Network idle can wait indefinitely on pages with persistent connections; a specific selector or bounded delay may be more suitable. Close the browser in a finally block in long-running production code so failures do not leak processes. Protect credentials and avoid capturing private content into broadly accessible storage.
Or skip the browser setup
ScreenshotNeo accepts a URL and returns a screenshot in one request. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month with no card.
9. FAQ
Can one project use more than one scripting language?
Yes. Keep each script’s inputs, outputs, dependencies, and exit behavior documented. Use a clear boundary between scripts so one language does not need to parse another’s informal terminal output.
Should I rewrite a working shell script in Python?
Only if the change solves a real maintenance, portability, data-handling, or error-management problem. A rewrite also introduces migration risk and a runtime dependency.
Where should I learn the exact shell rules?
Use the official [Bash Reference Manual](https://www.gnu.org/s/bash/manual/bash.html), [Python subprocess documentation](https://docs.python.org/3/library/subprocess.html), and [Microsoft’s PowerShell shell documentation](https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/running-commands?view=powershell-7.6). Check the documentation matching the runtime version you deploy.


