+def ip_v4_sort_key(txt: str) -> Optional[Tuple[int, ...]]:
+ """
+ Args:
+ txt: an IP address to chunk up for sorting purposes
+
+ Returns:
+ A tuple of IP components arranged such that the sorting of
+ IP addresses using a normal comparator will do something sane
+ and desireable.
+
+ >>> ip_v4_sort_key('10.0.0.18')
+ (10, 0, 0, 18)
+
+ >>> ips = ['10.0.0.10', '100.0.0.1', '1.2.3.4', '10.0.0.9']
+ >>> sorted(ips, key=lambda x: ip_v4_sort_key(x))
+ ['1.2.3.4', '10.0.0.9', '10.0.0.10', '100.0.0.1']
+ """
+ if not is_ip_v4(txt):
+ print(f"not IP: {txt}")
+ return None
+ return tuple(int(x) for x in txt.split('.'))
+
+
+def path_ancestors_before_descendants_sort_key(volume: str) -> Tuple[str, ...]:
+ """
+ Args:
+ volume: the string to chunk up for sorting purposes
+
+ Returns:
+ A tuple of volume's components such that the sorting of
+ volumes using a normal comparator will do something sane
+ and desireable.
+
+ >>> path_ancestors_before_descendants_sort_key('/usr/local/bin')
+ ('usr', 'local', 'bin')
+
+ >>> paths = ['/usr/local', '/usr/local/bin', '/usr']
+ >>> sorted(paths, key=lambda x: path_ancestors_before_descendants_sort_key(x))
+ ['/usr', '/usr/local', '/usr/local/bin']
+ """
+ return tuple(x for x in volume.split('/') if len(x) > 0)
+
+
+def replace_all(in_str: str, replace_set: str, replacement: str) -> str:
+ """
+ Execute several replace operations in a row.
+
+ Args:
+ in_str: the string in which to replace characters
+ replace_set: the set of target characters to replace
+ replacement: the character to replace any member of replace_set
+ with
+
+ Returns:
+ The string with replacements executed.
+
+ >>> s = 'this_is a-test!'
+ >>> replace_all(s, ' _-!', '')
+ 'thisisatest'
+ """
+ for char in replace_set:
+ in_str = in_str.replace(char, replacement)
+ return in_str
+
+
+def replace_nth(in_str: str, source: str, target: str, nth: int):
+ """
+ Replaces the nth occurrance of a substring within a string.
+
+ Args:
+ in_str: the string in which to run the replacement
+ source: the substring to replace
+ target: the replacement text
+ nth: which occurrance of source to replace?
+
+ >>> replace_nth('this is a test', ' ', '-', 3)
+ 'this is a-test'
+ """
+ where = [m.start() for m in re.finditer(source, in_str)][nth - 1]
+ before = in_str[:where]
+ after = in_str[where:]
+ after = after.replace(source, target, 1)
+ return before + after
+
+