diff --git a/maxdiff/freezing_utils.py b/maxdiff/freezing_utils.py
index d2049e5..9f4be35 100644
--- a/maxdiff/freezing_utils.py
+++ b/maxdiff/freezing_utils.py
@@ -133,3 +133,21 @@ def strip_trailing_nonjson(device_data_text: str) -> str:
# nothing worked; return original so caller can log/handle the parse error
return device_data_text
+
+
+EXTERNAL_FILE_SUFFIXES = [".mxe64", ".mxo", ".mxe"]
+EXTERNAL_BUNDLE_SEPARATOR = "/"
+
+
+def get_external_name(file_name: str) -> str:
+ """
+ Checks the file name of a bundled entry and returns the name of the external as it appears in the patch
+ """
+ if EXTERNAL_BUNDLE_SEPARATOR in file_name:
+ return file_name.split(EXTERNAL_BUNDLE_SEPARATOR)[0]
+
+ for suffix in EXTERNAL_FILE_SUFFIXES:
+ if file_name.endswith(suffix):
+ return file_name[: -len(suffix)]
+
+ return None
diff --git a/maxdiff/frozen_device_printer.py b/maxdiff/frozen_device_printer.py
index 2e8a337..a8cc863 100644
--- a/maxdiff/frozen_device_printer.py
+++ b/maxdiff/frozen_device_printer.py
@@ -1,5 +1,5 @@
from freezing_utils import *
-from get_frozen_stats import get_frozen_stats, get_used_files
+from get_frozen_stats import get_frozen_stats, get_frozen_file_usage, get_external_name
from patch_printer import get_parameters_string_block
@@ -34,7 +34,7 @@ def print_frozen_device(data: bytes) -> str:
if "parameters" in main_patcher:
frozen_string += get_parameters_string_block(main_patcher, bundled_patchers)
- used_files = get_used_files(device_entries)
+ frozen_file_usage = get_frozen_file_usage(device_entries)
i = 0
for entry in device_entries:
@@ -44,10 +44,15 @@ def print_frozen_device(data: bytes) -> str:
if i == 0:
frozen_string += f"{description} <= Device \n"
else:
- if file_name in used_files:
- frozen_string += f"{description}, {used_files[file_name]} instance{'s' if used_files[file_name] > 1 else ''}\n"
+ if file_name in frozen_file_usage:
+ frozen_string += f"{description}, {frozen_file_usage[file_name]} instance{'s' if frozen_file_usage[file_name] > 1 else ''}\n"
else:
- frozen_string += f"{description}, NOT FOUND IN PATCH\n"
+ # this could be an external
+ external_name = get_external_name(file_name)
+ if external_name in frozen_file_usage:
+ frozen_string += f"{description}, {frozen_file_usage[external_name]} instance{'s' if frozen_file_usage[external_name] > 1 else ''}\n"
+ else:
+ frozen_string += f"{description}, NOT FOUND IN PATCH\n"
i += 1
[object_count_total, line_count_total, object_count_unique, line_count_unique] = (
diff --git a/maxdiff/get_frozen_stats.py b/maxdiff/get_frozen_stats.py
index 640119a..4478f3f 100644
--- a/maxdiff/get_frozen_stats.py
+++ b/maxdiff/get_frozen_stats.py
@@ -1,5 +1,5 @@
-from freezing_utils import device_entry_with_data, get_patcher_dict
-from process_patcher import Processor, process_patch
+from freezing_utils import device_entry_with_data, get_patcher_dict, get_external_name
+from process_patcher import PatcherProcessor, walk_patch
def get_frozen_stats(entries: list[device_entry_with_data]) -> tuple[int, int, int, int]:
@@ -17,7 +17,7 @@ def get_frozen_stats(entries: list[device_entry_with_data]) -> tuple[int, int, i
# get total counts: parse every instance of every abstraction
count_processor = CountProcessor()
- process_patch(
+ walk_patch(
device_patch, abstraction_entries, count_processor # do recurse into abstractions
)
object_count_total, line_count_total = count_processor.get_results()
@@ -35,7 +35,7 @@ def get_frozen_stats(entries: list[device_entry_with_data]) -> tuple[int, int, i
continue
count_processor = CountProcessor()
- process_patch(entry_patch, [], count_processor) # don't recurse into abstractions
+ walk_patch(entry_patch, [], count_processor) # don't recurse into abstractions
o, l = count_processor.get_results()
object_count_unique += o
@@ -44,63 +44,97 @@ def get_frozen_stats(entries: list[device_entry_with_data]) -> tuple[int, int, i
return object_count_total, line_count_total, object_count_unique, line_count_unique
-class CountProcessor(Processor):
+class CountProcessor(PatcherProcessor):
def __init__(self):
self.object_count = 0
self.line_count = 0
- def process_elements(self, patcher, voice_count: int, abstraction_name=""):
+ def process_patcher(self, patcher, poly_voice_count: int, abstraction_name=""):
"""Counts objects and lines in the given patcher."""
- self.object_count += len(patcher.get("boxes", [])) * voice_count
- self.line_count += len(patcher.get("lines", [])) * voice_count
+ self.object_count += len(patcher.get("boxes", [])) * poly_voice_count
+ self.line_count += len(patcher.get("lines", [])) * poly_voice_count
def get_results(self):
"""Returns the current counts."""
return self.object_count, self.line_count
-def get_used_files(entries: list[device_entry_with_data]) -> dict[str, int]:
+def get_frozen_file_usage(entries: list[device_entry_with_data]) -> dict[str, int]:
+ """Returns a dict with the names of all the files that are frozen into this device
+ with how often they are used in the device.
+
+ It does this by parsing the top patcher and the abstractions found in `entries` recursively
+ and finding how many objects match with the file names of the bundled dependencies.
+
+ Externals are included in the dict just by name. These names can be matched later with
+ the actual bundled external files.
+ """
device = entries[0] # the first entry is always the device file
abstraction_entries = [
item for item in entries if str(item["file_name"]).endswith(".maxpat")
]
+ external_names = list(
+ dict.fromkeys(
+ get_external_name(item["file_name"])
+ for item in entries
+ if get_external_name(item["file_name"]) != ""
+ )
+ )
+
device_patch = get_patcher_dict(device)
if device_patch == {}:
return {}
- abstractions_processor = FileNamesProcessor()
- process_patch(device_patch, abstraction_entries, abstractions_processor)
- return abstractions_processor.get_results()
+ dependency_file_names_processor = DependencyUsageCounter(external_names)
+ walk_patch(device_patch, abstraction_entries, dependency_file_names_processor)
+ return dependency_file_names_processor.get_results()
-class FileNamesProcessor(Processor):
- def __init__(self):
- self.found_filenames = {}
+class DependencyUsageCounter(PatcherProcessor):
+ def __init__(self, external_names):
+ self.external_names = external_names
+ self.filename_occurrences = {}
- def process_elements(self, patcher, voice_count: int, abstraction_name=""):
+ def process_patcher(self, patcher, poly_voice_count: int, abstraction_name=""):
"""If this patcher is an abstraction, i.e. when an abstraction_name is passed in, increment the entry in the dict.
For other patchers, scan them for objects that use files.
"""
filenames = get_dependency_filenames(patcher)
+ filenames.extend(get_externals(patcher, self.external_names))
if abstraction_name != "":
filenames.append(abstraction_name)
for filename in filenames:
- if filename in self.found_filenames:
- self.found_filenames[filename] += voice_count
+ if filename in self.filename_occurrences:
+ self.filename_occurrences[filename] += poly_voice_count
else:
- self.found_filenames[filename] = voice_count
+ self.filename_occurrences[filename] = poly_voice_count
def get_results(self):
- """Returns a dict of used abstractions mapped to how oftern they are used."""
- return self.found_filenames
+ """Returns a dict of used abstractions mapped to how often they are used."""
+ return self.filename_occurrences
+
+
+def get_externals(patcher, external_names):
+ """Check all boxes in this patcher (don't recurse into subpatchers) and collect any objects that are externals"""
+ externals = []
+ for box_entry in patcher["boxes"]:
+ box = box_entry["box"]
+ object_type = box["maxclass"]
+ if object_type == "newobj":
+ if "text" in box:
+ boxtext = box["text"]
+ object_name = boxtext.split(" ")[0]
+ if object_name in external_names:
+ externals.append(object_name)
+ return externals
def get_dependency_filenames(patcher):
- """Check all boxes in this patcher and report any dependencies they might be referring to"""
+ """Check all boxes in this patcher (don't recurse into subpatchers) and collect any dependencies they might be referring to"""
filenames = []
for box_entry in patcher["boxes"]:
box = box_entry["box"]
diff --git a/maxdiff/print_unicode.py b/maxdiff/print_unicode.py
index 4f06fbb..e8b78ea 100644
--- a/maxdiff/print_unicode.py
+++ b/maxdiff/print_unicode.py
@@ -1,6 +1,6 @@
"""
Module that shares a function to gracefully handle printing unicode characters
-if the user has the PYTHONIOENCODING environment vairable set to 'ascii'.
+if the user has the PYTHONIOENCODING environment variable set to 'ascii'.
In that case, replace any characters that can't be encoded with ?.
"""
diff --git a/maxdiff/process_patcher.py b/maxdiff/process_patcher.py
index 305707d..04b7786 100644
--- a/maxdiff/process_patcher.py
+++ b/maxdiff/process_patcher.py
@@ -1,8 +1,8 @@
from freezing_utils import get_patcher_dict
-class Processor:
- def process_elements(self, patcher, voice_count: int, abstraction_name=""):
+class PatcherProcessor:
+ def process_patcher(self, patcher, poly_voice_count: int, abstraction_name=""):
"""Processes patchers."""
def get_results(self):
@@ -10,29 +10,29 @@ def get_results(self):
return None
-def process_patch(patcher, abstraction_entries: list[dict], processor: Processor):
- process_patch_recursive(patcher, abstraction_entries, processor, 1, "")
+def walk_patch(patcher, abstraction_entries: list[dict], processor: PatcherProcessor):
+ walk_patch_recursive(patcher, abstraction_entries, processor, 1, "")
-def process_patch_recursive(
+def walk_patch_recursive(
patcher,
abstraction_entries: list[dict],
- processor: Processor,
- voice_count: int,
+ processor: PatcherProcessor,
+ poly_voice_count: int,
abstraction_file_name: str,
):
"""Recursively progress through subpatchers, invoking the processor for every patcher
- inluding every instance of the patch's dependencies that can be found among the
+ including every instance of the patch's dependencies that can be found among the
abstraction files that were passed in.
Arguments
patcher: patcher to process
abstraction_entries: list of abstraction entries in frozen device
processor: instance of a Processor that is invoked for this patch
- voice_count: the amount of voices when this patcher occurs in a poly~ in its parent patch
+ poly_voice_count: the amount of voices when this patcher occurs in a poly~ in its parent patch
abstraction_file_name: the file name of the abstraction this patch is in, if it is in an abstraction
"""
- processor.process_elements(patcher, voice_count, abstraction_file_name)
+ processor.process_patcher(patcher, poly_voice_count, abstraction_file_name)
for box_entry in patcher["boxes"]:
box = box_entry["box"]
@@ -42,9 +42,9 @@ def process_patch_recursive(
):
patch = box["patcher"]
# get subpatcher or embedded bpatcher count
- process_patch_recursive(patch, abstraction_entries, processor, 1, "")
+ walk_patch_recursive(patch, abstraction_entries, processor, 1, "")
- # if no abstractions were passed in, we assume we don't want to recurse into abstarctions
+ # if no abstractions were passed in, we assume we don't want to recurse into abstractions
if len(abstraction_entries) == 0:
continue
@@ -60,17 +60,17 @@ def process_patch_recursive(
if patch == {}:
continue # something went wrong when parsing the abstraction
- voice_count = 1
+ poly_voice_count = 1
if "text" in box and box["text"].startswith("poly~"):
# get poly abstraction count
tokens = box["text"].split(" ")
- voice_count = int(tokens[2]) if len(tokens) > 2 else 1
+ poly_voice_count = int(tokens[2]) if len(tokens) > 2 else 1
- process_patch_recursive(
+ walk_patch_recursive(
patch,
abstraction_entries,
processor,
- voice_count,
+ poly_voice_count,
file_name,
)
diff --git a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt
index 8b38686..748e2e6 100644
--- a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt
+++ b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt
@@ -124,10 +124,10 @@ parameters:
banks:
0: (MyBank) encoders: ['live.dial', 'InsideBpatcher', '-', '-', '-', '-', '-', '-'] buttons: ['live.text', '-', '-', '-', '-', '-', '-', 'live.menu']
1: encoders: ['-', '-', 'live.dial', 'MyParameter', 'live.dial', 'live.dial[1]', '-', '-'] buttons: ['-', '-', '-', '-', '-', 'live.text', 'live.menu', '-']
-Test.amxd: 57703 bytes, modified at 2026/08/31 09:52:24 UTC <= Device
-MyAbstraction.maxpat: 2027 bytes, modified at 2026/08/31 09:51:02 UTC, 6 instances
-ParamAbstraction.maxpat: 1491 bytes, modified at 2026/08/31 09:51:02 UTC, 2 instances
-AbstractionWithParameter.maxpat: 1334 bytes, modified at 2026/08/31 09:51:02 UTC, 2 instances
+Test.amxd: 58702 bytes, modified at 2026/09/03 12:38:17 UTC <= Device
+MyAbstraction.maxpat: 2027 bytes, modified at 2026/08/31 10:48:30 UTC, 6 instances
+ParamAbstraction.maxpat: 1491 bytes, modified at 2026/08/31 10:48:30 UTC, 2 instances
+AbstractionWithParameter.maxpat: 1334 bytes, modified at 2026/08/31 10:48:30 UTC, 2 instances
hz-icon.svg: 484 bytes, modified at 2024/05/24 13:59:36 UTC, 3 instances
beat-icon.svg: 533 bytes, modified at 2024/05/24 13:59:36 UTC, 3 instances
fpic.png: 7094 bytes, modified at 2024/05/24 13:59:36 UTC, 5 instances
@@ -140,10 +140,16 @@ mystorage.json: 239 bytes, modified at 2026/08/27 14:58:36 UTC, 1 instance
myTestTable: 410 bytes, modified at 2026/08/27 14:58:36 UTC, 1 instance
TestScript.js: 87 bytes, modified at 2026/08/27 14:58:36 UTC, 1 instance
collContent.txt: 8 bytes, modified at 2024/05/24 13:59:36 UTC, NOT FOUND IN PATCH
+shell.mxo: 4 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
+shell/zipfile: 28122 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
+shell/executable: 182544 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
+shell/plist: 1441 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
+shell.mxe: 30720 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
+shell.mxe64: 37888 bytes, modified at 2026/08/31 10:47:56 UTC, 2 instances
Total - Counting every abstraction instance - Indicates loading time
- Object instances: 90
+ Object instances: 92
Connections: 26
Unique - Counting abstractions once - Indicates maintainability
- Object instances: 73
+ Object instances: 75
Connections: 16
diff --git a/maxdiff/tests/test_baselines/Test.amxd.txt b/maxdiff/tests/test_baselines/Test.amxd.txt
index 11044c4..c2d3f19 100644
--- a/maxdiff/tests/test_baselines/Test.amxd.txt
+++ b/maxdiff/tests/test_baselines/Test.amxd.txt
@@ -99,6 +99,8 @@ project:
----------- patcher -----------
appversion: 9.1.4-x64-1 | rect: [91, 153, 1288, 310] | openrect: [0, 0, 0, 169] | openrectmode: 0 | default_fontsize: 10.0 | default_fontname: Arial Bold | gridsize: [8, 8] | boxanimatetime: 500 | latency: 0 | is_mpe: 0 | external_mpe_tuning_enabled: 0 | platform_compatibility: 0 | autosave: 0
----------- objects -----------
+[shell] shell: (default)
+[shell dummyArgument] shell: (default)
[comment NOTE: after any changes to this device, also update FrozenTest.amxd]
[poly~ MyAbstraction]
[poly~ MyAbstraction 3]
diff --git a/maxdiff/tests/test_files/FrozenTest.amxd b/maxdiff/tests/test_files/FrozenTest.amxd
index 4462f2b..2bcded8 100644
Binary files a/maxdiff/tests/test_files/FrozenTest.amxd and b/maxdiff/tests/test_files/FrozenTest.amxd differ
diff --git a/maxdiff/tests/test_files/Test.amxd b/maxdiff/tests/test_files/Test.amxd
index 17629a0..1fcde96 100644
Binary files a/maxdiff/tests/test_files/Test.amxd and b/maxdiff/tests/test_files/Test.amxd differ
diff --git a/maxdiff/tests/test_files/shell.maxhelp b/maxdiff/tests/test_files/shell.maxhelp
new file mode 100644
index 0000000..682e7ed
--- /dev/null
+++ b/maxdiff/tests/test_files/shell.maxhelp
@@ -0,0 +1,909 @@
+{
+ "patcher" : {
+ "fileversion" : 1,
+ "appversion" : {
+ "major" : 6,
+ "minor" : 0,
+ "revision" : 8
+ }
+,
+ "rect" : [ 158.0, 146.0, 831.0, 513.0 ],
+ "bglocked" : 0,
+ "openinpresentation" : 0,
+ "default_fontsize" : 12.0,
+ "default_fontface" : 0,
+ "default_fontname" : "Arial",
+ "gridonopen" : 0,
+ "gridsize" : [ 15.0, 15.0 ],
+ "gridsnaponopen" : 0,
+ "statusbarvisible" : 2,
+ "toolbarvisible" : 1,
+ "boxanimatetime" : 200,
+ "imprint" : 0,
+ "enablehscroll" : 1,
+ "enablevscroll" : 1,
+ "devicewidth" : 0.0,
+ "description" : "",
+ "digest" : "",
+ "tags" : "",
+ "boxes" : [ {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-35",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 633.5, 179.0, 98.0, 16.0 ],
+ "text" : "/usr/local/bin/unrar"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-31",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 217.0, 408.0, 32.5, 16.0 ],
+ "text" : "set"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-30",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 500.0, 201.0, 56.0, 16.0 ],
+ "text" : "example-1"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-29",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 500.0, 179.0, 121.0, 16.0 ],
+ "text" : "/usr/local/bin/example-1"
+ }
+
+ }
+, {
+ "box" : {
+ "bgcolor" : [ 1.0, 0.0, 0.065008, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 12.0,
+ "id" : "obj-34",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 453.5, 53.0, 161.0, 19.0 ],
+ "text" : "TESTED SUDO WORKS !!!!!",
+ "textcolor" : [ 1.0, 1.0, 1.0, 1.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "bgcolor" : [ 1.0, 0.0, 0.065008, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 12.0,
+ "id" : "obj-33",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 522.5, 89.0, 232.0, 19.0 ],
+ "text" : "echo 'a' | sudo -S rm -rf /Library/Logs",
+ "textcolor" : [ 1.0, 1.0, 1.0, 1.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "bgcolor" : [ 1.0, 0.0, 0.065008, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 12.0,
+ "id" : "obj-32",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 385.5, 89.0, 128.0, 19.0 ],
+ "text" : "rm -rf /Library/Logs",
+ "textcolor" : [ 1.0, 1.0, 1.0, 1.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-28",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 391.0, 140.0, 47.0, 16.0 ],
+ "text" : "penter a"
+ }
+
+ }
+, {
+ "box" : {
+ "bgcolor" : [ 1.0, 0.0, 0.065008, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 12.0,
+ "id" : "obj-27",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 542.0, 118.0, 193.0, 19.0 ],
+ "text" : "echo 'a' | sudo -S mkdir /AAAA",
+ "textcolor" : [ 1.0, 1.0, 1.0, 1.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-26",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 370.0, 181.0, 84.0, 16.0 ],
+ "text" : "cd /usr/local/bin"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-25",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 66.0, 386.0, 232.0, 16.0 ],
+ "text" : "\" y Assume Yes on all queries\""
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-24",
+ "linecount" : 2,
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 366.5, 235.0, 335.0, 27.0 ],
+ "text" : "/usr/local/bin/rar a -s -y -hpXXX -x.DS_Store /Users/Mac/Desktop/AR.rar /Users/Mac/Desktop/ALL-NEW-RACK12"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-1",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 134.0, 148.0, 56.0, 16.0 ],
+ "text" : "vm_stat 1"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "frgb" : 0.0,
+ "id" : "obj-2",
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 70.0, 212.0, 105.0, 18.0 ],
+ "text" : "( works only as root )"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-3",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 138.0, 179.0, 32.5, 16.0 ],
+ "text" : "pkill"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-4",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 23.0, 210.0, 49.0, 16.0 ],
+ "text" : "tcpdump"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-5",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 345.0, 278.0, 55.0, 18.0 ],
+ "text" : "print done"
+ }
+
+ }
+, {
+ "box" : {
+ "color" : [ 1.0, 0.360784, 0.682353, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-6",
+ "maxclass" : "newobj",
+ "numinlets" : 0,
+ "numoutlets" : 0,
+ "patcher" : {
+ "fileversion" : 1,
+ "appversion" : {
+ "major" : 6,
+ "minor" : 0,
+ "revision" : 8
+ }
+,
+ "rect" : [ 621.0, 408.0, 222.0, 234.0 ],
+ "bglocked" : 0,
+ "openinpresentation" : 0,
+ "default_fontsize" : 12.0,
+ "default_fontface" : 0,
+ "default_fontname" : "Arial",
+ "gridonopen" : 0,
+ "gridsize" : [ 15.0, 15.0 ],
+ "gridsnaponopen" : 0,
+ "statusbarvisible" : 2,
+ "toolbarvisible" : 1,
+ "boxanimatetime" : 200,
+ "imprint" : 0,
+ "enablehscroll" : 1,
+ "enablevscroll" : 1,
+ "devicewidth" : 0.0,
+ "description" : "",
+ "digest" : "",
+ "tags" : "",
+ "boxes" : [ {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-1",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 28.0, 75.0, 110.0, 16.0 ],
+ "text" : "ls fakedirectory 2>&1"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-2",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 28.0, 156.0, 99.0, 18.0 ],
+ "text" : "print stdout&stderr"
+ }
+
+ }
+, {
+ "box" : {
+ "color" : [ 1.0, 0.890196, 0.090196, 1.0 ],
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-3",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 28.0, 100.0, 51.0, 18.0 ],
+ "text" : "tosymbol"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-4",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 2,
+ "outlettype" : [ "", "bang" ],
+ "patching_rect" : [ 28.0, 128.0, 30.0, 18.0 ],
+ "text" : "shell"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "frgb" : 0.0,
+ "id" : "obj-5",
+ "linecount" : 3,
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 27.0, 23.0, 152.0, 40.0 ],
+ "text" : "standard error can be combined with stdout by following a command with \"2>&1\""
+ }
+
+ }
+ ],
+ "lines" : [ {
+ "patchline" : {
+ "destination" : [ "obj-3", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-1", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-4", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-3", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-2", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-4", 0 ]
+ }
+
+ }
+ ]
+ }
+,
+ "patching_rect" : [ 217.0, 297.0, 81.0, 18.0 ],
+ "saved_object_attributes" : {
+ "default_fontface" : 0,
+ "default_fontname" : "Arial",
+ "default_fontsize" : 12.0,
+ "description" : "",
+ "digest" : "",
+ "fontface" : 0,
+ "fontname" : "Arial",
+ "fontsize" : 12.0,
+ "globalpatchername" : "",
+ "tags" : ""
+ }
+,
+ "text" : "p standard-error"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-7",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 196.5, 347.0, 62.0, 18.0 ],
+ "text" : "print stdout"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-8",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 149.0, 89.0, 132.0, 16.0 ],
+ "text" : "perl -e 'print \\\"howdy\\\\n\\\"'"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-9",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 144.0, 53.0, 173.0, 16.0 ],
+ "text" : "ls -l \\\"/Library/Application Support\\\""
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-10",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 26.0, 152.0, 62.0, 16.0 ],
+ "text" : "echo $USER"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-11",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 32.0, 181.0, 64.0, 16.0 ],
+ "text" : "perl -version"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-12",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 37.0, 112.0, 32.5, 16.0 ],
+ "text" : "date"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-13",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 41.0, 56.0, 83.0, 16.0 ],
+ "text" : "cat /etc/passwd"
+ }
+
+ }
+, {
+ "box" : {
+ "id" : "obj-14",
+ "maxclass" : "button",
+ "numinlets" : 1,
+ "numoutlets" : 1,
+ "outlettype" : [ "bang" ],
+ "patching_rect" : [ 32.0, 278.0, 20.0, 20.0 ]
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-15",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 148.0, 118.0, 159.0, 16.0 ],
+ "text" : "ps -x | grep MaxMSP | grep -v grep"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-16",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 12.0, 86.0, 96.0, 16.0 ],
+ "text" : "mkdir ~/anewfolder"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-17",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 78.0, 112.0, 43.0, 16.0 ],
+ "text" : "whoami"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-18",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 121.0, 235.0, 97.0, 16.0 ],
+ "text" : "ping -c 20 localhost"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-19",
+ "maxclass" : "message",
+ "numinlets" : 2,
+ "numoutlets" : 1,
+ "outlettype" : [ "" ],
+ "patching_rect" : [ 131.0, 31.0, 40.0, 16.0 ],
+ "text" : "ls -l ~/"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "id" : "obj-20",
+ "maxclass" : "newobj",
+ "numinlets" : 1,
+ "numoutlets" : 2,
+ "outlettype" : [ "", "bang" ],
+ "patching_rect" : [ 84.0, 278.0, 134.0, 18.0 ],
+ "text" : "shell"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Arial",
+ "fontsize" : 14.0,
+ "frgb" : 0.0,
+ "id" : "obj-21",
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 17.0, 27.0, 55.0, 22.0 ],
+ "text" : "SHELL"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Arial",
+ "fontsize" : 10.0,
+ "frgb" : 0.0,
+ "id" : "obj-22",
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 66.0, 28.0, 52.0, 18.0 ],
+ "text" : "unix shell"
+ }
+
+ }
+, {
+ "box" : {
+ "fontname" : "Geneva",
+ "fontsize" : 9.0,
+ "frgb" : 0.0,
+ "id" : "obj-23",
+ "maxclass" : "comment",
+ "numinlets" : 1,
+ "numoutlets" : 0,
+ "patching_rect" : [ 166.0, 181.0, 110.0, 18.0 ],
+ "text" : "kill the current process"
+ }
+
+ }
+ ],
+ "lines" : [ {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-1", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-10", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-11", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-12", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-13", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-14", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-15", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-16", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-17", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-18", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-19", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-25", 1 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-20", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-5", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-20", 1 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-7", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-20", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-24", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-26", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-28", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-29", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-3", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-30", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-25", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-31", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-35", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-4", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-8", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-20", 0 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "source" : [ "obj-9", 0 ]
+ }
+
+ }
+, {
+ "patchline" : {
+ "destination" : [ "obj-26", 1 ],
+ "disabled" : 0,
+ "hidden" : 0,
+ "midpoints" : [ 448.0, 181.0 ],
+ "source" : [ "", -1 ]
+ }
+
+ }
+ ],
+ "dependency_cache" : [ {
+ "name" : "shell.mxo",
+ "type" : "iLaX"
+ }
+ ]
+ }
+
+}
diff --git a/maxdiff/tests/test_files/shell.mxe b/maxdiff/tests/test_files/shell.mxe
new file mode 100755
index 0000000..0c3f8b7
Binary files /dev/null and b/maxdiff/tests/test_files/shell.mxe differ
diff --git a/maxdiff/tests/test_files/shell.mxe64 b/maxdiff/tests/test_files/shell.mxe64
new file mode 100755
index 0000000..73b0cca
Binary files /dev/null and b/maxdiff/tests/test_files/shell.mxe64 differ
diff --git a/maxdiff/tests/test_files/shell.mxo/Contents/Info.plist b/maxdiff/tests/test_files/shell.mxo/Contents/Info.plist
new file mode 100644
index 0000000..74b6984
--- /dev/null
+++ b/maxdiff/tests/test_files/shell.mxo/Contents/Info.plist
@@ -0,0 +1,50 @@
+
+
+
+
+ BuildMachineOSBuild
+ 19H1217
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ shell
+ CFBundleGetInfoString
+ shell 8.0.0, Copyright ©2018 Bill Orcutt/Jeremy Bernstein
+ CFBundleIdentifier
+ com.bootsquad.shell
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ iLaX
+ CFBundleShortVersionString
+ 8.0.0
+ CFBundleSignature
+ max2
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleVersion
+ 8.0.0
+ CSResourcesFileMapped
+
+ DTCompiler
+ com.apple.compilers.llvm.clang.1_0
+ DTPlatformBuild
+ 12D4e
+ DTPlatformName
+ macosx
+ DTPlatformVersion
+ 11.1
+ DTSDKBuild
+ 20C63
+ DTSDKName
+ macosx11.1
+ DTXcode
+ 1240
+ DTXcodeBuild
+ 12D4e
+ LSMinimumSystemVersion
+ 10.11
+
+
diff --git a/maxdiff/tests/test_files/shell.mxo/Contents/MacOS/shell b/maxdiff/tests/test_files/shell.mxo/Contents/MacOS/shell
new file mode 100755
index 0000000..1cdd809
Binary files /dev/null and b/maxdiff/tests/test_files/shell.mxo/Contents/MacOS/shell differ
diff --git a/maxdiff/tests/test_files/shell.mxo/Contents/PkgInfo b/maxdiff/tests/test_files/shell.mxo/Contents/PkgInfo
new file mode 100644
index 0000000..0febb6e
--- /dev/null
+++ b/maxdiff/tests/test_files/shell.mxo/Contents/PkgInfo
@@ -0,0 +1 @@
+iLaXmax2
\ No newline at end of file
diff --git a/maxdiff/tests/test_files/shell.mxo/Contents/_CodeSignature/CodeResources b/maxdiff/tests/test_files/shell.mxo/Contents/_CodeSignature/CodeResources
new file mode 100644
index 0000000..d5d0fd7
--- /dev/null
+++ b/maxdiff/tests/test_files/shell.mxo/Contents/_CodeSignature/CodeResources
@@ -0,0 +1,115 @@
+
+
+
+
+ files
+
+ files2
+
+ rules
+
+ ^Resources/
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
+
+ nested
+
+ weight
+ 10
+
+ ^.*
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^Resources/
+
+ weight
+ 20
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^[^/]+$
+
+ nested
+
+ weight
+ 10
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+