zsh: command substitution, proper quoting and backslash (again) - zsh

(Note: This is a successor question to my posting zsh: Command substitution and proper quoting , but now with an additional complication).
I have a function _iwpath_helper, which outputs to stdout a path, which possibly contains spaces. For the sake of this discussion, let's assume that _iwpath_helper always returns a constant text, for instance
function _iwpath_helper
{
echo "home/rovf/my directory with spaces"
}
I also have a function quote_stripped expects one parameter and if this parameter is surrounded by quotes, it removes them and returns the remaining text. If the parameter is not surrounded by quotes, it returns it unchanged. Here is its definition:
function quote_stripped
{
echo ${1//[\"\']/}
}
Now I combine both functions in the following way:
target=$(quote_stripped "${(q)$(_iwpath_helper)}")
(Of course, 'quote_stripped' would be unnecessary in this toy example, because _iwpath_helper doesn't return a quote-delimited path here, but in the real application, it sometimes does).
The problem now is that the variable target contains a real backslash character, i.e. if I do a
echo +++$target+++
I see
+++home/rovf/my\ directory\ with\ spaces
and if I try to
cd $target
I get on my system the error message, that the directory
home/rovf/my/ directory/ with/ spaces
would not exist.
(In case you are wondering where the forward slashes come from: I'm running on Cygwin, and I guess that the cd command just interprets backslashes as forward slashes in this case, to accomodate better for the Windows environment).
I guess the backslashes, which physically appear in the variable target are caused by the (q) expansion flag which I apply to $(_iwpath_helper). My problem is now that I can not simply drop the (q), because without it, the function quote_stripped would get on parameter $1 only the first part of the string, up to the first space (/home/rovf/my).
How can I write this correctly?

I think you just want to avoid trying to strip quotes manually, and use the (Q) expansion flag. Compare:
% v="a b c d"
% echo "$v"
a b c d
% echo "${(q)v}"
a\ b\ c\ d
% echo "${(Q)${(q)v}}"
a b c d

chepner was right: The way I tried to unquote the string was silly (I was thinking too much in a "Bourne Shell way"), and I should have used the (Q) flag.
Here is my solution:
target="${(Q)$(_iwpath_helper)}"
No need for the quote_stripped function anymore....

Related

Why is ZSH preforming string editing with`:r` present in a string [duplicate]

I try to run code under zsh
a=123
b="$a:r"
echo $b
which I want the result to be 123:r, but it turns out to be
123 # without ":r"
And the same thing for character t, q.
However, if I run it under bash, it brings me the desired result 123:r.
If I add {}, runs
a=123
b="${a}:r"
echo $b
which also brings the desired result.
Does anybody know what's going on here?
In zsh, "$a:r" is the same as "${a:r}" by default.
To quote from the documentation (Emphasis added):
${name}
The value, if any, of the parameter name is substituted. The braces are required if the expansion is to be followed by a letter, digit, or underscore that is not to be interpreted as part of name. In addition, more complicated forms of substitution usually require the braces to be present; exceptions, which only apply if the option KSH_ARRAYS is not set, are a single subscript or any colon modifiers appearing after the name, or any of the characters ‘^’, ‘=’, ‘~’, ‘#’ or ‘+’ appearing before the name, all of which work with or without braces.
The :r modifer means:
Remove a filename extension leaving the root name. Strings with no filename extension are not altered. A filename extension is a ‘.’ followed by any number of characters (including zero) that are neither ‘.’ nor ‘/’ and that continue to the end of the string. For example, the extension of ‘foo.orig.c’ is ‘.c’, and ‘dir.c/foo’ has no extension.
To disable this behavior:
$ setopt KSH_ARRAYS
(Note: Doing this on my setup actually causes zsh to segfault; the option changes behavior in multiple ways, one of which conflicts badly with something in my .zshrc. Your results may vary.)

zsh parameter expansion with asterisk as variable does not work

I am trying to remove a file extension using parameter expansion. e.g. given a filename of 123.sh, return 123.
If I store the pattern, ".*" in a variable, it does not work —${filename%$suffix} does not work.
If I specify the pattern literally, it does — ${filename%.*}
What am I doing wrong?
In the expansion ${filename%$suffix}, the value of $suffix is substituted literally. To have it be substitute as a pattern instead, you will need to use glob substitution: ${filename%$~suffix}
However, none of this is necessary for what you're trying to do. To remove the extension from a filename, you can simply use the r modifier:
❯ filename="123.sh"
❯ print $filename:r
123

filename expansion on assigning a non-array variable

This is about Zsh 5.5.1.
Say I have a glob pattern which expands to exactly one file, and I would like to assign this file to a variable. This works:
# N: No error if no files match. D: Match dot files. Y1: Expand to exactly one entry.
myfile=(*(NDY1))
and echo $myfile will show the file (or directory). But this one does not work:
myfile=*(NDY1)
In the latter case, echo $myfile holds the pattern, i.e. *(NDY1).
Of course I could do some cheap trick, such as creating a chilprocess via
myfile=$(echo *(NDY1))
but is there a way to do the assinment without such tricks?
By default, zsh does not do filename expansion in scalar assignment, but the option GLOB_ASSIGN could help. (This option is provided as for backwards compatibility only.)
local myfile=''
() {
setopt localoptions globassign
myfile=*(NDY1)
}
echo $myfile
;#>> something
Here are some descriptions in zsh docs:
The value of a scalar parameter may also be assigned by writing:
name=value
In scalar assignment, value is expanded as a single string, in which the elements of arrays are joined together; filename expansion is not performed unless the option GLOB_ASSIGN is set.
--- zshparam(1), Description, zsh parameters
GLOB_ASSIGN <C>
If this option is set, filename generation (globbing) is performed on the right hand side of scalar parameter assignments of the form 'name=pattern (e.g. foo=*'). If the result has more than one word the parameter will become an array with those words as arguments. This option is provided for backwards compatibility only: globbing is always performed on the right hand side of array assignments of the form name=(value) (e.g. foo=(*)) and this form is recommended for clarity; with this option set, it is not possible to predict whether the result will be an array or a scalar.
--- zshoptions(1), GLOB_ASSIGN, Expansion and Globbing, Description Of Options, zsh options

Zsh: append an element a dynamically-named associative array

I have several associative arrays,
typeset -A first
typeset -A second
# …
and want to add an element (e.g. [a]=b) to one of them, but cannot find the right syntax. Started at
if something; then
arr=first
else
arr=second
fi
$arr[a]+=b # bad
$arr+=([a]=b) # bad
and have tried many mixes of quotation marks, braces, various subscripts and parameter expansion flags, arr=$first/arr=$second, etc. Everything I've tried is an error. Can it be done?
I don't think this is ideal but it should get the job done.
export "${arr}[a]"=b
This should also work in many cases:
eval "${arr}[a]=b"
eval has a bad reputation (often deservedly so), but with simple substitutions like those being used here, it should be fine.

A operator to system() in R

I would like to make R a little bit easier to execute system command. Something like ipython vs python. Here are some thoughts:
Define cd function to change working directory by wrapping up the getwd and setwd
Define an operator to wrap up the system() command so that I can run something like "$ls" to replace the system("ls")
The first one is easy to accomplish. However, I am stuck with the second one. I found no ways to redefine an operator in R for a string. Then I took a step back, I tried to define a sys(param). But now, I still need to input the quotation marks. e.g. I need to run sys("ls") instead of sys(ls) to list the directory. Is there a way to make the parameter assume it is a string even without the quotation marks? Thanks. Any suggestions are welcome.
Updated to simplify functions (remove a regexp) and add support for character input
You can use match.call inside a function so that you can call the function without using quotation marks like this.
sys <- function(...) {
command <- match.call()[[2L]]
if (!is.character(command)) {
command <- gsub("- ", "-", deparse(command))
}
system(command)
}
Now, either of the following are equivalent to system("ls -a")
sys("ls -a")
sys(ls -a)
The sys function above extracts the second component of the call which is the stuff between the parentheses. i.e. ls -a or "ls -a" in these examples. It then passes that to system (through deparse first if it is not character)
[I added support for strings because otherwise it doesn't work with forward slashes, dots, etc. For example, sys(ls /home) does not work, but sys("ls /home") does.]
However, this still requires using parentheses :-(
To avoid the use of parentheses, you can mask an operator. In the initial version of this answer, I showed how to mask ! which is not a good a idea. You suggested using ? in the comments which could be done like this.
`?` <- function(...) {
command <- match.call()[[2L]]
if (!is.character(command)) {
command <- gsub("- ", "-", deparse(command))
}
system(command)
}
Now, this is the same as system("ls -a -l")
?ls -a -l
But, if you need to use forward slashes, you'd have to use quotes like this
?"ls /home"
Alternatively, you could create a special binary operator
"%sys%" <- function(...) {
system(sub("%sys%", "", deparse(match.call())))
}
You can use it like this
ls %sys% -l
ls %sys% .
If you need to use forward slashes, you have to quote the right side
ls %sys% "/home"
A downside is that it requires exactly one argument on the right side of the operator.

Resources