0

I am currently trying to write a function for my bash profile which causes an OSX LaunchAgent passed as argument to restart. It might be a bit pointless but for one I want to stop having to write long paths for launchctl un/load

I have got almost everything working, except for the the final calls to launchctl. Here's the function script and the consequent error messages:

function

launchctl_mgr() {
        failure() {
                local lineno=$1
                local msg=$2
                echo "Failed at $lineno: $msg"
        }
        trap 'failure ${lineno} "$BASH_COMMAND"' ERR
        trap 'trap - ERR' RETURN

        instruction=$1
        filepath=$2;
        #test whether the file argument is already a file or not and whether or not it is actually$
        if [[ ! -f $2 ]]
        then
                if [[ -f ~/Library/LaunchAgents/$2.plist ]]
                then
                        filepath="~/Library/Launchagents/$filepath.plist"
                        echo "found!: $filepath"
                elif [[ -f /Library/LaunchAgents/$2.plist ]]
                then
                        filepath="/Library/LaunchAgents/$filepath.plist"
                else
                        echo "debug: ~/Library/LaunchAgents/$filepath.plist"
                        echo "Filename supplied was not valid as a launchagent"
                        return 1
                fi
        fi
        if [[ $instruction=="restart" || instruction=="unload" ]]
        then
                echo "instruction is $instruction"
                echo "filepath is $filepath"
                launchctl stop $filepath
                launchctl unload $filepath
                if [[ instruction=="restart" ]]
                then
                        launchctl load $filepath
                        launchctl start $filepath
                fi
        fi
}

Output/errors

____________________ => launchctl_mgr unload local.unminimise
found!: ~/Library/Launchagents/local.unminimise.plist
instruction is unload
filepath is ~/Library/Launchagents/local.unminimise.plist
Failed at launchctl stop $filepath:
/Users/[current_user]/~/Library/Launchagents/local.unminimise.plist: No such file or directory
/Users/[current_user]/~/Library/Launchagents/local.unminimise.plist: No such file or directory
Failed at launchctl start $filepath:

('[current_user]' replaces my actual user account name)

As you can see for launchctl, whether it is bash or launchctl the current directory path and the path string I had fed launchctl are concatenated together, causing an error. The filepath string is printed and looks ok in the line directly before

  • you need spaces before and after the == in your [[ ... ]] tests (otherwise they're just evaluated as strings, and non-empty is true. The string $instruction=="restart" is non-empty and will always evaluate as true. $instruction == "restart" is an equivalence test and will evaluate to either true or false depending on the value in $instruction). You've also missed the $ before instruction == in a few places. – cas Sep 8 at 0:13
1
  1. You need spaces before and after the == in your [[ ... ]] tests, otherwise they're just evaluated as strings, and non-empty is always true.

    For example:

    $ instruction=whatever
    $ [[ $instruction=="restart" ]] && echo true || echo false
    true
    

    This is equivalent to:

    $ [[ whatever=="restart" ]] && echo true || echo false
    true
    

    It's also equivalent to the following (and several other variations):

    $ [[ -n 'whatever=="restart"' ]] && echo true || echo false
    true
    

    In all these cases, you're not checking whether $instruction is equal to "restart". Instead, you're testing whether the string whatever=="restart" is non-empty. The string happens to contain the character sequence == but that's not significant. Embedded within the string, they're just characters with no special meaning.

    Compare with:

    $ instruction=whatever
    $ [[ $instruction == "restart" ]] && echo true || echo false
    false
    $ instruction=restart
    $ [[ $instruction == "restart" ]] && echo true || echo false
    true
    

  1. There are also a few places in your script where the $ before a variable is missing. e.g.

    if [[ $instruction=="restart" || instruction=="unload" ]]
    

    This needs the spaces before and after == and a $ before the second instruction:

    if [[ $instruction == "restart" || $instruction == "unload" ]]
    

    Same here:

    if [[ instruction=="restart" ]]
    

    That should be:

    if [[ $instruction == "restart" ]]
    

  1. You need to double-quote variables (and shell positional parameters) when you use them. e.g.

    local lineno="$1"
    local msg="$2"
    

    and:

    instruction="$1"
    filepath="$2"
    

    and:

    launchctl stop "$filepath"
    launchctl unload "$filepath"
    

  1. Why write this as a function rather than a stand-alone script? Do you need it to change the current shell's environment?

    There's nothing wrong with writing functions but unless you want it to change the current shell's environment, you're better off with the isolation provided by a separate script, so you don't accidentally change the environment.

    Or declare all your function variables (incl. $instruction and $filepath) as local but that's only good for protecting against changes to variables, a function can still change other things, like the shell's current directory.

  • That's really comprehensive, thankyou for taking the time to help me. I modified it based upon your tips, but unfortunately the concatenation still happens on my machine. It think it is probably something obvious that I've missed but I can't see it. I have tried set -x to see if there is a problem but it doesn't really show anything meaningful – Scott Anderson Sep 8 at 10:47
  • Also I am keen to take on board your tips about translating functions to standalone scripts if it is possible to still call them in my normal shell via .bash_profile, is there any resource I can consult about doing so? I don't really want to litter my home directory with .sh files or otherwise put my scripts somewhere I'll forget about them – Scott Anderson Sep 8 at 10:49
  • 1. from reading the launchctl man page, it looks as if it already searches the appropriate paths. i.e. there's no need to pre-pend a path. try not doing that. 2. see In Bash, when to alias, when to script, and when to write a function? – cas Sep 8 at 10:54
  • There's also a case of How Can I Expand A Tilde ~ As Part Of A Variable? in filepath="~/Library/Launchagents/$filepath.plist" I think? – steeldriver Sep 8 at 11:27
1

Big thanks to Cas for his answer and help with bash syntax but I have discovered from some tinkering that the tilde in ~/Library/LaunchAgents/[agent] is what is causing the problem in actually getting the script to work as intended and not throw out a concatenated path.

For some reason the tilde does not get expanded in the launchctl address parameter, so if anyone else is experiencing this problem use:

$HOME/Library/LaunchAgents/[agent]

to address your agent in a launchctl command.

This path is as described here under 'legacy commands' then 'load'

N.B: for my script at least this doesn't actually resolve launchctl throwing errors, but does fix functionality. Despite the documentation saying:

Due to bugs in the previous implementation and long-stand- ing client expectations around those bugs, the load and unload subcommands will only return a non-zero exit code due to improper usage. Otherwise, zero is always returned.

..the ERR trap I have set up consistently throws errors without message, even though the script is working as intended and replicating the commands manually does not give any error message.

Perhaps someone can remedy my ignorance but if you are still head-scratching after my suggestion try launchctl list | grep [agent_name] to see if you have had the intended effect

  • 1
    yeah. i must admit i got distracted by all the errors in your script and forgot about the actual tilde problem....didn't want to even look at that until you had a script that could work. – cas Sep 10 at 11:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.