Automate creation of symbolic links on Windows bash - powershell

I'm trying to make a script that will do some directory management. The final script will run on Windows and will preferably be written in python. At one point in the script I need to automate the creation of multiple symbolic links between multiple folders. The script itself runs without administrator permissions from a bash terminal (Git Bash). Windows is not in developer mode.
The perfect solution would be to have a list of tuples (link, source) and create the corresponding symbolic links all at once, while having to press "Yes" for administrator rights only once.
I already did some research:
How to create a symlink between directories from inside an elevated cmd: Git Bash shell fails to create symbolic links
mklink /D link source_directory
How to run a command in cmd as an administrator from inside bash: Launch Elevated CMD.exe from Powershell
powershell 'start cmd -v runAs -Args /k, [comma-separated-args]'
How to set the working directory after launching the powershell command as an administrator (Otherwise it launches a terminal from inside C:\Windows\System32\): PowerShell: Run command from script's directory
powershell 'start cmd -v runAs -Args /k, cd, $pwd, "&", [comma-separated-args]'
Let's say I want to create a symbolic link in my current working directory to a relative directory. I tried 2 ways:
When I combine all of the above points and execute the following command from the Git Bash terminal:
powershell 'start cmd -v runAs -Args /k, cd, $pwd, "&", mklink, /D, \"link_to_utils\", \"common\utils\"'
A new terminal opens up (after agreeing for admin rights). But it resulted in a new symlink being created in the root of C:\ .
When I execute this:
powershell 'start cmd -v runAs -Args /k, cd, $pwd
A new terminal opens up (after agreeing for admin rights). I can now run this command:
mklink /D "link_to_utils" "common\utils"
The link is created in the current working directory, as I wanted.
So my questions are:
a) How can I make option 1 work in bash?
b) Why is it actually creating the symlink in C:\?
c) Is there a way to pipe a command into the opened elevated cmd terminal (to make option 2 work)?
Note: I have been trying to find a solution using python and the win32api (pywin32). But that resulted in a bunch of command prompts opening up for each symlink that needs to be created. Also there is barely any documentation regarding pywin32.

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

Trying to run a headless executable command through Powershell that works on cmd line

I am trying to run an executable through powershell to run headless, to install a program onto a VM/LocalHost machine. I can get the wizard to open, but for whatever reason I cannot get it to run headless. Here is the cmd line that I run that works:
start /WAIT setup.exe /clone_wait /S /v" /qn"
This is my attempts in powershell
Start-Process .\setup.exe /S -Wait -PassThru
Start-Process .\setup.exe /S /v /qn -Wait -PassThru
Start-Process setup.exe -ArgumentList '/clone_wait /S /v /qn' -Wait
In the cmd line instance the application installs without issue - in the powershell instance the wizard opens and is on the first "Next" prompt. Any help would be appreciated!
I also attempted to add the additional parameters "/v" and "/qn" which return an error : Start-Process : A positional parameter cannot be found that accepts argument '/v'
The bottom attempt runs but it's not waiting for the installation to complete
You may be overthinking it. Remember that PowerShell is a shell. One of the purposes of a shell is to run commands that you type.
Thus: You don't need Start-Process. Just type the command to run and press Enter.
PS C:\> .\setup.exe /clone_wait /S /v /qn
Now if the executable (or script) you want to run contains spaces in the path or name, then use the call/invocation operator (&) and specify the quotes; for example:
PS C:\> & "\package files\setup.exe" /clone_wait /S /v /qn
(This behavior is the same no matter whether you are at the PowerShell prompt or if you put the command in a script.)
This worked for me. You need to quote the whole argumentlist, plus embed double quotes to pass what you want to /v.
start-process -wait SetupStata16.exe -ArgumentList '/s /v"/qb ADDLOCAL=core,StataMP64"'
Running the command normally and then using wait-process after might be a simpler alternative, if you're sure there's only one process with that name:
notepad
wait-process notepad
To follow-up to all that you have been given thus far. Running executables via PowerShell is a well-documented use case.
PowerShell: Running Executables
Solve Problems with External Command Lines in PowerShell
Top 5 tips for running external commands in Powershell
Using Windows PowerShell to run old command-line tools (and their
weirdest parameters)
So, from the first link provides more validation of what you've been given.
5. The Call Operator &
Why: Used to treat a string as a SINGLE command. Useful for dealing with spaces.
In PowerShell V2.0, if you are running 7z.exe (7-Zip.exe) or another command that starts with a number, you have to use the command invocation operator &.
The PowerShell V3.0 parser do it now smarter, in this case you don’t need the & anymore.
Details: Runs a command, script, or script block. The call operator, also known as the "invocation operator," lets you run commands that are stored in variables and represented by strings. Because the call operator does not parse the command, it cannot interpret command parameters
Example:
& 'C:\Program Files\Windows Media Player\wmplayer.exe' "c:\videos\my home video.avi" /fullscreen
Things can get tricky when an external command has a lot of parameters or there are spaces in the arguments or paths!
With spaces you have to nest Quotation marks and the result it is not always clear!
In this case it is better to separate everything like so:
$CMD = 'SuperApp.exe'
$arg1 = 'filename1'
$arg2 = '-someswitch'
$arg3 = 'C:\documents and settings\user\desktop\some other file.txt'
$arg4 = '-yetanotherswitch'
& $CMD $arg1 $arg2 $arg3 $arg4
# or same like that:
$AllArgs = #('filename1', '-someswitch', 'C:\documents and settings\user\desktop\some other file.txt', '-yetanotherswitch')
& 'SuperApp.exe' $AllArgs
6. cmd /c - Using the old cmd shell
** This method should no longer be used with V3
Why: Bypasses PowerShell and runs the command from a cmd shell. Often times used with a DIR which runs faster in the cmd shell than in PowerShell (NOTE: This was an issue with PowerShell v2 and its use of .Net 2.0, this is not an issue with V3).
Details: Opens a CMD prompt from within powershell and then executes the command and returns the text of that command. The /c tells CMD that it should terminate after the command has completed. There is little to no reason to use this with V3.
Example:
#runs DIR from a cmd shell, DIR in PowerShell is an alias to GCI. This will return the directory listing as a string but returns much faster than a GCI
cmd /c dir c:\windows
7. Start-Process (start/saps)
Why: Starts a process and returns the .Net process object Jump if -PassThru is provided. It also allows you to control the environment in which the process is started (user profile, output redirection etc). You can also use the Verb parameter (right click on a file, that list of actions) so that you can, for example, play a wav file.
Details: Executes a program returning the process object of the application. Allows you to control the action on a file (verb mentioned above) and control the environment in which the app is run. You also have the ability to wait on the process to end. You can also subscribe to the processes Exited event.
Example:
#starts a process, waits for it to finish and then checks the exit code.
$p = Start-Process ping -ArgumentList "invalidhost" -wait -NoNewWindow -PassThru
$p.HasExited
$p.ExitCode
#to find available Verbs use the following code.
$startExe = new-object System.Diagnostics.ProcessStartInfo -args PowerShell.exe
$startExe.verbs

Difference between running command in PS shell and CMD shell

I don't use powershell often, so this may be an obvious question but my google-fu is failing me.
What is the difference between running the following commands, on a Windows platform?
In cmd prompt:
C:\> powershell cd d:\foo
and in powershell prompt:
PS C:\> cd d:\foo
The latter changes drive and directory as expected. The former does nothing.
Basically, when you run powershell cd d:\foo it opens a separate Powershell and runs the cd command. As opposed to the second one, you are actually in the Powershell session. So it, the first one, does open a Powershell process > change the directory and then it closes the process.

PSExec and Powershell fails to run a program located in Program Files (x86)

I am struggling to use Psexec inside of a PS script to execute an interactive program.
I have tried this:
PsExec.exe -i \\192.168.100.95 -u Administrador -p Test1234 cmd /c "echo . | powershell notepad" 2> $null
... and it runs perfectly fine. Notepad is launched on a remote machine. Now, when I want to run .exe from Program Files (x86) I get absolutely nothing.
I have tried this variations to run 1.exe located in ProgramFiles(x86):
PsExec.exe -i \\192.168.100.95 -u Administrador -p Test1234 cmd /c "echo . | powershell "${env:ProgramFiles(x86)}\1.exe"" 2> $null
PsExec.exe -i \\192.168.100.95 -u Administrador -p Test1234 cmd /c "echo . | powershell "${env:ProgramFiles(x86)}" + "\1.exe"" 2> $null
However none of them work. Any idea what´s wrong?
Try the following:
psexec cmd /c 'echo . | powershell "& \"${env:ProgramFiles(x86)}\1.exe\"' 2>$null
Note: To better focus on the fundamentals of the solution, I've simplified the psexec command, but the original command should work too.
The entire string passed to cmd /k is single-quoted to prevent PS from interpolating elements up front, notably ${env:ProgramFiles(x86)} whose expansion should be deferred until the command is executed on the target machine.
Note that you normally need a double-quoted string when you pass a command line to cmd /c when invoking from cmd.exe itself. From within PowerShell, however, this is not a requirement: PowerShell first parses the string - whether single- or double-quoted originally - interpolates, if applicable, and then passes the resulting string double-quoted to the external command.
Note the & \"...\" construct in the context of the powershell argument, which ensures that the path with embedded spaces is correctly executed.
Curiously, PS requires " chars. to be escaped as \" when a parameter is passed from the outside world (as opposed to escaping as `" inside the realm of PS).
The command passed to powershell as a whole must be double-quoted, because cmd.exe - in whose context powershell is invoked due to cmd /c - only recognizes double quotes as parameter delimiters and only double quotes protect the enclosed content (mostly) from interpretation.
Why your commands didn't work:
The primary problem was that the executable path that you wanted powershell.exe to invoke ended up containing spaces (C:\Program Files...), causing PowerShell not to recognize the entire path as a single argument. Such a path must be (a) quoted and (b) invoked with &, the call operator.
(In the 2nd attempt, with + ... (string concatenation), you would have had to use & also, and enclose the concatenation in (...)).
For debugging, using cmd /k instead of cmd /c can give you a better sense of how the command is ultimately executed (/k keeps the console window open after execution of the command).
A subtler point is that by using a double-quoted string overall, ${env:ProgramFiles(x86)} was expanded on the source machine rather than on the target machine, where the definition of that environment variable may or may not be the same.
You're putting yourself in Escape Hell by mixing PowerShell, CMD and PsExec. If all you want is run an executable on a remote host, just stick with CMD and PsExec (run the command from CMD too):
PsExec.exe -i \\192.168.100.95 -u Administrador -p Test1234 cmd /c echo. ^| "%ProgramFiles(x86)%\1.exe" 2>nul
That way you just need to escape the pipe (^|) and put the path with spaces in double quotes.

Running CMD command in PowerShell

I am having a bunch of issues with getting a PowerShell command to run. All it is doing is running a command that would be run in a CMD prompt window.
Here is the command:
"C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\i386\CmRcViewer.exe" PCNAME
I have tried the following with no success (I have tried many iterations of this to try and get one that works. Syntax is probably all screwed up):
$TEXT = $textbox.Text #$textbox is where the user enters the PC name.
$CMDCOMMAND = "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\i386\CmRcViewer.exe"
Start-Process '"$CMDCOMMAND" $TEXT'
#iex -Command ('"C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\i386\CmRcViewer.exe"' $TEXT)
The command will just open SCCM remote connection window to the computer the user specifies in the text box.
Try this:
& "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\i386\CmRcViewer.exe" PCNAME
To PowerShell a string "..." is just a string and PowerShell evaluates it by echoing it to the screen. To get PowerShell to execute the command whose name is in a string, you use the call operator &.
To run or convert batch files externally from PowerShell (particularly if you wish to sign all your scheduled task scripts with a certificate) I simply create a PowerShell script, e.g. deletefolders.ps1.
Input the following into the script:
cmd.exe /c "rd /s /q C:\#TEMP\test1"
cmd.exe /c "rd /s /q C:\#TEMP\test2"
cmd.exe /c "rd /s /q C:\#TEMP\test3"
*Each command needs to be put on a new line calling cmd.exe again.
This script can now be signed and run from PowerShell outputting the commands to command prompt / cmd directly.
It is a much safer way than running batch files!
One solution would be to pipe your command from PowerShell to CMD. Running the following command will pipe the notepad.exe command over to CMD, which will then open the Notepad application.
PS C:\> "notepad.exe" | cmd
Once the command has run in CMD, you will be returned to a PowerShell prompt, and can continue running your PowerShell script.
Edits
CMD's Startup Message is Shown
As mklement0 points out, this method shows CMD's startup message. If you were to copy the output using the method above into another terminal, the startup message will be copied along with it.
For those who may need this info:
I figured out that you can pretty much run a command that's in your PATH from a PS script, and it should work.
Sometimes you may have to pre-launch this command with cmd.exe /c
Examples
Calling git from a PS script
I had to repackage a git client wrapped in Chocolatey (for those who may not know, it's a package manager for Windows) which massively uses PS scripts.
I found out that, once git is in the PATH, commands like
$ca_bundle = git config --get http.sslCAInfo
will store the location of git crt file in $ca_bundle variable.
Looking for an App
Another example that is a combination of the present SO post and this SO post is the use of where command
$java_exe = cmd.exe /c where java
will store the location of java.exe file in $java_exe variable.
You must use the Invoke-Command cmdlet to launch this external program. Normally it works without an effort.
If you need more than one command you should use the Invoke-Expression cmdlet with the -scriptblock option.

How to change the cmd's current-dir using PowerShell?

I read some file using PowerShell, and change current dir accordingly, but all I can do is change the current PowerShell's current dir, not the caller's dir (the cmd.exe environment that called that ps1 file). Things I tried:
powershell ch-dir.ps1 | cd
(won't work, obviously, since CD is internal command)
powershell cd $myDir
(changes current dir in PowerShell, but when script exits, the cmd environment still in original dir)
I really hope I won't need to find the script's caller process (the cmd), and make a change in it's cur-dir by-force... (or even worse - to save the dir I want in some env-var and then cd %my_var% since it would require two lines of command)
I'm not sure if this meets your needs, but if you set it up so that the only output from your powershell script is your desired new working directory, you could do this:
c:\>for /F %i IN ('powershell -noprofile -command "write-output 'c:\users'" ') DO #cd %i
c:\Users>
The cmd prompt is hosting your powershell session, unless you can figure out a way to return an exit code to the prompt that will (on exit code 99999) change directory to (predefined values, switch?). As far as powershell is concerned they're different processes.
Heres a good example for you to try:
Open a cmd prompt.
Open task manager, find cmd.exe
In your cmd prompt type Powershell
View powershell as a different process (check the PID.)
End the powershell process. Watch what happens.
Alternatively, if you need something run from cmd in a specific directory based on logic in your powershell script, you can invoke it with a cmd /c from within Powershell.