Forum Discussion

jmaraviglia's avatar
jmaraviglia
Copper Contributor
Apr 02, 2025

Remove computers from multiple domains from one AD group

Hello!

     I've tried a couple of scripts I found and still cannot remove these computers from this one AD group.  The script I'm currently using is:

# Import the Active Directory module
Import-Module ActiveDirectory

# List of device names to be removed
$computers = @(
    "machine1.domain1",
    "machine2.domain2"
)

# Loop through each device name in the list
foreach ($computer in $computers) {
    # Get the device object from Active Directory
    $computer = Get-ADComputer -Identity $computer -ErrorAction SilentlyContinue

    # Check if the device exists
    if ($computer) {
        # Remove the device from Active Directory
        get-adcomputer $computer | remove-adobject -recursive -Confirm:$false 
        Write-Host "Removed device $computer from Active Directory."
    } else {
        Write-Host "Device $computer not found in Active Directory."
    }
}

 

The errors I get is the object cannot be found and it always lists Domain1.  I'm pretty new to PS so would appreciate any guidance!

7 Replies

  • LainRobertson's avatar
    LainRobertson
    Silver Contributor

    Hi jmaraviglia,

     

    It's good to see you giving PowerShell a crack. Keep practicing and you'll get the hang of it!

     

    With respect to your script above, it's a little at odds with your stated objective of removing particular computers from a particular group.

     

    The most important point of difference is on line 18, where instead of removing a computer from a group, you're actually attempting to remove a computer object (and it's child contents, if it has any) entirely from Active Directory.

     

    I'm going to provide an alternative example script that addresses your objective of only removing nominated computers from a specific group (the less dangerous interpretation).

     

    Invoke-ADGroupCleanup.ps1

    # Import the Active Directory module
    Import-Module ActiveDirectory;
    
    # List of device names to be removed expressed as a RegEx pattern.
    $Computers = @(
        "^cn=machine1,.*"                   # Produces a match when the distinguishedName begins with "cn=machine1," (case-insensitive).
        , "^cn=machine2,.*"                 # Produces a match when the distinguishedName begins with "cn=machine2," (case-insensitive).
        , "dc=robertsonpayne,dc=com$"       # Produces a match when the distinguishedName ends with "dc=robertsonpayne,dc=com" (case-insensitive).
    );
    
    # Common name of the group you want to clean up.
    $GroupName = "Print Servers";
    
    # Get the Active Directory group object and specifically ask for the member property, which holds the current members in distinguishedName form (they're actually just strings, not AD objects).
    $Group = Get-ADObject -Filter { (objectClass -eq "group") -and (name -eq $GroupName) } -Properties member;
    
    # Create an empty list that will hold all the matching distinguishedNames (string) values that match any of the RegEx patterns held in the $Computers variable from above.
    $RemovalList = [System.Collections.Generic.List[string]]::new();
    
    # Build the list of members to remove from the group.
    $Group.member | ForEach-Object {
        foreach ($RegExPattern in $Computers) {
            if ($_ -match $RegExPattern)
            {
                $RemovalList.Add($_);
                break;  # Break simply terminates the current iteration of a foreach() loop, which we want to do since there's nothing to be gained from performing anymore pattern matches.
            }
        }
    }
    
    # Now that we have our removal list, call Active Directory to perform the actual removal.
    $Group | Remove-ADGroupMember -Members ($RemovalList.ToArray()) -ErrorAction:Stop -Confirm:$false;
    
    # If the above Remove-ADGroupMember commandlet didn't error out, send the list of members removed to the pipeline.
    $RemovalList;

     

    Explanation

    Firstly, let's go over some concepts briefly.

     

    A group in Active Directory has a membership list - I'm sure you know that already. This membership list is effectively a list of strings, often referred to as a distinguished name.

    Here's a brief example of an Active Directory group with a single member:

     

    Conversely, when you run a command like Get-ADGroupMember, you get back a complex object that features many different attributes, as shown below:

    Sometimes this can be useful but most times it's just wasteful because you don't need the extra overhead from pulling all that unnecessary data.

     

    The idea with any script is to keep it lean and mean so it continues to perform well at large scale. You don't have to, of course, but it's a good habit to get into that most commonly requires little to no extra effort, so why wouldn't you?

     

    Next, you're making a few separate calls to Active Directory inside of the loop. While for small volumes - like the two computers you have in your $computers variable - the performance impact is irrelevant, this approach doesn't scale well once you are dealing with tens or hundreds of thousands of changes.

    Much as with the first point, you can seriously minimise this impact (again, only noticeable in larger changes) by changing your design to only call Active Directory a single time, after you have build the list of members you want to remove from the group.

    I should point out that if you're working with Active Directory, you're probably working with network latencies under 5 milliseconds. But depending on where you are in the world, if you're working with cloud services, you can be easily working with latencies between 50 to 200 milliseconds, which very quickly amplifies the overhead problems associated with making too many calls inside of a loop.

    So, as a concept, you want to minimise how often you're calling the remote service - be that Active Directory, Azure Active Directory, Exchange Online, etc.

     

    Let's move onto the script itself - which I've added a fair number of comments into to help in understanding what it's doing.

     

    The high-level approach is simple:

     

    1. Define the computers we want to remove as a regular expression (RegEx) pattern (more on this below);
    2. Define which group we want to remove them from;
    3. Fetch the group's current member list;
    4. See which of those current members match the entries we defined in step 1 for removal and save those matches;
    5. Call Active Directory to remove any matched entries with one call;
    6. If the call to Active Directory was successful, send the matching entries out to the pipeline.

     

    Step 1 and RegEx patterns.

    If you're new to PowerShell then I'm going to also assume you're new to development in general, and .NET development in particular. That means I need to explain myself when I talk about RegEx patterns.

    First, from the script above, the parts we'll be referring to is the $Computers variable spanning lines 5 to 9, and then the actual RegEx comparison on line 23.

     

    A regular expression comparison needs you to provide a minimum of two things for it to work:

    1. A source string that you want to scan;
    2. A pattern that you intend to search for within that source string.

     

    A very simple example - that you can type for yourself on a PowerShell command line - might look like:

     

    # A RegEx example where there is a match. Note the result is "True":
    "The quick brown fox jumped over the lazy brown dog!" -match "lazy";
    
    # A RegEx example where there isn't a match. Note the result is "False":
    "The quick brown fox jumped over the lazy brown dog!" -match "Toyota";

     

    Output

     

    I'm sure it's already obvious, but the string value on the left of the "-match" operator is the bigger string that we're searching within, while the string on the right is the pattern we're searching for.

     

    You can find a quick reference to the .NET RegEx implementation here but you don't have to read the Microsoft documentation yet as I'll explain what you need to know in relation to the script above:

     

     

    If you look at the first value I've used in the $Computers variable declaration, it's "^cn=machine1,.*". Here's the breakdown of this string:

     

    SectionMeaning
    ^This character translates to "the start of the string".
    cn=machine1,This is simply a string that is searched for within the bigger string. It's the same as "lazy" and "Toyota" from above.
    .*The period is a placeholder for any character, while the asterisk after it simply means "find this zero or more times".

     

    Putting this together, this pattern is saying "we have a match if the source string begins with 'cn=machine1,'"!

     

    PowerShell has other ways for performing substring matches but none of them are as flexible as the "-match" operator. The "-like" operator is one such alternative that you will commonly see, but I won't digress into comparing operators here.

     

    Briefly looking at the third string in $Computers, the dollar sign means "at the end of the string", so we're looking for a match if the source string ends with "dc=robertsonpayne,dc=com", i.e. the computer is in the domain named "robertsonpayne.com" (keeping the example simple).

     

    Steps 2 and 3.

    The section above is quite long so let's keep this one short as it should be pretty easy to see what's going on here:

     

    • Line 12: Specify the name of the group we want to clean up;
    • Line 15: Grab the group object from Active Directory, being sure to include the "member" attribute, which contains the current list of direct members of the group.

     

    Step 4: Look at the existing members and figure out which ones need removing.

    Line 18 simply defines an empty list of strings that will hold any string matches we find when comparing the current list of members with the string entries held in our $Computers variable.

    Line 21 simply grabs the next string held within $Group.members and passes it through to the ForEach-Object block, where that string is referred to as "$_". "$_" is an alias for "the current object that's been passed through to the ForEach-Object loop", but you can read a more comprehensive definition here:

     

     

    So, using the same screenshot as earlier as the point of reference, $_ would equal the string value of "CN=RPFILE02,OU=Servers,OU=RobertsonPayne,DC=robertsonpayne,DC=com" on the first pass:

    This group only has a single member, but if it had 20, then $_ would move onto the next member string value on the next iteration of the ForEach-Object loop.

     

    Lines 22 and 23 work together to perform the RegEx comparison, where $_ (with a value of "CN=RPFILE02,OU=Servers,OU=RobertsonPayne,DC=robertsonpayne,DC=com") is checked against the three RegEx patterns obtained from $Computers.

     

    If a match is found, we store that match (line 25) in the $RemovalList list of string for use later on and exit the innermost foreach loop using the "break;" statement on line 26. This bounces us back out to the member string passed into outermost ForEach-Object block.

     

    The outer ForEach-Object loop continues until all the member strings have been examined for matches.

     

    Step 5: Call Active Directory to remove the matching members we found.

    Remove-ADGroupMember (line 32) requires an Array for the "-Members" parameter as noted in the documentation below:

     

     

    Strictly-speaking, our $RemovalList is not an Array which is why rather than just using "$RemovalList" I've explicitly asked $RemovalList to construct an Array object for us by calling the .ToArray() method (i.e. this part of the command: "($RemovalList.ToArray())").

     

    PowerShell does have something called automatic typecasting, however, I've been verbose in this example since automatic typecasting doesn't always work and it helps to understand what PowerShell might really be doing - or not doing and why - under the hood.

     

    Note the "-ErrorAction:Stop" parameter? The "Stop" value (there are others to choose from) means that if there is an error, PowerShell should stop right here and not progress to any further statements below, which in this case is what we want, as we don't want to output the matches since we encountered an error and therefore didn't successfully remove them.

     

    Step 6: Output data to the pipeline.

    We will only reach line 35 if line 32 was successful, meaning it's quite safe now to send the list of members we successfully removed to the pipeline.

     

     

    When you send data to the pipeline, that data can be routed to another command - as you have done on line 18 of your own original script; a file; a printer; or as is most commonly the case, the screen (which itself is the default output device).

     

    A lot of folks tend to use Write-Host for outputting data but I advise against that as a general rule since it makes your scripts less versatile - particularly when you want the output from one script/commandlet to be fed into another script/commandlet - again, as with your existing line 18 where the data from Get-ADComputer is being sent (piped) to Remove-ADObject.

     

    If you use Write-Host on its own, then you prevent yourself from being able to pass data along to another script (simplistic statement but true enough for most people).

     

    Anyhow, that was a long one, but hopefully some of these basics help you navigate the more complex stuff you will eventually encounter.

     

    Cheers,

    Lain

    • jmaraviglia's avatar
      jmaraviglia
      Copper Contributor

      Thank you so much Lain!  I really appreciate your input.  I need a lot more training and practice, but I'll keep at it of course.  In this scenario, and to narrow it down, I have to remove computers from potentially 6 groups (3 in each domain) and move them to another group.  All 3 groups are the same in each domain.  The list of servers in question are split between 2 different domains so that complicates things.  To your point about the search string, the computer names are pretty scattered in terms of naming conventions.  Is there a way to provide a csv or txt file to plug in to remove them from possibly 2 groups?  I only ask because I have over 200 servers to go through and trying to find the distinguished name to all of them would be counter productive.  I did test your server with a couple of servers.  If I run the script in each domain, it works, so thank you!!!!  It would be great to just use an input file and somehow scan all 6 groups and remove accordingly.  Still, it's huge that this works so I'm very grateful  :-)

      • LainRobertson's avatar
        LainRobertson
        Silver Contributor

        Hi jmaraviglia,

         

        You have a few moving parts in this reply. Of note, we have:

         

        • Multiple groups;
        • Multiple computers in each group;
        • Multiple domains.

         

        I'm going to stick to the first two points in the interest of keeping things (including error handling) simple. You can always run the script manually in the second domain.

         

        The changes requires to cope with multiple groups are minimal.

         

        Firstly, you could store both the group and computer name regex patterns in separate files, however, you only have six groups so I've simply used a variable to hold the group names while electing to use a text file to hold the computer name regex patterns.

         

        Text file

        All I've done is take the values from the first script's $Computers variable and drop them into a text file. Note, you should remember these are regex patterns and not simple text values, so don't forget to include the "=" prefix and "," suffix or else you could match other computers you didn't intend to remove.

         

        For example, a regex match for "Computer1" will match all of the following:

         

        • Computer1
        • Computer11
        • Computer111

         

        And so on. While a pattern of "=Computer1," will only match the intended "Computer1" - noting that technically, "Computer1" could exist in multiple domains (you can combine the second and third example patterns to deal with that scenario).

         

        Next, here's the updated script.

         

        Invoke-ADGroupCleanup.ps1

        # Import the Active Directory module
        Import-Module ActiveDirectory;
        
        # List of group names in the current domain to search for.
        $GroupNames = @(
            "Group1"
            , "Group2"
            , "Print Servers"
        );
        
        # Read the list of computer name RegEx values into a variable.
        $ComputerNames = Get-Content -Path "D:\Data\Temp\Forum\forum.txt";
        
        $GroupNames | ForEach-Object {
            # Common name of the group you want to clean up.
            $GroupName = $_;
        
            # Get the Active Directory group object and specifically ask for the member property, which holds the current members in distinguishedName form (they're actually just strings, not AD objects).
            $Group = Get-ADObject -Filter { (objectClass -eq "group") -and (name -eq $GroupName) } -Properties member -ErrorAction:Stop;
        
            if ($null -eq $Group)
            {
                Write-Warning -Message "$GroupName not found.";
            }
            else
            {
                # Create an empty list that will hold all the matching distinguishedNames (string) values that match any of the RegEx patterns held in the $Computers variable from above.
                $RemovalList = [System.Collections.Generic.List[string]]::new();
        
                # Build the list of members to remove from the group.
                $Group.member | ForEach-Object {
                    foreach ($RegExPattern in $ComputerNames) {
                        if ($_ -match $RegExPattern)
                        {
                            $RemovalList.Add($_);
                            break;  # Break simply terminates the current iteration of a foreach() loop, which we want to do since there's nothing to be gained from performing anymore pattern matches.
                        }
                    }
                }
        
                if (0 -eq $RemovalList.Count)
                {
                    Write-Warning -Message "$GroupName contained no matching computers.";
                }
                else
                {
                    # Now that we have our removal list, call Active Directory to perform the actual removal.
                    $Group | Remove-ADGroupMember -Members ($RemovalList.ToArray()) -ErrorAction:Stop -Confirm:$false;
        
                    # If the above Remove-ADGroupMember commandlet didn't error out, send the current group name and the list of members removed to the pipeline.
                    $RemovalList | ForEach-Object {
                        [PSCustomObject] @{
                            Group = $GroupName;
                            Member = $_;
                        }
                    };
                }
            }
        }

         

        The main difference from the first version is we have another nested loop:

         

        • New: Outer-most loop which iterates through each group;
        • No change: Inner loop that iterates through the group's membership list;
        • No change: Inner-most loop that iterates through the computer name regex patterns looking for a match.

         

        Otherwise, things are pretty much the same.

         

        Some quality-of-life additions are:

         

        • Lines 21 to 24: A check for if no matching group was found;
        • Lines 41 to 44: A check for if the group has no matching computers, meaning we shouldn't bother calling Remove-ADGroupMember on line 48;
        • Lines 51 to 56: Now that we're dealing with multiple groups, I've adding the current group name to the data the script is outputting to the default output stream.

         

        You'll notice for these checks I'm sending the output to the "warning" stream rather than the default output stream. That's because in PowerShell, you should not limit your thinking to writing output to the screen, but assume that the output from the script could be sent anywhere - even to another script that does something extra with the data.

         

        By sending the notifications to the warning channel, we avoid putting the real data and these informational messages in the same bucket, keeping our dataset clean and easily consumable.

         

        Cheers,

        Lain

Resources