passing string with spaces to node cli-parser - yargs

var minimist = require("minimist")
const a = minimist(`executable --param "a b"`.split(' '))
console.log(a)
https://runkit.com/embed/57837xcuv5v0
actual output:
Object {_: ["executable", "b\""], param: "\"a"}
expected output:
Object {_: ["executable"], param: "a b"}
I'm also seeing same result when using yargs and commander.
It's strange because jest is using yargs and jest accept the following command: jest -t "test name with spaces"

Based on your example code, the problem is your preparation of the string array which has broken up the string-with-spaces before the parser gets to see it:
$ node -e 'console.log(`executable --param "a b"`.split(" "))'
[ 'executable', '--param', '"a', 'b"' ]
A simple fix when manually setting up the arguments is to construct the array of parameters yourself, instead of using a string and split, like:
$ node -e 'console.log(["executable", "--param", "a b"])'
[ 'executable', '--param', 'a b' ]
or
const a = minimist(['executable', '--param', 'a b'])
If what you need to do is break up a single string into arguments like the shell does, that is not done by Commander, or Yargs, or minimist.
You could look at https://www.npmjs.com/package/shell-quote which has a parse command.

Related

Bash how to add conditional quoted argument '--pull "always"' to docker command

I am trying to conditionally add arguments to a docker call in a bash script but docker says the flag is unknown, but I can run the command verbatim by hand and it works.
I have tried a few strategies to add the command, including using a string instead of an array, and I have tried using a substitution like the solution here ( using ${array[#]/#/'--pull '} ): https://stackoverflow.com/a/68675860/10542275
docker run --name application --pull "always" -p 3000:3000 -d private.docker.repository/group/application:version
This bash script
run() {
getDockerImageName "/group" "$PROJECT_NAME:$VERSION" "latest";
local imageName=${imageName};
local additionalRunParameters=${additionalRunParameters};
cd "$BASE_PATH/$PROJECT_NAME" || exit 1;
stopAnyRunning "$PROJECT_NAME";
echo docker run --name "$PROJECT_NAME" \
"${additionalRunParameters[#]}" \
-p 3000:3000 \
-d "$imageName";
// docker run --name application --pull "always" -p 3000:3000 -d private.docker.repository/group/application:version
docker run --name "$PROJECT_NAME" \
"${additionalRunParameters[#]}" \
-p 3000:3000 \
-d "$imageName";
//unknown flag: --pull "always"
}
The helper 'getDockerImageName'
# Gets the name of the docker image to use for deploy.
# $1 - The path to the image in the container registry
# $2 - The name of the image and the tag
# $3 - 'latest' if the deploy should use the container built by CI
export imageName="";
export additionalRunParameters=();
getDockerImageName() {
imageName="group/$2";
if [[ $3 == "latest" ]]; then
echo "Using docker image from CI...";
docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "https://$DOCKER_BASE_URL";
imageName="${DOCKER_BASE_URL}${1}/$2";
additionalRunParameters=('--pull "always"');
fi
}
Don't put code (such as arguments) in a variable. Basically, use an array is good, and you are almost doing that. This line -
local additionalRunParameters=${additionalRunParameters};
is probably what's causing you trouble, along with
additionalRunParameters=('--pull "always"');
which is embedding the spaces between what you seem to have meant to be two operands (the option and its argument), turning them into a single string that is an unrecognized garble. //unknown flag: --pull "always" is telling you the flag it's parsing is --pull "always", which is NOT the --pull flag docker DOES know, followed by an argument.
Also,
export additionalRunParameters=(); # nope
arrays don't really export. Take that out, it will only confuse someone.
A much simplified set of examples:
$: declare -f a b c
a ()
{
foo=('--pull "always"') # single value array
}
b ()
{
echo "1: \${foo}='${foo}' <= scalar, returns first element of the array";
echo "2: \"\${foo[#]}\"='${foo[#]}' <= returns entire array (be sure to put in quotes)";
echo "3: \"\${foo[1]}\"='${foo[1]}' <= indexed, returns only second element of array"
}
c ()
{
foo=(--pull "always") # two values in this array
}
$: a # sets ${foo[0]} to '--pull "always"'
$: b
1: ${foo}='--pull "always"' <= scalar, returns first element of the array
2: "${foo[#]}"='--pull "always"' <= returns entire array (be sure to put in quotes)
3: "${foo[1]}"='' <= indexed, returns only second element of array
$: c # setd ${foo[0]} to '--pull' and ${foo[1]} to "always"
$: b
1: ${foo}='--pull' <= scalar, returns first element of the array
2: "${foo[#]}"='--pull always' <= returns entire array (be sure to put in quotes)
3: "${foo[1]}"='always' <= indexed, returns only second element of array
So what you need is:
getDockerImageName(){
. . . # stuff
additionalRunParameters=( --pull "always" ); # no single quotes
}
and just take OUT
local additionalRunParameters=${additionalRunParameters}; # it's global, no need
You have one more issue though - "${additionalRunParameters[#]}" \ is good, as long as the array isn't empty. In your example it will apparently always be loaded with the same values, so I don't see why you are adding all this extra complication of putting it into a global array that gets loaded incidentally in another function... seems like an antipattern. Just put the arguments you are universally enforcing anyway on the command itself.
However, on the possibility that you simplified some details out, then if this array is ever empty it's going to pass a literal quoted empty string as an argument on the command line, and you're likely to get something like the following error:
$: docker run --name foo "" -p 3000:3000 -d bar # docker doesn't understand the empty string
docker: invalid reference format.
Maybe, rather than
additionalRunParameters=( --pull "always" ); # no single quotes
and
"${additionalRunParameters[#]}" \
what you really want is
pull_always=true
and
${pull_always:+ --pull always } \
...with no quotes, so that if the var has been set (with anything) it evaluates to the desired result, but if it's unset and unquoted it evaluates to nothing and gets ignored, as nothing actually gets passed in - not even a null string.
Good luck. Let us know if you need more help.

How to do a bash `for` loop in terraform termplatefile?

I'm trying to include a bash script in an AWS SSM Document, via the Terraform templatefile function. In the aws:runShellScript section of the SSM document, I have a Bash for loop with an # sign that seems to be creating an error during terraform validate.
Version of terraform: 0.13.5
Inside main.tf file:
resource "aws_ssm_document" "magical_document" {
name = "magical_ssm_doc"
document_type = "Command"
document_format = "YAML"
target_type = "/AWS::EC2::Instance"
content = templatefile(
"${path.module}/ssm-doc.yml",
{
Foo: var.foo
}
)
}
Inside my ssm-doc.yaml file, I loop through an array:
for i in "$\{arr[#]\}"; do
if test -f "$i" ; then
echo "[monitor://$i]" >> $f
echo "disabled=0" >> $f
echo "index=$INDEX" >> $f
fi
done
Error:
Error: Error in function call
Call to function "templatefile" failed:
./ssm-doc.yml:1,18-19: Invalid character;
This character is not used within the language., and 1 other diagnostic(s).
I tried escaping the # symbol, like \#, but it didn't help. How do I
Although the error is pointing to the # symbol as being the cause of the error, it's the ${ } that's causing the problem, because this is Terraform interpolation syntax, and it applies to templatefiles too. As the docs say:
The template syntax is the same as for string templates in the main Terraform language, including interpolation sequences delimited with ${ ... }.
And the way to escape interpolation syntax in Terraform is with a double dollar sign.
for i in "$${arr[#]}"; do
if test -f "$i" ; then
echo "[monitor://$i]" >> $f
echo "disabled=0" >> $f
echo "index=$INDEX" >> $f
fi
done
The interpolation syntax is useful with templatefile if you're trying to pass in an argument, such as, in the question Foo. This argument could be accessed within the yaml file as ${Foo}.
By the way, although this article didn't give the answer to this exact issue, it helped me get a deeper appreciation for all the work Terraform is doing to handle different languages via the templatefile function. It had some cool tricks for doing replacements to escape for different scenarios.

less: filter out pattern passed as command line argument + follow file via bash function

I'm trying to create a bash function that will use less to apply a pattern and follow the file using the argument passed to the function
my_less_function() {
if [ -z "$1" ]
then
# if no arg
less +F /var/log/my.log
else
# else, filter out the arg
less +$'&!'$1'\nF' /var/log/my.log
fi
}
my issue is that i can't get the arg to substitute properly in the else block
my_less_function MY_VALUE displays Non-match &/MY_VALUE\nF in less
it looks like it's concatenating the argument and \nF, but \nF is supposed to trigger the follow command instead of being interpreted as part of the argument
any ideas?
wrong : less +$'&!'$1'\nF' /var/log/my.log
right : less +$'&!'${1}$'\nF' /var/log/my.log

syntax error when assigning to command result to variable

I'm doing some basic unit testing with the shunit2 unit test framework.
I'm getting the error " syntax error near unexpected token `nodeError=$( node "node_fake_returns/return_error.js" )" on the first line of my function. the function is as follows:
function testHandleNodeReturnError{
nodeError=$( node "./node_fake_returns/return_error.js" )
if [ grep -i "Error" <<< "$nodeError" ]; then
assertTrue "true"
fi
}
It is suppose to run a node script that returns an error message to stdout, then assign that output to a variable. Only this first line in the function is important.
I'm quite new to bash and I've messed with the formatting of this line, mostly just adding spaces in different places, but I can't seem to find what's causing the syntax error. This is probably pretty simple but if somebody could show me what might be wrong I would be greatful.
Thanks!
By pasting your code to shellcheck I was left with:
function testHandleNodeReturnError{
^-- SC1095: You need a space or linefeed between the function name and body.
Which is quite literal. You need a space there.
function testHandleNodeReturnError
Using function keyword is deprecated. Just use function_name() { function_body; }.
if [ grep -i "Error" <<< "$nodeError" ]; then
This is very wrong. This is outputting the content of nodeError variable to standard input of [ command. The [ is a command, a executable, just like grep, it's an alias to test program. Then it runs [ comamnd with grep, -i, "Error" and ] as 4 of it's arguments. You don't want that. If you want to check for Error string, just use grep's exit status:
So do:
testHandleNodeReturnError() {
nodeError=$(node "./node_fake_returns/return_error.js")
if grep -q -i "Error" <<<"$nodeError"; then
assertTrue "true"
fi
}

How to pass arguments to a jshell script?

Question
I am willing to pass arguments to a jshell script. For instance, I would have liked something like this:
jshell myscript.jsh "some text"
and then to have the string "some text" available in some variable inside the script.
However, jshell only expects a list of files, therefore the answer is:
File 'some text' for 'jshell' is not found.
Is there any way to properly pass arguments to a jshell script?
Workaround so far
My only solution so far is to use an environment variable when calling the script:
ARG="some test" jshell myscript.jsh
And then I can access it in the script with:
System.getenv().get("ARG")
And what about option -R
> jshell -v -R-Da=b ./file.jsh
for script
{
String value = System.getProperty("a");
System.out.println("a="+value);
}
/exit
will give you
> jshell -v -R-Da=b ./file.jsh
a=b
Another way, would be following:
{
class A {
public void main(String args[])
{
for(String arg : args) {
System.out.println(arg);
}
}
}
new A().main(System.getProperty("args").split(" "));
}
and execution
> jshell -R-Dargs="aaa bbb ccc" ./file_2.jsh
Update
Previous solution will fail with more complex args. E.g. 'This is my arg'.
But we can benefit from ant and it's CommandLine class
import org.apache.tools.ant.types.Commandline;
{
class A {
public void main(String args[])
{
for(String arg : args) {
System.out.println(arg);
}
}
}
new A().main(Commandline.translateCommandline(System.getProperty("args")));
}
and then, we can call it like this:
jshell --class-path ./ant.jar -R-Dargs="aaa 'Some args with spaces' bbb ccc" ./file_2.jsh
aaa
Some args with spaces
bbb
ccc
Of course, ant.jar must be in the path that is passed via --class-path
Oracle really screwed this up, there is no good way to do this. In addition to #mko's answer and if you use Linux(probably will work on Mac too) you can use process substitution.
jshell <(echo 'String arg="some text"') myscript.jsh
And then you can just use arg in myscript.jsh for example:
System.out.println(arg) // will print "some text"
You can simplify it with some bash function and probably write a batch file that will write to a temp file and do the same on windows.
It's completely beyond me how Oracle could ignore this. 8-() But anyway: if your system uses bash as shell, you can combine this approach replacing the shebang with the idea to (ab-)use system properties to transport the whole command line into a variable:
//usr/bin/env jshell --execution local "-J-Da=$*" "$0"; exit $?
String commandline = System.getProperty("a");
System.out.println(commandline);
/exit
This way, you can call the script on the commandline simply adding the arguments: thisscript.jsh arg1 arg2 would print arg1 arg2.
Please note that this joins all parameters into one String, separated by one space. You can split it again with commandline.split("\s"), but please be aware that this isn't exact: there is no difference between two parameters a b and one parameter "a b".
If you have a fixed number of arguments, you can also pass all of these into separate system properties with "-J-Darg1=$1" "-J-Darg2=$1" "-J-Darg3=$1" etc. Please observe that you have to use -R-D... if you are not using --execution local
Another variant is generating the script on the fly with bash's process substitution. You can use such a script also simply as thisscript.jsh arg1 arg2 also on Unix-like systems having a bash.
#!/usr/bin/env bash
jshell <(
cat <<EOF
System.out.println("$1");
System.out.println("$2");
/exit
EOF
)
This allows to access individual parameters, though it will break when there are double quotes or other special characters in a parameter. Expanding on that idea: here's a way to put all parameters into an Java String array, quoting some of those characters:
#!/usr/bin/env bash
set -- "${#//\\/\\\\}"
set -- "${#//\"/\\\"}"
set -- "${#/#/\"}"
set -- "${#/%/\",}"
jshell <(
cat <<EOF
String[] args = new String[]{$#};
System.out.println(Arrays.asList(args));
/exit
EOF
)
The set -- statements double backslashes, quote double quotes and prefix a " and append a ", to transform the arguments into a valid Java array.
Recently, I was inspired by answers from Oleg and Hans-Peter Störr enough to try to combine them so that a) I could use normal shell arguments b) write regular Java code expecting a String[] args input:
//usr/bin/env jshell <(ARGS=; for var in "$#"; do ARGS+="\"$var\","; done; echo "String[] args = {$ARGS}") "$0"; exit $?
System.out.println("RESULT: " + Arrays.asList(args));
/exit
Using Hans' header line and then inlining as demonstrated by Oleg which builds the $# args into a String[] args var.
With that you can chmod +x your script and execute it with regular old arguments:
]$ ./script.jsh foo bar
RESULT: [test, bar]

Resources