ZSH: Escape or quote a string with backslashes - macos

I wrote a little function, that "translates" a Windows path to a OSX path and opens it in the Finder. The function works perfectly with bash, but not with zsh (I use oh-my-zsh).
The problem is that it parses specific backslash combinations, for instance: \f, \a, \01, \02, \03, etc...
For example, this path string is the input:
"\60_Project\6038_Projekt_Part\05_development\assets\img\facebook"
After the translation function, the \f sequence (from img\facebook) is incorrectly translated as whitespace, producing the output:
"/60_Project/6038_Project_Part_developmentssets/img
acebook"
My goal is to just paste in a Windows path and not have to manually change anything.
How can I escape or quote a string with zsh, to get the result I want?
Here is the code I wrote:
function parsewinpath {
echo $1 | sed -e 's/\\/\//g'
}
function openwinpath {
echo "Opening..."
open $(parsewinpath "/Volumes/myvolume$1")
}
Usage:
openwinpath '\60_Project\6038_Project_Part\05_development\assets\img\facebook'
The result should be that the Finder opens:
/Volumes/myvolume/60_Project/6038_Project_Part/05_development/assets/img/facebook

You don't need parsewinpath at all. Just use parameter expansion to replace backslashes with forward slashes.
openwinpath /Volumes/myvolume${1//\\//}

The problem is that echo is trying to interpret escape sequences in the string as it prints it. Some versions of echo do this; some do it only if you pass the -e option; some print "-e" as part of their output; some do ... other random things. Basically, if you give echo something that contains escapes and/or starts with "-", there's no telling what it'll do.
Option 1: Use printf instead. It's a little more complicated, because you have to give it a format string as well as the actual string to be printed, but it's much more predictable. Oh, and double-quote variable references:
function parsewinpath {
printf '%s\n' "$1" | sed -e 's/\\/\//g'
}
Option 2: As #chepner pointed out, you can just skip echo, sed, and the whole mess, and use a parameter expansion to do the job:
function openwinpath {
echo "Opening..."
open "/Volumes/myvolume${1//\\//}"
}

Just escape each backslash with another backslash:
openwinpath '\\60_Project\\6038_Project_Part\\05_development\\assets\\img\\facebook'

Sorry, I know I'm 5 years late, but I thought an explanation of the problem's root and a workaround might be worth it for anyone else who ends up here:
Bash has a certain syntax. It interprets backslashes in a certain way. So you can't paste text with backslashes into the Terminal.
However, if you've copied the text to the clipboard, you may be able to circumvent bash's syntax by using a shell command to read the clipboard inside your script. So instead of using $1 to get your path from your script's argument, use pbpaste to read the clipboard directly.

Related

Bash remove every occurrence after first in string

I'm trying to remove everything after a specific_string in a path string in Bash. I've tried using sed to no avail so far.
variable="specific_string"
input_string="/path/to/some/specific_string/specific_string.something/specific_string.something-else"
output=$(sed 's/$variable//' $input_string)
Output should be "/path/to/some/specific_string/"
Would be better if I didn't have to use commands such as sed!
The Problems
There are many problems
Variables are not evaluated inside single quotes. 's/$variable//' will be treated as a literal string, which does not contain specific_string
sed can modify text from files or STDIN, but not text given via parameters. With sed 's/...//' $input_string the /path/to/some/specific_string/.../file is opened and its content is read, instead of the path itself.
s/string// deletes only string, not the words afterwards.
Also remember to double quote your variables. cmd $variable is dangerous if the variable contains spaces. cmd "$variable" is safe.
Sed Solution
output="$(sed "s/$variable.*/$variable/" <<< "$input_string")"
GNU Grep Solution
output="$(grep -Po "^.*?$variable" <<< "$input_string")"
Pure Bash Solution
output="${input_string%%$variable*}$variable"
If you want to remove everything after "specific_string" it will remove the "/" also as it does with the following example:
output=$(echo $input_string|sed "s/${variable}.*$/${variable}/")
try with simple sed:
variable="specific_string"
input_string="/path/to/some/specific_string/specific_string.something/specific_string.something-else"
output=$(echo "$input_string" | sed "s/\(.*$variable\/\).*/\1/")
Output of variable output will be as follows.
echo $output
/path/to/some/specific_string/

removing backslash with tr

So Im removing special characters from filenames and replacing with spaces. I have all working apart from files with single backslashes contained therein.
Note these files are created in the Finder on OS X
old_name="testing\this\folder"
new_name=$(echo $old_name | tr '<>:\\#%|?*' ' ');
This results in new_name being "testing hisolder"
How can I just removed the backslashes and not the preceding character?
This results in new_name being "testing hisolder"
This string looks like the result of echo -e "testing\this\folder", because \t and \f are actually replaced with the tabulation and form feed control characters.
Maybe you have an alias like alias echo='echo -e', or maybe the implementation of echo in your version of the shell interprets backslash escapes:
POSIX does not require support for any options, and says that the
behavior of ‘echo’ is implementation-defined if any STRING contains a
backslash or if the first argument is ‘-n’. Portable programs can use
the ‘printf’ command if they need to omit trailing newlines or output
control characters or backslashes.
(from the info page)
So you should use printf instead of echo in new software. In particular, echo $old_name should be replaced with printf %s "$old_name".
There is a good explanation in this discussion, for instance.
No need for printf
As #mklement0 suggested, you can avoid the pipe by means of the Bash here string:
tr '<>:\\#%|?*' ' ' <<<"$old_name"
Ruslan's excellent answer explains why your command may not be working for you and offers a robust, portable solution.
tl;dr:
You probably ran your code with sh rather than bash (even though on macOS sh is Bash in disguise), or you had shell option xpg_echo explicitly turned on.
Use printf instead of echo for portability.
In Bash, with the default options and using the echo builtin, your command should work as-is (except that you should double-quote $old_name for robustness), because echo by default does not expand escape sequences such as \t in its operands.
However, Bash's echo can be made to expand control-character escape sequences:
explicitly, by executing shopt -s xpg_echo
implicitly, if you run Bash as sh or with the --posix option (which, among other options and behavior changes, activates xpg_echo)
Thus, your symptom may have been caused by running your code from a script with shebang line #!/bin/sh, for instance.
However, if you're targeting sh, i.e., if you're writing a portable script, then echo should be avoided altogether for the very reason that its behavior differs across shells and platforms - see Ruslan's printf solution.
As an aside: perhaps a more robust approach to your tr command is a whitelisting approach: stating only the characters that are explicitly allowed in your result, and excluding other with the -C option:
old_name='testing\this\folder'
new_name=$(printf '%s' "$old_name" | tr -C '[:alnum:]_-' ' ')
That way, any characters that aren't either letters, numbers, _, or - are replaced with a space.
With Bash, you can use parameter expansion:
$ old_name="testing\this\folder"
$ new_name=${old_name//[<>:\\#%|?*]/ }
$ echo $new_name
testing this folder
For more, please refer to the Bash manual on shell parameter expansion.
I think your test case is missing proper escaping for \, so you're not really testing the case of a backslash contained in a string.
This worked for me:
old_name='testing\\this\\folder'
new_name=$(echo $old_name | tr '<>:\\#%|?*' ' ');
echo $new_name
# testing this folder

proper syntax for the s command along to the addressing in sed

I want to issue this command from the bash script
sed -e $beginning,$s/pattern/$variable/ file
but any possible combination of quotes gives me an error, only one that works:
sed -e "$beginning,$"'s/pattern/$variable/' file
also not good, because it do not dereferences the variable.
Does my approach can be implemented with sed?
Feel free to switch the quotes up. The shell can keep things straight.
sed -e "$beginning"',$s/pattern/'"$variable"'/' file
You can try this:
$ sed -e "$beginning,$ s/pattern/$variable/" file
Example
file.txt:
one
two
three
Try:
$ beginning=1
$ variable=ONE
$ sed -e "$beginning,$ s/one/$variable/" file.txt
Output:
ONE
two
three
There are two types of quotes:
Single quotes preserve their contents (> is the prompt):
> var=blah
> echo '$var'
$var
Double quotes allow for parameter expansion:
> var=blah
> echo "$var"
blah
And two types of $ sign:
One to tell the shell that what follows is the name of a parameter to be expanded
One that stands for "last line" in sed.
You have to combine these so
The shell doesn't think sed's $ has anything to do with a parameter
The shell parameters still get expanded (can't be within single quotes)
The whole sed command is quoted.
One possibility would be
sed "$beginning,\$s/pattern/$variable/" file
The whole command is in double quotes, i.e., parameters get expanded ($beginning and $variable). To make sure the shell doesn't try to expand $s, which doesn't exist, the "end of line" $ is escaped so the shell doesn't try anything funny.
Other options are
Double quoting everything but adding a space between $ and s (see Ren's answer)
Mixing quoting types as needed (see Ignacio's answer)
Methods that don't work
sed '$beginning,$s/pattern/$variable/' file
Everything in single quotes: the shell parameters are not expanded (doesn't follow rule 2 above). $beginning is not a valid address, and pattern would be literally replaced by $variable.
sed "$beginning,$s/pattern/$variable/" file
Everything in double qoutes: the parameters are expanded, including $s, which isn't supposed to (doesn't follow rule 1 above).
the following form worked for me from within script
sed $beg,$ -e s/pattern/$variable/ file
the same form will also work if executed from the shell

Do zsh or bash have quotes that are convenient for English text?

If I use single quotes, words with apostrophes ("don't") are annoying to escape:
'Don'"'"'t do that'
If I use double quotes, dollar signs and exclamation points trip it up:
"It cost like \$1000\!"
Is there another kind of quoting I can use?
edit: I should also add that I would like to pass this string directly as a command line argument, rather than storing it in a variable. To this end I tried, using DigitalRoss's solution,
$ echo "$(cat << \EOF
Don't $worry be "happy".
EOF)"
but get
dquote cmdsubst>
after hitting enter :/ . So at this point ZyX's suggestion of setopt rcquotes looks to be the most convenient.
With zsh you may do
setopt rcquotes
. Then ASCII apostrophes are escaped just like this:
echo 'Don''t'
. Or setup your keymap to be able to enter UTF apostrophes, they have no issues with any kind of quotes (including none) in any shell:
echo 'Don’t'
. Third works both for zsh and bash:
echo $'Don\'t'
.
Neither first nor third can narrow down quote to a single character, but they are still less verbose for non-lengthy strings then heredocs suggested above. With zsh you can do this by using custom accept-line widget that will replace constructs like 'Don't' with 'Don'\''t'. Requires rather tricky regex magic that I can write only in perl; and is probably not the best idea as I can’t pretend I can cover all possible cases before they will hit. It won’t in any case touch any scripts.
I like the direction Zsolt Botykai is going. Here is an example that works with any Posix shell. (I also verified that it survived its paste into the SO server.)
$ read -r x << \EOF
Don't $worry be "happy".
EOF
$ echo $x
Don't $worry be "happy".
The things that make this work;
-r will make \ not be magic
the \EOF instead of just EOF makes $ not be magic
If you want to assign a quoted text to a variable, you still can use heredocs, like (and it can be a multiline text too):
read -r -d '' VAR <<'ENDOFVAR'
This "will be" a 'really well' escaped text. Or won't.
ENDOFVAR
Bash syntax $'string' is another quoting mechanism which allows ANSI C-like escape sequences and do expansion to single-quoted version.
$> echo $'Don\'t $worry be "happy".'
Don't $worry be "happy".
See https://stackoverflow.com/a/16605140/149221 for more details.

bash: capture '\*' in a command line

function abc ()
{
echo "You typed exactly this: $1"
}
Now run it:
myprompt$ abc abc\*
And I get:
You typed exactly this: abc*
I'm writing a function in which I need to capture the entire argument, including the backslash, for future use. Can it be done? I've tried every combination of quotes and 'set's and nothing keeps the backslash there. I know I can escape it, but then the argument as typed would not be identical to the argument as echoed. Note that you get the argument back perfectly via 'history'. How can I capture it inside my function, backslash and asterisk and all?
The shell interprets the \ character on the command line as an escape character that removes any special meaning from the following character. In order to have a literal \ in your command line, you need to persuade the shell to ignore the special meaning of \ which you do by putting a \ before it. Like this:
myprompt$ abc abc\\\*
Notice there are three \ characters. The first tells the shell to ignore the special meaning of the following character - which is the second \. The third \ tells the shell to ignore the special meaning of the *.
Another way to persuade the shell not to interpret the \ as an escape character is to put your argument in single quotes. Like this:
myprompt$ abc 'abc\*'
You can't get the arguments exactly as typed. Bash evaluates them before your function ever sees them. You'll have to escape or quote it.
abc abc\\*
abc 'abc\*'
You could always take the shotgun vs fly approach and implement your own shell. :)
Or tone it down a bit and find a shell that supports the input mechanism you want.
Note that you would have to change your login settings to utilize a "verbatim-shell".
All,
It looks like it is possible to capture an exact command line within a function.
As I suspected, 'history' gives us a way:
function exact_line ()
{
str1=`history 1`
str2=($str1)
str3=
# This isn't very elegant, all I want to do is remove the
# line count from the 'history' output. Tho this way I do
# have surplus spaces removed as well:
for ((i=1; ; i++))
do
str3="$str3 ${str2[$i]}"
if [ -z ${str2[$i]} ]; then break; fi
done
echo -e "Your exact command line was:\n\n$str3"
}
Thoughts? Can this be improved?

Resources