Environment Variables in Symbolic Links

Environment variables in symbolic links

Symbolic links are handled by the kernel, and the kernel does not care about environment variables. So, no.

unexpected behavior when creating a path with %userProfile% environment variable in pwershell

  • Shortcut files (.lnk) do support cmd.exe-style environment variable references (e.g. %USERPROFILE%) in their properties, but there's a caveat:

    • When a shortcut file is created or modified with WshShell COM object's .CreateShortcut() API:

      • Assigning property values, say, .TargetPath = '%USERPROFILE%\Desktop' works as expected - that is, the string is stored as-is and the reference to environment variable %USERPROFILE% is only expanded at runtime, i.e. when the shortcut file is opened.

      • However, querying such properties via this COM API also performs expansion, so that you won't be able to get raw definitions, and won't be able to distinguish between the .TargetPath property containing %USERPROFILE%\Desktop and, say, verbatim C:\Users\jdoe\Desktop

    • When in doubt about a given .lnk file's actual property values, inspect it via File Explorer.

  • Symbolic links (symlinks) do not.

The target of a symbolic link must be specified as a literal path, which in your case means using PowerShell's environment-variable syntax (e.g., $env:UserProfile) in order to resolve the variable reference to its value up front:

# This results in a *literal* path, because $env:UserProfile is
# instantly resolved; the result can be used with New-Item / mklink
$literalTargetPath = "$env:UserProfile\" + $midpart + $filename

Passing something like '%USERPROFILE%\Desktop' to the -Value / -Target parameter of New-Item -Type SymbolicLink therefore does not work (as intended), but the symptom depends on which PowerShell edition you're running:

  • Windows PowerShell, which you're using, tries to verify the existence of the target path up front, and therefore fails to create the symlink, because it interprets %USERPROFILE%\Desktop verbatim, as a relative path (relative to the current directory, which is why the latter's path is prepended), which doesn't exist.

  • PowerShell (Core) 7+ also interprets the path verbatim, but it allows creating symlinks with not-yet-existing targets, so the symlink creation itself succeeds. However, trying to use the resulting symlink then fails, because its target doesn't exist.

Symbolic Links to Variables

Aside from the scoping issues in your code, there is really one issue to keep in mind. Python does everything by binding a name to a reference. You are interested in getting the value assigned to a name, not to the reference it may have been bound to at registration time. This is sort of like a pointer-to-a-pointer in C and C++.

To keep track of arbitrary names, you need to keep track of their enclosing namespace as well. That way you can use the universal function getattr to fetch the latest value of your name. A namespace can be any instance (modules and classes are instances too).

Your registration method needs to keep track of the object containing the name of interest, the name itself, and the key you want to display it as:

op.register_output_variable(self, 'foo', "Foo")

The manager class can store the values in a dictionary. Instead of a nested dictionary, I'd recommend using a tuple or collections.namedtuple to hold the "pointer":

def register_output_variable(self, inst, attr, key):
self.output_vars_data[key] = (inst, attr)

When you output the variables, do something like

for key, spec in self.output_vars_data.items():
print(key, '=', getattr(*spec))

What this does is provide you with a way to reference object attributes by name instead of reference. If you set x.a = 1, register x.a, then do x.a += 1, you'd want to get 2 as your value. The important thing to remember is that x.a will be bound to a different reference because integers are immutable in Python. You completely bypass this by registering x as the namespace, and 'a' as the name.

You can generalize further by adding a method to register indexable elements, like the elements of a list and values of a dictionary. You would register the instance and index/key instead of instance and attribute name. Take a look at operator.attrgetter and operator.itemgetter for more ideas.

This is just a broad suggestion. It does not address the inconsistent mess of static, class and instance namespaces you are using.

What is the proper way to make symbolic links with regard to file paths being full or shortened and relying on token expansion

Consider:

ln -s target linkname

The complication occurs only when target is specified by a relative path. If it is, then the path is relative to the directory that holds the file linkname. The currect working directory is always ignored.

As an example, consider

ln -s hist ~/Desktop/hist

This command creates a link named hist in ~/Desktop. Since no path is given for the target, the target is interpreted as also being in the directory ~/Desktop. In this case, that means the the link hist points to itself.

As another example, consider

cd /var/tmp
ln -s ../hist ~/Desktop/hist

This will create a link from ~/Desktop/hist to ~/hist because the ../ is interpreted relative to the directory that holds the link, ~/Desktop. The directory that we are in when the ln command is executed and the directory that we are in when we access /Desktop/hist are both irrelevant.

Another subtlety

ln does not care what you use for target when the command is issued: it can be arbitrary text. The value of target is not intepreted until an attempt is made to access linkname. Consider:

$ ln -s "mary had a little lamb" ~/file1
$ ls -alt ~/file1
lrwxrwxrwx 1 user group 22 Sep 15 21:47 /home/user/file1 -> mary had a little lamb
$ cat ~/file1
cat: /home/user/file1: No such file or directory

If we want we can later create that file:

$ echo this is a test >~/"mary had a little lamb"
$ cat ~/file1
this is a test

Again, the existence of the target is only checked when an attempt is made to access the link.

Documentation

This behavior is documented in man ln:

Symbolic links can hold arbitrary text; if later resolved, a relative
link is interpreted in relation to its parent directory.

How to know when you are in a symbolic link

If you don't find anything else, you can use

os.getenv("PWD")

It's not really a portable python method, but works on POSIX systems. It gets the value of the PWD environment variable, which is set by the cd command (if you don't use cd -P) to the path name you navigated into (see man cd) before running the python script. That variable is not altered by python, of course. So if you os.chdir somewhere else, that variable will retain its value.

Anyway, as a side node, /tmp/foo/kiwi is the directory you are in. I'm not sure whether anything apart from the shell knows that you've really navigated through another path into that place, actually :)

Creating symlinks with ansible using variables

The issue is that you're passing two different objects to the with_items property. The first object has two properties (src_dir and src_name), while the second object has two different properties (dest_dir and dest_name).

It looks like you want to combine them into a single object like this:

- name: Create NPM symlink
file:
src: '{{ item.src_dir }}/{{ item.src_name }}'
dest: '{{ item.dest_dir }}/{{ item.dest_name }}'
owner: "{{ ansible_ssh_user }}"
group: "{{ ansible_ssh_user }}"
state: link
with_items:
- { src_dir: "{{ npm_real_dir }}", src_name: "{{ npm_real_name }}", dest_dir: "{{ nodenpm_link_dir }}", dest_name: "{{ npm_link_name }}" }

That should work better and get rid of the error, but in this case you don't really need the with_items, since it's only one item that you're dealing with. You can add more objects for other tools, e.g. gulp in the same manner, e.g. like this:

- name: Create symlinks
file:
src: '{{ item.src_dir }}/{{ item.src_name }}'
dest: '{{ item.dest_dir }}/{{ item.dest_name }}'
owner: "{{ ansible_ssh_user }}"
group: "{{ ansible_ssh_user }}"
state: link
with_items:
- { src_dir: "{{ npm_real_dir }}", src_name: "{{ npm_real_name }}", dest_dir: "{{ nodenpm_link_dir }}", dest_name: "{{ npm_link_name }}" }
- { src_dir: "{{ gulp_real_dir }}", src_name: "{{ gulp_real_name }}", dest_dir: "{{ gulp_link_dir }}", dest_name: "{{ gulp_link_name }}" }

Automate creation of symbolic links on Windows bash

Use the following:

powershell 'Start-Process -Verb RunAs cmd /k, " cd `"$PWD`" & mklink /D `"link_to_utils`" `"common\utils`" "'
  • Since it is PowerShell that interprets that verbatim content of the command line being passed, its syntax rules must be followed, meaning that a "..." (double-quoted) string is required for expansion (string interpolation) of the automatic $PWD variable to occur, and that embedded " characters inside that string must be escaped as `" ("" would work too).

  • The pass-through command line for cmd.exe is passed as a single string argument, for conceptual clarity.



Related Topics



Leave a reply



Submit