Fish shell: Check if argument is provided for function - function

I am creating a function (below) with which you can provide an argument, a directory. I test if the $argv is a directory with -d option, but that doesn’t seem to work, it always return true even if no arguments are supplied. I also tried test -n $argv -a -d $argv to test is $argv is empty sting, but that returns test: Missing argument at index 1 error. How should I test if any argument is provided with the function or not? Why is test -d $argv not working, from my understanding it should be false when no argument is provided, because empty string is not a directory.
function fcd
if test -d $argv
open $argv
else
open $PWD
end
end
Thanks for the help.

count is the right way to do this. For the common case of checking whether there are any arguments, you can use its exit status:
function fcd
if count $argv > /dev/null
open $argv
else
open $PWD
end
end
To answer your second question, test -d $argv returns true if $argv is empty, because POSIX requires that when test is passed one argument, it must "Exit true (0) if $1 is not null; otherwise, exit false". So when $argv is empty, test -d $argv means test -d which must exit true because -d is not empty! Argh!
edit Added a missing end, thanks to Ismail for noticing

In fish 2.1+ at least, you can name your arguments, which then allows for arguably more semantic code:
function fcd --argument-names 'filename'
if test -n "$filename"
open $filename
else
open $PWD
end
end

if not set -q argv[1]
echo 'none'
else
echo 'yes'
end
From the man set page:
set ( -q | --query ) [SCOPE_OPTIONS] VARIABLE_NAMES...
o -q or --query test if the specified variable names are defined. Does not output anything, but the builtins exit status is the number of variables specified
that were not defined.

$argv is a list, so you want to look at the first element, if there are elements in that list:
if begin; test (count $argv) -gt 0; and test -d $argv[1]; end
open $argv[1]
else
open $PWD
end

Maybe is non related, but i would like to add another perspective for the question.
I want to broaden the insight to a wider scope the scope of testing the shell code with the libraries developed in the fisherman group.
With mock you can check if the open command is called safely without side effects.
Example.:
function fcd
if count $argv > /dev/null
open $argv
else
open $PWD
end
end
mock open 0 "echo \$args"
fcd "cool" # echoes cool
mock open 0 "echo \$args"
fcd # echoes $PWD
Is a recent library, but it could help to test things that might be dangerous, like for example rm
mock rm 0 "echo nothing would happen on \$args"
rm "some file" # simply echoes the message with a list of the files that would been affected
I hope that it gives a more out of the box point of view
P.S.: Sorry for the blatant publicity, but i think that is a cool idea that would be nice to be adopted by shell scripters, to test and add sustainability to shell scripts is nuts. :P
EDIT: i recently noticed a bug about the sample i posted. Please do not use rm *, because the asterisk is not treated as a param, instead fish shell expands asterisk to the list of files found and the command only mocks the first call, this means that the first file would be ignored by the mock, but all the subsequent files will get erased so please be careful if trying the sample and use a single file for the example not the wildcard.

Related

Opening gnome-terminal and running script in tcl

When I try to execute below command it is showing Error like "extra characters after close-quote" but I gave it properly & when i try to it in unix command line terminal is opening properly.
exec gnome-terminal -e 'sh -c "bsub -Ip -n 1 -M <Memory> -q <queue_name> make"'
Can any one help me to resolve this issue or is there any way to do the same thing ??
Edited -> changed " from before sh to before bsub
Tcl's quoting is not the shell's quoting. Tcl uses {…} like the shell uses single quotes, except that braces nest nicely. Nesting single quotes is a recipe for shell headaches.
exec gnome-terminal -e {sh -c "bsub -Ip -n 1 -M <Memory> -q <queue_name> make"}
However, in this case I'd instead be tempted to go with this:
set memory "<Memory>"
set queue "<queue_name>"
set command "make"
set bsubcmd "bsub -Ip -n 1 -M $memory -q $queue $command"
# It's much more convenient to build this command like this here.
# Otherwise you're doing lots of backslashes and so on and it's horrible and very easy to make bugs
exec gnome-terminal -e [format {sh -c "%s"} $bsubcmd]
The only really messy thing is that command and bsubcmd have to be built using shell syntax if you're passing spaces around. “Fortunately” you're dealing with make anyway, so you probably really want to avoid having spaces in names passed there.

Catch an error in piped MySQL command (bash)

Is there any way to avoid the first eval for code below? I tried to play with ${LASTPIPE[0]} and such things but it was way too complicated for me. For instance when I tried to inject code with semicolon ($MYSQLCOMMAND; if ...) it broke my output to the pipe. I spent more than 8 hours wandering StackOverflow just to give up and write one more SQL command without pipe. Don't want to publish bad written code.
MYSQLQUERY="select * from $TABLENAME"
MYSQLTOPTIONS="--defaults-extra-file=$EXTRA -h $HOSTNAME -D $DBNAME -N -e"
MYSQLCOMMAND='mysql $MYSQLTOPTIONS "$MYSQLQUERY"'
# Exit immediately if something wrong with MySQL command
if eval "$MYSQLCOMMAND > /dev/null" ; then : ; else \
printf "Script returned non-zero exit code: %s\n" "$?" ; exit $?; fi
# Count rows for valid JSON output
ROWS=0
eval $MYSQLCOMMAND | \
while read ; \
do
((ROWS++))
done
(rest of the code generates JSON with calling the same
eval ... while read... and verified by https://jsonlint.com/)
Also I'd like to hear any your comment on the code since I'm not an experienced bash coder.

rsync error: when using source which contain "*" character

In my TCL script I use rsync command and unfortunately it is refusing to sync path contain "*" character.
Code Example:
set srs "/users/home/username/common/*"
catch {exec rsync -av $src /tmp} res
puts $res
Output:
building file list ... done
sent 29 bytes received 20 bytes 98.00 bytes/sec
total size is 0 speedup is 0.00
rsync: link_stat "/users/home/username/common/*" failed: No such file or directory (2)
rsync error: some files could not be transferred (code 23) at main.c(977) [sender=2.6.9]
while executing
"exec rsync -rLptgov $src /tmp "
The canonical way of writing that is:
set srs "/users/home/username/common/*"
catch {exec rsync -av {*}[glob $src] /tmp} res
puts $res
This is because Tcl doesn't expand glob metacharacters by default; it's safer that way and easier to write correct code (it's hard to write good shell code that is resistant to problems in this area) but it does mean that you need to do a little extra work. The extra work is:
Ask for glob expansion of $srs with the glob command. That returns a list.
Ask for expansion of the list out of the glob by using the {*} pseudo-operator (it's not an operator — it's technically a kind of syntax — but it works a lot like one).
If you're using Tcl 8.4 (or before!) then you do it a bit different way:
set srs "/users/home/username/common/*"
catch {eval [list exec rsync -av] [glob $src] [list /tmp]} res
puts $res
OK, that's sometimes often shortened by leaving out the list bits (and there are many more obscure ways of writing it!) but that's a bad habit as it can cause huge problems when dealing with variables with values that aren't “nice”, e.g., with spaces in pathnames. If you have 8.5 or later, use {*}. Really.
To elaborate on what GrAnd said, you have to build the list of source files explicitly, probably using glob, so you'd do it like this
set cmd [concat [list exec rsync -av] \
[glob -nocomplain /users/home/username/common/*]]
lappend cmd /tmp
catch {eval $cmd} res
puts $res
or, alternatively, use {*} to expand the agument list for exec (this is for Tcl >= 8.5), like this:
set cmd [concat [list rsync -av] \
[glob -nocomplain /users/home/username/common/*]]
lappend cmd /tmp
catch {exec {*}$cmd} res
puts $res
Update: fixed example so that it really works.
That's because tcl interpreter does not expand the '*' symbol as shell does.
For example:
$ tclsh
% cd /usr
% exec ls -d *
ls: *: No such file or directory
% exec sh -c "ls -d *"
X11R6
bin
docs
etc
...

Expect Tcl script - Error passing quoted argument using spawn

I just wrote a very simple Expect script for wrapping around rsync, but it seems to be giving me trouble. Basically, I am automating the SSH login prompt called from rsync. I also have to pass arguments through rsync to SSH so it doesn't do the host key checking. I am well aware of SSH authentication keys and ssh-keygen, but I have good reasons for doing things this way so no lectures on passing passwords on the command-line.
Script
#!/usr/local/bin/expect -f
if {$argc != 5} {
puts "usage: remoteCopy {remotehost, username, password, localfile, remoteloc}"
exit 1
}
set remotehost [lindex $argv 0]
set username [lindex $argv 1]
set password [lindex $argv 2]
set localfile [lindex $argv 3]
set remoteloc [lindex $argv 4]
set timeout -1
spawn rsync -e \"ssh -q -o StrictHostKeyChecking=no\" $localfile $username#$remotehost:$remoteloc
expect "Password"; send "$password\r"
Here is the complete output from the script:
Output
avoelker#localhost $ ./remoteFileCopy.tcl remotehost remoteuser remotepass ~/localfile /tmp/
spawn rsync -e "ssh -q -o StrictHostKeyChecking=no" /localhost/home/avoelker/localfile remoteuser#remotehost:/tmp/
rsync: Failed to exec "ssh: No such file or directory (2)
rsync error: error in IPC code (code 14) at pipe.c(83)
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(434)
send: spawn id exp6 not open
while executing
"send "$password\r""
(file "./remoteFileCopy.tcl" line 17)
avoelker#localhost $
It appears to me that rsync is trying to execute "ssh instead of just ssh, however, copying and pasting the first line of output from Expect's stdout (rsync -e "ssh -q -o StrictHostKeyChecking=no" /localhost/home/avoelker/localfile remoteuser#remotehost:/tmp/) directly into the shell works perfectly fine, so I know it isn't a problem with rsync.
When I remove the -e argument entirely from the spawn rsync line, the Expect script executes without errors, but I would have to add in host key checking to the script, which I do not want to do.
Does someone more experienced with Expect/Tcl know what I am doing wrong?
Drop your backslashes. The spawn line should just say:
spawn rsync -e "ssh -q -o StrictHostKeyChecking=no" $localfile $username#$remotehost:$remoteloc
After all, you don't want rsync to see a command with a quote in it, do you?

could not able to spawn(ssh) using expect

while executing
$expect filename.exp user pwd ip
I got the error
could not execute "ssh -l user ip": no such file or directory
The contents of filename.exp are this:
#!/opt/sfw/bin/expect -D
set OMC_Usr [lindex $argv 0]
set OMC_Pwd [lindex $argv 1]
set OMC_IP [lindex $argv 2]
set cmd "ssh -l $OMC_Usr $OMC_IP"
spawn $cmd
expect "Password:"
send "$OMC_Pwd\r"
interact
help me to proceed
Thanks...
The problem is that spawn needs the command and its arguments passed to it as multiple Tcl arguments, just like the standard Tcl exec command. The fix is to do this directly:
spawn ssh -l $OMC_Usr $OMC_IP
Or, if you prefer (and you've got Tcl 8.5):
set cmd "ssh -l $OMC_Usr $OMC_IP"
spawn {*}$cmd
Or, if you've got 8.4 or before:
eval spawn [lrange $cmd 0 end]
But don't do this:
eval spawn $cmd
because that will break unexpectedly if you have a Tcl metacharacter in the username (or IP address, but that's very unlikely).
Of course, the real fix is to set up an RSA keypair and use ssh-agent to manage it. Like that, you won't need to pass passwords on any command line; that's important because the command line of a process is public information about the process. Really. You can find it out with something trivial like ps -efww (or the equivalent for your OS). Environment variables are just as insecure too; there's an option to ps to show them too.