How to read the output AND the return value of a external program in shell script? - shell

I'm making a script that reads a tracking code, looks at the results of posting the tracking to a website and prints some messages and has a return value.
Here's part of the python code:
# update return True if there was a change to the .msg file
def update(cod):
msg = extract_msg(cod)
if msg == 'ERROR':
print('ERROR: invalid code\n')
sys.exit(2)
file = open('.msg', "r+")
old_msg = file.read()
if msg == old_msg:
return False
else:
print('Previous message: ' + old_msg)
print('Latest message: ' + msg)
file = overwrite(file, msg)
file.close()
return True
def main(argv):
if len(argv) > 1:
cod_rastr = argv[1]
else:
print("Error: no arg, no code\n")
return -1
# Verify if file exists
if os.path.isfile(".msg") == False:
arq = open('.msg', 'w')
arq.close()
# post() returns the source code of the resulting page of the posted code.
cod = post(cod_rastr)
if update(cod) == False:
return 0
else:
print ('\n Message!\n')
return 1
And here, I want to read not only the prints (for the final user) but the return values (for conditional use). This script should read the output of the .py and send me an email in case there is an update from the last check (I'll put this script in the crontab):
#!/bin/bash
if [ -z "$1" ]; then
echo usage: $0 CODE
exit
fi
CODE=$1
STATUS=$(myscript.py $CODE 2>&1)
VAL=$?
FILE=$(<.msg)
# always prints 0 (zero)
echo $VAL
# I want to check for an existing update case
if [[ $STATUS == 'Message!' ]]
then
echo $STATUS
echo $FILE | mail myuser#mydomain.com -s '$CODE: Tracking status'
fi
The problem is that $? always returns 0, and my string check inside the if, is not working, because I think It reads the update() prints too, which has variables in the print.
How can I make this shell script run, without changing the python script?
Thanks in advance.

I suspect that you can do what you want with the subprocess module. Either use rc = subprocess.call(...) to get a return code while directing stdout to a file, or use p = subprocess.Popen(...) and then perhaps p.communicate to get output and p.returncode to get the returncode.

Related

How to warn before opening certain filetypes in vim?

It is easy to accidentally open a large binary or data file with vim when relying on command line autocomplete.
Is it possible to add an interactive warning when opening certain file types in vim?
For example, I'd like to add a warning when opening files without an extension:
> vim someBinary
Edit someBinary? [y/N]
Or maybe:
> vim someBinary
# vim buffer opens and displays warning about filetype,
# giving user a chance to quit before loading the file
This could be applied to a range of extensions, such as .pdf, .so, .o, .a, no extension, etc.
There is a related question on preventing vim from opening binary files, but it is primarily about modifying autocomplete to prevent accidentally opening the files in the first place.
Below is the solution I came up with, using vim autocommands with the BufReadCmd event. It's a lot of vimscript, but it's pretty robust. It issues a warning if the file being opened is a non-ascii file or has a blacklisted extension (.csv and .tsv for this example):
augroup bigfiles
" Clear the bigfiles group in case defined elsewhere
autocmd!
" Set autocommand to run before reading buffer
autocmd BufReadCmd * silent call PromptFileEdit()
augroup end
" Prompt user input if editing an existing file before reading
function! PromptFileEdit()
" Current file
let file = expand("%")
" Whether or not we should continue to open the file
let continue = 1
" Skip if file has an extension or is not readable
if filereadable(file) && (IsNonAsciiFile(file) || IsBlacklistedFile())
" Get response from user
let response = input('Are you sure you want to open "' . file . '"? [y/n]')
" Bail if response is a 'n' or contains a 'q'
if response ==? "n" || response =~ "q"
let continue = 0
if (winnr("$") == 1)
" Quit if it was the only buffer open
quit
else
" Close buffer if other buffers open
bdelete
endif
endif
endif
if continue == 1
" Edit the file
execute "e" file
" Run the remaining autocommands for the file
execute "doautocmd BufReadPost" file
endif
endfunction
" Return 1 if file is a non-ascii file, otherwise 0
function! IsNonAsciiFile(file)
let ret = 1
let fileResult = system('file ' . a:file)
" Check if file contains ascii or is empty
if fileResult =~ "ASCII" || fileResult =~ "empty" || fileResult =~ "UTF"
let ret = 0
endif
return ret
endfunction
" Return 1 if file is blacklisted, otherwise 0
function! IsBlacklistedFile()
let ret = 0
let extension = expand('%:e')
" List contains ASCII files that we don't want to open by accident
let blacklistExtensions = ['csv', 'tsv']
" Check if we even have an extension
if strlen(extension) == 0
let ret = 0
" Check if our extension is in the blacklisted extensions
elseif index(blacklistExtensions, extension) >= 0
let ret = 1
endif
return ret
endfunction
To read with syntax highlighting enabled, see this gist.
Maybe not super elegant, but I enjoyed learning some vimscript along the way.
I am not too experienced with vimscript so I'm sure there is room for improvements -- suggestions and alternative solutions welcome.
Note: This is not expected to work on Windows systems outside of WSL or Cygwin, due to calling file.
You can use a function like this
promptvim() {
grep -Fq "." <<< "$1" || read -p "Edit $1? [y/N]" && [[ $REPLY == "y" ]] || return
/usr/bin/vim "$1"
}
You can choose a different function name. When you use vim other scripts might fail.
EDIT:
When you like this construction (wrapper, not vim settings), you can make the function better with more tests:
promptvim() {
if [ $# -eq 0 ]; then
echo "arguments are missing"
return
fi
local maybe_dot=$(grep -Fq "." <<< "$1")
for file in $*; do
# skip tests when creating a new file
if [ -f "${file}" ]; then
maybe_dot=$(grep -F "." <<< "${file}")
if (( ${#maybe_dot} == 0 )); then
read -p "Edit ${file}? [y/N]"
# check default response variable REPLY
# Convert to lowercase and check other ways of confirming too
if [[ ! "${REPLY,,}" =~ ^(y|yes|j|ja|s|si|o|oui)$ ]]; then
continue
fi
fi
fi
echo /usr/bin/vim "${file}"
done
}
This still not covers all special cases. You might want to add support of the vim parameters on the commandline, check for an interactive session and think what you want with here documents.

Kaldi librispeech data preparation error

I'm trying to do ASR system. Im using kaldi manual and librispeech corpus.
In data preparation step i get this error
utils/data/get_utt2dur.sh: segments file does not exist so getting durations
from wave files
utils/data/get_utt2dur.sh: could not get utterance lengths from sphere-file
headers, using wav-to-duration
utils/data/get_utt2dur.sh: line 99: wav-to-duration: command not found
And here the piece of code where this error occures
if cat $data/wav.scp | perl -e '
while (<>) { s/\|\s*$/ |/; # make sure final | is preceded by space.
#A = split;
if (!($#A == 5 && $A[1] =~ m/sph2pipe$/ &&
$A[2] eq "-f" && $A[3] eq "wav" && $A[5] eq "|")) { exit (1); }
$utt = $A[0]; $sphere_file = $A[4];
if (!open(F, "<$sphere_file")) { die "Error opening sphere file $sphere_file"; }
$sample_rate = -1; $sample_count = -1;
for ($n = 0; $n <= 30; $n++) {
$line = <F>;
if ($line =~ m/sample_rate -i (\d+)/) { $sample_rate = $1; }
if ($line =~ m/sample_count -i (\d+)/) { $sample_count = $1;
}
if ($line =~ m/end_head/) { break; }
}
close(F);
if ($sample_rate == -1 || $sample_count == -1) {
die "could not parse sphere header from $sphere_file";
}
$duration = $sample_count * 1.0 / $sample_rate;
print "$utt $duration\n";
} ' > $data/utt2dur; then
echo "$0: successfully obtained utterance lengths from sphere-file headers"
else
echo "$0: could not get utterance lengths from sphere-file headers,
using wav-to-duration"
if command -v wav-to-duration >/dev/null; then
echo "$0: wav-to-duration is not on your path"
exit 1;
fi
In file wav.scp i got such lines:
6295-64301-0002 flac -c -d -s /home/tinin/kaldi/egs/librispeech/s5/LibriSpeech/dev-clean/6295/64301/6295-64301-0002.flac |
In this dataset i have only flac files(they downloaded via provided script) and i dont understand why we search wav-files? And how run data preparation correctly(i didnt change source code in this manual.
Also, if you explain to me what is happening in this code, then I will be very grateful to you, because i'm not familiar with bash and perl.
Thank you a lot!
The problem I see from this line
utils/data/get_utt2dur.sh: line 99: wav-to-duration: command not found
is that you have not added the kaldi tools in your path.
Check the file path.sh and see if the directories that it adds to your path are correct (because it has ../../.. inside and it might not match your current folder setup)
As for the perl script, it counts the samples of the sound file and then it divides with the sample rate in order to get the duration. Don't worry about the 'wav' word, your files might be on another format, it's just the name of the kaldi functions.

How to avoid calling external utility (grep) twice while maintaining return code & output?

I have following Bash function which return property value from Java-style property file. It property wasn't found, it should return non-zero. If found, property's value will be printed & return code must be zero.
function property_get() {
local pfile="$1"
local pname="$2"
if egrep "^${pname}=" "$pfile" 2>&1 >/dev/null; then
local line="$(egrep "^${pname}=" "$pfile")"
printf "${line#*=}"
return 0 # success
else
return 1 # property not found
fi
}
The question is: how to avoid from calling egrep twice? First exec is for status code, 2nd is for property value. If I use $(grep parameters) notation, then grep will be launched in subshell and I can't get it's return code and won't be able to determine success or failure of property searching.
This should work:
...
local line
if line=$(egrep "^${pname}=" "$pfile" 2>/dev/null); then
...
As #matt points out, you can just get the line then check the exit status of your command:
line=$(...)
if test $? -eq 0; then
# success
...
else
# property not found
...
fi
Edit:
To summarize:
You can use var=$(cmd), to set var to the standard output of cmd and $? to the exit status of cmd
With local var=$(cmd), $? will be set to the exit status of local which is 0 as explained here: Why does "local" sweep the return code of a command?

Return value of function in IF statement shows strange behaviour

Can anyone explain what is going on here?
Why does an "IF" statement think the return is '1' (not Null) when i say 'Return 0' and the other way round.
I found that out while coding another script, so i developed this small script to test it:
#!/bin/bash
function testreturnzero()
{
echo $1
return 0
}
function testreturnone()
{
echo $1
return 1
}
if (testreturnzero 1) || (testreturnzero 2)
then
echo "zero returned '1'"
fi
if (testreturnone 1) || (testreturnone 2)
then
echo "one returned '1'"
fi
The IF which refers to the 'return 0' thinks its true (and doesn't process the second function), the IF which refers to 'return 1' thinks its false. Shouldn't it be the exact opposite?
1
zero returned '1'
1
2
I cant put the return value in a variable as I will have several of those checks.
In bash, a function returning 0 means success and returning a non-zero value means failure. Hence your testreturnzero succeeds and your testreturnone fails.
Does that help understanding why your ifs behave that way? (it should!).
The return code of the last executed command/function is stored in the special variable $?.
So:
testreturnzero 0
ret_testreturnzero=$?
testreturnone 1
ret_testreturnone=$?
echo "$ret_testreturnzero"
echo "$ret_testreturnone"
will output (the two last lines):
0
1
Now you may think of storing them in a variable (as here) and do your logic processing later. But there's a catch :). Because you didn't store true and false in variables, you stored 0 and 1 (bash can't store booleans in a variable). So to check success or failure later:
if ((ret_testreturnzero==0)); then
echo "testreturnzero succeeded"
fi
or
if ((ret_testreturnzero!=0)); then
echo "testreturnzero failed"
fi
In bash, the return code of a function is the same as a external program when you test the result.
So for test, a valid return code is 0 and an invalid is any other number
so, by doing
if ( testreturnone 1 ); then #it is ok
echo "error"; #it's supposed to happen, not an error
fi
You can explicitly test the value to the one you want to clear it up:
if [[ "$(testreturnzero 1)" = "1"); then #it is ok if you decide that 1 is the good value
echo "ok"; #But absolutly not the bash philosophy
fi

How can I check if stdin exists in PHP ( php-cgi )?

Setup and Background
I am working on script that needs to run as /usr/bin/php-cgi instead /usr/local/bin/php and I'm having trouble checking for stdin
If I use /usr/local/bin/php as the interpreter I can do something like
if defined('STDIN'){ ... }
This doesn't seem to work with php-cgi - Looks to always be undefined. I checked the man page for php-cgi but didn't find it very helpful. Also, if I understand it correctly, the STDIN constant is a file handle for php://stdin. I read somewhere that constant is not supposed to be available in php-cgi
Requirements
The shebang needs to be #!/usr/bin/php-cgi -q
The script will sometimes be passed arguments
The script will sometimes receive input via STDIN
Current Script
#!/usr/bin/php-cgi -q
<?php
$stdin = '';
$fh = fopen('php://stdin', 'r');
if($fh)
{
while ($line = fgets( $fh )) {
$stdin .= $line;
}
fclose($fh);
}
echo $stdin;
Problematic Behavior
This works OK:
$ echo hello | ./myscript.php
hello
This just hangs:
./myscript.php
These things don't work for me:
Checking defined('STDIN') // always returns false
Looking to see if CONTENT_LENGTH is defined
Checking variables and constants
I have added this to the script and run it both ways:
print_r(get_defined_constants());
print_r($GLOBALS);
print_r($_COOKIE);
print_r($_ENV);
print_r($_FILES);
print_r($_GET);
print_r($_POST);
print_r($_REQUEST);
print_r($_SERVER);
echo shell_exec('printenv');
I then diff'ed the output and it is the same.
I don't know any other way to check for / get stdin via php-cgi without locking up the script if it does not exist.
/usr/bin/php-cgi -v yields: PHP 5.4.17 (cgi-fcgi)
You can use the select function such as:
$stdin = '';
$fh = fopen('php://stdin', 'r');
$read = array($fh);
$write = NULL;
$except = NULL;
if ( stream_select( $read, $write, $except, 0 ) === 1 ) {
while ($line = fgets( $fh )) {
$stdin .= $line;
}
}
fclose($fh);
Regarding your specific problem of hanging when there is no input: php stream reads are blocking operations by default. You can change that behavior with stream_set_blocking(). Like so:
$fh = fopen('php://stdin', 'r');
stream_set_blocking($fh, false);
$stdin = fgets($fh);
echo "stdin: '$stdin'"; // immediately returns "stdin: ''"
Note that this solution does not work with that magic file handle STDIN.
stream_get_meta_data helped me :)
And as mentioned in the previous answer by Seth Battin stream_set_blocking($fh, false); works very well 👍
The next code reads data from the command line if provided and skips when it's not.
For example:
echo "x" | php render.php
and php render.php
In the first case, I provide some data from another stream (I really need to see the changed files from git, something like git status | php render.php.
Here is an example of my solution which works:
$input = [];
$fp = fopen('php://stdin', 'r+');
$info = stream_get_meta_data($fp);
if (!$info['seekable'] && $fp) {
while (false !== ($line = fgets($fp))) {
$input[] = trim($line);
}
fclose($fp);
}
The problem is that you create a endless loop with the while($line = fgets($fh)) part in your code.
$stdin = '';
$fh = fopen('php://stdin','r');
if($fh) {
// read *one* line from stdin upto "\r\n"
$stdin = fgets($fh);
fclose($fh);
}
echo $stdin;
The above would work if you're passing arguments like echo foo=bar | ./myscript.php and will read a single line when you call it like ./myscript.php
If you like to read more lines and keep your original code you can send a quit signal CTRL + D
To get parameters passed like ./myscript.php foo=bar you could check the contents of the $argv variable, in which the first argument always is the name of the executing script:
./myscript.php foo=bar
// File: myscript.php
$stdin = '';
for($i = 1; $i < count($argv); i++) {
$stdin .= $argv[$i];
}
I'm not sure that this solves anything but perhaps it give you some ideas.

Resources