sqlcmd :out not working as expected?

Posted on

Question :

Consider the following code:

DECLARE @db sysname;
DECLARE @filename varchar(260);
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT d.name
FROM sys.databases d;
OPEN cur;
FETCH NEXT FROM cur INTO @db
WHILE @@FETCH_STATUS = 0
BEGIN
    SET @filename = 'C:tempcreate_database_' + @db + '.txt';
    :setvar c @filename
    :out $(c)
    PRINT $(c);
    FETCH NEXT FROM cur INTO @db
END
CLOSE cur;
DEALLOCATE cur;

I’m expecting to get multiple files created in C:temp, one for each database, but nothing is created, and no errors are reported by SQL Server.

I’ve tried running this in SSMS using SQLCMD mode, and I’ve also tried running it from sqlcmd.exe

Answer :

The way to do this with sqlcmd mode is to script a script which scripts a script, then execute those scripts, so it’s a slightly different way of thinking about things, a bit like Inception or Borges, the dream within the dream:

-- Script a script which scripts a script
SET NOCOUNT ON
GO

:out d:temptemp.sql
GO

SELECT REPLACE( ':out d:temp@database.sql
-- Do some work here
PRINT ''select ''''@database''''''
GO

-- Return stdout to normal
:out STDOUT
GO
', '@database', name ) AS [--sql]
--C:tempcreate_database_' + name + '.txt'
FROM sys.databases 
GO


-- Return stdout to normal
:out STDOUT
GO

-- Now execute your scripted file
:r d:temptemp.sql
GO


-- Create another script which executes all thoses scripts ...
:out d:temptemp2.sql
GO

SELECT REPLACE( ':r d:temp@database.sql
GO
', '@database', name ) AS [--sql]
FROM sys.databases 
GO

-- Return stdout to normal
:out STDOUT
GO

-- Execute your script of scripts?
:r d:temptemp2.sql
GO

Using SQLCMD in this way was new to me and got me searching the Internet for examples.

After studying the information in SQL Server Management Studio: Hacking SQLCMD Mode and pondering the limitations of SQLCMD variables, I took a slightly different approach to your problem. My solution may not fully answer your question about why :OUT is not working as expected, but I’ll offer it up anyway.

My full script is at the end of my answer, but here are some fragments with explanations.


I started out by redirecting the output to my local user temp directory

set nocount on

--Redirect output to user temp folder
--On my computer, this was 'C:UsersuswshAppDataLocalTemp'
:OUT $(TEMP)DBScript.sql

Now, we invoke the cursor to read through the list of databases. I’m using the PRINT command to generate operating system echo commands to create the individual database create files. The output of the entire cursor execution is written to the DBScript.txt file in my user temp folder.

--Declare variables used by cursor
DECLARE @db sysname;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT d.name
FROM sys.databases d;
OPEN cur;
FETCH NEXT FROM cur INTO @db
WHILE @@FETCH_STATUS = 0
    BEGIN
        PRINT '!!echo nul > C:tempcreate_database_' + @db + '.txt';
        FETCH NEXT FROM cur INTO @db
    END
CLOSE cur;
DEALLOCATE cur;
go

Upon execution of the above, the DBScript.txt file now contains these commands (The double bang at the beginning of each command will tell SQLCMD to execute these as operating system commands):

!!echo nul > C:tempcreate_database_master.txt
!!echo nul > C:tempcreate_database_tempdb.txt
!!echo nul > C:tempcreate_database_model.txt
!!echo nul > C:tempcreate_database_msdb.txt
!!echo nul > C:tempcreate_database_Test.txt
!!echo nul > C:tempcreate_database_AdventureWorks2014.txt

Now, back to the original script, we revert the SQLCMD output to stdout and execute the commands in DBScript.txt

--Revert output to stdout
:OUT stdout

--Execute the script you just created that has the !!echo commands
:r $(TEMP)DBScript.sql
GO

When I look at my C:Temp directory, I now have the following individual files:

enter image description here

Here is the full script:

set nocount on

--Redirect output to user temp folder
--On my computer, this was 'C:UsersuswshAppDataLocalTemp'
:OUT $(TEMP)DBScript.sql

--Declare variables used by cursor
DECLARE @db sysname;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT d.name
FROM sys.databases d;
OPEN cur;
FETCH NEXT FROM cur INTO @db
WHILE @@FETCH_STATUS = 0
    BEGIN
        PRINT '!!echo nul > C:tempcreate_database_' + @db + '.txt';
        FETCH NEXT FROM cur INTO @db
    END
CLOSE cur;
DEALLOCATE cur;
go

--Revert output to stdout
:OUT stdout

--Execute the script you just created that has the !!echo commands
:r $(TEMP)DBScript.sql
GO

Ok, I think I get the answer from here

I’d like to quote it here

It’s not possible because sqlcmd vars and t-sql vars are evaluated
independently in different and disparate steps of the execution of a
sqlcmd script.

This is so true, in another word, your :setvar and :out are evaluated first before t-sql are run, and that also explains why my simple t-sql shown below gives the error message

DECLARE @filename varchar(260)='c:temptest.txt';

:setvar c @filename
:out $(c)
PRINT $(c);

enter image description here

You see, the error message has “unable to redirect output to @filename” and ‘c:windowssystem32@filename’, it is because, when :out is evaluated, it does not know @filename’s value, so it just use the literal string @filename for :out purpose, which will automatically default to the default path of “c:windowssystem32”

Leave a Reply

Your email address will not be published.