Advanced CLI Patterns
1. Overview
This guide covers advanced bash patterns for working with netapi, including heredocs, multi-line commands, configuration file generation, and handling complex automation scenarios.
|
When to use these patterns:
|
2. Heredocs - The Power Pattern
Heredocs (Here Documents) allow you to create multi-line content inline in your shell scripts. They’re incredibly powerful for generating configuration files, ACLs, and multi-command sequences.
2.1. Basic Heredoc Syntax
cat > /path/to/file << 'EOF'
line 1
line 2
line 3
EOF
|
Use single quotes around EOF (
|
2.2. Creating ISE dACL Files
The most common use case: creating downloadable ACL files for Cisco ISE.
# Create a zero-trust dACL file using heredoc
cat > /tmp/LINUX_RESEARCH_ZERO_TRUST.txt << 'EOF'
permit icmp any any
permit udp any host 10.50.1.1 eq 53
permit udp any host 10.50.1.50 eq 53
permit udp any eq 53 any gt 1023
permit udp any host 10.50.1.1 eq 123
permit udp any eq 123 any gt 1023
permit tcp any host 10.50.1.50 eq 88
permit udp any host 10.50.1.50 eq 88
permit tcp any host 10.50.1.50 eq 389
permit tcp any host 10.50.1.50 eq 636
permit tcp any host 10.50.1.50 eq 445
permit tcp any host 10.50.1.21 eq 8443
permit tcp any host 10.50.1.21 eq 8905
permit tcp any eq 22 10.50.0.0 0.0.255.255 gt 1023
permit tcp any any eq 80
permit tcp any eq 80 any gt 1023
permit tcp any any eq 443
permit tcp any eq 443 any gt 1023
deny ip any 10.0.0.0 0.255.255.255
deny ip any 172.16.0.0 0.15.255.255
deny ip any 192.168.0.0 0.0.255.255
EOF
# Verify the file
cat /tmp/LINUX_RESEARCH_ZERO_TRUST.txt
# Upload to ISE
uv run netapi ise create-dacl LINUX_RESEARCH_ZERO_TRUST \
--file /tmp/LINUX_RESEARCH_ZERO_TRUST.txt \
--descr "Zero-trust ACL for domain-joined Linux workstations"
|
Why this is better than inline ✅ Easier to read and maintain (ACL is formatted like Cisco config) ✅ Can validate syntax before uploading ✅ Version control friendly (commit the ACL file) ✅ Reusable across multiple profiles ✅ No semicolon/escaping issues |
2.3. Multi-Step Switch Configuration
For switch configuration that requires multiple commands, use heredoc to create a reusable script.
# Create a CoA configuration script
cat >| /tmp/configure-coa.sh << 'EOF'
#!/bin/bash
# Configure RADIUS dynamic-author for ISE CoA
# Note: Use --timeout 5 and || true to handle netmiko prompt timeouts
uv run netapi ios run --timeout 5 "configure terminal" || true
uv run netapi ios run --timeout 5 "aaa server radius dynamic-author" || true
uv run netapi ios run --timeout 5 "client 10.50.1.20 server-key Cisco123\!ISE" || true
uv run netapi ios run --timeout 5 "client 10.50.1.21 server-key Cisco123\!ISE" || true
uv run netapi ios run --timeout 5 "exit" || true
uv run netapi ios run --timeout 5 "end" || true
uv run netapi ios run --timeout 5 "write memory" || true
EOF
# Make executable
chmod +x /tmp/configure-coa.sh
# Run the script
/tmp/configure-coa.sh
# Verify configuration applied
uv run netapi ios exec "show run | section dynamic-author"
|
Why Netmiko may fail to detect the switch prompt after configuration commands, causing |
2.4. Creating Batch Files
Generate batch files for repetitive operations.
# Create a batch file to add multiple AD groups to ISE
cat > /tmp/add-ad-groups.sh << 'EOF'
#!/bin/bash
# Add all clinical department AD groups to ISE
GROUPS=(
"GRP-Clinical-Physicians"
"GRP-Clinical-Nurses"
"GRP-Clinical-Residents"
"GRP-IT-Network-Engineers"
"GRP-IT-Security-Team"
)
for group in "${GROUPS[@]}"; do
echo "Adding group: $group"
uv run netapi ise add-ad-groups "inside.domusdigitalis.dev" "$group" \
--dict "INSIDE-AD" || true
sleep 2 # Rate limit API calls
done
echo "Done! Groups added to ISE."
EOF
chmod +x /tmp/add-ad-groups.sh
/tmp/add-ad-groups.sh
2.5. Templated Configuration with Variables
Use heredoc WITH variable expansion (no quotes around EOF) for templated configs.
# Set variables
SWITCH_IP="10.50.1.10"
ISE_PSN1="10.50.1.20"
ISE_PSN2="10.50.1.21"
RADIUS_KEY="Cisco123!ISE"
# Create config script with variable substitution
cat > /tmp/configure-switch-${SWITCH_IP}.sh << EOF
#!/bin/bash
# Auto-generated switch configuration for ${SWITCH_IP}
echo "Configuring switch at ${SWITCH_IP}"
uv run netapi ios run --timeout 5 "configure terminal" || true
uv run netapi ios run --timeout 5 "aaa server radius dynamic-author" || true
uv run netapi ios run --timeout 5 "client ${ISE_PSN1} server-key ${RADIUS_KEY}" || true
uv run netapi ios run --timeout 5 "client ${ISE_PSN2} server-key ${RADIUS_KEY}" || true
uv run netapi ios run --timeout 5 "end" || true
uv run netapi ios run --timeout 5 "write memory" || true
echo "Configuration complete for ${SWITCH_IP}"
EOF
chmod +x /tmp/configure-switch-${SWITCH_IP}.sh
/tmp/configure-switch-${SWITCH_IP}.sh
|
Notice the difference:
Choose based on your needs! |
3. Advanced Pipeline Patterns
3.1. Process Line-by-Line with While Read
Execute a command for each line in a file or heredoc.
# Method 1: From heredoc
cat << 'EOF' | while read cmd; do
[ -z "$cmd" ] || uv run netapi ios run "$cmd"
done
configure terminal
interface GigabitEthernet1/0/5
description Connected to modestus-p50
no shutdown
end
write memory
EOF
# Method 2: From file
while read switch; do
echo "Checking switch: $switch"
uv run netapi ios exec "show version" --host "$switch"
done < /tmp/switch-list.txt
3.2. Parallel Execution for Speed
Run multiple independent commands simultaneously.
# Create a function to run commands in parallel
run_checks() {
local switch=$1
echo "=== Checking $switch ==="
uv run netapi ios exec "show version" --host "$switch" &
}
# Export function for subshells
export -f run_checks
# Run checks in parallel for all switches
cat << 'EOF' | xargs -I {} -P 5 bash -c 'run_checks "$@"' _ {}
10.50.1.10
10.50.1.11
10.50.1.12
10.50.1.13
10.50.1.14
EOF
# Wait for all background jobs to complete
wait
echo "All checks complete!"
3.3. Error Handling and Logging
Add robust error handling to your scripts.
cat > /tmp/robust-config.sh << 'EOF'
#!/bin/bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a /tmp/netapi-config.log
}
# Error handler
error_exit() {
log "ERROR: $1"
exit 1
}
# Configuration commands
log "Starting switch configuration"
uv run netapi ios run "configure terminal" || error_exit "Failed to enter config mode"
uv run netapi ios run "aaa server radius dynamic-author" || error_exit "Failed to configure dynamic-author"
uv run netapi ios run "client 10.50.1.21 server-key Cisco123\!ISE" || error_exit "Failed to add client"
uv run netapi ios run "end" || true
uv run netapi ios run "write memory" || error_exit "Failed to save config"
log "Configuration completed successfully"
EOF
chmod +x /tmp/robust-config.sh
/tmp/robust-config.sh
4. Practical Examples
4.1. Complete Workflow: Create and Deploy dACL
#!/bin/bash
# Complete workflow: Create, upload, and apply a dACL
DACL_NAME="LINUX_MEDICAL_WORKSTATION"
PROFILE_NAME="Medical_Linux_EAP_TLS"
# Step 1: Create dACL file
log "Creating dACL file..."
cat > "/tmp/${DACL_NAME}.txt" << 'EOF'
permit icmp any any
permit udp any host 10.50.1.50 eq 53
permit tcp any host 10.50.1.50 eq 88
permit tcp any host 10.50.1.50 eq 389
permit tcp any host 10.100.10.10 eq 443
permit tcp any host 10.100.10.11 eq 1433
deny ip any 10.0.0.0 0.255.255.255
EOF
# Step 2: Upload to ISE
log "Uploading dACL to ISE..."
uv run netapi ise create-dacl "$DACL_NAME" \
--file "/tmp/${DACL_NAME}.txt" \
--descr "Medical workstation zero-trust ACL"
# Step 3: Apply to authorization profile
log "Applying dACL to profile..."
uv run netapi ise update-authz-profile "$PROFILE_NAME" \
--dacl "$DACL_NAME"
# Step 4: Verify
log "Verifying dACL..."
uv run netapi ise get-dacl "$DACL_NAME"
log "Deployment complete!"
4.2. Batch Endpoint Provisioning
# Provision multiple endpoints at once
cat > /tmp/provision-endpoints.sh << 'EOF'
#!/bin/bash
# Endpoint data
cat << 'DATA' | while IFS=',' read mac desc group; do
echo "Provisioning: $mac ($desc)"
uv run netapi ise create-endpoint "$mac" \
--description "$desc" \
--group "$group" || true
sleep 1 # Rate limiting
done
AA:BB:CC:DD:EE:01,Medical Cart 1,Medical-Devices
AA:BB:CC:DD:EE:02,Medical Cart 2,Medical-Devices
AA:BB:CC:DD:EE:03,Nurse Station PC,Clinical-Workstations
AA:BB:CC:DD:EE:04,Physician Laptop,Clinical-Laptops
DATA
echo "Endpoint provisioning complete!"
EOF
chmod +x /tmp/provision-endpoints.sh
/tmp/provision-endpoints.sh
5. Best Practices
| Practice | Reason |
|---|---|
Use |
Prevents unintended variable expansion |
Add |
Exit on errors, catch issues early |
Include logging |
Debug issues, track execution history |
Add |
Handle prompt timeout errors gracefully |
Use |
Don’t wait forever for prompts |
Validate before applying |
|
Save scripts to |
Easy cleanup, no repo clutter |
Make scripts executable |
|
6. Common Pitfalls
|
Shell metacharacter issues:
Example:
|
|
Test heredocs before running:
|
7. Git Commit Messages with Heredoc
One of the most elegant uses of heredoc: multi-line git commit messages without escaping nightmares!
7.1. The Traditional (Painful) Way
# Using -m multiple times (awkward)
git commit -m "[feat] Add new feature" \
-m "" \
-m "Details:" \
-m "- Added X" \
-m "- Fixed Y"
# Or the interactive editor (breaks flow)
git commit # Opens editor, context switch
7.2. The Heredoc Way (Beautiful!)
git add . && git commit -m "$(cat <<'EOF'
[feat] Add comprehensive backup arsenal documentation
Added 6-tier backup strategy documentation:
Architecture:
- Tier 1: Live data (NVMe, working storage)
- Tier 2: Local backups (Synology NAS, Time Machine)
- Tier 3: Onsite redundancy (RAID, snapshots)
- Tier 4: Nearline cloud (Backblaze B2)
- Tier 5: Offline archive (encrypted external drives)
- Tier 6: Offsite cold storage (safe deposit box)
Documentation includes:
- 3-2-1-1-0 rule compliance
- NIST/ISO framework mapping
- Cost analysis (~$100/year operational)
- Recovery procedures for each tier
- Automation scripts and monitoring
This exceeds industry standards and provides defense-in-depth
for critical personal/business data.
EOF
)"
|
Why this is amazing: ✅ Write commit message in natural format (like writing docs) ✅ No escaping quotes or special characters ✅ Markdown formatting works perfectly ✅ Easy to add bullet points and sections ✅ Stay in the terminal, no editor context switch ✅ Perfect for detailed commit messages |
7.3. Real-World Examples
Feature commit:
git commit -m "$(cat <<'EOF'
[feat] Implement CoA error handling improvements
ISE MnT Client changes:
- Better error messages for CoA failures
- Added explicit 404 handling for session not found
- Include UDP 1700 connectivity hint in errors
- Fixed attribute naming (code vs status_code)
CLI improvements:
- Formatted NAD output as Rich table
- Show CoA port, RADIUS secret status
- Parse device groups hierarchically
Testing:
- Verified CoA error messages are actionable
- Confirmed NAD output is human-readable
EOF
)"
Bug fix commit:
git commit -m "$(cat <<'EOF'
[fix] Resolve ACL ordering causing DNS failures
Root cause:
- RFC1918 deny statements were BEFORE DNS permits
- ACL processing is top-to-bottom, first match wins
- DNS to 10.50.1.1 was denied by line 1
Solution:
- Moved specific permits (DNS, ISE, NTP) to top
- RFC1918 denies moved to bottom
- Added return traffic permits
Testing:
- Verified DNS resolution works
- Confirmed ISE communication functional
- No lateral movement possible (RFC1918 blocked)
Closes: #42
EOF
)"
Documentation commit:
git commit -m "$(cat <<'EOF'
[docs] Add CoA configuration guide with heredoc patterns
New sections:
- CoA prerequisites and validation
- Heredoc method for multi-line switch config
- Error handling with || true pattern
- Troubleshooting common CoA issues
Key learnings documented:
- Netmiko timeout errors are harmless if "write memory" succeeds
- Use --timeout 5 to fail fast
- Commands execute despite ReadTimeout
- Proper submode exit is critical
Related: PRJ-ISE-HOME-LINUX hardened-dacl.adoc
EOF
)"
7.4. Pro Tips for Git Heredoc Commits
# 1. Always use single quotes around EOF (no variable expansion)
git commit -m "$(cat <<'EOF'
...
EOF
)"
# 2. Keep to semantic commit format
[type] Brief summary (50 chars max)
Detailed explanation (wrap at 72 chars):
- Bullet points for changes
- Reference issues/tickets
- Link to related docs
# Common types: feat, fix, docs, refactor, test, chore
# 3. Use && to chain git add and commit
git add file1.txt file2.txt && git commit -m "$(cat <<'EOF'
[feat] Add amazing feature
...
EOF
)"
# 4. Create a commit template function
commit() {
cat <<'TEMPLATE'
[type] Brief description
Detailed explanation:
- Change 1
- Change 2
Testing:
- Test 1
- Test 2
TEMPLATE
}
# Then edit and commit
git add . && git commit -m "$(commit)"
|
Common mistakes: ❌ Forgetting quotes around EOF (variables expand unexpectedly)
✅ Use single quotes
|
7.5. Advanced: Interactive Commit Message Builder
#!/bin/bash
# Save as ~/.local/bin/gcommit
TYPE=$(gum choose "feat" "fix" "docs" "refactor" "test" "chore")
SUMMARY=$(gum input --placeholder "Brief summary (50 chars max)")
# Use heredoc to build commit message interactively
git commit -m "$(cat <<EOF
[$TYPE] $SUMMARY
$(gum write --placeholder "Detailed explanation...")
Testing:
$(gum write --placeholder "How you tested this...")
EOF
)"
|
Why heredoc beats
|
8. Multi-Remote Git Operations
When you have multiple git remotes (GitHub, GitLab, Gitea, Azure DevOps, etc.), pushing to all of them manually is tedious. Heredocs make this elegant.
8.1. The Problem
# Manual pushing to each remote (tedious!)
git push origin main
git push gitlab main
git push gitea main
git push azure main
git push codeberg main
8.2. Solution 1: Quick While Loop
# Push to all remotes in one command
git remote | while read remote; do
echo "=== Pushing to $remote ===" && git push "$remote" main
done
|
Why this works:
|
8.3. Solution 2: Reusable Heredoc Script
# Create a permanent script for pushing to all remotes
cat > ~/.local/bin/git-push-all << 'EOF'
#!/bin/bash
# Push current or specified branch to all git remotes
BRANCH="${1:-$(git branch --show-current)}"
echo "Pushing branch '$BRANCH' to all remotes..."
echo ""
git remote | while read remote; do
echo "=== Pushing to $remote ==="
if git push "$remote" "$BRANCH"; then
echo "✓ Successfully pushed to $remote"
else
echo "✗ Failed to push to $remote"
echo " (This is normal if remote doesn't have SSH keys configured)"
fi
echo ""
done
echo "All pushes complete!"
EOF
chmod +x ~/.local/bin/git-push-all
# Usage:
git-push-all # Push current branch to all remotes
git-push-all develop # Push 'develop' branch to all remotes
git-push-all --dry-run # Show what would be pushed (add logic for this)
|
Error handling: The script uses
|
8.4. Solution 3: Git Alias (No External Script)
# Add to ~/.gitconfig using heredoc
cat >> ~/.gitconfig << 'EOF'
[alias]
# Push to all remotes
push-all = "!f() { \
BRANCH=\"${1:-$(git branch --show-current)}\"; \
git remote | while read remote; do \
echo \"=== Pushing to $remote ===\";\
git push \"$remote\" \"$BRANCH\" || echo \"✗ Failed: $remote\";\
echo \"\";\
done; \
}; f"
# Show all remotes with their URLs
remotes = remote -v
EOF
# Usage:
git push-all # Push current branch
git push-all main # Push main branch
git remotes # Show all configured remotes
8.5. Real-World Example: Principia Multi-Remote Setup
# Check current remotes
$ git remote -v
azure git@ssh.dev.azure.com:v3/EvanusModestus/Principia/Principia (fetch)
azure git@ssh.dev.azure.com:v3/EvanusModestus/Principia/Principia (push)
codeberg git@codeberg.org:evanusmodestus/Principia.git (fetch)
codeberg git@codeberg.org:evanusmodestus/Principia.git (push)
gitea git@gitea:evanusmodestus/Principia.git (fetch)
gitea git@gitea:evanusmodestus/Principia.git (push)
gitlab git@gitlab.com:EvanusModestus/Principia.git (fetch)
gitlab git@gitlab.com:EvanusModestus/Principia.git (push)
origin git@github.com:EvanusModestus/Principia.git (fetch)
origin git@github.com:EvanusModestus/Principia.git (push)
# Push to all with one command
$ git remote | while read remote; do
echo "=== Pushing to $remote ==="
git push "$remote" main
done
=== Pushing to azure ===
Everything up-to-date
=== Pushing to codeberg ===
Everything up-to-date
=== Pushing to gitea ===
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To gitea:evanusmodestus/Principia.git
12d40d24..88f0cc64 main -> main
=== Pushing to gitlab ===
Everything up-to-date
=== Pushing to origin ===
Everything up-to-date
|
Why multiple remotes? ✅ Redundancy - If GitHub goes down, you have backups ✅ Compliance - Keep data in specific jurisdictions ✅ Speed - Use closest mirror for cloning ✅ Privacy - Self-hosted Gitea for sensitive repos ✅ CI/CD - Different platforms for different pipelines |
8.6. Advanced: Selective Multi-Push
Only push to specific remotes (exclude certain ones):
# Push to all remotes EXCEPT origin and azure
git remote | grep -v -E '^(origin|azure)$' | while read remote; do
echo "=== Pushing to $remote ==="
git push "$remote" main
done
# Or push only to self-hosted remotes
git remote | grep -E '^(gitea|gitlab)$' | while read remote; do
echo "=== Pushing to $remote ==="
git push "$remote" main
done
8.7. Automation: Post-Commit Hook
Automatically push to all remotes after every commit:
# Create post-commit hook using heredoc
cat > .git/hooks/post-commit << 'EOF'
#!/bin/bash
# Auto-push to all remotes after commit
echo ""
echo "Auto-pushing to all remotes..."
echo ""
git remote | while read remote; do
echo "=== Pushing to $remote ==="
git push "$remote" "$(git branch --show-current)" 2>&1 | head -3
done
echo ""
echo "All remotes updated!"
EOF
chmod +x .git/hooks/post-commit
|
Use auto-push hooks carefully:
|
8.8. Combining with Heredoc Commits
The ultimate workflow - commit with heredoc message AND push to all remotes:
# Commit and push in one go
git add . && \
git commit -m "$(cat <<'EOF'
[feat] Add multi-remote push patterns
Added comprehensive multi-remote git documentation:
- While loop pattern for quick pushes
- Reusable script with error handling
- Git alias for no external dependencies
- Selective push patterns
- Post-commit hook automation
This solves the problem of manually pushing to 5+ remotes
(GitHub, GitLab, Gitea, Azure DevOps, Codeberg).
EOF
)" && \
git remote | while read remote; do
echo "=== Pushing to $remote ==="
git push "$remote" main
done
|
Save this as a function in ~/.zshrc or ~/.bashrc:
|