This question already has answers here:
When to wrap quotes around a shell variable?
(5 answers)
How to pass all arguments passed to my Bash script to a function of mine? [duplicate]
(7 answers)
Closed 3 years ago.
The problem:
I'm writing a program that performs actions in python based on links, and possibly expanding it to do things beyond that. This program is meant to be quickly used through bash. So, I'm using a custom function to do that.
function youtube() {python3 <youtube program path here> $1}
As for the python file; I'm using sys, os, and re in order to make it function. sys, in order to use both sys.exit() and var = sys.argv[<argNum>], the former in order to exit the program using custom exceptions, like error.searchError() or error.usageError(), and the later for actualling using the arguments from the command itself. os is just for os.system('{}'.format(<your command here>)). And re is for removing the spaces from the second argument, where my problem lies, and replacing them with '+', as per query = re.sub(' ', '+', query).
Now, as for the problem itself. As I mentioned before, the problem lies with the second bash argument, or sys.argv[2]. With sys.argv[0] being the python file, and sys.argv[1] being the option, in this case being -s.
sys.argv[2] is meant to be the actual youtube search query. But, according to whenever I use the command with all three arguments, youtube -s Hi this is a test., I get the following output, as per the custom error I made: No search query provided!. This only happens when python excepts an IndexError, which means that the program is not receiving the second argument from bash or zsh. What is actually supposed to happen, when the second arguments does exist, is:
os.system('open https://www.youtube.com/results?search_query=Hi+this+is+a+test.')
Which opens that link in my default browser. I have tried to add $2 to the custom function, and various ways of entering the second argument through the python source itself, including using a x = input('Search Query: ') statement. But that isn't optimal for what I'm doing.
The code:
The following is the source code for all the nonsense I just typed out.
The custom function:
function youtube() {python3 <python program path here> $1}
For those that have no idea what this means (i.e.; people that don't know much (or anything) about bash coding); The function method creates a bash object, in this case, youtube(). As for the code in the brackets ({}), this uses the function python3, which just pushes the program in argument 0 to the python 3.x interpreter, to open <python program path here>, which is a placeholder for the actual path of the program. As for $1, this is a variable that always equals the text inputted after the function name.
The custom errors:
class error:
def usageError():
usageError = '''Usage: youtube [-s] [<search_query>]
Help: Performs actions related to https://www.youtube.com
Options:
-s Opens https://www.youtube.com/results?search_query=<your query here>'''
print(usageError)
sys.exit()
def searchError():
searchError = 'No search query provided!'
print(searchError)
sys.exit()
Is this irrelevant? I'm not sure, but I'm putting it in anyway! Now, if you don't understand it, the following should explain it.
The error class contains all of the customs errors for this program, ok? Ok, so you get that, but what do these functions do? usageError is raised when argument 1 simply doesn't exist, and prints the usage information to the terminal. Then sys.exit()s the program, basically the equivalent of hitting Alt+f4 in video game. searchError, on the other hand, only happens if argument 2 doesn't exist, meaning there is no search query. It then tells you that you're stupid, and will need to actually enter your query for it to work.
Well, maybe not that exactly, but you get the point.
The Juicy Bits:
option = ''
try: option = sys.argv[1];
except IndexError: raise error.usageError()
if option == '-s':
try:
query = sys.argv[2]
query = re.sub(' ', '+', query)
os.system('open https://www.youtube.com/results?search_query={}'.format(query))
except IndexError: raise error.searchError();
Just to explain; First, the program creates the variable option and then sets it to an empty string. Then, it tries to set option to argument 1, or the option. If argument 1 doesn't exist, it raises the error error.usageError, as explained in The Custom Errors. After that, the program tries to create the variable query, and set it to argument 2, then replace all of the spaces in query with '+' signs. If all of that succeeds to happen, it then loads up the youtube search in your default browser. If not, it raises the error error.searchError().
The Edits
Edit 1. The error was in The Custom Function. Where I should have had an $#, I had an $1. As Jeshurun Roach explains in his answer, $1 only holds the argument 1, and no other arguments. While $# contains all variables.
function youtube() {python3 <python program path here> $#}
$1 refers to the first argument passed into the function. in bash, spaces delimit arguments. so in your example youtube -s Hi this is a test.,
$1 is -s,
$2 is Hi,
$3 is this etc...
What you're looking for is the $# symbol. This value stands for all the arguments.
But just plugging in $# instead of $1 won't fix all your problems. in your python script, each argument will be broken up again by spaces, just like the bash function.
To fix this, you can put quotes around the text after the flag like so: youtube -s 'Hi this is a test.'.
If you call your program like this: youtube -s something cool, then sys.argv[2] is going to be "something".
I'd suggest wrapping your query in quotes. For example youtube -s "something cool".
Related
I have a shell script I need to call from a Python script. Only thing is that is needs to pass 7 parameters, that are basically variables, and some of those contain spaces.
I see a lot of references to os.system, and to subprocess, but what about the parameters with spaces? I think those will cause an issue.
Edit 2:
subject = "Proposal"
to = "LandonStatis#stackoverflow.com"
from = "Joshua#stackoverflow.com"
REPORT_DIRECTORY = "/home/reports"
file = "/home/files/proposal.txt"
message = "Please accept me as the best answer"
#i didnt include the "flag" because i dont know the flag format
os.system('./home/scripts/send_email.pl "'+subject+'" "'+to+'" "'+from+'" "'+REPORT_DIRECTORY+'" "'+file+'" "'+message+'"')
Edit:
If you want to add a variable, you can do so like this
os.system('ls -al '+str(x))
For example, if your username is "landon statis" with a space,
you can do this: os.system('ls -al "/home/landon statis/Desktop"')
(notice the space in the command above?)
This works for both Windows and Linux.
Use double quotes to prevent word splitting. An argument enclosed in
double quotes presents itself as a single word, even if it contains
whitespace separators.
This is the source, which is a link to Linux Documentation Project. Check them out, it's very useful: Advanced Bash Scripting Guide: Chapter 5 - Quoting
Please do not use os.system or subprocess with shell=True. Both execute a shell and are potentially vulnerable to shell injections. See https://docs.python.org/3/library/subprocess.html#security-considerations for more details.
Best way is to use subprocess and hand it a list of arguments. Example: Let's just use a Shell script that print its arguments. myscript:
#!/bin/sh
for arg in "$#"; do
echo "argument: $arg"
done
Then you can call this script using a list as argument:
subprocess.run(["./myscript", "first", "argument with space", "third"])
Output:
argument: first
argument: argument with space
argument: third
How can I run a python function from an AHK script? If it's possible, how would I:
Pass arguments to the python function?
Return data from the python function back to my running AHK script?
The only relevant information I could find was this answer, however I could not manage to implement it into my own script.
My best attempt at implementing this is the following snippet of code:
e::; nothing, because RunWait it's the command recommended in the question that I linked, so that means that I have to do a RunWait every time I press e?
There is no native way to do this, as the only interaction AHK can do with Python is to run an entire script. However, you can modify the python script to accept arguments, then you can interact between the scripts (like the linked question).
The solution is as follows- similar to the question you linked, set up the python script so that it takes the function name and function parameters as arguments, then have it run the function with those arguments.
Similar to my answer on the linked question, you can use sys.argv to do this:
# Import the arguments from the sys module
from sys import argv
# If any arguments were passed (the first argument is the script name itself, so you must check for >1 instead of >0)
if len(argv) > 1:
# This next line is basically saying- If argv is longer than 2 (the script name and function name)
# Then set the args variable to everything other than the first 2 items in the argv array
# Otherwise, set the args variable to None
args = argv[2:] if len(argv) > 2 else None
# If arguments were passed, then run the function (second item in argv) with the arguments (the star turns the list into args, or in other words turns the list into the parameter input format)
# Otherwise, run the function without arguments
argv[1](*args) if args else argv[1]()
# If there is code here, it will also execute. If you want to only run the function, then call the exit() function to quit the script.
Then, from AHK, all you would need to do is run the RunWait or Run command (depending on whether you want to wait for the function to finish) to call the function.
RunWait, script.py "functionName" "firstArgument" "secondArgument"
The second part of your question is tricky. In the question you linked, there is a nice explanation on how to return integer values (TLDR: use sys.exit(integer_value)), however if you want to return all sorts of data, like strings, for example, then the problem becomes more confusing. The thing is that at this point, I think the best solution is to write the output to a file, then have AHK read the file after the Python script is done executing. However, if you're already going to go down the "write to a file, then read it" route, then you might as well have already done that from the start and used that method to pass the function name and arguments to the python script.
I'm a beginner Python coder and I'm very unsure on how to create a simple shell program that accepts commands (ex. printrecipes, printinventory, load etc.)
The input should look like:
$ loadrecipes
$ printmoney()
20
For this shell, I'm trying to use a while loop so it continues through the program without crashing even if they input a command that is acceptable.
def handle_commands():
keep_going=True
command=input("$" + " ")
while keep_going:
if command == '$ quit':
keep_going = False
break
elif command == "$ loadrecipefile(recipe_file)"
j
elif command == "$ printrecipes":
printrecipes()
elif command == "$ printiinventory":
printiinventory()
elif command == "$ printmoney":
printmoney()
elif command == "$ buyingredient":
I have no idea what to go from here.
The commands are that loadrecipes(recipe_file) takes in one argument, all print commands don't take an argument, buyingredient(ingredient_name, number:int) takes in 2 arguments (the ingredient name and how many of those ingredients).
So, for each command I have created a function in correspondence. Such as for printiinventory() I have:
def printiinventory():
print(iinventory['apple'],iinventory['beets'],iinventory['carrots'])
so if the command is:
$ printiinventory
0 4 3
it should come out to be like this
So your flow should look like this:
while True:
command = input("$ ")
if command is ...
elif ...:
Very similar to what you have, with the difference that you don't need to expect $ into the user's input. Input function prints the argument passed and returns SOLELY the user's input, not the rest of the content in the same line. So you should check for commands like command == "printrecipes", etc.
Explanation:
This piece of code:
x = input(str)
Is equivalent to:
print(str); x = input(str)
with the only difference that print() creates a new line, so the input will be taken from the line just below the printed content.
You could emulate this behaviour (the printing in the same line, that is) with the IO low-level Python modules, but there is no need when you can do just that.
Edit
In order to parse the commands, you can opt for the classical command line interface syntax, that separates command name and argument with spaces, or you could make your own parser. In case you go for the first, you could use Python's built-in argparse module. In case you'd rather use the second (which is more of a headache, especially if you are a starter), you have to write your own parser from scratch. Is not that big of a deal if you know regex, but I'm afraid that's a different question you should ask in the site. I would recommend you to take a look at some tutorials. Just googling: "make my own command parser python" gives you thousands of results, even though most of them will go for classic command line parsing syntax.
Edit 2
I've noticed you use some sort of flag to check if you need to keep going inside the loop. That is useless in the piece of code you use; just use break command and you're good to go.
Edit 3
Taking a close look at the OP's comments, I see you are trying to write Python code to be executed by a Python script. You can for sure do that; you've got the eval and exec modules, BUT note that this is a very risky practice, code can very easily be injected into your program, causing huge security holes. It is highly discouraged to do that. You have to separate command parsing from task executing. The user cannot ever have direct access to the control flow of the program.
I have a python script that I'd like to supply with an argument (usually) containing wildcards, referring to a series of files that I'd like to do stuff with. Example here:
#!/usr/bin/env python
import argparse
import glob
parser = argparse.ArgumentParser()
parser.add_argument('-i', action="store", dest="i")
results = parser.parse_args()
print 'argument i is: ', results.i
list_of_matched_files = glob.glob(results.i)
In this case, everything works great if the user adds quotes to the passed argument like so:
./test_script.py -i "foo*.txt"
...but often times the users forget to add quotes to the argument and are stumped when the list only contains the first match because UNIX already expanded the list and argparse only then gets the first list element.
Is there a way (within the script) to prevent UNIX from expanding the list before passing it to python? Or maybe even just to test if the argument doesn't contain quotes and then warn the user?
No. Wildcards are expanded by the shell (Bash, zsh, csh, fish, whatever) before the script even runs, and the script can't do anything about them. Testing whether the argument contains quotes also won't work, as the shell similarly strips the quotes from "foo*.txt" before passing the argument to the script, so all Python sees is foo*.txt.
Its not UNIX that is doing the expansion, it is the shell.
Bash has an option set -o noglob (or -f) which turns off globbing (filename expansion), but that is non-standard.
If you give an end-user access to the command-line then they really should know about quoting. For example, the commonly used find command has a -name parameter which can take glob constructs but they have to be quoted in a similar manner. Your program is no different to any other.
If users can't handle that then maybe you should give them a different interface. You could go to the extreme of writing a GUI or a web/HTML front-end, but that's probably over the top.
Or why not prompt for the filename pattern? You could, for example, use a -p option to indicate prompting, e.g:
import argparse
import glob
parser = argparse.ArgumentParser()
parser.add_argument('-i', action="store", dest="i")
parser.add_argument('-p', action="store_true", default=False)
results = parser.parse_args()
if results.p:
pattern = raw_input("Enter filename pattern: ")
else:
pattern = results.i
list_of_matched_files = glob.glob(pattern)
print list_of_matched_files
(I have assumed Python 2 because of your print statement)
Here the input is not read by the shell but by python, which will not expand glob constructs unless you ask it to.
You can disable the expansion using set -f from the command line. (re-enable with set +f).
As jwodder correctly says though, this happens before the script is run, so the only way I can think of to do this is to wrap it with a shell script that disables expansion temporarily, runs the python script, and re-enables expansion. Preventing UNIX from expanding the list before passing it to python is not possible.
Here is an example for the Bash shell that shows what #Tom Wyllie is talking about:
alias sea='set -f; search_function'
search_function() { perl /home/scripts/search.pl $# ; set +f; }
This defines an alias called "sea" that:
Turns off expansion ("set -f")
Runs the search_function function which is a perl script
Turns expansion back on ("set +f")
The problem with this is that if a user stops execution with ^C or some such then the expansion may not be turned back on leaving the user puzzling why "ls *" is not working. So I'm not necessarily advocating using this. :).
This worked for me:
files = sys.argv[1:]
Even though only one string is on the command line, the shell expands the wildcards and fills sys.argv[] with the list.
I have to Python scripts: Tester1.py and Tester2.py.
Within Tester1 I want to start from time to time Tester2.py. I also want to pass Tester2.py some arguments. At the moment my code looks like this:
Tester1:
subprocess.call(['python3 Tester2.py testString'])
Tester2:
def start():
message = sys.argv[1]
print(message)
start()
Now my problem: If I run with my terminal Tester2 like 'python3 Tester2.py testString'my console prints out testString. But if I run Tester1 and Tester1 tries to start Tester2, I get an IndexError: "list index out of range".
How do I need to change my code to get everything working?
EDIT:
niemmi told me that I have to change my code to:
subprocess.call(['python3', 'Tester2.py', 'testString'])
but now I get a No such file or directory Error although both scripts are in the same directory. Someone knows why?
You need to provide the arguments either as separate elements on a list or as a string:
subprocess.call(['python3', 'Tester2.py', 'testString'])
# or
subprocess.call('python3 Tester2.py testString')
Python documentation has following description:
args is required for all calls and should be a string, or a sequence of program arguments. Providing a sequence of arguments is generally preferred, as it allows the module to take care of any required escaping and quoting of arguments (e.g. to permit spaces in file names). If passing a single string, either shell must be True (see below) or else the string must simply name the program to be executed without specifying any arguments.