3
\$\begingroup\$

I'm new to writing PowerShell scripts, and this would be my first relatively large undertaking in the language. Any feedback regarding style and readability would be greatly appreciated. Particularly, I was unsure of the best way of iterating through the various strings, and was wondering if there was a more elegant way of doing so.

The code is also available through this gist, where a sample CSV is provided to test the program.

function ShowHelp() {
    echo "Insufficient arguments passed."
    echo ""
    echo "Usage: "
    echo "    .\morse.ps1 ""words"" [path\to\table.csv]"
    echo ""
    echo "``morse`` takes a series of words as input through the first command line"
    echo "parameter and causes the Caps Lock key to blink accordingly."
    echo ""
    echo "    words                   the words to display"
    echo "    [path\to\table.csv]     uses the referenced file as the morse table."
    echo "                            default: $DefaultCsvPath"
    echo ""
    echo "Examples:"
    echo "    .\morse.ps1 ""SOS send help"""
    echo "    .\morse.ps1 ""wot is this???"" .\punct.csv"
}

# Definitions:
#  - A `symbol` is one of $LongBlinkSymbol or $ShortBlinkSymbol
#  - A `sequence` is a series of concatenated symbols
#  - A `letter` is any character
#  - A `letter` is valid if and only if it occurs in MorseTable
#  - A `word` is a series of concatenated letters
#  - A `sentence` is a series of `words` joined by whitespace characters

$TimeUnit = 200 # in milliseconds
$ShortBlinkDuration = 1 * $TimeUnit
$LongBlinkDuration = 3 * $TimeUnit

$BetweenSymbolsDuration = 1 * $TimeUnit
$BetweenLettersDuration = 3 * $TimeUnit
$BetweenWordsDuration = 7 * $TimeUnit

$LongBlinkSymbol = '1'
$ShortBlinkSymbol = '0'

$DefaultCsvPath = ".\morse.csv"

# strings which match this regex are valid symbol strings
$ValidSymbolRegex = "^($(
    [Regex]::Escape($LongBlinkSymbol)
)|$(
    [Regex]::Escape($ShortBlinkSymbol)
))*$"

# error types
$EmptyTableError = 1
$MalformedHeaderError = 2
$InvalidSequenceError = 3
$InvalidCharacterError = 4
$NoArgumentsPassedError = 5

# WScript interface for sending keys
$wshell = New-Object -ComObject wscript.shell
function SendKeys([string] $keys) {
    $wshell.SendKeys($keys)
}

function CapsLock() {
    SendKeys("{CAPSLOCK}")
}

function IsValidSequence([string] $code) {
    $code -match $ValidSymbolRegex
}

# NOTE: MorseTable is indexed by characters, **not** singleton strings
$MorseTable = @{}

function LoadMorseTable([string] $pathToCsv) {
    $csv = Import-Csv "$pathToCsv"

    # ensure non-empty csv
    if($csv.length -eq 0) {
        Write-Error "Table read was empty"
        exit $EmptyTableError
    }

    # ensure proper column naming
    $head = $csv[0] # sample csv; we know the first element must exist
    if($head.Symbol -eq $null -or $head.Sequence -eq $null) {
        Write-Error "Malformed table header"
        exit $MalformedHeaderError
    }

    # update $MorseTable
    $csv | foreach {
        # ensure proper formatting of Sequence
        if(-not (IsValidSequence($_.Sequence))) {
            Write-Error "Entry '$($_.Symbol)' has an invalid sequence '$($_.Sequence)'"
            exit $InvalidSequenceError
        }

        $MorseTable[[char] $_.Symbol] = $_.Sequence
    }
}

# given sequence of $LongBlinkSymbol and $ShortBlinkSymbol, blinks the light
# for the appropriate duration
function BlinkSequence([string] $code) {
    for($i = 0; $i -lt $code.length; $i++) {
        $char = $code[$i]
        CapsLock
        if($char -eq $LongBlinkSymbol) {
            Sleep -m $LongBlinkDuration
        }
        else {
            Sleep -m $ShortBlinkDuration
        }
        CapsLock

        # do not wait after the last symbol
        if($i + 1 -ne $code.length) {
            Sleep -m $BetweenSymbolsDuration
        }
    }
}

function GetSequence([char] $char) {
    return $MorseTable[$char]
}

function BlinkWord([string] $word) {
    for($i = 0; $i -lt $word.length; $i++) {
        $char = $word[$i]
        $code = GetSequence($char)
        BlinkSequence($code)

        # do not wait after the last letter
        if($i + 1 -ne $word.length) {
            Sleep -m $BetweenLettersDuration
        }
    }
}

function BlinkSentence([string] $sentence) {
    $words = $sentence.ToUpper().split()

    # verify that no invalid symbols occur
    $letterset = $words | % { $_.ToCharArray() } | Sort-Object | Get-Unique

    $letterset | foreach {
        if($MorseTable[$_] -eq $null) {
            Write-Error "Input contains the invalid character '$_'"
            exit $InvalidCharacterError
        }
    }

    # all symbols must be valid

    for($i = 0; $i -lt $words.length; $i++) {
        $word = $words[$i]
        BlinkWord($word)

        # do not wait after the last word
        if($i + 1 -ne $words.length) {
            Sleep -m $BetweenWordsDuration
        }
    }
}

if($args.length -eq 0) {
    ShowHelp
    exit $NoArgumentsPassedError
}

$input = $args[0]
$pathToCsv = $DefaultCsvPath
if($args.length -ge 2) {
    $pathToCsv = $args[1]
}

LoadMorseTable $pathToCsv
BlinkSentence $input
\$\endgroup\$
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.