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 08 '19 at 00:13

2 Answers2

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.

cas
  • 78,579
  • 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 08 '19 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 08 '19 at 10:49
  • 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 08 '19 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 08 '19 at 11:27