community.general/lib/ansible/modules/windows/win_lineinfile.ps1
Dag Wieers 1ad55ec9de Consistent path attribute for file-related modules
Not all file-related modules consistently use "path" as the attribute to specify a single filename, some use "dest", others use "name". Most do have aliases for either "name" or "destfile".

This change makes "path" the default attribute for (single) file-related modules, but also adds "dest" and "name" as aliases, so that people can use a consistent way of attributing paths, but also to ensure backward compatibility with existing playbooks.

NOTE: The reason for changing this, is that it makes Ansible needlessly harder to use if you have to remember that e.g. the xattr module requires the name attribute, the lineinfile module requires a dest attribute, and the stat module requires a path attribute.
2017-01-13 15:49:42 -05:00

441 lines
12 KiB
PowerShell

#!powershell
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# WANT_JSON
# POWERSHELL_COMMON
# Parse the parameters file dropped by the Ansible machinery
$params = Parse-Args $args;
# Initialize defaults for input parameters.
$path= Get-Attr $params "path" $FALSE;
$regexp = Get-Attr $params "regexp" $FALSE;
$state = Get-Attr $params "state" "present";
$line = Get-Attr $params "line" $FALSE;
$backrefs = Get-Attr $params "backrefs" "no";
$insertafter = Get-Attr $params "insertafter" $FALSE;
$insertbefore = Get-Attr $params "insertbefore" $FALSE;
$create = Get-Attr $params "create" "no";
$backup = Get-Attr $params "backup" "no";
$validate = Get-Attr $params "validate" $FALSE;
$encoding = Get-Attr $params "encoding" "auto";
$newline = Get-Attr $params "newline" "windows";
# Parse path / dest / destfile / name param aliases for compatibility with lineinfile
# and fail if at least one spelling of the parameter is not provided.
If ($path -eq $FALSE) {
$path = Get-Attr $params "dest" $FALSE;
If ($path -eq $FALSE) {
$path = Get-Attr $params "destfile" $FALSE;
If ($path -eq $FALSE) {
$path = Get-Attr $params "name" $FALSE;
If ($path -eq $FALSE) {
Fail-Json (New-Object psobject) "missing required argument: path";
}
}
}
}
# Fail if the path is not a file
If (Test-Path $path -pathType container) {
Fail-Json (New-Object psobject) "Path $path is a directory";
}
# Write lines to a file using the specified line separator and encoding,
# performing validation if a validation command was specified.
function WriteLines($outlines, $path, $linesep, $encodingobj, $validate) {
$temppath = [System.IO.Path]::GetTempFileName();
$joined = $outlines -join $linesep;
[System.IO.File]::WriteAllText($temppath, $joined, $encodingobj);
If ($validate -ne $FALSE) {
If (!($validate -like "*%s*")) {
Fail-Json (New-Object psobject) "validate must contain %s: $validate";
}
$validate = $validate.Replace("%s", $temppath);
$parts = [System.Collections.ArrayList] $validate.Split(" ");
$cmdname = $parts[0];
$cmdargs = $validate.Substring($cmdname.Length + 1);
$process = [Diagnostics.Process]::Start($cmdname, $cmdargs);
$process.WaitForExit();
If ($process.ExitCode -ne 0) {
[string] $output = $process.StandardOutput.ReadToEnd();
[string] $error = $process.StandardError.ReadToEnd();
Remove-Item $temppath -force;
Fail-Json (New-Object psobject) "failed to validate $cmdname $cmdargs with error: $output $error";
}
}
# Commit changes to the path
$cleanpath = $path.Replace("/", "\");
Copy-Item $temppath $cleanpath -force;
Remove-Item $temppath -force;
}
# Backup the file specified with a date/time filename
function BackupFile($path) {
$backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss");
Copy-Item $path $backuppath;
return $backuppath;
}
# Implement the functionality for state == 'present'
function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep) {
# Note that we have to clean up the path because ansible wants to treat / and \ as
# interchangeable in windows pathnames, but .NET framework internals do not support that.
$cleanpath = $path.Replace("/", "\");
# Check if path exists. If it does not exist, either create it if create == "yes"
# was specified or fail with a reasonable error message.
If (!(Test-Path $path)) {
If ($create -eq "no") {
Fail-Json (New-Object psobject) "Path $path does not exist !";
}
# Create new empty file, using the specified encoding to write correct BOM
[System.IO.File]::WriteAllLines($cleanpath, "", $encodingobj);
}
# Read the dest file lines using the indicated encoding into a mutable ArrayList.
$content = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj);
If ($content -eq $null) {
$lines = New-Object System.Collections.ArrayList;
}
Else {
$lines = [System.Collections.ArrayList] $content;
}
# Compile the regex specified, if provided
$mre = $FALSE;
If ($regexp -ne $FALSE) {
$mre = New-Object Regex $regexp, 'Compiled';
}
# Compile the regex for insertafter or insertbefore, if provided
$insre = $FALSE;
If ($insertafter -ne $FALSE -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") {
$insre = New-Object Regex $insertafter, 'Compiled';
}
ElseIf ($insertbefore -ne $FALSE -and $insertbefore -ne "BOF") {
$insre = New-Object Regex $insertbefore, 'Compiled';
}
# index[0] is the line num where regexp has been found
# index[1] is the line num where insertafter/inserbefore has been found
$index = -1, -1;
$lineno = 0;
# The latest match object and matched line
$matched_line = "";
$m = $FALSE;
# Iterate through the lines in the file looking for matches
Foreach ($cur_line in $lines) {
If ($regexp -ne $FALSE) {
$m = $mre.Match($cur_line);
$match_found = $m.Success;
If ($match_found) {
$matched_line = $cur_line;
}
}
Else {
$match_found = $line -ceq $cur_line;
}
If ($match_found) {
$index[0] = $lineno;
}
ElseIf ($insre -ne $FALSE -and $insre.Match($cur_line).Success) {
If ($insertafter -ne $FALSE) {
$index[1] = $lineno + 1;
}
If ($insertbefore -ne $FALSE) {
$index[1] = $lineno;
}
}
$lineno = $lineno + 1;
}
$changed = $FALSE;
$msg = "";
If ($index[0] -ne -1) {
If ($backrefs -ne "no") {
$new_line = [regex]::Replace($matched_line, $regexp, $line);
}
Else {
$new_line = $line;
}
If ($lines[$index[0]] -cne $new_line) {
$lines[$index[0]] = $new_line;
$msg = "line replaced";
$changed = $TRUE;
}
}
ElseIf ($backrefs -ne "no") {
# No matches - no-op
}
ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") {
$lines.Insert(0, $line);
$msg = "line added";
$changed = $TRUE;
}
ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) {
$lines.Add($line);
$msg = "line added";
$changed = $TRUE;
}
Else {
$lines.Insert($index[1], $line);
$msg = "line added";
$changed = $TRUE;
}
# Write backup file if backup == "yes"
$backuppath = "";
If ($changed -eq $TRUE -and $backup -eq "yes") {
$backuppath = BackupFile $path;
}
# Write changes to the path if changes were made
If ($changed) {
WriteLines $lines $path $linesep $encodingobj $validate;
}
$encodingstr = $encodingobj.WebName;
# Return result information
$result = New-Object psobject @{
changed = $changed
msg = $msg
backup = $backuppath
encoding = $encodingstr
}
Exit-Json $result;
}
# Implement the functionality for state == 'absent'
function Absent($path, $regexp, $line, $backup, $validate, $encodingobj, $linesep) {
# Check if path exists. If it does not exist, fail with a reasonable error message.
If (!(Test-Path $path)) {
Fail-Json (New-Object psobject) "Path $path does not exist !";
}
# Read the dest file lines using the indicated encoding into a mutable ArrayList. Note
# that we have to clean up the path because ansible wants to treat / and \ as
# interchangeable in windows pathnames, but .NET framework internals do not support that.
$cleanpath = $path.Replace("/", "\");
$content = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj);
If ($content -eq $null) {
$lines = New-Object System.Collections.ArrayList;
}
Else {
$lines = [System.Collections.ArrayList] $content;
}
# Initialize message to be returned on success
$msg = "";
# Compile the regex specified, if provided
$cre = $FALSE;
If ($regexp -ne $FALSE) {
$cre = New-Object Regex $regexp, 'Compiled';
}
$found = New-Object System.Collections.ArrayList;
$left = New-Object System.Collections.ArrayList;
$changed = $FALSE;
Foreach ($cur_line in $lines) {
If ($cre -ne $FALSE) {
$m = $cre.Match($cur_line);
$match_found = $m.Success;
}
Else {
$match_found = $line -ceq $cur_line;
}
If ($match_found) {
$found.Add($cur_line);
$changed = $TRUE;
}
Else {
$left.Add($cur_line);
}
}
# Write backup file if backup == "yes"
$backuppath = "";
If ($changed -eq $TRUE -and $backup -eq "yes") {
$backuppath = BackupFile $path;
}
# Write changes to the path if changes were made
If ($changed) {
WriteLines $left $path $linesep $encodingobj $validate;
}
# Return result information
$fcount = $found.Count;
$msg = "$fcount line(s) removed";
$encodingstr = $encodingobj.WebName;
$result = New-Object psobject @{
changed = $changed
msg = $msg
backup = $backuppath
found = $fcount
encoding = $encodingstr
}
Exit-Json $result;
}
# Default to windows line separator - probably most common
$linesep = "`r`n";
If ($newline -ne "windows") {
$linesep = "`n";
}
# Fix any CR/LF literals in the line argument. PS will not recognize either backslash
# or backtick literals in the incoming string argument without this bit of black magic.
If ($line -ne $FALSE) {
$line = $line.Replace("\r", "`r");
$line = $line.Replace("\n", "`n");
}
# Figure out the proper encoding to use for reading / writing the target file.
# The default encoding is UTF-8 without BOM
$encodingobj = [System.Text.UTF8Encoding] $FALSE;
# If an explicit encoding is specified, use that instead
If ($encoding -ne "auto") {
$encodingobj = [System.Text.Encoding]::GetEncoding($encoding);
}
# Otherwise see if we can determine the current encoding of the target file.
# If the file doesn't exist yet (create == 'yes') we use the default or
# explicitly specified encoding set above.
Elseif (Test-Path $path) {
# Get a sorted list of encodings with preambles, longest first
$max_preamble_len = 0;
$sortedlist = New-Object System.Collections.SortedList;
Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) {
$encoding = $encodinginfo.GetEncoding();
$plen = $encoding.GetPreamble().Length;
If ($plen -gt $max_preamble_len) {
$max_preamble_len = $plen;
}
If ($plen -gt 0) {
$sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding);
}
}
# Get the first N bytes from the file, where N is the max preamble length we saw
[Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -Path $path;
# Iterate through the sorted encodings, looking for a full match.
$found = $FALSE;
Foreach ($encoding in $sortedlist.GetValueList()) {
$preamble = $encoding.GetPreamble();
If ($preamble -and $bom) {
Foreach ($i in 0..($preamble.Length - 1)) {
If ($i -ge $bom.Length) {
break;
}
If ($preamble[$i] -ne $bom[$i]) {
break;
}
Elseif ($i + 1 -eq $preamble.Length) {
$encodingobj = $encoding;
$found = $TRUE;
}
}
If ($found) {
break;
}
}
}
}
# Main dispatch - based on the value of 'state', perform argument validation and
# call the appropriate handler function.
If ($state -eq "present") {
If ( $backrefs -ne "no" -and $regexp -eq $FALSE ) {
Fail-Json (New-Object psobject) "regexp= is required with backrefs=true";
}
If ($line -eq $FALSE) {
Fail-Json (New-Object psobject) "line= is required with state=present";
}
If ($insertbefore -eq $FALSE -and $insertafter -eq $FALSE) {
$insertafter = "EOF";
}
Present $path $regexp $line $insertafter $insertbefore $create $backup $backrefs $validate $encodingobj $linesep;
}
Else {
If ($regexp -eq $FALSE -and $line -eq $FALSE) {
Fail-Json (New-Object psobject) "one of line= or regexp= is required with state=absent";
}
Absent $path $regexp $line $backup $validate $encodingobj $linesep;
}