Need Help with Advanced Functions

This topic contains 14 replies, has 5 voices, and was last updated by  Ron 3 years, 6 months ago.

  • Author
    Posts
  • #13412

    Ron
    Participant

    I have a script that i wrote, but i would like to utilize advanced functions and i have never created one. The script that I have basically reads a userlist.txt file and a computerlist.txt file and it creates an AD group named after the computer with a "-adm" that it reads from the computerlist.txt file. For instance, "%computername%-adm". Then it takes the users in the userlist.txt file and puts them in the "%computername%-adm" AD group. Then, the script adds the "%computername%-adm" group to the computer's local admin group.

    I would like to be able to have the Tech Support team members where I work run the script with parameters rather than being asked several questions during the processing of the script. For instance, it would be nice to have a Tech Support member run the following command:

    Add-LocalAdminUser.ps1 -computer ".\ComputerList.txt" -user ".\UserList.txt" -adgrouptype "global"

    If someone can help me understand how I may be able to go about doing this, I would greatly appreciate it.

    Thanks alot everyone.

  • #13414

    Richard Siddaway
    Moderator

    If you could post you current script it would be easier to help.

    I'd recommend reading these help files

    about_Functions
    about_Functions_Advanced
    about_Functions_Advanced_Methods
    about_Functions_Advanced_Parameters
    about_Functions_CmdletBindingAttribute

  • #13416

    Jack Neff
    Participant

    Maybe I'm misunderstanding but your method seems a bit inefficient? For instance if each of your list files have 10 items in them (10 computers / 10 users) your script will create 10 groups in AD, all with the same members. Why not create one group and put it in ladmin on the 10 machines? You're not gaining anything by using AD in this case. In fact it's over complicating things. You would be better off with your script just taking a list of names and adding them directly to the ladmin group on the machines themselves.

  • #13417

    Ron
    Participant

    My apologies, I didnt explain that very good.

    The way it works is like this:

    1. A user (in this case only Systems Analysts) request to be local admins to test applications on their test boxes
    2. They submit an ITSM ticket
    3. I get the ticket
    4. I then email them a hyperlink to an InfoPath web form that i created and published to my team's SharePoint site
    5. The user then fills out the form with all the computers they need to be admins on
    6. User then submits the form
    7. My team's Exchange Distribution List is then notified that a request has come in for a user to be granted local admin rights on a computer(s)
    8. I then run my PowerShell script that does the following:
    – Creates the AD groups %computername%-adm
    – Adds user to that AD group
    – Adds that group to the local admin group on those computers

    We use a restricted group GPO with policy preferences that allows %computername-adm AD groups to be members of the local admin group on systems. Anything else is stripped out of the local admin group, with the exception of my team's AD group.

    Does that make more sense?

  • #13418

    Jack Neff
    Participant

    It does! It also sounds like a nightmare to manage, kudos to you and your team. Like Richard Siddaway said, it'd be easier to tweak what you have so far. Sounds like you just need to add a param block and you'll be good to go.

  • #13445

    Ron
    Participant

    Hell yes it is a nightmare, but that's the way this place is at times. Just gotta go with the punches. Anyway, I have written a separate advanced function to help me understand how advanced functions work before i get started on the much more involved script that will be adding users to AD groups, then adding them to computer local admin groups and it's actually working quite well.

    The name of test advanced funcction script as well as the name of the advanced function are both "Check-ADUserExist". Here is the beginning portion of my script:

    Function Check-ADUserExist
    {
    #Requires -Version 3.0
    #Requires -Modules ActiveDirectory

    [CmdletBinding()]
    [OutputType([String])]
    Param
    (
    [Parameter(Position=0,
    Mandatory=$true,
    HelpMessage="Enter a user name, or type 'userlist' to add multiple users to query in the UserList.txt file")]
    [Alias('user')]
    [ValidateNotNullOrEmpty()]
    [String]
    $UserName
    )

    The problem I am having is that i can basically enter any parameter and it will try to process. What is the best way to tell PowerShell that the only parameter allowed is the -username parameter?

    Thanks

  • #13450

    Dave Wyatt
    Moderator

    You've already done that. Aside from the common parameters that you get for free when writing an advanced function (-Verbose, -ErrorAction, etc), the only parameter defined for your function is -UserName. If you try to call it with some other parameter, you should get a ParameterBindingException, with an error that's something like this: "Check-ADUserExist : A parameter cannot be found that matches parameter name 'Blah'."

  • #13471

    Ron
    Participant

    Yeah I did get that. I had a brain fart and had to fix something in my script and now it's working.

    This forum is great. Everyone responds so quickly.

    I appreciate everyone trying to help me out with this.

    I'm almost done with my very first advanced function script. I call it the "Check If AD User Exist Tool".

    It searches throughout a forest with multiple domains looking for a user or a list of users.

    I'm going to use some of the code in this one to help me with my next one. This is why I wanted to get this done first.

    Is it okay if I attach here in a bit and have all you gurus take a look at it and offer some feedback?

    Thanks for being awesome everyone and being so helpful.

  • #13488

    Ron
    Participant

    Okay, attached is my script. I would greatly appreciate some feedback and constructive criticism if anyone would like to offer that may be able to help me improve it.

    Thanks again everyone

  • #13497

    Dave Wyatt
    Moderator

    Your code style is good; nice and readable with consistent bracing and indentation, plus good use of blank lines to group code into logical chunks.

    In terms of functionality, you've kind of tied your GUI code and worker code together. It would have been cleaner to start with a function that takes a list of domains and usernames as input, determines which ones exist, and output objects with whatever properties you feel are needed (Domain, SamAccountName, Full Name, etc). Then you could write your GUI stuff around that function. You'll see this type of code structure referred to as "tools" and "controller scripts" around here quite a bit: tools deal with objects (input via parameters and output to the pipeline), and tend to be placed into script modules. Controller scripts are written around tools, and deal with any user interaction, such as your menus, input and output files, etc. When you write the code this way, you retain the ability to run the tools directly from a command-line, even in an unattended script. The controller scripts, on the other hand, may require user interaction.

    Aside from that, there's just little nitpicky things. The -Verbose switch doesn't do anything useful when you're calling Write-Output and Write-Warning, for example.

  • #13599

    Ekaterina Ratzlaff
    Participant

    Yeah I am still trying to grasp the concepts of PowerShell toolmaking. I know my code looks a bit scatter brained. I guess I just need to study up on functions more.

    One question, I put cmdletbinding, param stuff within a function, is that not right? I was able to type it in and use tab completion in the ISE's host, but when I opened up a separate PowerShell command window and typed in:

    " .\Check-ADUserExist -UserName"

    It spit back an error regarding not being recognized and the -UserName parameter was not resolved using tab completion. So am I supposed to keep the cmdletbinding and param() adv function stuff completely external to a function?

    Why is it that the ISE had no issue with it, but the PowerShell command line did? I set the path to where the script was and still I got back an error.

    ————————————–
    Regarding Your Advice
    ————————————–

    So let me see if I understand you correctly. What you recommend is that I should do something like the following instead:

    #Example Script Tool Name: Do Stuff Tool
    #Example Script File Name: Do-Stuff.ps1
    #Author: Someone Doe
    #Date Created:
    #Modified By:
    #Date Modified:

    < # Modifications Record ————————-
    ————–

    Explain modifications made

    ————–

    Explain modifications made

    #>

    Function Show-Menu
    {
    1. Do stuff
    2. Help
    3. Exit

    $MenuPrompt = Read-Host -Prompt "Enter selection 1 thru 3"

    While ($MenuPrompt -lt "1" -or $MenuPrompt -gt 3)
    {
    [void][System.Windows.Forms.Messagebox]::Show("You did not make a proper selection! Please try again.", "Menu Selection Error")

    Show-Menu
    }

    Switch (Show-Menu)
    {
    "1"
    {
    Do-Stuff -Parm "something"
    }

    "2"
    {
    Do-Stuff -Help
    }

    "3"
    {
    Exit
    }
    }

    Function Do-Stuff
    {
    #Region: Parameter Input Validation

    [CmdletBinding()]
    [OutputType([String])]
    Param
    (
    [Parameter(Position=0,
    Mandatory=$true,
    HelpMessage="Does stuff")]
    [ValidateNotNullOrEmpty()]
    [String]
    $Parm,

    [Parameter(Position=1,
    Mandatory=$false,
    HelpMessage="Enter -ShowMenu to view the menu instead of using command line parameters")]
    [Alias('menu')]
    [String]
    $ShowMenu

    #EndRegion: Parameter Input Validation

    #Region: Load Prerequisites

    Begin
    {
    #Requires -Version x
    #Requires -Modules YadaYada

    [void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    }

    #EndRegion: Load Prerequisites

    #Region: Process Parameter Input and Call Functions

    Process
    {
    If ($Parm)
    {
    Do some stuff here
    }

    If($ShowMenu)
    {
    #Call the Show-Menu function
    here

    Show-Menu
    }

    If ($Parm -and $ShowMenu)
    {
    Write-Host "Cannot use both -DoStuff and -ShowMenu parameters together. Please use one or the other"

    Write-Host "Press any key to continue ..."

    $Pause = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

    Do-Stuff
    }

    #EndRegion: Process Parameter Input and Call Functions

    End

    {
    Exit
    }

    }

  • #13601

    Dave Wyatt
    Moderator

    Both scripts and functions (and any other script block, for that matter) can have a param statement. Typically, you'll put functions that you intend people to call directly into a script module (psm1 file). Alternatively, they can dot-source the script first to load the functions into the PowerShell session before they can be used. If you use this method, your script should assume it's going to be dot-sourced, and shouldn't automatically execute any of its code (or have its own param block, most likely.)

    What I'm suggesting is more like this:

    function Do-Stuff
    {
        [CmdletBinding()]
        param (
            $Parameter1,
            
            $Parameter2,
    
            $Parameter3
        )
    
        # Do stuff with parameters.
    
        # This function has no concept of a menu, and doesn't know that it's even possible to draw one.  It just
        # works based on what gets passed in for Parameter1, 2, and 3.
    }
    
    function Show-Menu
    {
        # Display a menu.  Based on user input, come up with values for $Parameter1 through $Parameter3, and call Do-Stuff
    }
    

    Do-Stuff and Show-Menu would likely be in separate files (with the Do-Stuff tool ideally living in a PSM1 script module somewhere.) Show-Menu might even just be a script instead of a function.

  • #13657

    Dave Wyatt
    Moderator

    Depends on how you organized the files / functions. Assuming that you had a single PS1 file with exactly the code I posted, you'd have two steps. First, dot-source the file to load up both functions, and second, either call Do-Stuff directly (command-line / unattended version) or call Show-Menu. You could also save that code as a PSM1 module file, and the user could import it instead of dot-sourcing a script. If the user is running PowerShell 3.0 or later and they install the module into their PSModulePath, they wouldn't even have to worry about the import step, as it would happen automatically the first time they called either of the functions.

    Example of the dot-sourcing version, assuming that the file was named Stuff.ps1:

    . .\Stuff.ps1
    Show-Menu
    

    Module example:

    Import-Module -Name .\Stuff.psm1
    Show-Menu
    

    Anyhow, it just depends on what your requirements are for this code. If it's always going to be accessed via a menu, and you never intend to have to worry about command-line execution (particularly in a scheduled task or other unattended scenario), then what you already have is fine.

  • #13683

    Ron
    Participant

    Great! Thanks alot for your help Dave, I appreciate it.

  • #13653

    Ron
    Participant

    Can you give me an example of how I would run that example code you offered from the PS command line (host) please? Like if I wanted to get a menu, then I would type in: .\Do-Stuff.ps1 Show-Menu?

    Also, if I wanted to give my script to someone, but I had a function in a separate module, then I wouldn't I need to give them the module in addition to my script?

    I guess I can just read DJ's books ("In a Month of Lunches") I bought off Amazon to gain some more clarity too. The cool thing is, you can just go to the publishers site and get the PDF for free if you bought the book(s) and search within the PDF for the answers – NICE. I really do like the interaction of the forums though.

    No offense DJ. Don, can I call you DJ? It has a nice ring to it.

    Thanks for your help Dave. I appreciate it brother.

You must be logged in to reply to this topic.