Escaping many layers of cascaded single and double quotes in osx bash - bash

I use a Mac (osx sierra) and I have been learning how to use the bash. I am trying new stuff as I go along to grasp some concepts. This time I was experimenting with functions and aliases. But then I couldn't wrap my head around this problem:
I first echoed this function into the .profile to insert new aliases and functions easily into the .profile file.
function editprofile(){
echo "$#" >> ~/.profile
}
This function worked very well for some alias insertions. But after I tried to insert an alias for the script (the script below) that makes my mac sleep, I realized that the function creates some diffuculties with the cascading single and double quotes. I believe this will be the case for most scripts that uses lots of layers of single and double quotes.
osascript -e 'tell application "Finder" to sleep' && exit
The code below is me trying to use my function to insert the code above as an alias into the .profile.
editprofile 'alias _sleep=' "'" 'osascript -e' "'" 'tell application "Finder" to sleep' "'" '&& exit' "'"
The problem is that when the second script is echoed into the .profile file, I should still keep some escape characters, otherwise the code is interpretted by the bash incorrectly. I think this would also be the case with may other scripts that have this many layers of quotes, so I though I should ask if there is any way around.
P.S.
On a related note, it seems like when I type this:
function editprofile(){echo "$#" >> ~/.profile}
instead of this:
function editprofile(){
echo "$#" >> ~/.profile
}
into the .profile file, the script doesn't work. Is it because of the line breaks?

Assuming that you have a function, as opposed to an alias:
_sleep() { osascript -e 'tell application "Finder" to sleep' && exit; }
...you can emit its text with declare -f. Thus:
declare -f _sleep >>~/.profile
...or, to use your existing editprofile function:
editprofile "$(declare -f _sleep)"
The easiest approach is just that: Define the function in your local shell, then have the shell itself do the work of emitting it -- and quote that emitted content so it doesn't get field-split into individual arguments (and then have those arguments individually evaluated as globs).
If you don't want to go that route, there are approaches available; they're just varying degrees of unpleasant.
printf -v cmd_var '%q ' osascript -e 'tell application "Finder" to sleep'
...will put correctly-quoted contents into "$cmd_var". You could then:
printf -v sleep_def '_sleep() { %s && exit; }' "$cmd_var"
...which will give you a function declaration in sleep_dev that can be evaled to execute it locally, or appended to your .profile, &c.
editprofile "$sleep_def"
...will behave appropriately in that context.

Related

Unable to run command in function (shell script)

I have this function in my ~/.zshrc
async () {
if ! [[ $# -gt 0 ]];then
echo "Not enough arguments!"
fi
local busus="$IFS"
export IFS=" "
echo "$* &"
command "$* &"
export IFS="$busus"
}
and an alias
alias word='async "libreoffice --writer"'
The echo "$* &" line is used only for debugging.
When I run word, libreoffice --writer & is shown on the screen (no extra spaces or newlines), but nothing happens.
I also tried executing command libreoffice --writer & and it worked perfectly.
(My current shell is zsh)
What is wrong?
Thanks
Usually (especially in bash), the problem is that people aren't using enough double-quotes; in this case, it's the opposite: you're using too many double-quotes. The basic problem is that the command name and each of the arguments to it must be a separate "word" (in shell syntax), but double-quoting something will (usually) make the shell treat it as all one word. Here's a quick demo:
% echo foo
foo
% "echo foo"
zsh: command not found: echo foo
Here, the double-quotes make the shell treat " foo" as part of the command name, rather than as a delimiter and an argument after the command name. Similarly, when you use "$* &", the double-quotes tell the shell to treat the entire thing (including even the ampersand) as a single long word (and pass it as an argument to command). (BTW, the command isn't needed, but isn't causing any harm either.)
The standard way to do this is to use "$#" instead -- here the $# acts specially within double-quotes, making each argument into a separate word. In zsh, you could omit the double-quotes, but that can cause trouble in other shells so I recommend using them anyway.
Also, don't mess with IFS. You don't need to, and it opens a can of worms that's best left closed. And if there are no arguments, you should return immediately, rather than continuing and trying to run an empty command.
But there's another problem: in the alias, you double-quote "libreoffice --writer", which is going to have pretty much the same effect again. So remove those double-quotes. But keep the single-quotes around the alias, so it'll be defined as a single alias.
So here's my proposed correction:
async () {
if ! [[ $# -gt 0 ]];then
echo "Not enough arguments!"
return 1 # Do not continue if there's no command to run!
fi
echo "$* &" # Here quoting is appropriate, so it's a single argument to echo
"$#" &
}
alias word='async libreoffice --writer'
Using "$#" directly is more reliable:
async () { [ "$#" -gt 0 ] && "$#" & }
alias word='async libreoffice --writer'

Proper formatting of Command Substitution

I am trying to add the currently playing song if there is one to my ZSH prompt. I'm using a JXA command osascript -l JavaScript -e "Application('Music').currentTrack.name()". I am trying to assign it to a variable. and then echo that command.
precmd() {
SONG=$( echo -e osascript -l JavaScript -e "Application('Music').currentTrack.name()" )
LEFT=echo $SONG
RIGHT="$(dracula_time_segment) $(battery_pct_prompt)"
RIGHTWIDTH=$(($COLUMNS-${#LEFT}))
}
I've tried a number of variations eg: echo inside and outside the expression and various flags.
You don't need echo at all, as demonstrated by your later command substitutions when setting RIGHT; command substitution just takes a command and executes it.
SONG=$(osascript -l JavaScript -e "Application('Music').currentTrack.name()")
LEFT="$SONG"
You could combine the previous two commands; SONG isn't needed.
LEFT=$(osascript ...)

How to programatically paste to terminal without executing?

Is it possible to type / paste something to the console without executing the command? Something that would emulate the normal Ctrl / Cmd + c, Ctrl / Cmd + v where the text is put on the current line without being executed so the user can continue typing, deleting, etc.
For example I have the following simple script:
#!/bin/bash
echo -n "foo" | pbcopy
pbpaste
Now when I run this, it just echoes foo% and goes on the next line.
Also, even if this would work, I would prefer a solution that works on both mac os and ubuntu (as far as I know pbcopy does not come pre-installed on all linux distros).
EDIT
Edit to explain the scenario better. Imagine the script above is called foo.sh. You run the script ./foo.sh and when it finishes you have a new prompt with only the text "foo"|. The | represents the cursor.
So... You already know about pbcopy and pbpaste in macOS. You probably want to know about xclip, which is a similar interface to X selections ("the clipboard") from the command line.
Each system (Aqua and X) handles clipboard data structures differently, and I'm not aware of any single tool which will function this way on both platforms. That said, you can perhaps write scripts that are portable between both systems:
#!/usr/bin/env bash
if type xclip >/dev/null; then
clip_copy="xclip"
clip_paste="xclip -o"
elif type pbcopy >/dev/null ; then
clip_copy="pbcopy"
clip_paste="pbpaste"
else
echo "ERROR: no clipboard functions. Where am I?" >&2
exit 1
fi
Also note that pbcopy/pbpaste support different data types, whereas xclip just deals with text.
That said, both of these functions deal with stdin and stdout. If you want to actually simulate keypresses (as your edit appears to imply), you need another tool.
In the Mac world, a number of options exist. Cliclick works well for me. This tool has full mouse support, but also has an option t:, which will simulate keyboard input. It seems reasonable that one might cliclick t:"$(pbpaste)", though I've never tried it.
You can also use AppleScript to print arbitrary text:
$ osascript -e 'tell application "System Events" to keystroke "Hello world."'
In X, xdotool seems to work.
$ xdotool type "Hello world."
To make a script which might run in both macOS and X environments, you could key on the output of uname -s:
#!/usr/bin/env bash
case "$(uname -s)" in
Darwin)
clip_copy="pbcopy" # note: $clip_copy isn't used in this script.
clip_paste="pbpaste"
type_cmd="osascript -e 'tell application \"System Events\" to keystroke \"%s\"'"
;;
*)
clip_copy="xclip"
clip_paste="xclip -o"
type_cmd='xdotool type "%s"'
;;
esac
text="$($clip_paste)"
printf "$type_cmd" "${text//[!A-Za-z0-9. ]/}" | sh
Note: untested. YMMV. May contain nuts.
Based on a gist in the comments I found a thread on stack exchange that helped me answer my own question: https://unix.stackexchange.com/a/213821.
I am only interested in supporting sh, bash, zsh so the following solution works fine:
#!/bin/bash
if [ "$(echo $ZSH_VERSION)" ]; then
print -z $#
else
# assume bash or sh
bind '"\e[0n": "'"$*"'"'; printf '\e[5n'
fi
Call with source inject.sh echo foo

What does ${1-1} in bash mean?

I'm reading the scripts from here and trying to understand what's going on. This function performs changing the directory of a Finder window:
function ee {
osascript -e 'set cwd to do shell script "pwd"'\
-e 'tell application "Finder"'\
-e "if (${1-1} <= (count Finder windows)) then"\
-e "set the target of window ${1-1} to (POSIX file cwd) as string"\
-e 'else' -e "open (POSIX file cwd) as string"\
-e 'end if' -e 'end tell';\
};\
I'm assuming the $ is interpreted by bash, since it's inside double-quotes. I haven't been able to find what could {1-1} mean. I've played with the expression in separate test scripts but couldn't find a difference from plain $1. Any ideas?
This means that if argument 1 (${1}) is not set, it will be set to 1.
See parameter substitution here.
${parameter-default}, ${parameter:-default}
If parameter not set, use default.

General function for suppressing output into bash -- spaced filenames

I would like to write bash function to suppress output into command line in bash.
I have included a following into the $HOME/.bashrc
# suspend output to the terminal
noout(){
$* &>/dev/null &
}
And as an example I have created alias for evince:
alias evince='noout evince'
This works just fine for files without spaces in the file names. However if I launch something like:
evince Jack\ London\ -\ The\ Star\ Rover.pdf
Bash splits the file names into several bits and evince opens several empty windows.
Thanks for any help to make it working.
Try this:
noout() {
"$#" >/dev/null 2>&1 &
}
I'm not sure why you want to do it in the background, but that's your choice. The relevant aspect of my answer is the quotes and the use of $# instead of $*. See also what does "$#" mean in a shell script.

Resources