{"id":1104,"date":"2022-03-07T23:26:11","date_gmt":"2022-03-08T04:26:11","guid":{"rendered":"https:\/\/www.brunerd.com\/blog\/?p=1104"},"modified":"2022-03-23T00:49:10","modified_gmt":"2022-03-23T05:49:10","slug":"respecting-focus-and-meeting-status-in-your-mac-scripts-aka-dont-be-a-jerk","status":"publish","type":"post","link":"https:\/\/www.brunerd.com\/blog\/2022\/03\/07\/respecting-focus-and-meeting-status-in-your-mac-scripts-aka-dont-be-a-jerk\/","title":{"rendered":"Respecting Focus and Meeting Status in Your Mac scripts (aka Don&#8217;t Be a Jerk)"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c.jpeg\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"455\" src=\"https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c-1024x455.jpeg\" alt=\"\" class=\"wp-image-1118\" srcset=\"https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c-1024x455.jpeg 1024w, https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c-300x133.jpeg 300w, https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c-768x341.jpeg 768w, https:\/\/www.brunerd.com\/blog\/wp-content\/uploads\/50BCDFC8-BDA1-4C54-AD99-EAB6FDB6BBE3_1_105_c.jpeg 1330w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In four words Barbara Krueger distills the Golden Rule into an in-your-face admonishment: <strong>Don&#8217;t be a jerk<\/strong>. It makes for a great coffee mug but how does this relate to shell scripting for a Mac admin and engineer? Well, sometimes a script can only go so far and you need <em>user<\/em> <em>consent<\/em> and <em>cooperation<\/em> to get the job done. Not being a jerk about it can pay off!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A good way to get cooperation from your users is to build upon a foundation of trust and respect. While IT has a long list of to-dos for users: &#8220;Did you reboot? Did you run updates? Did you open a ticket? Did you <em>really<\/em> reboot?&#8221; the users might have one for <em>us<\/em>: &#8220;Respect our screens when we&#8217;re in the middle of a client meeting and respect when Focus mode is turned on.&#8221; And they are right. That&#8217;s also <em>two<\/em> things. &#8220;Give an inch and they take a mile&#8221; I tell ya!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So how can we in IT can be considerate of our users? First, don&#8217;t do anything <a rel=\"noreferrer noopener\" href=\"https:\/\/www.youtube.com\/results?search_query=nick+burns+your+company%27s+computer+guy\" target=\"_blank\">Nick Burns<\/a> from SNL does. Second, use the functions below (and in my <a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/tree\/main\/Scripts\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a>) to check if a Zoom, Teams, Webex, or Goto meeting is happening or if Do Not Disturb\/Focus mode is on. When non-user-initiated scripts (i.e. daily pop-ups\/nags\/alerts) run they can bail or wait if the user is busy. If it were real life (and it is!) we wouldn&#8217;t walk into a meeting and bug a user, in front of everyone, to run update. If we did, they&#8217;d be more likely to remember how rude we were rather than actually running the updates. So let&#8217;s get their attention when they will be most receptive to what we have to say.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Detecting Online Meetings Apps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">First up is <code><a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/inMeeting_Zoom.sh\" target=\"_blank\" rel=\"noreferrer noopener\">inMeeting_Zoom<\/a><\/code> which simply checks for the <code>CptHost<\/code> process and returns success or fail. Notice how this simple behavior can be used with an <code>if\/then<\/code> statement. The return code is evaluated by the <code>if<\/code>, a zero is success and a non-zero is a failure. <code>&amp;&amp;<\/code> is a logical AND and <code>||<\/code> is a logical OR<\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/sh\n#inMeeting_Zoom (20220227) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#Licensed under the MIT License\n\nfunction inMeeting_Zoom {\n\t#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)\n\tpgrep \"CptHost\" &amp;&gt;\/dev\/null &amp;&amp; return 0 || return 1\n}\n\nif inMeeting_Zoom; then\n\techo \"In Zoom meeting... don't be a jerk\"\nelse\n\techo \"Not in Zoom meeting\"\nfi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Next is another process checker for Webex: <a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/inMeeting_Webex.sh\" target=\"_blank\" rel=\"noreferrer noopener\">inMeetng_Webex<\/a>. What is a bit more unique is the process appears in <code>ps<\/code> <a rel=\"noreferrer noopener\" href=\"https:\/\/docstore.mik.ua\/orelly\/unix\/upt\/ch38_07.htm\" target=\"_blank\">in parentheses<\/a> as <code>(WebexAppLauncher)<\/code>however pgrep cannot find this process (because the actual name has been rewritten by the Meeting Center process). We instead use a combination of <code>ps<\/code> and <code>grep<\/code>. A neat trick with <code>grep<\/code> is to use a <code>[]<\/code> regex character class to surround a single character, this keeps grep from matching itself in the process list. That way you don&#8217;t need to have an extra <code>grep -v grep<\/code> to clean up the output.<\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/sh\n#inMeeting_Webex (20220227) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#Licensed under the MIT License\n\nfunction inMeeting_Webex {\n\t#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)\n\tps auxww | grep -q \"&#91;(]WebexAppLauncher)\" &amp;&amp; return 0 || return 1\n}\n\nif inMeeting_Webex; then\n\techo \"In Zoom meeting... don't be a jerk\"\nelse\n\techo \"Not Webex in meeting\"\nfi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Goto Meeting is more straightforward, although it should be noted that regardless of quote type, single or double, the parentheses must be escaped with a backslash. Otherwise, it&#8217;s the same pattern, look for the process name which only appears during a meeting or during the meeting preview and return 0 or 1 for <code>if<\/code> to evaluate, find it here: <a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/inMeeting_Goto.sh\" target=\"_blank\" rel=\"noreferrer noopener\">inMeeting_Goto<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/sh\n#inMeeting_Goto (20220227) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#Licensed under the MIT License\n\nfunction inMeeting_Goto() {\n\t#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)\n\tpgrep \"GoTo Helper \\(Plugin\\)\" &amp;&gt;\/dev\/null &amp;&amp; return 0 || return 1\n}\n\nif inMeeting_Goto; then\n\techo \"In Goto meeting... don't be a jerk\"\nelse\n\techo \"Not in Goto meeting\"\nfi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lastly, Teams is a bit more complex, rather than looking for the presence of a process, we instead look for a JSON file in the user&#8217;s <code>\/Library\/Application Support\/Microsoft\/Teams<\/code> folder which has the current call status for both the app and the web plugin (the other methods above are for the app only). We&#8217;ll use the <code><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/brunerd\/ljt\" target=\"_blank\">ljt<\/a><\/code> to extract the value from the JSON. In fact I wrote <code>ljt<\/code> after starting to write this blog last week and realizing that <code><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/brunerd\/jpt\" target=\"_blank\">jpt<\/a><\/code> (weighing in at 64k) was just overkill. As a bonus to doing that, I just realized that bash functions <em>can<\/em> contain functions! Long ago I ditched using <code>()<\/code> in shell function declarations and just used the <code>function<\/code> keyword. Empty parentheses seemed decorative rather than functional since it&#8217;s <em>not<\/em> like it&#8217;s a C function that needs parameter names and types. However the lack of parentheses <code>()<\/code> apparently, prevents a function from being declared inside a function! Below I just wanted to make sure <code>ljt<\/code> doesn&#8217;t get separated from <code><a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/inMeeting_Teams.sh\" target=\"_blank\" rel=\"noreferrer noopener\">inMeetings_Teams<\/a><\/code><\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/bash\n#inMeeting_Teams (20220227) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#Licensed under the MIT License\n\nfunction inMeeting_Teams ()(\n\n\tfunction ljt () ( #v1.0.3\n\t\t&#91; -n \"${-\/\/&#91;^x]\/}\" ] &amp;&amp; set +x; read -r -d '' JSCode &lt;&lt;-'EOT'\n\t\ttry {var query=decodeURIComponent(escape(arguments&#91;0]));var file=decodeURIComponent(escape(arguments&#91;1]));if (query&#91;0]==='\/'){ query = query.split('\/').slice(1).map(function (f){return \"&#91;\"+JSON.stringify(f)+\"]\"}).join('')}if(\/&#91;^A-Za-z_$\\d\\.\\&#91;\\]'\"]\/.test(query.split('').reverse().join('').replace(\/(&#91;\"'])(.*?)\\1(?!\\\\)\/g, \"\"))){throw new Error(\"Invalid path: \"+ query)};if(query&#91;0]===\"$\"){query=query.slice(1,query.length)};var data=JSON.parse(readFile(file));var result=eval(\"(data)\"+query)}catch(e){printErr(e);quit()};if(result !==undefined){result!==null&amp;&amp;result.constructor===String?print(result): print(JSON.stringify(result,null,2))}else{printErr(\"Node not found.\")}\n\t\tEOT\n\t\tqueryArg=\"${1}\"; fileArg=\"${2}\";jsc=$(find \"\/System\/Library\/Frameworks\/JavaScriptCore.framework\/Versions\/Current\/\" -name 'jsc');&#91; -z \"${jsc}\" ] &amp;&amp; jsc=$(which jsc);&#91; -f \"${queryArg}\" -a -z \"${fileArg}\" ] &amp;&amp; fileArg=\"${queryArg}\" &amp;&amp; unset queryArg;if &#91; -f \"${fileArg:=\/dev\/stdin}\" ]; then { errOut=$( { { \"${jsc}\" -e \"${JSCode}\" -- \"${queryArg}\" \"${fileArg}\"; } 1&gt;&amp;3 ; } 2&gt;&amp;1); } 3&gt;&amp;1;else { errOut=$( { { \"${jsc}\" -e \"${JSCode}\" -- \"${queryArg}\" \"\/dev\/stdin\" &lt;&lt;&lt; \"$(cat)\"; } 1&gt;&amp;3 ; } 2&gt;&amp;1); } 3&gt;&amp;1; fi;if &#91; -n \"${errOut}\" ]; then \/bin\/echo \"$errOut\" &gt;&amp;2; return 1; fi\n\t)\n\n\tconsoleUser=$(stat -f %Su \/dev\/console)\n\tconsoleUserHomeFolder=$(dscl . -read \/Users\/\"${consoleUser}\" NFSHomeDirectory | awk -F ': ' '{print $2}')\n\tstorageJSON_path=\"${consoleUserHomeFolder}\/Library\/Application Support\/Microsoft\/Teams\/storage.json\"\n\t\n\t#no file, no meeting\n\t&#91; ! -f \"${storageJSON_path}\" ] &amp;&amp; return 1\n\n\t#get both states\n\tappState=$(ljt \/appStates\/states \"${storageJSON_path}\" | tr , $'\\n' | tail -n 1)\n\twebappState=$(ljt \/webAppStates\/states \"${storageJSON_path}\"| tr , $'\\n' | tail -n 1)\n\t\n\t#determine app state\n\tif &#91; \"${appState}\" = \"InCall\" ]\t|| &#91; \"${webAppState}\" = \"InCall\" ]; then\n\t\treturn 0\n\telse\n\t\treturn 1\n\tfi\n)\n\n\nif inMeeting_Teams; then\n\techo \"In Teams Meeting... don't be a jerk\"\nelse\n\techo \"Not in Teams Meeting\"\nfi<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Detecting Focus (formerly Do Not Disturb)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Last but not least is determining Focus (formerly Do Not Disturb) with <a href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/doNotDisturb.sh\" target=\"_blank\" rel=\"noreferrer noopener\">doNotDisturb<\/a>. As you can see there&#8217;s been a few different ways this has been implemented over the years. In macOS 10.13-11 the state was stored inside of a plist. For macOS 12 Monterey they&#8217;ve switched from a plist to a JSON file. A simple grep though is all that&#8217;s needed to find the key name <code>storeAssertionRecords<\/code>. If it is <strong>off<\/strong>, that string is nowhere to be find, when it&#8217;s on it&#8217;s there. Simple (as in Keep it Simple Stoopid)<\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/bash\n#doNotDisturb (grep) (20220227) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#Licensed under the MIT License\n\n#An example of detecting Do Not Disturb (macOS 10.13-12)\n\nfunction doNotDisturb()(\n\n\tOS_major=\"$(sw_vers -productVersion | cut -d. -f1)\"\n\tconsoleUserID=\"$(stat -f %u \/dev\/console)\"\n\tconsoleUser=\"$(stat -f %Su \/dev\/console)\"\n\t\n\t#get Do Not Disturb status\n\tif &#91; \"${OS_major}\" = \"10\" ]; then\n\t\t#returns c-cstyle boolean 0 (off) or 1 (on)\n\t\tdndStatus=\"$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} defaults -currentHost read com.apple.notificationcenterui doNotDisturb 2&gt;\/dev\/null)\"\n\n\t\t#eval c-style boolean and return shell style value\n\t\t&#91; \"${dndStatus}\" = \"1\" ] &amp;&amp; return 0 || return 1\n\t#this only works for macOS 11 - macOS12 does not affect any of the settings in com.apple.ncprefs\n\telif &#91; \"${OS_major}\" = \"11\" ]; then\n\t\t#returns \"true\" or &#91;blank]\n\t\tdndStatus=\"$(\/usr\/libexec\/PlistBuddy -c \"print :userPref:enabled\" \/dev\/stdin 2&gt;\/dev\/null &lt;&lt;&lt; \"$(plutil -extract dnd_prefs xml1 -o - \/dev\/stdin &lt;&lt;&lt; \"$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} defaults export com.apple.ncprefs.plist -)\" | xmllint --xpath \"string(\/\/data)\" - | base64 --decode | plutil -convert xml1 - -o -)\")\"\n\n\t\t#if we have ANYTHING it is ON (return 0) otherwise fail (return 1)\n\t\t&#91; -n \"${dndStatus}\" ] &amp;&amp; return 0 || return 1\n\telif &#91; \"${OS_major}\" -ge \"12\" ]; then\n\t\tconsoleUserHomeFolder=$(dscl . -read \/Users\/\"${consoleUser}\" NFSHomeDirectory | awk -F ': ' '{print $2}')\n\t\tfile_assertions=\"${consoleUserHomeFolder}\/Library\/DoNotDisturb\/DB\/Assertions.json\"\n\n\t\t#if Assertions.json file does NOT exist, then DnD is OFF\n\t\t&#91; ! -f \"${file_assertions}\" ] &amp;&amp; return 1\n\n\t\t#simply check for storeAssertionRecords existence, usually found at: \/data\/0\/storeAssertionRecords (and only exists when ON)\n\t\t! grep -q \"storeAssertionRecords\" \"${file_assertions}\" 2&gt;\/dev\/null &amp;&amp; return 1 || return 0\n\tfi\n)\nif doNotDisturb; then\n\techo \"DnD\/Focus is ON... don't be a jerk\"\nelse\n\techo \"DnD\/Focus is OFF\"\nfi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Since Focus can remain on indefinitely an end user may never see your pop-up. If so, build a counter with a local plist to record and increment the number of attempts. After a threshold has been reached you can then break through to the user (I certainly do).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Detecting Apps in Presentation Mode<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A newer addition to this page is some code to detect fullscreen presentation apps that I cannot take credit for. Adam Codega, one of the contributors to <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/Installomator\/Installomator\" target=\"_blank\">Installomator<\/a> hipped me to a <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/Macjutsu\/super\/blob\/2634d20ab58fb7f5698800fe343ef8ffa77afbcf\/super#L1940\" target=\"_blank\">cool line<\/a> of code that was added in <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/Installomator\/Installomator\/pull\/268\" target=\"_blank\">PR 268<\/a>. It leverages <code>pmset<\/code> to see what assertions have been made to the power management subsystem. It uses awk to look for the IOPMAssertionTypes named <code>NoDisplaySleepAssertion<\/code> and <code>PreventUserIdleDisplaySleep<\/code> with some additional logic to throw out false positives from <code>coreaudiod<\/code>. In testing I&#8217;ve found this able to detect the presentation modes of Keynote, Powerpoint and Google Slides in Slideshow mode in Chrome (but <em>not<\/em> Safari), your mileage may vary for other apps. Another caveat is that when a YouTube video is playing in a visible tab, it will assert a  <code>NoDisplaySleepAssertion<\/code>, however these will be named &#8220;Video Wake Lock&#8221; whereas a Slideshow presentation mode will have its name assertions named &#8220;Blink Wake Lock&#8221;. So I am adding an additional check to throw our &#8220;Video Wake Locks&#8221;. This may be more of a can of worms than you&#8217;d like, if so, user education to set Focus mode may be the way to go.  A functionalized version can be found here: <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/brunerd\/macAdminTools\/blob\/main\/Scripts\/inPresentationMode.sh\" target=\"_blank\">inPresentationMode<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/sh\n#inPresentationMode (20220319) Copyright (c) 2022 Joel Bruner (https:\/\/github.com\/brunerd)\n#with code from Installomator (PR 268) (https:\/\/github.com\/Installomator\/Installomator) Copyright 2020 Armin Briegel\n#Licensed under the MIT License\n\nfunction inPresentationMode {\n\t#Apple Dev Docs: https:\/\/developer.apple.com\/documentation\/iokit\/iopmlib_h\/iopmassertiontypes\n\t#ignore assertions without the process in parentheses, any coreaudiod procs, and \"Video Wake Lock\" is just Chrome playing a Youtube vid in the foreground\n\tassertingApps=$(\/usr\/bin\/pmset -g assertions | \/usr\/bin\/awk '\/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep\/ &amp;&amp; match($0,\/\\(.+\\)\/) &amp;&amp; ! \/coreaudiod\/ &amp;&amp; ! \/Video\\ Wake\\ Lock\/ {gsub(\/^.*\\(\/,\"\",$0); gsub(\/\\).*$\/,\"\",$0); print};')\n\t&#91; -n \"${assertingApps}\" ] &amp;&amp; return 0 || return 1\n}\n\nif inPresentationMode; then\n\techo \"In presentation mode... don't be a jerk\"\nelse\n\techo \"Not in presentation mode...\"\nfi<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">All together now<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Putting it all together here&#8217;s how you can test multiple functions in a single <code>if<\/code> statement, just chain them together with a bunch of <code>||<\/code> ORs<\/p>\n\n\n\n<pre class=\"wp-block-code language-bash line-numbers\"><code>#!\/bin\/bash\n#Joel Bruner - demo of meeting\/focus aware functions for your script\n\n#pretend we've declared all the functions above and copy and pasted them in here\nfunction doNotDisturb()(:)\nfunction inMeeting_Teams()(:)\nfunction inMeeting_Zoom(){:}\nfunction inMeeting_Goto(){:}\nfunction inMeeting_Webex(){:}\nfunction inPresentationMode(){:}\n\n#test each one with || OR conditionals\n#the FIRST successful test will \"short-circuit\" and no more functions will be run\nif doNotDisturb || inPresentationMode || inZoomMeeting || inMeeting_Goto || inMeeting_Webex || inMeeting_Teams; then\n\techo \"In a meeting, presentation, or Focus is On... don't be a jerk\"\n\t#do something else, like wait \nelse\n\techo \"Not in a meeting...\"\n\t#alert the user or do whatever you needed to do that might impact them\nfi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Using these functions in your scripts can help respect your users&#8217; online meeting times and Focus states. Also it doesn&#8217;t hurt to document it somewhere and toot your own horn in a user facing KB or wiki. If and when a user complains about that pop-up that destroyed their concentration and their world, you can show them the forethought and effort you&#8217;ve taken to be as considerate as possible regarding this perceived incursion. This usually has the effect of blowing their mind \ud83e\udd2f that someone in IT is actually <em>trying<\/em> to be considerate!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">P.S. I&#8217;m pretty stoked that <a rel=\"noreferrer noopener\" href=\"https:\/\/prismjs.com\" target=\"_blank\">Prism.js<\/a> can really jazz up my normally dreary grey code blocks! \ud83d\ude0d\ud83e\udd13 I found a good WordPress tutorial <a rel=\"noreferrer noopener\" href=\"https:\/\/awhitepixel.com\/blog\/wordpress-gutenberg-custom-block-styles\/\" target=\"_blank\">here<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In four words Barbara Krueger distills the Golden Rule into an in-your-face admonishment: Don&#8217;t be a jerk. It makes for a great coffee mug but how does this relate to shell scripting for a Mac admin and engineer? Well, sometimes a script can only go so far and you need user consent and cooperation to [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,51,4,12,39],"tags":[],"class_list":["post-1104","post","type-post","status-publish","format-standard","hentry","category-bash","category-json","category-os-x","category-scripting","category-zsh"],"_links":{"self":[{"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/posts\/1104","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/comments?post=1104"}],"version-history":[{"count":36,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/posts\/1104\/revisions"}],"predecessor-version":[{"id":1149,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/posts\/1104\/revisions\/1149"}],"wp:attachment":[{"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/media?parent=1104"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/categories?post=1104"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.brunerd.com\/blog\/wp-json\/wp\/v2\/tags?post=1104"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}