Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 35 additions & 27 deletions cli/arcgispro_cli/paths.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions for finding and validating .arcgispro folders."""

import json
import re
from pathlib import Path
from typing import Optional, Dict, Any, List

Expand Down Expand Up @@ -51,36 +52,43 @@ def get_snapshot_folder(arcgispro_path: Path) -> Path:


def sanitize_map_name(name: str) -> str:
"""
Sanitize a map name for use as a filename.

Replicates the C# SanitizeFileName logic from ImageExporter.cs.
Replaces invalid filename characters with underscores, truncates to 50 chars.

Args:
name: The map or layer name to sanitize.

Returns:
Sanitized filename without extension, or "unnamed" if empty.
"""Sanitize a map/layer name for use as a filename.

Intended to match the ProExporter add-in's filename sanitization.

Rules:
- Replace Windows-invalid filename characters with underscores
- Replace ASCII control chars (0-31) with underscores
- Collapse whitespace to single underscores
- Truncate to 50 characters

Returns "unnamed" if the input is empty/whitespace, or if sanitization
results in an empty name.
"""
if not name or not name.strip():
return "unnamed"

# Invalid filename characters: / \ : * ? " < > |
invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']

sanitized = name
for char in invalid_chars:
sanitized = sanitized.replace(char, '_')

# Replace spaces with underscores
sanitized = sanitized.replace(' ', '_')

# Truncate to 50 characters
if len(sanitized) > 50:
sanitized = sanitized[:50]

return sanitized

# Windows-invalid filename characters: <>:"/\\|?*
# Plus ASCII control chars (0-31) which Windows also disallows.
invalid_chars = set('<>:"/\\|?*')

def repl(match: re.Match) -> str:
ch = match.group(0)
if ch in invalid_chars or ord(ch) < 32:
return "_"
# Any whitespace becomes underscore
return "_"

# Replace invalid chars and whitespace with underscores.
sanitized = re.sub(r"[\s<>:\"/\\|\?\*\x00-\x1f]", repl, name)

# Collapse repeated underscores and trim.
sanitized = re.sub(r"_+", "_", sanitized).strip("_")

if not sanitized:
return "unnamed"

return sanitized[:50]


def load_json_file(path: Path) -> Optional[Dict[str, Any]]:
Expand Down
35 changes: 35 additions & 0 deletions cli/tests/test_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest

from arcgispro_cli.paths import sanitize_map_name


@pytest.mark.parametrize(
"raw,expected",
[
("", "unnamed"),
(" ", "unnamed"),
("Parcels", "Parcels"),
("A/B\\C:D*E?F\"G<H>I|J", "A_B_C_D_E_F_G_H_I_J"),
("hello world\tagain", "hello_world_again"),
],
)
def test_sanitize_map_name_basic(raw, expected):
assert sanitize_map_name(raw) == expected


def test_sanitize_map_name_control_chars_become_underscores():
# ASCII 0x01 and 0x1F are invalid on Windows
raw = "A\x01B\x1fC"
assert sanitize_map_name(raw) == "A_B_C"


def test_sanitize_map_name_truncates_to_50_chars():
raw = "x" * 80
out = sanitize_map_name(raw)
assert len(out) == 50
assert out == "x" * 50


def test_sanitize_map_name_all_invalid_falls_back_to_unnamed():
raw = "<>:\"/\\|?*\t\n"
assert sanitize_map_name(raw) == "unnamed"