PowerShell: часто задаваемые вопросы. Продолжение::Журнал СА 4.2008
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Подписка
Архив номеров
Где купить
Наука и технологии
Авторам
Рекламодателям
Контакты
   

  Опросы
  Статьи

День сисадмина  

Учите матчасть! Или как стать системным администратором

Лето – время не только отпусков, но и хорошая возможность определиться с профессией

 Читать далее...

День сисадмина  

Живой айтишник – это всегда движение. Остановка смерти подобна

Наши авторы рассказывают о своем опыте и дают советы начинающим системным администраторам.

 Читать далее...

Виртуализация  

Рынок решений для виртуализации

По данным «Обзора российского рынка инфраструктурного ПО и перспектив его развития», сделанного

 Читать далее...

Книжная полка  

Как стать креативным и востребованным

Издательский дом «Питер» предлагает новинки компьютерной литературы, а также книги по бизнесу

 Читать далее...

Книжная полка  

От создания сайтов до разработки и реализации API

В издательстве «БХВ» недавно вышли книги, которые будут интересны системным администраторам, создателям

 Читать далее...

Разбор полетов  

Ошибок опыт трудный

Как часто мы легко повторяем, что не надо бояться совершать ошибки, мол,

 Читать далее...

Принципы проектирования  

Dependency Inversion Principle. Принцип инверсии зависимостей в разработке

Мы подошли к последнему принципу проектирования приложений из серии SOLID – Dependency

 Читать далее...

Рынок труда  

Вакансия: Администратор 1С

Администратор 1С – это специалист, который необходим любой организации, где установлены программы

 Читать далее...

Книжная полка  

Книги для профессионалов, студентов и пользователей

Книги издательства «БХВ» вышли книги для тех, кто хочет овладеть самыми востребованными

 Читать далее...

Принципы проектирования  

Interface Segregation Principle. Принцип разделения интерфейсов в проектировании приложений

Эта статья из серии «SOLID» посвящена четвертому принципу проектирования приложений – Interface

 Читать далее...

1001 и 1 книга  
19.03.2018г.
Просмотров: 10696
Комментарии: 0
Потоковая обработка данных

 Читать далее...

19.03.2018г.
Просмотров: 8944
Комментарии: 0
Релевантный поиск с использованием Elasticsearch и Solr

 Читать далее...

19.03.2018г.
Просмотров: 8979
Комментарии: 0
Конкурентное программирование на SCALA

 Читать далее...

19.03.2018г.
Просмотров: 5666
Комментарии: 0
Машинное обучение с использованием библиотеки Н2О

 Читать далее...

12.03.2018г.
Просмотров: 6358
Комментарии: 0
Особенности киберпреступлений в России: инструменты нападения и защита информации

 Читать далее...

12.03.2018г.
Просмотров: 3663
Комментарии: 0
Глубокое обучение с точки зрения практика

 Читать далее...

12.03.2018г.
Просмотров: 2655
Комментарии: 0
Изучаем pandas

 Читать далее...

12.03.2018г.
Просмотров: 3465
Комментарии: 0
Программирование на языке Rust (Цветное издание)

 Читать далее...

19.12.2017г.
Просмотров: 3458
Комментарии: 0
Глубокое обучение

 Читать далее...

19.12.2017г.
Просмотров: 5949
Комментарии: 0
Анализ социальных медиа на Python

 Читать далее...

Друзья сайта  

 PowerShell: часто задаваемые вопросы. Продолжение

Архив номеров / 2008 / Выпуск №4 (65) / PowerShell: часто задаваемые вопросы. Продолжение

Рубрика: Программирование /  Программирование

Василий Гусев

PowerShell: часто задаваемые вопросы
Продолжение

В своей предыдущей статье [1] я уже ответил на многие популярные вопросы о PowerShell и о некоторых моментах работы с ним. Но, конечно, в рамках одной статьи сложно рассказать обо всём, поэтому продолжим.

Как вывести в строке какие-либо переменные или свойства объекта?

Думаю, все знают, как в PowerShell вывести строку на экран:

PS> "Hello world!"

Hello world!

И как объединить несколько строк или переменных в одну строку тоже:

PS> $w = "World"

PS> "Hello " + $w + "!"

Hello world!

Но все это можно делать куда более эффективно и удобно. Для того чтобы вставить значение переменной в строку, её достаточно поместить внутрь этой строки:

PS> "Hello $w!"

Hello world!

Логично, не правда ли? Но зачастую в строку надо вставить не простую переменную, а свойство какого-либо объекта, и тут возникает проблема:

PS> $file = Get-Item C:\test.zip

PS> "Размер файла $file составляет $file.Length байт"

Размер файла C:\test.zip составляет C:\test.zip.Length байт

Здесь я поместил в переменную $file объект, представляющий файл test.ps1, и затем пару раз упомянул его в строке. В первом вхождении переменная $file была преобразована в полный путь к файлу (всё равно как если бы мы выполнили метод $file.toString()). А во втором случае… Произошло то же самое! PowerShell посчитал, что .Length – это часть строки, не имеющая никакого отношения к переменной $file. Его тоже можно понять – вдруг пользователь пропустил пробел между предложениями? Но что же делать, если нам всё-таки надо поместить значение свойства в строку, а использовать временные переменные или использовать конкатенацию с помощью кучи плюсов не хочется? В таком случае нужно использовать конструкцию $(). Её можно вставить в строку, а между скобок поместить любое выражение. Это выражение будет выполнено, и его результат будет подставлен в строчку:

PS> "Размер файла $file составляет $($file.Length) байт"

Размер файла C:\test.zip составляет 1364964 байт

Неплохо? Но это еще не всё. Я не зря сказал, что внутрь $() можно поместить любое выражение, это действительно так:

PS> "Размер: $([Math]::Round($file.Length / 1kb)) килобайт"

Размер: 1333 килобайт

Тут я поделил размер файла в байтах на встроенную константу 1kb, и затем, воспользовавшись методом [Math]::Round() из .Net Framework, округлил полученный результат до целых. Есть и другой способ – воспользоваться оператором форматирования -f.

PS> "Размер: {0:n3} мегабайт" -f ($file.Length / 1mb)

Размер: 1,302 мегабайт

Те, кто знаком с программированием на языке C#, наверняка обрадуются знакомому синтаксису (http://msdn2.microsoft.com/en-us/library/fbxft59x.aspx). Для остальных же поясню:

Конструкция {0:n3} состоит из нескольких частей, первая из них – «0», это индекс элемента во втором операнде. В данном случае он один, но можно указать и несколько элементов, и при расстановке их внутри строки будет использоваться их порядковый номер, начиная с 0 у первого. Вторая часть конструкции – «n», указывает на то, что значение необходимо отформатировать как число (number), ну а следующая за нею цифра (в данном случае «3») определяет количество знаков после запятой.

Естественно, возможности оператора форматирования -f не ограничиваются обрезкой лишних знаков после запятой, к примеру, он обладает огромными возможностями форматирования дат. В следующем примере с помощью -f я получу путь к файлу, составленный из текущего каталога и сегодняшней даты:

PS> "{0}\{1:yyyy-MM-dd}.bak" -f $pwd, (Get-Date)

C:\backups\2008-04-12.bak

А какие в PowerShell маскирующие символы?

К счастью, этим маскирующим символом не является обратный слеш «\», как во многих языках программирования. Если бы так было в PowerShell, мы бы замучились набирать пути файловой системы, повторяя каждый слеш дважды.

В PowerShell роль маскирующего символа выполняет «`» – апостроф, символ, расположенный на большинстве клавиатур на клавише «Ё», под тильдой. С его помощью можно маскировать любые символы:

PS> "`$pwd = $pwd"

$pwd = C:\root

Здесь я замаскировал символ «$» в первом упоминании переменной, и она не была преобразована в значение. Еще можно использовать маскирующий символ для обозначения специальных символов. Так, например, «`n» будет означать переход на следующую строку:

PS> "Первая строка`nВторая строка"

Первая строка

Вторая строка

Вот некоторые из часто употребляемых специальных символов:

  • `n – новая строка.
  • `a (alert) – этот символ заставляет спикер компьютера издавать писк. Бывает полезно для того, чтобы привлечь внимание пользователя.
  • `t – символ табуляции.

В чем отличия между разными типами кавычек в PowerShell?

Начнем с самых простых и популярных – двойных кавычек. В PowerShell, как и во множестве других языков, они служат для ограничения и обозначения строк. Все знают, что если набрать в командной строке PowerShell текст в кавычках, то он будет выведен на экран. Ну и, конечно, можно присвоить это текстовое значение переменной. Но, кроме того, точно так же можно работать и с многострочными текстами. Если, не закрыв кавычек, нажать <Enter>, то командная строка PowerShell переведет курсор на новую строку, и продолжит ожидание ввода. Так будет продолжаться, пока вы не закроете кавычки:

PS> $hw = "Hello

>> World!"

>>

PS>

Символы «>>» тут лишь означают, что ввод продолжается, в саму переменную они помещены не будут. Когда вы захотите использовать такую конструкцию в скрипте, просто делайте переносы строки:

$SqlCommand = "BACKUP DATABASE [$Base]

TO DISK = '$Path'

WITH INIT"

Перейдем ко второму типу кавычек, к одинарным. Их основное отличие от двойных – это то, что, если поместить внутри них название переменной, оно не будет преобразовано в её значение. Это хорошо видно на следующем примере:

PS> $var = '$pwd = ' + "'$pwd'"

PS> $var

$pwd = 'c:\root'

 Имя переменной внутри одинарных кавычек осталось неизменным, а внутри двойных кавычек было подставлено значение переменной вместо её имени. А еще в этом примере видно, что один тип кавычек можно без проблем использовать внутри других кавычек, не волнуясь о какой-либо маскировке. То есть если вам необходимо составить строчку, внутри которой множество одинарных кавычек (к примеру, фильтр для WMI), то удобнее будет заключить эту строку в двойные кавычки, и наоборот.

Второе отличие одинарных кавычек от двойных – это игнорирование символа маскировки – «`»:

PS> 'Первая строка`nВторая строка'

Первая строка`nВторая строка

Впрочем, при необходимости можно использовать символ одинарной кавычки внутри строки, нужно повторить его дважды:

PS> 'Одинарная кавычка '' среди собратьев'

Одинарная кавычка ' среди собратьев

Но что делать, если в нашей строке используется множество кавычек обоих видов, например, если нужно поместить в переменную кусок кода PowerShell или SQL? Для такого случая предусмотрена специальная разновидность кавычек, специально предназначенная для многострочных текстов (так же называемая HereString):

PS> $MyCode = @'

>> $Proc = Get-Process explorer

>> $Message = 'Переменная $Proc содержит сведения о процессе

>> например, в "$Proc.Path" содержится ' + "'$Path'."

>> '@

>>

PS> $MyCode

$Proc = Get-Process explorer

$Message = 'Переменная $Proc содержит сведения о процессе

например, в "$Proc.Path" содержится ' + "'$Path'."

Разумеется, есть и вариант HereString для двойных кавычек, в нём переменные преобразовываются в свои значения.

Как посчитать количество возвращенных командой объектов?

Очень часто встречающийся вопрос. Большинство командлетов в PowerShell в качестве результата возвращают несколько объектов, объединенных в массив. Ну и, разумеется, очень часто хочется посчитать количество этих результатов. Сделать это очень просто, достаточно приставить в конец конвейера командлет Measure-Object. Например, вот так можно посчитать количество журналов событий в системе:

PS> Get-EventLog -List | Measure-Object

Count : 11

Average :

Sum :

Maximum :

Minimum :

Property :

Думаю, многим интересно, что это за строки – Average, Sum и т. д. Дело в том, что возможности Measure-Object не ограничиваются подсчетом количества элементов (хотя по умолчанию делает только это). Он может производить и некоторые другие вычисления, причем не только над самими объектами, но и над их свойствами:

PS> Get-Process | Measure-Object -Property WS -Sum -Average

Count : 77

Average : 13063952,6233766

Sum : 1005924352

Maximum :

Minimum :

Property : WS

Так мы получили данные об используемой памяти (WorkingSet) – среднее значение на процесс и сумму по всем процессам.

Но вернёмся к нашему вопросу. Кроме использования Measure-Object, есть и другой способ, зачастую более удобный. У всех массивов в PowerShell есть свойство .Count, в котором и содержится количество элементов массива. Вот пример, как его можно использовать:

PS> $Shares = Get-WmiObject Win32_Share

PS> $shares.Count

5

Конечно, можно обойтись и без временной переменной, достаточно заключить выражение в скобки:

PS> (Get-WmiObject Win32_Share).count

5

PS> (Get-Process | where {$_.path -like "c:\win*"}).count

46

Здорово выполнять команды интерактивно в консоли или запускать из неё скрипты. Но для многих административных задач необходимо запускать скрипты из планировщика заданий. Как это сделать?

Сначала еще раз напомню про необходимость разрешить в системе выполнение неподписанных скриптов. Хоть это и всем известный шаг, при переходе в производственную среду о нем многие забывают. Либо, если вы серьезно относитесь к безопасности, стоит подумать о том, чтобы подписывать скрипты PowerShell, выполняющиеся на серверах. Обо всём этом можно подробнее прочитать, выполнив команду:

PS> Get-Help About_Signing

Ну а чтобы вызвать скрипт из планировщика, надо лишь в качестве запускаемой программы указать PowerShell.exe (полный путь – C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe), а в качестве аргумента – путь к файлу скрипта.

Если выполнить PowerShell.exe с ключом «/?», то можно узнать и о других полезных аргументах. Я опишу лишь ключи, полезные для автоматизированного запуска скриптов:

  • -NoLogo – не будет выводиться приветственная строка;
  • -NoProfile – не загружать профили PowerShell;
  • -NonInteractive – не выдавать запросов пользователю, т.е. при вызове, например Read-Line, произойдет ошибка, и выполнение скрипта будет продолжено;
  • -Command – указывается код PowerShell, выполняемый при запуске. Кстати, тут может быть не просто скрипт, но и полноценная команда.

У командлетов зачастую весьма длинные названия аргументов. Но иногда они вообще не указываются. Как это работает?

Возьмем в качестве образца командлет Set-Content. Полный его синтаксис предполагает следующую конструкцию:

PS> Set-Content -Path test.txt -Value "123" -Verbose

Тут мы говорим командлету поместить значение «123» в файл test.txt. Но обязательно ли указывать названия параметров Path и Value?

Если выполнить команду:

PS> Get-Help Set-Content -Parameter Path

то можно увидеть, что у этого параметра свойство Position равно 1. Это же свойство у параметра Value того же командлета равно 2. Это означает, что если мы не будем указывать имен параметров, то PowerShell посчитает первый аргумент значением для Path, а второй для Value. То есть можно выполнять команду вот так:

PS> Set-Content test.txt "123"

Зачем же тогда вообще может понадобиться указывать имена параметров, если всё работает и без них? Ну, во-первых, скрипт с полными именами параметров будет куда читабельнее, чем без них. Те, кто хоть раз ломал голову над своим скриптом многолетней давности, оценят. А во-вторых, если мы указываем названия параметров, – нам не нужно помнить их порядок, он может быть любым:

PS> Set-Content -Value "123" -Path test.txt

Но и в этом случае незачем писать полные имена параметров. Возьмем теперь команду с действительно длинными параметрами – Write-Host. У неё есть два параметра, позволяющие задать цвета выводимого текста и его фона: ForegroundColor и BackgroundColor. И свойство Position у этих параметров равно «Named» (в чем можно убедиться, выполнив команду:

PS> Get-Help Write-Host -Parameter ForegroundColor)

Named в данном случае означает, что для использования параметра необходимо указать его имя, вариант с помещением аргументов в правильном порядке не сработает. Но и полное имя указывать не обязательно, достаточно указать лишь первые несколько букв, чтобы PowerShell смог отличить имя параметра от остальных:

PS> Write-Host test -f red -b blue

test

Впрочем, чтобы было несколько понятнее, можно написать и так:

PS> Write-Host test -fore red -back blue

test

Но если мы, например, попробуем найти все команды, работающие с процессами, используя Get-Command, и вместо параметра -Noun укажем -n, то нас ждет сообщение об ошибке:

PS> Get-Command -n process

Get-Command : Не удается обработать параметр, так как имя параметра "n"

неоднозначно. Возможные совпадения: -Name -Noun.

В строка:1 знак:12

+ Get-Command <<<< -n process

Дело в том, что в этом случае параметры отличаются со второй буквы, и для того чтобы PowerShell смог разобраться, какой параметр подразумевается, придется указывать на одну букву больше. В данном случае будет достаточно использовать -no.

Как получить значения параметров ключа реестра или, наоборот, задать их?

Несмотря на то что работа с реестром из PowerShell кажется очень простой, всё же существуют некоторые тонкости. Думаю, всем уже известно, что в PowerShell используется система так называемых «поставщиков» (provider), позволяющих работать с иерархическими системами хранения данных, как с обычной файловой системой (и даже автодополнение с помощью клавиши табуляции там тоже работает). И реестр как раз представлен в виде такого провайдера. Это дает возможность использовать для навигации по нему всё те же команды, как и для файловой системы: dir (Get-ChildItem), cd (Set-Location) или pwd (Get-Location). Но только этих команд для работы с реестром будет недостаточно.

Например, если нам понадобится посмотреть список автоматически запускаемых программ из HKCU:\Software\Microsoft\Windows\CurrentVersion\run, то одним Dir не обойтись. Дело в том, что параметры ключа не являются дочерними элементами по отношению к ключу. Они представляют собой его свойства, и для того чтобы получить их список, придётся использовать команду Get-ItemProperty (или её псевдоним – «gp»):

PS> gp HKCU:\Software\Microsoft\Windows\CurrentVersion\Run

Sidebar : C:\Program Files\Windows Sidebar\sidebar.exe /autoRun

MsnMsgr : "C:\Program Files\Windows Live\Messenger\MsnMsgr.Exe" /background

FolderShare : "C:\Program Files\FolderShare\FolderShare.exe" /background

Skype : "C:\Program Files\Skype\Phone\Skype.exe" /nosplash /minimized

WMPNSCFG : C:\Program Files\Windows Media Player\WMPNSCFG.exe

 PowerShell попытается самостоятельно подобрать наилучший метод форматирования, основываясь на количестве свойств объекта. Так, если у вас в этом ключе реестра менее 5 параметров, то они будут выведены в виде таблицы с параметрами в роли столбцов. Чтобы этого избежать, следует перенаправить вывод в командлет Format-List, используя конвейер. Для создания параметра используется командлет New-ItemProperty или Set-ItemProperty (псевдоним – «sp»):

PS> cd HKCU:\Software\Microsoft\Windows\CurrentVersion\Run

PS> sp -Path . -Name "Моя утилита" -Value "c:\ myutil.exe"

Тут я сначала установил в качестве текущего каталога ключ реестра, использовав cd (Set-Location). Затем с помощью командлета Set-ItemProperty создал параметр «Моя утилита» со значением «c:\myutil.exe» в текущем ключе (точка в качестве пути обозначает текущий каталог). Теперь можно проверить результат, снова использовав Get-ItemProperty, но на этот раз, указав ему конкретное свойство:

PS> gp . "Моя утилита" | Format-List

Моя утилита : c:\utils\myutil.exe

Ну и для завершения примера удалим этот созданный ключ. Не сложно догадаться, что для этого понадобится командлет «Remove-ItemProperty (“rp”)»:

PS> rp . "Моя утилита"

Как импортировать данные из Excel или, наоборот, поместить данные из PowerShell в Excel?

К сожалению, с PowerShell не поставляются командлеты для непосредственного импорта и экспорта файлов XLS. Но выход есть, и даже не один. Можно использовать для обмена данными с Excel файлы с разделителями запятыми – csv (Comma Separated File). По умолчанию этот формат даже открывается с помощью Excel и, разумеется, он может в него сохранять. Файлы CSV импортируются и экспортируются из PowerShell с помощью командлетов Import-Csv и Export-Csv соответственно.

Интересный момент – при импорте из csv-файла данные из первой его строки будут считаться заголовками, и в результате будут созданы объекты с такими же названиями свойств. Предположим, у нас есть файл следующего вида:

Имя,Отчество,Фамилия

Иван,Иванович,Иванов

Пётр,Петрович,Петров

Сидор,Сидорович,Сидоров

Импортировав этот файл, мы получим массив из трёх объектов, обладающих свойствами «Имя», «Отчество», и «Фамилия»:

PS> $fio = Import-Csv fio.csv

PS> $fio[0]

Имя Отчество Фамилия

--- -------- -------

Иван Иванович Иванов

PS> $fio | foreach {$_.Фамилия}

Иванов

Петров

Сидоров

Ну и, конечно, если очень хочется работать напрямую с файлами XLS и XLSX, то можно воспользоваться сторонними командлетами, например, бесплатной оснасткой PowerData, которую можно скачать по адресу http://www.ultimate-projects.ru. Кроме командлетов Import-Excel и Export-Excel, в комплект входит Invoke-SQL для выполнения SQL‑запросов и получения результатов в виде объектов PowerShell.

Каким образом в PowerShell можно перехватить ошибку?

Хорошие возможности в области обработки ошибок, несомненно, являются очень важным фактором для скриптового языка при применении в рабочем окружении. К счастью, у PowerShell в этом плане всё обстоит прекрасно. Есть и автоматические параметры для всех командлетов – ErrorAction и ErrorVariable, позволяющие определить поведение команды в случае ошибки и поместить объект ошибки в указанную переменную. Присутствуют специальные переменные – $ErrorActionPreference (глобально задает реакцию на ошибки) и $Error, содержащая массив последних произошедших ошибок (самая последняя $Error[0]). Но одним из самых полезных средств, конечно, является ключевое слово «trap». После этого слова задается скриптовый блок, который будет выполнен в случае ошибки. Кроме этого, в том же блоке можно обратиться к объекту ошибки (который внутри этого блока будет находится в переменной $_) и указать дальнейшие действия – break (прервать выполнение) или continue (продолжить выполнение скрипта дальше).

PS> trap {echo "Ошибка: $_"; break}; 1; 2/$null; 3

1

Ошибка: Попытка деления на нуль.

Попытка деления на нуль.

At line:1 char:39

+ trap {echo "Ошибка: $_"; break}; 1; 2/ <<<< $null; 3

Как видно из примера, после того как произошла ошибка деления на ноль, отработал код, указанный после ключевого слова trap, и затем выполнение скрипта было прервано. В случае же если указать continue, то после ошибки будут выполнены последующие команды:

PS> trap {echo "Ошибка: $_"; continue}; 1; 2/$null; 3

1

Ошибка: Попытка деления на нуль.

3

Trap очень удобен для применения в скриптах. Его можно поместить, к примеру, в начале файла, применив в скриптовом блоке командлет Export-Clixml для сохранения объекта ошибки в XML-файл:

trap {$_ | Export-Clixml Error.xml; stop}

Затем, при анализе причин проблемы, можно загрузить ошибку из этого файла в объект и детально разобраться в причинах:

PS> $Err = Import-Clixml Error.xml

PS> $Err

Copy-Item : Не найдено сетевое имя.

At line:1 char:10

+ copy-item <<<<  file.txt \\server\share

PS> $Err.InvocationInfo

MyCommand             : Copy-Item

CommandLineParameters : {Destination, Path}

ScriptLineNumber      : 1

OffsetInLine          : 10

ScriptName            :

Line                  : copy-item file.txt \\server\share

PositionMessage       :

                        At line:1 char:10

                        + copy-item <<<<  file.txt \\server\share

InvocationName        : copy-item

PipelineLength        : 1

PipelinePosition      : 1

ExpectingInput        : False

CommandOrigin         : Runspace

 Жду новых вопросов на адрес. Ну и, конечно, заходите на мой блог – http://xaegr.wordpress.com.

Помните! Никакой FAQ не заменит чтения документации, так что для использования всей мощи PowerShell, надо знать команду Get-Help, и ознакомиться с содержимым прилагающейся к PowerShell документации.

  1. Гусев В. PowerShell: часто задаваемые вопросы. //Системный администратор, №3, 2008 г. – С. 16-22.

Комментарии
 
  26.01.2018 - 10:37 |  Victor_VM

Здравствуйте, подскажите пожалуйста, есть такая задача, каждые 2 часа на экран пользователей в домене выводить текст.
Я написал на VBS скриптик, в гпо создал задачу и поместил в неё скрипт, но он не отрабатывает на ПК с Win 8 и Win 10, предполагаю, что из-за безопасности, можно написать такой же скрипт на PS? Как это сделать? Ниже прилагаю то, как я сделал на VBS.
MsgBox "Уважаемые коллеги, отдохните 10 минут." & vbCrLf & "В здоровом теле, здоровый дух!"

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru